There was a bug where all files treated as hardlinks in resulting tar archive, now it was hopefully fixed

This commit is contained in:
eleksir
2026-04-18 09:17:16 +03:00
parent 51897b20bd
commit d2e0fd2af0
5 changed files with 66 additions and 11 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -0,0 +1 @@
shared content for hardlinks

View File

@@ -0,0 +1 @@
shared content for hardlinks

View File

@@ -0,0 +1 @@
shared content for hardlinks