feat(files): add file entity migration

This commit is contained in:
yoyuzh
2026-04-08 15:02:42 +08:00
parent 9d5fdd9ea3
commit 5802f396c5
14 changed files with 699 additions and 4 deletions

View File

@@ -0,0 +1,114 @@
package com.yoyuzh.files;
import com.yoyuzh.auth.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class FileEntityBackfillServiceTest {
@Mock
private StoredFileRepository storedFileRepository;
@Mock
private FileEntityRepository fileEntityRepository;
@Mock
private StoredFileEntityRepository storedFileEntityRepository;
private FileEntityBackfillService backfillService;
@BeforeEach
void setUp() {
backfillService = new FileEntityBackfillService(
storedFileRepository,
fileEntityRepository,
storedFileEntityRepository
);
}
@Test
void shouldBackfillPrimaryEntityFromExistingBlob() {
StoredFile storedFile = createStoredFile(10L, 7L, "notes.txt", createBlob(20L, "blobs/blob-20"));
when(storedFileRepository.findAllByDirectoryFalseAndBlobIsNotNullAndPrimaryEntityIsNull())
.thenReturn(List.of(storedFile));
when(fileEntityRepository.findByObjectKeyAndEntityType("blobs/blob-20", FileEntityType.VERSION))
.thenReturn(Optional.empty());
when(fileEntityRepository.save(any(FileEntity.class))).thenAnswer(invocation -> {
FileEntity entity = invocation.getArgument(0);
entity.setId(100L);
return entity;
});
backfillService.backfillPrimaryEntities();
assertThat(storedFile.getPrimaryEntity()).isNotNull();
assertThat(storedFile.getPrimaryEntity().getObjectKey()).isEqualTo("blobs/blob-20");
assertThat(storedFile.getPrimaryEntity().getEntityType()).isEqualTo(FileEntityType.VERSION);
assertThat(storedFile.getPrimaryEntity().getReferenceCount()).isEqualTo(1);
verify(fileEntityRepository).save(any(FileEntity.class));
verify(storedFileRepository).save(storedFile);
verify(storedFileEntityRepository).save(any(StoredFileEntity.class));
}
@Test
void shouldReuseExistingFileEntityWhenBackfillRunsAgain() {
StoredFile storedFile = createStoredFile(11L, 8L, "report.pdf", createBlob(21L, "blobs/blob-21"));
FileEntity existingEntity = new FileEntity();
existingEntity.setId(101L);
existingEntity.setObjectKey("blobs/blob-21");
existingEntity.setEntityType(FileEntityType.VERSION);
existingEntity.setReferenceCount(3);
when(storedFileRepository.findAllByDirectoryFalseAndBlobIsNotNullAndPrimaryEntityIsNull())
.thenReturn(List.of(storedFile));
when(fileEntityRepository.findByObjectKeyAndEntityType("blobs/blob-21", FileEntityType.VERSION))
.thenReturn(Optional.of(existingEntity));
backfillService.backfillPrimaryEntities();
assertThat(storedFile.getPrimaryEntity()).isSameAs(existingEntity);
assertThat(existingEntity.getReferenceCount()).isEqualTo(4);
verify(fileEntityRepository).save(existingEntity);
verify(storedFileRepository).save(storedFile);
verify(storedFileEntityRepository).save(any(StoredFileEntity.class));
}
private StoredFile createStoredFile(Long id, Long userId, String filename, FileBlob blob) {
User user = new User();
user.setId(userId);
user.setUsername("user-" + userId);
StoredFile file = new StoredFile();
file.setId(id);
file.setUser(user);
file.setPath("/docs");
file.setFilename(filename);
file.setBlob(blob);
file.setContentType(blob.getContentType());
file.setSize(blob.getSize());
file.setDirectory(false);
file.setCreatedAt(LocalDateTime.now());
return file;
}
private FileBlob createBlob(Long id, String objectKey) {
FileBlob blob = new FileBlob();
blob.setId(id);
blob.setObjectKey(objectKey);
blob.setContentType("text/plain");
blob.setSize(5L);
blob.setCreatedAt(LocalDateTime.now());
return blob;
}
}

View File

