121 lines
3.4 KiB
Go
121 lines
3.4 KiB
Go
// 存量文件迁移到分层目录(文件存储分层改造 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"
|
||
}
|