mirror of
https://github.com/octoleo/restic.git
synced 2024-12-22 10:58:55 +00:00
Merge pull request #4379 from chenxiaolong/symlink_xattrs
Add support for extended attributes on symlinks
This commit is contained in:
commit
4d43509423
8
changelog/unreleased/issue-4375
Normal file
8
changelog/unreleased/issue-4375
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Enhancement: Add support for extended attributes on symlinks
|
||||||
|
|
||||||
|
Restic now supports extended attributes on symlinks when backing up,
|
||||||
|
restoring, or FUSE-mounting snapshots. This includes, for example, the
|
||||||
|
`security.selinux` xattr on Linux distributions that use SELinux.
|
||||||
|
|
||||||
|
https://github.com/restic/restic/issues/4375
|
||||||
|
https://github.com/restic/restic/pull/4379
|
@ -222,19 +222,10 @@ func (d *dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dir) Listxattr(_ context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
|
func (d *dir) Listxattr(_ context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
|
||||||
debug.Log("Listxattr(%v, %v)", d.node.Name, req.Size)
|
nodeToXattrList(d.node, req, resp)
|
||||||
for _, attr := range d.node.ExtendedAttributes {
|
|
||||||
resp.Append(attr.Name)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dir) Getxattr(_ context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
|
func (d *dir) Getxattr(_ context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
|
||||||
debug.Log("Getxattr(%v, %v, %v)", d.node.Name, req.Name, req.Size)
|
return nodeGetXattr(d.node, req, resp)
|
||||||
attrval := d.node.GetExtendedAttribute(req.Name)
|
|
||||||
if attrval != nil {
|
|
||||||
resp.Xattr = attrval
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fuse.ErrNoXattr
|
|
||||||
}
|
}
|
||||||
|
@ -167,19 +167,10 @@ func (f *openFile) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *file) Listxattr(_ context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
|
func (f *file) Listxattr(_ context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
|
||||||
debug.Log("Listxattr(%v, %v)", f.node.Name, req.Size)
|
nodeToXattrList(f.node, req, resp)
|
||||||
for _, attr := range f.node.ExtendedAttributes {
|
|
||||||
resp.Append(attr.Name)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *file) Getxattr(_ context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
|
func (f *file) Getxattr(_ context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
|
||||||
debug.Log("Getxattr(%v, %v, %v)", f.node.Name, req.Name, req.Size)
|
return nodeGetXattr(f.node, req, resp)
|
||||||
attrval := f.node.GetExtendedAttribute(req.Name)
|
|
||||||
if attrval != nil {
|
|
||||||
resp.Xattr = attrval
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fuse.ErrNoXattr
|
|
||||||
}
|
}
|
||||||
|
@ -271,6 +271,31 @@ func TestInodeFromNode(t *testing.T) {
|
|||||||
rtest.Assert(t, inoA != inoAbb, "inode(a/b/b) = inode(a)")
|
rtest.Assert(t, inoA != inoAbb, "inode(a/b/b) = inode(a)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLink(t *testing.T) {
|
||||||
|
node := &restic.Node{Name: "foo.txt", Type: "symlink", Links: 1, LinkTarget: "dst", ExtendedAttributes: []restic.ExtendedAttribute{
|
||||||
|
{Name: "foo", Value: []byte("bar")},
|
||||||
|
}}
|
||||||
|
|
||||||
|
lnk, err := newLink(&Root{}, 42, node)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
target, err := lnk.Readlink(context.TODO(), nil)
|
||||||
|
rtest.OK(t, err)
|
||||||
|
rtest.Equals(t, node.LinkTarget, target)
|
||||||
|
|
||||||
|
exp := &fuse.ListxattrResponse{}
|
||||||
|
exp.Append("foo")
|
||||||
|
resp := &fuse.ListxattrResponse{}
|
||||||
|
rtest.OK(t, lnk.Listxattr(context.TODO(), &fuse.ListxattrRequest{}, resp))
|
||||||
|
rtest.Equals(t, exp.Xattr, resp.Xattr)
|
||||||
|
|
||||||
|
getResp := &fuse.GetxattrResponse{}
|
||||||
|
rtest.OK(t, lnk.Getxattr(context.TODO(), &fuse.GetxattrRequest{Name: "foo"}, getResp))
|
||||||
|
rtest.Equals(t, node.ExtendedAttributes[0].Value, getResp.Xattr)
|
||||||
|
|
||||||
|
err = lnk.Getxattr(context.TODO(), &fuse.GetxattrRequest{Name: "invalid"}, nil)
|
||||||
|
rtest.Assert(t, err != nil, "missing error on reading invalid xattr")
|
||||||
|
}
|
||||||
|
|
||||||
var sink uint64
|
var sink uint64
|
||||||
|
|
||||||
func BenchmarkInode(b *testing.B) {
|
func BenchmarkInode(b *testing.B) {
|
||||||
|
@ -46,3 +46,12 @@ func (l *link) Attr(_ context.Context, a *fuse.Attr) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *link) Listxattr(_ context.Context, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) error {
|
||||||
|
nodeToXattrList(l.node, req, resp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *link) Getxattr(_ context.Context, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
|
||||||
|
return nodeGetXattr(l.node, req, resp)
|
||||||
|
}
|
||||||
|
24
internal/fuse/xattr.go
Normal file
24
internal/fuse/xattr.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package fuse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anacrolix/fuse"
|
||||||
|
"github.com/restic/restic/internal/debug"
|
||||||
|
"github.com/restic/restic/internal/restic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func nodeToXattrList(node *restic.Node, req *fuse.ListxattrRequest, resp *fuse.ListxattrResponse) {
|
||||||
|
debug.Log("Listxattr(%v, %v)", node.Name, req.Size)
|
||||||
|
for _, attr := range node.ExtendedAttributes {
|
||||||
|
resp.Append(attr.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodeGetXattr(node *restic.Node, req *fuse.GetxattrRequest, resp *fuse.GetxattrResponse) error {
|
||||||
|
debug.Log("Getxattr(%v, %v, %v)", node.Name, req.Name, req.Size)
|
||||||
|
attrval := node.GetExtendedAttribute(req.Name)
|
||||||
|
if attrval != nil {
|
||||||
|
resp.Xattr = attrval
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fuse.ErrNoXattr
|
||||||
|
}
|
@ -609,10 +609,6 @@ func (node *Node) fillExtra(path string, fi os.FileInfo) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (node *Node) fillExtendedAttributes(path string) error {
|
func (node *Node) fillExtendedAttributes(path string) error {
|
||||||
if node.Type == "symlink" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
xattrs, err := Listxattr(path)
|
xattrs, err := Listxattr(path)
|
||||||
debug.Log("fillExtendedAttributes(%v) %v %v", path, xattrs, err)
|
debug.Log("fillExtendedAttributes(%v) %v %v", path, xattrs, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -163,58 +164,99 @@ var nodeTests = []restic.Node{
|
|||||||
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "testXattrFile",
|
||||||
|
Type: "file",
|
||||||
|
Content: restic.IDs{},
|
||||||
|
UID: uint32(os.Getuid()),
|
||||||
|
GID: uint32(os.Getgid()),
|
||||||
|
Mode: 0604,
|
||||||
|
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
||||||
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
|
ExtendedAttributes: []restic.ExtendedAttribute{
|
||||||
|
{"user.foo", []byte("bar")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "testXattrDir",
|
||||||
|
Type: "dir",
|
||||||
|
Subtree: nil,
|
||||||
|
UID: uint32(os.Getuid()),
|
||||||
|
GID: uint32(os.Getgid()),
|
||||||
|
Mode: 0750 | os.ModeDir,
|
||||||
|
ModTime: parseTime("2005-05-14 21:07:03.111"),
|
||||||
|
AccessTime: parseTime("2005-05-14 21:07:04.222"),
|
||||||
|
ChangeTime: parseTime("2005-05-14 21:07:05.333"),
|
||||||
|
ExtendedAttributes: []restic.ExtendedAttribute{
|
||||||
|
{"user.foo", []byte("bar")},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNodeRestoreAt(t *testing.T) {
|
func TestNodeRestoreAt(t *testing.T) {
|
||||||
tempdir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-")
|
tempdir := t.TempDir()
|
||||||
rtest.OK(t, err)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if rtest.TestCleanupTempDirs {
|
|
||||||
rtest.RemoveAll(t, tempdir)
|
|
||||||
} else {
|
|
||||||
t.Logf("leaving tempdir at %v", tempdir)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for _, test := range nodeTests {
|
for _, test := range nodeTests {
|
||||||
nodePath := filepath.Join(tempdir, test.Name)
|
t.Run("", func(t *testing.T) {
|
||||||
rtest.OK(t, test.CreateAt(context.TODO(), nodePath, nil))
|
var nodePath string
|
||||||
rtest.OK(t, test.RestoreMetadata(nodePath))
|
if test.ExtendedAttributes != nil {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// restic does not support xattrs on windows
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if test.Type == "dir" {
|
// tempdir might be backed by a filesystem that does not support
|
||||||
rtest.OK(t, test.RestoreTimestamps(nodePath))
|
// extended attributes
|
||||||
}
|
nodePath = test.Name
|
||||||
|
defer func() {
|
||||||
|
_ = os.Remove(nodePath)
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
nodePath = filepath.Join(tempdir, test.Name)
|
||||||
|
}
|
||||||
|
rtest.OK(t, test.CreateAt(context.TODO(), nodePath, nil))
|
||||||
|
rtest.OK(t, test.RestoreMetadata(nodePath))
|
||||||
|
|
||||||
fi, err := os.Lstat(nodePath)
|
if test.Type == "dir" {
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, test.RestoreTimestamps(nodePath))
|
||||||
|
}
|
||||||
|
|
||||||
n2, err := restic.NodeFromFileInfo(nodePath, fi)
|
fi, err := os.Lstat(nodePath)
|
||||||
rtest.OK(t, err)
|
rtest.OK(t, err)
|
||||||
|
|
||||||
rtest.Assert(t, test.Name == n2.Name,
|
n2, err := restic.NodeFromFileInfo(nodePath, fi)
|
||||||
"%v: name doesn't match (%v != %v)", test.Type, test.Name, n2.Name)
|
rtest.OK(t, err)
|
||||||
rtest.Assert(t, test.Type == n2.Type,
|
|
||||||
"%v: type doesn't match (%v != %v)", test.Type, test.Type, n2.Type)
|
|
||||||
rtest.Assert(t, test.Size == n2.Size,
|
|
||||||
"%v: size doesn't match (%v != %v)", test.Size, test.Size, n2.Size)
|
|
||||||
|
|
||||||
if runtime.GOOS != "windows" {
|
rtest.Assert(t, test.Name == n2.Name,
|
||||||
rtest.Assert(t, test.UID == n2.UID,
|
"%v: name doesn't match (%v != %v)", test.Type, test.Name, n2.Name)
|
||||||
"%v: UID doesn't match (%v != %v)", test.Type, test.UID, n2.UID)
|
rtest.Assert(t, test.Type == n2.Type,
|
||||||
rtest.Assert(t, test.GID == n2.GID,
|
"%v: type doesn't match (%v != %v)", test.Type, test.Type, n2.Type)
|
||||||
"%v: GID doesn't match (%v != %v)", test.Type, test.GID, n2.GID)
|
rtest.Assert(t, test.Size == n2.Size,
|
||||||
if test.Type != "symlink" {
|
"%v: size doesn't match (%v != %v)", test.Size, test.Size, n2.Size)
|
||||||
// On OpenBSD only root can set sticky bit (see sticky(8)).
|
|
||||||
if runtime.GOOS != "openbsd" && runtime.GOOS != "netbsd" && runtime.GOOS != "solaris" && test.Name == "testSticky" {
|
if runtime.GOOS != "windows" {
|
||||||
rtest.Assert(t, test.Mode == n2.Mode,
|
rtest.Assert(t, test.UID == n2.UID,
|
||||||
"%v: mode doesn't match (0%o != 0%o)", test.Type, test.Mode, n2.Mode)
|
"%v: UID doesn't match (%v != %v)", test.Type, test.UID, n2.UID)
|
||||||
|
rtest.Assert(t, test.GID == n2.GID,
|
||||||
|
"%v: GID doesn't match (%v != %v)", test.Type, test.GID, n2.GID)
|
||||||
|
if test.Type != "symlink" {
|
||||||
|
// On OpenBSD only root can set sticky bit (see sticky(8)).
|
||||||
|
if runtime.GOOS != "openbsd" && runtime.GOOS != "netbsd" && runtime.GOOS != "solaris" && test.Name == "testSticky" {
|
||||||
|
rtest.Assert(t, test.Mode == n2.Mode,
|
||||||
|
"%v: mode doesn't match (0%o != 0%o)", test.Type, test.Mode, n2.Mode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
AssertFsTimeEqual(t, "AccessTime", test.Type, test.AccessTime, n2.AccessTime)
|
AssertFsTimeEqual(t, "AccessTime", test.Type, test.AccessTime, n2.AccessTime)
|
||||||
AssertFsTimeEqual(t, "ModTime", test.Type, test.ModTime, n2.ModTime)
|
AssertFsTimeEqual(t, "ModTime", test.Type, test.ModTime, n2.ModTime)
|
||||||
|
if len(n2.ExtendedAttributes) == 0 {
|
||||||
|
n2.ExtendedAttributes = nil
|
||||||
|
}
|
||||||
|
rtest.Assert(t, reflect.DeepEqual(test.ExtendedAttributes, n2.ExtendedAttributes),
|
||||||
|
"%v: xattrs don't match (%v != %v)", test.Name, test.ExtendedAttributes, n2.ExtendedAttributes)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,10 +5,13 @@ package restic
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
rtest "github.com/restic/restic/internal/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func stat(t testing.TB, filename string) (fi os.FileInfo, ok bool) {
|
func stat(t testing.TB, filename string) (fi os.FileInfo, ok bool) {
|
||||||
@ -25,6 +28,7 @@ func stat(t testing.TB, filename string) (fi os.FileInfo, ok bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkFile(t testing.TB, stat *syscall.Stat_t, node *Node) {
|
func checkFile(t testing.TB, stat *syscall.Stat_t, node *Node) {
|
||||||
|
t.Helper()
|
||||||
if uint32(node.Mode.Perm()) != uint32(stat.Mode&0777) {
|
if uint32(node.Mode.Perm()) != uint32(stat.Mode&0777) {
|
||||||
t.Errorf("Mode does not match, want %v, got %v", stat.Mode&0777, node.Mode)
|
t.Errorf("Mode does not match, want %v, got %v", stat.Mode&0777, node.Mode)
|
||||||
}
|
}
|
||||||
@ -37,7 +41,7 @@ func checkFile(t testing.TB, stat *syscall.Stat_t, node *Node) {
|
|||||||
t.Errorf("Dev does not match, want %v, got %v", stat.Dev, node.DeviceID)
|
t.Errorf("Dev does not match, want %v, got %v", stat.Dev, node.DeviceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Size != uint64(stat.Size) {
|
if node.Size != uint64(stat.Size) && node.Type != "symlink" {
|
||||||
t.Errorf("Size does not match, want %v, got %v", stat.Size, node.Size)
|
t.Errorf("Size does not match, want %v, got %v", stat.Size, node.Size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +87,10 @@ func checkDevice(t testing.TB, stat *syscall.Stat_t, node *Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNodeFromFileInfo(t *testing.T) {
|
func TestNodeFromFileInfo(t *testing.T) {
|
||||||
|
tmp := t.TempDir()
|
||||||
|
symlink := filepath.Join(tmp, "symlink")
|
||||||
|
rtest.OK(t, os.Symlink("target", symlink))
|
||||||
|
|
||||||
type Test struct {
|
type Test struct {
|
||||||
filename string
|
filename string
|
||||||
canSkip bool
|
canSkip bool
|
||||||
@ -90,6 +98,7 @@ func TestNodeFromFileInfo(t *testing.T) {
|
|||||||
var tests = []Test{
|
var tests = []Test{
|
||||||
{"node_test.go", false},
|
{"node_test.go", false},
|
||||||
{"/dev/sda", true},
|
{"/dev/sda", true},
|
||||||
|
{symlink, false},
|
||||||
}
|
}
|
||||||
|
|
||||||
// on darwin, users are not permitted to list the extended attributes of
|
// on darwin, users are not permitted to list the extended attributes of
|
||||||
@ -125,7 +134,7 @@ func TestNodeFromFileInfo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch node.Type {
|
switch node.Type {
|
||||||
case "file":
|
case "file", "symlink":
|
||||||
checkFile(t, s, node)
|
checkFile(t, s, node)
|
||||||
case "dev", "chardev":
|
case "dev", "chardev":
|
||||||
checkFile(t, s, node)
|
checkFile(t, s, node)
|
||||||
|
@ -13,20 +13,20 @@ import (
|
|||||||
|
|
||||||
// Getxattr retrieves extended attribute data associated with path.
|
// Getxattr retrieves extended attribute data associated with path.
|
||||||
func Getxattr(path, name string) ([]byte, error) {
|
func Getxattr(path, name string) ([]byte, error) {
|
||||||
b, err := xattr.Get(path, name)
|
b, err := xattr.LGet(path, name)
|
||||||
return b, handleXattrErr(err)
|
return b, handleXattrErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listxattr retrieves a list of names of extended attributes associated with the
|
// Listxattr retrieves a list of names of extended attributes associated with the
|
||||||
// given path in the file system.
|
// given path in the file system.
|
||||||
func Listxattr(path string) ([]string, error) {
|
func Listxattr(path string) ([]string, error) {
|
||||||
l, err := xattr.List(path)
|
l, err := xattr.LList(path)
|
||||||
return l, handleXattrErr(err)
|
return l, handleXattrErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setxattr associates name and data together as an attribute of path.
|
// Setxattr associates name and data together as an attribute of path.
|
||||||
func Setxattr(path, name string, data []byte) error {
|
func Setxattr(path, name string, data []byte) error {
|
||||||
return handleXattrErr(xattr.Set(path, name, data))
|
return handleXattrErr(xattr.LSet(path, name, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleXattrErr(err error) error {
|
func handleXattrErr(err error) error {
|
||||||
|
Loading…
Reference in New Issue
Block a user