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

backup

package
v0.32.38 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package backup 统一备份包(2026-10-31 起):换机器 / 换后端 / 灾备三件事一个工具。

为什么要有

此前「把整套实例搬到另一台机器」只有两条路,且都**不可靠**:

· 手工拷 data 目录 ⇒ 拷到一半、DB 与对象对不上 ⇒ 恢复后一半分享取不到文件;
· 只拷 DB ⇒ 对象全丢 ⇒ 所有文件分享失效。

⇒ 这里定义**一个格式**,把「DB 快照 + 文件清单 + 存储对象 + 校验」打成**一个包**,

导出与恢复都是同一套代码 ⇒ 灾备演练可以在隔离实例上真的跑一遍。

备份包格式 v1(tar.gz)

pigeonbox-backup.tar.gz
├── manifest.json      格式版本 + 组件版本 + 对象清单(逐个 sha256)
├── db/backup.db       数据库**一致快照**(SQLite:VACUUM INTO)
├── files.json         分享/取件码清单(独立核对用 ⇒ 链接不变的可证据)
└── objects/<相对路径>  存储对象(路径与 file_codes.file_path 同形)

三条语义(缺一条就危险)

  1. **取件码与分享链接不变**:DB 快照原样带走(含 file_codes / file_code_files / system_configs)⇒ 恢复后用户的对外名片(码 / 链接)照旧可用。 files.json 是这份承诺的**独立副本**,便于事后逐条核对。
  2. **恢复是破坏性操作**:它会替换 DB 与对象 ⇒ 必须**显式确认**(Confirm), 且必须**停服**后执行(DB 文件被替换时,运行中的实例会读到半旧半新的数据)。
  3. **导出/恢复全程不碰取件次数**:这是运维动作,不是取件动作。

为什么导出/恢复是「离线子命令」而不是 HTTP 端点

导出/恢复天然是**同机**操作(要读本地 DB 与本地存储),做成 HTTP 端点反而多一层 「服务端正在运行 ⇒ 恢复会砸自己的脚」的风险;而 `pb` 是**纯 HTTP 客户端** (不链接 core),要让 `pb backup` 能落地就得把 core 拖进这个轻量 CLI(体积 + 数十 MB)。⇒ 形态定为**server 二进制的离线子命令**(`pigeonbox backup export` / `pigeonbox restore`),与「恢复必须停服」的事实一致。

Index

Constants

View Source
const (
	ManifestName   = "manifest.json"
	DBEntryName    = "db/backup.db"
	FileListName   = "files.json"
	ObjectDirName  = "objects/"
	UnsupportedFmt = "不认识的备份包格式版本"
)

备份包内固定文件名(manifest 的读取方据此定位)。

View Source
const FormatVersion = 1

FormatVersion 当前备份包格式版本。

**格式本身按版本演进**(规划稿要求):格式一变就 +1,老包用老版本号解析 ⇒ 恢复时遇到**不认识**的版本必须**拒绝**(宁可报错也不猜着还原)。

Variables

View Source
var ErrRestoreNeedsConfirm = fmt.Errorf("恢复会替换数据库与对象,属破坏性操作:须显式确认")

ErrRestoreNeedsConfirm 恢复未显式确认(破坏性操作必须显式确认)。

View Source
var ErrUnsupportedDB = fmt.Errorf("当前仅支持 SQLite 的一致性快照;PG/MySQL 库请用 pg_dump/mysqldump")

ErrUnsupportedDB 非 SQLite 库无法做**一致性快照**(VACUUM INTO 是 SQLite 专有)。

不做「降级成逐表导出」:那种包恢复起来语义含糊(自增、索引、触发器都可能丢) ⇒ 宁可明确报错,让用户用 pg_dump/mysqldump,再走「换库」那条专门的流程。

Functions

This section is empty.

Types

type FileEntry

type FileEntry struct {
	ID         uint   `json:"id"`
	Code       string `json:"code"`
	FilePath   string `json:"file_path"`
	Size       int64  `json:"size"`
	Status     string `json:"status"`
	ExpiredAt  string `json:"expired_at"`
	UsedCount  int    `json:"used_count"`
	UUIDFileNm string `json:"uuid_file_name"`
}

FileEntry files.json 的一条分享记录(**独立核对用**的「链接不变」证据)。

type FileList

type FileList struct {
	// Count 记录条数。
	Count int `json:"count"`
	// Files 分享记录。
	Files []FileEntry `json:"files"`
}

FileList files.json 的结构。

type Manifest

type Manifest struct {
	// BackupFormat 格式版本(见 FormatVersion)。
	BackupFormat int `json:"backup_format"`
	// CreatedAt 导出时间(RFC3339)。
	CreatedAt string `json:"created_at"`
	// AppVersion 导出时的应用版本(构建版本)。
	AppVersion string `json:"app_version"`
	// CoreVersion 导出时的 core 版本(构建版本)。
	CoreVersion string `json:"core_version"`
	// DBType 数据库类型(当前仅 sqlite 支持快照;见 ErrUnsupportedDB)。
	DBType string `json:"db_type"`
	// StorageType 导出时的存储后端类型(local / s3 / …)。
	StorageType string `json:"storage_type"`
	// ObjectCount / ObjectBytes 对象数与总字节。
	ObjectCount int64 `json:"object_count"`
	ObjectBytes int64 `json:"object_bytes"`
	// FileCount 分享/取件码记录数。
	FileCount int64 `json:"file_count"`
	// Objects 对象清单(逐个 sha256)。
	Objects []ObjectEntry `json:"objects"`
}

