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 同形)
三条语义(缺一条就危险) ¶
- **取件码与分享链接不变**:DB 快照原样带走(含 file_codes / file_code_files / system_configs)⇒ 恢复后用户的对外名片(码 / 链接)照旧可用。 files.json 是这份承诺的**独立副本**,便于事后逐条核对。
- **恢复是破坏性操作**:它会替换 DB 与对象 ⇒ 必须**显式确认**(Confirm), 且必须**停服**后执行(DB 文件被替换时,运行中的实例会读到半旧半新的数据)。
- **导出/恢复全程不碰取件次数**:这是运维动作,不是取件动作。
为什么导出/恢复是「离线子命令」而不是 HTTP 端点 ¶
导出/恢复天然是**同机**操作(要读本地 DB 与本地存储),做成 HTTP 端点反而多一层 「服务端正在运行 ⇒ 恢复会砸自己的脚」的风险;而 `pb` 是**纯 HTTP 客户端** (不链接 core),要让 `pb backup` 能落地就得把 core 拖进这个轻量 CLI(体积 + 数十 MB)。⇒ 形态定为**server 二进制的离线子命令**(`pigeonbox backup export` / `pigeonbox restore`),与「恢复必须停服」的事实一致。
Index ¶
Constants ¶
const ( ManifestName = "manifest.json" DBEntryName = "db/backup.db" FileListName = "files.json" ObjectDirName = "objects/" UnsupportedFmt = "不认识的备份包格式版本" )
备份包内固定文件名(manifest 的读取方据此定位)。
const FormatVersion = 1
FormatVersion 当前备份包格式版本。
**格式本身按版本演进**(规划稿要求):格式一变就 +1,老包用老版本号解析 ⇒ 恢复时遇到**不认识**的版本必须**拒绝**(宁可报错也不猜着还原)。
Variables ¶
var ErrRestoreNeedsConfirm = fmt.Errorf("恢复会替换数据库与对象,属破坏性操作:须显式确认")
ErrRestoreNeedsConfirm 恢复未显式确认(破坏性操作必须显式确认)。
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 ¶
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 快照 + 存储对象)。
三步,缺一不可 ¶
- **解包到临时目录**:条目名逐个过 safeEntryName(防 tar 路径穿越)⇒ 来路不明的包不能往机器上写任意文件。
- **逐项校验**:manifest 的格式版本必须认识;每个对象的 sha256 必须对得上; DB 快照必须能打开。**任一不符 ⇒ 整体中止,一个字节都不落盘**。
- **落盘**:对象经存储层写回(路径逃逸由存储层自防);DB 用「写临时 + rename」 原子替换 ⇒ 中途断电不会留下半个 DB。