// 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 } }