feat: align storage object keys

This commit is contained in:
2026-01-08 15:22:40 +08:00
parent 675e7a6783
commit 6ffe58ffb4

View File

@@ -8,9 +8,11 @@ import (
"io" "io"
"mime/multipart" "mime/multipart"
"os" "os"
"path"
"path/filepath" "path/filepath"
"sort" "sort"
"strconv" "strconv"
"strings"
"time" "time"
"quyun/v2/app/errorx" "quyun/v2/app/errorx"
@@ -93,6 +95,16 @@ type UploadMeta struct {
MimeType string MimeType string
} }
func (s *common) buildObjectKey(tenant *models.Tenant, hash, filename string) string {
// 按租户维度组织对象路径quyun/<tenant_uuid>/<hash>.<ext>
tenantUUID := "public"
if tenant != nil && tenant.UUID.String() != "" {
tenantUUID = tenant.UUID.String()
}
ext := strings.ToLower(filepath.Ext(filename))
return path.Join("quyun", tenantUUID, hash+ext)
}
func (s *common) InitUpload(ctx context.Context, userID int64, form *common_dto.UploadInitForm) (*common_dto.UploadInitResponse, error) { func (s *common) InitUpload(ctx context.Context, userID int64, form *common_dto.UploadInitForm) (*common_dto.UploadInitResponse, error) {
uploadID := uuid.NewString() uploadID := uuid.NewString()
localPath := s.storage.Config.LocalPath localPath := s.storage.Config.LocalPath
@@ -177,9 +189,8 @@ func (s *common) CompleteUpload(ctx context.Context, userID int64, form *common_
} }
sort.Ints(parts) sort.Ints(parts)
objectKey := uuid.NewString() + "_" + meta.Filename mergedPath := filepath.Join(tempDir, "merged")
dstPath := filepath.Join(localPath, objectKey) dst, err := os.Create(mergedPath)
dst, err := os.Create(dstPath)
if err != nil { if err != nil {
return nil, errorx.ErrInternalError.WithCause(err) return nil, errorx.ErrInternalError.WithCause(err)
} }
@@ -205,7 +216,6 @@ func (s *common) CompleteUpload(ctx context.Context, userID int64, form *common_
hash := hex.EncodeToString(hasher.Sum(nil)) hash := hex.EncodeToString(hasher.Sum(nil))
dst.Close() // Ensure flush before potential removal dst.Close() // Ensure flush before potential removal
os.RemoveAll(tempDir)
// Deduplication Logic (Similar to Upload) // Deduplication Logic (Similar to Upload)
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(userID)).First() t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(userID)).First()
@@ -214,15 +224,17 @@ func (s *common) CompleteUpload(ctx context.Context, userID int64, form *common_
tid = t.ID tid = t.ID
} }
objectKey := s.buildObjectKey(t, hash, meta.Filename)
existing, err := models.MediaAssetQuery.WithContext(ctx).Where(models.MediaAssetQuery.Hash.Eq(hash)).First() existing, err := models.MediaAssetQuery.WithContext(ctx).Where(models.MediaAssetQuery.Hash.Eq(hash)).First()
var asset *models.MediaAsset var asset *models.MediaAsset
if err == nil { if err == nil {
os.Remove(dstPath) // Delete duplicate os.Remove(mergedPath) // Delete duplicate
myExisting, err := models.MediaAssetQuery.WithContext(ctx). myExisting, err := models.MediaAssetQuery.WithContext(ctx).
Where(models.MediaAssetQuery.Hash.Eq(hash), models.MediaAssetQuery.UserID.Eq(userID)). Where(models.MediaAssetQuery.Hash.Eq(hash), models.MediaAssetQuery.UserID.Eq(userID)).
First() First()
if err == nil { if err == nil {
os.RemoveAll(tempDir)
return s.composeUploadResult(myExisting), nil return s.composeUploadResult(myExisting), nil
} }
asset = &models.MediaAsset{ asset = &models.MediaAsset{
@@ -237,6 +249,13 @@ func (s *common) CompleteUpload(ctx context.Context, userID int64, form *common_
Meta: existing.Meta, Meta: existing.Meta,
} }
} else { } else {
finalPath := filepath.Join(localPath, objectKey)
if err := os.MkdirAll(filepath.Dir(finalPath), 0o755); err != nil {
return nil, errorx.ErrInternalError.WithCause(err)
}
if err := os.Rename(mergedPath, finalPath); err != nil {
return nil, errorx.ErrInternalError.WithCause(err)
}
asset = &models.MediaAsset{ asset = &models.MediaAsset{
TenantID: tid, TenantID: tid,
UserID: userID, UserID: userID,
@@ -253,6 +272,7 @@ func (s *common) CompleteUpload(ctx context.Context, userID int64, form *common_
} }
} }
os.RemoveAll(tempDir)
if err := models.MediaAssetQuery.WithContext(ctx).Create(asset); err != nil { if err := models.MediaAssetQuery.WithContext(ctx).Create(asset); err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err) return nil, errorx.ErrDatabaseError.WithCause(err)
} }
@@ -303,8 +323,6 @@ func (s *common) Upload(
) (*common_dto.UploadResult, error) { ) (*common_dto.UploadResult, error) {
// But this Upload endpoint accepts file. So we save it. // But this Upload endpoint accepts file. So we save it.
objectKey := uuid.NewString() + "_" + file.Filename
// Save file content to local storage // Save file content to local storage
src, err := file.Open() src, err := file.Open()
if err != nil { if err != nil {
@@ -316,13 +334,13 @@ func (s *common) Upload(
if localPath == "" { if localPath == "" {
localPath = "./storage" // Fallback localPath = "./storage" // Fallback
} }
dstPath := filepath.Join(localPath, objectKey) tmpDir := filepath.Join(localPath, "temp", "uploads", uuid.NewString())
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
return nil, errorx.ErrInternalError.WithCause(err).WithMsg("failed to create storage directory") return nil, errorx.ErrInternalError.WithCause(err).WithMsg("failed to create storage directory")
} }
tmpPath := filepath.Join(tmpDir, "file")
dst, err := os.Create(dstPath) dst, err := os.Create(tmpPath)
if err != nil { if err != nil {
return nil, errorx.ErrInternalError.WithCause(err).WithMsg("failed to create destination file") return nil, errorx.ErrInternalError.WithCause(err).WithMsg("failed to create destination file")
} }
@@ -343,13 +361,15 @@ func (s *common) Upload(
tid = t.ID tid = t.ID
} }
objectKey := s.buildObjectKey(t, hash, file.Filename)
var asset *models.MediaAsset var asset *models.MediaAsset
// Deduplication Check // Deduplication Check
existing, err := models.MediaAssetQuery.WithContext(ctx).Where(models.MediaAssetQuery.Hash.Eq(hash)).First() existing, err := models.MediaAssetQuery.WithContext(ctx).Where(models.MediaAssetQuery.Hash.Eq(hash)).First()
if err == nil { if err == nil {
// Found existing file (Storage Deduplication) // Found existing file (Storage Deduplication)
os.Remove(dstPath) // Delete the duplicate we just wrote os.Remove(tmpPath) // Delete the duplicate we just wrote
os.RemoveAll(tmpDir)
// Check if user already has it (Logic Deduplication) // Check if user already has it (Logic Deduplication)
myExisting, err := models.MediaAssetQuery.WithContext(ctx). myExisting, err := models.MediaAssetQuery.WithContext(ctx).
@@ -372,6 +392,15 @@ func (s *common) Upload(
Meta: existing.Meta, Meta: existing.Meta,
} }
} else { } else {
dstPath := filepath.Join(localPath, objectKey)
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
return nil, errorx.ErrInternalError.WithCause(err).WithMsg("failed to create storage directory")
}
if err := os.Rename(tmpPath, dstPath); err != nil {
return nil, errorx.ErrInternalError.WithCause(err).WithMsg("failed to finalize file")
}
os.RemoveAll(tmpDir)
// New unique file // New unique file
asset = &models.MediaAsset{ asset = &models.MediaAsset{
TenantID: tid, TenantID: tid,