26.9:直链下载 + 过期回收 + SHA512 去重 + 防盗链 + 媒体预览 + 文件夹上传提示
CI 测试 / go vet + go test (push) Failing after 5s
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
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"fileshare/internal/config"
|
||||
"fileshare/internal/janitor"
|
||||
"fileshare/internal/middleware"
|
||||
"fileshare/internal/model"
|
||||
"fileshare/internal/response"
|
||||
@@ -81,6 +83,9 @@ func registerAdmin(r *gin.Engine, d *Deps) {
|
||||
// 26.9 存储引擎:运行时热切换(健康检查通过才生效,失败保持原引擎)
|
||||
authed.POST("/storage/switch", d.adminStorageSwitch)
|
||||
|
||||
// 26.9 过期回收:手动触发一轮(定时循环之外的管理端入口)
|
||||
authed.POST("/recycle/run", d.adminRecycleRun)
|
||||
|
||||
// 审计日志查询(需求 ③;logs 为 list 的别名)
|
||||
authed.GET("/audit/list", d.adminAuditList)
|
||||
authed.GET("/audit/logs", d.adminAuditList)
|
||||
@@ -796,6 +801,9 @@ var configKeys = []string{
|
||||
"loginCount", "loginMinute",
|
||||
"opacity", "background", "showAdminAddr", "robotsText", "site_domain", // 26.9:站点对外域名
|
||||
"upload_rate", "download_rate", // 26.9:上下行带宽字节/秒(0=不限速)
|
||||
// 26.9 回收与下载安全
|
||||
"recycle_enabled", "recycle_interval", "retention_days", "dedup_enabled",
|
||||
"hotlink_enabled", "hotlink_whitelist", "direct_download", "direct_link_expire",
|
||||
"adminSessionExpire", "storage_path", "local_storage_path",
|
||||
"file_storage",
|
||||
// 26.9 存储引擎与引擎参数(热切换;凭据为敏感键,get 掩码/update 空跳过)
|
||||
@@ -816,6 +824,8 @@ var intConfigKeys = []string{
|
||||
"adminSessionExpire",
|
||||
"max_save_count", "max_file_size", "notify_enabled",
|
||||
"upload_rate", "download_rate", // 26.9
|
||||
"recycle_enabled", "recycle_interval", "retention_days", "dedup_enabled",
|
||||
"hotlink_enabled", "direct_download", "direct_link_expire", // 26.9(hotlink_whitelist 为字符串键)
|
||||
}
|
||||
|
||||
// validateConfigValue 按 settings.KVSchema 校验单个配置值:
|
||||
@@ -1300,11 +1310,15 @@ func (d *Deps) fileByID(c *gin.Context, id int64) (*model.FileCodes, error) {
|
||||
}
|
||||
|
||||
// deleteFileCode 删除分享记录与存储文件(文本分享无存储文件)。
|
||||
// 26.9:SHA512 去重开启时同一对象可能被多条分享引用——删除前按
|
||||
// ContentHash+Engine+UUIDFileName 引用计数,仍有其他引用则保留对象。
|
||||
func (d *Deps) deleteFileCode(c *gin.Context, fc *model.FileCodes) error {
|
||||
if fc.Text == nil && fc.FilePath != nil && fc.UUIDFileName != nil {
|
||||
// 26.9:删除走文件归属引擎(旧引擎里的文件也要能删掉)
|
||||
if delStore, dErr := d.storeFor(fc.Engine); dErr == nil {
|
||||
if err := delStore.DeleteFile(c.Request.Context(), fileSavePath(fc)); err != nil && !errors.Is(err, storage.ErrNotFound) {
|
||||
if d.referencedByOther(c.Request.Context(), fc) {
|
||||
// 还有其他分享引用该对象:仅删记录
|
||||
} else if err := delStore.DeleteFile(c.Request.Context(), fileSavePath(fc)); err != nil && !errors.Is(err, storage.ErrNotFound) {
|
||||
return errInternal("存储文件删除失败: " + err.Error())
|
||||
}
|
||||
}
|
||||
@@ -1316,6 +1330,31 @@ func (d *Deps) deleteFileCode(c *gin.Context, fc *model.FileCodes) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// referencedByOther 该分享的存储对象是否仍被其他分享引用(SHA512 去重)。
|
||||
// 无 ContentHash(历史数据/去重未开启)时恒 false——按旧语义直接删对象。
|
||||
func (d *Deps) referencedByOther(ctx context.Context, fc *model.FileCodes) bool {
|
||||
if fc.ContentHash == nil || *fc.ContentHash == "" || fc.UUIDFileName == nil {
|
||||
return false
|
||||
}
|
||||
var cnt int64
|
||||
_ = d.DB.WithContext(ctx).Model(&model.FileCodes{}).
|
||||
Where("content_hash = ? AND engine = ? AND uuid_file_name = ? AND id <> ?",
|
||||
*fc.ContentHash, fc.Engine, *fc.UUIDFileName, fc.ID).
|
||||
Count(&cnt).Error
|
||||
return cnt > 0
|
||||
}
|
||||
|
||||
// adminRecycleRun 手动触发一轮过期回收(26.9;定时循环之外的入口)。
|
||||
// 返回 {removed: 本轮回收条数}。
|
||||
func (d *Deps) adminRecycleRun(c *gin.Context) {
|
||||
removed := janitor.RecycleExpired(c.Request.Context(), d.DB, d.Store, &janitor.Recycler{
|
||||
Enabled: d.Cfg.RecycleEnabled,
|
||||
RetentionDays: d.Cfg.RetentionDays,
|
||||
})
|
||||
auditRecordSuccess(c, d.AuditSvc)
|
||||
response.OK(c, gin.H{"removed": removed})
|
||||
}
|
||||
|
||||
// deleteMany 批量删除:返回 (已删除, 不存在, 失败)。
|
||||
func (d *Deps) deleteMany(c *gin.Context, ids []int64) (deleted []int64, missing []int64, failed []gin.H) {
|
||||
deleted, missing = []int64{}, []int64{}
|
||||
|
||||
Reference in New Issue
Block a user