Implemented tar+parallel gzip compression + unit tests for it.

This commit is contained in:
eleksir
2026-03-28 00:19:09 +03:00
parent 3620623321
commit 15ac3be410
10 changed files with 204 additions and 26 deletions

View File

@@ -142,8 +142,8 @@ func LoadConfig(path string) error {
log.Warnf("Config option compression_algorithm is not set, fallback to %s", C.CompressionAlgorithm)
}
if C.CompressionAlgorithm != "gzip" && C.CompressionAlgorithm != "bzip2" && C.CompressionAlgorithm != "zstd" && C.CompressionAlgorithm != "lz4" && C.CompressionAlgorithm != "xz" {
return fmt.Errorf("Compression_algorithm must be gzip, bzip2, zstd, lz4 or xz, got %s", C.CompressionAlgorithm)
if C.CompressionAlgorithm != "gzip" && C.CompressionAlgorithm != "bzip2" && C.CompressionAlgorithm != "zstd" && C.CompressionAlgorithm != "lz4" && C.CompressionAlgorithm != "xz" && C.CompressionAlgorithm != "pgzip" {
return fmt.Errorf("Compression_algorithm must be gzip, bzip2, zstd, lz4, xz or pgzip, got %s", C.CompressionAlgorithm)
}
// Compile exclude patterns.

View File

@@ -316,6 +316,27 @@ func TestLoadConfigCompressionAlgorithmXz(t *testing.T) {
}
}
// TestLoadConfigCompressionAlgorithmPgzip tests pgzip compression algorithm.
func TestLoadConfigCompressionAlgorithmPgzip(t *testing.T) {
config := `{
"user": "test",
"password": "test",
"directories": ["../../test_data/test1/foo"],
"nohttps": true,
"compression_algorithm": "pgzip"
}`
tmpFile := writeTempConfig(t, config)
err := LoadConfig(tmpFile)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if C.CompressionAlgorithm != "pgzip" {
t.Errorf("C.CompressionAlgorithm should be pgzip, got %s", C.CompressionAlgorithm)
}
}
// TestLoadConfigCompressionAlgorithmInvalid tests invalid compression algorithm.
func TestLoadConfigCompressionAlgorithmInvalid(t *testing.T) {
config := `{

View File

@@ -95,6 +95,10 @@ func NewServer(configPath string) (*http.Server, error) {
archive = CreateTarXzStream(ctx, files)
extension = "tar.xz"
case "pgzip":
archive = CreateTarPgzipStream(ctx, files)
extension = "tar.gz"
default:
archive = CreateTarGzStream(ctx, files)
extension = "tar.gz"

View File

@@ -14,6 +14,7 @@ import (
"github.com/dsnet/compress/bzip2"
"github.com/klauspost/compress/zstd"
"github.com/klauspost/pgzip"
"github.com/pierrec/lz4"
"github.com/ulikunitz/xz"
)
@@ -730,6 +731,145 @@ func CreateTarXzStream(ctx context.Context, filepaths []string) io.ReadCloser {
return pr
}
// CreateTarPgzipStream takes a list of files and returns a reader that reads from a parallel gzip-compressed tar archive.
func CreateTarPgzipStream(ctx context.Context, filepaths []string) io.ReadCloser {
pr, pw := io.Pipe()
go func() {
var pipeErr error
closePipeWithError := func(err error) {
if pipeErr != nil {
return
}
pipeErr = err
pw.CloseWithError(err)
}
defer func() {
if pipeErr == nil {
pw.Close()
}
}()
gw, err := pgzip.NewWriterLevel(pw, C.CompressionLevel)
if err != nil {
closePipeWithError(fmt.Errorf("pgzip writer error: %w", err))
return
}
defer func() {
if pipeErr == nil {
if err := gw.Close(); err != nil {
if !isPipeClosedError(err) {
log.Errorf("Failed to close pgzip writer: %v", err)
}
}
}
}()
tw := tar.NewWriter(gw)
defer func() {
if pipeErr == nil {
if err := tw.Close(); err != nil {
if !isPipeClosedError(err) {
log.Errorf("Failed to close tar writer: %v", err)
}
}
}
}()
for _, fpath := range filepaths {
var (
err error
linkTarget string
)
select {
case <-ctx.Done():
closePipeWithError(ctx.Err())
return
default:
}
log.Debugf("Adding file: %s", fpath)
st, err := os.Lstat(fpath)
if err != nil {
log.Warnf("Skipping %s: %v", fpath, err)
continue
}
if st.Mode()&os.ModeSymlink != 0 {
linkTarget, err = os.Readlink(fpath)
if err != nil {
log.Warnf("Skipping %s: readlink failed: %v", fpath, err)
continue
}
}
if st.Mode()&os.ModeSocket != 0 {
log.Debugf("Skipping socket: %s", fpath)
continue
}
header, err := tar.FileInfoHeader(st, linkTarget)
if err != nil {
log.Warnf("Skipping %s: tar header failed: %v", fpath, err)
continue
}
header.Format = tar.FormatGNU
header.Name = strings.TrimPrefix(filepath.ToSlash(filepath.Clean(fpath)), "/")
mode := st.Mode()
if err := writeWithContext(ctx, func() error {
return tw.WriteHeader(header)
}); err != nil {
closePipeWithError(err)
return
}
if mode.IsRegular() {
f, err := os.Open(fpath)
if err != nil {
log.Warnf("Skipping %s: open failed: %v", fpath, err)
continue
}
err = copyWithContext(ctx, tw, f)
if e := f.Close(); e != nil {
log.Warnf("Failed to close file %s: %v", fpath, e)
}
if err != nil {
log.Warnf("Skipping %s: copy failed: %v", fpath, err)
continue
}
}
}
}()
return pr
}
// isPipeClosedError checks if the error is due to a closed pipe.
func isPipeClosedError(err error) bool {
if err == nil {