package ru.soune.nocopy.controller; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; 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.RegAnswer; import ru.soune.nocopy.dto.file.ChunkUploadResponse; import ru.soune.nocopy.dto.file.CompleteUploadResponse; import ru.soune.nocopy.dto.file.FileEntityResponse; import ru.soune.nocopy.dto.file.UploadProgress; import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.handler.*; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; 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.stream.Collectors; @Slf4j @RestController @RequestMapping("/api") @RequiredArgsConstructor public class ApiController { private final FileUploadService fileUploadService; private final Map handlers; private final FileEntityService fileEntityService; private final AuthTokenRepository authTokenRepository; @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 (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("upload_id") String uploadId, @RequestParam("chunk_number") Integer chunkNumber, @RequestParam("chunk") MultipartFile chunk) { log.info("Uploading chunk {} for session {}, file size: {} bytes, version: {}", chunkNumber, uploadId, chunk.getSize(), version); if (chunk.isEmpty()) { return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), "Chunk file is empty", null)); } if (uploadId == null || uploadId.isBlank()) { return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), "Upload ID is required", null)); } if (chunkNumber == null || chunkNumber < 0) { return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), "Valid chunk number is required", null)); } try { fileUploadService.uploadChunk(uploadId, chunkNumber, chunk); ChunkUploadResponse responseBody = ChunkUploadResponse.builder() .uploadId(uploadId) .chunkNumber(chunkNumber) .chunkSize(chunk.getSize()) .message("Chunk uploaded successfully") .build(); return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.SUCCESS.getCode(), "Chunk uploaded successfully", responseBody)); } catch (Exception e) { log.error("Error uploading chunk", e); ChunkUploadResponse responseBody = ChunkUploadResponse.builder() .uploadId(uploadId) .chunkNumber(chunkNumber) .build(); return ResponseEntity.ok().body(new BaseResponse( 20004, MessageCode.FILE_UPLOAD_ERROR.getCode(), "Failed to upload chunk: " + e.getMessage(), responseBody)); } } @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 String fileId, @PathVariable Integer version, @RequestHeader("Authorization") String tokenHeader) { try { Long userId = getUserIdFromToken(tokenHeader); FileEntityResponse fileInfo = fileEntityService.getById(fileId, version); if (!fileInfo.getUserId().equals(userId)) { return ResponseEntity.status(403).build(); } if (!fileInfo.isExistsOnDisk()) { return ResponseEntity.status(404) .body(null); } Path filePath = Paths.get(fileInfo.getFilePath()); Resource resource = new UrlResource(filePath.toUri()); if (!resource.exists()) { return ResponseEntity.status(404).build(); } String contentType = determineContentType(filePath); return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileInfo.getOriginalFileName() + "\"") .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileInfo.getFileSize())) .body(resource); } catch (NotFoundAuthToken e) { return ResponseEntity.status(401).build(); } catch (Exception e) { log.error("Error downloading file", e); return ResponseEntity.status(500).build(); } } @GetMapping("/v{version}/files/info/{fileId}") public ResponseEntity getFileInfo( @PathVariable String fileId, @PathVariable Integer version, @RequestHeader("Authorization") String tokenHeader) { try { Long userId = getUserIdFromToken(tokenHeader); FileEntityResponse fileInfo = fileEntityService.getById(fileId, version); if (!fileInfo.getUserId().equals(userId)) { return ResponseEntity.status(403).build(); } return ResponseEntity.ok(fileInfo); } catch (NotFoundAuthToken e) { return ResponseEntity.status(401).build(); } catch (Exception e) { log.error("Error getting file info", e); return ResponseEntity.status(500).build(); } } @GetMapping("/v{version}/files/preview/{fileId}") public ResponseEntity previewFile( @PathVariable String fileId, @PathVariable Integer version, @RequestHeader("Authorization") String tokenHeader) { try { Long userId = getUserIdFromToken(tokenHeader); FileEntityResponse fileInfo = fileEntityService.getById(fileId, version); if (!fileInfo.getUserId().equals(userId)) { return ResponseEntity.status(403).build(); } if (!isPreviewSupported(fileInfo.getMimeType())) { return ResponseEntity.status(415) .body(null); } Path filePath = Paths.get(fileInfo.getFilePath()); Resource resource = new UrlResource(filePath.toUri()); if (!resource.exists()) { return ResponseEntity.status(404).build(); } String contentType = determineContentType(filePath); return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + fileInfo.getOriginalFileName() + "\"") .body(resource); } catch (NotFoundAuthToken e) { return ResponseEntity.status(401).build(); } catch (Exception e) { log.error("Error previewing file", e); return ResponseEntity.status(500).build(); } } 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; } private boolean isPreviewSupported(String mimeType) { if (mimeType == null) { return false; } return mimeType.startsWith("image/") || mimeType.startsWith("text/") || mimeType.equals("application/pdf") || mimeType.startsWith("video/") || mimeType.startsWith("audio/"); } private Long getUserIdFromToken(String tokenHeader) { String token = tokenHeader.replace("Bearer ", ""); AuthToken authToken = authTokenRepository.findByToken(token) .orElseThrow(() -> new NotFoundAuthToken("Token not found")); return authToken.getUser().getId(); } }