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,163 @@
package com.yoyuzh.files;
import com.yoyuzh.auth.User;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import java.time.LocalDateTime;
@Entity
@Table(name = "portal_file_entity", indexes = {
@Index(name = "uk_file_entity_key_type", columnList = "object_key,entity_type", unique = true),
@Index(name = "idx_file_entity_created_at", columnList = "created_at")
})
public class FileEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "object_key", nullable = false, length = 512)
private String objectKey;
@Column(nullable = false)
private Long size;
@Column(name = "content_type", length = 255)
private String contentType;
@Enumerated(EnumType.STRING)
@Column(name = "entity_type", nullable = false, length = 32)
private FileEntityType entityType;
@Column(name = "reference_count", nullable = false)
private Integer referenceCount;
@Column(name = "storage_policy_id")
private Long storagePolicyId;
@Column(name = "upload_session_id", length = 64)
private String uploadSessionId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "created_by")
@OnDelete(action = OnDeleteAction.SET_NULL)
private User createdBy;
@Column(name = "props_json", columnDefinition = "TEXT")
private String propsJson;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@PrePersist
public void prePersist() {
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
if (referenceCount == null) {
referenceCount = 0;
}
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getObjectKey() {
return objectKey;
}
public void setObjectKey(String objectKey) {
this.objectKey = objectKey;
}
public Long getSize() {
return size;
}
public void setSize(Long size) {
this.size = size;
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public FileEntityType getEntityType() {
return entityType;
}
public void setEntityType(FileEntityType entityType) {
this.entityType = entityType;
}
public Integer getReferenceCount() {
return referenceCount;
}
public void setReferenceCount(Integer referenceCount) {
this.referenceCount = referenceCount;
}
public Long getStoragePolicyId() {
return storagePolicyId;
}
public void setStoragePolicyId(Long storagePolicyId) {
this.storagePolicyId = storagePolicyId;
}
public String getUploadSessionId() {
return uploadSessionId;
}
public void setUploadSessionId(String uploadSessionId) {
this.uploadSessionId = uploadSessionId;
}
public User getCreatedBy() {
return createdBy;
}
public void setCreatedBy(User createdBy) {
this.createdBy = createdBy;
}
public String getPropsJson() {
return propsJson;
}
public void setPropsJson(String propsJson) {
this.propsJson = propsJson;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@@ -0,0 +1,64 @@
package com.yoyuzh.files;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
@Component
@Order(1)
@RequiredArgsConstructor
public class FileEntityBackfillService implements CommandLineRunner {
static final String PRIMARY_ENTITY_ROLE = "PRIMARY";
private final StoredFileRepository storedFileRepository;
private final FileEntityRepository fileEntityRepository;
private final StoredFileEntityRepository storedFileEntityRepository;
@Override
@Transactional
public void run(String... args) {
backfillPrimaryEntities();
}
@Transactional
public void backfillPrimaryEntities() {
for (StoredFile storedFile : storedFileRepository.findAllByDirectoryFalseAndBlobIsNotNullAndPrimaryEntityIsNull()) {
FileBlob blob = storedFile.getBlob();
Optional<FileEntity> existingEntity = fileEntityRepository
.findByObjectKeyAndEntityType(blob.getObjectKey(), FileEntityType.VERSION);
FileEntity fileEntity = existingEntity.orElseGet(() -> createEntity(storedFile, blob));
if (existingEntity.isPresent()) {
fileEntity.setReferenceCount(fileEntity.getReferenceCount() + 1);
fileEntityRepository.save(fileEntity);
}
storedFile.setPrimaryEntity(fileEntity);
storedFileRepository.save(storedFile);
storedFileEntityRepository.save(createRelation(storedFile, fileEntity));
}
}
private FileEntity createEntity(StoredFile storedFile, FileBlob blob) {
FileEntity fileEntity = new FileEntity();
fileEntity.setObjectKey(blob.getObjectKey());
fileEntity.setSize(blob.getSize());
fileEntity.setContentType(blob.getContentType());
fileEntity.setEntityType(FileEntityType.VERSION);
fileEntity.setReferenceCount(1);
fileEntity.setCreatedBy(storedFile.getUser());
return fileEntityRepository.save(fileEntity);
}
private StoredFileEntity createRelation(StoredFile storedFile, FileEntity fileEntity) {
StoredFileEntity relation = new StoredFileEntity();
relation.setStoredFile(storedFile);
relation.setFileEntity(fileEntity);
relation.setEntityRole(PRIMARY_ENTITY_ROLE);
return relation;
}
}

View File

@@ -0,0 +1,10 @@
package com.yoyuzh.files;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface FileEntityRepository extends JpaRepository<FileEntity, Long> {
Optional<FileEntity> findByObjectKeyAndEntityType(String objectKey, FileEntityType entityType);
}

View File

@@ -0,0 +1,9 @@
package com.yoyuzh.files;
public enum FileEntityType {
VERSION,
THUMBNAIL,
LIVE_PHOTO,
TRANSCODE,
AVATAR
}

View File

@@ -51,6 +51,8 @@ public class FileService {
private final StoredFileRepository storedFileRepository;
private final FileBlobRepository fileBlobRepository;
private final FileEntityRepository fileEntityRepository;
private final StoredFileEntityRepository storedFileEntityRepository;
private final FileContentStorage fileContentStorage;
private final FileShareLinkRepository fileShareLinkRepository;
private final AdminMetricsService adminMetricsService;
@@ -63,15 +65,19 @@ public class FileService {
@Autowired
public FileService(StoredFileRepository storedFileRepository,
FileBlobRepository fileBlobRepository,
FileEntityRepository fileEntityRepository,
StoredFileEntityRepository storedFileEntityRepository,
FileContentStorage fileContentStorage,
FileShareLinkRepository fileShareLinkRepository,
AdminMetricsService adminMetricsService,
FileStorageProperties properties) {
this(storedFileRepository, fileBlobRepository, fileContentStorage, fileShareLinkRepository, adminMetricsService, properties, Clock.systemUTC());
this(storedFileRepository, fileBlobRepository, fileEntityRepository, storedFileEntityRepository, fileContentStorage, fileShareLinkRepository, adminMetricsService, properties, Clock.systemUTC());
}
FileService(StoredFileRepository storedFileRepository,
FileBlobRepository fileBlobRepository,
FileEntityRepository fileEntityRepository,
StoredFileEntityRepository storedFileEntityRepository,
FileContentStorage fileContentStorage,
FileShareLinkRepository fileShareLinkRepository,
AdminMetricsService adminMetricsService,
@@ -79,6 +85,8 @@ public class FileService {
Clock clock) {
this.storedFileRepository = storedFileRepository;
this.fileBlobRepository = fileBlobRepository;
this.fileEntityRepository = fileEntityRepository;
this.storedFileEntityRepository = storedFileEntityRepository;
this.fileContentStorage = fileContentStorage;
this.fileShareLinkRepository = fileShareLinkRepository;
this.adminMetricsService = adminMetricsService;
@@ -93,6 +101,25 @@ public class FileService {
this.clock = clock;
}
FileService(StoredFileRepository storedFileRepository,
FileBlobRepository fileBlobRepository,
FileContentStorage fileContentStorage,
FileShareLinkRepository fileShareLinkRepository,
AdminMetricsService adminMetricsService,
FileStorageProperties properties) {
this(storedFileRepository, fileBlobRepository, null, null, fileContentStorage, fileShareLinkRepository, adminMetricsService, properties, Clock.systemUTC());
}
FileService(StoredFileRepository storedFileRepository,
FileBlobRepository fileBlobRepository,
FileContentStorage fileContentStorage,
FileShareLinkRepository fileShareLinkRepository,
AdminMetricsService adminMetricsService,
FileStorageProperties properties,
Clock clock) {
this(storedFileRepository, fileBlobRepository, null, null, fileContentStorage, fileShareLinkRepository, adminMetricsService, properties, clock);
}
@Transactional
public FileMetadataResponse upload(User user, String path, MultipartFile multipartFile) {
String normalizedPath = normalizeDirectoryPath(path);
@@ -349,7 +376,7 @@ public class FileService {
if (!storedFile.isDirectory()) {
ensureWithinStorageQuota(user, storedFile.getSize());
return toResponse(storedFileRepository.save(copyStoredFile(storedFile, user, normalizedTargetPath)));
return toResponse(saveCopiedStoredFile(copyStoredFile(storedFile, user, normalizedTargetPath), user));
}
String oldLogicalPath = buildLogicalPath(storedFile);
@@ -385,7 +412,7 @@ public class FileService {
StoredFile savedRoot = null;
for (StoredFile copiedEntry : copiedEntries) {
StoredFile savedEntry = storedFileRepository.save(copiedEntry);
StoredFile savedEntry = saveCopiedStoredFile(copiedEntry, user);
if (savedRoot == null) {
savedRoot = savedEntry;
}
@@ -685,7 +712,52 @@ public class FileService {
storedFile.setSize(size);
storedFile.setDirectory(false);
storedFile.setBlob(blob);
return toResponse(storedFileRepository.save(storedFile));
FileEntity primaryEntity = createOrReferencePrimaryEntity(user, blob);
storedFile.setPrimaryEntity(primaryEntity);
StoredFile savedFile = storedFileRepository.save(storedFile);
savePrimaryEntityRelation(savedFile, primaryEntity);
return toResponse(savedFile);
}
private FileEntity createOrReferencePrimaryEntity(User user, FileBlob blob) {
if (fileEntityRepository == null) {
return createTransientPrimaryEntity(user, blob);
}
Optional<FileEntity> existingEntity = fileEntityRepository.findByObjectKeyAndEntityType(
blob.getObjectKey(),
FileEntityType.VERSION
);
if (existingEntity.isPresent()) {
FileEntity entity = existingEntity.get();
entity.setReferenceCount(entity.getReferenceCount() + 1);
return fileEntityRepository.save(entity);
}
return fileEntityRepository.save(createTransientPrimaryEntity(user, blob));
}
private FileEntity createTransientPrimaryEntity(User user, FileBlob blob) {
FileEntity entity = new FileEntity();
entity.setObjectKey(blob.getObjectKey());
entity.setContentType(blob.getContentType());
entity.setSize(blob.getSize());
entity.setEntityType(FileEntityType.VERSION);
entity.setReferenceCount(1);
entity.setCreatedBy(user);
return entity;
}
private void savePrimaryEntityRelation(StoredFile storedFile, FileEntity primaryEntity) {
if (storedFileEntityRepository == null) {
return;
}
StoredFileEntity relation = new StoredFileEntity();
relation.setStoredFile(storedFile);
relation.setFileEntity(primaryEntity);
relation.setEntityRole("PRIMARY");
storedFileEntityRepository.save(relation);
}
private FileShareLink getShareLink(String token) {
@@ -961,6 +1033,17 @@ public class FileService {
return copiedFile;
}
private StoredFile saveCopiedStoredFile(StoredFile copiedFile, User owner) {
if (!copiedFile.isDirectory() && copiedFile.getBlob() != null && copiedFile.getPrimaryEntity() == null) {
copiedFile.setPrimaryEntity(createOrReferencePrimaryEntity(owner, copiedFile.getBlob()));
}
StoredFile savedFile = storedFileRepository.save(copiedFile);
if (!savedFile.isDirectory() && savedFile.getPrimaryEntity() != null) {
savePrimaryEntityRelation(savedFile, savedFile.getPrimaryEntity());
}
return savedFile;
}
private String buildZipEntryName(String rootDirectoryName, String rootLogicalPath, StoredFile storedFile) {
StringBuilder entryName = new StringBuilder(rootDirectoryName).append('/');
if (!storedFile.getPath().equals(rootLogicalPath)) {

View File

@@ -42,6 +42,10 @@ public class StoredFile {
@JoinColumn(name = "blob_id")
private FileBlob blob;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "primary_entity_id")
private FileEntity primaryEntity;
@Column(name = "storage_name", length = 255)
private String legacyStorageName;
@@ -57,6 +61,9 @@ public class StoredFile {
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
@@ -74,6 +81,9 @@ public class StoredFile {
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
if (updatedAt == null) {
updatedAt = createdAt;
}
}
public Long getId() {
@@ -116,6 +126,14 @@ public class StoredFile {
this.blob = blob;
}
public FileEntity getPrimaryEntity() {
return primaryEntity;
}
public void setPrimaryEntity(FileEntity primaryEntity) {
this.primaryEntity = primaryEntity;
}
public String getLegacyStorageName() {
return legacyStorageName;
}
@@ -156,6 +174,14 @@ public class StoredFile {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
public LocalDateTime getDeletedAt() {
return deletedAt;
}

View File

@@ -0,0 +1,92 @@
package com.yoyuzh.files;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.PrePersist;
import jakarta.persistence.Table;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import java.time.LocalDateTime;
@Entity
@Table(name = "portal_stored_file_entity", indexes = {
@Index(name = "uk_stored_file_entity_role", columnList = "stored_file_id,file_entity_id,entity_role", unique = true),
@Index(name = "idx_stored_file_entity_file", columnList = "stored_file_id"),
@Index(name = "idx_stored_file_entity_entity", columnList = "file_entity_id")
})
public class StoredFileEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "stored_file_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private StoredFile storedFile;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "file_entity_id", nullable = false)
private FileEntity fileEntity;
@Column(name = "entity_role", nullable = false, length = 32)
private String entityRole;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@PrePersist
public void prePersist() {
if (createdAt == null) {
createdAt = LocalDateTime.now();
}
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public StoredFile getStoredFile() {
return storedFile;
}
public void setStoredFile(StoredFile storedFile) {
this.storedFile = storedFile;
}
public FileEntity getFileEntity() {
return fileEntity;
}
public void setFileEntity(FileEntity fileEntity) {
this.fileEntity = fileEntity;
}
public String getEntityRole() {
return entityRole;
}
public void setEntityRole(String entityRole) {
this.entityRole = entityRole;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@@ -0,0 +1,6 @@
package com.yoyuzh.files;
import org.springframework.data.jpa.repository.JpaRepository;
public interface StoredFileEntityRepository extends JpaRepository<StoredFileEntity, Long> {
}

View File

@@ -123,4 +123,7 @@ public interface StoredFileRepository extends JpaRepository<StoredFile, Long> {
Optional<StoredFile> findDetailedById(@Param("id") Long id);
List<StoredFile> findAllByDirectoryFalseAndBlobIsNull();
@EntityGraph(attributePaths = {"user", "blob"})
List<StoredFile> findAllByDirectoryFalseAndBlobIsNotNullAndPrimaryEntityIsNull();
}

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());
}