4 Commits
Author SHA1 Message Date
vladp f3aa6324c2 add fileentity controller
Test Workflow / test (push) Successful in 3s
2025-12-18 01:13:19 +07:00
vladp e2747fc999 Merge branch 'NCBACK-18' into NCBACK-15
# Conflicts:
#	src/main/java/ru/soune/nocopy/controller/AuthController.java
#	src/main/java/ru/soune/nocopy/controller/file/FileUploadController.java
#	src/main/java/ru/soune/nocopy/dto/AuthResponse.java
#	src/main/java/ru/soune/nocopy/dto/LoginResponse.java
#	src/main/java/ru/soune/nocopy/dto/RegisterRequest.java
#	src/main/java/ru/soune/nocopy/exception/UserAlreadyExistsException.java
#	src/main/java/ru/soune/nocopy/service/AuthService.java
#	src/test/java/ru/soune/nocopy/service/AuthServiceTest.java
2025-12-17 22:41:01 +07:00
vladp 7e2dd4a654 fix path
Test Workflow / test (push) Successful in 3s
2025-12-17 22:37:46 +07:00
vladp 9e8c7f0fa1 add fileentity
Test Workflow / test (push) Successful in 4s
2025-12-17 01:19:48 +07:00
20 changed files with 1117 additions and 88 deletions
@@ -0,0 +1,28 @@
package ru.soune.nocopy.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.soune.nocopy.handler.*;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class HandlerConfig {
@Bean
public Map<Integer, RequestHandler> handlers(
RegRequestHandler reg,
LoginRequestHandler login,
FileUploadHandler upload,
FileEntityHandler file
) {
Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login);
map.put(20002, reg);
map.put(20004, upload);
map.put(20005, file);
return map;
}
}
@@ -1,9 +1,7 @@
package ru.soune.nocopy.controller; package ru.soune.nocopy.controller;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
@@ -19,10 +17,7 @@ import ru.soune.nocopy.dto.file.UploadProgress;
import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.entity.file.UploadStatus;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.handler.FileUploadHandler; import ru.soune.nocopy.handler.*;
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 ru.soune.nocopy.service.file.FileUploadService;
import java.util.HashMap; import java.util.HashMap;
@@ -35,21 +30,10 @@ import java.util.stream.Collectors;
@RequestMapping("/api") @RequestMapping("/api")
@RequiredArgsConstructor @RequiredArgsConstructor
public class ApiController { public class ApiController {
private final ApplicationContext applicationContext;
private final FileUploadService fileUploadService; private final FileUploadService fileUploadService;
private Map<Integer, RequestHandler> handlers = new HashMap<>(); private final Map<Integer, RequestHandler> handlers;
@PostConstruct
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);
}
@PostMapping("/v{version}/data") @PostMapping("/v{version}/data")
public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request, public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request,
@@ -0,0 +1,172 @@
package ru.soune.nocopy.controller.file;
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.web.bind.annotation.*;
import ru.soune.nocopy.dto.file.FileEntityResponse;
import ru.soune.nocopy.entity.AuthToken;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.service.file.FileEntityService;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Slf4j
@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
public class FileDownloadController {
private final FileEntityService fileEntityService;
private final AuthTokenRepository authTokenRepository;
/**
* Скачать файл по ID FileEntity
*/
@GetMapping("/download/{fileId}")
public ResponseEntity<Resource> downloadFile(
@PathVariable String fileId,
@RequestHeader("Authorization") String tokenHeader) {
try {
Long userId = getUserIdFromToken(tokenHeader);
FileEntityResponse fileInfo = fileEntityService.getById(fileId);
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("/preview/{fileId}")
public ResponseEntity<Resource> previewFile(
@PathVariable String fileId,
@RequestHeader("Authorization") String tokenHeader) {
try {
Long userId = getUserIdFromToken(tokenHeader);
FileEntityResponse fileInfo = fileEntityService.getById(fileId);
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();
}
}
/**
* Получить информацию о файле для фронтенда
*/
@GetMapping("/info/{fileId}")
public ResponseEntity<FileEntityResponse> getFileInfo(
@PathVariable String fileId,
@RequestHeader("Authorization") String tokenHeader) {
try {
Long userId = getUserIdFromToken(tokenHeader);
FileEntityResponse fileInfo = fileEntityService.getById(fileId);
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();
}
}
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();
}
}
@@ -1,65 +0,0 @@
package ru.soune.nocopy.controller.file;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.FileApiResponse;
import ru.soune.nocopy.dto.file.InitUploadRequest;
import ru.soune.nocopy.dto.file.UploadProgressResponse;
import ru.soune.nocopy.entity.AuthToken;
import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.file.FileUploadSession;
import ru.soune.nocopy.entity.file.UploadStatus;
import ru.soune.nocopy.exception.FileUploadException;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.exception.UploadSessionNotFoundException;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository;
import ru.soune.nocopy.service.file.FileUploadService;
import java.util.*;
@Slf4j
@RestController
@RequestMapping("/api/files")
@RequiredArgsConstructor
public class FileUploadController {
private final FileUploadService fileUploadService;
@PostMapping("/chunk")
public ResponseEntity<FileApiResponse<UploadProgressResponse>> uploadChunk(
@RequestParam("uploadId") String uploadId,
@RequestParam("chunkNumber") Integer chunkNumber,
@RequestParam("chunk") MultipartFile chunk) {
log.info("Uploading chunk {} for session {}, file size: {} bytes",
chunkNumber, uploadId, chunk.getSize());
if (chunk.isEmpty()) {
return ResponseEntity.badRequest()
.body(FileApiResponse.error("Chunk file is empty"));
}
if (uploadId == null || uploadId.isBlank()) {
return ResponseEntity.badRequest()
.body(FileApiResponse.error("Upload ID is required"));
}
try {
UploadProgressResponse progress = fileUploadService.uploadChunk(
uploadId, chunkNumber, chunk);
return ResponseEntity.ok(FileApiResponse.success(progress));
} catch (Exception e) {
log.error("Error uploading chunk", e);
return ResponseEntity.badRequest()
.body(FileApiResponse.error(e.getMessage()));
}
}
}
@@ -0,0 +1,10 @@
package ru.soune.nocopy.dto;
import java.time.LocalDateTime;
public record AuthResponse (
boolean success,
String message,
String token,
LocalDateTime expiresAt
) {}
@@ -11,7 +11,8 @@ public enum MessageCode {
INVALID_JSON_BODY(2, "Invalid fields in JSON object"), INVALID_JSON_BODY(2, "Invalid fields in JSON object"),
INCOMPLETE_UPLOAD(2, "Not load all chunks"), INCOMPLETE_UPLOAD(2, "Not load all chunks"),
MSG_ID_NOT_FOUND(4, "Message id not found"), MSG_ID_NOT_FOUND(4, "Message id not found"),
FILE_ENTITY_ERROR(2, "File entity error"),
ACCESS_DENIED(2, "Access denied"),
AUTH_EMAIL_NOT_FOUND(4, "Email not found"), AUTH_EMAIL_NOT_FOUND(4, "Email not found"),
FILE_NOT_FOUND(4, "File not found"), FILE_NOT_FOUND(4, "File not found"),
AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"); AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match");
@@ -0,0 +1,19 @@
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
@AllArgsConstructor
@NoArgsConstructor
public class DeleteFileResponse {
@JsonProperty("file_id")
private String fileId;
@JsonProperty("message")
private String message;
}
@@ -0,0 +1,34 @@
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
@AllArgsConstructor
@NoArgsConstructor
public class FileEntityRequest {
@JsonProperty("action")
private String action;
@JsonProperty("file_id")
private String fileId;
@JsonProperty("upload_session_id")
private String uploadSessionId;
@JsonProperty("query")
private String query;
@JsonProperty("page")
private Integer page;
@JsonProperty("page_size")
private Integer pageSize;
@JsonProperty("token")
private String token;
}
@@ -0,0 +1,33 @@
package ru.soune.nocopy.dto.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import ru.soune.nocopy.entity.file.FileStatus;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileEntityResponse {
private String id;
private Long userId;
private String originalFileName;
private String storedFileName;
private String filePath;
private Long fileSize;
private String mimeType;
private String fileExtension;
private String checksum;
private String uploadSessionId;
private FileStatus status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private String formattedSize;
private String downloadUrl;
private boolean existsOnDisk;
}
@@ -0,0 +1,33 @@
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.List;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class FileListResponse {
@JsonProperty("files")
private List<FileEntityResponse> files;
@JsonProperty("total_count")
private Integer totalCount;
@JsonProperty("total_size")
private Long totalSize;
@JsonProperty("formatted_total_size")
private String formattedTotalSize;
@JsonProperty("page")
private Integer page;
@JsonProperty("page_size")
private Integer pageSize;
}
@@ -0,0 +1,22 @@
package ru.soune.nocopy.dto.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileResponse {
private List<FileEntityResponse> files;
private int totalCount;
private long totalSize;
private String formattedTotalSize;
private int page;
private int pageSize;
}
@@ -0,0 +1,22 @@
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
@AllArgsConstructor
@NoArgsConstructor
public class StorageUsageResponse {
@JsonProperty("user_id")
private Long userId;
@JsonProperty("storage_used")
private Long storageUsed;
@JsonProperty("formatted_storage_used")
private String formattedStorageUsed;
}
@@ -0,0 +1,78 @@
package ru.soune.nocopy.entity.file;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "file_entities")
@EntityListeners(AuditingEntityListener.class)
public class FileEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "original_file_name", nullable = false)
private String originalFileName;
@Column(name = "stored_file_name", nullable = false)
private String storedFileName;
@Column(name = "file_path", nullable = false, unique = true)
private String filePath;
@Column(name = "file_size", nullable = false)
private Long fileSize;
@Column(name = "mime_type")
private String mimeType;
@Column(name = "file_extension")
private String fileExtension;
@Column(name = "checksum")
private String checksum;
@Column(name = "upload_session_id")
private String uploadSessionId;
@Enumerated(EnumType.STRING)
@Column(name = "status")
private FileStatus status;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
public void prePersist() {
if (this.status == null) {
this.status = FileStatus.ACTIVE;
}
if (this.createdAt == null) {
this.createdAt = LocalDateTime.now();
}
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
@@ -0,0 +1,8 @@
package ru.soune.nocopy.entity.file;
public enum FileStatus {
ACTIVE,
DELETED,
PROCESSING,
ERROR
}
@@ -0,0 +1,11 @@
package ru.soune.nocopy.exception;
public class FileEntityNotFoundException extends RuntimeException {
public FileEntityNotFoundException(String fileId) {
super("FileEntity not found with ID: " + fileId);
}
public FileEntityNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,292 @@
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 ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.entity.AuthToken;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.service.file.FileEntityService;
import java.util.Arrays;
@Slf4j
@Component
@RequiredArgsConstructor
public class FileEntityHandler implements RequestHandler {
private final FileEntityService fileEntityService;
private final AuthTokenRepository authTokenRepository;
private final ObjectMapper objectMapper;
@Override
public BaseResponse handle(BaseRequest request) {
try {
FileEntityRequest fileRequest = objectMapper.convertValue(
request.getMessageBody(), FileEntityRequest.class);
String action = fileRequest.getAction();
switch (action) {
case "get_file_info":
return handleGetFileInfo(request, fileRequest);
case "get_file_by_session":
return handleGetFileBySession(request, fileRequest);
case "get_user_files":
return handleGetUserFiles(request, fileRequest);
case "search_files":
return handleSearchFiles(request, fileRequest);
case "get_storage_usage":
return handleGetStorageUsage(request, fileRequest);
case "delete_file":
return handleDeleteFile(request, fileRequest);
default:
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"))
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_ACTION.getCode(),
"Invalid action: " + action,
response);
}
} catch (Exception e) {
log.error("Error in FileEntityHandler", e);
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Handler error: " + e.getMessage(),
null);
}
}
private BaseResponse handleGetFileInfo(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
FileEntityResponse fileInfo = fileEntityService.getById(fileRequest.getFileId());
if (!fileInfo.getUserId().equals(userId)) {
return new BaseResponse(request.getMsgId(),
MessageCode.ACCESS_DENIED.getCode(),
"Access denied",
null);
}
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
fileInfo);
} catch (NotFoundAuthToken e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_TOKEN.getCode(),
"Authentication required",
null);
} catch (Exception e) {
log.error("Error getting file info", e);
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Failed to get file info: " + e.getMessage(),
null);
}
}
private BaseResponse handleGetFileBySession(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
FileEntityResponse fileInfo = fileEntityService.getByUploadSessionId(fileRequest.getUploadSessionId());
if (!fileInfo.getUserId().equals(userId)) {
return new BaseResponse(request.getMsgId(),
MessageCode.ACCESS_DENIED.getCode(),
"Access denied",
null);
}
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
fileInfo);
} catch (NotFoundAuthToken e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_TOKEN.getCode(),
"Authentication required",
null);
} catch (Exception e) {
log.error("Error getting file by session", e);
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Failed to get file: " + e.getMessage(),
null);
}
}
private BaseResponse handleGetUserFiles(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
FileResponse files = fileEntityService.getUserFiles(userId, page, pageSize);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
files);
} catch (NotFoundAuthToken e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_TOKEN.getCode(),
"Authentication required",
null);
} catch (Exception e) {
log.error("Error getting user files", e);
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Failed to get files: " + e.getMessage(),
null);
}
}
private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
if (fileRequest.getQuery() == null || fileRequest.getQuery().isBlank()) {
return handleGetUserFiles(request, fileRequest);
}
FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000);
var filteredFiles = allFiles.getFiles().stream()
.filter(f -> f.getOriginalFileName().toLowerCase().contains(fileRequest.getQuery().toLowerCase()))
.toList();
int start = (page - 1) * pageSize;
int end = Math.min(start + pageSize, filteredFiles.size());
FileListResponse response = FileListResponse.builder()
.files(filteredFiles.subList(start, Math.min(end, filteredFiles.size())))
.totalCount(filteredFiles.size())
.totalSize(allFiles.getTotalSize())
.formattedTotalSize(allFiles.getFormattedTotalSize())
.page(page)
.pageSize(pageSize)
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
} catch (NotFoundAuthToken e) {
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);
}
}
private BaseResponse handleGetStorageUsage(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
long usage = fileEntityService.getUserStorageUsed(userId);
StorageUsageResponse response = StorageUsageResponse.builder()
.userId(userId)
.storageUsed(usage)
.formattedStorageUsed(formatFileSize(usage))
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
} catch (NotFoundAuthToken e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_TOKEN.getCode(),
"Authentication required",
null);
} catch (Exception e) {
log.error("Error getting storage usage", e);
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Failed to get storage usage: " + e.getMessage(),
null);
}
}
private BaseResponse handleDeleteFile(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = getUserIdFromToken(fileRequest.getToken());
FileEntityResponse fileInfo = fileEntityService.getById(fileRequest.getFileId());
if (!fileInfo.getUserId().equals(userId)) {
return new BaseResponse(request.getMsgId(),
MessageCode.ACCESS_DENIED.getCode(),
"Access denied",
null);
}
fileEntityService.markAsDeleted(fileRequest.getFileId());
DeleteFileResponse 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(),
"Authentication required",
null);
} catch (Exception e) {
log.error("Error deleting file", e);
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Failed to delete file: " + e.getMessage(),
null);
}
}
private Long getUserIdFromToken(String token) {
if (token == null || token.isBlank()) {
throw new NotFoundAuthToken("Token is required");
}
if (token.startsWith("Bearer ")) {
token = token.replace("Bearer ", "");
}
AuthToken authToken = authTokenRepository.findByToken(token)
.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
return authToken.getUser().getId();
}
private String formatFileSize(long size) {
if (size < 1024) return size + " B";
int exp = (int) (Math.log(size) / Math.log(1024));
String pre = "KMGTPE".charAt(exp-1) + "";
return String.format("%.1f %sB", size / Math.pow(1024, exp), pre);
}
}
@@ -0,0 +1,33 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import java.util.List;
import java.util.Optional;
@Repository
public interface FileEntityRepository extends JpaRepository<FileEntity, String> {
List<FileEntity> findByUserId(Long userId);
List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status);
Optional<FileEntity> findByFilePath(String filePath);
Optional<FileEntity> findByUploadSessionId(String uploadSessionId);
boolean existsByFilePath(String filePath);
@Query("SELECT SUM(f.fileSize) FROM FileEntity f WHERE f.userId = :userId AND f.status = 'ACTIVE'")
Long getTotalSizeByUserId(@Param("userId") Long userId);
@Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.originalFileName LIKE %:keyword%")
List<FileEntity> searchByFileName(@Param("userId") Long userId, @Param("keyword") String keyword);
long countByUserId(Long userId);
}
@@ -0,0 +1,241 @@
package ru.soune.nocopy.service.file;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.file.FileEntityResponse;
import ru.soune.nocopy.dto.file.FileResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileUploadSession;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.repository.FileEntityRepository;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class FileEntityService {
private final FileEntityRepository fileEntityRepository;
@Transactional
public FileEntity createFromUploadSession(FileUploadSession session, String checksum) {
log.info("Creating FileEntity for upload session: {}", session.getUploadId());
try {
Path filePath = Paths.get(session.getFilePath());
if (!Files.exists(filePath)) {
throw new IOException("File not found on disk: " + filePath);
}
long fileSize = Files.size(filePath);
String originalName = session.getFileName();
String extension = extractFileExtension(originalName);
String storedName = filePath.getFileName().toString();
FileEntity fileEntity = FileEntity.builder()
.userId(session.getUserId())
.originalFileName(originalName)
.storedFileName(storedName)
.filePath(session.getFilePath())
.fileSize(fileSize)
.mimeType(session.getFileType())
.fileExtension(extension)
.checksum(checksum)
.uploadSessionId(session.getUploadId())
.status(FileStatus.ACTIVE)
.build();
FileEntity saved = fileEntityRepository.save(fileEntity);
log.info("FileEntity created successfully: {} (size: {} bytes)",
saved.getId(), fileSize);
return saved;
} catch (IOException e) {
log.error("Failed to create FileEntity for session {}: {}",
session.getUploadId(), e.getMessage(), e);
throw new RuntimeException("Failed to create file entity: " + e.getMessage(), e);
}
}
private String extractFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0 && dotIndex < fileName.length() - 1) {
return fileName.substring(dotIndex + 1).toLowerCase();
}
return "";
}
@Transactional(readOnly = true)
public FileEntityResponse getById(String fileId) {
FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
return convertToResponse(fileEntity);
}
@Transactional(readOnly = true)
public FileEntityResponse getByUploadSessionId(String uploadSessionId) {
FileEntity fileEntity = fileEntityRepository.findByUploadSessionId(uploadSessionId)
.orElseThrow(() -> new FileEntityNotFoundException(
"Not found for upload session: " + uploadSessionId));
return convertToResponse(fileEntity);
}
@Transactional(readOnly = true)
public FileEntityResponse getByFilePath(String filePath) {
FileEntity fileEntity = fileEntityRepository.findByFilePath(filePath)
.orElseThrow(() -> new FileEntityNotFoundException("Path: " + filePath));
return convertToResponse(fileEntity);
}
@Transactional(readOnly = true)
public FileResponse getAllUserFiles(Long userId) {
List<FileEntity> fileEntities = fileEntityRepository.findByUserIdAndStatus(
userId, FileStatus.ACTIVE);
List<FileEntityResponse> files = fileEntities.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
long totalSize = fileEntities.stream()
.mapToLong(FileEntity::getFileSize)
.sum();
return FileResponse.builder()
.files(files)
.totalCount(files.size())
.totalSize(totalSize)
.formattedTotalSize(formatFileSize(totalSize))
.page(1)
.pageSize(files.size())
.build();
}
@Transactional(readOnly = true)
public FileResponse getUserFiles(Long userId, int page, int pageSize) {
List<FileEntity> allFiles = fileEntityRepository.findByUserIdAndStatus(
userId, FileStatus.ACTIVE);
int start = (page - 1) * pageSize;
int end = Math.min(start + pageSize, allFiles.size());
if (start >= allFiles.size()) {
return FileResponse.builder()
.files(List.of())
.totalCount(allFiles.size())
.totalSize(0)
.formattedTotalSize("0 B")
.page(page)
.pageSize(pageSize)
.build();
}
List<FileEntity> pageFiles = allFiles.subList(start, end);
List<FileEntityResponse> files = pageFiles.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
long totalSize = allFiles.stream()
.mapToLong(FileEntity::getFileSize)
.sum();
return FileResponse.builder()
.files(files)
.totalCount(allFiles.size())
.totalSize(totalSize)
.formattedTotalSize(formatFileSize(totalSize))
.page(page)
.pageSize(pageSize)
.build();
}
@Transactional
public void markAsDeleted(String fileId) {
FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
fileEntity.setStatus(FileStatus.DELETED);
fileEntity.setUpdatedAt(LocalDateTime.now());
fileEntityRepository.save(fileEntity);
log.info("FileEntity marked as deleted: {}", fileId);
}
@Transactional(readOnly = true)
public long getUserStorageUsed(Long userId) {
Long totalSize = fileEntityRepository.getTotalSizeByUserId(userId);
return totalSize != null ? totalSize : 0L;
}
@Transactional(readOnly = true)
public List<FileEntityResponse> searchFiles(Long userId, String query) {
List<FileEntity> files = fileEntityRepository.searchByFileName(userId, query);
return files.stream()
.filter(f -> f.getStatus() == FileStatus.ACTIVE)
.map(this::convertToResponse)
.collect(Collectors.toList());
}
private boolean checkFileExistsOnDisk(String filePath) {
try {
return Files.exists(Paths.get(filePath));
} catch (Exception e) {
log.warn("Error checking file existence: {}", filePath, e);
return false;
}
}
private FileEntityResponse convertToResponse(FileEntity fileEntity) {
boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath());
return FileEntityResponse.builder()
.id(fileEntity.getId())
.userId(fileEntity.getUserId())
.originalFileName(fileEntity.getOriginalFileName())
.storedFileName(fileEntity.getStoredFileName())
.filePath(fileEntity.getFilePath())
.fileSize(fileEntity.getFileSize())
.mimeType(fileEntity.getMimeType())
.fileExtension(fileEntity.getFileExtension())
.checksum(fileEntity.getChecksum())
.uploadSessionId(fileEntity.getUploadSessionId())
.status(fileEntity.getStatus())
.createdAt(fileEntity.getCreatedAt())
.updatedAt(fileEntity.getUpdatedAt())
.formattedSize(formatFileSize(fileEntity.getFileSize()))
.downloadUrl("/api/files/download/" + fileEntity.getId())
.existsOnDisk(existsOnDisk)
.build();
}
private String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.1f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.1f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
}
}
}
@@ -3,6 +3,7 @@ package ru.soune.nocopy.service.file;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -56,6 +57,9 @@ public class FileUploadServiceImpl implements FileUploadService {
private Path storageRoot; private Path storageRoot;
@Autowired
private FileEntityService fileEntityService;
@PostConstruct @PostConstruct
public void init() { public void init() {
try { try {
@@ -328,14 +332,46 @@ public class FileUploadServiceImpl implements FileUploadService {
} }
} }
// 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 { private void assembleFile(FileUploadSession session) throws IOException {
log.info("Starting file assembly for session: {} ({})", log.info("Starting file assembly for session: {} ({})",
session.getUploadId(), session.getFileName()); session.getUploadId(), session.getFileName());
Path finalFilePath = null; Path finalFilePath = null;
String checksum = null;
try { try {
finalFilePath = prepareFinalFile(session); finalFilePath = prepareFinalFile(session);
log.info("Final file path: {}", finalFilePath);
validateAllChunksExist(session); validateAllChunksExist(session);
@@ -343,18 +379,48 @@ public class FileUploadServiceImpl implements FileUploadService {
validateFinalFile(session, finalFilePath); validateFinalFile(session, finalFilePath);
updateSessionOnSuccess(session, finalFilePath); checksum = calculateChecksum(finalFilePath);
log.debug("File checksum calculated: {}", checksum);
session.setFilePath(finalFilePath.toString());
session.setChecksum(checksum);
session.setStatus(UploadStatus.COMPLETED);
session.setCompletedAt(LocalDateTime.now());
sessionRepository.save(session);
log.info("Upload session updated to COMPLETED: {}", session.getUploadId());
try {
fileEntityService.createFromUploadSession(session, checksum);
log.info("FileEntity successfully created for session: {}",
session.getUploadId());
} catch (Exception e) {
log.error("Failed to create FileEntity for session {} (file uploaded, but metadata not saved): {}",
session.getUploadId(), e.getMessage());
}
cleanupSessionFiles(session); cleanupSessionFiles(session);
log.info("File assembly completed: {} -> {} ({} bytes)", log.info("File assembly completed successfully: {} -> {} ({} bytes)",
session.getFileName(), finalFilePath, session.getFileSize()); session.getFileName(), finalFilePath, session.getFileSize());
} catch (Exception e) { } catch (Exception e) {
log.error("File assembly failed for session {}: {}",
session.getUploadId(), e.getMessage(), e);
if (finalFilePath != null) { if (finalFilePath != null) {
Files.deleteIfExists(finalFilePath); try {
Files.deleteIfExists(finalFilePath);
log.debug("Cleaned up partial file: {}", finalFilePath);
} catch (IOException ioException) {
log.warn("Failed to cleanup partial file: {}", finalFilePath, ioException);
}
} }
throw e;
session.setStatus(UploadStatus.FAILED);
session.setLastError(e.getMessage());
sessionRepository.save(session);
throw new IOException("File assembly failed: " + e.getMessage(), e);
} }
} }
+7
View File
@@ -31,6 +31,13 @@ file:
temp-ttl-hours: ${TEMP_TTL_HOURS:72} # 3 дня temp-ttl-hours: ${TEMP_TTL_HOURS:72} # 3 дня
session-expiry-hours: ${SESSION_EXPIRY_HOURS:24} session-expiry-hours: ${SESSION_EXPIRY_HOURS:24}
security:
cors:
allowed-origins: "*"
allowed-methods: GET,POST,PUT,DELETE,OPTIONS
allowed-headers: "*"
allow-credentials: true
server: server:
port: ${SERVER_PORT:8080} port: ${SERVER_PORT:8080}