Manifest 备份包清单(manifest.json)。

Objects 逐个带 sha256 ⇒ 恢复前可**逐字节校验**,杜绝「包在传输中损坏/被截断」 却照样落盘。

type ObjectEntry

type ObjectEntry struct {
	// Path 存储相对路径(与 file_codes.file_path 同形 ⇒ 恢复时原样写回)。
	Path string `json:"path"`
	// Size 字节数。
	Size int64 `json:"size"`
	// SHA256 内容摘要(hex)⇒ 恢复前逐个校验。
	SHA256 string `json:"sha256"`
}

ObjectEntry 备份包内的一个存储对象。

type ObjectLister

type ObjectLister interface {
	// ListReferencedObjectPaths 返回全部被引用的对象路径(去重)。
	ListReferencedObjectPaths(ctx context.Context) ([]string, error)
}

ObjectLister 列出被 DB 引用的对象路径(admin 域实现)。

**本地声明窄接口**(不 import app/storage):Go 的结构化接口让 admin 的服务 直接满足它 ⇒ 备份域与 app/storage **零耦合**(域间跨域要登记白名单,棘轮守卫 会拦)⇒ 同时与存储迁移口径完全一致:两处都含**软删**记录 (软删分享可恢复 ⇒ 对象也得带走)。

type ObjectStore

type ObjectStore interface {
	// GetFileReader 打开对象读取器(同时给出大小)。
	GetFileReader(ctx context.Context, filePath string) (io.ReadCloser, int64, error)
	// EffectiveType 当前生效的存储后端类型(记进 manifest ⇒ 恢复时知道源后端)。
	EffectiveType() string
	// SaveStream 把 reader 写入相对路径(路径逃逸由存储层自防)。
	SaveStream(ctx context.Context, savePath string, r io.Reader, expectedSize int64) (int64, error)
}

ObjectStore 读写存储对象(core/storage 单例实现)。

用**接口**而非具体类型:备份域只依赖「读一个对象 / 写一个对象」两件事, 不必知道后端是 local 还是 S3 ⇒ 恢复时对象**原样写回当前后端**(换后端 + 换机器 一次做完)。

type RestoreOptions

type RestoreOptions struct {
	// Confirm 显式确认(**破坏性操作必须确认**)。
	//
	// 恢复会**替换**数据库文件并重写对象 ⇒ 没确认就恢复 = 把线上数据换成一份
	// 可能不是你要的快照 ⇒ 一律先报错,由调用方(CLI)显式传 --yes。
	Confirm bool
	// DBPath 目标数据库文件(SQLite)。空 = 不恢复 DB(只恢复对象)。
	DBPath string
	// DryRun 只校验(manifest / 摘要 / DB 可用性),不落任何盘。
	DryRun bool
}

RestoreOptions 恢复选项。

type RestoreResult

type RestoreResult struct {
	// Manifest 备份包清单。
	Manifest *Manifest
	// ObjectsRestored / BytesRestored 恢复的对象数与字节数。
	ObjectsRestored int64
	BytesRestored   int64
	// DBRestored 是否已替换数据库。
	DBRestored bool
	// MissingObjects DB 快照引用、但包里没有的对象(导出期间新上传 ⇒ 如实列出,不静默)。
	MissingObjects []string
}

RestoreResult 恢复结果。

type Service

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

Service 备份包服务。

func NewService

func NewService(db *gorm.DB, objects ObjectLister, store ObjectStore) *Service

NewService 创建备份包服务。

依赖都是**窄接口**(对象清单 / 对象读写 / DB)⇒ 备份域不直接依赖任何业务域, 装配在 bootstrap 完成(与「存储域不直接碰 DB」同款纪律)。

func (*Service) Export

func (s *Service) Export(ctx context.Context, w io.Writer) (*Manifest, error)

Export 把「DB 快照 + 文件清单 + 存储对象 + 校验」导出成一个 tar.gz 包。

顺序(为什么 manifest 最后写)

manifest 要含**每个对象的 sha256** ⇒ 只有对象都写完才知道 ⇒ manifest 放最后。 (代价:包内 manifest 不在头部;换来的是**只读一遍对象**,大实例不用双倍 IO。)

一致性口径

DB 快照(VACUUM INTO)是一致视图;对象是**逐个读**的 ⇒ 导出期间新上传的对象 可能只进包不进快照(或反之)⇒ 恢复后这类记录会指向缺失对象 ⇒ 恢复结果里 **如实列出缺失清单**(Missing),不静默。

func (*Service) Restore

func (s *Service) Restore(ctx context.Context, r io.Reader, opts RestoreOptions) (*RestoreResult, error)

Restore 从备份包恢复(DB 快照 + 存储对象)。

三步,缺一不可

  1. **解包到临时目录**:条目名逐个过 safeEntryName(防 tar 路径穿越)⇒ 来路不明的包不能往机器上写任意文件。
  2. **逐项校验**:manifest 的格式版本必须认识;每个对象的 sha256 必须对得上; DB 快照必须能打开。**任一不符 ⇒ 整体中止,一个字节都不落盘**。
  3. **落盘**:对象经存储层写回(路径逃逸由存储层自防);DB 用「写临时 + rename」 原子替换 ⇒ 中途断电不会留下半个 DB。

Jump to

Keyboard shortcuts

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