diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index babc9e4..b966fd7 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -1,7 +1,10 @@ package ru.soune.nocopy.controller; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.http.HttpHeaders; @@ -17,12 +20,15 @@ import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.RegAnswer; import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.exception.*; import ru.soune.nocopy.handler.*; import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.service.FileSimilarityService; +import ru.soune.nocopy.service.ImageHashService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; @@ -33,6 +39,7 @@ import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; @Slf4j @@ -50,6 +57,8 @@ public class ApiController { private final FileSimilarityService fileSimilarityService; + private final FileEntityRepository fileEntityRepository; + @PostMapping("/v{version}/data") public ResponseEntity handlePostRequest(@RequestBody BaseRequest request, @PathVariable("version") int version) { @@ -93,62 +102,72 @@ public class ApiController { @RequestParam(value = "chunk", required = false) MultipartFile chunk) { try { if (chunk == null || chunk.isEmpty()) { - return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), - "Chunk file null or empty", ChunkUploadResponse.builder() - .uploadId(uploadId) - .chunkNumber(chunkNumber) - .build())); + return buildErrorResponse(uploadId, chunkNumber, "Chunk file null or empty"); } - if (uploadId == null || uploadId.isBlank()) { - return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), - "Upload ID is required", ChunkUploadResponse.builder() - .uploadId(uploadId) - .chunkNumber(chunkNumber) - .build())); + return buildErrorResponse(uploadId, chunkNumber, "Upload ID is required"); } - if (chunkNumber == null || chunkNumber < 0) { - return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), - "Valid chunk number is required", ChunkUploadResponse.builder() - .uploadId(uploadId) - .chunkNumber(chunkNumber) - .build())); + return buildErrorResponse(uploadId, chunkNumber, "Valid chunk number is required"); } - fileUploadService.uploadChunk(uploadId, chunkNumber, chunk); + UploadProgressResponse uploadProgressResponse = fileUploadService.uploadChunk(uploadId, chunkNumber, chunk); - ChunkUploadResponse responseBody = ChunkUploadResponse.builder() - .uploadId(uploadId) - .chunkNumber(chunkNumber) - .chunkSize(chunk.getSize()) - .message("Chunk uploaded successfully") - .build(); + if (uploadProgressResponse.getStatus().equals(UploadStatus.COMPLETED)) { + ResponseEntity duplicateCheckResult = checkForDuplicates(uploadProgressResponse.getUploadId()); + if (duplicateCheckResult != null) { + return duplicateCheckResult; + } + } - return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.SUCCESS.getCode(), - "Chunk uploaded successfully", responseBody)); - } catch (DuplicateImageException e) { - log.error("Error uploading chunk", e); - ChunkUploadResponse responseBody = ChunkUploadResponse.builder() - .uploadId(uploadId) - .chunkNumber(chunkNumber) - .build(); + return buildSuccessResponse(uploadId, chunkNumber, chunk); - return ResponseEntity.ok().body(new BaseResponse( - 20004, MessageCode.DUPLICATE_FILE_UPLOAD.getCode(), - "Failed to upload chunk: " + e.getMessage(), responseBody)); } catch (Exception e) { log.error("Error uploading chunk", e); + return buildErrorResponse(uploadId, chunkNumber, "Failed to upload chunk: " + e.getMessage()); + } + } - ChunkUploadResponse responseBody = ChunkUploadResponse.builder() - .uploadId(uploadId) - .chunkNumber(chunkNumber) - .build(); + private ResponseEntity checkForDuplicates(String uploadId) throws IOException { + Optional uploadedFile = fileEntityRepository.findByUploadSessionId(uploadId); + + if (uploadedFile.isEmpty() || !uploadedFile.get().getMimeType().equals("image")) { + return null; + } + + FileEntity fileEntity = uploadedFile.get(); + List similarFiles = fileSimilarityService.findSimilarFiles(fileEntity.getId()); + + if (hasDuplicate(similarFiles)) { + return handleDuplicate(fileEntity, similarFiles); + } + + return null; + } + + private boolean hasDuplicate(List similarFiles) { + return similarFiles.stream().anyMatch(f -> f.getHammingDistance() <= 5); + } + + private ResponseEntity handleDuplicate(FileEntity fileEntity, List similarFiles) + throws IOException { + fileEntityService.deleteFromDisk(fileEntity); + + Optional originalFile = fileEntityRepository.findById(similarFiles.get(0).getFileId()); + + if (originalFile.isPresent()) { + Map duplicateInfo = Map.of( + "duplicate_file_id", originalFile.get().getId(), + "owner_user_id", String.valueOf(originalFile.get().getUserId())); return ResponseEntity.ok().body(new BaseResponse( - 20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), - "Failed to upload chunk: " + e.getMessage(), responseBody)); + 20004, + MessageCode.DUPLICATE_FILE_UPLOAD.getCode(), + "Failed to upload chunk, duplicate", + duplicateInfo)); } + + return null; } @GetMapping("/files/{fileId}/similar") @@ -179,7 +198,6 @@ public class ApiController { return ResponseEntity.ok().body(new BaseResponse( 20004, MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), responseBody)); - } catch (Exception e) { log.error("Error getting progress for upload: {}", uploadId, e); @@ -187,8 +205,7 @@ public class ApiController { .uploadId(uploadId) .build(); - return ResponseEntity.ok().body(new BaseResponse( - 20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), + return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), "Failed to get upload progress: " + e.getMessage(), responseBody)); } } @@ -347,6 +364,34 @@ public class ApiController { } } + private ResponseEntity buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk) { + ChunkUploadResponse responseBody = ChunkUploadResponse.builder() + .uploadId(uploadId) + .chunkNumber(chunkNumber) + .chunkSize(chunk.getSize()) + .message("Chunk uploaded successfully") + .build(); + + return ResponseEntity.ok().body(new BaseResponse( + 20000, + MessageCode.SUCCESS.getCode(), + "Chunk uploaded successfully", + responseBody)); + } + + private ResponseEntity buildErrorResponse(String uploadId, Integer chunkNumber, String errorMessage) { + ChunkUploadResponse responseBody = ChunkUploadResponse.builder() + .uploadId(uploadId) + .chunkNumber(chunkNumber) + .build(); + + return ResponseEntity.ok().body(new BaseResponse( + 20004, + MessageCode.FILE_UPLOAD_ERROR.getCode(), + errorMessage, + responseBody)); + } + private ResponseEntity createValidationErrorResponse(BindingResult bindingResult, Integer msgId) { List> fieldErrors = bindingResult.getFieldErrors() .stream() diff --git a/src/main/java/ru/soune/nocopy/handler/GlobalExceptionHandler.java b/src/main/java/ru/soune/nocopy/handler/GlobalExceptionHandler.java index 071b9ae..7303fb5 100644 --- a/src/main/java/ru/soune/nocopy/handler/GlobalExceptionHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/GlobalExceptionHandler.java @@ -5,15 +5,16 @@ import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.exception.*; import java.util.Map; @RestControllerAdvice @AllArgsConstructor +@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) @@ -81,4 +82,12 @@ public class GlobalExceptionHandler { "message" ,ex.getMessage() )); } + + @ExceptionHandler(DuplicateImageException.class) + @ResponseBody + public ResponseEntity handleDuplicateImage(DuplicateImageException e) { + return ResponseEntity.ok().body(new BaseResponse( + 20004, MessageCode.DUPLICATE_FILE_UPLOAD.getCode(), + "Duplicate image detected", e.getMessage())); + } } diff --git a/src/main/java/ru/soune/nocopy/repository/ImageHashRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageHashRepository.java index e1ff003..ee0aac2 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageHashRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageHashRepository.java @@ -6,4 +6,5 @@ import ru.soune.nocopy.entity.file.ImageHashEntity; @Repository public interface ImageHashRepository extends JpaRepository { + void deleteByFileId(String fileId); } diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index f72ac63..0a17c22 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -9,7 +9,6 @@ import ru.soune.nocopy.dto.file.FileResponse; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileUploadSession; -import ru.soune.nocopy.exception.DuplicateImageException; import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.service.FileSimilarityService; @@ -66,22 +65,6 @@ public class FileEntityService { if (saved.getMimeType().startsWith("image")) { imageHashService.create(saved, Path.of(saved.getFilePath())); - - var similarFiles = fileSimilarityService.findSimilarFiles(saved.getId()); - - boolean hasDuplicate = similarFiles.stream() - .anyMatch(f -> f.getHammingDistance() <= 5); - - if (hasDuplicate) { - log.warn("Duplicate image detected for upload session {}. Rolling back.", - session.getUploadId()); - throw new DuplicateImageException("Duplicate image detected for upload session " + session.getUploadId()); - } - - similarFiles.stream() - .filter(f -> f.getHammingDistance() > 5) - .forEach(f -> log.info("Similar image found: {} (distance {})", - f.getFileId(), f.getHammingDistance())); } return saved; @@ -166,6 +149,8 @@ public class FileEntityService { Files.delete(path); + markAsDeleted(fileEntity); + return true; }