完善前端直连oss功能,新增重命名以及删除文件功能,完善后端接口
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package com.yoyuzh.config;
|
||||
|
||||
import com.yoyuzh.files.storage.FileContentStorage;
|
||||
import com.yoyuzh.files.storage.LocalFileContentStorage;
|
||||
import com.yoyuzh.files.storage.OssFileContentStorage;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class FileStorageConfiguration {
|
||||
|
||||
@Bean
|
||||
public FileContentStorage fileContentStorage(FileStorageProperties properties) {
|
||||
if ("oss".equalsIgnoreCase(properties.getProvider())) {
|
||||
return new OssFileContentStorage(properties);
|
||||
}
|
||||
return new LocalFileContentStorage(properties);
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,25 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@ConfigurationProperties(prefix = "app.storage")
|
||||
public class FileStorageProperties {
|
||||
|
||||
private String rootDir = "./storage";
|
||||
private String provider = "local";
|
||||
private final Local local = new Local();
|
||||
private final Oss oss = new Oss();
|
||||
private long maxFileSize = 50 * 1024 * 1024L;
|
||||
|
||||
public String getRootDir() {
|
||||
return rootDir;
|
||||
public String getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
public void setRootDir(String rootDir) {
|
||||
this.rootDir = rootDir;
|
||||
public void setProvider(String provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
public Local getLocal() {
|
||||
return local;
|
||||
}
|
||||
|
||||
public Oss getOss() {
|
||||
return oss;
|
||||
}
|
||||
|
||||
public long getMaxFileSize() {
|
||||
@@ -23,4 +33,82 @@ public class FileStorageProperties {
|
||||
public void setMaxFileSize(long maxFileSize) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
}
|
||||
|
||||
// Backward-compatible convenience accessors used by existing tests and dev tooling.
|
||||
public String getRootDir() {
|
||||
return local.getRootDir();
|
||||
}
|
||||
|
||||
public void setRootDir(String rootDir) {
|
||||
local.setRootDir(rootDir);
|
||||
}
|
||||
|
||||
public static class Local {
|
||||
private String rootDir = "./storage";
|
||||
|
||||
public String getRootDir() {
|
||||
return rootDir;
|
||||
}
|
||||
|
||||
public void setRootDir(String rootDir) {
|
||||
this.rootDir = rootDir;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Oss {
|
||||
private String endpoint;
|
||||
private String bucket;
|
||||
private String accessKeyId;
|
||||
private String accessKeySecret;
|
||||
private String publicBaseUrl;
|
||||
private boolean privateBucket = true;
|
||||
|
||||
public String getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
public void setEndpoint(String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public String getBucket() {
|
||||
return bucket;
|
||||
}
|
||||
|
||||
public void setBucket(String bucket) {
|
||||
this.bucket = bucket;
|
||||
}
|
||||
|
||||
public String getAccessKeyId() {
|
||||
return accessKeyId;
|
||||
}
|
||||
|
||||
public void setAccessKeyId(String accessKeyId) {
|
||||
this.accessKeyId = accessKeyId;
|
||||
}
|
||||
|
||||
public String getAccessKeySecret() {
|
||||
return accessKeySecret;
|
||||
}
|
||||
|
||||
public void setAccessKeySecret(String accessKeySecret) {
|
||||
this.accessKeySecret = accessKeySecret;
|
||||
}
|
||||
|
||||
public String getPublicBaseUrl() {
|
||||
return publicBaseUrl;
|
||||
}
|
||||
|
||||
public void setPublicBaseUrl(String publicBaseUrl) {
|
||||
this.publicBaseUrl = publicBaseUrl;
|
||||
}
|
||||
|
||||
public boolean isPrivateBucket() {
|
||||
return privateBucket;
|
||||
}
|
||||
|
||||
public void setPrivateBucket(boolean privateBucket) {
|
||||
this.privateBucket = privateBucket;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ public class SecurityConfig {
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(corsProperties.getAllowedOrigins());
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
configuration.setAllowedHeaders(List.of("*"));
|
||||
configuration.setAllowCredentials(false);
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record CompleteUploadRequest(
|
||||
@NotBlank String path,
|
||||
@NotBlank String filename,
|
||||
@NotBlank String storageName,
|
||||
String contentType,
|
||||
@Min(0) long size
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
public record DownloadUrlResponse(String url) {
|
||||
}
|
||||
@@ -14,8 +14,10 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -38,6 +40,26 @@ public class FileController {
|
||||
return ApiResponse.success(fileService.upload(userDetailsService.loadDomainUser(userDetails.getUsername()), path, file));
|
||||
}
|
||||
|
||||
@Operation(summary = "初始化上传")
|
||||
@PostMapping("/upload/initiate")
|
||||
public ApiResponse<InitiateUploadResponse> initiateUpload(@AuthenticationPrincipal UserDetails userDetails,
|
||||
@Valid @RequestBody InitiateUploadRequest request) {
|
||||
return ApiResponse.success(fileService.initiateUpload(
|
||||
userDetailsService.loadDomainUser(userDetails.getUsername()),
|
||||
request
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成上传")
|
||||
@PostMapping("/upload/complete")
|
||||
public ApiResponse<FileMetadataResponse> completeUpload(@AuthenticationPrincipal UserDetails userDetails,
|
||||
@Valid @RequestBody CompleteUploadRequest request) {
|
||||
return ApiResponse.success(fileService.completeUpload(
|
||||
userDetailsService.loadDomainUser(userDetails.getUsername()),
|
||||
request
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "创建目录")
|
||||
@PostMapping("/mkdir")
|
||||
public ApiResponse<FileMetadataResponse> mkdir(@AuthenticationPrincipal UserDetails userDetails,
|
||||
@@ -62,11 +84,30 @@ public class FileController {
|
||||
|
||||
@Operation(summary = "下载文件")
|
||||
@GetMapping("/download/{fileId}")
|
||||
public ResponseEntity<byte[]> download(@AuthenticationPrincipal UserDetails userDetails,
|
||||
@PathVariable Long fileId) {
|
||||
public ResponseEntity<?> download(@AuthenticationPrincipal UserDetails userDetails,
|
||||
@PathVariable Long fileId) {
|
||||
return fileService.download(userDetailsService.loadDomainUser(userDetails.getUsername()), fileId);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取下载链接")
|
||||
@GetMapping("/download/{fileId}/url")
|
||||
public ApiResponse<DownloadUrlResponse> downloadUrl(@AuthenticationPrincipal UserDetails userDetails,
|
||||
@PathVariable Long fileId) {
|
||||
return ApiResponse.success(fileService.getDownloadUrl(
|
||||
userDetailsService.loadDomainUser(userDetails.getUsername()),
|
||||
fileId
|
||||
));
|
||||
}
|
||||
|
||||
@Operation(summary = "重命名文件")
|
||||
@PatchMapping("/{fileId}/rename")
|
||||
public ApiResponse<FileMetadataResponse> rename(@AuthenticationPrincipal UserDetails userDetails,
|
||||
@PathVariable Long fileId,
|
||||
@Valid @RequestBody RenameFileRequest request) {
|
||||
return ApiResponse.success(
|
||||
fileService.rename(userDetailsService.loadDomainUser(userDetails.getUsername()), fileId, request.filename()));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除文件")
|
||||
@DeleteMapping("/{fileId}")
|
||||
public ApiResponse<Void> delete(@AuthenticationPrincipal UserDetails userDetails,
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.yoyuzh.common.BusinessException;
|
||||
import com.yoyuzh.common.ErrorCode;
|
||||
import com.yoyuzh.common.PageResponse;
|
||||
import com.yoyuzh.config.FileStorageProperties;
|
||||
import com.yoyuzh.files.storage.FileContentStorage;
|
||||
import com.yoyuzh.files.storage.PreparedUpload;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -15,12 +17,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@@ -28,52 +27,58 @@ public class FileService {
|
||||
private static final List<String> DEFAULT_DIRECTORIES = List.of("下载", "文档", "图片");
|
||||
|
||||
private final StoredFileRepository storedFileRepository;
|
||||
private final Path rootPath;
|
||||
private final FileContentStorage fileContentStorage;
|
||||
private final long maxFileSize;
|
||||
|
||||
public FileService(StoredFileRepository storedFileRepository, FileStorageProperties properties) {
|
||||
public FileService(StoredFileRepository storedFileRepository,
|
||||
FileContentStorage fileContentStorage,
|
||||
FileStorageProperties properties) {
|
||||
this.storedFileRepository = storedFileRepository;
|
||||
this.rootPath = Path.of(properties.getRootDir()).toAbsolutePath().normalize();
|
||||
this.fileContentStorage = fileContentStorage;
|
||||
this.maxFileSize = properties.getMaxFileSize();
|
||||
try {
|
||||
Files.createDirectories(rootPath);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException("无法初始化存储目录", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FileMetadataResponse upload(User user, String path, MultipartFile multipartFile) {
|
||||
String normalizedPath = normalizeDirectoryPath(path);
|
||||
String filename = StringUtils.cleanPath(multipartFile.getOriginalFilename());
|
||||
if (!StringUtils.hasText(filename)) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "文件名不能为空");
|
||||
}
|
||||
if (multipartFile.getSize() > maxFileSize) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "文件大小超出限制");
|
||||
}
|
||||
if (storedFileRepository.existsByUserIdAndPathAndFilename(user.getId(), normalizedPath, filename)) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "同目录下文件已存在");
|
||||
}
|
||||
String filename = normalizeUploadFilename(multipartFile.getOriginalFilename());
|
||||
validateUpload(user.getId(), normalizedPath, filename, multipartFile.getSize());
|
||||
|
||||
Path targetDir = resolveUserPath(user.getId(), normalizedPath);
|
||||
Path targetFile = targetDir.resolve(filename).normalize();
|
||||
try {
|
||||
Files.createDirectories(targetDir);
|
||||
Files.copy(multipartFile.getInputStream(), targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException ex) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "文件上传失败");
|
||||
}
|
||||
fileContentStorage.upload(user.getId(), normalizedPath, filename, multipartFile);
|
||||
return saveFileMetadata(user, normalizedPath, filename, filename, multipartFile.getContentType(), multipartFile.getSize());
|
||||
}
|
||||
|
||||
StoredFile storedFile = new StoredFile();
|
||||
storedFile.setUser(user);
|
||||
storedFile.setFilename(filename);
|
||||
storedFile.setPath(normalizedPath);
|
||||
storedFile.setStorageName(filename);
|
||||
storedFile.setContentType(multipartFile.getContentType());
|
||||
storedFile.setSize(multipartFile.getSize());
|
||||
storedFile.setDirectory(false);
|
||||
return toResponse(storedFileRepository.save(storedFile));
|
||||
public InitiateUploadResponse initiateUpload(User user, InitiateUploadRequest request) {
|
||||
String normalizedPath = normalizeDirectoryPath(request.path());
|
||||
String filename = normalizeLeafName(request.filename());
|
||||
validateUpload(user.getId(), normalizedPath, filename, request.size());
|
||||
|
||||
PreparedUpload preparedUpload = fileContentStorage.prepareUpload(
|
||||
user.getId(),
|
||||
normalizedPath,
|
||||
filename,
|
||||
request.contentType(),
|
||||
request.size()
|
||||
);
|
||||
|
||||
return new InitiateUploadResponse(
|
||||
preparedUpload.direct(),
|
||||
preparedUpload.uploadUrl(),
|
||||
preparedUpload.method(),
|
||||
preparedUpload.headers(),
|
||||
preparedUpload.storageName()
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public FileMetadataResponse completeUpload(User user, CompleteUploadRequest request) {
|
||||
String normalizedPath = normalizeDirectoryPath(request.path());
|
||||
String filename = normalizeLeafName(request.filename());
|
||||
String storageName = normalizeLeafName(request.storageName());
|
||||
validateUpload(user.getId(), normalizedPath, filename, request.size());
|
||||
|
||||
fileContentStorage.completeUpload(user.getId(), normalizedPath, storageName, request.contentType(), request.size());
|
||||
return saveFileMetadata(user, normalizedPath, filename, storageName, request.contentType(), request.size());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -87,11 +92,8 @@ public class FileService {
|
||||
if (storedFileRepository.existsByUserIdAndPathAndFilename(user.getId(), parentPath, directoryName)) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "目录已存在");
|
||||
}
|
||||
try {
|
||||
Files.createDirectories(resolveUserPath(user.getId(), normalizedPath));
|
||||
} catch (IOException ex) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "目录创建失败");
|
||||
}
|
||||
|
||||
fileContentStorage.createDirectory(user.getId(), normalizedPath);
|
||||
|
||||
StoredFile storedFile = new StoredFile();
|
||||
storedFile.setUser(user);
|
||||
@@ -126,11 +128,8 @@ public class FileService {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createDirectories(resolveUserPath(user.getId(), "/").resolve(directoryName));
|
||||
} catch (IOException ex) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "默认目录初始化失败");
|
||||
}
|
||||
String logicalPath = "/" + directoryName;
|
||||
fileContentStorage.ensureDirectory(user.getId(), logicalPath);
|
||||
|
||||
StoredFile storedFile = new StoredFile();
|
||||
storedFile.setUser(user);
|
||||
@@ -146,53 +145,146 @@ public class FileService {
|
||||
|
||||
@Transactional
|
||||
public void delete(User user, Long fileId) {
|
||||
StoredFile storedFile = storedFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "文件不存在"));
|
||||
if (!storedFile.getUser().getId().equals(user.getId())) {
|
||||
throw new BusinessException(ErrorCode.PERMISSION_DENIED, "没有权限删除该文件");
|
||||
}
|
||||
try {
|
||||
Path basePath = resolveUserPath(user.getId(), storedFile.getPath());
|
||||
Path target = storedFile.isDirectory()
|
||||
? basePath.resolve(storedFile.getFilename()).normalize()
|
||||
: basePath.resolve(storedFile.getStorageName()).normalize();
|
||||
Files.deleteIfExists(target);
|
||||
} catch (IOException ex) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "删除文件失败");
|
||||
StoredFile storedFile = getOwnedFile(user, fileId, "删除");
|
||||
if (storedFile.isDirectory()) {
|
||||
String logicalPath = buildLogicalPath(storedFile);
|
||||
List<StoredFile> descendants = storedFileRepository.findByUserIdAndPathEqualsOrDescendant(user.getId(), logicalPath);
|
||||
fileContentStorage.deleteDirectory(user.getId(), logicalPath, descendants);
|
||||
if (!descendants.isEmpty()) {
|
||||
storedFileRepository.deleteAll(descendants);
|
||||
}
|
||||
} else {
|
||||
fileContentStorage.deleteFile(user.getId(), storedFile.getPath(), storedFile.getStorageName());
|
||||
}
|
||||
storedFileRepository.delete(storedFile);
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> download(User user, Long fileId) {
|
||||
StoredFile storedFile = storedFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "文件不存在"));
|
||||
if (!storedFile.getUser().getId().equals(user.getId())) {
|
||||
throw new BusinessException(ErrorCode.PERMISSION_DENIED, "没有权限下载该文件");
|
||||
@Transactional
|
||||
public FileMetadataResponse rename(User user, Long fileId, String nextFilename) {
|
||||
StoredFile storedFile = getOwnedFile(user, fileId, "重命名");
|
||||
String sanitizedFilename = normalizeLeafName(nextFilename);
|
||||
if (sanitizedFilename.equals(storedFile.getFilename())) {
|
||||
return toResponse(storedFile);
|
||||
}
|
||||
if (storedFileRepository.existsByUserIdAndPathAndFilename(user.getId(), storedFile.getPath(), sanitizedFilename)) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "同目录下文件已存在");
|
||||
}
|
||||
|
||||
if (storedFile.isDirectory()) {
|
||||
String oldLogicalPath = buildLogicalPath(storedFile);
|
||||
String newLogicalPath = "/".equals(storedFile.getPath())
|
||||
? "/" + sanitizedFilename
|
||||
: storedFile.getPath() + "/" + sanitizedFilename;
|
||||
|
||||
List<StoredFile> descendants = storedFileRepository.findByUserIdAndPathEqualsOrDescendant(user.getId(), oldLogicalPath);
|
||||
fileContentStorage.renameDirectory(user.getId(), oldLogicalPath, newLogicalPath, descendants);
|
||||
for (StoredFile descendant : descendants) {
|
||||
if (descendant.getPath().equals(oldLogicalPath)) {
|
||||
descendant.setPath(newLogicalPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
descendant.setPath(newLogicalPath + descendant.getPath().substring(oldLogicalPath.length()));
|
||||
}
|
||||
if (!descendants.isEmpty()) {
|
||||
storedFileRepository.saveAll(descendants);
|
||||
}
|
||||
} else {
|
||||
fileContentStorage.renameFile(user.getId(), storedFile.getPath(), storedFile.getStorageName(), sanitizedFilename);
|
||||
}
|
||||
|
||||
storedFile.setFilename(sanitizedFilename);
|
||||
storedFile.setStorageName(sanitizedFilename);
|
||||
return toResponse(storedFileRepository.save(storedFile));
|
||||
}
|
||||
|
||||
public ResponseEntity<?> download(User user, Long fileId) {
|
||||
StoredFile storedFile = getOwnedFile(user, fileId, "下载");
|
||||
if (storedFile.isDirectory()) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "目录不支持下载");
|
||||
}
|
||||
try {
|
||||
Path filePath = resolveUserPath(user.getId(), storedFile.getPath()).resolve(storedFile.getStorageName()).normalize();
|
||||
byte[] body = Files.readAllBytes(filePath);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename*=UTF-8''" + URLEncoder.encode(storedFile.getFilename(), StandardCharsets.UTF_8))
|
||||
.contentType(MediaType.parseMediaType(
|
||||
storedFile.getContentType() == null ? MediaType.APPLICATION_OCTET_STREAM_VALUE : storedFile.getContentType()))
|
||||
.body(body);
|
||||
} catch (IOException ex) {
|
||||
throw new BusinessException(ErrorCode.FILE_NOT_FOUND, "文件不存在");
|
||||
|
||||
if (fileContentStorage.supportsDirectDownload()) {
|
||||
return ResponseEntity.status(302)
|
||||
.location(URI.create(fileContentStorage.createDownloadUrl(
|
||||
user.getId(),
|
||||
storedFile.getPath(),
|
||||
storedFile.getStorageName(),
|
||||
storedFile.getFilename())))
|
||||
.build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename*=UTF-8''" + URLEncoder.encode(storedFile.getFilename(), StandardCharsets.UTF_8))
|
||||
.contentType(MediaType.parseMediaType(
|
||||
storedFile.getContentType() == null ? MediaType.APPLICATION_OCTET_STREAM_VALUE : storedFile.getContentType()))
|
||||
.body(fileContentStorage.readFile(user.getId(), storedFile.getPath(), storedFile.getStorageName()));
|
||||
}
|
||||
|
||||
public DownloadUrlResponse getDownloadUrl(User user, Long fileId) {
|
||||
StoredFile storedFile = getOwnedFile(user, fileId, "下载");
|
||||
if (storedFile.isDirectory()) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "目录不支持下载");
|
||||
}
|
||||
|
||||
if (fileContentStorage.supportsDirectDownload()) {
|
||||
return new DownloadUrlResponse(fileContentStorage.createDownloadUrl(
|
||||
user.getId(),
|
||||
storedFile.getPath(),
|
||||
storedFile.getStorageName(),
|
||||
storedFile.getFilename()
|
||||
));
|
||||
}
|
||||
|
||||
return new DownloadUrlResponse("/api/files/download/" + storedFile.getId());
|
||||
}
|
||||
|
||||
private FileMetadataResponse saveFileMetadata(User user,
|
||||
String normalizedPath,
|
||||
String filename,
|
||||
String storageName,
|
||||
String contentType,
|
||||
long size) {
|
||||
StoredFile storedFile = new StoredFile();
|
||||
storedFile.setUser(user);
|
||||
storedFile.setFilename(filename);
|
||||
storedFile.setPath(normalizedPath);
|
||||
storedFile.setStorageName(storageName);
|
||||
storedFile.setContentType(contentType);
|
||||
storedFile.setSize(size);
|
||||
storedFile.setDirectory(false);
|
||||
return toResponse(storedFileRepository.save(storedFile));
|
||||
}
|
||||
|
||||
private StoredFile getOwnedFile(User user, Long fileId, String action) {
|
||||
StoredFile storedFile = storedFileRepository.findById(fileId)
|
||||
.orElseThrow(() -> new BusinessException(ErrorCode.FILE_NOT_FOUND, "文件不存在"));
|
||||
if (!storedFile.getUser().getId().equals(user.getId())) {
|
||||
throw new BusinessException(ErrorCode.PERMISSION_DENIED, "没有权限" + action + "该文件");
|
||||
}
|
||||
return storedFile;
|
||||
}
|
||||
|
||||
private void validateUpload(Long userId, String normalizedPath, String filename, long size) {
|
||||
if (size > maxFileSize) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "文件大小超出限制");
|
||||
}
|
||||
if (storedFileRepository.existsByUserIdAndPathAndFilename(userId, normalizedPath, filename)) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "同目录下文件已存在");
|
||||
}
|
||||
}
|
||||
|
||||
private FileMetadataResponse toResponse(StoredFile storedFile) {
|
||||
String logicalPath = storedFile.getPath();
|
||||
if (storedFile.isDirectory()) {
|
||||
logicalPath = "/".equals(storedFile.getPath())
|
||||
? "/" + storedFile.getFilename()
|
||||
: storedFile.getPath() + "/" + storedFile.getFilename();
|
||||
private String normalizeUploadFilename(String originalFilename) {
|
||||
String filename = StringUtils.cleanPath(originalFilename);
|
||||
if (!StringUtils.hasText(filename)) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "文件名不能为空");
|
||||
}
|
||||
return normalizeLeafName(filename);
|
||||
}
|
||||
|
||||
private FileMetadataResponse toResponse(StoredFile storedFile) {
|
||||
String logicalPath = storedFile.isDirectory() ? buildLogicalPath(storedFile) : storedFile.getPath();
|
||||
return new FileMetadataResponse(
|
||||
storedFile.getId(),
|
||||
storedFile.getFilename(),
|
||||
@@ -221,16 +313,6 @@ public class FileService {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private Path resolveUserPath(Long userId, String normalizedPath) {
|
||||
Path userRoot = rootPath.resolve(userId.toString()).normalize();
|
||||
Path relative = "/".equals(normalizedPath) ? Path.of("") : Path.of(normalizedPath.substring(1));
|
||||
Path resolved = userRoot.resolve(relative).normalize();
|
||||
if (!resolved.startsWith(userRoot)) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "路径不合法");
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private String extractParentPath(String normalizedPath) {
|
||||
int lastSlash = normalizedPath.lastIndexOf('/');
|
||||
return lastSlash <= 0 ? "/" : normalizedPath.substring(0, lastSlash);
|
||||
@@ -239,4 +321,21 @@ public class FileService {
|
||||
private String extractLeafName(String normalizedPath) {
|
||||
return normalizedPath.substring(normalizedPath.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
private String buildLogicalPath(StoredFile storedFile) {
|
||||
return "/".equals(storedFile.getPath())
|
||||
? "/" + storedFile.getFilename()
|
||||
: storedFile.getPath() + "/" + storedFile.getFilename();
|
||||
}
|
||||
|
||||
private String normalizeLeafName(String filename) {
|
||||
String cleaned = StringUtils.cleanPath(filename == null ? "" : filename).trim();
|
||||
if (!StringUtils.hasText(cleaned)) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "文件名不能为空");
|
||||
}
|
||||
if (cleaned.contains("/") || cleaned.contains("\\") || cleaned.contains("..")) {
|
||||
throw new BusinessException(ErrorCode.UNKNOWN, "文件名不合法");
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record InitiateUploadRequest(
|
||||
@NotBlank String path,
|
||||
@NotBlank String filename,
|
||||
String contentType,
|
||||
@Min(0) long size
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public record InitiateUploadResponse(
|
||||
boolean direct,
|
||||
String uploadUrl,
|
||||
String method,
|
||||
Map<String, String> headers,
|
||||
String storageName
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.yoyuzh.files;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public record RenameFileRequest(
|
||||
@NotBlank(message = "文件名不能为空")
|
||||
String filename
|
||||
) {
|
||||
}
|
||||
@@ -28,5 +28,13 @@ public interface StoredFileRepository extends JpaRepository<StoredFile, Long> {
|
||||
@Param("path") String path,
|
||||
Pageable pageable);
|
||||
|
||||
@Query("""
|
||||
select f from StoredFile f
|
||||
where f.user.id = :userId and (f.path = :path or f.path like concat(:path, '/%'))
|
||||
order by f.createdAt asc
|
||||
""")
|
||||
List<StoredFile> findByUserIdAndPathEqualsOrDescendant(@Param("userId") Long userId,
|
||||
@Param("path") String path);
|
||||
|
||||
List<StoredFile> findTop12ByUserIdAndDirectoryFalseOrderByCreatedAtDesc(Long userId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user