整体平台上传功能大改造
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
)
|
||||
|
||||
// FileUploadParams 创建文件记录所需参数
|
||||
type FileUploadParams struct {
|
||||
Source string // backend / platform
|
||||
Scope string // tenant / user
|
||||
Tid uint64
|
||||
Tuid uint64 // 归属用户,0 表示租户共享
|
||||
Uid uint64 // 上传者
|
||||
Name string
|
||||
Ext string
|
||||
Cate uint64
|
||||
Size uint64
|
||||
Src string
|
||||
ObjectKey string
|
||||
Storage string
|
||||
MD5 string
|
||||
}
|
||||
|
||||
// FindDuplicate 按「来源端 + 归属范围 + 租户 + 归属用户 + MD5」精确查重。
|
||||
//
|
||||
// 规则:
|
||||
// - 租户共享文件:同一 tid 内 MD5 相同才算重复(tuid 为 NULL)
|
||||
// - 用户个人文件:同一 tid + 同一 tuid 内 MD5 相同才算重复
|
||||
// - 因此「租户已有 a 文件」与「用户 c 上传同样文件」互不冲突,可并存
|
||||
//
|
||||
// 未命中时返回 (nil, nil),调用方按 err == nil && file == nil 处理即可。
|
||||
func FindDuplicate(source, scope string, tid, tuid uint64, md5Str string) (*models.SystemFile, error) {
|
||||
if md5Str == "" {
|
||||
return nil, nil
|
||||
}
|
||||
models.EnsureSystemFileStorageColumns()
|
||||
|
||||
qs := models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("source", source).
|
||||
Filter("scope", scope).
|
||||
Filter("tid", tid).
|
||||
Filter("md5", md5Str).
|
||||
Filter("delete_time__isnull", true)
|
||||
|
||||
if scope == ScopeUser {
|
||||
qs = qs.Filter("tuid", tuid)
|
||||
} else {
|
||||
qs = qs.Filter("tuid__isnull", true)
|
||||
}
|
||||
|
||||
var f models.SystemFile
|
||||
if err := qs.OrderBy("-id").One(&f); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
// CreateFileRecord 写入文件记录(同步写入归属与存储字段)
|
||||
func CreateFileRecord(p FileUploadParams) (uint64, error) {
|
||||
models.EnsureSystemFileStorageColumns()
|
||||
|
||||
uid := p.Uid
|
||||
row := &models.SystemFile{
|
||||
Tid: p.Tid,
|
||||
Uid: &uid,
|
||||
Name: p.Name,
|
||||
Type: DetectFileType(p.Ext),
|
||||
Cate: p.Cate,
|
||||
Size: p.Size,
|
||||
Src: p.Src,
|
||||
Uploader: p.Uid,
|
||||
Md5: p.MD5,
|
||||
Source: p.Source,
|
||||
Scope: p.Scope,
|
||||
Storage: p.Storage,
|
||||
ObjectKey: p.ObjectKey,
|
||||
}
|
||||
if p.Tuid > 0 {
|
||||
tuid := p.Tuid
|
||||
row.Tuid = &tuid
|
||||
}
|
||||
if row.Source == "" {
|
||||
row.Source = SourceBackend
|
||||
}
|
||||
if row.Scope == "" {
|
||||
row.Scope = ScopeTenant
|
||||
}
|
||||
|
||||
id, err := models.Orm.Insert(row)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint64(id), nil
|
||||
}
|
||||
|
||||
// RemovePhysical 删除物理文件。优先用 object_key,老数据则从 src 反推。
|
||||
func RemovePhysical(svc StorageService, objectKey, src string) error {
|
||||
if svc == nil {
|
||||
return fmt.Errorf("存储服务未初始化")
|
||||
}
|
||||
key := strings.TrimSpace(objectKey)
|
||||
if key == "" {
|
||||
key = KeyFromSrc(src, svc)
|
||||
}
|
||||
if key == "" {
|
||||
return fmt.Errorf("无法解析文件路径: %s", src)
|
||||
}
|
||||
return svc.Delete(key)
|
||||
}
|
||||
|
||||
// KeyFromSrc 从访问 URL/相对路径中解析出存储 key(object_key)
|
||||
func KeyFromSrc(src string, svc StorageService) string {
|
||||
src = strings.TrimSpace(src)
|
||||
if src == "" {
|
||||
return ""
|
||||
}
|
||||
switch s := svc.(type) {
|
||||
case *LocalStorage:
|
||||
base := filepath.ToSlash(s.BaseDir)
|
||||
rel := src
|
||||
if i := strings.Index(rel, base+"/"); i >= 0 {
|
||||
rel = rel[i+len(base)+1:]
|
||||
}
|
||||
rel = strings.TrimPrefix(rel, "/")
|
||||
// 去掉可能的查询串
|
||||
if i := strings.IndexAny(rel, "?#"); i >= 0 {
|
||||
rel = rel[:i]
|
||||
}
|
||||
return rel
|
||||
case *QiniuStorage:
|
||||
domain := strings.TrimRight(s.Domain, "/")
|
||||
rel := src
|
||||
if domain != "" && strings.HasPrefix(rel, domain) {
|
||||
rel = strings.TrimPrefix(strings.TrimPrefix(rel, domain), "/")
|
||||
} else if u, err := url.Parse(rel); err == nil && u.Host != "" {
|
||||
rel = strings.TrimPrefix(u.Path, "/")
|
||||
}
|
||||
rel = strings.TrimPrefix(rel, "/")
|
||||
if i := strings.IndexAny(rel, "?#"); i >= 0 {
|
||||
rel = rel[:i]
|
||||
}
|
||||
return rel
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// 文件类型与扩展名白名单(两端共用同一套规则)
|
||||
var fileTypeByCategory = map[string]uint8{
|
||||
"image": 1,
|
||||
"document": 2,
|
||||
"video": 3,
|
||||
"audio": 4,
|
||||
"appsupgrade": 2,
|
||||
}
|
||||
|
||||
var allowedExtByCategory = map[string][]string{
|
||||
"image": {"jpg", "jpeg", "png", "gif", "bmp", "webp"},
|
||||
"document": {"pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt"},
|
||||
"video": {"mp4", "webm", "mov"},
|
||||
"audio": {"mp3", "wav", "ogg"},
|
||||
"appsupgrade": {"zip", "exe", "dmg", "msi", "msix", "apk", "deb", "rpm", "7z", "tar", "gz", "pkg"},
|
||||
}
|
||||
|
||||
// FileExt 取小写扩展名(不含点)
|
||||
func FileExt(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
if i := strings.LastIndex(name, "."); i >= 0 && i < len(name)-1 {
|
||||
return strings.ToLower(name[i+1:])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DetectFileType 根据扩展名推断文件类型:1图片 2文档 3视频 4音频,未匹配返回 2(文档/其他)
|
||||
func DetectFileType(ext string) uint8 {
|
||||
ext = strings.ToLower(strings.TrimPrefix(ext, "."))
|
||||
for cat, exts := range allowedExtByCategory {
|
||||
for _, e := range exts {
|
||||
if e == ext {
|
||||
if t, ok := fileTypeByCategory[cat]; ok {
|
||||
return t
|
||||
}
|
||||
return 2
|
||||
}
|
||||
}
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
// SoftDeleteFiles 软删除(标记 delete_time)
|
||||
func SoftDeleteFiles(ids []uint64) (int64, error) {
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
models.EnsureSystemFileStorageColumns()
|
||||
return models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id__in", ids).
|
||||
Filter("delete_time__isnull", true).
|
||||
Update(map[string]interface{}{"delete_time": time.Now()})
|
||||
}
|
||||
|
||||
// DeleteFilesPermanently 彻底删除数据库记录(物理文件由调用方先删)
|
||||
func DeleteFilesPermanently(ids []uint64) (int64, error) {
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
models.EnsureSystemFileStorageColumns()
|
||||
return models.Orm.QueryTable(new(models.SystemFile)).
|
||||
Filter("id__in", ids).
|
||||
Delete()
|
||||
}
|
||||
Reference in New Issue
Block a user