Files
2026-07-16 22:16:45 +07:00

130 lines
3.0 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"os"
"time"
"wucher/internal/config"
filemanager "wucher/internal/domain/file_manager"
"wucher/internal/repository/mysql"
s3repo "wucher/internal/repository/s3"
)
type backfillReport struct {
Total int `json:"total"`
ActiveFiles int `json:"active_files"`
ProbableOrphans int `json:"probable_orphan_files"`
MissingS3Key int `json:"missing_s3_key"`
InvalidReference int `json:"invalid_references"`
Skipped int `json:"skipped"`
DryRun bool `json:"dry_run"`
}
func main() {
ctx := context.Background()
cfg, err := config.LoadFromFile(configFilePath())
if err != nil {
fmt.Fprintf(os.Stderr, "load config: %v\n", err)
os.Exit(1)
}
db, err := mysql.Connect(cfg.MySQL)
if err != nil {
fmt.Fprintf(os.Stderr, "connect db: %v\n", err)
os.Exit(1)
}
fileRepo := mysql.NewFileManagerFileRepository(db)
lifecycleRepo, ok := any(fileRepo).(filemanager.FileLifecycleRepository)
if !ok {
fmt.Fprintln(os.Stderr, "file repository does not support lifecycle operations")
os.Exit(1)
}
dryRun := true
if raw := os.Getenv("BACKFILL_DRY_RUN"); raw != "" && (raw == "false" || raw == "0") {
dryRun = false
}
var storage filemanager.ObjectStorage
if cfg.FileStorage.Bucket != "" && cfg.FileStorage.Region != "" {
objStorage, s3Err := s3repo.NewFileObjectStorage(ctx, cfg.FileStorage)
if s3Err == nil {
storage = objStorage
}
}
rows := make([]filemanager.File, 0)
if err := db.WithContext(ctx).Model(&filemanager.File{}).Find(&rows).Error; err != nil {
fmt.Fprintf(os.Stderr, "query files: %v\n", err)
os.Exit(1)
}
report := backfillReport{DryRun: dryRun}
now := time.Now().UTC()
for i := range rows {
row := rows[i]
report.Total++
if row.DeletedAt != nil {
report.Skipped++
continue
}
referenced, refErr := lifecycleRepo.IsFileReferenced(ctx, row.ID)
if refErr != nil {
report.InvalidReference++
report.Skipped++
continue
}
if storage != nil {
if _, headErr := storage.HeadObject(ctx, row.ObjectKey); headErr != nil {
report.MissingS3Key++
}
}
if referenced {
report.ActiveFiles++
if !dryRun {
_ = lifecycleRepo.UpdateLifecycle(ctx, filemanager.FileLifecycleTransitionParams{
ID: row.ID,
Status: filemanager.FileLifecycleStatusActive,
AttachedAt: &now,
ClearOrphanedAt: true,
})
}
continue
}
report.ProbableOrphans++
if !dryRun {
_ = lifecycleRepo.UpdateLifecycle(ctx, filemanager.FileLifecycleTransitionParams{
ID: row.ID,
Status: filemanager.FileLifecycleStatusOrphanPending,
OrphanedAt: &now,
})
}
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
_ = enc.Encode(report)
}
func configFilePath() string {
if v := os.Getenv("CONFIG_FILE"); v != "" {
return v
}
basePath := os.Getenv("CONFIG_BASE_PATH")
if basePath == "" {
basePath = "/tmp"
}
filename := os.Getenv("CONFIG_FILENAME")
if filename == "" {
filename = "config.generated.json"
}
return basePath + "/" + filename
}