From d2e0fd2af0e90eb9b01816064bcd2fb59dfb362d Mon Sep 17 00:00:00 2001 From: eleksir Date: Sat, 18 Apr 2026 09:17:16 +0300 Subject: [PATCH] There was a bug where all files treated as hardlinks in resulting tar archive, now it was hopefully fixed --- internal/backer/archive.go | 21 +++++----- internal/backer/utils_test.go | 53 +++++++++++++++++++++++++ test_data/test1/hardlinks/hardlink1.txt | 1 + test_data/test1/hardlinks/hardlink2.txt | 1 + test_data/test1/hardlinks/original.txt | 1 + 5 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 test_data/test1/hardlinks/hardlink1.txt create mode 100644 test_data/test1/hardlinks/hardlink2.txt create mode 100644 test_data/test1/hardlinks/original.txt diff --git a/internal/backer/archive.go b/internal/backer/archive.go index 75aac24..fe71e16 100644 --- a/internal/backer/archive.go +++ b/internal/backer/archive.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "strings" + "syscall" "backer/internal/log" ) @@ -149,6 +150,7 @@ func writeFilesToTar(ctx context.Context, filepaths []string, tw *tar.Writer, cl // If linkTarget is set, ensure the header is marked as a link. if linkTarget != "" { header.Typeflag = tar.TypeLink + header.Linkname = linkTarget } header.Format = tar.FormatGNU @@ -164,8 +166,8 @@ func writeFilesToTar(ctx context.Context, filepaths []string, tw *tar.Writer, cl return } - // Only stream file contents for regular files. - if mode.IsRegular() { + // Only stream file contents for regular files that are not hard links. + if mode.IsRegular() && linkTarget == "" { f, err := os.Open(fpath) if err != nil { log.Warnf("Skipping %s: open failed: %v", fpath, err) @@ -195,15 +197,12 @@ func getInode(fi os.FileInfo) uint64 { return 0 } - // Use type assertion to get inode - works on Unix, returns 0 on Windows. - type inodeGetter interface { - Dev() uint64 - Ino() uint64 - } - - if ig, ok := fi.Sys().(inodeGetter); ok { - // Combine device and inode for uniqueness across file systems. - return (ig.Dev() << 32) | ig.Ino() + // Use reflection to get Dev and Ino fields from the syscall.Stat_t struct. + st := fi.Sys() + // Try type assertion for the struct with fields. + switch v := st.(type) { + case *syscall.Stat_t: + return (v.Dev << 32) | v.Ino } return 0 diff --git a/internal/backer/utils_test.go b/internal/backer/utils_test.go index e18f9f8..5ef5529 100644 --- a/internal/backer/utils_test.go +++ b/internal/backer/utils_test.go @@ -247,6 +247,59 @@ func TestCreateTarGzStreamHardLink(t *testing.T) { } } +// TestCreateTarGzStreamHardLinkFromTestData tests hard link handling with test data. +func TestCreateTarGzStreamHardLinkFromTestData(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Hard link detection not supported on Windows (inodes unavailable)") + } + + testDataPath := filepath.Join("../../test_data", "test1", "hardlinks") + filepaths, err := GetFilesFromDirectories(context.Background(), []string{testDataPath}) + if err != nil { + t.Fatalf("Failed to get files from directory: %v", err) + } + + ctx := context.Background() + reader := CreateTarGzStream(ctx, filepaths) + defer reader.Close() + + gzReader, err := gzip.NewReader(reader) + if err != nil { + t.Fatalf("Failed to create gzip reader: %v", err) + } + defer gzReader.Close() + + tarReader := tar.NewReader(gzReader) + + entries := 0 + linkCount := 0 + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("Failed to read tar entry: %v", err) + } + + entries++ + t.Logf("Entry %d: name=%s typeflag=%c linkname=%s", entries, header.Name, header.Typeflag, header.Linkname) + + if header.Typeflag == tar.TypeLink { + linkCount++ + } + } + + if entries != 4 { + t.Errorf("Expected 4 entries (dir + 1 original + 2 hardlinks), got %d", entries) + } + + if linkCount != 2 { + t.Errorf("Expected 2 hard link entries, got %d", linkCount) + } +} + // TestCreateTarGzStreamCancellation tests context cancellation. func TestCreateTarGzStreamCancellation(t *testing.T) { tmpDir := t.TempDir() diff --git a/test_data/test1/hardlinks/hardlink1.txt b/test_data/test1/hardlinks/hardlink1.txt new file mode 100644 index 0000000..508d3cf --- /dev/null +++ b/test_data/test1/hardlinks/hardlink1.txt @@ -0,0 +1 @@ +shared content for hardlinks diff --git a/test_data/test1/hardlinks/hardlink2.txt b/test_data/test1/hardlinks/hardlink2.txt new file mode 100644 index 0000000..508d3cf --- /dev/null +++ b/test_data/test1/hardlinks/hardlink2.txt @@ -0,0 +1 @@ +shared content for hardlinks diff --git a/test_data/test1/hardlinks/original.txt b/test_data/test1/hardlinks/original.txt new file mode 100644 index 0000000..508d3cf --- /dev/null +++ b/test_data/test1/hardlinks/original.txt @@ -0,0 +1 @@ +shared content for hardlinks