From 2fdf0b805d2fb49e7531a0fbc49c068f3e311801 Mon Sep 17 00:00:00 2001 From: Catherine Date: Sat, 28 Mar 2026 17:01:23 +0000 Subject: [PATCH] Add hardlink support for tar archive upload. "Why the fuck would anybody want that", you could reasonably ask. Well, most wouldn't want this. However, if you wanted to use git-pages to deduplicate your backups, you might find it that some backups include hardlinks. "Why the fuck would anybody put their backups in git-pages", you could even more reasonably ask. Well, almost nobody would! However, tarsnap doesn't let you download deduplicated data (even though it deduplicates data in storage), restic can't ingest tarballs, I didn't have a partition I could format for btrfs, and git-pages performed much better than alternatives like juicefs. In the end this is correct and not expensive to do, just very niche. --- src/extract.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/extract.go b/src/extract.go index 5fbb1e3..2cf20d6 100644 --- a/src/extract.go +++ b/src/extract.go @@ -87,6 +87,7 @@ func ExtractTar(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* index := IndexManifestByGitHash(oldManifest) missing := []string{} manifest := NewManifest() + hardLinks := map[string]*Entry{} for { header, err := archive.Next() if err == io.EOF { @@ -107,11 +108,13 @@ func ExtractTar(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* if err != nil { return nil, fmt.Errorf("tar: %s: %w", fileName, err) } - AddFile(manifest, fileName, fileData) + entry := AddFile(manifest, fileName, fileData) + hardLinks[header.Name] = entry dataBytesTransferred += int64(len(fileData)) case tar.TypeSymlink: entry := addSymlinkOrBlobReference( manifest, fileName, header.Linkname, index, &missing) + hardLinks[header.Name] = entry switch { case entry == nil: // unresolved blob reference @@ -120,6 +123,12 @@ func ExtractTar(ctx context.Context, reader io.Reader, oldManifest *Manifest) (* default: dataBytesTransferred += int64(len(header.Linkname)) // actual symlink } + case tar.TypeLink: + if entry, found := hardLinks[header.Linkname]; found { + manifest.Contents[fileName] = entry + } else { + AddProblem(manifest, fileName, "tar: invalid hardlink %q", header.Linkname) + } case tar.TypeDir: AddDirectory(manifest, fileName) default: