syncthing/lib/fs/basicfs_xattr_unix.go
Jakob Borg 6cac308bcd
all: Support syncing extended attributes (fixes #2698) (#8513)
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>
2022-09-14 09:50:55 +02:00

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]
}