+17
-12
@@ -41,10 +41,20 @@ func (validationError) String() string {
|
||||
return "validationError"
|
||||
}
|
||||
|
||||
func TestReplaceCommit(t *testing.T) {
|
||||
t.Skip("broken, fails randomly, #3834")
|
||||
func replace(t testing.TB, w Wrapper, to Configuration) {
|
||||
t.Helper()
|
||||
waiter, err := w.Modify(func(cfg *Configuration) {
|
||||
*cfg = to
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waiter.Wait()
|
||||
}
|
||||
|
||||
func TestReplaceCommit(t *testing.T) {
|
||||
w := wrap("/dev/null", Configuration{Version: 0}, device1)
|
||||
defer w.stop()
|
||||
if w.RawCopy().Version != 0 {
|
||||
t.Fatal("Config incorrect")
|
||||
}
|
||||
@@ -52,10 +62,7 @@ func TestReplaceCommit(t *testing.T) {
|
||||
// Replace config. We should get back a clean response and the config
|
||||
// should change.
|
||||
|
||||
_, err := w.Replace(Configuration{Version: 1})
|
||||
if err != nil {
|
||||
t.Fatal("Should not have a validation error:", err)
|
||||
}
|
||||
replace(t, w, Configuration{Version: 1})
|
||||
if w.RequiresRestart() {
|
||||
t.Fatal("Should not require restart")
|
||||
}
|
||||
@@ -69,11 +76,7 @@ func TestReplaceCommit(t *testing.T) {
|
||||
sub0 := requiresRestart{committed: make(chan struct{}, 1)}
|
||||
w.Subscribe(sub0)
|
||||
|
||||
_, err = w.Replace(Configuration{Version: 2})
|
||||
if err != nil {
|
||||
t.Fatal("Should not have a validation error:", err)
|
||||
}
|
||||
|
||||
replace(t, w, Configuration{Version: 1})
|
||||
<-sub0.committed
|
||||
if !w.RequiresRestart() {
|
||||
t.Fatal("Should require restart")
|
||||
@@ -87,7 +90,9 @@ func TestReplaceCommit(t *testing.T) {
|
||||
|
||||
w.Subscribe(validationError{})
|
||||
|
||||
_, err = w.Replace(Configuration{Version: 3})
|
||||
_, err := w.Modify(func(cfg *Configuration) {
|
||||
*cfg = Configuration{Version: 3}
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("Should have a validation error")
|
||||
}
|
||||
|
||||
@@ -372,6 +372,15 @@ func (cfg *Configuration) applyMigrations() {
|
||||
migrationsMut.Unlock()
|
||||
}
|
||||
|
||||
func (cfg *Configuration) Device(id protocol.DeviceID) (DeviceConfiguration, int, bool) {
|
||||
for i, device := range cfg.Devices {
|
||||
if device.DeviceID == id {
|
||||
return device, i, true
|
||||
}
|
||||
}
|
||||
return DeviceConfiguration{}, 0, false
|
||||
}
|
||||
|
||||
// DeviceMap returns a map of device ID to device configuration for the given configuration.
|
||||
func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration {
|
||||
m := make(map[protocol.DeviceID]DeviceConfiguration, len(cfg.Devices))
|
||||
@@ -381,6 +390,44 @@ func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration
|
||||
return m
|
||||
}
|
||||
|
||||
func (cfg *Configuration) SetDevice(device DeviceConfiguration) {
|
||||
cfg.SetDevices([]DeviceConfiguration{device})
|
||||
}
|
||||
|
||||
func (cfg *Configuration) SetDevices(devices []DeviceConfiguration) {
|
||||
inds := make(map[protocol.DeviceID]int, len(cfg.Devices))
|
||||
for i, device := range cfg.Devices {
|
||||
inds[device.DeviceID] = i
|
||||
}
|
||||
filtered := devices[:0]
|
||||
for _, device := range devices {
|
||||
if i, ok := inds[device.DeviceID]; ok {
|
||||
cfg.Devices[i] = device
|
||||
} else {
|
||||
filtered = append(filtered, device)
|
||||
}
|
||||
}
|
||||
cfg.Devices = append(cfg.Devices, filtered...)
|
||||
}
|
||||
|
||||
func (cfg *Configuration) Folder(id string) (FolderConfiguration, int, bool) {
|
||||
for i, folder := range cfg.Folders {
|
||||
if folder.ID == id {
|
||||
return folder, i, true
|
||||
}
|
||||
}
|
||||
return FolderConfiguration{}, 0, false
|
||||
}
|
||||
|
||||
// FolderMap returns a map of folder ID to folder configuration for the given configuration.
|
||||
func (cfg *Configuration) FolderMap() map[string]FolderConfiguration {
|
||||
m := make(map[string]FolderConfiguration, len(cfg.Folders))
|
||||
for _, folder := range cfg.Folders {
|
||||
m[folder.ID] = folder
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// FolderPasswords returns the folder passwords set for this device, for
|
||||
// folders that have an encryption password set.
|
||||
func (cfg Configuration) FolderPasswords(device protocol.DeviceID) map[string]string {
|
||||
@@ -397,6 +444,26 @@ nextFolder:
|
||||
return res
|
||||
}
|
||||
|
||||
func (cfg *Configuration) SetFolder(folder FolderConfiguration) {
|
||||
cfg.SetFolders([]FolderConfiguration{folder})
|
||||
}
|
||||
|
||||
func (cfg *Configuration) SetFolders(folders []FolderConfiguration) {
|
||||
inds := make(map[string]int, len(cfg.Folders))
|
||||
for i, folder := range cfg.Folders {
|
||||
inds[folder.ID] = i
|
||||
}
|
||||
filtered := folders[:0]
|
||||
for _, folder := range folders {
|
||||
if i, ok := inds[folder.ID]; ok {
|
||||
cfg.Folders[i] = folder
|
||||
} else {
|
||||
filtered = append(filtered, folder)
|
||||
}
|
||||
}
|
||||
cfg.Folders = append(cfg.Folders, filtered...)
|
||||
}
|
||||
|
||||
func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.DeviceID) []FolderDeviceConfiguration {
|
||||
for _, device := range devices {
|
||||
if device.DeviceID.Equals(myID) {
|
||||
|
||||
+122
-33
@@ -8,9 +8,11 @@ package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/d4l3k/messagediff"
|
||||
"github.com/thejerf/suture/v4"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
@@ -95,7 +98,8 @@ func TestDeviceConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
os.RemoveAll(filepath.Join("testdata", DefaultMarkerName))
|
||||
wr, err := load(cfgFile, device1)
|
||||
wr, wrCancel, err := copyAndLoad(cfgFile, device1)
|
||||
defer wrCancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -107,7 +111,7 @@ func TestDeviceConfig(t *testing.T) {
|
||||
t.Fatal("Unexpected file")
|
||||
}
|
||||
|
||||
cfg := wr.(*wrapper).cfg
|
||||
cfg := wr.Wrapper.(*wrapper).cfg
|
||||
|
||||
expectedFolders := []FolderConfiguration{
|
||||
{
|
||||
@@ -170,7 +174,8 @@ func TestDeviceConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoListenAddresses(t *testing.T) {
|
||||
cfg, err := load("testdata/nolistenaddress.xml", device1)
|
||||
cfg, cfgCancel, err := copyAndLoad("testdata/nolistenaddress.xml", device1)
|
||||
defer cfgCancel()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -228,7 +233,8 @@ func TestOverriddenValues(t *testing.T) {
|
||||
}
|
||||
|
||||
os.Unsetenv("STNOUPGRADE")
|
||||
cfg, err := load("testdata/overridenvalues.xml", device1)
|
||||
cfg, cfgCancel, err := copyAndLoad("testdata/overridenvalues.xml", device1)
|
||||
defer cfgCancel()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -269,7 +275,8 @@ func TestDeviceAddressesDynamic(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := load("testdata/deviceaddressesdynamic.xml", device4)
|
||||
cfg, cfgCancel, err := copyAndLoad("testdata/deviceaddressesdynamic.xml", device4)
|
||||
defer cfgCancel()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -314,7 +321,8 @@ func TestDeviceCompression(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := load("testdata/devicecompression.xml", device4)
|
||||
cfg, cfgCancel, err := copyAndLoad("testdata/devicecompression.xml", device4)
|
||||
defer cfgCancel()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -356,7 +364,8 @@ func TestDeviceAddressesStatic(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := load("testdata/deviceaddressesstatic.xml", device4)
|
||||
cfg, cfgCancel, err := copyAndLoad("testdata/deviceaddressesstatic.xml", device4)
|
||||
defer cfgCancel()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -368,7 +377,8 @@ func TestDeviceAddressesStatic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestVersioningConfig(t *testing.T) {
|
||||
cfg, err := load("testdata/versioningconfig.xml", device4)
|
||||
cfg, cfgCancel, err := copyAndLoad("testdata/versioningconfig.xml", device4)
|
||||
defer cfgCancel()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -395,7 +405,8 @@ func TestIssue1262(t *testing.T) {
|
||||
t.Skipf("path gets converted to absolute as part of the filesystem initialization on linux")
|
||||
}
|
||||
|
||||
cfg, err := load("testdata/issue-1262.xml", device4)
|
||||
cfg, cfgCancel, err := copyAndLoad("testdata/issue-1262.xml", device4)
|
||||
defer cfgCancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -409,7 +420,8 @@ func TestIssue1262(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIssue1750(t *testing.T) {
|
||||
cfg, err := load("testdata/issue-1750.xml", device4)
|
||||
cfg, cfgCancel, err := copyAndLoad("testdata/issue-1750.xml", device4)
|
||||
defer cfgCancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -505,6 +517,7 @@ func TestFolderCheckPath(t *testing.T) {
|
||||
func TestNewSaveLoad(t *testing.T) {
|
||||
path := "testdata/temp.xml"
|
||||
os.Remove(path)
|
||||
defer os.Remove(path)
|
||||
|
||||
exists := func(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
@@ -513,6 +526,7 @@ func TestNewSaveLoad(t *testing.T) {
|
||||
|
||||
intCfg := New(device1)
|
||||
cfg := wrap(path, intCfg, device1)
|
||||
defer cfg.stop()
|
||||
|
||||
if exists(path) {
|
||||
t.Error(path, "exists")
|
||||
@@ -527,6 +541,7 @@ func TestNewSaveLoad(t *testing.T) {
|
||||
}
|
||||
|
||||
cfg2, err := load(path, device1)
|
||||
defer cfg2.stop()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -534,8 +549,6 @@ func TestNewSaveLoad(t *testing.T) {
|
||||
if diff, equal := messagediff.PrettyDiff(cfg.RawCopy(), cfg2.RawCopy()); !equal {
|
||||
t.Errorf("Configs are not equal. Diff:\n%s", diff)
|
||||
}
|
||||
|
||||
os.Remove(path)
|
||||
}
|
||||
|
||||
func TestPrepare(t *testing.T) {
|
||||
@@ -553,7 +566,8 @@ func TestPrepare(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
wrapper, err := load("testdata/example.xml", device1)
|
||||
wrapper, wrapperCancel, err := copyAndLoad("testdata/example.xml", device1)
|
||||
defer wrapperCancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -592,7 +606,8 @@ func TestCopy(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPullOrder(t *testing.T) {
|
||||
wrapper, err := load("testdata/pullorder.xml", device1)
|
||||
wrapper, wrapperCleanup, err := copyAndLoad("testdata/pullorder.xml", device1)
|
||||
defer wrapperCleanup()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -632,8 +647,9 @@ func TestPullOrder(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wrapper = wrap("testdata/pullorder.xml", cfg, device1)
|
||||
folders = wrapper.Folders()
|
||||
wrapper2 := wrap(wrapper.ConfigPath(), cfg, device1)
|
||||
defer wrapper2.stop()
|
||||
folders = wrapper2.Folders()
|
||||
|
||||
for _, tc := range expected {
|
||||
if actual := folders[tc.name].Order; actual != tc.order {
|
||||
@@ -643,7 +659,8 @@ func TestPullOrder(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLargeRescanInterval(t *testing.T) {
|
||||
wrapper, err := load("testdata/largeinterval.xml", device1)
|
||||
wrapper, wrapperCancel, err := copyAndLoad("testdata/largeinterval.xml", device1)
|
||||
defer wrapperCancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -681,7 +698,8 @@ func TestGUIConfigURL(t *testing.T) {
|
||||
func TestDuplicateDevices(t *testing.T) {
|
||||
// Duplicate devices should be removed
|
||||
|
||||
wrapper, err := load("testdata/dupdevices.xml", device1)
|
||||
wrapper, wrapperCancel, err := copyAndLoad("testdata/dupdevices.xml", device1)
|
||||
defer wrapperCancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -699,7 +717,8 @@ func TestDuplicateDevices(t *testing.T) {
|
||||
func TestDuplicateFolders(t *testing.T) {
|
||||
// Duplicate folders are a loading error
|
||||
|
||||
_, err := load("testdata/dupfolders.xml", device1)
|
||||
_, _Cancel, err := copyAndLoad("testdata/dupfolders.xml", device1)
|
||||
defer _Cancel()
|
||||
if err == nil || !strings.Contains(err.Error(), errFolderIDDuplicate.Error()) {
|
||||
t.Fatal(`Expected error to mention "duplicate folder ID":`, err)
|
||||
}
|
||||
@@ -710,7 +729,8 @@ func TestEmptyFolderPaths(t *testing.T) {
|
||||
// get messed up by the prepare steps (e.g., become the current dir or
|
||||
// get a slash added so that it becomes the root directory or similar).
|
||||
|
||||
_, err := load("testdata/nopath.xml", device1)
|
||||
_, _Cancel, err := copyAndLoad("testdata/nopath.xml", device1)
|
||||
defer _Cancel()
|
||||
if err == nil || !strings.Contains(err.Error(), errFolderPathEmpty.Error()) {
|
||||
t.Fatal("Expected error due to empty folder path, got", err)
|
||||
}
|
||||
@@ -779,7 +799,8 @@ func TestIgnoredDevices(t *testing.T) {
|
||||
// Verify that ignored devices that are also present in the
|
||||
// configuration are not in fact ignored.
|
||||
|
||||
wrapper, err := load("testdata/ignoreddevices.xml", device1)
|
||||
wrapper, wrapperCancel, err := copyAndLoad("testdata/ignoreddevices.xml", device1)
|
||||
defer wrapperCancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -797,7 +818,8 @@ func TestIgnoredFolders(t *testing.T) {
|
||||
// configuration are not in fact ignored.
|
||||
// Also, verify that folders that are shared with a device are not ignored.
|
||||
|
||||
wrapper, err := load("testdata/ignoredfolders.xml", device1)
|
||||
wrapper, wrapperCancel, err := copyAndLoad("testdata/ignoredfolders.xml", device1)
|
||||
defer wrapperCancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -833,7 +855,8 @@ func TestIgnoredFolders(t *testing.T) {
|
||||
func TestGetDevice(t *testing.T) {
|
||||
// Verify that the Device() call does the right thing
|
||||
|
||||
wrapper, err := load("testdata/ignoreddevices.xml", device1)
|
||||
wrapper, wrapperCancel, err := copyAndLoad("testdata/ignoreddevices.xml", device1)
|
||||
defer wrapperCancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -860,7 +883,8 @@ func TestGetDevice(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
|
||||
wrapper, err := load("testdata/example.xml", device1)
|
||||
wrapper, wrapperCancel, err := copyAndLoad("testdata/example.xml", device1)
|
||||
defer wrapperCancel()
|
||||
if err != nil {
|
||||
t.Errorf("Failed: %s", err)
|
||||
}
|
||||
@@ -872,10 +896,7 @@ func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
|
||||
t.Error("Should have less devices")
|
||||
}
|
||||
|
||||
_, err = wrapper.Replace(raw)
|
||||
if err != nil {
|
||||
t.Errorf("Failed: %s", err)
|
||||
}
|
||||
replace(t, wrapper, raw)
|
||||
|
||||
raw = wrapper.RawCopy()
|
||||
if len(raw.Folders[0].Devices) > len(raw.Devices) {
|
||||
@@ -947,6 +968,7 @@ func TestIssue4219(t *testing.T) {
|
||||
}
|
||||
|
||||
w := wrap("/tmp/cfg", cfg, myID)
|
||||
defer w.stop()
|
||||
if !w.IgnoredFolder(device2, "t1") {
|
||||
t.Error("Folder device2 t1 should be ignored")
|
||||
}
|
||||
@@ -1157,13 +1179,80 @@ func defaultConfigAsMap() map[string]interface{} {
|
||||
return tmp
|
||||
}
|
||||
|
||||
func load(path string, myID protocol.DeviceID) (Wrapper, error) {
|
||||
cfg, _, err := Load(path, myID, events.NoopLogger)
|
||||
return cfg, err
|
||||
func copyToTmp(path string) (string, error) {
|
||||
orig, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer orig.Close()
|
||||
temp, err := ioutil.TempFile("", "syncthing-configTest-")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer temp.Close()
|
||||
if _, err := io.Copy(temp, orig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return temp.Name(), nil
|
||||
}
|
||||
|
||||
func wrap(path string, cfg Configuration, myID protocol.DeviceID) Wrapper {
|
||||
return Wrap(path, cfg, myID, events.NoopLogger)
|
||||
func copyAndLoad(path string, myID protocol.DeviceID) (*testWrapper, func(), error) {
|
||||
temp, err := copyToTmp(path)
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
wrapper, err := load(temp, myID)
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
return wrapper, func() {
|
||||
wrapper.stop()
|
||||
os.Remove(temp)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func load(path string, myID protocol.DeviceID) (*testWrapper, error) {
|
||||
cfg, _, err := Load(path, myID, events.NoopLogger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return startWrapper(cfg), nil
|
||||
}
|
||||
|
||||
func wrap(path string, cfg Configuration, myID protocol.DeviceID) *testWrapper {
|
||||
wrapper := Wrap(path, cfg, myID, events.NoopLogger)
|
||||
return startWrapper(wrapper)
|
||||
}
|
||||
|
||||
type testWrapper struct {
|
||||
Wrapper
|
||||
cancel context.CancelFunc
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (w *testWrapper) stop() {
|
||||
w.cancel()
|
||||
<-w.done
|
||||
}
|
||||
|
||||
func startWrapper(wrapper Wrapper) *testWrapper {
|
||||
tw := &testWrapper{
|
||||
Wrapper: wrapper,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
s, ok := wrapper.(suture.Service)
|
||||
if !ok {
|
||||
tw.cancel = func() {}
|
||||
close(tw.done)
|
||||
return tw
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
tw.cancel = cancel
|
||||
go func() {
|
||||
s.Serve(ctx)
|
||||
close(tw.done)
|
||||
}()
|
||||
return tw
|
||||
}
|
||||
|
||||
func TestInternalVersioningConfiguration(t *testing.T) {
|
||||
|
||||
+148
-145
@@ -7,8 +7,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
@@ -16,6 +20,13 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
const (
|
||||
maxModifications = 1000
|
||||
minSaveInterval = 5 * time.Second
|
||||
)
|
||||
|
||||
var errTooManyModifications = errors.New("too many concurrent config modifications")
|
||||
|
||||
// The Committer interface is implemented by objects that need to know about
|
||||
// or have a say in configuration changes.
|
||||
//
|
||||
@@ -35,6 +46,10 @@ import (
|
||||
// false will result in a "restart needed" response to the API/user. Note that
|
||||
// the new configuration will still have been applied by those who were
|
||||
// capable of doing so.
|
||||
//
|
||||
// A Committer must take care not to hold any locks while changing the
|
||||
// configuration (e.g. calling Wrapper.SetFolder), that are also acquired in any
|
||||
// methods of the Committer interface.
|
||||
type Committer interface {
|
||||
VerifyConfiguration(from, to Configuration) error
|
||||
CommitConfiguration(from, to Configuration) (handled bool)
|
||||
@@ -50,45 +65,47 @@ type noopWaiter struct{}
|
||||
|
||||
func (noopWaiter) Wait() {}
|
||||
|
||||
// A Wrapper around a Configuration that manages loads, saves and published
|
||||
// notifications of changes to registered Handlers
|
||||
// ModifyFunction gets a pointer to a copy of the currently active configuration
|
||||
// for modification.
|
||||
type ModifyFunction func(*Configuration)
|
||||
|
||||
// Wrapper handles a Configuration, i.e. it provides methods to access, change
|
||||
// and save the config, and notifies registered subscribers (Committer) of
|
||||
// changes.
|
||||
//
|
||||
// Modify allows changing the currently active configuration through the given
|
||||
// ModifyFunction. It can be called concurrently: All calls will be queued and
|
||||
// called in order.
|
||||
type Wrapper interface {
|
||||
ConfigPath() string
|
||||
MyID() protocol.DeviceID
|
||||
|
||||
RawCopy() Configuration
|
||||
Replace(cfg Configuration) (Waiter, error)
|
||||
RequiresRestart() bool
|
||||
Save() error
|
||||
|
||||
GUI() GUIConfiguration
|
||||
SetGUI(gui GUIConfiguration) (Waiter, error)
|
||||
LDAP() LDAPConfiguration
|
||||
SetLDAP(ldap LDAPConfiguration) (Waiter, error)
|
||||
Modify(ModifyFunction) (Waiter, error)
|
||||
RemoveFolder(id string) (Waiter, error)
|
||||
RemoveDevice(id protocol.DeviceID) (Waiter, error)
|
||||
|
||||
GUI() GUIConfiguration
|
||||
LDAP() LDAPConfiguration
|
||||
Options() OptionsConfiguration
|
||||
SetOptions(opts OptionsConfiguration) (Waiter, error)
|
||||
|
||||
Folder(id string) (FolderConfiguration, bool)
|
||||
Folders() map[string]FolderConfiguration
|
||||
FolderList() []FolderConfiguration
|
||||
RemoveFolder(id string) (Waiter, error)
|
||||
SetFolder(fld FolderConfiguration) (Waiter, error)
|
||||
SetFolders(folders []FolderConfiguration) (Waiter, error)
|
||||
FolderPasswords(device protocol.DeviceID) map[string]string
|
||||
|
||||
Device(id protocol.DeviceID) (DeviceConfiguration, bool)
|
||||
Devices() map[protocol.DeviceID]DeviceConfiguration
|
||||
DeviceList() []DeviceConfiguration
|
||||
RemoveDevice(id protocol.DeviceID) (Waiter, error)
|
||||
SetDevice(DeviceConfiguration) (Waiter, error)
|
||||
SetDevices([]DeviceConfiguration) (Waiter, error)
|
||||
|
||||
IgnoredDevices() []ObservedDevice
|
||||
IgnoredDevice(id protocol.DeviceID) bool
|
||||
IgnoredFolder(device protocol.DeviceID, folder string) bool
|
||||
|
||||
Subscribe(c Committer)
|
||||
Subscribe(c Committer) Configuration
|
||||
Unsubscribe(c Committer)
|
||||
}
|
||||
|
||||
@@ -97,6 +114,7 @@ type wrapper struct {
|
||||
path string
|
||||
evLogger events.Logger
|
||||
myID protocol.DeviceID
|
||||
queue chan modifyEntry
|
||||
|
||||
waiter Waiter // Latest ongoing config change
|
||||
subs []Committer
|
||||
@@ -107,12 +125,15 @@ type wrapper struct {
|
||||
|
||||
// Wrap wraps an existing Configuration structure and ties it to a file on
|
||||
// disk.
|
||||
// The returned Wrapper is a suture.Service, thus needs to be started (added to
|
||||
// a supervisor).
|
||||
func Wrap(path string, cfg Configuration, myID protocol.DeviceID, evLogger events.Logger) Wrapper {
|
||||
w := &wrapper{
|
||||
cfg: cfg,
|
||||
path: path,
|
||||
evLogger: evLogger,
|
||||
myID: myID,
|
||||
queue: make(chan modifyEntry, maxModifications),
|
||||
waiter: noopWaiter{}, // Noop until first config change
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
@@ -121,6 +142,8 @@ func Wrap(path string, cfg Configuration, myID protocol.DeviceID, evLogger event
|
||||
|
||||
// Load loads an existing file on disk and returns a new configuration
|
||||
// wrapper.
|
||||
// The returned Wrapper is a suture.Service, thus needs to be started (added to
|
||||
// a supervisor).
|
||||
func Load(path string, myID protocol.DeviceID, evLogger events.Logger) (Wrapper, int, error) {
|
||||
fd, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -145,11 +168,13 @@ func (w *wrapper) MyID() protocol.DeviceID {
|
||||
}
|
||||
|
||||
// Subscribe registers the given handler to be called on any future
|
||||
// configuration changes.
|
||||
func (w *wrapper) Subscribe(c Committer) {
|
||||
// configuration changes. It returns the config that is in effect while
|
||||
// subscribing, that can be used for initial setup.
|
||||
func (w *wrapper) Subscribe(c Committer) Configuration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
w.subs = append(w.subs, c)
|
||||
w.mut.Unlock()
|
||||
return w.cfg.Copy()
|
||||
}
|
||||
|
||||
// Unsubscribe de-registers the given handler from any future calls to
|
||||
@@ -179,11 +204,84 @@ func (w *wrapper) RawCopy() Configuration {
|
||||
return w.cfg.Copy()
|
||||
}
|
||||
|
||||
// Replace swaps the current configuration object for the given one.
|
||||
func (w *wrapper) Replace(cfg Configuration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
return w.replaceLocked(cfg.Copy())
|
||||
func (w *wrapper) Modify(fn ModifyFunction) (Waiter, error) {
|
||||
return w.modifyQueued(fn)
|
||||
}
|
||||
|
||||
func (w *wrapper) modifyQueued(modifyFunc ModifyFunction) (Waiter, error) {
|
||||
e := modifyEntry{
|
||||
modifyFunc: modifyFunc,
|
||||
res: make(chan modifyResult),
|
||||
}
|
||||
select {
|
||||
case w.queue <- e:
|
||||
default:
|
||||
return noopWaiter{}, errTooManyModifications
|
||||
}
|
||||
res := <-e.res
|
||||
return res.w, res.err
|
||||
}
|
||||
|
||||
func (w *wrapper) Serve(ctx context.Context) error {
|
||||
defer w.serveSave()
|
||||
|
||||
var e modifyEntry
|
||||
saveTimer := time.NewTimer(0)
|
||||
<-saveTimer.C
|
||||
saveTimerRunning := false
|
||||
for {
|
||||
select {
|
||||
case e = <-w.queue:
|
||||
case <-saveTimer.C:
|
||||
w.serveSave()
|
||||
saveTimerRunning = false
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
var waiter Waiter = noopWaiter{}
|
||||
var err error
|
||||
|
||||
// Let the caller modify the config.
|
||||
to := w.RawCopy()
|
||||
e.modifyFunc(&to)
|
||||
|
||||
// Check if the config was actually changed at all.
|
||||
w.mut.Lock()
|
||||
if !reflect.DeepEqual(w.cfg, to) {
|
||||
waiter, err = w.replaceLocked(to)
|
||||
if !saveTimerRunning {
|
||||
saveTimer.Reset(minSaveInterval)
|
||||
saveTimerRunning = true
|
||||
}
|
||||
}
|
||||
w.mut.Unlock()
|
||||
|
||||
e.res <- modifyResult{
|
||||
w: waiter,
|
||||
err: err,
|
||||
}
|
||||
|
||||
// Wait for all subscriber to handle the config change before continuing
|
||||
// to process the next change.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
waiter.Wait()
|
||||
close(done)
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wrapper) serveSave() {
|
||||
if err := w.Save(); err != nil {
|
||||
l.Warnln("Failed to save config:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *wrapper) replaceLocked(to Configuration) (Waiter, error) {
|
||||
@@ -246,55 +344,16 @@ func (w *wrapper) DeviceList() []DeviceConfiguration {
|
||||
return w.cfg.Copy().Devices
|
||||
}
|
||||
|
||||
// SetDevices adds new devices to the configuration, or overwrites existing
|
||||
// devices with the same ID.
|
||||
func (w *wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
newCfg := w.cfg.Copy()
|
||||
var replaced bool
|
||||
for oldIndex := range devs {
|
||||
replaced = false
|
||||
for newIndex := range newCfg.Devices {
|
||||
if newCfg.Devices[newIndex].DeviceID == devs[oldIndex].DeviceID {
|
||||
newCfg.Devices[newIndex] = devs[oldIndex].Copy()
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
newCfg.Devices = append(newCfg.Devices, devs[oldIndex].Copy())
|
||||
}
|
||||
}
|
||||
|
||||
return w.replaceLocked(newCfg)
|
||||
}
|
||||
|
||||
// SetDevice adds a new device to the configuration, or overwrites an existing
|
||||
// device with the same ID.
|
||||
func (w *wrapper) SetDevice(dev DeviceConfiguration) (Waiter, error) {
|
||||
return w.SetDevices([]DeviceConfiguration{dev})
|
||||
}
|
||||
|
||||
// RemoveDevice removes the device from the configuration
|
||||
func (w *wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
newCfg := w.cfg.Copy()
|
||||
for i := range newCfg.Devices {
|
||||
if newCfg.Devices[i].DeviceID == id {
|
||||
newCfg.Devices = append(newCfg.Devices[:i], newCfg.Devices[i+1:]...)
|
||||
return w.replaceLocked(newCfg)
|
||||
return w.modifyQueued(func(cfg *Configuration) {
|
||||
if _, i, ok := cfg.Device(id); ok {
|
||||
cfg.Devices = append(cfg.Devices[:i], cfg.Devices[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
return noopWaiter{}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// Folders returns a map of folders. Folder structures should not be changed,
|
||||
// other than for the purpose of updating via SetFolder().
|
||||
// Folders returns a map of folders.
|
||||
func (w *wrapper) Folders() map[string]FolderConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
@@ -312,51 +371,13 @@ func (w *wrapper) FolderList() []FolderConfiguration {
|
||||
return w.cfg.Copy().Folders
|
||||
}
|
||||
|
||||
// SetFolder adds a new folder to the configuration, or overwrites an existing
|
||||
// folder with the same ID.
|
||||
func (w *wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {
|
||||
return w.SetFolders([]FolderConfiguration{fld})
|
||||
}
|
||||
|
||||
// SetFolders adds new folders to the configuration, or overwrites existing
|
||||
// folders with the same ID.
|
||||
func (w *wrapper) SetFolders(folders []FolderConfiguration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
newCfg := w.cfg.Copy()
|
||||
|
||||
inds := make(map[string]int, len(w.cfg.Folders))
|
||||
for i, folder := range newCfg.Folders {
|
||||
inds[folder.ID] = i
|
||||
}
|
||||
filtered := folders[:0]
|
||||
for _, folder := range folders {
|
||||
if i, ok := inds[folder.ID]; ok {
|
||||
newCfg.Folders[i] = folder
|
||||
} else {
|
||||
filtered = append(filtered, folder)
|
||||
}
|
||||
}
|
||||
newCfg.Folders = append(newCfg.Folders, filtered...)
|
||||
|
||||
return w.replaceLocked(newCfg)
|
||||
}
|
||||
|
||||
// RemoveFolder removes the folder from the configuration
|
||||
func (w *wrapper) RemoveFolder(id string) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
newCfg := w.cfg.Copy()
|
||||
for i := range newCfg.Folders {
|
||||
if newCfg.Folders[i].ID == id {
|
||||
newCfg.Folders = append(newCfg.Folders[:i], newCfg.Folders[i+1:]...)
|
||||
return w.replaceLocked(newCfg)
|
||||
return w.modifyQueued(func(cfg *Configuration) {
|
||||
if _, i, ok := cfg.Folder(id); ok {
|
||||
cfg.Folders = append(cfg.Folders[:i], cfg.Folders[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
return noopWaiter{}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// FolderPasswords returns the folder passwords set for this device, for
|
||||
@@ -374,29 +395,12 @@ func (w *wrapper) Options() OptionsConfiguration {
|
||||
return w.cfg.Options.Copy()
|
||||
}
|
||||
|
||||
// SetOptions replaces the current options configuration object.
|
||||
func (w *wrapper) SetOptions(opts OptionsConfiguration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
newCfg.Options = opts.Copy()
|
||||
return w.replaceLocked(newCfg)
|
||||
}
|
||||
|
||||
func (w *wrapper) LDAP() LDAPConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
return w.cfg.LDAP.Copy()
|
||||
}
|
||||
|
||||
func (w *wrapper) SetLDAP(ldap LDAPConfiguration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
newCfg.LDAP = ldap.Copy()
|
||||
return w.replaceLocked(newCfg)
|
||||
}
|
||||
|
||||
// GUI returns the current GUI configuration object.
|
||||
func (w *wrapper) GUI() GUIConfiguration {
|
||||
w.mut.Lock()
|
||||
@@ -404,15 +408,6 @@ func (w *wrapper) GUI() GUIConfiguration {
|
||||
return w.cfg.GUI.Copy()
|
||||
}
|
||||
|
||||
// SetGUI replaces the current GUI configuration object.
|
||||
func (w *wrapper) SetGUI(gui GUIConfiguration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
newCfg.GUI = gui.Copy()
|
||||
return w.replaceLocked(newCfg)
|
||||
}
|
||||
|
||||
// IgnoredDevice returns whether or not connection attempts from the given
|
||||
// device should be silently ignored.
|
||||
func (w *wrapper) IgnoredDevice(id protocol.DeviceID) bool {
|
||||
@@ -449,24 +444,22 @@ func (w *wrapper) IgnoredFolder(device protocol.DeviceID, folder string) bool {
|
||||
func (w *wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
for _, device := range w.cfg.Devices {
|
||||
if device.DeviceID == id {
|
||||
return device.Copy(), true
|
||||
}
|
||||
device, _, ok := w.cfg.Device(id)
|
||||
if !ok {
|
||||
return DeviceConfiguration{}, false
|
||||
}
|
||||
return DeviceConfiguration{}, false
|
||||
return device.Copy(), ok
|
||||
}
|
||||
|
||||
// Folder returns the configuration for the given folder and an "ok" bool.
|
||||
func (w *wrapper) Folder(id string) (FolderConfiguration, bool) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
for _, folder := range w.cfg.Folders {
|
||||
if folder.ID == id {
|
||||
return folder.Copy(), true
|
||||
}
|
||||
fcfg, _, ok := w.cfg.Folder(id)
|
||||
if !ok {
|
||||
return FolderConfiguration{}, false
|
||||
}
|
||||
return FolderConfiguration{}, false
|
||||
return fcfg.Copy(), ok
|
||||
}
|
||||
|
||||
// Save writes the configuration to disk, and generates a ConfigSaved event.
|
||||
@@ -502,3 +495,13 @@ func (w *wrapper) RequiresRestart() bool {
|
||||
func (w *wrapper) setRequiresRestart() {
|
||||
atomic.StoreUint32(&w.requiresRestart, 1)
|
||||
}
|
||||
|
||||
type modifyEntry struct {
|
||||
modifyFunc ModifyFunction
|
||||
res chan modifyResult
|
||||
}
|
||||
|
||||
type modifyResult struct {
|
||||
w Waiter
|
||||
err error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user