Files
FileShare/server/internal/janitor/janitor.go
T
SKYMirror 7f060dd0e4 26.9(安全审计修复版)
Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务:

- 安全审计全部修复(docs/security-audit-2026-09-05.md):
  bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、
  全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、
  管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、
  会话默认 7 天、限流缓存故障降级、robots.txt 端点等
- 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固
- Redis 支持库号(FCB_REDIS_DB / redis://…/db URL)
- 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、
  提码 5 位起、chunk 32MiB 上限、admin 审计动作等)

验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
2026-09-05 04:22:41 +08:00

125 lines
4.5 KiB
Go

// Package janitor 后台清理循环(安全审计 M5):
// 回收过期容量预留、超时未完成的上传会话(含其分片对象)与过期预签名会话
// (direct 模式残留对象一并删除)。此前这些资源仅在同 token 复用/显式取消时
// 释放,恶意 init 可长期占用容量预留或累积垃圾数据。
package janitor
import (
"context"
"errors"
"log"
"time"
"gorm.io/gorm"
"filecodebox/internal/model"
"filecodebox/internal/storage"
)
// chunkSessionMaxAge 未完成分片会话的最大保留时长(预留 TTL 为 2h,
// 会话保留 24h 以支持断点续传;超时后由本循环清理)。
const chunkSessionMaxAge = 24 * time.Hour
// presignGrace 过期预签名会话的宽限时长(到点即删,避免与在途 confirm 竞争)。
const presignGrace = time.Hour
// Start 启动周期清理循环;ctx 取消时退出。
func Start(ctx context.Context, db *gorm.DB, store *storage.Manager, interval time.Duration) {
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
Run(ctx, db, store)
}
}
}()
}
// Run 执行一轮清理;单项失败仅记日志,不影响其他项。
func Run(ctx context.Context, db *gorm.DB, store *storage.Manager) {
now := time.Now()
cleanExpiredReservations(ctx, db, now)
cleanExpiredChunkSessions(ctx, db, store, now)
cleanExpiredPresignSessions(ctx, db, store, now)
}
// cleanExpiredReservations 删除全部过期容量预留。
func cleanExpiredReservations(ctx context.Context, db *gorm.DB, now time.Time) {
if err := db.WithContext(ctx).
Where("expires_at <= ?", now).
Delete(&model.StorageReservation{}).Error; err != nil {
log.Printf("[janitor] 清理过期容量预留失败: %v", err)
}
}
// engineFor 按归属引擎取回实例;空/未知引擎回落当前引擎(对齐 API 层 storeFor 语义)。
func engineFor(store *storage.Manager, name string) (storage.Storage, error) {
if name != "" && storage.ValidEngine(name) {
if s, err := store.EngineOf(name); err == nil {
return s, nil
}
}
return store.Current(), nil
}
// cleanExpiredChunkSessions 清理超时未完成的分片会话及其分片对象。
func cleanExpiredChunkSessions(ctx context.Context, db *gorm.DB, store *storage.Manager, now time.Time) {
var sessions []model.UploadChunk
if err := db.WithContext(ctx).
Where("chunk_index = -1 AND created_at < ?", now.Add(-chunkSessionMaxAge)).
Limit(200).
Find(&sessions).Error; err != nil {
log.Printf("[janitor] 查询过期分片会话失败: %v", err)
return
}
for _, s := range sessions {
engine, err := engineFor(store, s.Engine)
if err == nil && s.SavePath != "" {
if err := engine.CleanChunks(ctx, s.UploadID, s.SavePath); err != nil &&
!errors.Is(err, storage.ErrNotFound) && !errors.Is(err, storage.ErrInvalidPath) {
log.Printf("[janitor] 清理分片对象失败 upload_id=%s: %v", s.UploadID, err)
}
}
if err := db.WithContext(ctx).
Where("upload_id = ?", s.UploadID).
Delete(&model.UploadChunk{}).Error; err != nil {
log.Printf("[janitor] 删除过期分片会话失败 upload_id=%s: %v", s.UploadID, err)
continue
}
log.Printf("[janitor] 已清理超时分片会话 upload_id=%s file=%s", s.UploadID, s.FileName)
}
}
// cleanExpiredPresignSessions 清理过期预签名会话;direct 模式残留对象一并删除。
func cleanExpiredPresignSessions(ctx context.Context, db *gorm.DB, store *storage.Manager, now time.Time) {
var sessions []model.PresignUploadSession
if err := db.WithContext(ctx).
Where("expires_at < ?", now.Add(-presignGrace)).
Limit(200).
Find(&sessions).Error; err != nil {
log.Printf("[janitor] 查询过期预签名会话失败: %v", err)
return
}
for _, s := range sessions {
if s.Mode == "direct" && s.SavePath != "" {
if engine, err := engineFor(store, s.Engine); err == nil {
if err := engine.DeleteFile(ctx, s.SavePath); err != nil &&
!errors.Is(err, storage.ErrNotFound) && !errors.Is(err, storage.ErrInvalidPath) {
log.Printf("[janitor] 删除直传残留对象失败 upload_id=%s: %v", s.UploadID, err)
}
}
}
if err := db.WithContext(ctx).
Where("upload_id = ?", s.UploadID).
Delete(&model.PresignUploadSession{}).Error; err != nil {
log.Printf("[janitor] 删除过期预签名会话失败 upload_id=%s: %v", s.UploadID, err)
continue
}
log.Printf("[janitor] 已清理过期预签名会话 upload_id=%s mode=%s", s.UploadID, s.Mode)
}
}