NCBACK-34 need fix delete filentity and hash
Test Workflow / test (push) Successful in 4s

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