Skip to content

Commit

Permalink
all: Support syncing extended attributes (fixes #2698) (#8513)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
calmh and tomasz1986 committed Sep 14, 2022
1 parent 8065cf7 commit 6cac308
Show file tree
Hide file tree
Showing 37 changed files with 2,743 additions and 550 deletions.
39 changes: 39 additions & 0 deletions gui/default/syncthing/folder/editFolderModalView.html
Expand Up @@ -284,6 +284,45 @@
</p>
</div>
</div>

<div class="row">
<div class="col-md-6 form-group">
<p>
<label translate>Ownership</label>
&nbsp;<a href="{{docsURL('advanced/folder-sync-ownership')}}" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
</p>
<label>
<input type="checkbox" ng-disabled="currentFolder.type == 'sendonly' || currentFolder.type == 'receiveencrypted'" ng-model="currentFolder.syncOwnership" /> <span translate>Sync Ownership</span>
</label>
<p translate class="help-block">
Enables sending ownership information to other devices, and applying incoming ownership information. Typically requires running with elevated privileges.
</p>
<label>
<input type="checkbox" ng-disabled="currentFolder.type == 'receiveonly' || currentFolder.type == 'receiveencrypted' || currentFolder.syncOwnership" ng-checked="currentFolder.sendOwnership || currentFolder.syncOwnership" ng-model="currentFolder.sendOwnership" /> <span translate>Send Ownership</span>
</label>
<p translate class="help-block">
Enables sending ownership to other devices, but not applying incoming ownership information. This can have a significant performance impact. Always enabled when "Sync Ownership" is enabled.
</p>
</div>
<div class="col-md-6 form-group">
<p>
<label translate>Extended Attributes</label>
&nbsp;<a href="{{docsURL('advanced/folder-sync-xattrs')}}" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
</p>
<label>
<input type="checkbox" ng-disabled="currentFolder.type == 'sendonly' || currentFolder.type == 'receiveencrypted'" ng-model="currentFolder.syncXattrs" /> <span translate>Sync Extended Attributes</span>
</label>
<p translate class="help-block">
Enables sending extended attributes to other devices, and applying incoming extended attributes. May require running with elevated privileges.
</p>
<label>
<input type="checkbox" ng-disabled="currentFolder.type == 'receiveonly' || currentFolder.type == 'receiveencrypted' || currentFolder.syncXattrs" ng-checked="currentFolder.sendXattrs || currentFolder.syncXattrs" ng-model="currentFolder.sendXattrs" /> <span translate>Send Extended Attributes</span>
</label>
<p translate class="help-block">
Enables sending extended attributes to other devices, but not applying incoming extended attributes. This can have a significant performance impact. Always enabled when "Sync Extended Attributes" is enabled.
</p>
</div>
</div>
</div>
</div>
</form>
Expand Down
2 changes: 2 additions & 0 deletions lib/api/api.go
Expand Up @@ -1820,6 +1820,8 @@ func fileIntfJSONMap(f protocol.FileIntf) map[string]interface{} {
"sequence": f.SequenceNo(),
"version": jsonVersionVector(f.FileVersion()),
"localFlags": f.FileLocalFlags(),
"platform": f.PlatformData(),
"inodeChange": f.InodeChangeTime(),
}
if f.HasPermissionBits() {
out["permissions"] = fmt.Sprintf("%#o", f.FilePermissions())
Expand Down
2 changes: 1 addition & 1 deletion lib/config/config.go
Expand Up @@ -28,7 +28,7 @@ import (

const (
OldestHandledVersion = 10
CurrentVersion = 36
CurrentVersion = 37
MaxRescanIntervalS = 365 * 24 * 60 * 60
)

Expand Down
48 changes: 48 additions & 0 deletions lib/config/config_test.go
Expand Up @@ -106,6 +106,11 @@ func TestDefaultValues(t *testing.T) {
WeakHashThresholdPct: 25,
MarkerName: ".stfolder",
MaxConcurrentWrites: 2,
XattrFilter: XattrFilter{
Entries: []XattrFilterEntry{},
MaxSingleEntrySize: 1024,
MaxTotalSize: 4096,
},
},
Device: DeviceConfiguration{
Addresses: []string{"dynamic"},
Expand Down Expand Up @@ -177,6 +182,9 @@ func TestDeviceConfig(t *testing.T) {
MarkerName: DefaultMarkerName,
JunctionsAsDirs: true,
MaxConcurrentWrites: maxConcurrentWritesDefault,
XattrFilter: XattrFilter{
Entries: []XattrFilterEntry{},
},
},
}

Expand Down Expand Up @@ -1420,3 +1428,43 @@ func TestReceiveEncryptedFolderFixed(t *testing.T) {
t.Error("IgnorePerms should be true")
}
}

func TestXattrFilter(t *testing.T) {
cases := []struct {
in []string
filter []XattrFilterEntry
out []string
}{
{in: nil, filter: nil, out: nil},
{in: []string{"foo", "bar", "baz"}, filter: nil, out: []string{"foo", "bar", "baz"}},
{
in: []string{"foo", "bar", "baz"},
filter: []XattrFilterEntry{{Match: "b*", Permit: true}},
out: []string{"bar", "baz"},
},
{
in: []string{"foo", "bar", "baz"},
filter: []XattrFilterEntry{{Match: "b*", Permit: false}, {Match: "*", Permit: true}},
out: []string{"foo"},
},
{
in: []string{"foo", "bar", "baz"},
filter: []XattrFilterEntry{{Match: "yoink", Permit: true}},
out: []string{},
},
}

for _, tc := range cases {
f := XattrFilter{Entries: tc.filter}
var out []string
for _, s := range tc.in {
if f.Permit(s) {
out = append(out, s)
}
}

if fmt.Sprint(out) != fmt.Sprint(tc.out) {
t.Errorf("Filter.Apply(%v, %v) == %v, expected %v", tc.in, tc.filter, out, tc.out)
}
}
}
22 changes: 22 additions & 0 deletions lib/config/folderconfiguration.go
Expand Up @@ -9,6 +9,7 @@ package config
import (
"errors"
"fmt"
"path"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -272,3 +273,24 @@ func (f *FolderConfiguration) CheckAvailableSpace(req uint64) error {
}
return nil
}

func (f XattrFilter) Permit(s string) bool {
if len(f.Entries) == 0 {
return true
}

for _, entry := range f.Entries {
if ok, _ := path.Match(entry.Match, s); ok {
return entry.Permit
}
}
return false
}

func (f XattrFilter) GetMaxSingleEntrySize() int {
return f.MaxSingleEntrySize
}

func (f XattrFilter) GetMaxTotalSize() int {
return f.MaxTotalSize
}

0 comments on commit 6cac308

Please sign in to comment.