Help us shape the future of Go! Take the 2026 Go Developer Survey

update

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2026 License: AGPL-3.0 Imports: 35 Imported by: 0

Documentation

Overview

Package update 提供从 GitHub Releases 就地升级的能力:检查新版本、 下载并核对校验和、备份当前二进制、替换并重启。

这是整个程序里唯一一处会改写「自己」的代码,故每一步都按「失败了也要留个能跑的站点」 来设计:校验和不对不装、新二进制跑不起来不装、替换之前先备份、替换失败原样退回。

不在容器里做就地升级:镜像是只读的,换掉的文件会在下次重建容器时消失, 站长会得到一次「升级成功但版本没变」的诡异经历。容器部署该换镜像标签。

Index

Constants

View Source
const Name = "update"

Name 是模块名,也是 App.Provide 的键。

Variables

View Source
var (
	// ErrDisabled 表示在线升级功能被配置关闭。
	ErrDisabled = errors.New("在线升级已关闭")
	// ErrBusy 表示已有一个升级任务在进行。
	ErrBusy = errors.New("已有升级任务在进行")
	// ErrNotUpgradable 表示当前环境不支持就地升级。
	ErrNotUpgradable = errors.New("当前部署形态不支持就地升级")
	// ErrNoUpdate 表示没有可安装的新版本。
	ErrNoUpdate = errors.New("当前已是最新版本")
)

升级流程的错误哨兵。

View Source
var (
	// ErrNoRelease 表示源仓库还没有任何可用发布。
	ErrNoRelease = errors.New("更新源还没有发布任何版本")
	// ErrRateLimited 表示被 GitHub 限流。
	ErrRateLimited = errors.New("已达到 GitHub 的访问频率上限")
	// ErrNoAsset 表示该版本没有当前平台的发布包。
	ErrNoAsset = errors.New("该版本没有提供当前平台的发布包")
)

更新源相关的错误哨兵。措辞要能直接显示给站长:这些错误多半不是「程序坏了」, 而是「源仓库还没发过版」「今天问得太频繁」这类需要人去处置的状况。

View Source
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 说了算,转手的一方改不了内容,最多让下载失败。

View Source
var ErrBadBackupName = errors.New("备份名不合法")

ErrBadBackupName 表示备份名不合法。

View Source
var ErrChecksumMismatch = errors.New("下载的文件与校验和不符")

ErrChecksumMismatch 表示下载内容与发布方公布的校验和不符。

View Source
var ErrInvalidMirror = errors.New("加速地址不合法")

ErrInvalidMirror 表示加速地址不合法。

Functions

func CompareVersions

func CompareVersions(a, b Version) int

CompareVersions 比较两个版本,返回 -1 / 0 / 1。

func DeleteBackup

func DeleteBackup(dir, name string) error

DeleteBackup 删除一份备份。

func NormalizeMirrors added in v1.0.0

func NormalizeMirrors(in []string) ([]string, error)

NormalizeMirrors 校验并整理加速地址:只收 https,不带账号、查询串与锚点,统一以斜杠结尾,去重。

地址用作前缀,下载时拼成「前缀 + GitHub 原地址」,这是这类加速服务通用的写法。

func PruneBackups

func PruneBackups(dir string, keep int) int

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 是一份旧版本备份。

func ListBackups

func ListBackups(dir string) ([]Backup, error)

ListBackups 列出备份目录里的旧版本,最新的在前。

目录不存在时返回空列表而不是报错:从没升级过的站点就没有这个目录。

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、宝塔的运行环境是「容器里跑挂载目录上的 二进制」——文件在宿主机磁盘上,容器重建一根汗毛都不少。拿前者当后者的替身, 就会把面板用户里最常见的那种装法整个挡在门外。

func DetectEnvironment

func DetectEnvironment() Environment

DetectEnvironment 探测当前运行环境。

func (Environment) CanUpdate

func (e Environment) CanUpdate(allowInContainer bool) (ok bool, reason string)

CanUpdate 报告能否就地升级,不能时一并给出原因。

allowInContainer 来自配置,只对「程序文件不持久」这一条起作用:它是站长 明确选择承担「重建容器就退回去」这个代价(回退检测见 state.go)。 它推翻不了「目录不可写」——那是物理事实,不是策略。

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 —— 内置角色的权限只在首次创建时写入,新增权限串会让已有站点的 管理员拿不到它。语义上也说得通:能改站点设置的人,本就是那个决定「要不要升级」的人。

func NewHandler

func NewHandler(service *Service) *Handler

NewHandler 构造 Handler。

func (*Handler) Register

func (h *Handler) Register(console huma.API)

Register 挂载接口。

type Installer

type Installer struct {
	// contains filtered or unexported fields
}

Installer 把下载好的发布包换成正在运行的这份二进制。

顺序是「解包 → 自检 → 备份 → 替换」,每一步失败都不留下半成品: 一次装坏的升级意味着站点起不来,而那时站长手上只剩 SSH。

func NewInstaller

func NewInstaller(env Environment, backupDir string) *Installer

NewInstaller 构造安装器。

func (*Installer) Install

func (i *Installer) Install(ctx context.Context, archivePath string, expect Version, currentVersion string) (string, error)

Install 安装一个已经过校验和核对的发布包,返回备份文件名。

expect 是期望装上的版本,用于自检时核对——发布包名字对不代表里面的东西对。

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module 是在线升级模块。

