feat(storage): 完善对象存储配置与过期策略

补齐 OSS/S3 的 Endpoint、Region、Bucket、CDN、对象前缀和签名有效期配置,并为生成结果与请求素材自动维护分级生命周期规则。普通上传继续保持永久,私有资源按配置生成限时签名 URL,管理端连接测试覆盖生命周期、上传、读取和删除。\n\n新增可重复的真实 OSS 验收脚本,凭据仅从本地环境读取,接口响应继续保持脱敏。\n\n验证:Go 全量测试、迁移安全检查、pnpm lint、pnpm test、pnpm build、本地阿里云 OSS 真实上传下载删除验收。
This commit is contained in:
2026-08-04 12:44:24 +08:00
parent fe56aa46b9
commit 6a64d3936a
16 changed files with 1274 additions and 47 deletions
@@ -280,7 +280,7 @@ func (s *Server) deleteFileStorageChannel(w http.ResponseWriter, r *http.Request
// testFileStorageChannel godoc
// @Summary 测试对象存储通道
// @Description 对指定 OSS 或 S3 通道执行隔离的 Put、Head、Delete 探针不返回凭据或对象键。
// @Description 对指定 OSS 或 S3 通道检查并初始化临时文件生命周期规则,然后执行隔离的 Put、Head、Delete 探针不返回凭据或对象键。
// @Tags system
// @Produce json
// @Security BearerAuth
@@ -358,9 +358,26 @@ func validateFileStorageChannelInput(input store.FileStorageChannelInput, existi
if !validFileStorageBaseURL(endpoint) {
return "object storage config.endpoint must be an http or https URL without embedded credentials"
}
if publicBaseURL := firstNonEmpty(fileStorageConfigString(input.Config, "publicBaseUrl"), fileStorageConfigString(input.Config, "publicBaseURL")); publicBaseURL != "" && !validFileStorageBaseURL(publicBaseURL) {
if publicBaseURL := firstNonEmpty(
fileStorageConfigString(input.Config, "publicBaseUrl"),
fileStorageConfigString(input.Config, "publicBaseURL"),
fileStorageConfigString(input.Config, "cdnDomain"),
fileStorageConfigString(input.Config, "publicDomain"),
); publicBaseURL != "" && !validFileStorageBaseURL(publicBaseURL) {
return "object storage config.publicBaseUrl must be an http or https URL without embedded credentials"
}
if policy := firstNonEmpty(
fileStorageConfigString(input.Config, "temporaryFileExpirePolicy"),
fileStorageConfigString(input.Config, "apiReturnExpirePolicy"),
); policy != "" && !validFileStorageExpirationPolicy(policy) {
return "object storage config.temporaryFileExpirePolicy must be never, 1d, 1m, 3m or 6m"
}
if value, exists := input.Config["signedUrlExpiresSeconds"]; exists {
seconds, ok := fileStorageConfigInteger(value)
if !ok || seconds < 60 || seconds > 7*24*60*60 {
return "object storage config.signedUrlExpiresSeconds must be an integer between 60 and 604800"
}
}
accessKeyID := input.AccessKeyID
if accessKeyID == nil {
accessKeyID = input.AccessKey
@@ -378,6 +395,34 @@ func validateFileStorageChannelInput(input store.FileStorageChannelInput, existi
return ""
}
func validFileStorageExpirationPolicy(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "never", "1d", "1m", "3m", "6m":
return true
default:
return false
}
}
func fileStorageConfigInteger(value any) (int64, bool) {
switch typed := value.(type) {
case int:
return int64(typed), true
case int32:
return int64(typed), true
case int64:
return typed, true
case float64:
converted := int64(typed)
return converted, float64(converted) == typed
case float32:
converted := int64(typed)
return converted, float32(converted) == typed
default:
return 0, false
}
}
func validFileStorageBaseURL(value string) bool {
parsed, err := url.Parse(strings.TrimSpace(value))
return err == nil && parsed.User == nil && parsed.Host != "" && (parsed.Scheme == "http" || parsed.Scheme == "https")