feat(backup): 新增 zstd 压缩选项 (#89)

备份压缩在 gzip 之外新增 zstd(更高压缩率、更快解压)。pkg/compress 新增 ZstdFile/UnzstdFile,Master 与 Agent 压缩/解压按后缀分流,任务校验与前端下拉同步;往返单测覆盖。
This commit is contained in:
Wu Qing
2026-05-27 19:15:06 +08:00
committed by GitHub
parent 90b58d58d6
commit 65cf3a04d4
9 changed files with 153 additions and 4 deletions

View File

@@ -0,0 +1,65 @@
package compress
import (
"fmt"
"io"
"os"
"strings"
"github.com/klauspost/compress/zstd"
)
// ZstdFile 将文件压缩为 .zstzstd返回压缩产物路径。
// 相比 gzipzstd 在相近 CPU 开销下提供更高压缩率与显著更快的解压速度。
func ZstdFile(sourcePath string) (string, error) {
source, err := os.Open(sourcePath)
if err != nil {
return "", fmt.Errorf("open source file: %w", err)
}
defer source.Close()
targetPath := sourcePath + ".zst"
target, err := os.Create(targetPath)
if err != nil {
return "", fmt.Errorf("create zstd file: %w", err)
}
defer target.Close()
writer, err := zstd.NewWriter(target)
if err != nil {
return "", fmt.Errorf("create zstd writer: %w", err)
}
if _, err := io.Copy(writer, source); err != nil {
_ = writer.Close()
return "", fmt.Errorf("zstd source file: %w", err)
}
if err := writer.Close(); err != nil {
return "", fmt.Errorf("close zstd writer: %w", err)
}
return targetPath, nil
}
// UnzstdFile 解压 .zst 文件,返回解压产物路径。
func UnzstdFile(sourcePath string) (string, error) {
source, err := os.Open(sourcePath)
if err != nil {
return "", fmt.Errorf("open zstd file: %w", err)
}
defer source.Close()
reader, err := zstd.NewReader(source)
if err != nil {
return "", fmt.Errorf("create zstd reader: %w", err)
}
defer reader.Close()
targetPath := strings.TrimSuffix(sourcePath, ".zst")
if targetPath == sourcePath {
targetPath += ".out"
}
target, err := os.Create(targetPath)
if err != nil {
return "", fmt.Errorf("create target file: %w", err)
}
defer target.Close()
if _, err := io.Copy(target, reader); err != nil {
return "", fmt.Errorf("unzstd file: %w", err)
}
return targetPath, nil
}

View File

@@ -0,0 +1,40 @@
package compress
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestZstdRoundTrip(t *testing.T) {
dir := t.TempDir()
src := filepath.Join(dir, "data.txt")
content := []byte("hello zstd roundtrip 差异压缩测试 " + strings.Repeat("payload-", 2000))
if err := os.WriteFile(src, content, 0o644); err != nil {
t.Fatalf("write source: %v", err)
}
compressed, err := ZstdFile(src)
if err != nil {
t.Fatalf("ZstdFile: %v", err)
}
if !strings.HasSuffix(compressed, ".zst") {
t.Fatalf("expected .zst suffix, got %s", compressed)
}
// 删除原文件,确保后续读到的是解压结果而非残留原文件。
if err := os.Remove(src); err != nil {
t.Fatalf("remove source: %v", err)
}
out, err := UnzstdFile(compressed)
if err != nil {
t.Fatalf("UnzstdFile: %v", err)
}
got, err := os.ReadFile(out)
if err != nil {
t.Fatalf("read decompressed: %v", err)
}
if !bytes.Equal(got, content) {
t.Fatalf("roundtrip mismatch: got %d bytes, want %d bytes", len(got), len(content))
}
}