Documentation
¶
Overview ¶
Package update 提供从 GitHub Releases 就地升级的能力:检查新版本、 下载并核对校验和、备份当前二进制、替换并重启。
这是整个程序里唯一一处会改写「自己」的代码,故每一步都按「失败了也要留个能跑的站点」 来设计:校验和不对不装、新二进制跑不起来不装、替换之前先备份、替换失败原样退回。
不在容器里做就地升级:镜像是只读的,换掉的文件会在下次重建容器时消失, 站长会得到一次「升级成功但版本没变」的诡异经历。容器部署该换镜像标签。
Index ¶
- Constants
- Variables
- func CompareVersions(a, b Version) int
- func DeleteBackup(dir, name string) error
- func NormalizeMirrors(in []string) ([]string, error)
- func PruneBackups(dir string, keep int) int
- type Asset
- type Backup
- type BuildInfo
- type Downloader
- type Environment
- type FetchHooks
- type Handler
- type Installer
- type Module
- type Phase
- type Release
- type Rollback
- type Service
- func (s *Service) Apply(base context.Context) (UpdateProgress, error)
- func (s *Service) Backups() ([]Backup, error)
- func (s *Service) Check(ctx context.Context) (*Release, error)
- func (s *Service) CleanupStale()
- func (s *Service) DeleteBackup(name string) error
- func (s *Service) DetectRollback()
- func (s *Service) Environment() Environment
- func (s *Service) OnShutdownRequest(fn func())
- func (s *Service) Progress() UpdateProgress
- func (s *Service) Relaunch() error
- func (s *Service) RelaunchPending() bool
- func (s *Service) ResetMirrors() error
- func (s *Service) SetMirrors(mirrors []string) error
- func (s *Service) StartAutoCheck(ctx context.Context)
- func (s *Service) Status() UpdateStatus
- type Source
- type UpdateProgress
- type UpdateStatus
- type Version
Constants ¶
const Name = "update"
Name 是模块名,也是 App.Provide 的键。
Variables ¶
var ( // ErrDisabled 表示在线升级功能被配置关闭。 ErrDisabled = errors.New("在线升级已关闭") // ErrBusy 表示已有一个升级任务在进行。 ErrBusy = errors.New("已有升级任务在进行") // ErrNotUpgradable 表示当前环境不支持就地升级。 ErrNotUpgradable = errors.New("当前部署形态不支持就地升级") // ErrNoUpdate 表示没有可安装的新版本。 ErrNoUpdate = errors.New("当前已是最新版本") )
升级流程的错误哨兵。
var ( // ErrNoRelease 表示源仓库还没有任何可用发布。 ErrNoRelease = errors.New("更新源还没有发布任何版本") // ErrRateLimited 表示被 GitHub 限流。 ErrRateLimited = errors.New("已达到 GitHub 的访问频率上限") // ErrNoAsset 表示该版本没有当前平台的发布包。 ErrNoAsset = errors.New("该版本没有提供当前平台的发布包") )
更新源相关的错误哨兵。措辞要能直接显示给站长:这些错误多半不是「程序坏了」, 而是「源仓库还没发过版」「今天问得太频繁」这类需要人去处置的状况。
var DefaultMirrors = []string{
"https://ghfast.top/",
"https://gh-proxy.com/",
"https://gh-proxy.org/",
"https://gh.llkk.cc/",
}
DefaultMirrors 是内置的下载加速地址:发布包在 GitHub 上,国内服务器直连常常只有几十 KB/s, 这几个公共服务把 GitHub 的下载转一手。都是别人的免费服务,随时可能失效,所以升级时先测速再挑; 装的是哪个文件由 GitHub 接口给的 SHA-256 说了算,转手的一方改不了内容,最多让下载失败。
var ErrBadBackupName = errors.New("备份名不合法")
ErrBadBackupName 表示备份名不合法。
var ErrChecksumMismatch = errors.New("下载的文件与校验和不符")
ErrChecksumMismatch 表示下载内容与发布方公布的校验和不符。
var ErrInvalidMirror = errors.New("加速地址不合法")
ErrInvalidMirror 表示加速地址不合法。
Functions ¶
func NormalizeMirrors ¶ added in v1.0.0
NormalizeMirrors 校验并整理加速地址:只收 https,不带账号、查询串与锚点,统一以斜杠结尾,去重。
地址用作前缀,下载时拼成「前缀 + GitHub 原地址」,这是这类加速服务通用的写法。
func PruneBackups ¶
PruneBackups 只保留最新的 keep 份备份,返回删掉的份数。 keep 小于 1 时不清理——站长把它设成 0 就是「都留着,我自己管」。
Types ¶
type Asset ¶
type Asset struct {
Name string `json:"name"`
URL string `json:"url"`
Size int64 `json:"size"`
// SHA256 是 GitHub 接口给出的内容摘要(十六进制小写);老的发布没有,为空。
SHA256 string `json:"-"`
}
Asset 是一个发布资产。
type Backup ¶
type Backup struct {
Name string `json:"name" doc:"备份文件名,删除时用它定位"`
// Version 是这份备份对应的版本,从文件名解析。
Version string `json:"version"`
Size int64 `json:"size" doc:"字节数"`
// CreatedAt 取自文件名里的时间戳,不是文件的修改时间——
// 拷贝、迁移都会改后者,而备份是哪天的这件事不该跟着变。
CreatedAt time.Time `json:"createdAt"`
}
Backup 是一份旧版本备份。
type BuildInfo ¶
type BuildInfo struct {
Version string `json:"version"`
Commit string `json:"commit"`
Date string `json:"date"`
Platform string `json:"platform"`
}
BuildInfo 是当前运行的构建。
type Downloader ¶
type Downloader struct {
// contains filtered or unexported fields
}
Downloader 取回发布资产并验明正身。
func NewDownloader ¶
func NewDownloader(token, userAgent string, logger *slog.Logger) *Downloader
NewDownloader 构造下载器。
func (*Downloader) Fetch ¶
func (d *Downloader) Fetch(ctx context.Context, rel *Release, dir string, mirrors []string, hooks FetchHooks) (string, error)
Fetch 把发布包下载到 dir 下并核对校验和,返回归档文件的路径。
校验和不是可选步骤。优先用 GitHub 接口给的 SHA-256:它和资产列表出自同一次接口调用, 发布包从加速地址下载时,加速地址改不了这个值;老的发布没有这个字段,退回 checksums.txt, 且只从 GitHub 直连取。HTTPS 只保证「东西是从那台服务器来的」,校验和才保证「是发布者打出来的那一份」—— 这个文件接下来要被当作本机的主程序执行。
来源是加速地址加上 GitHub 直连:先各下一小段测速,按快慢依次试,前一个失败或内容对不上就换下一个。
type Environment ¶
type Environment struct {
// Executable 是当前二进制的绝对路径,已解开符号链接。
//
// 解符号链接是必须的:不少部署把 /usr/local/bin/lumo 指到带版本号的实体文件,
// 替换链接本身会把那套版本管理弄乱,而替换实体才是站长想要的。
Executable string
// Dir 是二进制所在目录,新版本先落到这里再原地改名。
Dir string
// Container 为真表示跑在容器里。
Container bool
// Persistent 为真表示程序文件落在挂载卷上,活得比容器长。
// 不在容器里时恒为真(宿主机的磁盘本来就不会因为谁重建而消失)。
Persistent bool
// MountPoint 是程序文件所属的挂载点,排障时的第一手线索。
MountPoint string
// Writable 为真表示二进制所在目录可写。
Writable bool
// OS 与 Arch 是当前平台,用来挑发布资产。
OS string
Arch string
// Err 是探测过程中的错误描述,空串表示探测正常。
Err string
}
Environment 是「这份二进制能不能自己换掉自己」的判断依据。
每个否定条件都对应一种真实部署:拿不到自己的路径就无从替换、目录只读的话 根本写不进去、程序文件躺在容器自己的文件系统上时换了也白换(重建容器就回到 镜像里的版本)。判断结果要原样告诉站长 ——「更新按钮为什么是灰的」必须当场能答。
注意第三条问的是「程序文件会不会随容器消失」,**不是**「在不在容器里」。 两者在官方镜像上同解,但 1Panel、宝塔的运行环境是「容器里跑挂载目录上的 二进制」——文件在宿主机磁盘上,容器重建一根汗毛都不少。拿前者当后者的替身, 就会把面板用户里最常见的那种装法整个挡在门外。
type FetchHooks ¶ added in v1.0.0
type FetchHooks struct {
// Source 在开始用某个下载来源时调用,参数是它的域名。
Source func(name string)
// Progress 汇报这个来源已下载与总共的字节数,已节流;换来源时从 0 重新算。
Progress func(done, total int64)
}
FetchHooks 是下载过程中给调用方的回调,都可以为 nil。
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler 提供在线升级的 Console 接口。
四个端点全部要 settings:manage,与日志页同一个理由:沿用而不是新增一条 update:manage —— 内置角色的权限只在首次创建时写入,新增权限串会让已有站点的 管理员拿不到它。语义上也说得通:能改站点设置的人,本就是那个决定「要不要升级」的人。
type Installer ¶
type Installer struct {
// contains filtered or unexported fields
}
Installer 把下载好的发布包换成正在运行的这份二进制。
顺序是「解包 → 自检 → 备份 → 替换」,每一步失败都不留下半成品: 一次装坏的升级意味着站点起不来,而那时站长手上只剩 SSH。
func NewInstaller ¶
func NewInstaller(env Environment, backupDir string) *Installer
NewInstaller 构造安装器。
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module 是在线升级模块。
无数据库表,故不实现 Migrator:升级状态是「这台机器此刻的事」, 重启之后本就该重新探测,落库只会留下一堆过期的判断。 也不声明权限串(见 Handler 的说明)与菜单项——升级界面在关于页里, 一年用不到几次的功能不该在侧边栏常驻一个入口。
func (*Module) Permissions ¶
func (m *Module) Permissions() []app.Permission
Permissions 实现 app.PermissionProvider:本模块不新增权限串。
type Phase ¶
type Phase string
Phase 是升级任务所处的阶段。
const ( // PhaseIdle 表示没有正在进行的升级。 PhaseIdle Phase = "idle" // PhaseConnecting 表示正在连接下载源,发布包还没收到一个字节。 PhaseConnecting Phase = "connecting" // PhaseDownloading 表示正在下载发布包,此时进度有意义。 PhaseDownloading Phase = "downloading" // PhaseInstalling 表示正在校验与替换程序文件。 PhaseInstalling Phase = "installing" // PhaseRestarting 表示新版本已就位,正在重启服务。 PhaseRestarting Phase = "restarting" // PhaseReady 表示已装好但没能自动重启,需要人工重启。 PhaseReady Phase = "ready" // PhaseFailed 表示这次升级失败,原因在 Error 里。 PhaseFailed Phase = "failed" )
升级任务的阶段取值。界面按它决定显示进度条、结果还是错误。
type Release ¶
type Release struct {
Tag string `json:"tag"`
Name string `json:"name"`
Notes string `json:"notes"`
HTMLURL string `json:"htmlUrl"`
PublishedAt time.Time `json:"publishedAt"`
Prerelease bool `json:"prerelease"`
// Version 是从 Tag 解析出的版本,解析失败时为零值。
Version Version `json:"-"`
// Asset 是当前平台的发布包。
Asset Asset `json:"asset"`
// Checksums 是校验和清单,没有它就不允许安装(见 download.go)。
Checksums Asset `json:"-"`
}
Release 是一次发布,已挑好当前平台要用的资产。
type Rollback ¶ added in v0.1.4
type Rollback struct {
// From 是在线升级装过的版本,也就是本该在跑的那个。
From string `json:"from"`
// To 是现在实际在跑的版本。
To string `json:"to"`
// At 是那次升级的时刻,用来让站长对上「我那天确实升过级」。
At time.Time `json:"at"`
}
Rollback 描述一次被检测到的版本回退。
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service 是在线升级的全部业务逻辑。
func NewService ¶
NewService 构造服务。
func (*Service) Apply ¶
func (s *Service) Apply(base context.Context) (UpdateProgress, error)
Apply 启动一次升级。接口立即返回,进度经 Status 查询。
ctx 只用于启动前的校验:任务本身跑在应用级的 context 上, 不能让一次浏览器刷新把正在替换文件的过程掐断。
func (*Service) CleanupStale ¶
func (s *Service) CleanupStale()
CleanupStale 清理上一次升级遗留的旧程序文件,启动时调用一次。
func (*Service) DeleteBackup ¶
DeleteBackup 删除一份备份。
func (*Service) DetectRollback ¶ added in v0.1.4
func (s *Service) DetectRollback()
DetectRollback 在启动时检测版本回退,结果留给 Status 展示。
最典型的一幕:容器里开着 allowInContainer 升到了新版本,几个月后站长在面板上 改了个端口,容器被重建,站点悄悄回到镜像里的旧版本——而数据库已经迁到新版本的 schema 了。这条 warn 是把它说出来的唯一机会。
func (*Service) OnShutdownRequest ¶
func (s *Service) OnShutdownRequest(fn func())
OnShutdownRequest 注入「请求停机」的回调。
func (*Service) Relaunch ¶
Relaunch 用替换后的二进制重新拉起服务。
必须在 HTTP 已经停机、模块与数据库都关好之后调用:Unix 上它不会返回 (当前进程镜像就地被换掉),Windows 上它会起一个新进程,而那个新进程 立刻就要去绑同一个端口。
func (*Service) RelaunchPending ¶
RelaunchPending 报告停机之后是否要拉起新版本。
func (*Service) ResetMirrors ¶ added in v1.0.0
ResetMirrors 回到内置的加速地址。
func (*Service) SetMirrors ¶ added in v1.0.0
SetMirrors 保存站长填的下载加速地址;空列表表示只直连 GitHub。
func (*Service) StartAutoCheck ¶
StartAutoCheck 起一个后台循环定期检查新版本。ctx 结束即退出。
type Source ¶
type Source struct {
// contains filtered or unexported fields
}
Source 从 GitHub Releases 查询版本。
type UpdateProgress ¶
type UpdateProgress struct {
Phase Phase `json:"phase" doc:"idle / connecting / downloading / installing / restarting / ready / failed"`
// Version 是这次要装的版本。
Version string `json:"version,omitempty"`
// Source 是正在用的下载来源(域名),测速之前为空。
Source string `json:"source,omitempty" doc:"正在用的下载来源"`
// Downloaded 与 Total 只在下载阶段有意义,Total 为 0 表示源没给出长度。换来源时 Downloaded 从 0 重新算。
Downloaded int64 `json:"downloaded" doc:"已下载字节数"`
Total int64 `json:"total" doc:"发布包总字节数,0 表示未知"`
// Backup 是这次升级留下的备份文件名。
Backup string `json:"backup,omitempty"`
// Error 是失败原因,可直接显示。
Error string `json:"error,omitempty"`
StartedAt *time.Time `json:"startedAt,omitempty"`
// UpdatedAt 是状态最后一次变化的时刻,界面用它判断任务是不是卡住了。
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
}
UpdateProgress 是一次升级任务的实时状态。
type UpdateStatus ¶
type UpdateStatus struct {
Enabled bool `json:"enabled" doc:"功能是否启用,关闭时其余字段仅供展示"`
Current BuildInfo `json:"current"`
// Latest 是查到的最新发布,尚未检查或检查失败时为 null。
Latest *Release `json:"latest,omitempty"`
// HasUpdate 为真表示 Latest 比当前版本新。
HasUpdate bool `json:"hasUpdate"`
// VersionComparable 为假表示当前版本号解析不出来(源码直接构建的开发版),
// 此时不做「有没有新版本」的判断,只把查到的最新版本摆出来。
VersionComparable bool `json:"versionComparable"`
CheckedAt *time.Time `json:"checkedAt,omitempty"`
CheckError string `json:"checkError,omitempty"`
// CanUpdate 为假表示这台机器上不能就地升级,原因在 Reason 里。
CanUpdate bool `json:"canUpdate"`
Reason string `json:"reason,omitempty" doc:"不能就地升级的原因"`
Container bool `json:"container" doc:"是否运行在容器中"`
// Persistent 为真表示程序文件在挂载卷上,重建容器不会把它换回去。
Persistent bool `json:"persistent" doc:"程序文件是否活得比容器长"`
// MountPoint 是程序文件所属的挂载点,容器里排障用。
MountPoint string `json:"mountPoint,omitempty"`
// Image 是容器部署该换成的镜像标签,仅在容器里且查到新版本时有值。
Image string `json:"image,omitempty" doc:"容器部署要换成的镜像引用"`
// Rollback 非空表示当前跑的版本比在线升级装过的那个旧——多半是容器被重建,
// 回到了镜像里的版本。
Rollback *Rollback `json:"rollback,omitempty"`
// Executable 是当前程序的路径,排障时要看的第一样东西。
Executable string `json:"executable,omitempty"`
Repo string `json:"repo" doc:"更新源仓库"`
AutoCheck bool `json:"autoCheck"`
CheckInterval string `json:"checkInterval" doc:"自动检查间隔"`
Prerelease bool `json:"prerelease" doc:"是否把预发布版本算作升级目标"`
Progress UpdateProgress `json:"progress"`
Backups []Backup `json:"backups"`
BackupDir string `json:"backupDir"`
// KeepBackups 是升级后自动保留的备份份数,0 表示不自动清理。
KeepBackups int `json:"keepBackups"`
// Mirrors 是下载加速地址,升级时与 GitHub 直连一起测速,挑快的下。
Mirrors []string `json:"mirrors" doc:"下载加速地址(前缀),空表示只直连 GitHub"`
// MirrorsCustom 为假表示用的是内置地址,站长没改过。
MirrorsCustom bool `json:"mirrorsCustom" doc:"加速地址是否被站长改过"`
DefaultMirrors []string `json:"defaultMirrors" doc:"内置的加速地址"`
}
UpdateStatus 是关于页要展示的全部信息,一个端点一次给全。
类型名带 Update 前缀而不是简单的 Status:OpenAPI 的组件名是全局命名空间, 一个叫 Status 的模式会先到先得地占住那个名字,让后来者被迫改名或加后缀。
合成一个响应而不是拆成「版本/进度/备份」三个端点:这一页会被轮询 (升级过程中每秒一次),三个端点就是三倍的请求与三份可能互相矛盾的快照。
type Version ¶
type Version struct {
Major int
Minor int
Patch int
// Prerelease 是 `-` 之后、`+` 之前的部分,空串表示正式版。
Prerelease string
// Raw 是去掉 v 前缀后的原文,回显给界面用。
Raw string
}
Version 是一个解析后的语义化版本。
自己解析而不是引依赖:需要的只是「比大小」这一件事,而发布用的 tag 与 本地 go build 注入的 `git describe` 输出(如 v0.3.0-4-gab12cd-dirty) 都不是严格的 semver —— 严格库会直接拒收后者,于是开发态下整页报错, 而那恰恰是最常看到这个页面的场景。
func ParseVersion ¶
ParseVersion 解析版本串,宽容地接受 `v` 前缀与构建元数据。
解析不出主版本号时返回 false:调用方据此把「比不出大小」与「比出来更旧」 区分开 —— 前者要在界面上说明原因,后者才是提示升级。