backup

package
v0.33.3 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 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"`
	// MissingObjects **DB 引用了但磁盘上已经没有**的对象(存量数据本身缺失)。
	//
	// 为什么要单独记:真机实录(2026-10-31,10.10.10.3)——DB 里有 300+ 条分享记录,
	// 其中若干对象文件早被清理 ⇒ 若「读不到就整体失败」,**备份包在这台实例上根本导不出来**
	// ⇒ 灾备能力在最需要它的机器上反而不可用。
	// ⇒ 改成:缺失对象**跳过并如实记账**(不是静默吞掉);其余故障(权限/网络/驱动错)
	// 仍然整体报错 —— 真故障不能吞。
	MissingObjects []string `json:"missing_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)
	// FileExists 对象是否已存在(**存量数据缺失**的判定:读之前先问一句,
	// 免得把「文件本来就没了」与「存储后端出故障」混为一谈)。
	FileExists(ctx context.Context, filePath string) bool
}

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