- 数据库默认文件 filecodebox.db → fileshare.db(config.go 默认值与全部文档/编排同步)
- Go module filecodebox → fileshare(全部 import 同步,build/vet/test 全绿)
- 应用版本 APP_VERSION 2.5.6 → 26.9(health 接口已验证返回 26.9)
- deploy 编排统一:compose 项目名、Postgres 默认凭据、minio 桶名、env 注释
- JWT issuer、存储临时目录前缀、web 包名同步 fileshare
- CI:镜像 tag 以 APP_VERSION 为唯一版本源,main/tag 推送即发布
${VER} + latest;tag 触发时校验 tag 名与 APP_VERSION 一致,防错版
- 本地开发库文件已改名 fileshare.db(含 -shm/-wal 清理)
125 lines
4.5 KiB
Go
125 lines
4.5 KiB
Go
// Package janitor 后台清理循环(安全审计 M5):
|
|
// 回收过期容量预留、超时未完成的上传会话(含其分片对象)与过期预签名会话
|
|
// (direct 模式残留对象一并删除)。此前这些资源仅在同 token 复用/显式取消时
|
|
// 释放,恶意 init 可长期占用容量预留或累积垃圾数据。
|
|
package janitor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"fileshare/internal/model"
|
|
"fileshare/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)
|
|
}
|
|
}
|