package ru.soune.nocopy.controller; import com.vrt.NoCopyFileService; import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.NoCopyCheckResult; import com.vrt.fileprotection.audio.AudioCheckResult; import com.vrt.fileprotection.documents.DocumentCheckResult; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.company.Company; 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.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; 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.repository.UserRepository; import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.file.*; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.ProtectionsLimitService; import ru.soune.nocopy.service.file.cloud.CloudStorageService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.user.moderation.UserVerificationService; 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.*; 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; private final ProtectionsLimitService protectionsLimitService; private final AuthTokenRepository authTokenRepository; private final UserRepository userRepository; private final CheckCounterService checkCounterService; private final CloudStorageService cloudStorageService; private final ZipService zipService; private final UserVerificationService userVerificationService; @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 (FileFormatException e){ return buildErrorResponse(uploadId, chunkNumber, e.getMessage()); } catch (Exception e) { log.error("Error uploading chunk", e); return buildErrorResponse(uploadId, chunkNumber, "Failed to upload chunk: " + e.getMessage()); } } @PostMapping("/v{version}/private/files/chunk") public ResponseEntity uploadChunk( @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 = "last_file", required = false) Boolean last_file, @RequestParam(value = "token") String token) { 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"); } Long userId = authService.useUserAuthToken(token); UploadProgressResponse uploadProgressResponse = fileUploadService.uploadPassportChunk(uploadId, chunkNumber, chunk, userId); if (last_file) { zipService.createAndSplitZip(userId, fileEntityService.getAllUserFiles(userId, List.of(FileStatus.PRIVATE))); } return buildSuccessResponse(uploadId, chunkNumber, chunk, fileEntityService.findFileIdByPath(uploadProgressResponse.getFilePath())); }catch (FileFormatException e){ return buildErrorResponse(uploadId, chunkNumber, e.getMessage()); } catch (Exception e) { log.error("Error uploading chunk", e); return buildErrorResponse(uploadId, chunkNumber, "Failed to upload chunk: " + e.getMessage()); } } @GetMapping("/check/{fileId}") public ResponseEntity check(@PathVariable String fileId) { FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); File file = new File(fileEntity.getFilePath()); NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, FileProtector.Type.IMAGE); log.info("checkResult: {}", checkResult); return ResponseEntity.ok(new BaseResponse(1, 1, "1", checkResult)); } @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) { try { FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); if (fileEntity == null) { return ResponseEntity.ok().body(new BaseResponse(0, MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), null)); } BaseResponse response; if ("image".equals(fileEntity.getMimeType())) { Page similarFilesPage = fileSimilarityService.findSimilarFiles( fileId, similarityLevels, pageable, authToken); response = buildSuccessResponse(similarFilesPage, MessageCode.SIMILAR_FILES_FOUND); } else { List results = processNonImageFile(fileEntity, authToken); log.info("results: {}", results); response = buildSuccessResponse(results, MessageCode.SIMILAR_FILES_FOUND); checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); } return ResponseEntity.ok(response); } catch(IllegalArgumentException e) { log.error("Error with file extensions : {}", e.getMessage(), e); return ResponseEntity.ok().body(new BaseResponse(MessageCode.FILE_FOR_SEARCH_NOT_VALID.getCode(), MessageCode.FILE_FOR_SEARCH_NOT_VALID.getCode(), MessageCode.FILE_FOR_SEARCH_NOT_VALID.getDescription(), null)); } catch (Exception e) { log.error("Error finding similar files: {}", e.getMessage(), e); return ResponseEntity.ok().body(new BaseResponse(MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(), null)); } } private List processNonImageFile(FileEntity fileEntity, String authToken) throws Exception { List results = new ArrayList<>(); FileEntity duplicateByHash = fileSimilarityService.findDuplicateByHash(fileEntity.getFilePath(), fileEntity.getMimeType(), fileEntity.getUserId()); log.info("duplicateByHash: {}", duplicateByHash); if (duplicateByHash != null) { results.add(fileSimilarityService.buildDTO(duplicateByHash)); return results; } File file = new File(fileEntity.getFilePath()); FileProtector.Type type = "document".equals(fileEntity.getMimeType()) ? FileProtector.Type.DOC : FileProtector.Type.AUDIO; NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type); log.info("checkResult: {}", checkResult); switch (checkResult) { case DocumentCheckResult.Success success -> { FileProtector.FileInfo info = success.getInfo(); FileEntity entity = fileEntityRepository.findByFileId(info.getId()); if (entity != null) { results.add(fileSimilarityService.buildDTO(entity)); } } case AudioCheckResult.Success success -> { FileProtector.FileInfo info = success.getInfo(); FileEntity entity = fileEntityRepository.findByFileId(info.getId()); if (entity != null) { results.add(fileSimilarityService.buildDTO(entity)); } } case DocumentCheckResult.Failed failed -> log.warn("Document check failed: {}", failed.getMessage()); case AudioCheckResult.Failed failed -> log.warn("Audio check failed: {}", failed.getMessage()); default -> log.warn("Unexpected result type: {}", checkResult.getClass().getSimpleName()); } return results; } private BaseResponse buildSuccessResponse(Page page, MessageCode messageCode) { Map responseData = new HashMap<>(); responseData.put("content", page.getContent()); responseData.put("page", page.getNumber()); responseData.put("size", page.getSize()); responseData.put("totalElements", page.getTotalElements()); responseData.put("totalPages", page.getTotalPages()); responseData.put("hasNext", page.hasNext()); responseData.put("hasPrevious", page.hasPrevious()); return new BaseResponse(20004, messageCode.getCode(), messageCode.getDescription(), responseData); } private BaseResponse buildSuccessResponse(List list, MessageCode messageCode) { Map responseData = new HashMap<>(); responseData.put("content", list); responseData.put("totalElements", list.size()); return new BaseResponse(20004, messageCode.getCode(), messageCode.getDescription(), 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 || tokenHeader.isBlank()) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.TOKEN_IS_NULL.getCode(), MessageCode.TOKEN_IS_NULL.getDescription(), errorData)); } Long userId = authService.useUserAuthToken(tokenHeader); FileEntityResponse entityResponse = fileEntityService.getById(fileId, version); FileStatus status = entityResponse.getStatus(); if (status.equals(FileStatus.BLOCKED) || status.equals(FileStatus.DELETED) || status.equals(FileStatus.REMOVED)) { Map errorData = new HashMap<>(); errorData.put("fileId", entityResponse.getId()); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_IS_BLOCKED.getCode(), MessageCode.FILE_IS_BLOCKED.getDescription(), errorData)); } User fileUser = userRepository.findById(entityResponse.getUserId()).orElseThrow(); User user = userRepository.findById(userId).orElseThrow(); Company companyUser = user.getCompany(); if (companyUser != null && !fileUser.getCompany().getId().equals(companyUser.getId())) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.USER_NOT_HAD_PERMISSION.getCode(), MessageCode.USER_NOT_HAD_PERMISSION.getDescription(), errorData)); } if (companyUser == null && !entityResponse.getUserId().equals(userId)) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.USER_NOT_HAD_PERMISSION.getCode(), MessageCode.USER_NOT_HAD_PERMISSION.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_DELETE.getCode(), MessageCode.FILE_DELETE.getDescription(), errorData)); } // if (!entityResponse.isExistsOnDisk()) { // Map errorData = new HashMap<>(); // errorData.put("onDisk", entityResponse.isExistsOnDisk()); // // return ResponseEntity.ok().body(new BaseResponse(20004, // MessageCode.FILE_NOT_EXIST.getCode(), // MessageCode.FILE_NOT_EXIST.getDescription(), // errorData)); // } File file = cloudStorageService.readFileFromStorageByPath(entityResponse.getProtectedFilePath()); if (!file.exists()) { Map errorData = new HashMap<>(); errorData.put("file", file.exists()); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_DOWNLOAD_ERROR.getCode(), MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), errorData)); } return ResponseEntity.ok() .contentLength(file.length()) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + entityResponse.getFileName() + "\"") .body(file); } 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 e) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), errorData)); } catch (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("/check/file_stats") public ResponseEntity checkFileProtectStats(@RequestHeader("Authorization") String tokenHeader) { String token = tokenHeader.replace("Bearer ", ""); Optional tokenOptional = authTokenRepository.findByToken(token); AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); Map fileTypeStats = protectionsLimitService.getFileTypeStats(authToken.getUser().getId()); return ResponseEntity.ok().body(fileTypeStats); } 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; } }