This commit is contained in:
@@ -8,15 +8,22 @@ 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.UploadProgress;
|
||||
import ru.soune.nocopy.entity.file.UploadStatus;
|
||||
import ru.soune.nocopy.exception.NotValidFieldException;
|
||||
import ru.soune.nocopy.exception.ValidationException;
|
||||
import ru.soune.nocopy.handler.FileUploadHandler;
|
||||
import ru.soune.nocopy.handler.LoginRequestHandler;
|
||||
import ru.soune.nocopy.handler.RegRequestHandler;
|
||||
import ru.soune.nocopy.handler.RequestHandler;
|
||||
import ru.soune.nocopy.service.file.FileUploadService;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -29,6 +36,7 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class ApiController {
|
||||
private final ApplicationContext applicationContext;
|
||||
private final FileUploadService fileUploadService;
|
||||
|
||||
private Map<Integer, RequestHandler> handlers = new HashMap<>();
|
||||
|
||||
@@ -36,7 +44,9 @@ public class ApiController {
|
||||
public void init() {
|
||||
RegRequestHandler regHandler = applicationContext.getBean(RegRequestHandler.class);
|
||||
LoginRequestHandler loginHandler = applicationContext.getBean(LoginRequestHandler.class);
|
||||
FileUploadHandler fileUploadHandler = applicationContext.getBean(FileUploadHandler.class);
|
||||
|
||||
handlers.put(20004, fileUploadHandler);
|
||||
handlers.put(20002, regHandler);
|
||||
handlers.put(20001, loginHandler);
|
||||
}
|
||||
@@ -76,6 +86,155 @@ public class ApiController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/v{version}/files/chunk")
|
||||
public ResponseEntity<BaseResponse> 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<BaseResponse> 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<BaseResponse> 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));
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<BaseResponse> createValidationErrorResponse(BindingResult bindingResult, Integer msgId) {
|
||||
List<Map<String, String>> fieldErrors = bindingResult.getFieldErrors()
|
||||
.stream()
|
||||
|
||||
@@ -31,38 +31,6 @@ public class FileUploadController {
|
||||
|
||||
private final FileUploadService fileUploadService;
|
||||
|
||||
private final AuthTokenRepository authTokenRepository;
|
||||
|
||||
private final FileUploadSessionRepository fileUploadSessionRepository;
|
||||
|
||||
//TODO ADD TEMPLATE JSON, RESPONSE OK,ADD MESSAGE CODE
|
||||
@PostMapping("/init")
|
||||
public ResponseEntity<FileApiResponse<FileUploadSession>> initUpload(@Valid @RequestBody InitUploadRequest request,
|
||||
@RequestHeader("Authorization") String tokenHeader) {
|
||||
String token = tokenHeader.replace("Bearer ", "");
|
||||
|
||||
Optional<AuthToken> tokenOptional = authTokenRepository.findByToken(token);
|
||||
|
||||
AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found"));
|
||||
|
||||
try {
|
||||
FileUploadSession session = fileUploadService.initUpload(authToken.getUser().getId(), request.getFileName(),
|
||||
request.getFileType(), request.getExtension(), request.getFileSize());
|
||||
|
||||
return ResponseEntity.ok(FileApiResponse.success(session));
|
||||
} catch (Exception e) {
|
||||
log.error("Error initializing upload", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(FileApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/file-types")
|
||||
public ResponseEntity<List<FileType>> getFileTypes() {
|
||||
return ResponseEntity.ok(Arrays.asList(FileType.values()));
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/chunk")
|
||||
public ResponseEntity<FileApiResponse<UploadProgressResponse>> uploadChunk(
|
||||
@RequestParam("uploadId") String uploadId,
|
||||
@@ -94,131 +62,4 @@ public class FileUploadController {
|
||||
.body(FileApiResponse.error(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/progress/{uploadId}")
|
||||
public ResponseEntity<FileApiResponse<UploadProgressResponse>> getProgress(
|
||||
@PathVariable String uploadId) {
|
||||
|
||||
try {
|
||||
UploadProgressResponse progress = fileUploadService.getUploadProgress(uploadId);
|
||||
return ResponseEntity.ok(FileApiResponse.success(progress));
|
||||
|
||||
} catch (UploadSessionNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(FileApiResponse.error("Upload session not found"));
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting progress", e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(FileApiResponse.error("Failed to get progress"));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{uploadId}/complete")
|
||||
public ResponseEntity<FileApiResponse<UploadProgressResponse>> completeUpload(
|
||||
@PathVariable String uploadId) {
|
||||
|
||||
log.info("Manual completion requested for session: {}", uploadId);
|
||||
|
||||
try {
|
||||
UploadProgressResponse progress = fileUploadService.getUploadProgress(uploadId);
|
||||
|
||||
if (progress.getStatus() == UploadStatus.COMPLETED) {
|
||||
return ResponseEntity.ok(FileApiResponse.success(
|
||||
"Upload already completed", progress));
|
||||
}
|
||||
|
||||
if (progress.getUploadedChunks() < progress.getTotalChunks()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(FileApiResponse.error(
|
||||
String.format("Not all chunks uploaded: %d/%d",
|
||||
progress.getUploadedChunks(), progress.getTotalChunks())));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(FileApiResponse.success(
|
||||
"File assembly in progress", progress));
|
||||
|
||||
} catch (UploadSessionNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(FileApiResponse.error("Upload session not found"));
|
||||
} catch (Exception e) {
|
||||
log.error("Error completing upload", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(FileApiResponse.error("Failed to complete upload"));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{uploadId}/cancel")
|
||||
public ResponseEntity<FileApiResponse<Void>> cancelUpload(
|
||||
@PathVariable String uploadId) {
|
||||
|
||||
log.info("Cancelling upload session: {}", uploadId);
|
||||
|
||||
try {
|
||||
fileUploadService.cancelUpload(uploadId);
|
||||
return ResponseEntity.ok(FileApiResponse.success(
|
||||
"Upload cancelled successfully", null));
|
||||
|
||||
} catch (UploadSessionNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(FileApiResponse.error("Upload session not found"));
|
||||
} catch (FileUploadException e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(FileApiResponse.error(e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
log.error("Error cancelling upload", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(FileApiResponse.error("Failed to cancel upload"));
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/{uploadId}/retry")
|
||||
public ResponseEntity<FileApiResponse<UploadProgressResponse>> retryUpload(
|
||||
@PathVariable String uploadId) {
|
||||
|
||||
log.info("Retry requested for failed upload: {}", uploadId);
|
||||
|
||||
try {
|
||||
fileUploadService.retryFailedUpload(uploadId);
|
||||
UploadProgressResponse progress = fileUploadService.getUploadProgress(uploadId);
|
||||
|
||||
return ResponseEntity.ok(FileApiResponse.success(
|
||||
"Upload retry initiated", progress));
|
||||
|
||||
} catch (UploadSessionNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(FileApiResponse.error("Upload session not found"));
|
||||
} catch (FileUploadException e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(FileApiResponse.error(e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrying upload", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(FileApiResponse.error("Failed to retry upload"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{uploadId}/chunks")
|
||||
public ResponseEntity<FileApiResponse<Map<Integer, Boolean>>> getChunkStatus(
|
||||
@PathVariable String uploadId) {
|
||||
|
||||
try {
|
||||
FileUploadSession session = fileUploadSessionRepository.findById(uploadId)
|
||||
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
|
||||
|
||||
Map<Integer, Boolean> chunkStatus = new HashMap<>();
|
||||
for (int i = 0; i < session.getTotalChunks(); i++) {
|
||||
chunkStatus.put(i, session.getChunkPaths().containsKey(i));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(FileApiResponse.success(chunkStatus));
|
||||
|
||||
} catch (UploadSessionNotFoundException e) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.body(FileApiResponse.error("Upload session not found"));
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting chunk status", e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(FileApiResponse.error("Failed to get chunk status"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,15 @@ public enum MessageCode {
|
||||
REG_EMAIL_EXISTS(1, "Email already registered"),
|
||||
REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"),
|
||||
INVALID_FIELD(2, "Invalid field"),
|
||||
INVALID_TOKEN(2, "Invalid token"),
|
||||
INVALID_ACTION(2, "Invalid action"),
|
||||
FILE_UPLOAD_ERROR(2, "File upload error"),
|
||||
INVALID_JSON_BODY(2, "Invalid fields in JSON object"),
|
||||
INCOMPLETE_UPLOAD(2, "Not load all chunks"),
|
||||
MSG_ID_NOT_FOUND(4, "Message id not found"),
|
||||
|
||||
AUTH_EMAIL_NOT_FOUND(4, "Email not found"),
|
||||
FILE_NOT_FOUND(4, "File not found"),
|
||||
AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match");
|
||||
|
||||
private final Integer code;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ActionResponse {
|
||||
private List<String> availableActions;
|
||||
private String action;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class CanceledUploadResponse {
|
||||
@JsonProperty("upload_id")
|
||||
private String uploadId;
|
||||
|
||||
@JsonProperty("message")
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class ChunkStatusResponse {
|
||||
@JsonProperty("upload_id")
|
||||
private String uploadId;
|
||||
|
||||
@JsonProperty("total_chunks")
|
||||
private Integer totalChunks;
|
||||
|
||||
@JsonProperty("chunk_status")
|
||||
private Map<String, Boolean> chunkStatus;
|
||||
|
||||
@JsonProperty("uploaded_chunks")
|
||||
private Integer uploadedChunks;
|
||||
|
||||
@JsonProperty("missing_chunks")
|
||||
private Integer missingChunks;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ChunkUploadResponse {
|
||||
@JsonProperty("upload_id")
|
||||
private String uploadId;
|
||||
|
||||
@JsonProperty("chunk_number")
|
||||
private Integer chunkNumber;
|
||||
|
||||
@JsonProperty("chunk_size")
|
||||
private Long chunkSize;
|
||||
|
||||
@JsonProperty("message")
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CompleteUploadResponse {
|
||||
@JsonProperty("upload_id")
|
||||
private String uploadId;
|
||||
|
||||
@JsonProperty("status")
|
||||
private String status;
|
||||
|
||||
@JsonProperty("uploaded_chunks")
|
||||
private Integer uploadedChunks;
|
||||
|
||||
@JsonProperty("total_chunks")
|
||||
private Integer totalChunks;
|
||||
|
||||
@JsonProperty("message")
|
||||
private String message;
|
||||
|
||||
@JsonProperty("file_path")
|
||||
private String filePath;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import ru.soune.nocopy.entity.file.FileType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class FileTypesResponse {
|
||||
@JsonProperty("file_types")
|
||||
private List<FileType> fileTypes;
|
||||
|
||||
@JsonProperty("count")
|
||||
private Integer count;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FileUploadRequest {
|
||||
@JsonProperty("upload_id")
|
||||
private String uploadId;
|
||||
|
||||
@JsonProperty("file_name")
|
||||
private String fileName;
|
||||
|
||||
@JsonProperty("file_type")
|
||||
private String fileType;
|
||||
|
||||
@JsonProperty("extension")
|
||||
private String extension;
|
||||
|
||||
@JsonProperty("file_size")
|
||||
private Long fileSize;
|
||||
|
||||
@JsonProperty("chunk_number")
|
||||
private Integer chunkNumber;
|
||||
|
||||
@JsonProperty("action")
|
||||
private String action;
|
||||
|
||||
@JsonProperty("token")
|
||||
private String token;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class InitFileResponse {
|
||||
@JsonProperty("upload_id")
|
||||
private String uploadId;
|
||||
|
||||
@JsonProperty("file_name")
|
||||
private String fileName;
|
||||
|
||||
@JsonProperty("total_chunks")
|
||||
private Integer totalChunks;
|
||||
|
||||
@JsonProperty("chunk_size")
|
||||
private Long chunkSize;
|
||||
|
||||
@JsonProperty("status")
|
||||
private String status;
|
||||
|
||||
@JsonProperty("field_errors")
|
||||
private List<Map<String, String>> fieldErrors;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class RetryUploadResponse {
|
||||
@JsonProperty("upload_id")
|
||||
private String uploadId;
|
||||
|
||||
@JsonProperty("message")
|
||||
private String message;
|
||||
|
||||
@JsonProperty("status")
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class UploadProgress {
|
||||
@JsonProperty("upload_id")
|
||||
private String uploadId;
|
||||
|
||||
@JsonProperty("file_name")
|
||||
private String fileName;
|
||||
|
||||
@JsonProperty("total_chunks")
|
||||
private Integer totalChunks;
|
||||
|
||||
@JsonProperty("uploaded_chunks")
|
||||
private Integer uploadedChunks;
|
||||
|
||||
@JsonProperty("status")
|
||||
private String status;
|
||||
|
||||
@JsonProperty("progress_percentage")
|
||||
private Integer progressPercentage;
|
||||
|
||||
@JsonProperty("file_path")
|
||||
private String filePath;
|
||||
|
||||
@JsonProperty("remaining_chunks")
|
||||
private Integer remainingChunks;
|
||||
|
||||
@JsonProperty("message")
|
||||
private String message;
|
||||
}
|
||||
@@ -33,6 +33,9 @@ public class FileUploadSession {
|
||||
@Column(name = "file_size")
|
||||
private Long fileSize;
|
||||
|
||||
@Column(name = "chunk_size")
|
||||
private Long chunkSize;
|
||||
|
||||
@Column(name = "total_chunks")
|
||||
private Integer totalChunks;
|
||||
|
||||
@@ -84,9 +87,14 @@ public class FileUploadSession {
|
||||
public void prePersist() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.expiresAt = LocalDateTime.now().plusHours(24);
|
||||
this.chunksUploaded = 0;
|
||||
if (this.chunksUploaded == null) {
|
||||
this.chunksUploaded = 0;
|
||||
}
|
||||
if (this.status == null) {
|
||||
this.status = UploadStatus.INITIATED;
|
||||
}
|
||||
if (this.retryCount == null) {
|
||||
this.retryCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package ru.soune.nocopy.handler;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import ru.soune.nocopy.dto.*;
|
||||
import ru.soune.nocopy.dto.file.*;
|
||||
import ru.soune.nocopy.entity.AuthToken;
|
||||
import ru.soune.nocopy.entity.file.FileType;
|
||||
import ru.soune.nocopy.entity.file.FileUploadSession;
|
||||
import ru.soune.nocopy.exception.*;
|
||||
import ru.soune.nocopy.handler.validator.FileUploadRequestValidator;
|
||||
import ru.soune.nocopy.repository.AuthTokenRepository;
|
||||
import ru.soune.nocopy.repository.FileUploadSessionRepository;
|
||||
import ru.soune.nocopy.service.file.FileUploadService;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class FileUploadHandler implements RequestHandler {
|
||||
private final FileUploadService fileUploadService;
|
||||
private final AuthTokenRepository authTokenRepository;
|
||||
private final FileUploadSessionRepository fileUploadSessionRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final FileUploadRequestValidator fileUploadRequestValidator;
|
||||
|
||||
@Override
|
||||
public BaseResponse handle(BaseRequest request) {
|
||||
try {
|
||||
FileUploadRequest fileRequest = objectMapper.convertValue(
|
||||
request.getMessageBody(), FileUploadRequest.class);
|
||||
|
||||
String action = fileRequest.getAction();
|
||||
|
||||
switch (action) {
|
||||
case "init":
|
||||
return handleInitUpload(request, fileRequest);
|
||||
case "file_types":
|
||||
return handleGetFileTypes(request);
|
||||
case "chunks":
|
||||
return handleGetChunkStatus(request, fileRequest);
|
||||
case "cancel":
|
||||
return handleCancelUpload(request, fileRequest);
|
||||
case "progress":
|
||||
return handleGetProgress(request, fileRequest);
|
||||
case "retry":
|
||||
return handleRetryUpload(request, fileRequest);
|
||||
default:
|
||||
ActionResponse response = ActionResponse.builder()
|
||||
.action(action)
|
||||
.availableActions(Arrays.asList("init", "file_types", "chunks", "cancel", "progress", "retry"))
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_ACTION.getCode(),
|
||||
"Invalid action: " + action, response);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error in FileUploadHandler", e);
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
"Handler error: " + e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
private BaseResponse handleInitUpload(BaseRequest request, FileUploadRequest fileRequest) {
|
||||
try {
|
||||
String token = fileRequest.getToken();
|
||||
|
||||
Optional<AuthToken> tokenOptional = authTokenRepository.findByToken(token);
|
||||
|
||||
AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
|
||||
|
||||
BindingResult bindingResult = new BeanPropertyBindingResult(fileRequest, "fileRequest");
|
||||
fileUploadRequestValidator.validate(fileRequest, bindingResult);
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
throw new ValidationException(bindingResult, request.getMsgId());
|
||||
}
|
||||
|
||||
FileUploadSession session = fileUploadService.initUpload(
|
||||
authToken.getUser().getId(),
|
||||
fileRequest.getFileName(),
|
||||
fileRequest.getFileType(),
|
||||
fileRequest.getExtension(),
|
||||
fileRequest.getFileSize());
|
||||
|
||||
InitFileResponse fileResponse = InitFileResponse.builder()
|
||||
.uploadId(session.getUploadId())
|
||||
.fileName(session.getFileName())
|
||||
.totalChunks(session.getTotalChunks())
|
||||
.chunkSize(session.getChunkSize())
|
||||
.status(session.getStatus().toString())
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
||||
MessageCode.SUCCESS.getDescription(), fileResponse);
|
||||
|
||||
} catch (NotFoundAuthToken e) {
|
||||
InitFileResponse initFileResponse = InitFileResponse.builder()
|
||||
.build();
|
||||
|
||||
throw new NotValidFieldException("Invalid or expired token: " + fileRequest.getToken(),
|
||||
new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
|
||||
MessageCode.INVALID_TOKEN.getDescription(), initFileResponse));
|
||||
}
|
||||
}
|
||||
|
||||
private BaseResponse handleGetProgress(BaseRequest request, FileUploadRequest fileRequest) {
|
||||
try {
|
||||
ru.soune.nocopy.dto.file.UploadProgressResponse progress =
|
||||
fileUploadService.getUploadProgress(fileRequest.getUploadId());
|
||||
|
||||
UploadProgressResponse responseDto = UploadProgressResponse.builder()
|
||||
.uploadId(progress.getUploadId())
|
||||
.fileName(progress.getFileName())
|
||||
.totalChunks(progress.getTotalChunks())
|
||||
.uploadedChunks(progress.getUploadedChunks())
|
||||
.status(progress.getStatus())
|
||||
.progressPercentage(progress.getProgressPercentage())
|
||||
.filePath(progress.getFilePath())
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
||||
MessageCode.SUCCESS.getDescription(), responseDto);
|
||||
|
||||
} catch (UploadSessionNotFoundException e) {
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_NOT_FOUND.getCode(),
|
||||
MessageCode.FILE_NOT_FOUND.getDescription(), null);
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting progress", e);
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
MessageCode.FILE_UPLOAD_ERROR.getDescription(), null);
|
||||
}
|
||||
}
|
||||
|
||||
private BaseResponse handleCancelUpload(BaseRequest request, FileUploadRequest fileRequest) {
|
||||
try {
|
||||
fileUploadService.cancelUpload(fileRequest.getUploadId());
|
||||
|
||||
CanceledUploadResponse response = CanceledUploadResponse.builder()
|
||||
.uploadId(fileRequest.getUploadId())
|
||||
.message("Upload cancelled successfully")
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
||||
"Upload cancelled successfully", response);
|
||||
|
||||
} catch (UploadSessionNotFoundException e) {
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_NOT_FOUND.getCode(),
|
||||
"Upload session not found", null);
|
||||
} catch (FileUploadException e) {
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(), e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
log.error("Error cancelling upload", e);
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
"Failed to cancel upload", null);
|
||||
}
|
||||
}
|
||||
|
||||
private BaseResponse handleRetryUpload(BaseRequest request, FileUploadRequest fileRequest) {
|
||||
try {
|
||||
fileUploadService.retryFailedUpload(fileRequest.getUploadId());
|
||||
ru.soune.nocopy.dto.file.UploadProgressResponse progress =
|
||||
fileUploadService.getUploadProgress(fileRequest.getUploadId());
|
||||
|
||||
RetryUploadResponse response = RetryUploadResponse.builder()
|
||||
.uploadId(progress.getUploadId())
|
||||
.message("Upload retry initiated")
|
||||
.status(progress.getStatus().toString())
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), "Upload retry initiated",
|
||||
response);
|
||||
|
||||
} catch (UploadSessionNotFoundException e) {
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_NOT_FOUND.getCode(),
|
||||
"Upload session not found", null);
|
||||
} catch (FileUploadException e) {
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(), e.getMessage(),
|
||||
null);
|
||||
} catch (Exception e) {
|
||||
log.error("Error retrying upload", e);
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
"Failed to retry upload", null);
|
||||
}
|
||||
}
|
||||
|
||||
private BaseResponse handleGetChunkStatus(BaseRequest request, FileUploadRequest fileRequest) {
|
||||
try {
|
||||
FileUploadSession session = fileUploadSessionRepository.findById(fileRequest.getUploadId())
|
||||
.orElseThrow(() -> new UploadSessionNotFoundException(fileRequest.getUploadId()));
|
||||
|
||||
Map<String, Boolean> chunkStatus = new HashMap<>();
|
||||
for (int i = 0; i < session.getTotalChunks(); i++) {
|
||||
chunkStatus.put("chunk_" + i, session.getChunkPaths().containsKey(i));
|
||||
}
|
||||
|
||||
ChunkStatusResponse response = ChunkStatusResponse.builder()
|
||||
.uploadId(fileRequest.getUploadId())
|
||||
.totalChunks(session.getTotalChunks())
|
||||
.uploadedChunks(session.getChunksUploaded())
|
||||
.missingChunks(session.getTotalChunks() - session.getChunksUploaded())
|
||||
.chunkStatus(chunkStatus)
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
||||
MessageCode.SUCCESS.getDescription(), response);
|
||||
|
||||
} catch (UploadSessionNotFoundException e) {
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_NOT_FOUND.getCode(),
|
||||
"Upload session not found", null);
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting chunk status", e);
|
||||
return new BaseResponse(request.getMsgId(),
|
||||
MessageCode.FILE_UPLOAD_ERROR.getCode(), "Failed to get chunk status",
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
private BaseResponse handleGetFileTypes(BaseRequest request) {
|
||||
try {
|
||||
FileTypesResponse response = FileTypesResponse.builder()
|
||||
.fileTypes(Arrays.asList(FileType.values()))
|
||||
.count(FileType.values().length)
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
||||
MessageCode.SUCCESS.getDescription(), response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting file types", e);
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
"Failed to get file types", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package ru.soune.nocopy.handler.validator;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.Errors;
|
||||
import org.springframework.validation.Validator;
|
||||
import ru.soune.nocopy.dto.file.FileUploadRequest;
|
||||
import ru.soune.nocopy.entity.file.FileType;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Component
|
||||
public class FileUploadRequestValidator implements Validator {
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
return FileUploadRequest.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Object target, Errors errors) {
|
||||
FileUploadRequest request = (FileUploadRequest) target;
|
||||
|
||||
validateType(request.getFileType(), errors);
|
||||
validateFileName(request.getFileName(), errors);
|
||||
validateExtension(request.getExtension(), errors);
|
||||
}
|
||||
|
||||
private void validateType(String fileType, Errors errors) {
|
||||
if (fileType == null || fileType.isBlank()) {
|
||||
errors.rejectValue("fileType", "fileType.required", "File type is required");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FileType.valueOf(fileType.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
errors.rejectValue("fileType", "fileType.invalid",
|
||||
"Invalid file type. Valid types: " + Arrays.toString(FileType.values()));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFileName(String fileName, Errors errors) {
|
||||
if (fileName == null || fileName.isBlank()) {
|
||||
errors.rejectValue("fileName", "fileName.required", "File name is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileName.length() > 255) {
|
||||
errors.rejectValue("fileName", "fileName.too.long",
|
||||
"File name too long, max 255 characters");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateExtension(String extension, Errors errors) {
|
||||
if (extension == null || extension.isBlank()) {
|
||||
errors.rejectValue("extension", "extension.required", "Extension is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!extension.startsWith(".")) {
|
||||
errors.rejectValue("extension", "extension.invalid.format",
|
||||
"Extension must start with '.'");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -510,8 +510,8 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
FileUploadSession session = sessionRepository.findById(uploadId)
|
||||
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
|
||||
|
||||
if (session.getStatus() == UploadStatus.COMPLETED) {
|
||||
throw new FileUploadException("Cannot cancel completed upload");
|
||||
if (session.getStatus() == UploadStatus.COMPLETED || session.getStatus() == UploadStatus.CANCELLED) {
|
||||
throw new FileUploadException("Cannot cancel completed or cancelled upload");
|
||||
}
|
||||
|
||||
session.setStatus(UploadStatus.CANCELLED);
|
||||
@@ -539,6 +539,7 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
session.setStatus(UploadStatus.UPLOADING);
|
||||
session.setRetryCount(session.getRetryCount() + 1);
|
||||
session.setLastError(null);
|
||||
|
||||
sessionRepository.save(session);
|
||||
|
||||
if (session.getChunksUploaded().equals(session.getTotalChunks())) {
|
||||
|
||||
Reference in New Issue
Block a user