This adds support for syncing ownership on Unixes and on Windows. The scanner always picks up ownership information, but it is not applied unless the new folder option "Sync Ownership" is set. Ownership data is stored in a new FileInfo field called "platform data". This is intended to hold further platform-specific data in the future (specifically, extended attributes), which is why the whole design is a bit overkill for just ownership.
This commit is contained in:
@@ -129,14 +129,6 @@ func (f *BasicFilesystem) Chmod(name string, mode FileMode) error {
|
||||
return os.Chmod(name, os.FileMode(mode))
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Lchown(name string, uid, gid int) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Lchown(name, uid, gid)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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
|
||||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func (f *BasicFilesystem) PlatformData(name string) (protocol.PlatformData, error) {
|
||||
return unixPlatformData(f, name)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/user"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func (f *BasicFilesystem) PlatformData(name string) (protocol.PlatformData, error) {
|
||||
rootedName, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return protocol.PlatformData{}, fmt.Errorf("rooted for %s: %w", name, err)
|
||||
}
|
||||
hdl, err := openReadOnlyWithBackupSemantics(rootedName)
|
||||
if err != nil {
|
||||
return protocol.PlatformData{}, fmt.Errorf("open %s: %w", rootedName, err)
|
||||
}
|
||||
defer windows.Close(hdl)
|
||||
|
||||
// GetSecurityInfo returns an owner SID.
|
||||
sd, err := windows.GetSecurityInfo(hdl, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION)
|
||||
if err != nil {
|
||||
return protocol.PlatformData{}, fmt.Errorf("get security info for %s: %w", rootedName, err)
|
||||
}
|
||||
owner, _, err := sd.Owner()
|
||||
if err != nil {
|
||||
return protocol.PlatformData{}, fmt.Errorf("get owner for %s: %w", rootedName, err)
|
||||
}
|
||||
|
||||
// The owner SID might represent a user or a group. We try to look it up
|
||||
// as both, and set the appropriate fields in the OS data.
|
||||
pd := &protocol.WindowsData{}
|
||||
if us, err := user.LookupId(owner.String()); err == nil {
|
||||
pd.OwnerName = us.Username
|
||||
} else if gr, err := user.LookupGroupId(owner.String()); err == nil {
|
||||
pd.OwnerName = gr.Name
|
||||
pd.OwnerIsGroup = true
|
||||
} else {
|
||||
l.Debugf("Failed to resolve owner for %s: %v", rootedName, err)
|
||||
}
|
||||
|
||||
return protocol.PlatformData{Windows: pd}, nil
|
||||
}
|
||||
|
||||
func openReadOnlyWithBackupSemantics(path string) (fd windows.Handle, err error) {
|
||||
// This is windows.Open but simplified to read-only only, and adding
|
||||
// FILE_FLAG_BACKUP_SEMANTICS which is required to open directories.
|
||||
if len(path) == 0 {
|
||||
return windows.InvalidHandle, windows.ERROR_FILE_NOT_FOUND
|
||||
}
|
||||
pathp, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return windows.InvalidHandle, err
|
||||
}
|
||||
var access uint32 = windows.GENERIC_READ
|
||||
var sharemode uint32 = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE
|
||||
var sa *windows.SecurityAttributes
|
||||
var createmode uint32 = windows.OPEN_EXISTING
|
||||
var attrs uint32 = windows.FILE_ATTRIBUTE_READONLY | windows.FILE_FLAG_BACKUP_SEMANTICS
|
||||
return windows.CreateFile(pathp, access, sharemode, sa, createmode, attrs, 0)
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -84,7 +85,7 @@ func TestChownFile(t *testing.T) {
|
||||
newUID := 1000 + rand.Intn(30000)
|
||||
newGID := 1000 + rand.Intn(30000)
|
||||
|
||||
if err := fs.Lchown("file", newUID, newGID); err != nil {
|
||||
if err := fs.Lchown("file", strconv.Itoa(newUID), strconv.Itoa(newGID)); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ package fs
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -57,6 +58,22 @@ func (f *BasicFilesystem) Roots() ([]string, error) {
|
||||
return []string{"/"}, nil
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Lchown(name, uid, gid string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nuid, err := strconv.Atoi(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ngid, err := strconv.Atoi(gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Lchown(name, nuid, ngid)
|
||||
}
|
||||
|
||||
// unrootedChecked returns the path relative to the folder root (same as
|
||||
// unrooted) or an error if the given path is not a subpath and handles the
|
||||
// special case when the given path is the folder root without a trailing
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var errNotSupported = errors.New("symlinks not supported")
|
||||
@@ -152,6 +154,42 @@ func (f *BasicFilesystem) Roots() ([]string, error) {
|
||||
return drives, nil
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Lchown(name, uid, gid string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hdl, err := windows.Open(name, windows.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer windows.Close(hdl)
|
||||
|
||||
// Depending on whether we got an uid or a gid, we need to set the
|
||||
// appropriate flag and parse the corresponding SID. The one we're not
|
||||
// setting remains nil, which is what we want in the call to
|
||||
// SetSecurityInfo.
|
||||
|
||||
var si windows.SECURITY_INFORMATION
|
||||
var ownerSID, groupSID *syscall.SID
|
||||
if uid != "" {
|
||||
ownerSID, err = syscall.StringToSid(uid)
|
||||
if err == nil {
|
||||
si |= windows.OWNER_SECURITY_INFORMATION
|
||||
}
|
||||
} else if gid != "" {
|
||||
groupSID, err = syscall.StringToSid(uid)
|
||||
if err == nil {
|
||||
si |= windows.GROUP_SECURITY_INFORMATION
|
||||
}
|
||||
} else {
|
||||
return errors.New("neither uid nor gid specified")
|
||||
}
|
||||
|
||||
return windows.SetSecurityInfo(hdl, windows.SE_FILE_OBJECT, si, (*windows.SID)(ownerSID), (*windows.SID)(groupSID), nil, nil)
|
||||
}
|
||||
|
||||
// unrootedChecked returns the path relative to the folder root (same as
|
||||
// unrooted) or an error if the given path is not a subpath and handles the
|
||||
// special case when the given path is the folder root without a trailing
|
||||
|
||||
+1
-1
@@ -153,7 +153,7 @@ func (f *caseFilesystem) Chmod(name string, mode FileMode) error {
|
||||
return f.Filesystem.Chmod(name, mode)
|
||||
}
|
||||
|
||||
func (f *caseFilesystem) Lchown(name string, uid, gid int) error {
|
||||
func (f *caseFilesystem) Lchown(name, uid, gid string) error {
|
||||
if err := f.checkCase(name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+6
-1
@@ -9,6 +9,8 @@ package fs
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type errorFilesystem struct {
|
||||
@@ -18,7 +20,7 @@ type errorFilesystem struct {
|
||||
}
|
||||
|
||||
func (fs *errorFilesystem) Chmod(name string, mode FileMode) error { return fs.err }
|
||||
func (fs *errorFilesystem) Lchown(name string, uid, gid int) error { return fs.err }
|
||||
func (fs *errorFilesystem) Lchown(name, uid, gid string) error { return fs.err }
|
||||
func (fs *errorFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
return fs.err
|
||||
}
|
||||
@@ -52,6 +54,9 @@ func (fs *errorFilesystem) SameFile(fi1, fi2 FileInfo) bool { return false }
|
||||
func (fs *errorFilesystem) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, <-chan error, error) {
|
||||
return nil, nil, fs.err
|
||||
}
|
||||
func (fs *errorFilesystem) PlatformData(name string) (protocol.PlatformData, error) {
|
||||
return protocol.PlatformData{}, fs.err
|
||||
}
|
||||
|
||||
func (fs *errorFilesystem) underlying() (Filesystem, bool) {
|
||||
return nil, false
|
||||
|
||||
+18
-13
@@ -21,6 +21,8 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
// see readShortAt()
|
||||
@@ -29,19 +31,19 @@ const randomBlockShift = 14 // 128k
|
||||
// fakeFS is a fake filesystem for testing and benchmarking. It has the
|
||||
// following properties:
|
||||
//
|
||||
// - File metadata is kept in RAM. Specifically, we remember which files and
|
||||
// directories exist, their dates, permissions and sizes. Symlinks are
|
||||
// not supported.
|
||||
// - File metadata is kept in RAM. Specifically, we remember which files and
|
||||
// directories exist, their dates, permissions and sizes. Symlinks are
|
||||
// not supported.
|
||||
//
|
||||
// - File contents are generated pseudorandomly with just the file name as
|
||||
// seed. Writes are discarded, other than having the effect of increasing
|
||||
// the file size. If you only write data that you've read from a file with
|
||||
// the same name on a different fakeFS, you'll never know the difference...
|
||||
// - File contents are generated pseudorandomly with just the file name as
|
||||
// seed. Writes are discarded, other than having the effect of increasing
|
||||
// the file size. If you only write data that you've read from a file with
|
||||
// the same name on a different fakeFS, you'll never know the difference...
|
||||
//
|
||||
// - We totally ignore permissions - pretend you are root.
|
||||
//
|
||||
// - The root path can contain URL query-style parameters that pre populate
|
||||
// the filesystem at creation with a certain amount of random data:
|
||||
// - The root path can contain URL query-style parameters that pre populate
|
||||
// the filesystem at creation with a certain amount of random data:
|
||||
//
|
||||
// files=n to generate n random files (default 0)
|
||||
// maxsize=n to generate files up to a total of n MiB (default 0)
|
||||
@@ -51,7 +53,6 @@ const randomBlockShift = 14 // 128k
|
||||
// latency=d to set the amount of time each "disk" operation takes, where d is time.ParseDuration format
|
||||
//
|
||||
// - Two fakeFS:s pointing at the same root path see the same files.
|
||||
//
|
||||
type fakeFS struct {
|
||||
counters fakeFSCounters
|
||||
uri string
|
||||
@@ -220,7 +221,7 @@ func (fs *fakeFS) Chmod(name string, mode FileMode) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fakeFS) Lchown(name string, uid, gid int) error {
|
||||
func (fs *fakeFS) Lchown(name, uid, gid string) error {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
fs.counters.Lchown++
|
||||
@@ -229,8 +230,8 @@ func (fs *fakeFS) Lchown(name string, uid, gid int) error {
|
||||
if entry == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
entry.uid = uid
|
||||
entry.gid = gid
|
||||
entry.uid, _ = strconv.Atoi(uid)
|
||||
entry.gid, _ = strconv.Atoi(gid)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -656,6 +657,10 @@ 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)
|
||||
}
|
||||
|
||||
func (fs *fakeFS) underlying() (Filesystem, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestFakeFS(t *testing.T) {
|
||||
}
|
||||
|
||||
// Chown
|
||||
if err := fs.Lchown("dira", 1234, 5678); err != nil {
|
||||
if err := fs.Lchown("dira", "1234", "5678"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info, err := fs.Lstat("dira"); err != nil {
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type filesystemWrapperType int32
|
||||
@@ -30,7 +32,7 @@ const (
|
||||
// The Filesystem interface abstracts access to the file system.
|
||||
type Filesystem interface {
|
||||
Chmod(name string, mode FileMode) error
|
||||
Lchown(name string, uid, gid int) error
|
||||
Lchown(name string, uid, gid string) error // uid/gid as strings; numeric on POSIX, SID on Windows, like in os/user package
|
||||
Chtimes(name string, atime time.Time, mtime time.Time) error
|
||||
Create(name string) (File, error)
|
||||
CreateSymlink(target, name string) error
|
||||
@@ -60,6 +62,7 @@ type Filesystem interface {
|
||||
URI() string
|
||||
Options() []Option
|
||||
SameFile(fi1, fi2 FileInfo) bool
|
||||
PlatformData(name string) (protocol.PlatformData, error)
|
||||
|
||||
// Used for unwrapping things
|
||||
underlying() (Filesystem, bool)
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ func (o *optionMtime) apply(fs Filesystem) Filesystem {
|
||||
return f
|
||||
}
|
||||
|
||||
func (_ *optionMtime) String() string {
|
||||
func (*optionMtime) String() string {
|
||||
return "mtime"
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os/user"
|
||||
"strconv"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
// 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) (protocol.PlatformData, error) {
|
||||
stat, err := fs.Lstat(name)
|
||||
if err != nil {
|
||||
return protocol.PlatformData{}, err
|
||||
}
|
||||
|
||||
ownerUID := stat.Owner()
|
||||
ownerName := ""
|
||||
if u, err := user.LookupId(strconv.Itoa(ownerUID)); err == nil && u.Username != "" {
|
||||
ownerName = u.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"
|
||||
}
|
||||
|
||||
groupID := stat.Group()
|
||||
groupName := ""
|
||||
if g, err := user.LookupGroupId(strconv.Itoa(groupID)); err == nil && g.Name != "" {
|
||||
groupName = g.Name
|
||||
} else if groupID == 0 {
|
||||
groupName = "root"
|
||||
}
|
||||
|
||||
return protocol.PlatformData{
|
||||
Unix: &protocol.UnixData{
|
||||
OwnerName: ownerName,
|
||||
GroupName: groupName,
|
||||
UID: ownerUID,
|
||||
GID: groupID,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user