# Conflicts: # docker-compose.yaml # src/main/java/ru/soune/nocopy/dto/MessageCode.java
This commit is contained in:
@@ -2,8 +2,10 @@ package ru.soune.nocopy;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class NoCopyApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -20,10 +20,9 @@ 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.FileStatus;
|
||||
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.exception.*;
|
||||
import ru.soune.nocopy.handler.*;
|
||||
import ru.soune.nocopy.repository.AuthTokenRepository;
|
||||
import ru.soune.nocopy.service.file.FileEntityService;
|
||||
@@ -89,29 +88,34 @@ 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));
|
||||
}
|
||||
|
||||
@RequestParam(value = "upload_id", required = false) String uploadId,
|
||||
@RequestParam(value = "chunk_number", required = false) Integer chunkNumber,
|
||||
@RequestParam(value = "chunk", required = false) MultipartFile chunk) {
|
||||
try {
|
||||
if (chunk == null || chunk.isEmpty()) {
|
||||
return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
"Chunk file null or empty", ChunkUploadResponse.builder()
|
||||
.uploadId(uploadId)
|
||||
.chunkNumber(chunkNumber)
|
||||
.build()));
|
||||
}
|
||||
|
||||
if (uploadId == null || uploadId.isBlank()) {
|
||||
return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
"Upload ID is required", ChunkUploadResponse.builder()
|
||||
.uploadId(uploadId)
|
||||
.chunkNumber(chunkNumber)
|
||||
.build()));
|
||||
}
|
||||
|
||||
if (chunkNumber == null || chunkNumber < 0) {
|
||||
return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
"Valid chunk number is required", ChunkUploadResponse.builder()
|
||||
.uploadId(uploadId)
|
||||
.chunkNumber(chunkNumber)
|
||||
.build()));
|
||||
}
|
||||
|
||||
fileUploadService.uploadChunk(uploadId, chunkNumber, chunk);
|
||||
|
||||
ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
|
||||
@@ -123,7 +127,6 @@ public class ApiController {
|
||||
|
||||
return ResponseEntity.ok().body(new BaseResponse(20004, MessageCode.SUCCESS.getCode(),
|
||||
"Chunk uploaded successfully", responseBody));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error uploading chunk", e);
|
||||
|
||||
@@ -237,29 +240,67 @@ public class ApiController {
|
||||
|
||||
|
||||
@GetMapping("/v{version}/files/download/{fileId}")
|
||||
public ResponseEntity<Resource> downloadFile(
|
||||
@PathVariable String fileId,
|
||||
@PathVariable Integer version,
|
||||
@RequestHeader("Authorization") String tokenHeader) {
|
||||
|
||||
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<String, Object> 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 = getUserIdFromToken(tokenHeader);
|
||||
FileEntityResponse fileInfo = fileEntityService.getById(fileId, version);
|
||||
FileEntityResponse entityResponse = fileEntityService.getById(fileId, version);
|
||||
|
||||
if (!fileInfo.getUserId().equals(userId)) {
|
||||
return ResponseEntity.status(403).build();
|
||||
if (!entityResponse.getUserId().equals(userId)) {
|
||||
Map<String, Object> 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 (!fileInfo.isExistsOnDisk()) {
|
||||
return ResponseEntity.status(404)
|
||||
.body(null);
|
||||
if (entityResponse.getStatus().equals(FileStatus.DELETED)) {
|
||||
Map<String, Object> 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));
|
||||
}
|
||||
|
||||
Path filePath = Paths.get(fileInfo.getFilePath());
|
||||
if (!entityResponse.isExistsOnDisk()) {
|
||||
Map<String, Object> 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.getFilePath());
|
||||
Resource resource = new UrlResource(filePath.toUri());
|
||||
|
||||
if (!resource.exists()) {
|
||||
return ResponseEntity.status(404).build();
|
||||
Map<String, Object> 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);
|
||||
@@ -267,85 +308,30 @@ public class ApiController {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(contentType))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename=\"" + fileInfo.getOriginalFileName() + "\"")
|
||||
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(fileInfo.getFileSize()))
|
||||
"attachment; filename=\"" + entityResponse.getOriginalFileName() + "\"")
|
||||
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(entityResponse.getFileSize()))
|
||||
.body(resource);
|
||||
} catch (FileEntityNotFoundException e) {
|
||||
Map<String, Object> errorData = new HashMap<>();
|
||||
errorData.put("fileId", fileId);
|
||||
|
||||
} catch (NotFoundAuthToken e) {
|
||||
return ResponseEntity.status(401).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Error downloading file", e);
|
||||
return ResponseEntity.status(500).build();
|
||||
return ResponseEntity.ok().body(new BaseResponse(20004,
|
||||
MessageCode.FILE_NOT_FOUND.getCode(),
|
||||
MessageCode.FILE_NOT_FOUND.getDescription(),
|
||||
errorData));
|
||||
} catch (NotFoundAuthToken | IOException e) {
|
||||
Map<String, Object> 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("/v{version}/files/info/{fileId}")
|
||||
public ResponseEntity<FileEntityResponse> 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<Resource> 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<BaseResponse> createValidationErrorResponse(BindingResult bindingResult, Integer msgId) {
|
||||
List<Map<String, String>> fieldErrors = bindingResult.getFieldErrors()
|
||||
.stream()
|
||||
@@ -382,18 +368,6 @@ public class ApiController {
|
||||
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)
|
||||
|
||||
@@ -8,6 +8,8 @@ public enum MessageCode {
|
||||
INVALID_TOKEN(2, "Invalid token"),
|
||||
INVALID_ACTION(2, "Invalid action"),
|
||||
FILE_UPLOAD_ERROR(2, "File upload error"),
|
||||
FILE_DOWNLOAD_ERROR(2, "File download error"),
|
||||
FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"),
|
||||
IMAGE_FOUND_ERROR(2, "Image found error"),
|
||||
INVALID_JSON_BODY(2, "Invalid fields in JSON object"),
|
||||
INCOMPLETE_UPLOAD(2, "Not load all chunks"),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.soune.nocopy.dto.file;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class FileExtensionResponse {
|
||||
@JsonProperty("file_extension")
|
||||
private List<String> extension;
|
||||
|
||||
@JsonProperty("count")
|
||||
private Integer count;
|
||||
|
||||
@JsonProperty("max_file_size")
|
||||
private Long maxFileSize;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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 FileInfoUserResponse {
|
||||
@JsonProperty("all_files_size")
|
||||
private Long allFileSize;
|
||||
|
||||
@JsonProperty("all_files_quantity")
|
||||
private Integer fileCount;
|
||||
|
||||
@JsonProperty("all_files_check")
|
||||
private Integer filesCheck;
|
||||
|
||||
@JsonProperty("all_files_violation")
|
||||
private Integer filesViolation;
|
||||
|
||||
@JsonProperty("images_size")
|
||||
private Long imagesSize;
|
||||
|
||||
@JsonProperty("images_quantity")
|
||||
private Integer imagesCount;
|
||||
|
||||
@JsonProperty("images_check")
|
||||
private Integer imagesCheck;
|
||||
|
||||
@JsonProperty("images_violations")
|
||||
private Integer imagesViolations;
|
||||
|
||||
@JsonProperty("videos_size")
|
||||
private Long videosSize;
|
||||
|
||||
@JsonProperty("videos_quantity")
|
||||
private Integer videosCount;
|
||||
|
||||
@JsonProperty("videos_check")
|
||||
private Integer videosCheck;
|
||||
|
||||
@JsonProperty("videos_violations")
|
||||
private Integer videosViolations;
|
||||
|
||||
@JsonProperty("audios_size")
|
||||
private Long audiosSize;
|
||||
|
||||
@JsonProperty("audios_quantity")
|
||||
private Integer audiosCount;
|
||||
|
||||
@JsonProperty("audios_check")
|
||||
private Integer audiosCheck;
|
||||
|
||||
@JsonProperty("audios_violations")
|
||||
private Integer audiosViolations;
|
||||
}
|
||||
@@ -4,5 +4,7 @@ public enum FileStatus {
|
||||
ACTIVE,
|
||||
DELETED,
|
||||
PROCESSING,
|
||||
VIOLATION,
|
||||
CHECKED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
@@ -2,21 +2,33 @@ package ru.soune.nocopy.entity.file;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
public enum FileType {
|
||||
PHOTO("photo"),
|
||||
IMAGE("image"),
|
||||
VIDEO("video"),
|
||||
AUDIO("audio"),
|
||||
DOCUMENT("document");
|
||||
IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp", "jfif")),
|
||||
VIDEO("video", Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "mpg", "mpeg",
|
||||
"3gp", "3g2", "f4v", "m2ts", "mts", "vob", "ogv", "divx")),
|
||||
AUDIO("audio", Arrays.asList("mp3", "wav", "flac"));
|
||||
|
||||
private final String code;
|
||||
private final String displayName;
|
||||
private final List<String> allowedExtensions;
|
||||
|
||||
FileType(String code) {
|
||||
this.code = code;
|
||||
FileType(String displayName, List<String> allowedExtensions) {
|
||||
this.displayName = displayName;
|
||||
this.allowedExtensions = allowedExtensions;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public List<String> getAllowedExtensions() {
|
||||
return allowedExtensions;
|
||||
}
|
||||
|
||||
public boolean supportsExtension(String extension) {
|
||||
return allowedExtensions.contains(extension.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ public class FileUploadSession {
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
this.expiresAt = LocalDateTime.now().plusHours(24);
|
||||
this.expiresAt = LocalDateTime.now().plusMinutes(1);
|
||||
if (this.chunksUploaded == null) {
|
||||
this.chunksUploaded = 0;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ package ru.soune.nocopy.exception;
|
||||
|
||||
public class ChunkSizeExceededException extends RuntimeException {
|
||||
public ChunkSizeExceededException(long actualSize, long maxSize) {
|
||||
super(String.format("Chunk size exceeded: %d > %d", actualSize, maxSize));
|
||||
super(String.format("Chunk size not valid: " + actualSize));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,17 @@ import org.springframework.stereotype.Component;
|
||||
import ru.soune.nocopy.dto.*;
|
||||
import ru.soune.nocopy.dto.file.*;
|
||||
import ru.soune.nocopy.entity.AuthToken;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.entity.file.FileStatus;
|
||||
import ru.soune.nocopy.exception.FileEntityNotFoundException;
|
||||
import ru.soune.nocopy.exception.NotFoundAuthToken;
|
||||
import ru.soune.nocopy.repository.AuthTokenRepository;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
import ru.soune.nocopy.service.file.FileEntityService;
|
||||
import ru.soune.nocopy.service.file.FileStatsService;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -19,9 +25,15 @@ import java.util.Arrays;
|
||||
public class FileEntityHandler implements RequestHandler {
|
||||
|
||||
private final FileEntityService fileEntityService;
|
||||
|
||||
private final FileStatsService fileStatsService;
|
||||
|
||||
private final AuthTokenRepository authTokenRepository;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
@Override
|
||||
public BaseResponse handle(BaseRequest request) {
|
||||
try {
|
||||
@@ -31,15 +43,17 @@ public class FileEntityHandler implements RequestHandler {
|
||||
String action = fileRequest.getAction();
|
||||
|
||||
switch (action) {
|
||||
case "get_file_info":
|
||||
case "file_info":
|
||||
return handleGetFileInfo(request, fileRequest);
|
||||
case "get_file_by_session":
|
||||
case "user_files_info":
|
||||
return handleGetFilesUserInfo(request, fileRequest);
|
||||
case "file_by_session":
|
||||
return handleGetFileBySession(request, fileRequest);
|
||||
case "get_user_files":
|
||||
case "user_files":
|
||||
return handleGetUserFiles(request, fileRequest);
|
||||
case "search_files":
|
||||
return handleSearchFiles(request, fileRequest);
|
||||
case "get_storage_usage":
|
||||
case "storage_usage":
|
||||
return handleGetStorageUsage(request, fileRequest);
|
||||
case "delete_file":
|
||||
return handleDeleteFile(request, fileRequest);
|
||||
@@ -47,8 +61,8 @@ public class FileEntityHandler implements RequestHandler {
|
||||
ActionResponse response = ActionResponse.builder()
|
||||
.action(action)
|
||||
.availableActions(Arrays.asList(
|
||||
"get_file_info", "get_file_by_session", "get_user_files",
|
||||
"search_files", "get_storage_usage", "delete_file"))
|
||||
"file_info", "file_by_session", "user_files",
|
||||
"search_files", "storage_usage", "delete_file"))
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(),
|
||||
@@ -65,6 +79,16 @@ public class FileEntityHandler implements RequestHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private BaseResponse handleGetFilesUserInfo(BaseRequest request, FileEntityRequest fileRequest) {
|
||||
Long userId = getUserIdFromToken(fileRequest.getToken());
|
||||
FileInfoUserResponse userFileStats = fileStatsService.getUserFileStats(userId);
|
||||
|
||||
return new BaseResponse(request.getMsgId(),
|
||||
MessageCode.SUCCESS.getCode(),
|
||||
MessageCode.SUCCESS.getDescription(),
|
||||
userFileStats);
|
||||
}
|
||||
|
||||
private BaseResponse handleGetFileInfo(BaseRequest request, FileEntityRequest fileRequest) {
|
||||
try {
|
||||
Long userId = getUserIdFromToken(fileRequest.getToken());
|
||||
@@ -168,17 +192,22 @@ public class FileEntityHandler implements RequestHandler {
|
||||
FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion());
|
||||
|
||||
var filteredFiles = allFiles.getFiles().stream()
|
||||
.filter(f -> f.getOriginalFileName().toLowerCase().contains(fileRequest.getQuery().toLowerCase()))
|
||||
.filter(f -> (f.getOriginalFileName() + f.getFileExtension()).toLowerCase()
|
||||
.contains(fileRequest.getQuery().toLowerCase()))
|
||||
.toList();
|
||||
|
||||
int start = (page - 1) * pageSize;
|
||||
int end = Math.min(start + pageSize, filteredFiles.size());
|
||||
List<FileEntityResponse> fileEntityResponses = filteredFiles.subList(start, Math.min(end, filteredFiles.size()));
|
||||
long sumSize = fileEntityResponses.stream()
|
||||
.mapToLong(FileEntityResponse::getFileSize)
|
||||
.sum();
|
||||
|
||||
FileListResponse response = FileListResponse.builder()
|
||||
.files(filteredFiles.subList(start, Math.min(end, filteredFiles.size())))
|
||||
.files(fileEntityResponses)
|
||||
.totalCount(filteredFiles.size())
|
||||
.totalSize(allFiles.getTotalSize())
|
||||
.formattedTotalSize(allFiles.getFormattedTotalSize())
|
||||
.totalSize(sumSize)
|
||||
.formattedTotalSize(fileEntityService.formatFileSize(sumSize))
|
||||
.page(page)
|
||||
.pageSize(pageSize)
|
||||
.build();
|
||||
@@ -187,18 +216,13 @@ public class FileEntityHandler implements RequestHandler {
|
||||
MessageCode.SUCCESS.getCode(),
|
||||
MessageCode.SUCCESS.getDescription(),
|
||||
response);
|
||||
|
||||
} catch (NotFoundAuthToken e) {
|
||||
return new BaseResponse(request.getMsgId(),
|
||||
MessageCode.INVALID_TOKEN.getCode(),
|
||||
"Authentication required",
|
||||
null);
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
|
||||
"Authentication required", null);
|
||||
} catch (Exception e) {
|
||||
log.error("Error searching files", e);
|
||||
return new BaseResponse(request.getMsgId(),
|
||||
MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
"Failed to search files: " + e.getMessage(),
|
||||
null);
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
"Failed to search files: " + e.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +259,9 @@ public class FileEntityHandler implements RequestHandler {
|
||||
private BaseResponse handleDeleteFile(BaseRequest request, FileEntityRequest fileRequest) {
|
||||
try {
|
||||
Long userId = getUserIdFromToken(fileRequest.getToken());
|
||||
FileEntityResponse fileInfo = fileEntityService.getById(fileRequest.getFileId(), request.getVersion());
|
||||
String fileId = fileRequest.getFileId();
|
||||
FileEntityResponse fileInfo = fileEntityService.getById(fileId, request.getVersion());
|
||||
DeleteFileResponse response;
|
||||
|
||||
if (!fileInfo.getUserId().equals(userId)) {
|
||||
return new BaseResponse(request.getMsgId(),
|
||||
@@ -244,18 +270,27 @@ public class FileEntityHandler implements RequestHandler {
|
||||
null);
|
||||
}
|
||||
|
||||
fileEntityService.markAsDeleted(fileRequest.getFileId());
|
||||
FileEntity fileEntity = fileEntityRepository.findById(fileId)
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||
|
||||
DeleteFileResponse response = DeleteFileResponse.builder()
|
||||
.fileId(fileRequest.getFileId())
|
||||
.message("File marked as deleted")
|
||||
.build();
|
||||
if (fileEntity.getStatus().equals(FileStatus.DELETED)) {
|
||||
fileEntityService.deleteFromDisk(fileEntity);
|
||||
response = DeleteFileResponse.builder()
|
||||
.fileId(fileRequest.getFileId())
|
||||
.message("File deleted from disk")
|
||||
.build();
|
||||
} else {
|
||||
fileEntityService.markAsDeleted(fileEntity);
|
||||
response = DeleteFileResponse.builder()
|
||||
.fileId(fileRequest.getFileId())
|
||||
.message("File marked as deleted")
|
||||
.build();
|
||||
}
|
||||
|
||||
return new BaseResponse(request.getMsgId(),
|
||||
MessageCode.SUCCESS.getCode(),
|
||||
"File marked as deleted",
|
||||
response);
|
||||
|
||||
} catch (NotFoundAuthToken e) {
|
||||
return new BaseResponse(request.getMsgId(),
|
||||
MessageCode.INVALID_TOKEN.getCode(),
|
||||
|
||||
@@ -3,9 +3,11 @@ package ru.soune.nocopy.handler;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.validation.BeanPropertyBindingResult;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.FieldError;
|
||||
import ru.soune.nocopy.dto.*;
|
||||
import ru.soune.nocopy.dto.file.*;
|
||||
import ru.soune.nocopy.entity.AuthToken;
|
||||
@@ -17,21 +19,26 @@ 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;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@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;
|
||||
|
||||
@Value("${file.storage.max-file-size}")
|
||||
private long maxFileSize;
|
||||
|
||||
@Override
|
||||
public BaseResponse handle(BaseRequest request) {
|
||||
try {
|
||||
@@ -45,18 +52,18 @@ public class FileUploadHandler implements RequestHandler {
|
||||
return handleInitUpload(request, fileRequest);
|
||||
case "file_types":
|
||||
return handleGetFileTypes(request);
|
||||
case "file_extension":
|
||||
return handleGetFileExtensions(fileRequest, 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"))
|
||||
.availableActions(Arrays.asList("init", "file_types", "chunks", "cancel", "progress"))
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_ACTION.getCode(),
|
||||
@@ -70,18 +77,31 @@ public class FileUploadHandler implements RequestHandler {
|
||||
}
|
||||
|
||||
private BaseResponse handleInitUpload(BaseRequest request, FileUploadRequest fileRequest) {
|
||||
try {
|
||||
String token = fileRequest.getToken();
|
||||
|
||||
Optional<AuthToken> tokenOptional = authTokenRepository.findByToken(token);
|
||||
|
||||
if (tokenOptional.isEmpty()) {
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
|
||||
MessageCode.INVALID_TOKEN.getDescription(), Map.of("token", 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());
|
||||
Map<String, String> fieldErrors = bindingResult.getFieldErrors()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
FieldError::getField,
|
||||
fieldError -> fieldError.getDefaultMessage() != null
|
||||
? fieldError.getDefaultMessage()
|
||||
: "Validation error"));
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_FIELD.getCode(),
|
||||
MessageCode.INVALID_FIELD.getDescription(), fieldErrors);
|
||||
}
|
||||
|
||||
FileUploadSession session = fileUploadService.initUpload(
|
||||
@@ -101,15 +121,6 @@ public class FileUploadHandler implements RequestHandler {
|
||||
|
||||
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) {
|
||||
@@ -240,4 +251,24 @@ public class FileUploadHandler implements RequestHandler {
|
||||
"Failed to get file types", null);
|
||||
}
|
||||
}
|
||||
|
||||
private BaseResponse handleGetFileExtensions(FileUploadRequest fileRequest, BaseRequest request) {
|
||||
try {
|
||||
FileType fileType = FileType.valueOf(fileRequest.getFileType().toUpperCase());
|
||||
List<String> allowedExtensions = fileType.getAllowedExtensions();
|
||||
|
||||
FileExtensionResponse response = FileExtensionResponse.builder()
|
||||
.extension(allowedExtensions)
|
||||
.count(allowedExtensions.size())
|
||||
.maxFileSize(maxFileSize)
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
||||
MessageCode.SUCCESS.getDescription(), response);
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting file extensions", e);
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(),
|
||||
"Failed to get file extensions", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,12 @@ import ru.soune.nocopy.dto.file.FileUploadRequest;
|
||||
import ru.soune.nocopy.entity.file.FileType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class FileUploadRequestValidator implements Validator {
|
||||
private final List<FileType> supportedFileTypes = Arrays.asList(FileType.values());
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> clazz) {
|
||||
return FileUploadRequest.class.isAssignableFrom(clazz);
|
||||
@@ -21,7 +24,7 @@ public class FileUploadRequestValidator implements Validator {
|
||||
|
||||
validateType(request.getFileType(), errors);
|
||||
validateFileName(request.getFileName(), errors);
|
||||
validateExtension(request.getExtension(), errors);
|
||||
validateExtension(request.getExtension(), request.getFileType(), errors);
|
||||
}
|
||||
|
||||
private void validateType(String fileType, Errors errors) {
|
||||
@@ -31,7 +34,12 @@ public class FileUploadRequestValidator implements Validator {
|
||||
}
|
||||
|
||||
try {
|
||||
FileType.valueOf(fileType.toUpperCase());
|
||||
FileType parsedType = FileType.valueOf(fileType.toUpperCase());
|
||||
|
||||
if (!supportedFileTypes.contains(parsedType)) {
|
||||
errors.rejectValue("fileType", "fileType.unsupported",
|
||||
"Unsupported file type. Valid types: " + Arrays.toString(FileType.values()));
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
errors.rejectValue("fileType", "fileType.invalid",
|
||||
"Invalid file type. Valid types: " + Arrays.toString(FileType.values()));
|
||||
@@ -50,10 +58,34 @@ public class FileUploadRequestValidator implements Validator {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateExtension(String extension, Errors errors) {
|
||||
private void validateExtension(String extension, String fileType, Errors errors) {
|
||||
if (extension == null || extension.isBlank()) {
|
||||
errors.rejectValue("extension", "extension.required", "Extension is required");
|
||||
errors.rejectValue("fileType", "fileType.required", "File type is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (extension.contains(".")) {
|
||||
errors.rejectValue("extension", "extension.required", "Extension contains comma");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FileType parsedType = FileType.valueOf(fileType.toUpperCase());
|
||||
|
||||
if (!parsedType.supportsExtension(extension)) {
|
||||
errors.rejectValue("fileName", "fileType.extension.mismatch",
|
||||
String.format("File extension '%s' does not match file type '%s'. Allowed extensions for %s: %s",
|
||||
extension, parsedType.getDisplayName(), parsedType.getDisplayName(),
|
||||
parsedType.getAllowedExtensions()));
|
||||
}
|
||||
|
||||
} catch (IllegalArgumentException e) {
|
||||
errors.rejectValue("fileType", "fileType.invalid",
|
||||
"Invalid file type. Valid types: " + Arrays.toString(FileType.values()));
|
||||
}
|
||||
}
|
||||
|
||||
private String getFileExtension(String fileName) {
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -40,7 +41,6 @@ public class FileEntityService {
|
||||
|
||||
long fileSize = Files.size(filePath);
|
||||
String originalName = session.getFileName();
|
||||
String extension = extractFileExtension(originalName);
|
||||
String storedName = filePath.getFileName().toString();
|
||||
|
||||
FileEntity fileEntity = FileEntity.builder()
|
||||
@@ -50,7 +50,7 @@ public class FileEntityService {
|
||||
.filePath(session.getFilePath())
|
||||
.fileSize(fileSize)
|
||||
.mimeType(session.getFileType())
|
||||
.fileExtension(extension)
|
||||
.fileExtension(session.getExtension())
|
||||
.checksum(checksum)
|
||||
.uploadSessionId(session.getUploadId())
|
||||
.status(FileStatus.ACTIVE)
|
||||
@@ -168,15 +168,24 @@ public class FileEntityService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void markAsDeleted(String fileId) {
|
||||
FileEntity fileEntity = fileEntityRepository.findById(fileId)
|
||||
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
|
||||
|
||||
public void markAsDeleted(FileEntity fileEntity) {
|
||||
fileEntity.setStatus(FileStatus.DELETED);
|
||||
fileEntity.setUpdatedAt(LocalDateTime.now());
|
||||
fileEntityRepository.save(fileEntity);
|
||||
|
||||
log.info("FileEntity marked as deleted: {}", fileId);
|
||||
fileEntityRepository.save(fileEntity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public boolean deleteFromDisk(FileEntity fileEntity) throws IOException {
|
||||
Path path = Paths.get(fileEntity.getFilePath());
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Files.delete(path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@@ -217,7 +226,7 @@ public class FileEntityService {
|
||||
.build();
|
||||
}
|
||||
|
||||
private String formatFileSize(long size) {
|
||||
public String formatFileSize(long size) {
|
||||
if (size < 1024) {
|
||||
return size + " B";
|
||||
} else if (size < 1024 * 1024) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package ru.soune.nocopy.service.file;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import ru.soune.nocopy.dto.file.FileInfoUserResponse;
|
||||
import ru.soune.nocopy.entity.file.FileEntity;
|
||||
import ru.soune.nocopy.entity.file.FileStatus;
|
||||
import ru.soune.nocopy.entity.file.FileType;
|
||||
import ru.soune.nocopy.repository.FileEntityRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FileStatsService {
|
||||
private final FileEntityRepository fileEntityRepository;
|
||||
|
||||
public FileInfoUserResponse getUserFileStats(Long userId) {
|
||||
List<FileEntity> userFiles = fileEntityRepository.findByUserId(userId);
|
||||
return calculateStats(userFiles);
|
||||
}
|
||||
|
||||
public FileInfoUserResponse calculateStats(List<FileEntity> files) {
|
||||
return FileInfoUserResponse.builder()
|
||||
.allFileSize(calculateTotalSize(files))
|
||||
.fileCount(calculateTotalCount(files))
|
||||
.filesCheck(calculateByStatus(files, FileStatus.CHECKED))
|
||||
.filesViolation(calculateByStatus(files, FileStatus.VIOLATION))
|
||||
|
||||
.imagesSize(calculateMediaSize(files, FileType.IMAGE))
|
||||
.imagesCount(calculateMediaCount(files, FileType.IMAGE))
|
||||
.imagesCheck(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.CHECKED))
|
||||
.imagesViolations(calculateMediaByStatus(files, FileType.IMAGE, FileStatus.VIOLATION))
|
||||
|
||||
.videosSize(calculateMediaSize(files, FileType.VIDEO))
|
||||
.videosCount(calculateMediaCount(files, FileType.VIDEO))
|
||||
.videosCheck(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.CHECKED))
|
||||
.videosViolations(calculateMediaByStatus(files, FileType.VIDEO, FileStatus.VIOLATION))
|
||||
|
||||
.audiosSize(calculateMediaSize(files, FileType.AUDIO))
|
||||
.audiosCount(calculateMediaCount(files, FileType.AUDIO))
|
||||
.audiosCheck(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.CHECKED))
|
||||
.audiosViolations(calculateMediaByStatus(files, FileType.AUDIO, FileStatus.VIOLATION))
|
||||
.build();
|
||||
}
|
||||
private Long calculateTotalSize(List<FileEntity> files) {
|
||||
return files.stream()
|
||||
.filter(file -> file.getStatus() != FileStatus.DELETED)
|
||||
.mapToLong(FileEntity::getFileSize)
|
||||
.sum();
|
||||
}
|
||||
|
||||
private Integer calculateTotalCount(List<FileEntity> files) {
|
||||
return (int) files.stream()
|
||||
.filter(file -> file.getStatus() != FileStatus.DELETED)
|
||||
.count();
|
||||
}
|
||||
|
||||
private Integer calculateByStatus(List<FileEntity> files, FileStatus status) {
|
||||
return (int) files.stream()
|
||||
.filter(file -> file.getStatus() == status)
|
||||
.count();
|
||||
}
|
||||
|
||||
private Long calculateMediaSize(List<FileEntity> files, FileType fileType) {
|
||||
return files.stream()
|
||||
.filter(file -> file.getStatus() != FileStatus.DELETED)
|
||||
.filter(file -> isFileType(file, fileType))
|
||||
.mapToLong(FileEntity::getFileSize)
|
||||
.sum();
|
||||
}
|
||||
|
||||
private Integer calculateMediaCount(List<FileEntity> files, FileType fileType) {
|
||||
return (int) files.stream()
|
||||
.filter(file -> file.getStatus() != FileStatus.DELETED)
|
||||
.filter(file -> isFileType(file, fileType))
|
||||
.count();
|
||||
}
|
||||
|
||||
private Integer calculateMediaByStatus(List<FileEntity> files, FileType fileType, FileStatus status) {
|
||||
return (int) files.stream()
|
||||
.filter(file -> isFileType(file, fileType))
|
||||
.filter(file -> file.getStatus() == status)
|
||||
.count();
|
||||
}
|
||||
|
||||
private boolean isFileType(FileEntity file, FileType fileType) {
|
||||
if (file.getFileExtension() == null) return false;
|
||||
|
||||
String extension = file.getFileExtension().toLowerCase().replace(".", "");
|
||||
return fileType.supportsExtension(extension);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public interface FileUploadService {
|
||||
|
||||
UploadProgressResponse getUploadProgress(String uploadId);
|
||||
|
||||
void cleanupExpiredSessions();
|
||||
void handleExpiredSession(FileUploadSession session);
|
||||
|
||||
public void retryFailedUpload(String uploadId);
|
||||
|
||||
|
||||
@@ -102,12 +102,14 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
int totalChunks = (int) Math.ceil((double) fileSize / chunkSize);
|
||||
log.debug("File will be split into {} chunks (chunk size: {} bytes)",
|
||||
totalChunks, chunkSize);
|
||||
Long chunkSize = totalChunks == 1 ? fileSize : 1000000L;
|
||||
|
||||
FileUploadSession session = FileUploadSession.builder()
|
||||
.userId(userId)
|
||||
.extension(extension)
|
||||
.fileName(fileName)
|
||||
.fileType(fileType)
|
||||
.chunkSize(chunkSize)
|
||||
.fileSize(fileSize)
|
||||
.totalChunks(totalChunks)
|
||||
.chunksUploaded(0)
|
||||
@@ -143,17 +145,103 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
|
||||
if (chunkNumber < 0 || chunkNumber >= session.getTotalChunks()) {
|
||||
throw new FileUploadException(
|
||||
String.format("Invalid chunk number %d. Expected 0-%d",
|
||||
String.format("Invalid chunk number %d. Expected number: %d",
|
||||
chunkNumber, session.getTotalChunks() - 1));
|
||||
}
|
||||
|
||||
if (chunkFile.getSize() > chunkSize) {
|
||||
if (chunkNumber == 0 && chunkFile.getSize() > chunkSize || chunkNumber + 1 == (session.getTotalChunks())
|
||||
&& chunkFile.getSize() > chunkSize) {
|
||||
throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize);
|
||||
}
|
||||
|
||||
if (chunkNumber > 0 && chunkNumber + 1 < session.getTotalChunks() && chunkFile.getSize() != chunkSize) {
|
||||
throw new ChunkSizeExceededException(chunkFile.getSize(), chunkSize);
|
||||
}
|
||||
|
||||
return processChunk(session, chunkNumber, chunkFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void handleExpiredSession(FileUploadSession session) {
|
||||
session.setStatus(UploadStatus.FAILED);
|
||||
session.setLastError("Upload session expired");
|
||||
sessionRepository.save(session);
|
||||
|
||||
CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
|
||||
}
|
||||
|
||||
@Async("fileUploadTaskExecutor")
|
||||
@Transactional
|
||||
public void assembleFileAsync(FileUploadSession session) {
|
||||
try {
|
||||
assembleFile(session);
|
||||
log.info("File assembly completed successfully for session: {}",
|
||||
session.getUploadId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to assemble file for session {}: {}",
|
||||
session.getUploadId(), e.getMessage(), e);
|
||||
|
||||
handleAssemblyFailure(session, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void cancelUpload(String uploadId) {
|
||||
FileUploadSession session = sessionRepository.findById(uploadId)
|
||||
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
|
||||
|
||||
if (session.getStatus() == UploadStatus.COMPLETED || session.getStatus() == UploadStatus.CANCELLED) {
|
||||
throw new FileUploadException("Cannot cancel completed or cancelled upload");
|
||||
}
|
||||
|
||||
session.setStatus(UploadStatus.CANCELLED);
|
||||
sessionRepository.save(session);
|
||||
|
||||
CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
|
||||
|
||||
log.info("Upload cancelled: {}", uploadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void retryFailedUpload(String uploadId) {
|
||||
FileUploadSession session = sessionRepository.findById(uploadId)
|
||||
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
|
||||
|
||||
if (session.getStatus() != UploadStatus.FAILED) {
|
||||
throw new FileUploadException("Only failed uploads can be retried");
|
||||
}
|
||||
|
||||
if (session.getRetryCount() >= maxRetryAttempts) {
|
||||
throw new FileUploadException("Max retry attempts exceeded");
|
||||
}
|
||||
|
||||
session.setStatus(UploadStatus.UPLOADING);
|
||||
session.setRetryCount(session.getRetryCount() + 1);
|
||||
session.setLastError(null);
|
||||
|
||||
sessionRepository.save(session);
|
||||
|
||||
if (session.getChunksUploaded().equals(session.getTotalChunks())) {
|
||||
log.info("Retrying file assembly for session: {}", uploadId);
|
||||
assembleFileAsync(session);
|
||||
}
|
||||
|
||||
log.info("Upload retry initiated for session: {} (attempt {})",
|
||||
uploadId, session.getRetryCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadProgressResponse getUploadProgress(String uploadId) {
|
||||
FileUploadSession session = sessionRepository.findById(uploadId)
|
||||
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
|
||||
|
||||
return UploadProgressResponse.fromSession(session);
|
||||
}
|
||||
|
||||
private void validateSession(FileUploadSession session) {
|
||||
UploadStatus status = session.getStatus();
|
||||
|
||||
@@ -183,14 +271,6 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleExpiredSession(FileUploadSession session) {
|
||||
session.setStatus(UploadStatus.FAILED);
|
||||
session.setLastError("Upload session expired");
|
||||
sessionRepository.save(session);
|
||||
|
||||
CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
|
||||
}
|
||||
|
||||
private UploadProgressResponse processChunk(FileUploadSession session,
|
||||
Integer chunkNumber,
|
||||
MultipartFile chunkFile) {
|
||||
@@ -206,6 +286,7 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
session.getChunkPaths().put(chunkNumber, chunkPath);
|
||||
session.setChunksUploaded(session.getChunksUploaded() + 1);
|
||||
session.setStatus(UploadStatus.UPLOADING);
|
||||
session.setExpiresAt(LocalDateTime.now().plusMinutes(1));
|
||||
|
||||
log.debug("Chunk {} saved successfully. Uploaded: {}/{}",
|
||||
chunkNumber, session.getChunksUploaded(), session.getTotalChunks());
|
||||
@@ -316,52 +397,6 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
@Async("fileUploadTaskExecutor")
|
||||
@Transactional
|
||||
public void assembleFileAsync(FileUploadSession session) {
|
||||
try {
|
||||
assembleFile(session);
|
||||
log.info("File assembly completed successfully for session: {}",
|
||||
session.getUploadId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to assemble file for session {}: {}",
|
||||
session.getUploadId(), e.getMessage(), e);
|
||||
|
||||
handleAssemblyFailure(session, e);
|
||||
}
|
||||
}
|
||||
|
||||
// private void assembleFile(FileUploadSession session) throws IOException {
|
||||
// log.info("Starting file assembly for session: {} ({})",
|
||||
// session.getUploadId(), session.getFileName());
|
||||
//
|
||||
// Path finalFilePath = null;
|
||||
//
|
||||
// try {
|
||||
// finalFilePath = prepareFinalFile(session);
|
||||
//
|
||||
// validateAllChunksExist(session);
|
||||
//
|
||||
// mergeChunksToFile(session, finalFilePath);
|
||||
//
|
||||
// validateFinalFile(session, finalFilePath);
|
||||
//
|
||||
// updateSessionOnSuccess(session, finalFilePath);
|
||||
//
|
||||
// cleanupSessionFiles(session);
|
||||
//
|
||||
// log.info("File assembly completed: {} -> {} ({} bytes)",
|
||||
// session.getFileName(), finalFilePath, session.getFileSize());
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// if (finalFilePath != null) {
|
||||
// Files.deleteIfExists(finalFilePath);
|
||||
// }
|
||||
// throw e;
|
||||
// }
|
||||
// }
|
||||
|
||||
private void assembleFile(FileUploadSession session) throws IOException {
|
||||
log.info("Starting file assembly for session: {} ({})",
|
||||
session.getUploadId(), session.getFileName());
|
||||
@@ -526,14 +561,6 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
log.debug("Final file checksum: {}", checksum);
|
||||
}
|
||||
|
||||
private void updateSessionOnSuccess(FileUploadSession session, Path finalPath) {
|
||||
session.setFilePath(finalPath.toString());
|
||||
session.setChecksum(calculateChecksum(finalPath));
|
||||
session.setStatus(UploadStatus.COMPLETED);
|
||||
session.setCompletedAt(LocalDateTime.now());
|
||||
sessionRepository.save(session);
|
||||
}
|
||||
|
||||
private void cleanupSessionFiles(FileUploadSession session) {
|
||||
try {
|
||||
Path chunkDir = getChunkDirectory(session.getUserId(), session.getUploadId());
|
||||
@@ -570,90 +597,37 @@ public class FileUploadServiceImpl implements FileUploadService {
|
||||
session.getUploadId(), e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void cancelUpload(String uploadId) {
|
||||
FileUploadSession session = sessionRepository.findById(uploadId)
|
||||
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
|
||||
|
||||
if (session.getStatus() == UploadStatus.COMPLETED || session.getStatus() == UploadStatus.CANCELLED) {
|
||||
throw new FileUploadException("Cannot cancel completed or cancelled upload");
|
||||
}
|
||||
|
||||
session.setStatus(UploadStatus.CANCELLED);
|
||||
sessionRepository.save(session);
|
||||
|
||||
CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
|
||||
|
||||
log.info("Upload cancelled: {}", uploadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void retryFailedUpload(String uploadId) {
|
||||
FileUploadSession session = sessionRepository.findById(uploadId)
|
||||
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
|
||||
|
||||
if (session.getStatus() != UploadStatus.FAILED) {
|
||||
throw new FileUploadException("Only failed uploads can be retried");
|
||||
}
|
||||
|
||||
if (session.getRetryCount() >= maxRetryAttempts) {
|
||||
throw new FileUploadException("Max retry attempts exceeded");
|
||||
}
|
||||
|
||||
session.setStatus(UploadStatus.UPLOADING);
|
||||
session.setRetryCount(session.getRetryCount() + 1);
|
||||
session.setLastError(null);
|
||||
|
||||
sessionRepository.save(session);
|
||||
|
||||
if (session.getChunksUploaded().equals(session.getTotalChunks())) {
|
||||
log.info("Retrying file assembly for session: {}", uploadId);
|
||||
assembleFileAsync(session);
|
||||
}
|
||||
|
||||
log.info("Upload retry initiated for session: {} (attempt {})",
|
||||
uploadId, session.getRetryCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadProgressResponse getUploadProgress(String uploadId) {
|
||||
FileUploadSession session = sessionRepository.findById(uploadId)
|
||||
.orElseThrow(() -> new UploadSessionNotFoundException(uploadId));
|
||||
|
||||
return UploadProgressResponse.fromSession(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void cleanupExpiredSessions() {
|
||||
log.info("Starting cleanup of expired upload sessions");
|
||||
|
||||
LocalDateTime expiryThreshold = LocalDateTime.now().minusHours(sessionExpiryHours);
|
||||
|
||||
try {
|
||||
var expiredSessions = sessionRepository.findByStatusInAndCreatedAtBefore(
|
||||
Set.of(UploadStatus.INITIATED, UploadStatus.UPLOADING, UploadStatus.FAILED),
|
||||
expiryThreshold
|
||||
);
|
||||
|
||||
int cleanedCount = 0;
|
||||
for (FileUploadSession session : expiredSessions) {
|
||||
session.setStatus(UploadStatus.FAILED);
|
||||
session.setLastError("Session expired during cleanup");
|
||||
sessionRepository.save(session);
|
||||
|
||||
CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
|
||||
cleanedCount++;
|
||||
}
|
||||
|
||||
log.info("Cleaned up {} expired upload sessions", cleanedCount);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error during session cleanup", e);
|
||||
}
|
||||
}
|
||||
// @Override
|
||||
// @Transactional
|
||||
// public void cleanupExpiredSessions() {
|
||||
// log.info("Starting cleanup of expired upload sessions");
|
||||
//
|
||||
// LocalDateTime expiryThreshold = LocalDateTime.now().minusHours(sessionExpiryHours);
|
||||
//
|
||||
// try {
|
||||
// var expiredSessions = sessionRepository.findByStatusInAndCreatedAtBefore(
|
||||
// Set.of(UploadStatus.INITIATED, UploadStatus.UPLOADING, UploadStatus.FAILED),
|
||||
// expiryThreshold
|
||||
// );
|
||||
//
|
||||
// int cleanedCount = 0;
|
||||
// for (FileUploadSession session : expiredSessions) {
|
||||
// session.setStatus(UploadStatus.FAILED);
|
||||
// session.setLastError("Session expired during cleanup");
|
||||
// sessionRepository.save(session);
|
||||
//
|
||||
// CompletableFuture.runAsync(() -> cleanupSessionFiles(session));
|
||||
// cleanedCount++;
|
||||
// }
|
||||
//
|
||||
// log.info("Cleaned up {} expired upload sessions", cleanedCount);
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// log.error("Error during session cleanup", e);
|
||||
// }
|
||||
// }
|
||||
|
||||
private Path getChunkDirectory(Long userId, String uploadId) {
|
||||
return storageRoot.resolve("temp")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package ru.soune.nocopy.service.file;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.entity.file.FileUploadSession;
|
||||
import ru.soune.nocopy.entity.file.UploadStatus;
|
||||
import ru.soune.nocopy.repository.FileUploadSessionRepository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UploadSessionCleanupService {
|
||||
@Autowired
|
||||
private FileUploadSessionRepository fileUploadSessionRepository;
|
||||
|
||||
@Autowired
|
||||
private FileUploadService fileUploadService;
|
||||
|
||||
@Transactional
|
||||
@Scheduled(fixedDelay = 30000)
|
||||
public void cleanupSessions() {
|
||||
List<FileUploadSession> expiredSessions = fileUploadSessionRepository.findExpiredSessions(
|
||||
LocalDateTime.now(), Set.of(UploadStatus.INITIATED, UploadStatus.UPLOADING));
|
||||
|
||||
for (FileUploadSession session : expiredSessions) {
|
||||
fileUploadService.handleExpiredSession(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user