// 存量文件迁移到分层目录(文件存储分层改造 S9) // // 用法(在 go/ 目录下执行): // // go run ./cmd/migrate_storage # 默认 dry-run,只打印计划,不改动任何数据 // go run ./cmd/migrate_storage --apply # 真正执行迁移 // go run ./cmd/migrate_storage --tid=234573 # 只迁移指定租户 // go run ./cmd/migrate_storage --limit=10 # 只处理前 N 条(调试用) // // 迁移原理: // - 七牛云:BucketManager.Move 同 bucket 内服务端改名,不重新上传、不消耗流量、与文件大小无关 // - 本地 :os.Rename 同盘改名,不搬数据 // // 幂等:目标 key 已存在时跳过(Skipped),可重复执行。 // // 注意:老数据没有「个人/共享」归属信息(tuid 全为 NULL、uid 是上传者), // // 因此老数据统一按「租户共享」迁移,个人目录只对新上传的文件生效。 package main import ( "flag" "fmt" "os" "server/models" "server/services" ) func main() { apply := flag.Bool("apply", false, "真正执行迁移;不传则只打印计划(dry-run)") tid := flag.Uint64("tid", 0, "只迁移指定租户,0 表示全部") limit := flag.Int("limit", 0, "只处理前 N 条,0 表示不限制") flag.Parse() // 注意:需在 go/ 目录下运行,beego 会自动加载 conf/app.conf models.Init("") if models.Orm == nil { fmt.Println("数据库未初始化") os.Exit(1) } models.EnsureSystemFileStorageColumns() storageSvc, err := services.GetStorageService() if err != nil { fmt.Println("获取存储服务失败:", err) os.Exit(1) } fmt.Printf("当前存储类型: %s\n", storageSvc.Type()) qs := models.Orm.QueryTable(new(models.SystemFile)).Filter("delete_time__isnull", true) if *tid > 0 { qs = qs.Filter("tid", *tid) } if *limit > 0 { qs = qs.Limit(*limit) } var files []models.SystemFile if _, err := qs.OrderBy("id").All(&files); err != nil { fmt.Println("读取文件列表失败:", err) os.Exit(1) } fmt.Printf("待处理文件数: %d(模式: %s)\n\n", len(files), modeName(*apply)) planned, skipped, failed := 0, 0, 0 for i := range files { f := files[i] oldKey := f.ObjectKey if oldKey == "" { oldKey = services.KeyFromSrc(f.Src, storageSvc) } if oldKey == "" { fmt.Printf("[跳过] id=%d 无法解析原路径: %s\n", f.ID, f.Src) skipped++ continue } newKey := services.TargetKey(&f) if oldKey == newKey { skipped++ continue } planned++ fmt.Printf("[%s] id=%d tid=%d\n 老: %s\n 新: %s\n", modeName(*apply), f.ID, f.Tid, oldKey, newKey) if !*apply { continue } if err := storageSvc.Move(oldKey, newKey); err != nil { fmt.Printf(" !! 迁移失败: %v\n", err) failed++ continue } newSrc := storageSvc.GetPublicURL(newKey) if _, err := models.Orm.QueryTable(new(models.SystemFile)). Filter("id", f.ID). Update(map[string]interface{}{ "src": newSrc, "object_key": newKey, "storage": storageSvc.Type(), }); err != nil { fmt.Printf(" !! 更新数据库失败(文件已移动,请重跑本脚本): %v\n", err) failed++ } } fmt.Printf("\n==== 汇总 ====\n计划迁移: %d\n跳过: %d\n失败: %d\n", planned, skipped, failed) if !*apply { fmt.Println("当前为 dry-run,未改动任何数据。确认无误后加 --apply 执行。") } } func modeName(apply bool) string { if apply { return "APPLY" } return "DRY-RUN" }