Files
BackupX/server/internal/backup/manifest.go
Wu Qing 90b58d58d6 feat(backup): 新增差异备份(differential)模式 (#88)
文件备份新增差异模式:仅打包自上次全量以来的变更并记录删除,恢复自动按全量+差异链还原。含基线解析、链式恢复、保留链保护与本机文件任务校验;清单/比对/删除/往返/保留保护单测全覆盖。
2026-05-27 19:03:40 +08:00

93 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package backup
import (
"encoding/json"
"os"
"path/filepath"
"sort"
)
// deletionsEntryName 是差异备份归档中记录「自全量以来被删除路径」的特殊条目名。
// 恢复时该条目不落地为文件,而是用于在基线之上删除对应路径。
const deletionsEntryName = ".backupx/deletions.json"
// ManifestEntry 记录一次全量备份中单个归档条目(文件或目录)的指纹,
// 供差异备份比对「自全量以来的变化」。Path 为归档内相对名slash 分隔,
// 与 tar header.Name 一致)。字段使用短键以压缩清单体积。
type ManifestEntry struct {
Path string `json:"p"`
Size int64 `json:"s"`
ModTimeNs int64 `json:"m"`
Mode uint32 `json:"o"`
IsDir bool `json:"d,omitempty"`
}
// Manifest 是一次全量备份的完整条目清单(文件与目录)。
type Manifest struct {
Entries []ManifestEntry `json:"entries"`
}
// EncodeManifest 将清单序列化为紧凑 JSON。
func EncodeManifest(m Manifest) ([]byte, error) {
return json.Marshal(m)
}
// DecodeManifest 反序列化清单;空输入返回空清单(视为「无基线」)。
func DecodeManifest(data []byte) (Manifest, error) {
m := Manifest{}
if len(data) == 0 {
return m, nil
}
if err := json.Unmarshal(data, &m); err != nil {
return Manifest{}, err
}
return m, nil
}
// index 构建 path -> entry 映射,便于差异比对 O(1) 查找。
func (m Manifest) index() map[string]ManifestEntry {
idx := make(map[string]ManifestEntry, len(m.Entries))
for _, e := range m.Entries {
idx[e.Path] = e
}
return idx
}
// entryFromInfo 由归档名与文件信息构造指纹条目。
func entryFromInfo(archiveName string, info os.FileInfo) ManifestEntry {
return ManifestEntry{
Path: filepath.ToSlash(archiveName),
Size: info.Size(),
ModTimeNs: info.ModTime().UnixNano(),
Mode: uint32(info.Mode().Perm()),
IsDir: info.IsDir(),
}
}
// changedSince 判断当前条目相对基线是否为「新增或变更」(即应纳入差异归档)。
// - 不在基线中 → 新增,纳入;
// - 已存在的目录 → 不携带数据,跳过(其下变更文件会各自判定);
// - 文件大小或 mtime 变化 → 变更纳入rsync 风格启发式)。
func changedSince(base map[string]ManifestEntry, cur ManifestEntry) bool {
prev, ok := base[cur.Path]
if !ok {
return true
}
if cur.IsDir {
return false
}
return prev.Size != cur.Size || prev.ModTimeNs != cur.ModTimeNs
}
// deletedPaths 返回基线中存在、但本次遍历未出现的路径(被删除的条目),按路径升序。
func deletedPaths(base map[string]ManifestEntry, seen map[string]struct{}) []string {
deleted := make([]string, 0)
for p := range base {
if _, ok := seen[p]; !ok {
deleted = append(deleted, p)
}
}
sort.Strings(deleted)
return deleted
}