仰望星辰工作室

gitcat

gitcat/ internal/server/upgrade.go 7.2 KB · 240 行 原始文件
1package server
2
3import (
4 "fmt"
5 "net/http"
6 "os"
7 "path/filepath"
8 "runtime"
9 "strings"
10 "time"
11
12 "gitcat/internal/store"
13 "gitcat/internal/upgrade"
14)
15
16// 升级相关的设置键。
17const (
18 settingUpgradeURL = "upgrade_url"
19 settingUpgradeAutoApply = "upgrade_auto_apply"
20)
21
22// upgradeInfo 返回当前二进制的版本信息。
23func (s *Server) upgradeInfo() upgrade.Info {
24 return upgrade.Info{
25 Version: s.cfg.Version,
26 Commit: s.cfg.Commit,
27 BuildDate: s.cfg.BuildDate,
28 ExePath: s.cfg.ExePath,
29 GOOS: runtime.GOOS,
30 GOARCH: runtime.GOARCH,
31 }
32}
33
34func (s *Server) handleAdminUpgradePage(w http.ResponseWriter, r *http.Request) {
35 if s.requireAdmin(w, r) == nil {
36 return
37 }
38 info := s.upgradeInfo()
39 p := s.page(r, "版本与升级")
40 p.Active = "admin"
41 p.Data["Tab"] = "upgrade"
42 p.Data["Version"] = info.Version
43 p.Data["Commit"] = info.ShortSHA()
44 p.Data["HasCommit"] = info.Commit != ""
45 p.Data["BuildDate"] = info.BuildDate
46 p.Data["Platform"] = info.GOOS + "/" + info.GOARCH
47 p.Data["IsDev"] = info.IsDev()
48 p.Data["ExePath"] = info.ExePath
49 p.Data["SchemaVersion"] = s.st.SchemaVersion()
50 p.Data["SchemaSupported"] = store.SchemaVersion
51 p.Data["UpgradeURL"] = s.st.GetSetting(settingUpgradeURL)
52 p.Data["AutoApply"] = s.st.GetSetting(settingUpgradeAutoApply) == "1"
53 p.Data["RestartSupported"] = s.cfg.Restart != nil
54 p.Data["ExeReady"] = s.upgradeExeExists()
55 p.Data["LastCheck"] = s.st.GetSetting("upgrade_checked_at")
56 p.Data["LastCheckText"] = humanTime(s.st.GetSetting("upgrade_checked_at"))
57 p.Data["BackupDir"] = filepath.Join(s.cfg.DataDir, "backup-bin")
58
59 if pend := upgrade.ReadPending(s.cfg.DataDir); pend != nil {
60 p.Data["Pending"] = pend
61 p.Data["PendingFrom"] = pend.FromVersion
62 p.Data["PendingTo"] = pend.ToVersion
63 p.Data["PendingBackup"] = pend.BackupPath
64 p.Data["PendingAt"] = pend.AppliedAt
65 }
66 s.render(w, r, "admin_upgrade", p)
67}
68
69// handleAdminUpgradeAction 统一处理升级页的三种动作。
70//
71// 用 POST + CSRF 而不是可被预取的 GET:升级会替换服务端的可执行文件,
72// 必须确认请求来自已登录的管理员。
73func (s *Server) handleAdminUpgradeAction(w http.ResponseWriter, r *http.Request) {
74 if s.requireAdmin(w, r) == nil {
75 return
76 }
77 if !s.checkCSRF(r) {
78 s.renderError(w, r, http.StatusForbidden, "表单已过期,请重试")
79 return
80 }
81 action := r.FormValue("action")
82 target := "/admin/upgrade"
83
84 switch action {
85 case "save":
86 url := strings.TrimSpace(r.FormValue("upgrade_url"))
87 if url != "" && !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
88 url = "https://" + url
89 }
90 if url != "" && !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
91 setFlash(w, "升级源必须是 http/https 地址")
92 http.Redirect(w, r, target, http.StatusFound)
93 return
94 }
95 _ = s.st.SetSetting(settingUpgradeURL, url)
96 _ = s.st.SetSetting(settingUpgradeAutoApply, boolSetting(r.FormValue("auto_apply") == "on"))
97 setFlash(w, "升级设置已保存")
98 http.Redirect(w, r, target, http.StatusFound)
99
100 case "check":
101 res, err := s.upgradeInfo().Check(r.Context(), s.st.GetSetting(settingUpgradeURL))
102 if err != nil {
103 setFlash(w, "检查更新失败:"+err.Error())
104 http.Redirect(w, r, target, http.StatusFound)
105 return
106 }
107 _ = s.st.SetSetting("upgrade_checked_at", res.CheckedAt.Format(time.RFC3339))
108 _ = s.st.SetSetting("upgrade_latest", res.Latest)
109 msg := res.Message
110 if res.Upgradable && res.Release != nil {
111 msg = fmt.Sprintf("发现新版本 %s,当前 %s", res.Release.Version, res.Current)
112 if res.Release.Notes != "" {
113 msg += ":" + res.Release.Notes
114 }
115 }
116 setFlash(w, msg)
117 http.Redirect(w, r, target, http.StatusFound)
118
119 case "apply":
120 s.applyUpgrade(w, r, target)
121
122 case "restart":
123 s.triggerRestart(w, r, target)
124
125 default:
126 http.Redirect(w, r, target, http.StatusFound)
127 }
128}
129
130// applyUpgrade 执行下载 → 校验 → 替换。
131func (s *Server) applyUpgrade(w http.ResponseWriter, r *http.Request, target string) {
132 if !s.upgrading.CompareAndSwap(false, true) {
133 setFlash(w, "已有升级任务在进行中,请稍候")
134 http.Redirect(w, r, target, http.StatusFound)
135 return
136 }
137 defer s.upgrading.Store(false)
138
139 // 危险操作二次确认:要求管理员输入当前版本号。
140 if want := strings.TrimSpace(r.FormValue("confirm")); want != s.cfg.Version {
141 setFlash(w, "请输入当前版本号 "+s.cfg.Version+" 以确认升级")
142 http.Redirect(w, r, target, http.StatusFound)
143 return
144 }
145 manifestURL := s.st.GetSetting(settingUpgradeURL)
146 if manifestURL == "" {
147 setFlash(w, "请先配置升级源")
148 http.Redirect(w, r, target, http.StatusFound)
149 return
150 }
151 info := s.upgradeInfo()
152 res, err := info.Check(r.Context(), manifestURL)
153 if err != nil {
154 setFlash(w, "检查更新失败:"+err.Error())
155 http.Redirect(w, r, target, http.StatusFound)
156 return
157 }
158 if !res.Upgradable || res.Release == nil {
159 msg := res.Message
160 if msg == "" {
161 msg = "没有可安装的新版本"
162 }
163 setFlash(w, msg)
164 http.Redirect(w, r, target, http.StatusFound)
165 return
166 }
167
168 backupDir := filepath.Join(s.cfg.DataDir, "backup-bin")
169 inst, err := upgrade.Install(s.cfg.DataDir, info.ExePath, backupDir, res.Release, s.cfg.Version)
170 if err != nil {
171 setFlash(w, "升级失败:"+err.Error())
172 http.Redirect(w, r, target, http.StatusFound)
173 return
174 }
175 _ = s.st.SetSetting("upgrade_history", time.Now().Format(time.RFC3339)+" → "+inst.ToVersion)
176
177 if inst.Note != "" {
178 setFlash(w, inst.Note)
179 http.Redirect(w, r, target, http.StatusFound)
180 return
181 }
182 if s.cfg.Restart == nil {
183 setFlash(w, fmt.Sprintf("已升级到 %s,请重启服务后生效(旧程序备份在 %s)",
184 inst.ToVersion, inst.BackupPath))
185 http.Redirect(w, r, target, http.StatusFound)
186 return
187 }
188 setFlash(w, fmt.Sprintf("已升级到 %s,正在重启…", inst.ToVersion))
189 http.Redirect(w, r, target, http.StatusFound)
190 // 响应已经写出,goroutine 里不能再碰 w —— 只负责稍后触发重启。
191 scheduleRestart(s.cfg.Restart)
192}
193
194// scheduleRestart 延迟触发重启,留出时间把响应刷给浏览器。
195func scheduleRestart(restart func()) {
196 go func() {
197 time.Sleep(time.Second)
198 restart()
199 }()
200}
201
202// triggerRestart 触发优雅重启。
203func (s *Server) triggerRestart(w http.ResponseWriter, r *http.Request, target string) {
204 if s.cfg.Restart == nil {
205 setFlash(w, "当前部署方式无法自动重启,请手工重启服务")
206 http.Redirect(w, r, target, http.StatusFound)
207 return
208 }
209 if !s.upgrading.CompareAndSwap(false, true) {
210 http.Redirect(w, r, target, http.StatusFound)
211 return
212 }
213 setFlash(w, "服务正在重启,请稍候几秒后刷新页面")
214 http.Redirect(w, r, target, http.StatusFound)
215 if f, ok := w.(http.Flusher); ok {
216 f.Flush()
217 }
218 scheduleRestart(s.cfg.Restart)
219}
220
221// upgradeExeExists 用于页面提示:可执行文件是否可定位。
222// humanTime 把 RFC3339 字符串渲染成人话;解析失败原样返回。
223func humanTime(s string) string {
224 if s == "" {
225 return "从未"
226 }
227 t, err := time.Parse(time.RFC3339, s)
228 if err != nil {
229 return s
230 }
231 return t.Local().Format("2006-01-02 15:04")
232}
233
234func (s *Server) upgradeExeExists() bool {
235 if s.cfg.ExePath == "" {
236 return false
237 }
238 _, err := os.Stat(s.cfg.ExePath)
239 return err == nil
240}