feat(files): add storage policy skeleton

This commit is contained in:
yoyuzh
2026-04-08 15:37:43 +08:00
parent f582e600aa
commit 6da0d196ee
16 changed files with 507 additions and 2 deletions

View File

@@ -85,6 +85,7 @@ public class UploadSessionV2Controller {
session.getFilename(),
session.getContentType(),
session.getSize(),
session.getStoragePolicyId(),
session.getStatus().name(),
session.getChunkSize(),
session.getChunkCount(),

View File

@@ -9,6 +9,7 @@ public record UploadSessionV2Response(
String filename,
String contentType,
long size,
Long storagePolicyId,
String status,
long chunkSize,
int chunkCount,

View File

@@ -0,0 +1,207 @@
package com.yoyuzh.files;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import java.time.LocalDateTime;
@Entity
@Table(name = "portal_storage_policy", indexes = {
@Index(name = "idx_storage_policy_enabled", columnList = "enabled"),
@Index(name = "idx_storage_policy_default", columnList = "default_policy")
})
public class StoragePolicy {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 128)
private String name;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 32)
private StoragePolicyType type;
@Column(name = "bucket_name", length = 255)
private String bucketName;
@Column(length = 512)
private String endpoint;
@Column(length = 64)
private String region;
@Column(name = "private_bucket", nullable = false)
private boolean privateBucket;
@Column(length = 512)
private String prefix;
@Enumerated(EnumType.STRING)
@Column(name = "credential_mode", nullable = false, length = 32)
private StoragePolicyCredentialMode credentialMode;
@Column(name = "max_size_bytes", nullable = false)
private long maxSizeBytes;
@Column(name = "capabilities_json", columnDefinition = "TEXT")
private String capabilitiesJson;
@Column(nullable = false)
private boolean enabled;
@Column(name = "default_policy", nullable = false)
private boolean defaultPolicy;
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
if (createdAt == null) {
createdAt = now;
}
if (updatedAt == null) {
updatedAt = now;
}
}
@PreUpdate
public void preUpdate() {
updatedAt = LocalDateTime.now();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public StoragePolicyType getType() {
return type;
}
public void setType(StoragePolicyType type) {
this.type = type;
}
public String getBucketName() {
return bucketName;
}
public void setBucketName(String bucketName) {
this.bucketName = bucketName;
}
public String getEndpoint() {
return endpoint;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
public boolean isPrivateBucket() {
return privateBucket;
}
public void setPrivateBucket(boolean privateBucket) {
this.privateBucket = privateBucket;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public StoragePolicyCredentialMode getCredentialMode() {
return credentialMode;
}
public void setCredentialMode(StoragePolicyCredentialMode credentialMode) {
this.credentialMode = credentialMode;
}
public long getMaxSizeBytes() {
return maxSizeBytes;
}
public void setMaxSizeBytes(long maxSizeBytes) {
this.maxSizeBytes = maxSizeBytes;
}
public String getCapabilitiesJson() {
return capabilitiesJson;
}
public void setCapabilitiesJson(String capabilitiesJson) {
this.capabilitiesJson = capabilitiesJson;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isDefaultPolicy() {
return defaultPolicy;
}
public void setDefaultPolicy(boolean defaultPolicy) {
this.defaultPolicy = defaultPolicy;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,14 @@
package com.yoyuzh.files;
public record StoragePolicyCapabilities(
boolean directUpload,
boolean multipartUpload,
boolean signedDownloadUrl,
boolean serverProxyDownload,
boolean thumbnailNative,
boolean friendlyDownloadName,
boolean requiresCors,
boolean supportsInternalEndpoint,
long maxObjectSize
) {
}

View File

@@ -0,0 +1,7 @@
package com.yoyuzh.files;
public enum StoragePolicyCredentialMode {
NONE,
STATIC,
DOGECLOUD_TEMP
}

View File

@@ -0,0 +1,10 @@
package com.yoyuzh.files;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface StoragePolicyRepository extends JpaRepository<StoragePolicy, Long> {
Optional<StoragePolicy> findFirstByDefaultPolicyTrueOrderByIdAsc();
}

View File

@@ -0,0 +1,121 @@
package com.yoyuzh.files;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yoyuzh.config.FileStorageProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@Service
@Order(-1)
@RequiredArgsConstructor
public class StoragePolicyService implements CommandLineRunner {
private final StoragePolicyRepository storagePolicyRepository;
private final FileStorageProperties properties;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
@Transactional
public void run(String... args) {
ensureDefaultPolicy();
}
@Transactional
public StoragePolicy ensureDefaultPolicy() {
return storagePolicyRepository.findFirstByDefaultPolicyTrueOrderByIdAsc()
.orElseGet(() -> storagePolicyRepository.save(createDefaultPolicy()));
}
public StoragePolicyCapabilities readCapabilities(StoragePolicy policy) {
try {
return objectMapper.readValue(policy.getCapabilitiesJson(), StoragePolicyCapabilities.class);
} catch (Exception ex) {
throw new IllegalStateException("Storage policy capabilities are invalid", ex);
}
}
private StoragePolicy createDefaultPolicy() {
if ("s3".equalsIgnoreCase(properties.getProvider())) {
return createDefaultS3Policy();
}
return createDefaultLocalPolicy();
}
private StoragePolicy createDefaultS3Policy() {
StoragePolicy policy = new StoragePolicy();
policy.setName("Default S3 Compatible Storage");
policy.setType(StoragePolicyType.S3_COMPATIBLE);
policy.setBucketName(extractScopeBucketName(properties.getS3().getScope()));
policy.setRegion(properties.getS3().getRegion());
policy.setPrivateBucket(true);
policy.setPrefix(extractScopePrefix(properties.getS3().getScope()));
policy.setCredentialMode(StoragePolicyCredentialMode.DOGECLOUD_TEMP);
policy.setMaxSizeBytes(properties.getMaxFileSize());
policy.setCapabilitiesJson(writeCapabilities(new StoragePolicyCapabilities(
true,
false,
true,
true,
false,
true,
true,
false,
properties.getMaxFileSize()
)));
policy.setEnabled(true);
policy.setDefaultPolicy(true);
return policy;
}
private StoragePolicy createDefaultLocalPolicy() {
StoragePolicy policy = new StoragePolicy();
policy.setName("Default Local Storage");
policy.setType(StoragePolicyType.LOCAL);
policy.setPrivateBucket(true);
policy.setPrefix(properties.getLocal().getRootDir());
policy.setCredentialMode(StoragePolicyCredentialMode.NONE);
policy.setMaxSizeBytes(properties.getMaxFileSize());
policy.setCapabilitiesJson(writeCapabilities(new StoragePolicyCapabilities(
false,
false,
false,
true,
false,
true,
false,
false,
properties.getMaxFileSize()
)));
policy.setEnabled(true);
policy.setDefaultPolicy(true);
return policy;
}
private String writeCapabilities(StoragePolicyCapabilities capabilities) {
try {
return objectMapper.writeValueAsString(capabilities);
} catch (Exception ex) {
throw new IllegalStateException("Storage policy capabilities cannot be serialized", ex);
}
}
private String extractScopeBucketName(String scope) {
if (!StringUtils.hasText(scope)) {
return null;
}
int separatorIndex = scope.indexOf(':');
return separatorIndex >= 0 ? scope.substring(0, separatorIndex) : scope;
}
private String extractScopePrefix(String scope) {
if (!StringUtils.hasText(scope)) {
return "";
}
int separatorIndex = scope.indexOf(':');
return separatorIndex >= 0 ? scope.substring(separatorIndex + 1) : "";
}
}

View File

@@ -0,0 +1,6 @@
package com.yoyuzh.files;
public enum StoragePolicyType {
LOCAL,
S3_COMPATIBLE
}

View File

@@ -55,6 +55,9 @@ public class UploadSession {
@Column(name = "object_key", nullable = false, length = 512)
private String objectKey;
@Column(name = "storage_policy_id")
private Long storagePolicyId;
@Column(name = "chunk_size", nullable = false)
private Long chunkSize;
@@ -160,6 +163,14 @@ public class UploadSession {
this.objectKey = objectKey;
}
public Long getStoragePolicyId() {
return storagePolicyId;
}
public void setStoragePolicyId(Long storagePolicyId) {
this.storagePolicyId = storagePolicyId;
}
public Long getChunkSize() {
return chunkSize;
}

View File

@@ -37,6 +37,7 @@ public class UploadSessionService {
private final StoredFileRepository storedFileRepository;
private final FileService fileService;
private final FileContentStorage fileContentStorage;
private final StoragePolicyService storagePolicyService;
private final ObjectMapper objectMapper = new ObjectMapper();
private final long maxFileSize;
private final Clock clock;
@@ -46,20 +47,23 @@ public class UploadSessionService {
StoredFileRepository storedFileRepository,
FileService fileService,
FileContentStorage fileContentStorage,
StoragePolicyService storagePolicyService,
FileStorageProperties properties) {
this(uploadSessionRepository, storedFileRepository, fileService, fileContentStorage, properties, Clock.systemUTC());
this(uploadSessionRepository, storedFileRepository, fileService, fileContentStorage, storagePolicyService, properties, Clock.systemUTC());
}
UploadSessionService(UploadSessionRepository uploadSessionRepository,
StoredFileRepository storedFileRepository,
FileService fileService,
FileContentStorage fileContentStorage,
StoragePolicyService storagePolicyService,
FileStorageProperties properties,
Clock clock) {
this.uploadSessionRepository = uploadSessionRepository;
this.storedFileRepository = storedFileRepository;
this.fileService = fileService;
this.fileContentStorage = fileContentStorage;
this.storagePolicyService = storagePolicyService;
this.maxFileSize = properties.getMaxFileSize();
this.clock = clock;
}
@@ -78,6 +82,7 @@ public class UploadSessionService {
session.setContentType(command.contentType());
session.setSize(command.size());
session.setObjectKey(createBlobObjectKey());
session.setStoragePolicyId(storagePolicyService.ensureDefaultPolicy().getId());
session.setChunkSize(DEFAULT_CHUNK_SIZE);
session.setChunkCount(calculateChunkCount(command.size(), DEFAULT_CHUNK_SIZE));
session.setUploadedPartsJson("[]");