dev update search methods,update find endpoints
Test Workflow / test (push) Successful in 3s

This commit is contained in:
vladp
2026-01-26 21:03:45 +07:00
parent 9ba3d8b973
commit aa87ae32bc
11 changed files with 74 additions and 20 deletions
@@ -114,7 +114,8 @@ public class ApiController {
@PathVariable("version") int version, @PathVariable("version") int version,
@RequestParam(value = "upload_id", required = false) String uploadId, @RequestParam(value = "upload_id", required = false) String uploadId,
@RequestParam(value = "chunk_number", required = false) Integer chunkNumber, @RequestParam(value = "chunk_number", required = false) Integer chunkNumber,
@RequestParam(value = "chunk", required = false) MultipartFile chunk) { @RequestParam(value = "chunk", required = false) MultipartFile chunk,
@RequestParam(value = "findSimilar", required = false) Integer findSimilar) {
try { try {
if (chunk == null || chunk.isEmpty()) { if (chunk == null || chunk.isEmpty()) {
return buildErrorResponse(uploadId, chunkNumber, "Chunk file null or empty"); return buildErrorResponse(uploadId, chunkNumber, "Chunk file null or empty");
@@ -126,9 +127,11 @@ public class ApiController {
return buildErrorResponse(uploadId, chunkNumber, "Valid chunk number is required"); return buildErrorResponse(uploadId, chunkNumber, "Valid chunk number is required");
} }
fileUploadService.uploadChunk(uploadId, chunkNumber, chunk); UploadProgressResponse uploadProgressResponse = fileUploadService.uploadChunk(uploadId, chunkNumber,
chunk, findSimilar);
return buildSuccessResponse(uploadId, chunkNumber, chunk); return buildSuccessResponse(uploadId, chunkNumber, chunk,
fileEntityService.findFileIdByPath(uploadProgressResponse.getFilePath()));
} catch (DuplicateImageException e) { } catch (DuplicateImageException e) {
Map<String, Object> duplicateData = new HashMap<>(); Map<String, Object> duplicateData = new HashMap<>();
duplicateData.put("duplicateFileId", e.duplicateFileId()); duplicateData.put("duplicateFileId", e.duplicateFileId());
@@ -151,16 +154,17 @@ public class ApiController {
} }
} }
@GetMapping("/v{version}/files/{fileId}/similar") @GetMapping("/v{version}/files/{fileId}/similar/{authToken}")
public ResponseEntity<BaseResponse> findSimilarFiles( public ResponseEntity<BaseResponse> findSimilarFiles(
@PathVariable("version") int version, @PathVariable("version") int version,
@PathVariable String fileId, @PathVariable String fileId,
@PathVariable(required = false) String authToken,
@RequestParam(required = false) List<String> similarityLevels, @RequestParam(required = false) List<String> similarityLevels,
@PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) { @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) {
SimilarityFilter filter = SimilarityFilter.builder() SimilarityFilter filter = SimilarityFilter.builder()
.similarityLevels(similarityLevels) .similarityLevels(similarityLevels)
.build(); .build();
Page<SimilarFileDTO> similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable); Page<SimilarFileDTO> similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable, authToken);
String messageDesc; String messageDesc;
MessageCode success; MessageCode success;
@@ -459,11 +463,13 @@ public class ApiController {
return null; return null;
} }
private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk) { private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk,
String fileId) {
ChunkUploadResponse responseBody = ChunkUploadResponse.builder() ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
.uploadId(uploadId) .uploadId(uploadId)
.chunkNumber(chunkNumber) .chunkNumber(chunkNumber)
.chunkSize(chunk.getSize()) .chunkSize(chunk.getSize())
.fileId(fileId)
.message("Chunk uploaded successfully") .message("Chunk uploaded successfully")
.build(); .build();
@@ -20,6 +20,9 @@ public class ChunkUploadResponse {
@JsonProperty("chunk_size") @JsonProperty("chunk_size")
private Long chunkSize; private Long chunkSize;
@JsonProperty("file_id")
private String fileId;
@JsonProperty("message") @JsonProperty("message")
private String message; private String message;
} }
@@ -17,6 +17,9 @@ public class FileEntityRequest {
@JsonProperty("file_id") @JsonProperty("file_id")
private String fileId; private String fileId;
@JsonProperty("full_delete")
private Integer fullDelete;
@JsonProperty("upload_session_id") @JsonProperty("upload_session_id")
private String uploadSessionId; private String uploadSessionId;
@@ -273,7 +273,7 @@ public class FileEntityHandler implements RequestHandler {
FileEntity fileEntity = fileEntityRepository.findById(fileId) FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(fileId)); .orElseThrow(() -> new FileEntityNotFoundException(fileId));
if (fileEntity.getStatus().equals(FileStatus.DELETED)) { if (fileEntity.getStatus().equals(FileStatus.DELETED) || fileRequest.getFullDelete() == 1) {
fileEntityService.deleteFromDisk(fileEntity); fileEntityService.deleteFromDisk(fileEntity);
response = DeleteFileResponse.builder() response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId()) .fileId(fileRequest.getFileId())
@@ -1,6 +1,7 @@
package ru.soune.nocopy.repository; package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.AuthToken;
@@ -13,4 +14,9 @@ public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
List<AuthToken> findByExpiresAtBefore(LocalDateTime expiresAtBefore); List<AuthToken> findByExpiresAtBefore(LocalDateTime expiresAtBefore);
Optional<AuthToken> findByLastUsedAtBefore(LocalDateTime lastUsedAt); Optional<AuthToken> findByLastUsedAtBefore(LocalDateTime lastUsedAt);
Optional<AuthToken> findByToken(String token); Optional<AuthToken> findByToken(String token);
@Query(value = """
SELECT a.user_id FROM auth_tokens a WHERE a.token = :token
""",
nativeQuery = true)
Long findUserIdByToken(String token);
} }
@@ -46,5 +46,8 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
@Query("SELECT f FROM FileEntity f WHERE f.signature = :signature") @Query("SELECT f FROM FileEntity f WHERE f.signature = :signature")
FileEntity findBySignature(@Param("signature") String signature); FileEntity findBySignature(@Param("signature") String signature);
@Query("SELECT f.id FROM FileEntity f WHERE f.filePath = :filePath")
String findFileIdByFilePath(@Param("filePath") String filePath);
long countByUserId(Long userId); long countByUserId(Long userId);
} }
@@ -31,6 +31,28 @@ public interface ImageSimilarityRepository
@Param("fileId") String fileId @Param("fileId") String fileId
); );
@Query(value = """
SELECT
f.id AS id,
f.original_file_name AS originalFileName,
f.file_size AS fileSize,
f.user_id AS userId,
h.hash64_hi AS hash64Hi,
h.hash64_lo AS hash64Lo
FROM image_hashes ref
JOIN image_hashes h
ON ref.file_id <> h.file_id
JOIN file_entities f
ON f.id = h.file_id
WHERE ref.file_id = :fileId AND f.user_id = :userId
""",
nativeQuery = true)
List<SimilarImageProjection> findCandidatesFromUserFiles(
@Param("fileId") String fileId,
@Param("userId") Long userId
);
@Query(value = """ @Query(value = """
SELECT SELECT
h.hash64_hi AS hash64Hi, h.hash64_hi AS hash64Hi,
@@ -9,6 +9,7 @@ import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.file.SimilarFileDTO; import ru.soune.nocopy.dto.file.SimilarFileDTO;
import ru.soune.nocopy.dto.file.SimilarityFilter; import ru.soune.nocopy.dto.file.SimilarityFilter;
import ru.soune.nocopy.entity.file.ImageHashEntity; import ru.soune.nocopy.entity.file.ImageHashEntity;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.ImageHashRepository; import ru.soune.nocopy.repository.ImageHashRepository;
import ru.soune.nocopy.repository.ImageSimilarityRepository; import ru.soune.nocopy.repository.ImageSimilarityRepository;
import ru.soune.nocopy.repository.SimilarImageProjection; import ru.soune.nocopy.repository.SimilarImageProjection;
@@ -28,6 +29,8 @@ public class FileSimilarityService {
private final FileUtil fileUtil; private final FileUtil fileUtil;
private final AuthTokenRepository authTokenRepository;
public List<SimilarFileDTO> findSimilarFiles(String fileId) { public List<SimilarFileDTO> findSimilarFiles(String fileId) {
var imageHashEntity = hashRepository.findById(fileId) var imageHashEntity = hashRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("Hash not found")); .orElseThrow(() -> new RuntimeException("Hash not found"));
@@ -128,14 +131,15 @@ public class FileSimilarityService {
return duplicates; return duplicates;
} }
public Page<SimilarFileDTO> findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable) { public Page<SimilarFileDTO> findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, String authToken) {
var imageHashEntity = hashRepository.findById(fileId) var imageHashEntity = hashRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("Hash not found")); .orElseThrow(() -> new RuntimeException("Hash not found"));
Long hash64Hi = imageHashEntity.getHash64Hi(); Long hash64Hi = imageHashEntity.getHash64Hi();
Long hash64Lo = imageHashEntity.getHash64Lo(); Long hash64Lo = imageHashEntity.getHash64Lo();
List<SimilarImageProjection> candidates = repository.findCandidates(fileId); List<SimilarImageProjection> candidates = authToken.equals("all") ? repository.findCandidates(fileId):
repository.findCandidatesFromUserFiles(fileId, authTokenRepository.findUserIdByToken(authToken));
List<String> similarityLevels = (filter != null && filter.getSimilarityLevels() != null) List<String> similarityLevels = (filter != null && filter.getSimilarityLevels() != null)
? filter.getSimilarityLevels() ? filter.getSimilarityLevels()
@@ -202,7 +202,8 @@ public class FileEntityService {
Files.delete(path); Files.delete(path);
markAsDeleted(fileEntity); // markAsDeleted(fileEntity);
fileEntityRepository.delete(fileEntity);
return true; return true;
} }
@@ -248,6 +249,10 @@ public class FileEntityService {
return fileEntityRepository.findBySignature(signature); return fileEntityRepository.findBySignature(signature);
} }
public String findFileIdByPath(String filePath) {
return fileEntityRepository.findFileIdByFilePath(filePath);
}
public File getFileById(String id) { public File getFileById(String id) {
try { try {
FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() -> FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
@@ -14,7 +14,7 @@ public interface FileUploadService {
void cancelUpload(String uploadId); void cancelUpload(String uploadId);
void uploadChunk(String uploadId, Integer chunkNumber, MultipartFile chunkFile); UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, MultipartFile chunkFile, Integer findSimilar);
UploadProgressResponse getUploadProgress(String uploadId); UploadProgressResponse getUploadProgress(String uploadId);
@@ -177,8 +177,8 @@ public class FileUploadServiceImpl implements FileUploadService {
@Override @Override
@Transactional @Transactional
public void uploadChunk(String uploadId, Integer chunkNumber, public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber,
MultipartFile chunkFile) { MultipartFile chunkFile, Integer findSimilar) {
FileUploadSession session = sessionRepository.findById(uploadId) FileUploadSession session = sessionRepository.findById(uploadId)
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId)); .orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
@@ -204,7 +204,7 @@ public class FileUploadServiceImpl implements FileUploadService {
throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize); throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize);
} }
processChunk(session, chunkNumber, chunkFile); return processChunk(session, chunkNumber, chunkFile, findSimilar);
} }
@Override @Override
@@ -285,12 +285,13 @@ public class FileUploadServiceImpl implements FileUploadService {
} }
} }
private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile) { private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile,
Integer findSimilar) {
String chunkPath = null; String chunkPath = null;
try { try {
if (session.getChunkPaths().containsKey(chunkNumber)) { if (session.getChunkPaths().containsKey(chunkNumber)) {
return handleExistingChunk(session, chunkNumber, chunkFile); return handleExistingChunk(session, chunkNumber, chunkFile, findSimilar);
} }
chunkPath = saveChunkWithIntegrityCheck(session, chunkNumber, chunkFile); chunkPath = saveChunkWithIntegrityCheck(session, chunkNumber, chunkFile);
@@ -311,7 +312,7 @@ public class FileUploadServiceImpl implements FileUploadService {
String finalFilePath = assembleFileSynchronously(session); String finalFilePath = assembleFileSynchronously(session);
if (session.getFileType().startsWith("image")) { if (session.getFileType().startsWith("image") && findSimilar == 0) {
checkForDuplicatesSynchronously(finalFilePath); checkForDuplicatesSynchronously(finalFilePath);
} }
@@ -381,7 +382,8 @@ public class FileUploadServiceImpl implements FileUploadService {
private UploadProgressResponse handleExistingChunk(FileUploadSession session, private UploadProgressResponse handleExistingChunk(FileUploadSession session,
Integer chunkNumber, Integer chunkNumber,
MultipartFile chunkFile) throws IOException { MultipartFile chunkFile,
Integer findSimilar) throws IOException {
String existingPath = session.getChunkPaths().get(chunkNumber); String existingPath = session.getChunkPaths().get(chunkNumber);
Path chunkPath = Paths.get(existingPath); Path chunkPath = Paths.get(existingPath);
@@ -390,7 +392,7 @@ public class FileUploadServiceImpl implements FileUploadService {
session.setChunksUploaded(session.getChunksUploaded() - 1); session.setChunksUploaded(session.getChunksUploaded() - 1);
sessionRepository.save(session); sessionRepository.save(session);
return processChunk(session, chunkNumber, chunkFile); return processChunk(session, chunkNumber, chunkFile, findSimilar);
} }
long existingSize = Files.size(chunkPath); long existingSize = Files.size(chunkPath);
@@ -401,7 +403,7 @@ public class FileUploadServiceImpl implements FileUploadService {
session.setChunksUploaded(session.getChunksUploaded() - 1); session.setChunksUploaded(session.getChunksUploaded() - 1);
sessionRepository.save(session); sessionRepository.save(session);
return processChunk(session, chunkNumber, chunkFile); return processChunk(session, chunkNumber, chunkFile, findSimilar);
} }
return UploadProgressResponse.fromSession(session); return UploadProgressResponse.fromSession(session);