feat: 媒体库 blob 去重存储与品牌/注册等前端调整
新增: - blob_store 二进制去重存储 + blob_dedup 测试 其它: - 后端 settings/auth/media_library/upload/operations 服务与 handler 调整 - 前端品牌(BrandLockup/BrandCropModal/BrandSeoPanel)、注册、媒体库、分析详情页、Header/SiteChrome 等更新
This commit is contained in:
@@ -2,10 +2,12 @@ package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"unicode/utf8"
|
||||
@@ -145,8 +147,9 @@ func (s *PostFileService) SaveDraftFile(userID uint, originalName string, src io
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// LimitReader 多读 1 字节以区分「刚好上限」与「超限」
|
||||
written, copyErr := io.Copy(f, io.LimitReader(src, maxB+1))
|
||||
// LimitReader 多读 1 字节以区分「刚好上限」与「超限」;落盘同时计算内容 hash
|
||||
hasher := sha256.New()
|
||||
written, copyErr := io.Copy(io.MultiWriter(f, hasher), io.LimitReader(src, maxB+1))
|
||||
_ = f.Close()
|
||||
if copyErr != nil {
|
||||
_ = os.Remove(tmp)
|
||||
@@ -167,28 +170,45 @@ func (s *PostFileService) SaveDraftFile(userID uint, originalName string, src io
|
||||
return nil, err
|
||||
}
|
||||
|
||||
att := &model.PostAttachment{
|
||||
PostID: 0,
|
||||
UserID: userID,
|
||||
Name: name,
|
||||
StoredName: stored,
|
||||
MIME: mimeType,
|
||||
Size: int(written),
|
||||
PricePoints: pricePoints,
|
||||
}
|
||||
// 内容寻址:私有 blob 以 hash 命名(不带扩展名,下载时以 att.MIME + 原文件名下发)
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
final := absPath(s.dir, hash)
|
||||
var objectID string
|
||||
if s.ops != nil {
|
||||
id, e := s.ops.StoreFile(full, att.MIME, false)
|
||||
id, e := s.ops.StoreFileHashed(full, mimeType, false, hash)
|
||||
if e != nil {
|
||||
_ = os.Remove(full)
|
||||
return nil, e
|
||||
}
|
||||
att.ObjectID = id
|
||||
objectID = id
|
||||
if id != "" {
|
||||
_ = os.Remove(full)
|
||||
}
|
||||
}
|
||||
if err := s.db.Create(att).Error; err != nil {
|
||||
_ = os.Remove(full)
|
||||
if objectID == "" {
|
||||
if e := placeLocalBlob(full, final); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
|
||||
att := &model.PostAttachment{
|
||||
PostID: 0,
|
||||
UserID: userID,
|
||||
Name: name,
|
||||
StoredName: hash,
|
||||
BlobHash: hash,
|
||||
ObjectID: objectID,
|
||||
MIME: mimeType,
|
||||
Size: int(written),
|
||||
PricePoints: pricePoints,
|
||||
}
|
||||
if err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if e := acquireBlobRef(tx, hash, false, int(written), mimeType, objectID); e != nil {
|
||||
return e
|
||||
}
|
||||
return tx.Create(att).Error
|
||||
}); err != nil {
|
||||
s.releasePostBlobBestEffort(hash)
|
||||
return nil, err
|
||||
}
|
||||
return att, nil
|
||||
@@ -283,14 +303,51 @@ func (s *PostFileService) DeleteOwn(userID, attID uint) error {
|
||||
if att.UserID != userID {
|
||||
return ErrAttachmentForbidden
|
||||
}
|
||||
path := absPath(s.dir, att.StoredName)
|
||||
if err := s.db.Delete(&att).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if att.BlobHash != "" {
|
||||
return s.releasePostBlob(&att)
|
||||
}
|
||||
if s.ops != nil && att.ObjectID != "" {
|
||||
return s.ops.RemoveObject(att.ObjectID)
|
||||
}
|
||||
_ = os.Remove(path)
|
||||
_ = os.Remove(absPath(s.dir, att.StoredName))
|
||||
return nil
|
||||
}
|
||||
|
||||
// releasePostBlob 私有附件 blob 引用计数-1;最后一个引用删除时回收物理文件
|
||||
func (s *PostFileService) releasePostBlob(att *model.PostAttachment) error {
|
||||
b, last, err := releaseBlobRef(s.db, att.BlobHash, false)
|
||||
if err != nil || !last {
|
||||
return err
|
||||
}
|
||||
return s.removePrivateBlobObject(b)
|
||||
}
|
||||
|
||||
// releasePostBlobBestEffort 附件记录落库失败时回滚刚加的引用,错误只记日志
|
||||
func (s *PostFileService) releasePostBlobBestEffort(hash string) {
|
||||
b, last, err := releaseBlobRef(s.db, hash, false)
|
||||
if err != nil {
|
||||
log.Printf("[post-file] 回滚 blob 引用失败 hash=%s: %v", hash, err)
|
||||
return
|
||||
}
|
||||
if !last {
|
||||
return
|
||||
}
|
||||
if err := s.removePrivateBlobObject(b); err != nil {
|
||||
log.Printf("[post-file] 回滚 blob 物理文件失败 hash=%s: %v", hash, err)
|
||||
}
|
||||
}
|
||||
|
||||
// removePrivateBlobObject 引用归零后的物理回收(私有 blob 文件名即 hash,无扩展名)
|
||||
func (s *PostFileService) removePrivateBlobObject(b model.Blob) error {
|
||||
if b.ObjectID != "" && s.ops != nil {
|
||||
return s.ops.RemoveObject(b.ObjectID)
|
||||
}
|
||||
if err := os.Remove(absPath(s.dir, b.Hash)); err != nil && !os.IsNotExist(err) {
|
||||
log.Printf("[post-file] 删除 blob 文件失败 hash=%s: %v", b.Hash, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user