无数据库表,故不实现 Migrator:升级状态是「这台机器此刻的事」, 重启之后本就该重新探测,落库只会留下一堆过期的判断。 也不声明权限串(见 Handler 的说明)与菜单项——升级界面在关于页里, 一年用不到几次的功能不该在侧边栏常驻一个入口。

func From

func From(a *app.App) *Module

From 从 App 取回本模块;未装配时返回 nil。

func New

func New() *Module

New 构造模块。

func (*Module) Name

func (m *Module) Name() string

Name 实现 app.Module。

func (*Module) Permissions

func (m *Module) Permissions() []app.Permission

Permissions 实现 app.PermissionProvider:本模块不新增权限串。

func (*Module) Register

func (m *Module) Register(a *app.App) error

Register 实现 app.Module:只做装配,不访问数据库,也不碰网络。

func (*Module) Routes

func (m *Module) Routes(r app.Router)

Routes 实现 app.RouteProvider。

func (*Module) Service

func (m *Module) Service() *Service

Service 返回升级服务,供 cmd/lumo 接线停机与重启。

func (*Module) Start

func (m *Module) Start(ctx context.Context) error

Start 实现 app.Starter:清理上次升级的残留,并起后台检查循环。

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

func NewService(cfg config.Config, current version.Info, logger *slog.Logger) *Service

NewService 构造服务。

func (*Service) Apply

func (s *Service) Apply(base context.Context) (UpdateProgress, error)

Apply 启动一次升级。接口立即返回,进度经 Status 查询。

ctx 只用于启动前的校验:任务本身跑在应用级的 context 上, 不能让一次浏览器刷新把正在替换文件的过程掐断。

func (*Service) Backups

func (s *Service) Backups() ([]Backup, error)

Backups 列出备份。

func (*Service) Check

func (s *Service) Check(ctx context.Context) (*Release, error)

Check 查询更新源并缓存结果。

func (*Service) CleanupStale

func (s *Service) CleanupStale()

CleanupStale 清理上一次升级遗留的旧程序文件,启动时调用一次。

func (*Service) DeleteBackup

func (s *Service) DeleteBackup(name string) error

DeleteBackup 删除一份备份。

func (*Service) DetectRollback added in v0.1.4

func (s *Service) DetectRollback()

DetectRollback 在启动时检测版本回退,结果留给 Status 展示。

最典型的一幕:容器里开着 allowInContainer 升到了新版本,几个月后站长在面板上 改了个端口,容器被重建,站点悄悄回到镜像里的旧版本——而数据库已经迁到新版本的 schema 了。这条 warn 是把它说出来的唯一机会。

func (*Service) Environment

func (s *Service) Environment() Environment

Environment 返回探测到的运行环境。

func (*Service) OnShutdownRequest

func (s *Service) OnShutdownRequest(fn func())

OnShutdownRequest 注入「请求停机」的回调。

func (*Service) Progress

func (s *Service) Progress() UpdateProgress

Progress 返回当前任务进度。

func (*Service) Relaunch

func (s *Service) Relaunch() error

Relaunch 用替换后的二进制重新拉起服务。

必须在 HTTP 已经停机、模块与数据库都关好之后调用:Unix 上它不会返回 (当前进程镜像就地被换掉),Windows 上它会起一个新进程,而那个新进程 立刻就要去绑同一个端口。

func (*Service) RelaunchPending

func (s *Service) RelaunchPending() bool

RelaunchPending 报告停机之后是否要拉起新版本。

func (*Service) ResetMirrors added in v1.0.0

func (s *Service) ResetMirrors() error

ResetMirrors 回到内置的加速地址。

func (*Service) SetMirrors added in v1.0.0

func (s *Service) SetMirrors(mirrors []string) error

SetMirrors 保存站长填的下载加速地址;空列表表示只直连 GitHub。

func (*Service) StartAutoCheck

func (s *Service) StartAutoCheck(ctx context.Context)

StartAutoCheck 起一个后台循环定期检查新版本。ctx 结束即退出。

func (*Service) Status

func (s *Service) Status() UpdateStatus

Status 汇总当前状态。

type Source

type Source struct {
	// contains filtered or unexported fields
}

Source 从 GitHub Releases 查询版本。

func NewSource

func NewSource(repo, token, userAgent, baseURL string) *Source

NewSource 构造更新源。repo 形如 owner/name,token 与 baseURL 可为空。

func (*Source) Latest

func (s *Source) Latest(ctx context.Context, allowPrerelease bool) (*Release, error)

Latest 取回可升级到的最新发布。

allowPrerelease 为假时直接问 GitHub 的 latest —— 它本就排除草稿与预发布, 且尊重仓库主人手工指定的「最新」。为真时改拉列表自行挑版本号最大的那个: 列表按创建时间倒序,而「后创建的 tag 版本号一定更大」并不成立 (补丁版往往在新主版本之后才发)。

func (*Source) Repo

func (s *Source) Repo() string

Repo 返回更新源仓库。

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

func ParseVersion(s string) (Version, bool)

ParseVersion 解析版本串,宽容地接受 `v` 前缀与构建元数据。

解析不出主版本号时返回 false:调用方据此把「比不出大小」与「比出来更旧」 区分开 —— 前者要在界面上说明原因,后者才是提示升级。

func (Version) IsPrerelease

func (v Version) IsPrerelease() bool

IsPrerelease 报告是否为预发布版本。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL