clearlove2.1
1package handlers
2
3import (
4 "fmt"
5 "io"
6 "net/http"
7 "os"
8 "path/filepath"
9 "strings"
10 "sync"
11 "time"
12
13 "clearlove/internal/config"
14 "clearlove/internal/migrate"
15 "clearlove/internal/models"
16 "clearlove/internal/util"
17)
18
19// 单个上传文件大小上限(MB)
20const maxMigrateMB = 200
21
22// migrateJob 迁移任务状态(同一时间只允许一个任务)
23type migrateJob struct {
24 mu sync.Mutex
25 running bool
26 done bool
27 err string
28 stats *migrate.Stats
29 dry bool
30 startAt time.Time
31}
32
33var migJob = &migrateJob{}
34
35// snapshot 读取任务状态快照(供页面渲染与轮询接口共用)
36func (j *migrateJob) snapshot() map[string]any {
37 j.mu.Lock()
38 defer j.mu.Unlock()
39 out := map[string]any{
40 "running": j.running,
41 "done": j.done,
42 "err": j.err,
43 "dry_run": j.dry,
44 }
45 if j.stats != nil {
46 out["stats"] = j.stats
47 }
48 if !j.startAt.IsZero() {
49 out["seconds"] = int(time.Since(j.startAt).Seconds())
50 }
51 return out
52}
53
54// AdminMigrate 旧版数据迁移页
55func AdminMigrate(w http.ResponseWriter, r *http.Request) {
56 if a := requireAdmin(w, r, models.PermSetting); a == nil {
57 return
58 }
59 Render(w, r, "admin_layout.html", "pg_admin_migrate", map[string]any{
60 "status": migJob.snapshot(),
61 "maxMB": maxMigrateMB,
62 })
63}
64
65// AdminMigrateStart 接收旧版数据库文件并异步执行迁移
66func AdminMigrateStart(w http.ResponseWriter, r *http.Request) {
67 admin := requireAdmin(w, r, models.PermSetting)
68 if admin == nil {
69 return
70 }
71 migJob.mu.Lock()
72 running := migJob.running
73 migJob.mu.Unlock()
74 if running {
75 fail(w, http.StatusConflict, "已有迁移任务正在执行,请等待完成")
76 return
77 }
78
79 r.Body = http.MaxBytesReader(w, r.Body, maxMigrateMB<<20)
80 if err := r.ParseMultipartForm(16 << 20); err != nil {
81 fail(w, 400, fmt.Sprintf("上传失败:文件不能超过 %dMB", maxMigrateMB))
82 return
83 }
84 file, hdr, err := r.FormFile("file")
85 if err != nil {
86 fail(w, 400, "请选择旧版数据库文件")
87 return
88 }
89 defer file.Close()
90
91 tmpDir := filepath.Join(config.Cfg.DataDir, "tmp")
92 if err := os.MkdirAll(tmpDir, 0o755); err != nil {
93 fail(w, 500, "无法创建临时目录:"+err.Error())
94 return
95 }
96 ext := strings.ToLower(filepath.Ext(hdr.Filename))
97 switch ext {
98 case ".sql", ".sqlite", ".sqlite3":
99 default:
100 ext = ".db"
101 }
102 dst := filepath.Join(tmpDir, "migrate-"+util.RandomHex(8)+ext)
103 out, err := os.Create(dst)
104 if err != nil {
105 fail(w, 500, "无法保存上传文件:"+err.Error())
106 return
107 }
108 if _, err := io.Copy(out, file); err != nil {
109 out.Close()
110 _ = os.Remove(dst)
111 fail(w, 500, "写入文件失败:"+err.Error())
112 return
113 }
114 out.Close()
115
116 uploadsDir := strings.TrimSpace(r.FormValue("uploads_dir"))
117 dryRun := r.FormValue("dry") == "1"
118 if !startMigrateJob(dst, uploadsDir, dryRun) {
119 _ = os.Remove(dst)
120 fail(w, http.StatusConflict, "已有迁移任务正在执行,请等待完成")
121 return
122 }
123 util.Log("info", "管理员 %s 启动旧版数据迁移(试运行=%v):%s", admin.Username, dryRun, hdr.Filename)
124 okJSON(w, map[string]any{"msg": "迁移任务已启动"})
125}
126
127// AdminMigrateStatus 查询迁移进度(前端轮询)
128func AdminMigrateStatus(w http.ResponseWriter, r *http.Request) {
129 if a := requireAdmin(w, r, models.PermSetting); a == nil {
130 return
131 }
132 okJSON(w, migJob.snapshot())
133}
134
135// startMigrateJob 启动迁移协程,已有任务运行时返回 false
136func startMigrateJob(dbFile, uploadsDir string, dryRun bool) bool {
137 migJob.mu.Lock()
138 if migJob.running {
139 migJob.mu.Unlock()
140 return false
141 }
142 migJob.running = true
143 migJob.done = false
144 migJob.err = ""
145 migJob.stats = nil
146 migJob.dry = dryRun
147 migJob.startAt = time.Now()
148 migJob.mu.Unlock()
149
150 go func() {
151 st, err := migrate.Run(dbFile, uploadsDir, dryRun)
152 migJob.mu.Lock()
153 migJob.running = false
154 migJob.done = true
155 if err != nil {
156 migJob.err = err.Error()
157 } else {
158 migJob.stats = st
159 }
160 migJob.mu.Unlock()
161 // 迁移结束后删除临时上传文件
162 _ = os.Remove(dbFile)
163 }()
164 return true
165}