原因:任务标准结果中的 Base64、Data URI 和 Buffer 会进入 PostgreSQL JSON,导致 TOAST 与备份体积快速增长。 影响:新增统一 JSON 持久化关口;upload_none 将二进制原子写入本地结果目录,数据库仅保存带 SHA-256 的有界占位符;任务详情、同步响应、异步查询和兼容协议按需校验恢复。补充 24 小时清理、容量上限、历史小批量治理命令及管理端说明。 风险:本地结果超过 TTL、丢失或损坏时分别返回明确的 410/500;空间不足时返回 503 且不重试上游。未自动执行历史治理。 验证:三种真实图片模型同步/异步与幂等重放通过;Go vet/全量测试、前端 111 测试、lint/typecheck/build、OpenAPI、迁移安全、govulncheck、依赖审计、手工发布测试及 Linux amd64 构建通过。
149 lines
4.1 KiB
Go
149 lines
4.1 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
)
|
|
|
|
func (s *Server) startLocalTempAssetCleanup(ctx context.Context) {
|
|
go func() {
|
|
s.cleanupExpiredLocalTempAssets(ctx, time.Now())
|
|
ticker := time.NewTicker(time.Hour)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case now := <-ticker.C:
|
|
s.cleanupExpiredLocalTempAssets(ctx, now)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (s *Server) cleanupExpiredLocalTempAssets(ctx context.Context, now time.Time) int {
|
|
targets := []localTempAssetCleanupTarget{
|
|
{
|
|
StorageDir: s.cfg.LocalGeneratedStorageDir,
|
|
FallbackDir: config.DefaultLocalGeneratedStorageDir,
|
|
MarkRequestAsset: false,
|
|
},
|
|
{
|
|
StorageDir: s.cfg.LocalUploadedStorageDir,
|
|
FallbackDir: config.DefaultLocalUploadedStorageDir,
|
|
MarkRequestAsset: true,
|
|
},
|
|
}
|
|
deleted := 0
|
|
for _, target := range targets {
|
|
deleted += s.cleanupExpiredLocalTempAssetsInDir(ctx, now, target)
|
|
}
|
|
deleted += s.cleanupExpiredLocalBinaryResults(now)
|
|
return deleted
|
|
}
|
|
|
|
func (s *Server) cleanupExpiredLocalBinaryResults(now time.Time) int {
|
|
storageDir := strings.TrimSpace(s.cfg.LocalGeneratedStorageDir)
|
|
if storageDir == "" {
|
|
storageDir = config.DefaultLocalGeneratedStorageDir
|
|
}
|
|
root := filepath.Join(storageDir, "results")
|
|
ttlHours := s.cfg.LocalResultTTLHours
|
|
if ttlHours <= 0 {
|
|
ttlHours = 24
|
|
}
|
|
expiredBefore := now.Add(-time.Duration(ttlHours) * time.Hour)
|
|
deleted := 0
|
|
var directories []string
|
|
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
if !errors.Is(walkErr, os.ErrNotExist) && s.logger != nil {
|
|
s.logger.Warn("walk local binary result failed", "path", path, "error", walkErr)
|
|
}
|
|
return nil
|
|
}
|
|
if entry.IsDir() {
|
|
if path != root {
|
|
directories = append(directories, path)
|
|
}
|
|
return nil
|
|
}
|
|
if entry.Type()&os.ModeSymlink != 0 {
|
|
return nil
|
|
}
|
|
info, infoErr := entry.Info()
|
|
if infoErr != nil || info.ModTime().After(expiredBefore) {
|
|
return nil
|
|
}
|
|
if removeErr := os.Remove(path); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
|
|
if s.logger != nil {
|
|
s.logger.Warn("remove local binary result failed", "path", path, "error", removeErr)
|
|
}
|
|
return nil
|
|
}
|
|
deleted++
|
|
return nil
|
|
})
|
|
if err != nil && !errors.Is(err, os.ErrNotExist) && s.logger != nil {
|
|
s.logger.Warn("scan local binary result root failed", "dir", root, "error", err)
|
|
}
|
|
for index := len(directories) - 1; index >= 0; index-- {
|
|
_ = os.Remove(directories[index])
|
|
}
|
|
return deleted
|
|
}
|
|
|
|
type localTempAssetCleanupTarget struct {
|
|
StorageDir string
|
|
FallbackDir string
|
|
MarkRequestAsset bool
|
|
}
|
|
|
|
func (s *Server) cleanupExpiredLocalTempAssetsInDir(ctx context.Context, now time.Time, target localTempAssetCleanupTarget) int {
|
|
storageDir := strings.TrimSpace(target.StorageDir)
|
|
if storageDir == "" {
|
|
storageDir = target.FallbackDir
|
|
}
|
|
entries, err := os.ReadDir(storageDir)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
s.logger.Warn("read local temp asset dir failed", "dir", storageDir, "error", err)
|
|
}
|
|
return 0
|
|
}
|
|
ttl := time.Duration(s.localTempAssetTTLHours()) * time.Hour
|
|
expiredBefore := now.Add(-ttl)
|
|
deleted := 0
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
info, err := entry.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if info.ModTime().After(expiredBefore) {
|
|
continue
|
|
}
|
|
localPath := filepath.Join(storageDir, entry.Name())
|
|
if err := os.Remove(localPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
s.logger.Warn("remove local temp asset failed", "path", localPath, "error", err)
|
|
continue
|
|
}
|
|
deleted++
|
|
if target.MarkRequestAsset && strings.HasPrefix(entry.Name(), requestAssetFilePrefix) && s.store != nil {
|
|
if err := s.store.MarkRequestAssetExpiredByLocalPath(ctx, localPath, now); err != nil && !store.IsUndefinedDatabaseObject(err) {
|
|
s.logger.Warn("mark local temp asset expired failed", "path", localPath, "error", err)
|
|
}
|
|
}
|
|
}
|
|
return deleted
|
|
}
|