mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-06-13 05:39:35 +08:00
文件备份新增差异模式:仅打包自上次全量以来的变更并记录删除,恢复自动按全量+差异链还原。含基线解析、链式恢复、保留链保护与本机文件任务校验;清单/比对/删除/往返/保留保护单测全覆盖。
142 lines
4.7 KiB
Go
142 lines
4.7 KiB
Go
package backup
|
||
|
||
import (
|
||
"archive/tar"
|
||
"context"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
)
|
||
|
||
func diffWrite(t *testing.T, p, content string) {
|
||
t.Helper()
|
||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||
t.Fatalf("mkdir %s: %v", filepath.Dir(p), err)
|
||
}
|
||
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
||
t.Fatalf("write %s: %v", p, err)
|
||
}
|
||
}
|
||
|
||
func diffAssertContent(t *testing.T, p, want string) {
|
||
t.Helper()
|
||
got, err := os.ReadFile(p)
|
||
if err != nil {
|
||
t.Fatalf("read %s: %v", p, err)
|
||
}
|
||
if string(got) != want {
|
||
t.Fatalf("%s content = %q, want %q", p, string(got), want)
|
||
}
|
||
}
|
||
|
||
func diffAssertAbsent(t *testing.T, p string) {
|
||
t.Helper()
|
||
if _, err := os.Stat(p); !os.IsNotExist(err) {
|
||
t.Fatalf("expected %s to be absent, stat err=%v", p, err)
|
||
}
|
||
}
|
||
|
||
func diffArchiveNames(t *testing.T, artifactPath string) map[string]bool {
|
||
t.Helper()
|
||
f, err := os.Open(artifactPath)
|
||
if err != nil {
|
||
t.Fatalf("open artifact: %v", err)
|
||
}
|
||
defer f.Close()
|
||
names := map[string]bool{}
|
||
tr := tar.NewReader(f)
|
||
for {
|
||
h, err := tr.Next()
|
||
if err == io.EOF {
|
||
break
|
||
}
|
||
if err != nil {
|
||
t.Fatalf("read tar: %v", err)
|
||
}
|
||
names[h.Name] = true
|
||
}
|
||
return names
|
||
}
|
||
|
||
// TestFileRunnerDifferentialRoundTrip 验证差异备份的端到端正确性:
|
||
// 全量 → 修改源(变更/删除/新增)→ 差异 → 链式恢复(全量+差异)→ 结果与修改后源一致。
|
||
func TestFileRunnerDifferentialRoundTrip(t *testing.T) {
|
||
work := t.TempDir()
|
||
src := filepath.Join(work, "src")
|
||
diffWrite(t, filepath.Join(src, "a.txt"), "alpha")
|
||
diffWrite(t, filepath.Join(src, "b.txt"), "bravo")
|
||
diffWrite(t, filepath.Join(src, "sub", "c.txt"), "charlie")
|
||
|
||
runner := NewFileRunner()
|
||
|
||
full, err := runner.Run(context.Background(), TaskSpec{Name: "diff", Type: "file", SourcePath: src, TempDir: t.TempDir()}, NopLogWriter{})
|
||
if err != nil {
|
||
t.Fatalf("full Run: %v", err)
|
||
}
|
||
if full.Manifest == nil || len(full.Manifest.Entries) == 0 {
|
||
t.Fatalf("full backup must produce a manifest, got %#v", full.Manifest)
|
||
}
|
||
|
||
// 变更 a.txt(内容变长 → size 差异必被检出)、删除 b.txt、新增 d.txt;sub/c.txt 不变
|
||
diffWrite(t, filepath.Join(src, "a.txt"), "ALPHA-modified-and-longer")
|
||
if err := os.Remove(filepath.Join(src, "b.txt")); err != nil {
|
||
t.Fatalf("remove b.txt: %v", err)
|
||
}
|
||
diffWrite(t, filepath.Join(src, "d.txt"), "delta")
|
||
|
||
diff, err := runner.Run(context.Background(), TaskSpec{Name: "diff", Type: "file", SourcePath: src, TempDir: t.TempDir(), Differential: true, BaseManifest: *full.Manifest}, NopLogWriter{})
|
||
if err != nil {
|
||
t.Fatalf("differential Run: %v", err)
|
||
}
|
||
if diff.Manifest != nil {
|
||
t.Fatalf("differential backup must not produce a manifest")
|
||
}
|
||
|
||
// 差异归档应包含变更/新增条目与删除清单,但不含未变更的 sub/c.txt
|
||
names := diffArchiveNames(t, diff.ArtifactPath)
|
||
if !names["src/a.txt"] || !names["src/d.txt"] {
|
||
t.Fatalf("differential archive missing changed/new entries: %v", names)
|
||
}
|
||
if names["src/sub/c.txt"] {
|
||
t.Fatalf("differential archive should not contain unchanged file sub/c.txt")
|
||
}
|
||
if !names[deletionsEntryName] {
|
||
t.Fatalf("differential archive missing deletions entry: %v", names)
|
||
}
|
||
|
||
// 链式恢复到全新目标
|
||
restoreRoot := t.TempDir()
|
||
restoreSrc := filepath.Join(restoreRoot, "src")
|
||
restoreTask := TaskSpec{Name: "diff", Type: "file", SourcePath: restoreSrc}
|
||
if err := runner.Restore(context.Background(), restoreTask, full.ArtifactPath, NopLogWriter{}); err != nil {
|
||
t.Fatalf("restore full: %v", err)
|
||
}
|
||
if err := runner.Restore(context.Background(), restoreTask, diff.ArtifactPath, NopLogWriter{}); err != nil {
|
||
t.Fatalf("restore differential: %v", err)
|
||
}
|
||
|
||
diffAssertContent(t, filepath.Join(restoreSrc, "a.txt"), "ALPHA-modified-and-longer")
|
||
diffAssertContent(t, filepath.Join(restoreSrc, "sub", "c.txt"), "charlie")
|
||
diffAssertContent(t, filepath.Join(restoreSrc, "d.txt"), "delta")
|
||
diffAssertAbsent(t, filepath.Join(restoreSrc, "b.txt"))
|
||
}
|
||
|
||
// TestFileRunnerDifferentialWithoutBaseIsFull 验证无基线时差异请求回退为全量(产出清单、含全部文件)。
|
||
func TestFileRunnerDifferentialWithoutBaseIsFull(t *testing.T) {
|
||
src := filepath.Join(t.TempDir(), "src")
|
||
diffWrite(t, filepath.Join(src, "a.txt"), "alpha")
|
||
|
||
runner := NewFileRunner()
|
||
res, err := runner.Run(context.Background(), TaskSpec{Name: "diff", Type: "file", SourcePath: src, TempDir: t.TempDir(), Differential: true}, NopLogWriter{})
|
||
if err != nil {
|
||
t.Fatalf("Run: %v", err)
|
||
}
|
||
if res.Manifest == nil {
|
||
t.Fatalf("differential without base must fall back to full and produce a manifest")
|
||
}
|
||
if names := diffArchiveNames(t, res.ArtifactPath); !names["src/a.txt"] || names[deletionsEntryName] {
|
||
t.Fatalf("fallback-full archive unexpected: %v", names)
|
||
}
|
||
}
|