mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-06-15 22:59:35 +08:00
feat(backup): 新增 zstd 压缩选项 (#89)
备份压缩在 gzip 之外新增 zstd(更高压缩率、更快解压)。pkg/compress 新增 ZstdFile/UnzstdFile,Master 与 Agent 压缩/解压按后缀分流,任务校验与前端下拉同步;往返单测覆盖。
This commit is contained in:
65
server/pkg/compress/zstd.go
Normal file
65
server/pkg/compress/zstd.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package compress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
)
|
||||
|
||||
// ZstdFile 将文件压缩为 .zst(zstd),返回压缩产物路径。
|
||||
// 相比 gzip,zstd 在相近 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
|
||||
}
|
||||
40
server/pkg/compress/zstd_test.go
Normal file
40
server/pkg/compress/zstd_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user