CI 测试 / go vet + go test (push) Failing after 5s
- 对象存储直链:S3 引擎 302 到限时预签名 URL(有效期钳位分享剩余时效),失败自动回落代理 - 过期回收:janitor 定时扫描 + 取件惰性回收 + 管理端手动触发(POST /admin/recycle/run), retention_days 最长存储时长;删除走引用计数(去重对象安全) - SHA512 内容去重:三条上传链路落库后计算哈希,命中即复用旧对象并删除本次副本 - 下载防盗链:Referer 白名单(同源/空 Referer/通配域名放行),挂 /share/download - 取件页图片/音频内联预览(下载地址直连,加载失败回退下载按钮) - 文件夹上传:拖拽目录明确提示"建议压缩后上传"(webkitGetAsEntry 探测) - 管理端设置卡「回收与下载安全」8 个新配置键(KVSchema + configKeys + UI + i18n) - 前端产物重建并同步 server/web/dist 与 web-embed - 文档:10-config 配置表、03-file-share 直链/防盗链/文件夹章节、07-admin 回收端点、openapi
81 lines
2.7 KiB
Go
81 lines
2.7 KiB
Go
// Package api — dedup.go SHA512 内容去重(26.9):
|
|
// 上传完成后计算对象 SHA512,命中已有分享(同哈希+同引擎)则复用其存储对象、
|
|
// 删除本次上传的副本——相同文件只存储一份。历史数据(无哈希)不受影响。
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"io"
|
|
"log"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"fileshare/internal/model"
|
|
"fileshare/internal/storage"
|
|
)
|
|
|
|
// hashObject 流式计算存储对象 SHA512(hex);读取失败返回空串(去重按尽力而为降级)。
|
|
func hashObject(ctx context.Context, store storage.Storage, savePath string) string {
|
|
dl, err := store.Open(ctx, savePath, nil)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer func() { _ = dl.Close() }()
|
|
h := sha512.New()
|
|
if _, err := io.Copy(h, dl); err != nil {
|
|
return ""
|
|
}
|
|
return hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// applyDedup 上传落库后执行去重:
|
|
// 1. 计算刚保存对象的 SHA512;
|
|
// 2. 命中同哈希+同引擎的其他分享 → 复用其 FilePath/UUIDFileName,删除本次副本;
|
|
// 3. 未命中 → 只回填 ContentHash。
|
|
//
|
|
// 任何失败都不影响上传结果(记录保留、哈希留空 = 不参与去重)。
|
|
func (d *Deps) applyDedup(ctx context.Context, store storage.Storage, savedPath string, fc *model.FileCodes) {
|
|
if !d.Cfg.DedupEnabled() || fc == nil || fc.ID == 0 || fc.Text != nil {
|
|
return
|
|
}
|
|
hash := hashObject(ctx, store, savedPath)
|
|
if hash == "" {
|
|
log.Printf("[dedup] 哈希计算失败 code=%s(跳过去重)", fc.Code)
|
|
return
|
|
}
|
|
updates := map[string]any{"content_hash": hash}
|
|
|
|
var old model.FileCodes
|
|
err := d.DB.WithContext(ctx).
|
|
Where("content_hash = ? AND engine = ? AND id <> ? AND uuid_file_name IS NOT NULL",
|
|
hash, fc.Engine, fc.ID).
|
|
First(&old).Error
|
|
switch {
|
|
case err == nil && old.UUIDFileName != nil && old.FilePath != nil:
|
|
// 命中:复用旧对象,删除本次副本
|
|
updates["file_path"] = *old.FilePath
|
|
updates["uuid_file_name"] = *old.UUIDFileName
|
|
if err := store.DeleteFile(ctx, savedPath); err != nil {
|
|
log.Printf("[dedup] 删除重复副本失败 code=%s: %v", fc.Code, err)
|
|
}
|
|
log.Printf("[dedup] 命中同内容分享 code=%s 复用 %s", fc.Code, old.Code)
|
|
case err != nil && err != gorm.ErrRecordNotFound:
|
|
log.Printf("[dedup] 去重查询失败 code=%s: %v", fc.Code, err)
|
|
}
|
|
if err := d.DB.WithContext(ctx).Model(fc).Updates(updates).Error; err != nil {
|
|
log.Printf("[dedup] 回填哈希失败 code=%s: %v", fc.Code, err)
|
|
return
|
|
}
|
|
fc.ContentHash = &hash
|
|
if fp, ok := updates["file_path"]; ok {
|
|
s := fp.(string)
|
|
fc.FilePath = &s
|
|
}
|
|
if un, ok := updates["uuid_file_name"]; ok {
|
|
s := un.(string)
|
|
fc.UUIDFileName = &s
|
|
}
|
|
}
|