实现直连 Sora 客户端、媒体落地与清理策略\n更新网关与前端配置以支持 Sora 平台\n补齐单元测试与契约测试,新增 curl 测试脚本\n\n测试: go test ./... -tags=unit
118 lines
2.7 KiB
Go
118 lines
2.7 KiB
Go
package service
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/robfig/cron/v3"
|
|
)
|
|
|
|
var soraCleanupCronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
|
|
|
// SoraMediaCleanupService 定期清理本地媒体文件
|
|
type SoraMediaCleanupService struct {
|
|
storage *SoraMediaStorage
|
|
cfg *config.Config
|
|
|
|
cron *cron.Cron
|
|
|
|
startOnce sync.Once
|
|
stopOnce sync.Once
|
|
}
|
|
|
|
func NewSoraMediaCleanupService(storage *SoraMediaStorage, cfg *config.Config) *SoraMediaCleanupService {
|
|
return &SoraMediaCleanupService{
|
|
storage: storage,
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
func (s *SoraMediaCleanupService) Start() {
|
|
if s == nil || s.cfg == nil {
|
|
return
|
|
}
|
|
if !s.cfg.Sora.Storage.Cleanup.Enabled {
|
|
log.Printf("[SoraCleanup] not started (disabled)")
|
|
return
|
|
}
|
|
if s.storage == nil || !s.storage.Enabled() {
|
|
log.Printf("[SoraCleanup] not started (storage disabled)")
|
|
return
|
|
}
|
|
|
|
s.startOnce.Do(func() {
|
|
schedule := strings.TrimSpace(s.cfg.Sora.Storage.Cleanup.Schedule)
|
|
if schedule == "" {
|
|
log.Printf("[SoraCleanup] not started (empty schedule)")
|
|
return
|
|
}
|
|
loc := time.Local
|
|
if strings.TrimSpace(s.cfg.Timezone) != "" {
|
|
if parsed, err := time.LoadLocation(strings.TrimSpace(s.cfg.Timezone)); err == nil && parsed != nil {
|
|
loc = parsed
|
|
}
|
|
}
|
|
c := cron.New(cron.WithParser(soraCleanupCronParser), cron.WithLocation(loc))
|
|
if _, err := c.AddFunc(schedule, func() { s.runCleanup() }); err != nil {
|
|
log.Printf("[SoraCleanup] not started (invalid schedule=%q): %v", schedule, err)
|
|
return
|
|
}
|
|
s.cron = c
|
|
s.cron.Start()
|
|
log.Printf("[SoraCleanup] started (schedule=%q tz=%s)", schedule, loc.String())
|
|
})
|
|
}
|
|
|
|
func (s *SoraMediaCleanupService) Stop() {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.stopOnce.Do(func() {
|
|
if s.cron != nil {
|
|
ctx := s.cron.Stop()
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(3 * time.Second):
|
|
log.Printf("[SoraCleanup] cron stop timed out")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (s *SoraMediaCleanupService) runCleanup() {
|
|
retention := s.cfg.Sora.Storage.Cleanup.RetentionDays
|
|
if retention <= 0 {
|
|
log.Printf("[SoraCleanup] skipped (retention_days=%d)", retention)
|
|
return
|
|
}
|
|
cutoff := time.Now().AddDate(0, 0, -retention)
|
|
deleted := 0
|
|
|
|
roots := []string{s.storage.ImageRoot(), s.storage.VideoRoot()}
|
|
for _, root := range roots {
|
|
if root == "" {
|
|
continue
|
|
}
|
|
_ = filepath.Walk(root, func(p string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
if info.ModTime().Before(cutoff) {
|
|
if rmErr := os.Remove(p); rmErr == nil {
|
|
deleted++
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
log.Printf("[SoraCleanup] cleanup finished, deleted=%d", deleted)
|
|
}
|