diff --git a/changelog/unreleased/pull-21784 b/changelog/unreleased/pull-21784 new file mode 100644 index 000000000..012505234 --- /dev/null +++ b/changelog/unreleased/pull-21784 @@ -0,0 +1,9 @@ +Bugfix: Support exporting a `restic mount` of a Windows system via Samba + +A repository mounted using `restic mount` on a POSIX system, could not use +Samba to export files from restic backups of Windows systems. Backups of +other systems were not affected. This has been fixed. + +https://github.com/restic/restic/pull/21784 +https://github.com/restic/restic/issues/2034 +https://github.com/restic/restic/issues/4382 diff --git a/internal/fuse/file.go b/internal/fuse/file.go index 42a83d652..6fd042f81 100644 --- a/internal/fuse/file.go +++ b/internal/fuse/file.go @@ -55,7 +55,10 @@ func (f *file) Attr(_ context.Context, a *fuse.Attr) error { a.Size = f.node.Size a.Blocks = (f.node.Size + blockSize - 1) / blockSize a.BlockSize = blockSize - a.Nlink = uint32(f.node.Links) + // Windows (and other non-POSIX) backups may store a link count of 0. + // FUSE must still report a positive nlink so tools that validate stat() + // (e.g. Samba) accept the file. + a.Nlink = max(uint32(1), uint32(f.node.Links)) if !f.root.cfg.OwnerIsRoot { a.Uid = f.node.UID diff --git a/internal/fuse/fuse_test.go b/internal/fuse/fuse_test.go index c82252458..05dc5f91e 100644 --- a/internal/fuse/fuse_test.go +++ b/internal/fuse/fuse_test.go @@ -5,6 +5,7 @@ package fuse import ( "bytes" "context" + "fmt" "math/rand" "os" "strings" @@ -277,6 +278,28 @@ func TestBlocks(t *testing.T) { } } +// Windows (and other non-POSIX) backups may store a link count of 0; FUSE +// must still report a positive nlink so tools that validate stat() (e.g. +// Samba) accept the file. +func TestFileAttrNlink(t *testing.T) { + root := &Root{} + for _, tc := range []struct { + links uint64 + want uint32 + }{ + {0, 1}, + {1, 1}, + {42, 42}, + } { + t.Run(fmt.Sprintf("links_%d", tc.links), func(t *testing.T) { + f := &file{root: root, node: &data.Node{Links: tc.links}} + var a fuse.Attr + rtest.OK(t, f.Attr(context.TODO(), &a)) + rtest.Equals(t, tc.want, a.Nlink) + }) + } +} + func TestInodeFromNode(t *testing.T) { node := &data.Node{Name: "foo.txt", Type: data.NodeTypeCharDev, Links: 2} ino1 := inodeFromNode(1, node)