@@ -104,9 +104,16 @@ func (s *common) CheckHash(ctx context.Context, tenantID, userID int64, hash str
}
type UploadMeta struct {
Filename string
Type string
MimeType string
// Filename 原始文件名,用于生成对象路径。
Filename string ` json:"filename" `
// Type 业务媒体类型( video/audio/image 等)。
Type string ` json:"type" `
// MimeType 上传文件的 MIME 类型。
MimeType string ` json:"mime_type" `
// TenantID 上传所属租户ID, 用于归属校验。
TenantID int64 ` json:"tenant_id" `
// UserID 上传发起用户ID, 用于归属校验。
UserID int64 ` json:"user_id" `
}
func ( s * common ) buildObjectKey ( tenant * models . Tenant , hash , filename string ) string {
@@ -119,6 +126,31 @@ func (s *common) buildObjectKey(tenant *models.Tenant, hash, filename string) st
return path . Join ( "quyun" , tenantUUID , hash + ext )
}
func ( s * common ) loadUploadMeta ( tempDir string ) ( * UploadMeta , error ) {
metaFile , err := os . Open ( filepath . Join ( tempDir , "meta.json" ) )
if err != nil {
if errors . Is ( err , os . ErrNotExist ) {
return nil , errorx . ErrRecordNotFound . WithCause ( err ) . WithMsg ( "上传会话不存在" )
}
return nil , errorx . ErrInternalError . WithCause ( err )
}
defer metaFile . Close ( )
var meta UploadMeta
if err := json . NewDecoder ( metaFile ) . Decode ( & meta ) ; err != nil {
return nil , errorx . ErrDataCorrupted . WithCause ( err ) . WithMsg ( "上传会话元信息损坏" )
}
return & meta , nil
}
func ( s * common ) verifyUploadOwner ( meta * UploadMeta , tenantID , userID int64 ) error {
// 校验上传会话归属,避免同租户猜测 upload_id 进行越权操作。
if meta . TenantID != tenantID || meta . UserID != userID {
return errorx . ErrForbidden . WithMsg ( "无权访问该上传会话" )
}
return nil
}
func ( s * common ) resolveTenant ( ctx context . Context , tenantID , userID int64 ) ( * models . Tenant , error ) {
if tenantID > 0 {
tbl , q := models . TenantQuery . QueryContext ( ctx )
@@ -169,6 +201,8 @@ func (s *common) InitUpload(ctx context.Context, tenantID, userID int64, form *c
Filename : form . Filename ,
Type : form . Type , // Ensure form has Type
MimeType : form . MimeType ,
TenantID : tenantID ,
UserID : userID ,
}
metaFile , _ := os . Create ( filepath . Join ( tempDir , "meta.json" ) )
json . NewEncoder ( metaFile ) . Encode ( meta )
@@ -185,7 +219,15 @@ func (s *common) UploadPart(ctx context.Context, tenantID, userID int64, file *m
if localPath == "" {
localPath = "./storage"
}
partPath := filepath . Join ( s . uploadTempDir ( localPath , tenantID , form . UploadID ) , strconv . Itoa ( form . PartNumber ) )
tempDir := s . uploadTempDir ( localPath , tenantID , form . UploadID )
meta , err := s . loadUploadMeta ( tempDir )
if err != nil {
return err
}
if err := s . verifyUploadOwner ( meta , tenantID , userID ) ; err != nil {
return err
}
partPath := filepath . Join ( tempDir , strconv . Itoa ( form . PartNumber ) )
src , err := file . Open ( )
if err != nil {
@@ -212,14 +254,14 @@ func (s *common) CompleteUpload(ctx context.Context, tenantID, userID int64, for
}
tempDir := s . uploadTempDir ( localPath , tenantID , form . UploadID )
// Read Meta
var meta UploadMeta
metaFile , err := os . Open ( filepath . Join ( tempDir , "meta.json" ) )
// 校验上传会话归属,避免同租户猜测 upload_id 进行越权操作。
meta , err := s . loadUploadMeta ( tempDir )
if err != nil {
return nil , errorx . ErrRecordNotFound . WithMsg ( "Upload session expired or invalid" )
return nil , err
}
if err := s . verifyUploadOwner ( meta , tenantID , userID ) ; err != nil {
return nil , err
}
json . NewDecoder ( metaFile ) . Decode ( & meta )
metaFile . Close ( )
// List parts
entries , err := os . ReadDir ( tempDir )
@@ -373,6 +415,13 @@ func (s *common) AbortUpload(ctx context.Context, tenantID, userID int64, upload
localPath = "./storage"
}
tempDir := s . uploadTempDir ( localPath , tenantID , uploadId )
meta , err := s . loadUploadMeta ( tempDir )
if err != nil {
return err
}
if err := s . verifyUploadOwner ( meta , tenantID , userID ) ; err != nil {
return err
}
return os . RemoveAll ( tempDir )
}