@@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.AdditionalMatchers.aryEq;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentCaptor.forClass;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
@@ -50,6 +51,10 @@ class FileServiceTest {
@Mock
private FileBlobRepository fileBlobRepository;
@Mock
private FileEntityRepository fileEntityRepository;
@Mock
private StoredFileEntityRepository storedFileEntityRepository;
@Mock
private FileContentStorage fileContentStorage;
@@ -104,6 +109,86 @@ class FileServiceTest {
&& "text/plain".equals(blob.getContentType())));
}
@Test
void shouldAttachPrimaryEntityWhenUploadingFile() {
User user = createUser(7L);
MockMultipartFile multipartFile = new MockMultipartFile(
"file", "notes.txt", "text/plain", "hello".getBytes());
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> {
FileBlob blob = invocation.getArgument(0);
blob.setId(100L);
return blob;
});
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> {
StoredFile file = invocation.getArgument(0);
file.setId(10L);
return file;
});
fileService.upload(user, "/docs", multipartFile);
var savedFileCaptor = forClass(StoredFile.class);
verify(storedFileRepository, times(2)).save(savedFileCaptor.capture());
StoredFile storedFile = savedFileCaptor.getAllValues().stream()
.filter(file -> !file.isDirectory())
.findFirst()
.orElseThrow();
assertThat(storedFile.getPrimaryEntity()).isNotNull();
assertThat(storedFile.getPrimaryEntity().getObjectKey()).isEqualTo(storedFile.getBlob().getObjectKey());
assertThat(storedFile.getPrimaryEntity().getEntityType()).isEqualTo(FileEntityType.VERSION);
assertThat(storedFile.getPrimaryEntity().getReferenceCount()).isEqualTo(1);
}
@Test
void shouldPersistFileEntityAndRelationWhenUploadingFile() {
fileService = new FileService(
storedFileRepository,
fileBlobRepository,
fileEntityRepository,
storedFileEntityRepository,
fileContentStorage,
fileShareLinkRepository,
adminMetricsService,
new FileStorageProperties()
);
User user = createUser(7L);
MockMultipartFile multipartFile = new MockMultipartFile(
"file", "notes.txt", "text/plain", "hello".getBytes());
when(storedFileRepository.existsByUserIdAndPathAndFilename(7L, "/docs", "notes.txt")).thenReturn(false);
when(fileBlobRepository.save(any(FileBlob.class))).thenAnswer(invocation -> {
FileBlob blob = invocation.getArgument(0);
blob.setId(100L);
return blob;
});
when(fileEntityRepository.findByObjectKeyAndEntityType(org.mockito.ArgumentMatchers.anyString(), eq(FileEntityType.VERSION)))
.thenReturn(Optional.empty());
when(fileEntityRepository.save(any(FileEntity.class))).thenAnswer(invocation -> {
FileEntity entity = invocation.getArgument(0);
entity.setId(200L);
return entity;
});
when(storedFileRepository.save(any(StoredFile.class))).thenAnswer(invocation -> {
StoredFile file = invocation.getArgument(0);
file.setId(10L);
return file;
});
fileService.upload(user, "/docs", multipartFile);
var entityCaptor = forClass(FileEntity.class);
verify(fileEntityRepository).save(entityCaptor.capture());
assertThat(entityCaptor.getValue().getObjectKey()).startsWith("blobs/");
assertThat(entityCaptor.getValue().getEntityType()).isEqualTo(FileEntityType.VERSION);
assertThat(entityCaptor.getValue().getCreatedBy()).isSameAs(user);
var relationCaptor = forClass(StoredFileEntity.class);
verify(storedFileEntityRepository).save(relationCaptor.capture());
assertThat(relationCaptor.getValue().getStoredFile().getId()).isEqualTo(10L);
assertThat(relationCaptor.getValue().getFileEntity().getId()).isEqualTo(200L);
assertThat(relationCaptor.getValue().getEntityRole()).isEqualTo("PRIMARY");
}
@Test
void shouldInitiateDirectUploadThroughStorage() {
User user = createUser(7L);
@@ -323,6 +408,10 @@ class FileServiceTest {
assertThat(response.id()).isEqualTo(20L);
assertThat(response.path()).isEqualTo("/下载");
assertThat(file.getBlob()).isSameAs(blob);
var copiedFileCaptor = forClass(StoredFile.class);
verify(storedFileRepository).save(copiedFileCaptor.capture());
assertThat(copiedFileCaptor.getValue().getPrimaryEntity()).isNotNull();
assertThat(copiedFileCaptor.getValue().getPrimaryEntity().getObjectKey()).isEqualTo(blob.getObjectKey());
verify(fileContentStorage, never()).copyFile(any(), any(), any(), any());
}