update

package
v0.1.3 Latest Latest
Warning

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

Go to latest
Published: Sep 22, 2026 License: AGPL-3.0 Imports: 32 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 ErrBadBackupName = errors.New("备份名不合法")

ErrBadBackupName 表示备份名不合法。

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

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

Functions

func CompareVersions

func CompareVersions(a, b Version) int

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

func DeleteBackup

func DeleteBackup(dir, name string) error

DeleteBackup 删除一份备份。

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"`
}

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) *Downloader

NewDownloader 构造下载器。

func (*Downloader) Fetch

func (d *Downloader) Fetch(ctx context.Context, rel *Release, dir string, onProgress func(done, total int64)) (string, error)

Fetch 把发布包下载到 dir 下并核对校验和,返回归档文件的路径。

校验和不是可选步骤:没有 checksums.txt 就直接拒绝安装。HTTPS 只保证 「东西是从 GitHub 来的」,校验和才保证「是发布者打出来的那一份」—— 而这个文件接下来要被当作本机的主程序执行。

type Environment

type Environment struct {
	// Executable 是当前二进制的绝对路径,已解开符号链接。
	//
	// 解符号链接是必须的:不少部署把 /usr/local/bin/lumo 指到带版本号的实体文件,
	// 替换链接本身会把那套版本管理弄乱,而替换实体才是站长想要的。
	Executable string
	// Dir 是二进制所在目录,新版本先落到这里再原地改名。
	Dir string
	// Container 为真表示跑在容器里。
	Container bool
	// Writable 为真表示二进制所在目录可写。
	Writable bool
	// OS 与 Arch 是当前平台,用来挑发布资产。
	OS   string
	Arch string
	// Err 是探测过程中的错误描述,空串表示探测正常。
	Err string
}

Environment 是「这份二进制能不能自己换掉自己」的判断依据。

三个否定条件各自对应一种真实部署:容器里换了也白换(重启回到镜像里的旧版本)、 目录只读的话根本写不进去、拿不到自己的路径就无从替换。 判断结果要原样告诉站长 —— 「更新按钮为什么是灰的」必须当场能答。

func DetectEnvironment

func DetectEnvironment() Environment

DetectEnvironment 探测当前运行环境。

func (Environment) CanUpdate

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

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

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"
	// 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 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) 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) 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 / downloading / installing / restarting / ready / failed"`
	// Version 是这次要装的版本。
	Version string `json:"version,omitempty"`
	// Downloaded 与 Total 只在下载阶段有意义,Total 为 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:"是否运行在容器中"`
	// 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"`
}

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