package ru.soune.nocopy.controller; import com.vrt.NoCopyFileService; import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.NoCopyCheckResult; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.exception.*; import ru.soune.nocopy.handler.*; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.util.FileUtil; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; 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 @RestController @RequestMapping("/api") @RequiredArgsConstructor public class ApiController { private final FileUploadService fileUploadService; private final Map handlers; private final FileEntityService fileEntityService; private final AuthService authService; private final FileSimilarityService fileSimilarityService; private final FileEntityRepository fileEntityRepository; private final NoCopyFileService noCopyFileService; private final FileUtil fileUtil; @PostMapping("/v{version}/data") public ResponseEntity handlePostRequest(@RequestBody BaseRequest request, @PathVariable("version") int version) { Integer msgId = request.getMsgId(); BaseResponse response; try { RequestHandler handler = handlers.get(msgId); if (handler == null) { response = new BaseResponse(msgId, MessageCode.MSG_ID_NOT_FOUND.getCode(), MessageCode.MSG_ID_NOT_FOUND.getDescription(), new HashMap<>()); } else { response = handler.handle(request); } return ResponseEntity.ok().body(response); } catch (ValidationException e) { return createValidationErrorResponse(e.getBindingResult(), e.getMsgId()); } catch (UserNotFoundException e) { return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.USER_NOT_FOUND.getCode(), MessageCode.USER_NOT_FOUND.getDescription(), new HashMap<>())); } catch (NotFoundAuthToken e) { return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), new HashMap<>())); } catch (NotValidFieldException e) { throw e; } catch (Exception e) { log.error("Handler execution failed for msgId: {}", msgId, e); BaseResponse errorResponse = new BaseResponse(msgId, MessageCode.INVALID_JSON_BODY.getCode(), MessageCode.INVALID_JSON_BODY.getDescription(), new HashMap<>()); return ResponseEntity.ok().body(errorResponse); } } @PostMapping("/v{version}/files/chunk") public ResponseEntity uploadChunk( @PathVariable("version") int version, @RequestParam(value = "upload_id", required = false) String uploadId, @RequestParam(value = "chunk_number", required = false) Integer chunkNumber, @RequestParam(value = "chunk", required = false) MultipartFile chunk, @RequestParam(value = "findSimilar", required = false, defaultValue = "0") Integer findSimilar) { try { if (chunk == null || chunk.isEmpty()) { return buildErrorResponse(uploadId, chunkNumber, "Chunk file null or empty"); } if (uploadId == null || uploadId.isBlank()) { return buildErrorResponse(uploadId, chunkNumber, "Upload ID is required"); } if (chunkNumber == null || chunkNumber < 0) { return buildErrorResponse(uploadId, chunkNumber, "Valid chunk number is required"); } UploadProgressResponse uploadProgressResponse = fileUploadService.uploadChunk(uploadId, chunkNumber, chunk, findSimilar); return buildSuccessResponse(uploadId, chunkNumber, chunk, fileEntityService.findFileIdByPath(uploadProgressResponse.getFilePath())); } catch (DuplicateImageException e) { Map duplicateData = new HashMap<>(); duplicateData.put("duplicateFileId", e.duplicateFileId()); duplicateData.put("userId", e.userId()); duplicateData.put("message", e.getMessage()); if (uploadId != null) { duplicateData.put("uploadId", uploadId); } return ResponseEntity.ok().body(new BaseResponse( 20004, MessageCode.DUPLICATE_FILE_UPLOAD.getCode(), MessageCode.DUPLICATE_FILE_UPLOAD.getDescription(), duplicateData )); } catch (Exception e) { log.error("Error uploading chunk", e); return buildErrorResponse(uploadId, chunkNumber, "Failed to upload chunk: " + e.getMessage()); } } @GetMapping("/v{version}/files/{fileId}/similar") public ResponseEntity findSimilarFiles( @PathVariable("version") int version, @PathVariable String fileId, @RequestParam(value = "auth_token",required = false) String authToken, @RequestParam(required = false) List similarityLevels, @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) { SimilarityFilter filter = SimilarityFilter.builder() .similarityLevels(similarityLevels) .build(); Page similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable, authToken); String messageDesc; MessageCode success; if (similarFiles.isEmpty()) { messageDesc = MessageCode.FILE_NOT_FOUND.getDescription(); success = MessageCode.SUCCESS; } else { messageDesc = MessageCode.SIMILAR_FILES_FOUND.getDescription(); success = MessageCode.SUCCESS; } Map responseData = new HashMap<>(); responseData.put("content", similarFiles.getContent()); responseData.put("page", similarFiles.getNumber()); responseData.put("size", similarFiles.getSize()); responseData.put("totalElements", similarFiles.getTotalElements()); responseData.put("totalPages", similarFiles.getTotalPages()); responseData.put("hasNext", similarFiles.hasNext()); responseData.put("hasPrevious", similarFiles.hasPrevious()); return ResponseEntity.ok() .body(new BaseResponse(20004, success.getCode(), messageDesc, responseData)); } @GetMapping("/v{version}/files/progress/{uploadId}") public ResponseEntity getUploadProgress( @PathVariable("version") int version, @PathVariable String uploadId) { log.info("Getting progress for upload session: {}, version: {}", uploadId, version); try { var progress = fileUploadService.getUploadProgress(uploadId); UploadProgress responseBody = UploadProgress.builder() .uploadId(progress.getUploadId()) .fileName(progress.getFileName()) .totalChunks(progress.getTotalChunks()) .uploadedChunks(progress.getUploadedChunks()) .status(progress.getStatus().toString()) .progressPercentage(progress.getProgressPercentage()) .filePath(progress.getFilePath()) .remainingChunks(progress.getTotalChunks() - progress.getUploadedChunks()) .build(); 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); UploadProgress responseBody = UploadProgress.builder() .uploadId(uploadId) .build(); return ResponseEntity.ok().body(new BaseResponse( 20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), "Failed to get upload progress: " + e.getMessage(), responseBody)); } } @PostMapping("/v{version}/files/{uploadId}/complete") public ResponseEntity completeUpload( @PathVariable("version") int version, @PathVariable String uploadId) { log.info("Completing upload session: {}, version: {}", uploadId, version); try { var progress = fileUploadService.getUploadProgress(uploadId); if (progress.getStatus() == UploadStatus.COMPLETED) { CompleteUploadResponse responseBody = CompleteUploadResponse.builder() .uploadId(progress.getUploadId()) .status(progress.getStatus().toString()) .uploadedChunks(progress.getUploadedChunks()) .totalChunks(progress.getTotalChunks()) .message("Upload already completed") .filePath(progress.getFilePath()) .build(); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.SUCCESS.getCode(), "Upload already completed", responseBody)); } if (progress.getUploadedChunks() < progress.getTotalChunks()) { CompleteUploadResponse responseBody = CompleteUploadResponse.builder() .uploadId(progress.getUploadId()) .status(progress.getStatus().toString()) .uploadedChunks(progress.getUploadedChunks()) .totalChunks(progress.getTotalChunks()) .message("Not all chunks uploaded") .build(); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.INCOMPLETE_UPLOAD.getCode(), "Not all chunks uploaded", responseBody)); } CompleteUploadResponse responseBody = CompleteUploadResponse.builder() .uploadId(progress.getUploadId()) .status(progress.getStatus().toString()) .uploadedChunks(progress.getUploadedChunks()) .totalChunks(progress.getTotalChunks()) .message("File assembly in progress") .build(); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.SUCCESS.getCode(), "File assembly in progress", responseBody)); } catch (Exception e) { log.error("Error completing upload: {}", uploadId, e); CompleteUploadResponse responseBody = CompleteUploadResponse.builder() .uploadId(uploadId) .build(); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), "Failed to complete upload: " + e.getMessage(), responseBody)); } } @GetMapping("/v{version}/files/download/{fileId}") public ResponseEntity downloadFile( @PathVariable(required = false) String fileId, @PathVariable(required = false) Integer version, @RequestHeader(value = "Authorization", required = false) String tokenHeader) { try { if (tokenHeader == null) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); errorData.put("fileId", fileId); errorData.put("version", version); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getCode(), MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getDescription(), errorData)); } Long userId = authService.useUserAuthToken(tokenHeader); FileEntityResponse entityResponse = fileEntityService.getById(fileId, version); if (!entityResponse.getUserId().equals(userId)) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_DOWNLOAD_ERROR.getCode(), MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), errorData)); } if (entityResponse.getStatus().equals(FileStatus.DELETED)) { Map errorData = new HashMap<>(); errorData.put("file_status", entityResponse.getStatus()); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_DOWNLOAD_ERROR.getCode(), MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), errorData)); } if (!entityResponse.isExistsOnDisk()) { Map errorData = new HashMap<>(); errorData.put("onDisk", entityResponse.isExistsOnDisk()); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_DOWNLOAD_ERROR.getCode(), MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), errorData)); } Path filePath = Paths.get(entityResponse.getProtectedFilePath()); // Resource resource = new UrlResource(filePath.toUri()); FileSystemResource resource = new FileSystemResource(filePath); long fileSize = Files.size(filePath); if (!resource.exists()) { Map errorData = new HashMap<>(); errorData.put("resource", resource.exists()); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_DOWNLOAD_ERROR.getCode(), MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), errorData)); } String contentType = determineContentType(filePath); return ResponseEntity.ok() .contentLength(fileSize) .contentType(MediaType.parseMediaType(contentType)) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + entityResponse.getOriginalFileName() + "\"") .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(entityResponse.getFileSize())) .body(resource); } catch (FileEntityNotFoundException e) { Map errorData = new HashMap<>(); errorData.put("fileId", fileId); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), errorData)); } catch (NotFoundAuthToken | IOException e) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); errorData.put("fileId", fileId); errorData.put("version", version); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getCode(), MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getDescription(), errorData)); } } @GetMapping("/protect/{fileId}") public ResponseEntity protect( @PathVariable(required = false) String fileId) { Optional optionalFileEntity = fileEntityRepository.findById(fileId); if (!optionalFileEntity.isPresent()) { return ResponseEntity.notFound().build(); } FileEntity fileEntity = optionalFileEntity.get(); FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); noCopyFileService.addFile(fileInfo); fileEntity.setProtectionStatus(ProtectionStatus.PROCESSING); fileEntityRepository.save(fileEntity); return ResponseEntity.ok().build(); } @GetMapping("/check/{fileId}/{type}") public ResponseEntity check(@PathVariable(required = false) String fileId, @PathVariable(required = false) String type) { Optional optionalFileEntity = fileEntityRepository.findById(fileId); if (!optionalFileEntity.isPresent()) { return ResponseEntity.notFound().build(); } FileEntity fileEntity = optionalFileEntity.get(); Path path = Paths.get(fileEntity.getProtectedFilePath()); File file = path.toFile(); NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(file, FileProtector.Type.valueOf(type.toUpperCase())); return ResponseEntity.ok().body(noCopyCheckResult); } 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); fileEntityService.softDeleteFileWithHash(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.DUPLICATE_FILE_UPLOAD.getCode(), "Failed to upload chunk, duplicate", duplicateInfo)); } return null; } private ResponseEntity buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk, String fileId) { ChunkUploadResponse responseBody = ChunkUploadResponse.builder() .uploadId(uploadId) .chunkNumber(chunkNumber) .chunkSize(chunk.getSize()) .fileId(fileId) .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() .map(this::createErrorDetail) .collect(Collectors.toList()); RegAnswer regAnswer = new RegAnswer(); regAnswer.setFieldErrors(fieldErrors); return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), regAnswer)); } private Map createErrorDetail(FieldError fieldError) { Map errorDetail = new HashMap<>(); errorDetail.put("field", fieldError.getField()); errorDetail.put("code", fieldError.getCode() != null ? fieldError.getCode() : "VALIDATION_ERROR"); errorDetail.put("message", fieldError.getDefaultMessage()); if (fieldError.getRejectedValue() != null && !fieldError.getField().toLowerCase().contains("password")) { errorDetail.put("rejected_value", fieldError.getRejectedValue().toString()); } return errorDetail; } private String determineContentType(Path filePath) throws IOException { String contentType = Files.probeContentType(filePath); if (contentType == null) { contentType = "application/octet-stream"; } return contentType; } }