完善前端直连oss功能,新增重命名以及删除文件功能,完善后端接口

This commit is contained in:
yoyuzh
2026-03-19 10:26:50 +08:00
parent 96079b7e5b
commit e0d859bd82
26 changed files with 2545 additions and 183 deletions

View File

@@ -0,0 +1,31 @@
package com.yoyuzh.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import static org.assertj.core.api.Assertions.assertThat;
class SecurityConfigTest {
@Test
void corsConfigurationShouldAllowPatchRequests() {
CorsProperties corsProperties = new CorsProperties();
corsProperties.setAllowedOrigins(java.util.List.of("https://yoyuzh.xyz"));
SecurityConfig securityConfig = new SecurityConfig(
null,
null,
new ObjectMapper(),
corsProperties
);
CorsConfigurationSource source = securityConfig.corsConfigurationSource();
CorsConfiguration configuration = source.getCorsConfiguration(
new org.springframework.mock.web.MockHttpServletRequest("OPTIONS", "/api/files/1/rename"));
assertThat(configuration).isNotNull();
assertThat(configuration.getAllowedMethods()).contains("PATCH");
}
}

View File

@@ -3,9 +3,10 @@ package com.yoyuzh.files;
import com.yoyuzh.auth.User;
import com.yoyuzh.common.BusinessException;
import com.yoyuzh.config.FileStorageProperties;
import com.yoyuzh.files.storage.FileContentStorage;
import com.yoyuzh.files.storage.PreparedUpload;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -13,14 +14,16 @@ import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.mock.web.MockMultipartFile;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -31,21 +34,20 @@ class FileServiceTest {
@Mock
private StoredFileRepository storedFileRepository;
private FileService fileService;
@Mock
private FileContentStorage fileContentStorage;
@TempDir
Path tempDir;
private FileService fileService;
@BeforeEach
void setUp() {
FileStorageProperties properties = new FileStorageProperties();
properties.setRootDir(tempDir.toString());
properties.setMaxFileSize(50 * 1024 * 1024);
fileService = new FileService(storedFileRepository, properties);
fileService = new FileService(storedFileRepository, fileContentStorage, properties);
}
@Test
void shouldStoreUploadedFileUnderUserDirectory() {
void shouldStoreUploadedFileViaConfiguredStorage() {
User user = createUser(7L);
MockMultipartFile multipartFile = new MockMultipartFile(
"file", "notes.txt", "text/plain", "hello".getBytes());
@@ -60,8 +62,71 @@ class FileServiceTest {
assertThat(response.id()).isEqualTo(10L);
assertThat(response.path()).isEqualTo("/docs");
assertThat(response.directory()).isFalse();
assertThat(tempDir.resolve("7/docs/notes.txt")).exists();
verify(fileContentStorage).upload(7L, "/docs", "notes.txt", multipartFile);
}
@Test
void shouldInitiateDirectUploadThroughStorage() {
User user = createUser(7L);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
when(fileContentStorage.prepareUpload(7L, "/docs", "notes.txt", "text/plain", 12L))
.thenReturn(new PreparedUpload(true, "https://upload.example.com", "PUT", Map.of("Content-Type", "text/plain"), "notes.txt"));
InitiateUploadResponse response = fileService.initiateUpload(user,
new InitiateUploadRequest("/docs", "notes.txt", "text/plain", 12L));
assertThat(response.direct()).isTrue();
assertThat(response.uploadUrl()).isEqualTo("https://upload.example.com");
verify(fileContentStorage).prepareUpload(7L, "/docs", "notes.txt", "text/plain", 12L);
}
@Test
void shouldCompleteDirectUploadAndPersistMetadata() {
User user = createUser(7L);
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> {
StoredFile file = invocation.getArgument(0);
file.setId(11L);
return file;
});
FileMetadataResponse response = fileService.completeUpload(user,
new CompleteUploadRequest("/docs", "notes.txt", "notes.txt", "text/plain", 12L));
assertThat(response.id()).isEqualTo(11L);
verify(fileContentStorage).completeUpload(7L, "/docs", "notes.txt", "text/plain", 12L);
}
@Test
void shouldRenameFileThroughConfiguredStorage() {
User user = createUser(7L);
StoredFile storedFile = createFile(10L, user, "/docs", "notes.txt");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(storedFile));
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "renamed.txt")).thenReturn(false);
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
FileMetadataResponse response = fileService.rename(user, 10L, "renamed.txt");
assertThat(response.filename()).isEqualTo("renamed.txt");
verify(fileContentStorage).renameFile(7L, "/docs", "notes.txt", "renamed.txt");
}
@Test
void shouldRenameDirectoryAndUpdateDescendantPaths() {
User user = createUser(7L);
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
StoredFile childFile = createFile(11L, user, "/docs/archive", "nested.txt");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "renamed-archive")).thenReturn(false);
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive")).thenReturn(List.of(childFile));
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> invocation.getArgument(0));
FileMetadataResponse response = fileService.rename(user, 10L, "renamed-archive");
assertThat(response.filename()).isEqualTo("renamed-archive");
assertThat(childFile.getPath()).isEqualTo("/docs/renamed-archive");
verify(fileContentStorage).renameDirectory(7L, "/docs/archive", "/docs/renamed-archive", List.of(childFile));
}
@Test
@@ -76,6 +141,22 @@ class FileServiceTest {
.hasMessageContaining("没有权限");
}
@Test
void shouldDeleteDirectoryWithNestedFilesViaStorage() {
User user = createUser(7L);
StoredFile directory = createDirectory(10L, user, "/docs", "archive");
StoredFile childFile = createFile(11L, user, "/docs/archive", "nested.txt");
when(storedFileRepository.findById(10L)).thenReturn(Optional.of(directory));
when(storedFileRepository.findByUserIdAndPathEqualsOrDescendant(7L, "/docs/archive")).thenReturn(List.of(childFile));
fileService.delete(user, 10L);
verify(fileContentStorage).deleteDirectory(7L, "/docs/archive", List.of(childFile));
verify(storedFileRepository).deleteAll(List.of(childFile));
verify(storedFileRepository).delete(directory);
}
@Test
void shouldListFilesByPathWithPagination() {
User user = createUser(7L);
@@ -100,15 +181,39 @@ class FileServiceTest {
fileService.ensureDefaultDirectories(user);
assertThat(tempDir.resolve("7/下载")).exists();
assertThat(tempDir.resolve("7/文档")).exists();
assertThat(tempDir.resolve("7/图片")).exists();
verify(storedFileRepository).existsByUserIdAndPathAndFilename(7L, "/", "下载");
verify(storedFileRepository).existsByUserIdAndPathAndFilename(7L, "/", "文档");
verify(storedFileRepository).existsByUserIdAndPathAndFilename(7L, "/", "图片");
verify(fileContentStorage).ensureDirectory(7L, "/下载");
verify(fileContentStorage).ensureDirectory(7L, "/文档");
verify(fileContentStorage).ensureDirectory(7L, "/图片");
verify(storedFileRepository, times(3)).save(any(StoredFile.class));
}
@Test
void shouldUseSignedDownloadUrlWhenStorageSupportsDirectDownload() {
User user = createUser(7L);
StoredFile file = createFile(22L, user, "/docs", "notes.txt");
when(storedFileRepository.findById(22L)).thenReturn(Optional.of(file));
when(fileContentStorage.supportsDirectDownload()).thenReturn(true);
when(fileContentStorage.createDownloadUrl(7L, "/docs", "notes.txt", "notes.txt"))
.thenReturn("https://download.example.com/file");
DownloadUrlResponse response = fileService.getDownloadUrl(user, 22L);
assertThat(response.url()).isEqualTo("https://download.example.com/file");
}
@Test
void shouldFallbackToBackendDownloadUrlWhenStorageIsLocal() {
User user = createUser(7L);
StoredFile file = createFile(22L, user, "/docs", "notes.txt");
when(storedFileRepository.findById(22L)).thenReturn(Optional.of(file));
when(fileContentStorage.supportsDirectDownload()).thenReturn(false);
DownloadUrlResponse response = fileService.getDownloadUrl(user, 22L);
assertThat(response.url()).isEqualTo("/api/files/download/22");
verify(fileContentStorage, never()).createDownloadUrl(any(), any(), any(), any());
}
private User createUser(Long id) {
User user = new User();
user.setId(id);
@@ -131,4 +236,12 @@ class FileServiceTest {
file.setCreatedAt(LocalDateTime.now());
return file;
}
private StoredFile createDirectory(Long id, User user, String path, String filename) {
StoredFile directory = createFile(id, user, path, filename);
directory.setDirectory(true);
directory.setContentType("directory");
directory.setSize(0L);
return directory;
}
}