This adds support for syncing extended attributes on supported filesystem on Linux, macOS, FreeBSD and NetBSD. Windows is currently excluded because the APIs seem onerous and annoying and frankly the uses cases seem few and far between. On Unixes this also covers ACLs as those are stored as extended attributes. Similar to ownership syncing this will optional & opt-in, which two settings controlling the main behavior: one to "sync" xattrs (read & write) and another one to "scan" xattrs (only read them so other devices can "sync" them, but not apply any locally). Co-authored-by: Tomasz Wilczyński <twilczynski@naver.com>
This commit is contained in:
co-authored by
Tomasz Wilczyński
parent
8065cf7e97
commit
6cac308bcd
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) 2022 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:build darwin || freebsd || netbsd
|
||||
// +build darwin freebsd netbsd
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (fi basicFileInfo) InodeChangeTime() time.Time {
|
||||
if sys, ok := fi.FileInfo.Sys().(*syscall.Stat_t); ok {
|
||||
return time.Unix(0, sys.Ctimespec.Nano())
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) 2022 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:build aix || dragonfly || linux || openbsd || solaris || illumos
|
||||
// +build aix dragonfly linux openbsd solaris illumos
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (fi basicFileInfo) InodeChangeTime() time.Time {
|
||||
if sys, ok := fi.FileInfo.Sys().(*syscall.Stat_t); ok {
|
||||
return time.Unix(0, sys.Ctim.Nano())
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var execExts map[string]bool
|
||||
@@ -57,6 +58,10 @@ func (e basicFileInfo) Group() int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (basicFileInfo) InodeChangeTime() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// osFileInfo converts e to os.FileInfo that is suitable
|
||||
// to be passed to os.SameFile.
|
||||
func (e *basicFileInfo) osFileInfo() os.FileInfo {
|
||||
|
||||
@@ -13,6 +13,6 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func (f *BasicFilesystem) PlatformData(name string) (protocol.PlatformData, error) {
|
||||
return unixPlatformData(f, name, f.userCache, f.groupCache)
|
||||
func (f *BasicFilesystem) PlatformData(name string, scanOwnership, scanXattrs bool, xattrFilter XattrFilter) (protocol.PlatformData, error) {
|
||||
return unixPlatformData(f, name, f.userCache, f.groupCache, scanOwnership, scanXattrs, xattrFilter)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func (f *BasicFilesystem) PlatformData(name string) (protocol.PlatformData, error) {
|
||||
func (f *BasicFilesystem) PlatformData(name string, scanOwnership, _ bool, _ XattrFilter) (protocol.PlatformData, error) {
|
||||
if !scanOwnership {
|
||||
// That's the only thing we do, currently
|
||||
return protocol.PlatformData{}, nil
|
||||
}
|
||||
|
||||
rootedName, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return protocol.PlatformData{}, fmt.Errorf("rooted for %s: %w", name, err)
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -16,6 +19,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
)
|
||||
|
||||
@@ -565,6 +569,87 @@ func TestRel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestXattr(t *testing.T) {
|
||||
tfs, _ := setup(t)
|
||||
if err := tfs.Mkdir("/test", 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
xattrSize := func() int { return 20 + rand.Intn(20) }
|
||||
|
||||
// Create a set of random attributes that we will set and read back
|
||||
var attrs []protocol.Xattr
|
||||
for i := 0; i < 10; i++ {
|
||||
key := fmt.Sprintf("user.test-%d", i)
|
||||
value := make([]byte, xattrSize())
|
||||
rand.Read(value)
|
||||
attrs = append(attrs, protocol.Xattr{
|
||||
Name: key,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
|
||||
// Set the xattrs, read them back and compare
|
||||
if err := tfs.SetXattr("/test", attrs, noopXattrFilter{}); errors.Is(err, ErrXattrsNotSupported) {
|
||||
t.Skip("xattrs not supported")
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, err := tfs.GetXattr("/test", noopXattrFilter{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res) != len(attrs) {
|
||||
t.Fatalf("length of returned xattrs does not match (%d != %d)", len(res), len(attrs))
|
||||
}
|
||||
for i, xa := range res {
|
||||
if xa.Name != attrs[i].Name {
|
||||
t.Errorf("xattr name %q != %q", xa.Name, attrs[i].Name)
|
||||
}
|
||||
if !bytes.Equal(xa.Value, attrs[i].Value) {
|
||||
t.Errorf("xattr value %q != %q", xa.Value, attrs[i].Value)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a couple, change a couple, and add another couple of
|
||||
// attributes. Replacing the xattrs again should work.
|
||||
attrs = attrs[2:]
|
||||
attrs[1].Value = make([]byte, xattrSize())
|
||||
rand.Read(attrs[1].Value)
|
||||
attrs[3].Value = make([]byte, xattrSize())
|
||||
rand.Read(attrs[3].Value)
|
||||
for i := 10; i < 12; i++ {
|
||||
key := fmt.Sprintf("user.test-%d", i)
|
||||
value := make([]byte, xattrSize())
|
||||
rand.Read(value)
|
||||
attrs = append(attrs, protocol.Xattr{
|
||||
Name: key,
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
sort.Slice(attrs, func(i, j int) bool { return attrs[i].Name < attrs[j].Name })
|
||||
|
||||
// Set the xattrs, read them back and compare
|
||||
if err := tfs.SetXattr("/test", attrs, noopXattrFilter{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, err = tfs.GetXattr("/test", noopXattrFilter{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res) != len(attrs) {
|
||||
t.Fatalf("length of returned xattrs does not match (%d != %d)", len(res), len(attrs))
|
||||
}
|
||||
for i, xa := range res {
|
||||
if xa.Name != attrs[i].Name {
|
||||
t.Errorf("xattr name %q != %q", xa.Name, attrs[i].Name)
|
||||
}
|
||||
if !bytes.Equal(xa.Value, attrs[i].Value) {
|
||||
t.Errorf("xattr value %q != %q", xa.Value, attrs[i].Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBasicWalkSkipSymlink(t *testing.T) {
|
||||
_, dir := setup(t)
|
||||
testWalkSkipSymlink(t, FilesystemTypeBasic, dir)
|
||||
@@ -579,3 +664,9 @@ func TestWalkInfiniteRecursion(t *testing.T) {
|
||||
_, dir := setup(t)
|
||||
testWalkInfiniteRecursion(t, FilesystemTypeBasic, dir)
|
||||
}
|
||||
|
||||
type noopXattrFilter struct{}
|
||||
|
||||
func (noopXattrFilter) Permit(string) bool { return true }
|
||||
func (noopXattrFilter) GetMaxSingleEntrySize() int { return 0 }
|
||||
func (noopXattrFilter) GetMaxTotalSize() int { return 0 }
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright (C) 2022 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:build freebsd || netbsd
|
||||
// +build freebsd netbsd
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var (
|
||||
namespaces = [...]int{unix.EXTATTR_NAMESPACE_USER, unix.EXTATTR_NAMESPACE_SYSTEM}
|
||||
namespacePrefixes = [...]string{unix.EXTATTR_NAMESPACE_USER: "user.", unix.EXTATTR_NAMESPACE_SYSTEM: "system."}
|
||||
)
|
||||
|
||||
func listXattr(path string) ([]string, error) {
|
||||
var attrs []string
|
||||
|
||||
// List the two namespaces explicitly and prefix any results with the
|
||||
// namespace name.
|
||||
for _, nsid := range namespaces {
|
||||
buf := make([]byte, 1024)
|
||||
size, err := unixLlistxattr(path, buf, nsid)
|
||||
if errors.Is(err, unix.ERANGE) || size == len(buf) {
|
||||
// Buffer is too small. Try again with a zero sized buffer to
|
||||
// get the size, then allocate a buffer of the correct size. We
|
||||
// inlude the size == len(buf) because apparently macOS doesn't
|
||||
// return ERANGE as it should -- no harm done, just an extra
|
||||
// read if we happened to need precisely 1024 bytes on the first
|
||||
// pass.
|
||||
size, err = unixLlistxattr(path, nil, nsid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Listxattr %s: %w", path, err)
|
||||
}
|
||||
buf = make([]byte, size)
|
||||
size, err = unixLlistxattr(path, buf, nsid)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Listxattr %s: %w", path, err)
|
||||
}
|
||||
|
||||
buf = buf[:size]
|
||||
|
||||
// "Each list entry consists of a single byte containing the length
|
||||
// of the attribute name, followed by the attribute name. The
|
||||
// attribute name is not terminated by ASCII 0 (nul)."
|
||||
i := 0
|
||||
for i < len(buf) {
|
||||
l := int(buf[i])
|
||||
i++
|
||||
if i+l > len(buf) {
|
||||
// uh-oh
|
||||
return nil, fmt.Errorf("get xattr %s: attribute length %d at offset %d exceeds buffer length %d", path, l, i, len(buf))
|
||||
}
|
||||
if l > 0 {
|
||||
attrs = append(attrs, namespacePrefixes[nsid]+string(buf[i:i+l]))
|
||||
i += l
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(attrs)
|
||||
return attrs, nil
|
||||
}
|
||||
|
||||
// This is unix.Llistxattr except taking a namespace parameter to dodge
|
||||
// https://github.com/golang/go/issues/54357 ("Listxattr on FreeBSD loses
|
||||
// namespace info")
|
||||
func unixLlistxattr(link string, dest []byte, nsid int) (sz int, err error) {
|
||||
d := initxattrdest(dest, 0)
|
||||
destsiz := len(dest)
|
||||
|
||||
s, e := unix.ExtattrListLink(link, nsid, uintptr(d), destsiz)
|
||||
if e != nil && e == unix.EPERM && nsid != unix.EXTATTR_NAMESPACE_USER {
|
||||
return 0, nil
|
||||
} else if e != nil {
|
||||
return s, e
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var _zero uintptr
|
||||
|
||||
func initxattrdest(dest []byte, idx int) (d unsafe.Pointer) {
|
||||
if len(dest) > idx {
|
||||
return unsafe.Pointer(&dest[idx])
|
||||
} else {
|
||||
return unsafe.Pointer(_zero)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2022 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:build linux || darwin
|
||||
// +build linux darwin
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func listXattr(path string) ([]string, error) {
|
||||
buf := make([]byte, 1024)
|
||||
size, err := unix.Llistxattr(path, buf)
|
||||
if errors.Is(err, unix.ERANGE) {
|
||||
// Buffer is too small. Try again with a zero sized buffer to get
|
||||
// the size, then allocate a buffer of the correct size.
|
||||
size, err = unix.Llistxattr(path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Listxattr %s: %w", path, err)
|
||||
}
|
||||
buf = make([]byte, size)
|
||||
size, err = unix.Llistxattr(path, buf)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Listxattr %s: %w", path, err)
|
||||
}
|
||||
|
||||
buf = buf[:size]
|
||||
attrs := compact(strings.Split(string(buf), "\x00"))
|
||||
|
||||
sort.Strings(attrs)
|
||||
return attrs, nil
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright (C) 2022 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:build !windows && !dragonfly && !illumos && !solaris && !openbsd
|
||||
// +build !windows,!dragonfly,!illumos,!solaris,!openbsd
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (f *BasicFilesystem) GetXattr(path string, xattrFilter XattrFilter) ([]protocol.Xattr, error) {
|
||||
path, err := f.rooted(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get xattr %s: %w", path, err)
|
||||
}
|
||||
|
||||
attrs, err := listXattr(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get xattr %s: %w", path, err)
|
||||
}
|
||||
|
||||
res := make([]protocol.Xattr, 0, len(attrs))
|
||||
var val, buf []byte
|
||||
var totSize int
|
||||
for _, attr := range attrs {
|
||||
if !xattrFilter.Permit(attr) {
|
||||
l.Debugf("get xattr %s: skipping attribute %q denied by filter", path, attr)
|
||||
continue
|
||||
}
|
||||
val, buf, err = getXattr(path, attr, buf)
|
||||
var errNo syscall.Errno
|
||||
if errors.As(err, &errNo) && errNo == 0x5d {
|
||||
// ENOATTR, returned on BSD when asking for an attribute that
|
||||
// doesn't exist (any more?)
|
||||
continue
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("get xattr %s: %w", path, err)
|
||||
}
|
||||
if max := xattrFilter.GetMaxSingleEntrySize(); max > 0 && len(attr)+len(val) > max {
|
||||
l.Debugf("get xattr %s: attribute %q exceeds max size", path, attr)
|
||||
continue
|
||||
}
|
||||
totSize += len(attr) + len(val)
|
||||
if max := xattrFilter.GetMaxTotalSize(); max > 0 && totSize > max {
|
||||
l.Debugf("get xattr %s: attribute %q would cause max size to be exceeded", path, attr)
|
||||
continue
|
||||
}
|
||||
res = append(res, protocol.Xattr{
|
||||
Name: attr,
|
||||
Value: val,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func getXattr(path, name string, buf []byte) (val []byte, rest []byte, err error) {
|
||||
if len(buf) == 0 {
|
||||
buf = make([]byte, 1024)
|
||||
}
|
||||
size, err := unix.Lgetxattr(path, name, buf)
|
||||
if errors.Is(err, unix.ERANGE) {
|
||||
// Buffer was too small. Figure out how large it needs to be, and
|
||||
// allocate.
|
||||
size, err = unix.Lgetxattr(path, name, nil)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Lgetxattr %s %q: %w", path, name, err)
|
||||
}
|
||||
if size > len(buf) {
|
||||
buf = make([]byte, size)
|
||||
}
|
||||
size, err = unix.Lgetxattr(path, name, buf)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, buf, fmt.Errorf("Lgetxattr %s %q: %w", path, name, err)
|
||||
}
|
||||
return buf[:size], buf[size:], nil
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) SetXattr(path string, xattrs []protocol.Xattr, xattrFilter XattrFilter) error {
|
||||
// Index the new attribute set.
|
||||
xattrsIdx := make(map[string]int)
|
||||
for i, xa := range xattrs {
|
||||
xattrsIdx[xa.Name] = i
|
||||
}
|
||||
|
||||
// Get and index the existing attribute set
|
||||
current, err := f.GetXattr(path, xattrFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set xattrs %s: GetXattr: %w", path, err)
|
||||
}
|
||||
currentIdx := make(map[string]int)
|
||||
for i, xa := range current {
|
||||
currentIdx[xa.Name] = i
|
||||
}
|
||||
|
||||
path, err = f.rooted(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set xattrs %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Remove all existing xattrs that are not in the new set
|
||||
for _, xa := range current {
|
||||
if _, ok := xattrsIdx[xa.Name]; !ok {
|
||||
if err := unix.Lremovexattr(path, xa.Name); err != nil {
|
||||
return fmt.Errorf("set xattrs %s: Removexattr %q: %w", path, xa.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set all xattrs that are different in the new set
|
||||
for _, xa := range xattrs {
|
||||
if old, ok := currentIdx[xa.Name]; ok && bytes.Equal(xa.Value, current[old].Value) {
|
||||
continue
|
||||
}
|
||||
if err := unix.Lsetxattr(path, xa.Name, xa.Value, 0); err != nil {
|
||||
return fmt.Errorf("set xattrs %s: Setxattr %q: %w", path, xa.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func compact(ss []string) []string {
|
||||
i := 0
|
||||
for _, s := range ss {
|
||||
if s != "" {
|
||||
ss[i] = s
|
||||
i++
|
||||
}
|
||||
}
|
||||
return ss[:i]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) 2022 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:build windows || dragonfly || illumos || solaris || openbsd
|
||||
// +build windows dragonfly illumos solaris openbsd
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func (f *BasicFilesystem) GetXattr(path string, xattrFilter XattrFilter) ([]protocol.Xattr, error) {
|
||||
return nil, ErrXattrsNotSupported
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) SetXattr(path string, xattrs []protocol.Xattr, xattrFilter XattrFilter) error {
|
||||
return ErrXattrsNotSupported
|
||||
}
|
||||
+10
-4
@@ -24,9 +24,15 @@ func (fs *errorFilesystem) Lchown(_, _, _ string) error { return fs.err }
|
||||
func (fs *errorFilesystem) Chtimes(_ string, _ time.Time, _ time.Time) error {
|
||||
return fs.err
|
||||
}
|
||||
func (fs *errorFilesystem) Create(_ string) (File, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) CreateSymlink(_, _ string) error { return fs.err }
|
||||
func (fs *errorFilesystem) DirNames(_ string) ([]string, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) Create(_ string) (File, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) CreateSymlink(_, _ string) error { return fs.err }
|
||||
func (fs *errorFilesystem) DirNames(_ string) ([]string, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) GetXattr(_ string, _ XattrFilter) ([]protocol.Xattr, error) {
|
||||
return nil, fs.err
|
||||
}
|
||||
func (fs *errorFilesystem) SetXattr(_ string, _ []protocol.Xattr, _ XattrFilter) error {
|
||||
return fs.err
|
||||
}
|
||||
func (fs *errorFilesystem) Lstat(_ string) (FileInfo, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) Mkdir(_ string, _ FileMode) error { return fs.err }
|
||||
func (fs *errorFilesystem) MkdirAll(_ string, _ FileMode) error { return fs.err }
|
||||
@@ -54,7 +60,7 @@ func (*errorFilesystem) SameFile(_, _ FileInfo) bool { return false }
|
||||
func (fs *errorFilesystem) Watch(_ string, _ Matcher, _ context.Context, _ bool) (<-chan Event, <-chan error, error) {
|
||||
return nil, nil, fs.err
|
||||
}
|
||||
func (fs *errorFilesystem) PlatformData(_ string) (protocol.PlatformData, error) {
|
||||
func (fs *errorFilesystem) PlatformData(_ string, _, _ bool, _ XattrFilter) (protocol.PlatformData, error) {
|
||||
return protocol.PlatformData{}, fs.err
|
||||
}
|
||||
|
||||
|
||||
+18
-2
@@ -622,6 +622,14 @@ func (*fakeFS) Unhide(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*fakeFS) GetXattr(_ string, _ XattrFilter) ([]protocol.Xattr, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (*fakeFS) SetXattr(_ string, _ []protocol.Xattr, _ XattrFilter) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*fakeFS) Glob(_ string) ([]string, error) {
|
||||
// gnnh we don't seem to actually require this in practice
|
||||
return nil, errors.New("not implemented")
|
||||
@@ -662,8 +670,8 @@ func (fs *fakeFS) SameFile(fi1, fi2 FileInfo) bool {
|
||||
return ok && fi1.ModTime().Equal(fi2.ModTime()) && fi1.Mode() == fi2.Mode() && fi1.IsDir() == fi2.IsDir() && fi1.IsRegular() == fi2.IsRegular() && fi1.IsSymlink() == fi2.IsSymlink() && fi1.Owner() == fi2.Owner() && fi1.Group() == fi2.Group()
|
||||
}
|
||||
|
||||
func (fs *fakeFS) PlatformData(name string) (protocol.PlatformData, error) {
|
||||
return unixPlatformData(fs, name, fs.userCache, fs.groupCache)
|
||||
func (fs *fakeFS) PlatformData(name string, scanOwnership, scanXattrs bool, xattrFilter XattrFilter) (protocol.PlatformData, error) {
|
||||
return unixPlatformData(fs, name, fs.userCache, fs.groupCache, scanOwnership, scanXattrs, xattrFilter)
|
||||
}
|
||||
|
||||
func (*fakeFS) underlying() (Filesystem, bool) {
|
||||
@@ -961,3 +969,11 @@ func (f *fakeFileInfo) Owner() int {
|
||||
func (f *fakeFileInfo) Group() int {
|
||||
return f.gid
|
||||
}
|
||||
|
||||
func (*fakeFileInfo) Sys() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*fakeFileInfo) InodeChangeTime() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
+15
-2
@@ -29,6 +29,12 @@ const (
|
||||
filesystemWrapperTypeLog
|
||||
)
|
||||
|
||||
type XattrFilter interface {
|
||||
Permit(string) bool
|
||||
GetMaxSingleEntrySize() int
|
||||
GetMaxTotalSize() int
|
||||
}
|
||||
|
||||
// The Filesystem interface abstracts access to the file system.
|
||||
type Filesystem interface {
|
||||
Chmod(name string, mode FileMode) error
|
||||
@@ -62,7 +68,9 @@ type Filesystem interface {
|
||||
URI() string
|
||||
Options() []Option
|
||||
SameFile(fi1, fi2 FileInfo) bool
|
||||
PlatformData(name string) (protocol.PlatformData, error)
|
||||
PlatformData(name string, withOwnership, withXattrs bool, xattrFilter XattrFilter) (protocol.PlatformData, error)
|
||||
GetXattr(name string, xattrFilter XattrFilter) ([]protocol.Xattr, error)
|
||||
SetXattr(path string, xattrs []protocol.Xattr, xattrFilter XattrFilter) error
|
||||
|
||||
// Used for unwrapping things
|
||||
underlying() (Filesystem, bool)
|
||||
@@ -94,11 +102,13 @@ type FileInfo interface {
|
||||
Size() int64
|
||||
ModTime() time.Time
|
||||
IsDir() bool
|
||||
Sys() interface{}
|
||||
// Extensions
|
||||
IsRegular() bool
|
||||
IsSymlink() bool
|
||||
Owner() int
|
||||
Group() int
|
||||
InodeChangeTime() time.Time // may be zero if not supported
|
||||
}
|
||||
|
||||
// FileMode is similar to os.FileMode
|
||||
@@ -154,7 +164,10 @@ func (evType EventType) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
var ErrWatchNotSupported = errors.New("watching is not supported")
|
||||
var (
|
||||
ErrWatchNotSupported = errors.New("watching is not supported")
|
||||
ErrXattrsNotSupported = errors.New("extended attributes are not supported on this platform")
|
||||
)
|
||||
|
||||
// Equivalents from os package.
|
||||
|
||||
|
||||
+38
-32
@@ -17,42 +17,48 @@ import (
|
||||
// unixPlatformData is used on all platforms, because apart from being the
|
||||
// implementation for BasicFilesystem on Unixes it's also the implementation
|
||||
// in fakeFS.
|
||||
func unixPlatformData(fs Filesystem, name string, userCache *userCache, groupCache *groupCache) (protocol.PlatformData, error) {
|
||||
stat, err := fs.Lstat(name)
|
||||
if err != nil {
|
||||
return protocol.PlatformData{}, err
|
||||
func unixPlatformData(fs Filesystem, name string, userCache *userCache, groupCache *groupCache, scanOwnership, scanXattrs bool, xattrFilter XattrFilter) (protocol.PlatformData, error) {
|
||||
var pd protocol.PlatformData
|
||||
if scanOwnership {
|
||||
var ud protocol.UnixData
|
||||
|
||||
stat, err := fs.Lstat(name)
|
||||
if err != nil {
|
||||
return protocol.PlatformData{}, err
|
||||
}
|
||||
|
||||
ud.UID = stat.Owner()
|
||||
if user := userCache.lookup(strconv.Itoa(ud.UID)); user != nil {
|
||||
ud.OwnerName = user.Username
|
||||
} else if ud.UID == 0 {
|
||||
// We couldn't look up a name, but UID zero should be "root". This
|
||||
// fixup works around the (unlikely) situation where the ownership
|
||||
// is 0:0 but we can't look up a name for either uid zero or gid
|
||||
// zero. If that were the case we'd return a zero PlatformData which
|
||||
// wouldn't get serialized over the wire and the other side would
|
||||
// assume a lack of ownership info...
|
||||
ud.OwnerName = "root"
|
||||
}
|
||||
|
||||
ud.GID = stat.Group()
|
||||
if group := groupCache.lookup(strconv.Itoa(ud.GID)); group != nil {
|
||||
ud.GroupName = group.Name
|
||||
} else if ud.GID == 0 {
|
||||
ud.GroupName = "root"
|
||||
}
|
||||
|
||||
pd.Unix = &ud
|
||||
}
|
||||
|
||||
ownerUID := stat.Owner()
|
||||
ownerName := ""
|
||||
if user := userCache.lookup(strconv.Itoa(ownerUID)); user != nil {
|
||||
ownerName = user.Username
|
||||
} else if ownerUID == 0 {
|
||||
// We couldn't look up a name, but UID zero should be "root". This
|
||||
// fixup works around the (unlikely) situation where the ownership
|
||||
// is 0:0 but we can't look up a name for either uid zero or gid
|
||||
// zero. If that were the case we'd return a zero PlatformData which
|
||||
// wouldn't get serialized over the wire and the other side would
|
||||
// assume a lack of ownership info...
|
||||
ownerName = "root"
|
||||
if scanXattrs {
|
||||
xattrs, err := fs.GetXattr(name, xattrFilter)
|
||||
if err != nil {
|
||||
return protocol.PlatformData{}, err
|
||||
}
|
||||
pd.SetXattrs(xattrs)
|
||||
}
|
||||
|
||||
groupID := stat.Group()
|
||||
groupName := ""
|
||||
if group := groupCache.lookup(strconv.Itoa(ownerUID)); group != nil {
|
||||
groupName = group.Name
|
||||
} else if groupID == 0 {
|
||||
groupName = "root"
|
||||
}
|
||||
|
||||
return protocol.PlatformData{
|
||||
Unix: &protocol.UnixData{
|
||||
OwnerName: ownerName,
|
||||
GroupName: groupName,
|
||||
UID: ownerUID,
|
||||
GID: groupID,
|
||||
},
|
||||
}, nil
|
||||
return pd, nil
|
||||
}
|
||||
|
||||
type valueCache[K comparable, V any] struct {
|
||||
|
||||
Reference in New Issue
Block a user