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>
144 lines
3.8 KiB
Go
144 lines
3.8 KiB
Go
// 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]
|
|
}
|