feat: 媒体库 blob 去重存储与品牌/注册等前端调整
新增: - blob_store 二进制去重存储 + blob_dedup 测试 其它: - 后端 settings/auth/media_library/upload/operations 服务与 handler 调整 - 前端品牌(BrandLockup/BrandCropModal/BrandSeoPanel)、注册、媒体库、分析详情页、Header/SiteChrome 等更新
This commit is contained in:
@@ -16,6 +16,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (o *Operations) s3(c StorageConfig) (*minio.Client, error) {
|
||||
@@ -137,6 +139,65 @@ func (o *Operations) StoreFile(path, mimeType string, public bool) (string, erro
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// blobObjectID 内容寻址对象的稳定 ID:公开/私有加不同前缀,同字节也不共享可见性。
|
||||
func blobObjectID(hash string, public bool) string {
|
||||
if public {
|
||||
return "pub_" + hash
|
||||
}
|
||||
return "prv_" + hash
|
||||
}
|
||||
|
||||
// StoreFileHashed 按内容 hash 幂等上传:同一 (hash, public) 只存一份,重复上传跳过传输。
|
||||
// 本地后端返回空 ID(文件留在本地 blob 目录,由调用方做引用计数)。
|
||||
func (o *Operations) StoreFileHashed(path, mimeType string, public bool, hash string) (string, error) {
|
||||
v, version, e := o.read(o.db, "storage")
|
||||
if e != nil {
|
||||
return "", errors.New("读取存储配置失败")
|
||||
}
|
||||
c := *v.(*StorageConfig)
|
||||
if c.Backend == "local" {
|
||||
return "", nil
|
||||
}
|
||||
id := blobObjectID(hash, public)
|
||||
// hash 相同字节必然相同:对象已存在时直接复用,省掉整次上传
|
||||
var existing model.StoredObject
|
||||
if e := o.db.First(&existing, "id = ?", id).Error; e == nil {
|
||||
return id, nil
|
||||
} else if !errors.Is(e, gorm.ErrRecordNotFound) {
|
||||
return "", e
|
||||
}
|
||||
client, e := o.s3(c)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
f, e := os.Open(path)
|
||||
if e != nil {
|
||||
return "", e
|
||||
}
|
||||
stat, e := f.Stat()
|
||||
if e != nil {
|
||||
_ = f.Close()
|
||||
return "", e
|
||||
}
|
||||
key := c.Prefix + "objects/" + id
|
||||
if _, e = client.PutObject(ctx, c.Bucket, key, f, stat.Size(), minio.PutObjectOptions{ContentType: mimeType, DisableMultipart: true}); e != nil {
|
||||
_ = f.Close()
|
||||
return "", errors.New("上传到 S3 失败,当前目标未自动切换")
|
||||
}
|
||||
_ = f.Close()
|
||||
obj := model.StoredObject{ID: id, ConfigName: fmt.Sprintf("storage-%d", version), Key: key, MIME: mimeType, Public: public}
|
||||
if e = o.db.Create(&obj).Error; e != nil {
|
||||
if isUniqueConflict(e) { // 并发:另一请求已写入同内容对象,直接复用
|
||||
return id, nil
|
||||
}
|
||||
_ = client.RemoveObject(ctx, c.Bucket, key, minio.RemoveObjectOptions{})
|
||||
return "", errors.New("保存文件记录失败")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
func (o *Operations) OpenObject(ctx context.Context, id string, requirePublic bool) (io.ReadCloser, string, error) {
|
||||
var obj model.StoredObject
|
||||
if e := o.db.First(&obj, "id = ?", id).Error; e != nil {
|
||||
|
||||
Reference in New Issue
Block a user