Bläddra i källkod

all: Hide implementations behind interfaces for mocked testing (#5548)

* lib/model: Hide implementations behind interfaces for mocked testing

* review
Simon Frei 6 år sedan
förälder
incheckning
722b3fce6a

+ 5 - 65
cmd/syncthing/gui.go

@@ -40,11 +40,9 @@ import (
 	"github.com/syncthing/syncthing/lib/model"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/rand"
-	"github.com/syncthing/syncthing/lib/stats"
 	"github.com/syncthing/syncthing/lib/sync"
 	"github.com/syncthing/syncthing/lib/tlsutil"
 	"github.com/syncthing/syncthing/lib/upgrade"
-	"github.com/syncthing/syncthing/lib/versioner"
 	"github.com/vitrun/qart/qr"
 	"golang.org/x/crypto/bcrypt"
 )
@@ -64,15 +62,15 @@ const (
 
 type apiService struct {
 	id                 protocol.DeviceID
-	cfg                configIntf
+	cfg                config.Wrapper
 	httpsCertFile      string
 	httpsKeyFile       string
 	statics            *staticsServer
-	model              modelIntf
+	model              model.Model
 	eventSubs          map[events.EventType]events.BufferedSubscription
 	eventSubsMut       sync.Mutex
 	discoverer         discover.CachingMux
-	connectionsService connectionsIntf
+	connectionsService connections.Service
 	fss                *folderSummaryService
 	systemConfigMut    sync.Mutex    // serializes posts to /rest/system/config
 	stop               chan struct{} // signals intentional stop
@@ -86,69 +84,11 @@ type apiService struct {
 	systemLog logger.Recorder
 }
 
-type modelIntf interface {
-	GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
-	Completion(device protocol.DeviceID, folder string) model.FolderCompletion
-	Override(folder string)
-	Revert(folder string)
-	NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated)
-	RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error)
-	LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated
-	NeedSize(folder string) db.Counts
-	ConnectionStats() map[string]interface{}
-	DeviceStatistics() map[string]stats.DeviceStatistics
-	FolderStatistics() map[string]stats.FolderStatistics
-	CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool)
-	CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool)
-	ResetFolder(folder string)
-	Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []model.Availability
-	GetIgnores(folder string) ([]string, []string, error)
-	GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error)
-	RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error)
-	SetIgnores(folder string, content []string) error
-	DelayScan(folder string, next time.Duration)
-	ScanFolder(folder string) error
-	ScanFolders() map[string]error
-	ScanFolderSubdirs(folder string, subs []string) error
-	BringToFront(folder, file string)
-	Connection(deviceID protocol.DeviceID) (connections.Connection, bool)
-	GlobalSize(folder string) db.Counts
-	LocalSize(folder string) db.Counts
-	ReceiveOnlyChangedSize(folder string) db.Counts
-	CurrentSequence(folder string) (int64, bool)
-	RemoteSequence(folder string) (int64, bool)
-	State(folder string) (string, time.Time, error)
-	UsageReportingStats(version int, preview bool) map[string]interface{}
-	FolderErrors(folder string) ([]model.FileError, error)
-	WatchError(folder string) error
-}
-
-type configIntf interface {
-	GUI() config.GUIConfiguration
-	LDAP() config.LDAPConfiguration
-	RawCopy() config.Configuration
-	Options() config.OptionsConfiguration
-	Replace(cfg config.Configuration) (config.Waiter, error)
-	Subscribe(c config.Committer)
-	Folders() map[string]config.FolderConfiguration
-	Devices() map[protocol.DeviceID]config.DeviceConfiguration
-	SetDevice(config.DeviceConfiguration) (config.Waiter, error)
-	SetDevices([]config.DeviceConfiguration) (config.Waiter, error)
-	Save() error
-	ListenAddresses() []string
-	RequiresRestart() bool
-}
-
-type connectionsIntf interface {
-	Status() map[string]interface{}
-	NATType() string
-}
-
 type rater interface {
 	Rate() float64
 }
 
-func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, defaultSub, diskSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder, cpu rater) *apiService {
+func newAPIService(id protocol.DeviceID, cfg config.Wrapper, httpsCertFile, httpsKeyFile, assetDir string, m model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connections.Service, errors, systemLog logger.Recorder, cpu rater) *apiService {
 	service := &apiService{
 		id:            id,
 		cfg:           cfg,
@@ -719,7 +659,7 @@ func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func folderSummary(cfg configIntf, m modelIntf, folder string) (map[string]interface{}, error) {
+func folderSummary(cfg config.Wrapper, m model.Model, folder string) (map[string]interface{}, error) {
 	var res = make(map[string]interface{})
 
 	errors, err := m.FolderErrors(folder)

+ 8 - 8
cmd/syncthing/main.go

@@ -956,7 +956,7 @@ func setupSignalHandling() {
 	}()
 }
 
-func loadOrDefaultConfig() (*config.Wrapper, error) {
+func loadOrDefaultConfig() (config.Wrapper, error) {
 	cfgFile := locations.Get(locations.ConfigFile)
 	cfg, err := config.Load(cfgFile, myID)
 
@@ -967,7 +967,7 @@ func loadOrDefaultConfig() (*config.Wrapper, error) {
 	return cfg, err
 }
 
-func loadConfigAtStartup() (*config.Wrapper, error) {
+func loadConfigAtStartup() (config.Wrapper, error) {
 	cfgFile := locations.Get(locations.ConfigFile)
 	cfg, err := config.Load(cfgFile, myID)
 	if os.IsNotExist(err) {
@@ -996,7 +996,7 @@ func loadConfigAtStartup() (*config.Wrapper, error) {
 	return cfg, nil
 }
 
-func archiveAndSaveConfig(cfg *config.Wrapper) error {
+func archiveAndSaveConfig(cfg config.Wrapper) error {
 	// Copy the existing config to an archive copy
 	archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", cfg.RawCopy().OriginalVersion)
 	l.Infoln("Archiving a copy of old config file format at:", archivePath)
@@ -1061,7 +1061,7 @@ func startAuditing(mainService *suture.Supervisor, auditFile string) {
 	l.Infoln("Audit log in", auditDest)
 }
 
-func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService *connections.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) {
+func setupGUI(mainService *suture.Supervisor, cfg config.Wrapper, m model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connections.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) {
 	guiCfg := cfg.GUI()
 
 	if !guiCfg.Enabled {
@@ -1091,7 +1091,7 @@ func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Mode
 	}
 }
 
-func defaultConfig(cfgFile string) (*config.Wrapper, error) {
+func defaultConfig(cfgFile string) (config.Wrapper, error) {
 	newCfg, err := config.NewWithFreePorts(myID)
 	if err != nil {
 		return nil, err
@@ -1154,7 +1154,7 @@ func standbyMonitor() {
 	}
 }
 
-func autoUpgrade(cfg *config.Wrapper) {
+func autoUpgrade(cfg config.Wrapper) {
 	timer := time.NewTimer(0)
 	sub := events.Default.Subscribe(events.DeviceConnected)
 	for {
@@ -1256,7 +1256,7 @@ func cleanConfigDirectory() {
 // checkShortIDs verifies that the configuration won't result in duplicate
 // short ID:s; that is, that the devices in the cluster all have unique
 // initial 64 bits.
-func checkShortIDs(cfg *config.Wrapper) error {
+func checkShortIDs(cfg config.Wrapper) error {
 	exists := make(map[protocol.ShortID]protocol.DeviceID)
 	for deviceID := range cfg.Devices() {
 		shortID := deviceID.Short()
@@ -1278,7 +1278,7 @@ func showPaths(options RuntimeOptions) {
 	fmt.Printf("Default sync folder directory:\n\t%s\n\n", locations.Get(locations.DefFolder))
 }
 
-func setPauseState(cfg *config.Wrapper, paused bool) {
+func setPauseState(cfg config.Wrapper, paused bool) {
 	raw := cfg.RawCopy()
 	for i := range raw.Devices {
 		raw.Devices[i].Paused = paused

+ 54 - 0
cmd/syncthing/mocked_config_test.go

@@ -44,6 +44,8 @@ func (c *mockedConfig) Replace(cfg config.Configuration) (config.Waiter, error)
 
 func (c *mockedConfig) Subscribe(cm config.Committer) {}
 
+func (c *mockedConfig) Unsubscribe(cm config.Committer) {}
+
 func (c *mockedConfig) Folders() map[string]config.FolderConfiguration {
 	return nil
 }
@@ -68,6 +70,58 @@ func (c *mockedConfig) RequiresRestart() bool {
 	return false
 }
 
+func (c *mockedConfig) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) {}
+
+func (c *mockedConfig) AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID) {}
+
+func (m *mockedConfig) MyName() string {
+	return ""
+}
+
+func (m *mockedConfig) ConfigPath() string {
+	return ""
+}
+
+func (m *mockedConfig) SetGUI(gui config.GUIConfiguration) (config.Waiter, error) {
+	return noopWaiter{}, nil
+}
+
+func (m *mockedConfig) SetOptions(opts config.OptionsConfiguration) (config.Waiter, error) {
+	return noopWaiter{}, nil
+}
+
+func (m *mockedConfig) Folder(id string) (config.FolderConfiguration, bool) {
+	return config.FolderConfiguration{}, false
+}
+
+func (m *mockedConfig) FolderList() []config.FolderConfiguration {
+	return nil
+}
+
+func (m *mockedConfig) SetFolder(fld config.FolderConfiguration) (config.Waiter, error) {
+	return noopWaiter{}, nil
+}
+
+func (m *mockedConfig) Device(id protocol.DeviceID) (config.DeviceConfiguration, bool) {
+	return config.DeviceConfiguration{}, false
+}
+
+func (m *mockedConfig) RemoveDevice(id protocol.DeviceID) (config.Waiter, error) {
+	return noopWaiter{}, nil
+}
+
+func (m *mockedConfig) IgnoredDevice(id protocol.DeviceID) bool {
+	return false
+}
+
+func (m *mockedConfig) IgnoredFolder(device protocol.DeviceID, folder string) bool {
+	return false
+}
+
+func (m *mockedConfig) GlobalDiscoveryServers() []string {
+	return nil
+}
+
 type noopWaiter struct{}
 
 func (noopWaiter) Wait() {}

+ 4 - 0
cmd/syncthing/mocked_connections_test.go

@@ -15,3 +15,7 @@ func (m *mockedConnections) Status() map[string]interface{} {
 func (m *mockedConnections) NATType() string {
 	return ""
 }
+
+func (m *mockedConnections) Serve() {}
+
+func (m *mockedConnections) Stop() {}

+ 37 - 0
cmd/syncthing/mocked_model_test.go

@@ -7,8 +7,10 @@
 package main
 
 import (
+	"net"
 	"time"
 
+	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/connections"
 	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/model"
@@ -150,3 +152,38 @@ func (m *mockedModel) WatchError(folder string) error {
 func (m *mockedModel) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
 	return nil
 }
+
+func (m *mockedModel) Serve()                                                                     {}
+func (m *mockedModel) Stop()                                                                      {}
+func (m *mockedModel) Index(deviceID protocol.DeviceID, folder string, files []protocol.FileInfo) {}
+func (m *mockedModel) IndexUpdate(deviceID protocol.DeviceID, folder string, files []protocol.FileInfo) {
+}
+
+func (m *mockedModel) Request(deviceID protocol.DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (protocol.RequestResponse, error) {
+	return nil, nil
+}
+
+func (m *mockedModel) ClusterConfig(deviceID protocol.DeviceID, config protocol.ClusterConfig) {}
+
+func (m *mockedModel) Closed(conn protocol.Connection, err error) {}
+
+func (m *mockedModel) DownloadProgress(deviceID protocol.DeviceID, folder string, updates []protocol.FileDownloadProgressUpdate) {
+}
+
+func (m *mockedModel) AddConnection(conn connections.Connection, hello protocol.HelloResult) {}
+
+func (m *mockedModel) OnHello(protocol.DeviceID, net.Addr, protocol.HelloResult) error {
+	return nil
+}
+
+func (m *mockedModel) GetHello(protocol.DeviceID) protocol.HelloIntf {
+	return nil
+}
+
+func (m *mockedModel) AddFolder(cfg config.FolderConfiguration) {}
+
+func (m *mockedModel) RestartFolder(from, to config.FolderConfiguration) {}
+
+func (m *mockedModel) StartFolder(folder string) {}
+
+func (m *mockedModel) StartDeadlockDetector(timeout time.Duration) {}

+ 5 - 3
cmd/syncthing/summaryservice.go

@@ -9,7 +9,9 @@ package main
 import (
 	"time"
 
+	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/events"
+	"github.com/syncthing/syncthing/lib/model"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/sync"
 	"github.com/thejerf/suture"
@@ -20,8 +22,8 @@ import (
 type folderSummaryService struct {
 	*suture.Supervisor
 
-	cfg       configIntf
-	model     modelIntf
+	cfg       config.Wrapper
+	model     model.Model
 	stop      chan struct{}
 	immediate chan string
 
@@ -34,7 +36,7 @@ type folderSummaryService struct {
 	lastEventReqMut sync.Mutex
 }
 
-func newFolderSummaryService(cfg configIntf, m modelIntf) *folderSummaryService {
+func newFolderSummaryService(cfg config.Wrapper, m model.Model) *folderSummaryService {
 	service := &folderSummaryService{
 		Supervisor: suture.New("folderSummaryService", suture.Spec{
 			PassThroughPanics: true,

+ 5 - 5
cmd/syncthing/usage_report.go

@@ -37,7 +37,7 @@ const usageReportVersion = 3
 
 // reportData returns the data to be sent in a usage report. It's used in
 // various places, so not part of the usageReportingManager object.
-func reportData(cfg configIntf, m modelIntf, connectionsService connectionsIntf, version int, preview bool) map[string]interface{} {
+func reportData(cfg config.Wrapper, m model.Model, connectionsService connections.Service, version int, preview bool) map[string]interface{} {
 	opts := cfg.Options()
 	res := make(map[string]interface{})
 	res["urVersion"] = version
@@ -323,16 +323,16 @@ func reportData(cfg configIntf, m modelIntf, connectionsService connectionsIntf,
 }
 
 type usageReportingService struct {
-	cfg                *config.Wrapper
-	model              *model.Model
-	connectionsService *connections.Service
+	cfg                config.Wrapper
+	model              model.Model
+	connectionsService connections.Service
 	forceRun           chan struct{}
 	stop               chan struct{}
 	stopped            chan struct{}
 	stopMut            sync.RWMutex
 }
 
-func newUsageReportingService(cfg *config.Wrapper, model *model.Model, connectionsService *connections.Service) *usageReportingService {
+func newUsageReportingService(cfg config.Wrapper, model model.Model, connectionsService connections.Service) *usageReportingService {
 	svc := &usageReportingService{
 		cfg:                cfg,
 		model:              model,

+ 3 - 3
lib/config/config_test.go

@@ -93,7 +93,7 @@ func TestDeviceConfig(t *testing.T) {
 			t.Fatal("Unexpected file")
 		}
 
-		cfg := wr.cfg
+		cfg := wr.(*wrapper).cfg
 
 		expectedFolders := []FolderConfiguration{
 			{
@@ -515,7 +515,7 @@ func TestNewSaveLoad(t *testing.T) {
 	cfg := Wrap(path, intCfg)
 
 	// To make the equality pass later
-	cfg.cfg.XMLName.Local = "configuration"
+	cfg.(*wrapper).cfg.XMLName.Local = "configuration"
 
 	if exists(path) {
 		t.Error(path, "exists")
@@ -827,7 +827,7 @@ func TestIgnoredFolders(t *testing.T) {
 
 	// 2 for folder2, 1 for folder1, as non-existing device and device the folder is shared with is removed.
 	expectedIgnoredFolders := 3
-	for _, dev := range wrapper.cfg.Devices {
+	for _, dev := range wrapper.Devices() {
 		expectedIgnoredFolders -= len(dev.IgnoredFolders)
 	}
 	if expectedIgnoredFolders != 0 {

+ 75 - 37
lib/config/wrapper.go

@@ -52,10 +52,48 @@ type noopWaiter struct{}
 
 func (noopWaiter) Wait() {}
 
-// A wrapper around a Configuration that manages loads, saves and published
+// A Wrapper around a Configuration that manages loads, saves and published
 // notifications of changes to registered Handlers
+type Wrapper interface {
+	MyName() string
+	ConfigPath() string
 
-type Wrapper struct {
+	RawCopy() Configuration
+	Replace(cfg Configuration) (Waiter, error)
+	RequiresRestart() bool
+	Save() error
+
+	GUI() GUIConfiguration
+	SetGUI(gui GUIConfiguration) (Waiter, error)
+	LDAP() LDAPConfiguration
+
+	Options() OptionsConfiguration
+	SetOptions(opts OptionsConfiguration) (Waiter, error)
+
+	Folder(id string) (FolderConfiguration, bool)
+	Folders() map[string]FolderConfiguration
+	FolderList() []FolderConfiguration
+	SetFolder(fld FolderConfiguration) (Waiter, error)
+
+	Device(id protocol.DeviceID) (DeviceConfiguration, bool)
+	Devices() map[protocol.DeviceID]DeviceConfiguration
+	RemoveDevice(id protocol.DeviceID) (Waiter, error)
+	SetDevice(DeviceConfiguration) (Waiter, error)
+	SetDevices([]DeviceConfiguration) (Waiter, error)
+
+	AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string)
+	AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID)
+	IgnoredDevice(id protocol.DeviceID) bool
+	IgnoredFolder(device protocol.DeviceID, folder string) bool
+
+	ListenAddresses() []string
+	GlobalDiscoveryServers() []string
+
+	Subscribe(c Committer)
+	Unsubscribe(c Committer)
+}
+
+type wrapper struct {
 	cfg  Configuration
 	path string
 
@@ -69,8 +107,8 @@ type Wrapper struct {
 
 // Wrap wraps an existing Configuration structure and ties it to a file on
 // disk.
-func Wrap(path string, cfg Configuration) *Wrapper {
-	w := &Wrapper{
+func Wrap(path string, cfg Configuration) Wrapper {
+	w := &wrapper{
 		cfg:  cfg,
 		path: path,
 		mut:  sync.NewMutex(),
@@ -80,7 +118,7 @@ func Wrap(path string, cfg Configuration) *Wrapper {
 
 // Load loads an existing file on disk and returns a new configuration
 // wrapper.
-func Load(path string, myID protocol.DeviceID) (*Wrapper, error) {
+func Load(path string, myID protocol.DeviceID) (Wrapper, error) {
 	fd, err := os.Open(path)
 	if err != nil {
 		return nil, err
@@ -95,13 +133,13 @@ func Load(path string, myID protocol.DeviceID) (*Wrapper, error) {
 	return Wrap(path, cfg), nil
 }
 
-func (w *Wrapper) ConfigPath() string {
+func (w *wrapper) ConfigPath() string {
 	return w.path
 }
 
 // Subscribe registers the given handler to be called on any future
 // configuration changes.
-func (w *Wrapper) Subscribe(c Committer) {
+func (w *wrapper) Subscribe(c Committer) {
 	w.mut.Lock()
 	w.subs = append(w.subs, c)
 	w.mut.Unlock()
@@ -109,7 +147,7 @@ func (w *Wrapper) Subscribe(c Committer) {
 
 // Unsubscribe de-registers the given handler from any future calls to
 // configuration changes
-func (w *Wrapper) Unsubscribe(c Committer) {
+func (w *wrapper) Unsubscribe(c Committer) {
 	w.mut.Lock()
 	for i := range w.subs {
 		if w.subs[i] == c {
@@ -123,20 +161,20 @@ func (w *Wrapper) Unsubscribe(c Committer) {
 }
 
 // RawCopy returns a copy of the currently wrapped Configuration object.
-func (w *Wrapper) RawCopy() Configuration {
+func (w *wrapper) RawCopy() Configuration {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	return w.cfg.Copy()
 }
 
 // Replace swaps the current configuration object for the given one.
-func (w *Wrapper) Replace(cfg Configuration) (Waiter, error) {
+func (w *wrapper) Replace(cfg Configuration) (Waiter, error) {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	return w.replaceLocked(cfg.Copy())
 }
 
-func (w *Wrapper) replaceLocked(to Configuration) (Waiter, error) {
+func (w *wrapper) replaceLocked(to Configuration) (Waiter, error) {
 	from := w.cfg
 
 	if err := to.clean(); err != nil {
@@ -158,7 +196,7 @@ func (w *Wrapper) replaceLocked(to Configuration) (Waiter, error) {
 	return w.notifyListeners(from.Copy(), to.Copy()), nil
 }
 
-func (w *Wrapper) notifyListeners(from, to Configuration) Waiter {
+func (w *wrapper) notifyListeners(from, to Configuration) Waiter {
 	wg := sync.NewWaitGroup()
 	wg.Add(len(w.subs))
 	for _, sub := range w.subs {
@@ -170,7 +208,7 @@ func (w *Wrapper) notifyListeners(from, to Configuration) Waiter {
 	return wg
 }
 
-func (w *Wrapper) notifyListener(sub Committer, from, to Configuration) {
+func (w *wrapper) notifyListener(sub Committer, from, to Configuration) {
 	l.Debugln(sub, "committing configuration")
 	if !sub.CommitConfiguration(from, to) {
 		l.Debugln(sub, "requires restart")
@@ -179,7 +217,7 @@ func (w *Wrapper) notifyListener(sub Committer, from, to Configuration) {
 }
 
 // Devices returns a map of devices.
-func (w *Wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
+func (w *wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	if w.deviceMap == nil {
@@ -193,7 +231,7 @@ func (w *Wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
 
 // SetDevices adds new devices to the configuration, or overwrites existing
 // devices with the same ID.
-func (w *Wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
+func (w *wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 
@@ -218,12 +256,12 @@ func (w *Wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
 
 // 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) {
+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) {
+func (w *wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 
@@ -240,7 +278,7 @@ func (w *Wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) {
 
 // Folders returns a map of folders. Folder structures should not be changed,
 // other than for the purpose of updating via SetFolder().
-func (w *Wrapper) Folders() map[string]FolderConfiguration {
+func (w *wrapper) Folders() map[string]FolderConfiguration {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	if w.folderMap == nil {
@@ -253,7 +291,7 @@ func (w *Wrapper) Folders() map[string]FolderConfiguration {
 }
 
 // FolderList returns a slice of folders.
-func (w *Wrapper) FolderList() []FolderConfiguration {
+func (w *wrapper) FolderList() []FolderConfiguration {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	return w.cfg.Copy().Folders
@@ -261,7 +299,7 @@ func (w *Wrapper) FolderList() []FolderConfiguration {
 
 // 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) {
+func (w *wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 
@@ -280,14 +318,14 @@ func (w *Wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {
 }
 
 // Options returns the current options configuration object.
-func (w *Wrapper) Options() OptionsConfiguration {
+func (w *wrapper) Options() OptionsConfiguration {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	return w.cfg.Options.Copy()
 }
 
 // SetOptions replaces the current options configuration object.
-func (w *Wrapper) SetOptions(opts OptionsConfiguration) (Waiter, error) {
+func (w *wrapper) SetOptions(opts OptionsConfiguration) (Waiter, error) {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	newCfg := w.cfg.Copy()
@@ -295,21 +333,21 @@ func (w *Wrapper) SetOptions(opts OptionsConfiguration) (Waiter, error) {
 	return w.replaceLocked(newCfg)
 }
 
-func (w *Wrapper) LDAP() LDAPConfiguration {
+func (w *wrapper) LDAP() LDAPConfiguration {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	return w.cfg.LDAP.Copy()
 }
 
 // GUI returns the current GUI configuration object.
-func (w *Wrapper) GUI() GUIConfiguration {
+func (w *wrapper) GUI() GUIConfiguration {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	return w.cfg.GUI.Copy()
 }
 
 // SetGUI replaces the current GUI configuration object.
-func (w *Wrapper) SetGUI(gui GUIConfiguration) (Waiter, error) {
+func (w *wrapper) SetGUI(gui GUIConfiguration) (Waiter, error) {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	newCfg := w.cfg.Copy()
@@ -319,7 +357,7 @@ func (w *Wrapper) SetGUI(gui GUIConfiguration) (Waiter, error) {
 
 // IgnoredDevice returns whether or not connection attempts from the given
 // device should be silently ignored.
-func (w *Wrapper) IgnoredDevice(id protocol.DeviceID) bool {
+func (w *wrapper) IgnoredDevice(id protocol.DeviceID) bool {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	for _, device := range w.cfg.IgnoredDevices {
@@ -332,7 +370,7 @@ func (w *Wrapper) IgnoredDevice(id protocol.DeviceID) bool {
 
 // IgnoredFolder returns whether or not share attempts for the given
 // folder should be silently ignored.
-func (w *Wrapper) IgnoredFolder(device protocol.DeviceID, folder string) bool {
+func (w *wrapper) IgnoredFolder(device protocol.DeviceID, folder string) bool {
 	dev, ok := w.Device(device)
 	if !ok {
 		return false
@@ -341,7 +379,7 @@ func (w *Wrapper) IgnoredFolder(device protocol.DeviceID, folder string) bool {
 }
 
 // Device returns the configuration for the given device and an "ok" bool.
-func (w *Wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
+func (w *wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	for _, device := range w.cfg.Devices {
@@ -353,7 +391,7 @@ func (w *Wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
 }
 
 // Folder returns the configuration for the given folder and an "ok" bool.
-func (w *Wrapper) Folder(id string) (FolderConfiguration, bool) {
+func (w *wrapper) Folder(id string) (FolderConfiguration, bool) {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 	for _, folder := range w.cfg.Folders {
@@ -365,7 +403,7 @@ func (w *Wrapper) Folder(id string) (FolderConfiguration, bool) {
 }
 
 // Save writes the configuration to disk, and generates a ConfigSaved event.
-func (w *Wrapper) Save() error {
+func (w *wrapper) Save() error {
 	w.mut.Lock()
 	defer w.mut.Unlock()
 
@@ -390,7 +428,7 @@ func (w *Wrapper) Save() error {
 	return nil
 }
 
-func (w *Wrapper) GlobalDiscoveryServers() []string {
+func (w *wrapper) GlobalDiscoveryServers() []string {
 	var servers []string
 	for _, srv := range w.Options().GlobalAnnServers {
 		switch srv {
@@ -407,7 +445,7 @@ func (w *Wrapper) GlobalDiscoveryServers() []string {
 	return util.UniqueStrings(servers)
 }
 
-func (w *Wrapper) ListenAddresses() []string {
+func (w *wrapper) ListenAddresses() []string {
 	var addresses []string
 	for _, addr := range w.Options().ListenAddresses {
 		switch addr {
@@ -420,15 +458,15 @@ func (w *Wrapper) ListenAddresses() []string {
 	return util.UniqueStrings(addresses)
 }
 
-func (w *Wrapper) RequiresRestart() bool {
+func (w *wrapper) RequiresRestart() bool {
 	return atomic.LoadUint32(&w.requiresRestart) != 0
 }
 
-func (w *Wrapper) setRequiresRestart() {
+func (w *wrapper) setRequiresRestart() {
 	atomic.StoreUint32(&w.requiresRestart, 1)
 }
 
-func (w *Wrapper) MyName() string {
+func (w *wrapper) MyName() string {
 	w.mut.Lock()
 	myID := w.cfg.MyID
 	w.mut.Unlock()
@@ -436,7 +474,7 @@ func (w *Wrapper) MyName() string {
 	return cfg.Name
 }
 
-func (w *Wrapper) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) {
+func (w *wrapper) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) {
 	defer w.Save()
 
 	w.mut.Lock()
@@ -459,7 +497,7 @@ func (w *Wrapper) AddOrUpdatePendingDevice(device protocol.DeviceID, name, addre
 	})
 }
 
-func (w *Wrapper) AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID) {
+func (w *wrapper) AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID) {
 	defer w.Save()
 
 	w.mut.Lock()

+ 1 - 1
lib/connections/lan_test.go

@@ -36,7 +36,7 @@ func TestIsLANHost(t *testing.T) {
 			AlwaysLocalNets: []string{"10.20.30.0/24"},
 		},
 	})
-	s := &Service{cfg: cfg}
+	s := &service{cfg: cfg}
 
 	for _, tc := range cases {
 		res := s.isLANHost(tc.addr)

+ 1 - 1
lib/connections/limiter.go

@@ -36,7 +36,7 @@ type waiter interface {
 
 const limiterBurstSize = 4 * 128 << 10
 
-func newLimiter(cfg *config.Wrapper) *limiter {
+func newLimiter(cfg config.Wrapper) *limiter {
 	l := &limiter{
 		write:               rate.NewLimiter(rate.Inf, limiterBurstSize),
 		read:                rate.NewLimiter(rate.Inf, limiterBurstSize),

+ 1 - 1
lib/connections/limiter_test.go

@@ -24,7 +24,7 @@ func init() {
 	device4, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
 }
 
-func initConfig() *config.Wrapper {
+func initConfig() config.Wrapper {
 	cfg := config.Wrap("/dev/null", config.New(device1))
 	dev1Conf = config.NewDeviceConfiguration(device1, "device1")
 	dev2Conf = config.NewDeviceConfiguration(device2, "device2")

+ 2 - 2
lib/connections/relay_dial.go

@@ -22,7 +22,7 @@ func init() {
 }
 
 type relayDialer struct {
-	cfg    *config.Wrapper
+	cfg    config.Wrapper
 	tlsCfg *tls.Config
 }
 
@@ -70,7 +70,7 @@ func (d *relayDialer) RedialFrequency() time.Duration {
 
 type relayDialerFactory struct{}
 
-func (relayDialerFactory) New(cfg *config.Wrapper, tlsCfg *tls.Config) genericDialer {
+func (relayDialerFactory) New(cfg config.Wrapper, tlsCfg *tls.Config) genericDialer {
 	return &relayDialer{
 		cfg:    cfg,
 		tlsCfg: tlsCfg,

+ 2 - 2
lib/connections/relay_listen.go

@@ -29,7 +29,7 @@ type relayListener struct {
 	onAddressesChangedNotifier
 
 	uri     *url.URL
-	cfg     *config.Wrapper
+	cfg     config.Wrapper
 	tlsCfg  *tls.Config
 	conns   chan internalConn
 	factory listenerFactory
@@ -180,7 +180,7 @@ func (t *relayListener) NATType() string {
 
 type relayListenerFactory struct{}
 
-func (f *relayListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
+func (f *relayListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
 	return &relayListener{
 		uri:     uri,
 		cfg:     cfg,

+ 23 - 17
lib/connections/service.go

@@ -77,9 +77,15 @@ var tlsCipherSuiteNames = map[uint16]string{
 
 // Service listens and dials all configured unconnected devices, via supported
 // dialers. Successful connections are handed to the model.
-type Service struct {
+type Service interface {
+	suture.Service
+	Status() map[string]interface{}
+	NATType() string
+}
+
+type service struct {
 	*suture.Supervisor
-	cfg                  *config.Wrapper
+	cfg                  config.Wrapper
 	myID                 protocol.DeviceID
 	model                Model
 	tlsCfg               *tls.Config
@@ -97,10 +103,10 @@ type Service struct {
 	listenerSupervisor *suture.Supervisor
 }
 
-func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder,
-	bepProtocolName string, tlsDefaultCommonName string) *Service {
+func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder,
+	bepProtocolName string, tlsDefaultCommonName string) *service {
 
-	service := &Service{
+	service := &service{
 		Supervisor: suture.New("connections.Service", suture.Spec{
 			Log: func(line string) {
 				l.Infoln(line)
@@ -156,7 +162,7 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
 	return service
 }
 
-func (s *Service) handle() {
+func (s *service) handle() {
 next:
 	for c := range s.conns {
 		cs := c.ConnectionState()
@@ -282,7 +288,7 @@ next:
 	}
 }
 
-func (s *Service) connect() {
+func (s *service) connect() {
 	nextDial := make(map[string]time.Time)
 
 	// Used as delay for the first few connection attempts, increases
@@ -433,7 +439,7 @@ func (s *Service) connect() {
 	}
 }
 
-func (s *Service) isLANHost(host string) bool {
+func (s *service) isLANHost(host string) bool {
 	// Probably we are called with an ip:port combo which we can resolve as
 	// a TCP address.
 	if addr, err := net.ResolveTCPAddr("tcp", host); err == nil {
@@ -447,7 +453,7 @@ func (s *Service) isLANHost(host string) bool {
 	return false
 }
 
-func (s *Service) isLAN(addr net.Addr) bool {
+func (s *service) isLAN(addr net.Addr) bool {
 	var ip net.IP
 
 	switch addr := addr.(type) {
@@ -488,7 +494,7 @@ func (s *Service) isLAN(addr net.Addr) bool {
 	return false
 }
 
-func (s *Service) createListener(factory listenerFactory, uri *url.URL) bool {
+func (s *service) createListener(factory listenerFactory, uri *url.URL) bool {
 	// must be called with listenerMut held
 
 	l.Debugln("Starting listener", uri)
@@ -500,7 +506,7 @@ func (s *Service) createListener(factory listenerFactory, uri *url.URL) bool {
 	return true
 }
 
-func (s *Service) logListenAddressesChangedEvent(l genericListener) {
+func (s *service) logListenAddressesChangedEvent(l genericListener) {
 	events.Default.Log(events.ListenAddressesChanged, map[string]interface{}{
 		"address": l.URI(),
 		"lan":     l.LANAddresses(),
@@ -508,11 +514,11 @@ func (s *Service) logListenAddressesChangedEvent(l genericListener) {
 	})
 }
 
-func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
+func (s *service) VerifyConfiguration(from, to config.Configuration) error {
 	return nil
 }
 
-func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
+func (s *service) CommitConfiguration(from, to config.Configuration) bool {
 	newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
 	for _, dev := range to.Devices {
 		newDevices[dev.DeviceID] = true
@@ -589,7 +595,7 @@ func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
 	return true
 }
 
-func (s *Service) AllAddresses() []string {
+func (s *service) AllAddresses() []string {
 	s.listenersMut.RLock()
 	var addrs []string
 	for _, listener := range s.listeners {
@@ -604,7 +610,7 @@ func (s *Service) AllAddresses() []string {
 	return util.UniqueStrings(addrs)
 }
 
-func (s *Service) ExternalAddresses() []string {
+func (s *service) ExternalAddresses() []string {
 	s.listenersMut.RLock()
 	var addrs []string
 	for _, listener := range s.listeners {
@@ -616,7 +622,7 @@ func (s *Service) ExternalAddresses() []string {
 	return util.UniqueStrings(addrs)
 }
 
-func (s *Service) Status() map[string]interface{} {
+func (s *service) Status() map[string]interface{} {
 	s.listenersMut.RLock()
 	result := make(map[string]interface{})
 	for addr, listener := range s.listeners {
@@ -636,7 +642,7 @@ func (s *Service) Status() map[string]interface{} {
 	return result
 }
 
-func (s *Service) NATType() string {
+func (s *service) NATType() string {
 	s.listenersMut.RLock()
 	defer s.listenersMut.RUnlock()
 	for _, listener := range s.listeners {

+ 2 - 2
lib/connections/structs.go

@@ -122,7 +122,7 @@ func (c internalConn) String() string {
 }
 
 type dialerFactory interface {
-	New(*config.Wrapper, *tls.Config) genericDialer
+	New(config.Wrapper, *tls.Config) genericDialer
 	Priority() int
 	AlwaysWAN() bool
 	Valid(config.Configuration) error
@@ -135,7 +135,7 @@ type genericDialer interface {
 }
 
 type listenerFactory interface {
-	New(*url.URL, *config.Wrapper, *tls.Config, chan internalConn, *nat.Service) genericListener
+	New(*url.URL, config.Wrapper, *tls.Config, chan internalConn, *nat.Service) genericListener
 	Valid(config.Configuration) error
 }
 

+ 2 - 2
lib/connections/tcp_dial.go

@@ -24,7 +24,7 @@ func init() {
 }
 
 type tcpDialer struct {
-	cfg    *config.Wrapper
+	cfg    config.Wrapper
 	tlsCfg *tls.Config
 }
 
@@ -62,7 +62,7 @@ func (d *tcpDialer) RedialFrequency() time.Duration {
 
 type tcpDialerFactory struct{}
 
-func (tcpDialerFactory) New(cfg *config.Wrapper, tlsCfg *tls.Config) genericDialer {
+func (tcpDialerFactory) New(cfg config.Wrapper, tlsCfg *tls.Config) genericDialer {
 	return &tcpDialer{
 		cfg:    cfg,
 		tlsCfg: tlsCfg,

+ 2 - 2
lib/connections/tcp_listen.go

@@ -29,7 +29,7 @@ type tcpListener struct {
 	onAddressesChangedNotifier
 
 	uri     *url.URL
-	cfg     *config.Wrapper
+	cfg     config.Wrapper
 	tlsCfg  *tls.Config
 	stop    chan struct{}
 	conns   chan internalConn
@@ -195,7 +195,7 @@ func (t *tcpListener) NATType() string {
 
 type tcpListenerFactory struct{}
 
-func (f *tcpListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
+func (f *tcpListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
 	return &tcpListener{
 		uri:        fixupPort(uri, config.DefaultTCPPort),
 		cfg:        cfg,

+ 2 - 2
lib/model/folder.go

@@ -39,7 +39,7 @@ type folder struct {
 	config.FolderConfiguration
 	localFlags uint32
 
-	model   *Model
+	model   *model
 	shortID protocol.ShortID
 	ctx     context.Context
 	cancel  context.CancelFunc
@@ -73,7 +73,7 @@ type puller interface {
 	pull() bool // true when successfull and should not be retried
 }
 
-func newFolder(model *Model, cfg config.FolderConfiguration) folder {
+func newFolder(model *model, cfg config.FolderConfiguration) folder {
 	ctx, cancel := context.WithCancel(context.Background())
 
 	return folder{

+ 1 - 1
lib/model/folder_recvonly.go

@@ -56,7 +56,7 @@ type receiveOnlyFolder struct {
 	*sendReceiveFolder
 }
 
-func newReceiveOnlyFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem) service {
+func newReceiveOnlyFolder(model *model, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem) service {
 	sr := newSendReceiveFolder(model, cfg, ver, fs).(*sendReceiveFolder)
 	sr.localFlags = protocol.FlagLocalReceiveOnly // gets propagated to the scanner, and set on locally changed files
 	return &receiveOnlyFolder{sr}

+ 2 - 2
lib/model/folder_recvonly_test.go

@@ -336,7 +336,7 @@ func setupKnownFiles(t *testing.T, data []byte) []protocol.FileInfo {
 	return knownFiles
 }
 
-func setupROFolder() *Model {
+func setupROFolder() *model {
 	fcfg := config.NewFolderConfiguration(myID, "ro", "receive only test", fs.FilesystemTypeBasic, "_recvonly")
 	fcfg.Type = config.FolderTypeReceiveOnly
 	fcfg.Devices = []config.FolderDeviceConfiguration{{DeviceID: device1}}
@@ -349,7 +349,7 @@ func setupROFolder() *Model {
 	wrp := createTmpWrapper(cfg)
 
 	db := db.OpenMemory()
-	m := NewModel(wrp, myID, "syncthing", "dev", db, nil)
+	m := newModel(wrp, myID, "syncthing", "dev", db, nil)
 
 	m.ServeBackground()
 	m.AddFolder(fcfg)

+ 1 - 1
lib/model/folder_sendonly.go

@@ -22,7 +22,7 @@ type sendOnlyFolder struct {
 	folder
 }
 
-func newSendOnlyFolder(model *Model, cfg config.FolderConfiguration, _ versioner.Versioner, _ fs.Filesystem) service {
+func newSendOnlyFolder(model *model, cfg config.FolderConfiguration, _ versioner.Versioner, _ fs.Filesystem) service {
 	f := &sendOnlyFolder{
 		folder: newFolder(model, cfg),
 	}

+ 1 - 1
lib/model/folder_sendrecv.go

@@ -104,7 +104,7 @@ type sendReceiveFolder struct {
 	pullErrorsMut sync.Mutex
 }
 
-func newSendReceiveFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem) service {
+func newSendReceiveFolder(model *model, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem) service {
 	f := &sendReceiveFolder{
 		folder:        newFolder(model, cfg),
 		fs:            fs,

+ 2 - 2
lib/model/folder_sendrecv_test.go

@@ -75,9 +75,9 @@ func setUpFile(filename string, blockNumbers []int) protocol.FileInfo {
 	}
 }
 
-func setupSendReceiveFolder(files ...protocol.FileInfo) (*Model, *sendReceiveFolder, string) {
+func setupSendReceiveFolder(files ...protocol.FileInfo) (*model, *sendReceiveFolder, string) {
 	w := createTmpWrapper(defaultCfg)
-	model := NewModel(w, myID, "syncthing", "dev", db.OpenMemory(), nil)
+	model := newModel(w, myID, "syncthing", "dev", db.OpenMemory(), nil)
 	fcfg, tmpDir := testFolderConfigTmp()
 	model.AddFolder(fcfg)
 

+ 132 - 82
lib/model/model.go

@@ -76,10 +76,60 @@ type Availability struct {
 	FromTemporary bool              `json:"fromTemporary"`
 }
 
-type Model struct {
+type Model interface {
+	suture.Service
+
+	connections.Model
+
+	AddFolder(cfg config.FolderConfiguration)
+	RestartFolder(from, to config.FolderConfiguration)
+	StartFolder(folder string)
+	ResetFolder(folder string)
+	DelayScan(folder string, next time.Duration)
+	ScanFolder(folder string) error
+	ScanFolders() map[string]error
+	ScanFolderSubdirs(folder string, subs []string) error
+	State(folder string) (string, time.Time, error)
+	FolderErrors(folder string) ([]FileError, error)
+	WatchError(folder string) error
+	Override(folder string)
+	Revert(folder string)
+	BringToFront(folder, file string)
+	GetIgnores(folder string) ([]string, []string, error)
+	SetIgnores(folder string, content []string) error
+
+	GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error)
+	RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error)
+
+	LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated
+	NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated)
+	RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error)
+	CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool)
+	CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool)
+	Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability
+
+	GlobalSize(folder string) db.Counts
+	LocalSize(folder string) db.Counts
+	NeedSize(folder string) db.Counts
+	ReceiveOnlyChangedSize(folder string) db.Counts
+
+	CurrentSequence(folder string) (int64, bool)
+	RemoteSequence(folder string) (int64, bool)
+
+	Completion(device protocol.DeviceID, folder string) FolderCompletion
+	ConnectionStats() map[string]interface{}
+	DeviceStatistics() map[string]stats.DeviceStatistics
+	FolderStatistics() map[string]stats.FolderStatistics
+	UsageReportingStats(version int, preview bool) map[string]interface{}
+
+	StartDeadlockDetector(timeout time.Duration)
+	GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
+}
+
+type model struct {
 	*suture.Supervisor
 
-	cfg               *config.Wrapper
+	cfg               config.Wrapper
 	db                *db.Lowlevel
 	finder            *db.BlockFinder
 	progressEmitter   *ProgressEmitter
@@ -112,7 +162,7 @@ type Model struct {
 	foldersRunning int32 // for testing only
 }
 
-type folderFactory func(*Model, config.FolderConfiguration, versioner.Versioner, fs.Filesystem) service
+type folderFactory func(*model, config.FolderConfiguration, versioner.Versioner, fs.Filesystem) service
 
 var (
 	folderFactories = make(map[config.FolderType]folderFactory)
@@ -134,8 +184,8 @@ var (
 // NewModel creates and starts a new model. The model starts in read-only mode,
 // where it sends index information to connected peers and responds to requests
 // for file data without altering the local folder in any way.
-func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string) *Model {
-	m := &Model{
+func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string) Model {
+	m := &model{
 		Supervisor: suture.New("model", suture.Spec{
 			Log: func(line string) {
 				l.Debugln(line)
@@ -180,7 +230,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersi
 // StartDeadlockDetector starts a deadlock detector on the models locks which
 // causes panics in case the locks cannot be acquired in the given timeout
 // period.
-func (m *Model) StartDeadlockDetector(timeout time.Duration) {
+func (m *model) StartDeadlockDetector(timeout time.Duration) {
 	l.Infof("Starting deadlock detector with %v timeout", timeout)
 	detector := newDeadlockDetector(timeout)
 	detector.Watch("fmut", m.fmut)
@@ -188,7 +238,7 @@ func (m *Model) StartDeadlockDetector(timeout time.Duration) {
 }
 
 // StartFolder constructs the folder service and starts it.
-func (m *Model) StartFolder(folder string) {
+func (m *model) StartFolder(folder string) {
 	m.fmut.Lock()
 	m.pmut.Lock()
 	folderType := m.startFolderLocked(folder)
@@ -199,7 +249,7 @@ func (m *Model) StartFolder(folder string) {
 	l.Infof("Ready to synchronize %s (%s)", folderCfg.Description(), folderType)
 }
 
-func (m *Model) startFolderLocked(folder string) config.FolderType {
+func (m *model) startFolderLocked(folder string) config.FolderType {
 	if err := m.checkFolderRunningLocked(folder); err == errFolderMissing {
 		panic("cannot start nonexistent folder " + folder)
 	} else if err == nil {
@@ -274,7 +324,7 @@ func (m *Model) startFolderLocked(folder string) config.FolderType {
 	return cfg.Type
 }
 
-func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
+func (m *model) warnAboutOverwritingProtectedFiles(folder string) {
 	if m.folderCfgs[folder].Type == config.FolderTypeSendOnly {
 		return
 	}
@@ -308,7 +358,7 @@ func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
 	}
 }
 
-func (m *Model) AddFolder(cfg config.FolderConfiguration) {
+func (m *model) AddFolder(cfg config.FolderConfiguration) {
 	if len(cfg.ID) == 0 {
 		panic("cannot add empty folder id")
 	}
@@ -322,7 +372,7 @@ func (m *Model) AddFolder(cfg config.FolderConfiguration) {
 	m.fmut.Unlock()
 }
 
-func (m *Model) addFolderLocked(cfg config.FolderConfiguration) {
+func (m *model) addFolderLocked(cfg config.FolderConfiguration) {
 	m.folderCfgs[cfg.ID] = cfg
 	folderFs := cfg.Filesystem()
 	m.folderFiles[cfg.ID] = db.NewFileSet(cfg.ID, folderFs, m.db)
@@ -334,7 +384,7 @@ func (m *Model) addFolderLocked(cfg config.FolderConfiguration) {
 	m.folderIgnores[cfg.ID] = ignores
 }
 
-func (m *Model) RemoveFolder(cfg config.FolderConfiguration) {
+func (m *model) RemoveFolder(cfg config.FolderConfiguration) {
 	m.fmut.Lock()
 	m.pmut.Lock()
 	// Delete syncthing specific files
@@ -348,7 +398,7 @@ func (m *Model) RemoveFolder(cfg config.FolderConfiguration) {
 	m.fmut.Unlock()
 }
 
-func (m *Model) tearDownFolderLocked(cfg config.FolderConfiguration, err error) {
+func (m *model) tearDownFolderLocked(cfg config.FolderConfiguration, err error) {
 	// Close connections to affected devices
 	// Must happen before stopping the folder service to abort ongoing
 	// transmissions and thus allow timely service termination.
@@ -376,7 +426,7 @@ func (m *Model) tearDownFolderLocked(cfg config.FolderConfiguration, err error)
 	delete(m.folderStatRefs, cfg.ID)
 }
 
-func (m *Model) RestartFolder(from, to config.FolderConfiguration) {
+func (m *model) RestartFolder(from, to config.FolderConfiguration) {
 	if len(to.ID) == 0 {
 		panic("bug: cannot restart empty folder ID")
 	}
@@ -421,7 +471,7 @@ func (m *Model) RestartFolder(from, to config.FolderConfiguration) {
 	l.Infof("%v folder %v (%v)", infoMsg, to.Description(), to.Type)
 }
 
-func (m *Model) UsageReportingStats(version int, preview bool) map[string]interface{} {
+func (m *model) UsageReportingStats(version int, preview bool) map[string]interface{} {
 	stats := make(map[string]interface{})
 	if version >= 3 {
 		// Block stats
@@ -540,7 +590,7 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
 }
 
 // ConnectionStats returns a map with connection statistics for each device.
-func (m *Model) ConnectionStats() map[string]interface{} {
+func (m *model) ConnectionStats() map[string]interface{} {
 	m.fmut.RLock()
 	m.pmut.RLock()
 
@@ -587,7 +637,7 @@ func (m *Model) ConnectionStats() map[string]interface{} {
 }
 
 // DeviceStatistics returns statistics about each device
-func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
+func (m *model) DeviceStatistics() map[string]stats.DeviceStatistics {
 	res := make(map[string]stats.DeviceStatistics)
 	for id := range m.cfg.Devices() {
 		res[id.String()] = m.deviceStatRef(id).GetStatistics()
@@ -596,7 +646,7 @@ func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
 }
 
 // FolderStatistics returns statistics about each folder
-func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
+func (m *model) FolderStatistics() map[string]stats.FolderStatistics {
 	res := make(map[string]stats.FolderStatistics)
 	for id := range m.cfg.Folders() {
 		res[id] = m.folderStatRef(id).GetStatistics()
@@ -614,7 +664,7 @@ type FolderCompletion struct {
 
 // Completion returns the completion status, in percent, for the given device
 // and folder.
-func (m *Model) Completion(device protocol.DeviceID, folder string) FolderCompletion {
+func (m *model) Completion(device protocol.DeviceID, folder string) FolderCompletion {
 	m.fmut.RLock()
 	rf, ok := m.folderFiles[folder]
 	m.fmut.RUnlock()
@@ -696,7 +746,7 @@ func addSizeOfFile(s *db.Counts, f db.FileIntf) {
 
 // GlobalSize returns the number of files, deleted files and total bytes for all
 // files in the global model.
-func (m *Model) GlobalSize(folder string) db.Counts {
+func (m *model) GlobalSize(folder string) db.Counts {
 	m.fmut.RLock()
 	defer m.fmut.RUnlock()
 	if rf, ok := m.folderFiles[folder]; ok {
@@ -707,7 +757,7 @@ func (m *Model) GlobalSize(folder string) db.Counts {
 
 // LocalSize returns the number of files, deleted files and total bytes for all
 // files in the local folder.
-func (m *Model) LocalSize(folder string) db.Counts {
+func (m *model) LocalSize(folder string) db.Counts {
 	m.fmut.RLock()
 	defer m.fmut.RUnlock()
 	if rf, ok := m.folderFiles[folder]; ok {
@@ -719,7 +769,7 @@ func (m *Model) LocalSize(folder string) db.Counts {
 // ReceiveOnlyChangedSize returns the number of files, deleted files and
 // total bytes for all files that have changed locally in a receieve only
 // folder.
-func (m *Model) ReceiveOnlyChangedSize(folder string) db.Counts {
+func (m *model) ReceiveOnlyChangedSize(folder string) db.Counts {
 	m.fmut.RLock()
 	defer m.fmut.RUnlock()
 	if rf, ok := m.folderFiles[folder]; ok {
@@ -729,7 +779,7 @@ func (m *Model) ReceiveOnlyChangedSize(folder string) db.Counts {
 }
 
 // NeedSize returns the number and total size of currently needed files.
-func (m *Model) NeedSize(folder string) db.Counts {
+func (m *model) NeedSize(folder string) db.Counts {
 	m.fmut.RLock()
 	defer m.fmut.RUnlock()
 
@@ -753,7 +803,7 @@ func (m *Model) NeedSize(folder string) db.Counts {
 // NeedFolderFiles returns paginated list of currently needed files in
 // progress, queued, and to be queued on next puller iteration, as well as the
 // total number of files currently needed.
-func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) {
+func (m *model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) {
 	m.fmut.RLock()
 	defer m.fmut.RUnlock()
 
@@ -820,7 +870,7 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
 // LocalChangedFiles returns a paginated list of currently needed files in
 // progress, queued, and to be queued on next puller iteration, as well as the
 // total number of files currently needed.
-func (m *Model) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
+func (m *model) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
 	m.fmut.RLock()
 	defer m.fmut.RUnlock()
 
@@ -861,7 +911,7 @@ func (m *Model) LocalChangedFiles(folder string, page, perpage int) []db.FileInf
 // RemoteNeedFolderFiles returns paginated list of currently needed files in
 // progress, queued, and to be queued on next puller iteration, as well as the
 // total number of files currently needed.
-func (m *Model) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error) {
+func (m *model) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error) {
 	m.fmut.RLock()
 	m.pmut.RLock()
 	if err := m.checkDeviceFolderConnectedLocked(device, folder); err != nil {
@@ -891,17 +941,17 @@ func (m *Model) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, p
 
 // Index is called when a new device is connected and we receive their full index.
 // Implements the protocol.Model interface.
-func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo) {
+func (m *model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo) {
 	m.handleIndex(deviceID, folder, fs, false)
 }
 
 // IndexUpdate is called for incremental updates to connected devices' indexes.
 // Implements the protocol.Model interface.
-func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo) {
+func (m *model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo) {
 	m.handleIndex(deviceID, folder, fs, true)
 }
 
-func (m *Model) handleIndex(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo, update bool) {
+func (m *model) handleIndex(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo, update bool) {
 	op := "Index"
 	if update {
 		op += " update"
@@ -956,7 +1006,7 @@ func (m *Model) handleIndex(deviceID protocol.DeviceID, folder string, fs []prot
 	})
 }
 
-func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterConfig) {
+func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterConfig) {
 	// Check the peer device's announced folders against our own. Emits events
 	// for folders that we don't expect (unknown or not shared).
 	// Also, collect a list of folders we do share, and if he's interested in
@@ -1136,7 +1186,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
 }
 
 // handleIntroductions handles adding devices/shares that are shared by an introducer device
-func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig) (folderDeviceSet, bool) {
+func (m *model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig) (folderDeviceSet, bool) {
 	// This device is an introducer. Go through the announced lists of folders
 	// and devices and add what we are missing, remove what we have extra that
 	// has been introducer by the introducer.
@@ -1192,7 +1242,7 @@ func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
 }
 
 // handleDeintroductions handles removals of devices/shares that are removed by an introducer device
-func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig, foldersDevices folderDeviceSet) bool {
+func (m *model) handleDeintroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig, foldersDevices folderDeviceSet) bool {
 	changed := false
 	devicesNotIntroduced := make(map[protocol.DeviceID]struct{})
 
@@ -1249,7 +1299,7 @@ func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
 
 // handleAutoAccepts handles adding and sharing folders for devices that have
 // AutoAcceptFolders set to true.
-func (m *Model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder protocol.Folder) bool {
+func (m *model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder protocol.Folder) bool {
 	if cfg, ok := m.cfg.Folder(folder.ID); !ok {
 		defaultPath := m.cfg.Options().DefaultFolderPath
 		defaultPathFs := fs.NewFilesystem(fs.FilesystemTypeBasic, defaultPath)
@@ -1295,7 +1345,7 @@ func (m *Model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder p
 	}
 }
 
-func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) {
+func (m *model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) {
 	addresses := []string{"dynamic"}
 	for _, addr := range device.Addresses {
 		if addr != "dynamic" {
@@ -1324,7 +1374,7 @@ func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.Dev
 }
 
 // Closed is called when a connection has been closed
-func (m *Model) Closed(conn protocol.Connection, err error) {
+func (m *model) Closed(conn protocol.Connection, err error) {
 	device := conn.ID()
 
 	m.pmut.Lock()
@@ -1350,14 +1400,14 @@ func (m *Model) Closed(conn protocol.Connection, err error) {
 }
 
 // close will close the underlying connection for a given device
-func (m *Model) close(device protocol.DeviceID, err error) {
+func (m *model) close(device protocol.DeviceID, err error) {
 	m.pmut.Lock()
 	m.closeLocked(device, err)
 	m.pmut.Unlock()
 }
 
 // closeLocked will close the underlying connection for a given device
-func (m *Model) closeLocked(device protocol.DeviceID, err error) {
+func (m *model) closeLocked(device protocol.DeviceID, err error) {
 	conn, ok := m.conn[device]
 	if !ok {
 		// There is no connection to close
@@ -1398,7 +1448,7 @@ func (r *requestResponse) Wait() {
 
 // Request returns the specified data segment by reading it from local disk.
 // Implements the protocol.Model interface.
-func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (out protocol.RequestResponse, err error) {
+func (m *model) Request(deviceID protocol.DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (out protocol.RequestResponse, err error) {
 	if size < 0 || offset < 0 {
 		return nil, protocol.ErrInvalid
 	}
@@ -1519,7 +1569,7 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, size in
 	return res, nil
 }
 
-func (m *Model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem, folder, name string, blockIndex int, hash []byte) {
+func (m *model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem, folder, name string, blockIndex int, hash []byte) {
 	cf, ok := m.CurrentFolderFile(folder, name)
 	if !ok {
 		l.Debugf("%v recheckFile: %s: %q / %q: no current file", m, deviceID, folder, name)
@@ -1560,7 +1610,7 @@ func (m *Model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem,
 	}
 }
 
-func (m *Model) CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
+func (m *model) CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
 	m.fmut.RLock()
 	fs, ok := m.folderFiles[folder]
 	m.fmut.RUnlock()
@@ -1570,7 +1620,7 @@ func (m *Model) CurrentFolderFile(folder string, file string) (protocol.FileInfo
 	return fs.Get(protocol.LocalDeviceID, file)
 }
 
-func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool) {
+func (m *model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool) {
 	m.fmut.RLock()
 	fs, ok := m.folderFiles[folder]
 	m.fmut.RUnlock()
@@ -1581,7 +1631,7 @@ func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo
 }
 
 type cFiler struct {
-	m *Model
+	m Model
 	r string
 }
 
@@ -1591,7 +1641,7 @@ func (cf cFiler) CurrentFile(file string) (protocol.FileInfo, bool) {
 }
 
 // Connection returns the current connection for device, and a boolean whether a connection was found.
-func (m *Model) Connection(deviceID protocol.DeviceID) (connections.Connection, bool) {
+func (m *model) Connection(deviceID protocol.DeviceID) (connections.Connection, bool) {
 	m.pmut.RLock()
 	cn, ok := m.conn[deviceID]
 	m.pmut.RUnlock()
@@ -1601,7 +1651,7 @@ func (m *Model) Connection(deviceID protocol.DeviceID) (connections.Connection,
 	return cn, ok
 }
 
-func (m *Model) GetIgnores(folder string) ([]string, []string, error) {
+func (m *model) GetIgnores(folder string) ([]string, []string, error) {
 	m.fmut.RLock()
 	defer m.fmut.RUnlock()
 
@@ -1630,7 +1680,7 @@ func (m *Model) GetIgnores(folder string) ([]string, []string, error) {
 	return ignores.Lines(), ignores.Patterns(), nil
 }
 
-func (m *Model) SetIgnores(folder string, content []string) error {
+func (m *model) SetIgnores(folder string, content []string) error {
 	cfg, ok := m.cfg.Folders()[folder]
 	if !ok {
 		return fmt.Errorf("folder %s does not exist", cfg.Description())
@@ -1664,7 +1714,7 @@ func (m *Model) SetIgnores(folder string, content []string) error {
 // OnHello is called when an device connects to us.
 // This allows us to extract some information from the Hello message
 // and add it to a list of known devices ahead of any checks.
-func (m *Model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protocol.HelloResult) error {
+func (m *model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protocol.HelloResult) error {
 	if m.cfg.IgnoredDevice(remoteID) {
 		return errDeviceIgnored
 	}
@@ -1694,7 +1744,7 @@ func (m *Model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protoco
 }
 
 // GetHello is called when we are about to connect to some remote device.
-func (m *Model) GetHello(id protocol.DeviceID) protocol.HelloIntf {
+func (m *model) GetHello(id protocol.DeviceID) protocol.HelloIntf {
 	name := ""
 	if _, ok := m.cfg.Device(id); ok {
 		name = m.cfg.MyName()
@@ -1709,7 +1759,7 @@ func (m *Model) GetHello(id protocol.DeviceID) protocol.HelloIntf {
 // AddConnection adds a new peer connection to the model. An initial index will
 // be sent to the connected peer, thereafter index updates whenever the local
 // folder changes.
-func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloResult) {
+func (m *model) AddConnection(conn connections.Connection, hello protocol.HelloResult) {
 	deviceID := conn.ID()
 	device, ok := m.cfg.Device(deviceID)
 	if !ok {
@@ -1778,7 +1828,7 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
 	m.deviceWasSeen(deviceID)
 }
 
-func (m *Model) DownloadProgress(device protocol.DeviceID, folder string, updates []protocol.FileDownloadProgressUpdate) {
+func (m *model) DownloadProgress(device protocol.DeviceID, folder string, updates []protocol.FileDownloadProgressUpdate) {
 	m.fmut.RLock()
 	cfg, ok := m.folderCfgs[folder]
 	m.fmut.RUnlock()
@@ -1799,7 +1849,7 @@ func (m *Model) DownloadProgress(device protocol.DeviceID, folder string, update
 	})
 }
 
-func (m *Model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatisticsReference {
+func (m *model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatisticsReference {
 	m.fmut.Lock()
 	defer m.fmut.Unlock()
 
@@ -1812,11 +1862,11 @@ func (m *Model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatistic
 	return sr
 }
 
-func (m *Model) deviceWasSeen(deviceID protocol.DeviceID) {
+func (m *model) deviceWasSeen(deviceID protocol.DeviceID) {
 	m.deviceStatRef(deviceID).WasSeen()
 }
 
-func (m *Model) folderStatRef(folder string) *stats.FolderStatisticsReference {
+func (m *model) folderStatRef(folder string) *stats.FolderStatisticsReference {
 	m.fmut.Lock()
 	defer m.fmut.Unlock()
 
@@ -1828,7 +1878,7 @@ func (m *Model) folderStatRef(folder string) *stats.FolderStatisticsReference {
 	return sr
 }
 
-func (m *Model) receivedFile(folder string, file protocol.FileInfo) {
+func (m *model) receivedFile(folder string, file protocol.FileInfo) {
 	m.folderStatRef(folder).ReceivedFile(file.Name, file.IsDeleted())
 }
 
@@ -1949,7 +1999,7 @@ func sendIndexTo(prevSequence int64, conn protocol.Connection, folder string, fs
 	return f.Sequence, err
 }
 
-func (m *Model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo) {
+func (m *model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo) {
 	m.updateLocals(folder, fs)
 
 	m.fmut.RLock()
@@ -1959,7 +2009,7 @@ func (m *Model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo)
 	m.diskChangeDetected(folderCfg, fs, events.LocalChangeDetected)
 }
 
-func (m *Model) updateLocalsFromPulling(folder string, fs []protocol.FileInfo) {
+func (m *model) updateLocalsFromPulling(folder string, fs []protocol.FileInfo) {
 	m.updateLocals(folder, fs)
 
 	m.fmut.RLock()
@@ -1969,7 +2019,7 @@ func (m *Model) updateLocalsFromPulling(folder string, fs []protocol.FileInfo) {
 	m.diskChangeDetected(folderCfg, fs, events.RemoteChangeDetected)
 }
 
-func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
+func (m *model) updateLocals(folder string, fs []protocol.FileInfo) {
 	m.fmut.RLock()
 	files := m.folderFiles[folder]
 	m.fmut.RUnlock()
@@ -1992,7 +2042,7 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
 	})
 }
 
-func (m *Model) diskChangeDetected(folderCfg config.FolderConfiguration, files []protocol.FileInfo, typeOfEvent events.EventType) {
+func (m *model) diskChangeDetected(folderCfg config.FolderConfiguration, files []protocol.FileInfo, typeOfEvent events.EventType) {
 	for _, file := range files {
 		if file.IsInvalid() {
 			continue
@@ -2035,7 +2085,7 @@ func (m *Model) diskChangeDetected(folderCfg config.FolderConfiguration, files [
 	}
 }
 
-func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
+func (m *model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
 	m.pmut.RLock()
 	nc, ok := m.conn[deviceID]
 	m.pmut.RUnlock()
@@ -2049,7 +2099,7 @@ func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, o
 	return nc.Request(folder, name, offset, size, hash, weakHash, fromTemporary)
 }
 
-func (m *Model) ScanFolders() map[string]error {
+func (m *model) ScanFolders() map[string]error {
 	m.fmut.RLock()
 	folders := make([]string, 0, len(m.folderCfgs))
 	for folder := range m.folderCfgs {
@@ -2087,11 +2137,11 @@ func (m *Model) ScanFolders() map[string]error {
 	return errors
 }
 
-func (m *Model) ScanFolder(folder string) error {
+func (m *model) ScanFolder(folder string) error {
 	return m.ScanFolderSubdirs(folder, nil)
 }
 
-func (m *Model) ScanFolderSubdirs(folder string, subs []string) error {
+func (m *model) ScanFolderSubdirs(folder string, subs []string) error {
 	m.fmut.RLock()
 	if err := m.checkFolderRunningLocked(folder); err != nil {
 		m.fmut.RUnlock()
@@ -2103,7 +2153,7 @@ func (m *Model) ScanFolderSubdirs(folder string, subs []string) error {
 	return runner.Scan(subs)
 }
 
-func (m *Model) DelayScan(folder string, next time.Duration) {
+func (m *model) DelayScan(folder string, next time.Duration) {
 	m.fmut.Lock()
 	runner, ok := m.folderRunners[folder]
 	m.fmut.Unlock()
@@ -2115,7 +2165,7 @@ func (m *Model) DelayScan(folder string, next time.Duration) {
 
 // numHashers returns the number of hasher routines to use for a given folder,
 // taking into account configuration and available CPU cores.
-func (m *Model) numHashers(folder string) int {
+func (m *model) numHashers(folder string) int {
 	m.fmut.Lock()
 	folderCfg := m.folderCfgs[folder]
 	numFolders := len(m.folderCfgs)
@@ -2144,7 +2194,7 @@ func (m *Model) numHashers(folder string) int {
 
 // generateClusterConfig returns a ClusterConfigMessage that is correct for
 // the given peer device
-func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.ClusterConfig {
+func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.ClusterConfig {
 	var message protocol.ClusterConfig
 
 	m.fmut.RLock()
@@ -2201,7 +2251,7 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
 	return message
 }
 
-func (m *Model) State(folder string) (string, time.Time, error) {
+func (m *model) State(folder string) (string, time.Time, error) {
 	m.fmut.RLock()
 	runner, ok := m.folderRunners[folder]
 	m.fmut.RUnlock()
@@ -2215,7 +2265,7 @@ func (m *Model) State(folder string) (string, time.Time, error) {
 	return state.String(), changed, err
 }
 
-func (m *Model) FolderErrors(folder string) ([]FileError, error) {
+func (m *model) FolderErrors(folder string) ([]FileError, error) {
 	m.fmut.RLock()
 	defer m.fmut.RUnlock()
 	if err := m.checkFolderRunningLocked(folder); err != nil {
@@ -2224,7 +2274,7 @@ func (m *Model) FolderErrors(folder string) ([]FileError, error) {
 	return m.folderRunners[folder].Errors(), nil
 }
 
-func (m *Model) WatchError(folder string) error {
+func (m *model) WatchError(folder string) error {
 	m.fmut.RLock()
 	defer m.fmut.RUnlock()
 	if err := m.checkFolderRunningLocked(folder); err != nil {
@@ -2233,7 +2283,7 @@ func (m *Model) WatchError(folder string) error {
 	return m.folderRunners[folder].WatchError()
 }
 
-func (m *Model) Override(folder string) {
+func (m *model) Override(folder string) {
 	// Grab the runner and the file set.
 
 	m.fmut.RLock()
@@ -2251,7 +2301,7 @@ func (m *Model) Override(folder string) {
 	})
 }
 
-func (m *Model) Revert(folder string) {
+func (m *model) Revert(folder string) {
 	// Grab the runner and the file set.
 
 	m.fmut.RLock()
@@ -2272,7 +2322,7 @@ func (m *Model) Revert(folder string) {
 // CurrentSequence returns the change version for the given folder.
 // This is guaranteed to increment if the contents of the local folder has
 // changed.
-func (m *Model) CurrentSequence(folder string) (int64, bool) {
+func (m *model) CurrentSequence(folder string) (int64, bool) {
 	m.fmut.RLock()
 	fs, ok := m.folderFiles[folder]
 	m.fmut.RUnlock()
@@ -2288,7 +2338,7 @@ func (m *Model) CurrentSequence(folder string) (int64, bool) {
 // RemoteSequence returns the change version for the given folder, as
 // sent by remote peers. This is guaranteed to increment if the contents of
 // the remote or global folder has changed.
-func (m *Model) RemoteSequence(folder string) (int64, bool) {
+func (m *model) RemoteSequence(folder string) (int64, bool) {
 	m.fmut.RLock()
 	defer m.fmut.RUnlock()
 
@@ -2308,7 +2358,7 @@ func (m *Model) RemoteSequence(folder string) (int64, bool) {
 	return ver, true
 }
 
-func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{} {
+func (m *model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{} {
 	m.fmut.RLock()
 	files, ok := m.folderFiles[folder]
 	m.fmut.RUnlock()
@@ -2372,7 +2422,7 @@ func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly
 	return output
 }
 
-func (m *Model) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) {
+func (m *model) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) {
 	fcfg, ok := m.cfg.Folder(folder)
 	if !ok {
 		return nil, errFolderMissing
@@ -2432,7 +2482,7 @@ func (m *Model) GetFolderVersions(folder string) (map[string][]versioner.FileVer
 	return files, nil
 }
 
-func (m *Model) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
+func (m *model) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
 	fcfg, ok := m.cfg.Folder(folder)
 	if !ok {
 		return nil, errFolderMissing
@@ -2503,7 +2553,7 @@ func (m *Model) RestoreFolderVersions(folder string, versions map[string]time.Ti
 	return errors, nil
 }
 
-func (m *Model) Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
+func (m *model) Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
 	// The slightly unusual locking sequence here is because we need to hold
 	// pmut for the duration (as the value returned from foldersFiles can
 	// get heavily modified on Close()), but also must acquire fmut before
@@ -2544,7 +2594,7 @@ next:
 }
 
 // BringToFront bumps the given files priority in the job queue.
-func (m *Model) BringToFront(folder, file string) {
+func (m *model) BringToFront(folder, file string) {
 	m.pmut.RLock()
 	defer m.pmut.RUnlock()
 
@@ -2554,20 +2604,20 @@ func (m *Model) BringToFront(folder, file string) {
 	}
 }
 
-func (m *Model) ResetFolder(folder string) {
+func (m *model) ResetFolder(folder string) {
 	l.Infof("Cleaning data for folder %q", folder)
 	db.DropFolder(m.db, folder)
 }
 
-func (m *Model) String() string {
+func (m *model) String() string {
 	return fmt.Sprintf("model@%p", m)
 }
 
-func (m *Model) VerifyConfiguration(from, to config.Configuration) error {
+func (m *model) VerifyConfiguration(from, to config.Configuration) error {
 	return nil
 }
 
-func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
+func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 	// TODO: This should not use reflect, and should take more care to try to handle stuff without restart.
 
 	// Go through the folder configs and figure out if we need to restart or not.
@@ -2661,7 +2711,7 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
 // checkFolderRunningLocked returns nil if the folder is up and running and a
 // descriptive error if not.
 // Need to hold (read) lock on m.fmut when calling this.
-func (m *Model) checkFolderRunningLocked(folder string) error {
+func (m *model) checkFolderRunningLocked(folder string) error {
 	_, ok := m.folderRunners[folder]
 	if ok {
 		return nil
@@ -2679,7 +2729,7 @@ func (m *Model) checkFolderRunningLocked(folder string) error {
 // checkFolderDeviceStatusLocked first checks the folder and then whether the
 // given device is connected and shares this folder.
 // Need to hold (read) lock on both m.fmut and m.pmut when calling this.
-func (m *Model) checkDeviceFolderConnectedLocked(device protocol.DeviceID, folder string) error {
+func (m *model) checkDeviceFolderConnectedLocked(device protocol.DeviceID, folder string) error {
 	if err := m.checkFolderRunningLocked(folder); err != nil {
 		return err
 	}

+ 24 - 20
lib/model/model_test.go

@@ -38,7 +38,7 @@ import (
 )
 
 var myID, device1, device2 protocol.DeviceID
-var defaultCfgWrapper *config.Wrapper
+var defaultCfgWrapper config.Wrapper
 var defaultFolderConfig config.FolderConfiguration
 var defaultFs fs.Filesystem
 var defaultCfg config.Configuration
@@ -161,7 +161,7 @@ func prepareTmpFile(to fs.Filesystem) (string, error) {
 	return tmpName, nil
 }
 
-func createTmpWrapper(cfg config.Configuration) *config.Wrapper {
+func createTmpWrapper(cfg config.Configuration) config.Wrapper {
 	tmpFile, err := ioutil.TempFile(tmpLocation, "syncthing-testConfig-")
 	if err != nil {
 		panic(err)
@@ -171,7 +171,11 @@ func createTmpWrapper(cfg config.Configuration) *config.Wrapper {
 	return wrapper
 }
 
-func newState(cfg config.Configuration) (*config.Wrapper, *Model) {
+func newModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string) *model {
+	return NewModel(cfg, id, clientName, clientVersion, ldb, protectedFiles).(*model)
+}
+
+func newState(cfg config.Configuration) (config.Wrapper, *model) {
 	wcfg := createTmpWrapper(cfg)
 
 	m := setupModel(wcfg)
@@ -183,9 +187,9 @@ func newState(cfg config.Configuration) (*config.Wrapper, *Model) {
 	return wcfg, m
 }
 
-func setupModel(w *config.Wrapper) *Model {
+func setupModel(w config.Wrapper) *model {
 	db := db.OpenMemory()
-	m := NewModel(w, myID, "syncthing", "dev", db, nil)
+	m := newModel(w, myID, "syncthing", "dev", db, nil)
 	m.ServeBackground()
 	for id, cfg := range w.Folders() {
 		if !cfg.Paused {
@@ -322,7 +326,7 @@ type fakeConnection struct {
 	files                    []protocol.FileInfo
 	fileData                 map[string][]byte
 	folder                   string
-	model                    *Model
+	model                    *model
 	indexFn                  func(string, []protocol.FileInfo)
 	requestFn                func(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error)
 	mut                      sync.Mutex
@@ -563,7 +567,7 @@ func TestDeviceRename(t *testing.T) {
 	cfg := config.Wrap("testdata/tmpconfig.xml", rawCfg)
 
 	db := db.OpenMemory()
-	m := NewModel(cfg, myID, "syncthing", "dev", db, nil)
+	m := newModel(cfg, myID, "syncthing", "dev", db, nil)
 
 	if cfg.Devices()[device1].Name != "" {
 		t.Errorf("Device already has a name")
@@ -662,7 +666,7 @@ func TestClusterConfig(t *testing.T) {
 
 	wrapper := createTmpWrapper(cfg)
 	defer os.Remove(wrapper.ConfigPath())
-	m := NewModel(wrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
 	m.AddFolder(cfg.Folders[0])
 	m.AddFolder(cfg.Folders[1])
 	m.ServeBackground()
@@ -1644,7 +1648,7 @@ func TestAutoAcceptPausedWhenFolderConfigNotChanged(t *testing.T) {
 	}
 }
 
-func changeIgnores(t *testing.T, m *Model, expected []string) {
+func changeIgnores(t *testing.T, m *model, expected []string) {
 	arrEqual := func(a, b []string) bool {
 		if len(a) != len(b) {
 			return false
@@ -1796,7 +1800,7 @@ func TestROScanRecovery(t *testing.T) {
 
 	testOs.RemoveAll(fcfg.Path)
 
-	m := NewModel(cfg, myID, "syncthing", "dev", ldb, nil)
+	m := newModel(cfg, myID, "syncthing", "dev", ldb, nil)
 	m.AddFolder(fcfg)
 	m.StartFolder("default")
 	m.ServeBackground()
@@ -1883,7 +1887,7 @@ func TestRWScanRecovery(t *testing.T) {
 
 	testOs.RemoveAll(fcfg.Path)
 
-	m := NewModel(cfg, myID, "syncthing", "dev", ldb, nil)
+	m := newModel(cfg, myID, "syncthing", "dev", ldb, nil)
 	m.AddFolder(fcfg)
 	m.StartFolder("default")
 	m.ServeBackground()
@@ -1941,7 +1945,7 @@ func TestRWScanRecovery(t *testing.T) {
 
 func TestGlobalDirectoryTree(t *testing.T) {
 	db := db.OpenMemory()
-	m := NewModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
 	m.AddFolder(defaultFolderConfig)
 	m.ServeBackground()
 	defer m.Stop()
@@ -2193,7 +2197,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 
 func TestGlobalDirectorySelfFixing(t *testing.T) {
 	db := db.OpenMemory()
-	m := NewModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
 	m.AddFolder(defaultFolderConfig)
 	m.ServeBackground()
 
@@ -2368,7 +2372,7 @@ func BenchmarkTree_100_10(b *testing.B) {
 
 func benchmarkTree(b *testing.B, n1, n2 int) {
 	db := db.OpenMemory()
-	m := NewModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
 	m.AddFolder(defaultFolderConfig)
 	m.ServeBackground()
 
@@ -2438,7 +2442,7 @@ func TestIssue4357(t *testing.T) {
 	// Create a separate wrapper not to pollute other tests.
 	wrapper := createTmpWrapper(config.Configuration{})
 	defer os.Remove(wrapper.ConfigPath())
-	m := NewModel(wrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
 	m.ServeBackground()
 	defer m.Stop()
 
@@ -2568,7 +2572,7 @@ func TestIndexesForUnknownDevicesDropped(t *testing.T) {
 		t.Error("expected two devices")
 	}
 
-	m := NewModel(defaultCfgWrapper, myID, "syncthing", "dev", dbi, nil)
+	m := newModel(defaultCfgWrapper, myID, "syncthing", "dev", dbi, nil)
 	m.AddFolder(defaultFolderConfig)
 	m.StartFolder("default")
 
@@ -3055,7 +3059,7 @@ func TestCustomMarkerName(t *testing.T) {
 	testOs.RemoveAll(fcfg.Path)
 	defer testOs.RemoveAll(fcfg.Path)
 
-	m := NewModel(cfg, myID, "syncthing", "dev", ldb, nil)
+	m := newModel(cfg, myID, "syncthing", "dev", ldb, nil)
 	m.AddFolder(fcfg)
 	m.StartFolder("default")
 	m.ServeBackground()
@@ -3466,7 +3470,7 @@ func TestIssue4094(t *testing.T) {
 	// Create a separate wrapper not to pollute other tests.
 	wrapper := createTmpWrapper(config.Configuration{})
 	defer os.Remove(wrapper.ConfigPath())
-	m := NewModel(wrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
 	m.ServeBackground()
 	defer m.Stop()
 
@@ -3505,7 +3509,7 @@ func TestIssue4903(t *testing.T) {
 	// Create a separate wrapper not to pollute other tests.
 	wrapper := createTmpWrapper(config.Configuration{})
 	defer os.Remove(wrapper.ConfigPath())
-	m := NewModel(wrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
 	m.ServeBackground()
 	defer m.Stop()
 
@@ -3575,7 +3579,7 @@ func TestParentOfUnignored(t *testing.T) {
 	}
 }
 
-func addFakeConn(m *Model, dev protocol.DeviceID) *fakeConnection {
+func addFakeConn(m *model, dev protocol.DeviceID) *fakeConnection {
 	fc := &fakeConnection{id: dev, model: m}
 	m.AddConnection(fc, protocol.HelloResult{})
 

+ 1 - 1
lib/model/progressemitter.go

@@ -31,7 +31,7 @@ type ProgressEmitter struct {
 
 // NewProgressEmitter creates a new progress emitter which emits
 // DownloadProgress events every interval.
-func NewProgressEmitter(cfg *config.Wrapper) *ProgressEmitter {
+func NewProgressEmitter(cfg config.Wrapper) *ProgressEmitter {
 	t := &ProgressEmitter{
 		stop:               make(chan struct{}),
 		registry:           make(map[string]*sharedPullerState),

+ 3 - 3
lib/model/requests_test.go

@@ -684,7 +684,7 @@ func TestRequestSymlinkWindows(t *testing.T) {
 	}
 }
 
-func tmpDefaultWrapper() (*config.Wrapper, string) {
+func tmpDefaultWrapper() (config.Wrapper, string) {
 	w := createTmpWrapper(defaultCfgWrapper.RawCopy())
 	fcfg, tmpDir := testFolderConfigTmp()
 	w.SetFolder(fcfg)
@@ -703,13 +703,13 @@ func testFolderConfig(path string) config.FolderConfiguration {
 	return cfg
 }
 
-func setupModelWithConnection() (*Model, *fakeConnection, string, *config.Wrapper) {
+func setupModelWithConnection() (*model, *fakeConnection, string, config.Wrapper) {
 	w, tmpDir := tmpDefaultWrapper()
 	m, fc := setupModelWithConnectionFromWrapper(w)
 	return m, fc, tmpDir, w
 }
 
-func setupModelWithConnectionFromWrapper(w *config.Wrapper) (*Model, *fakeConnection) {
+func setupModelWithConnectionFromWrapper(w config.Wrapper) (*model, *fakeConnection) {
 	m := setupModel(w)
 
 	fc := addFakeConn(m, device1)

+ 2 - 2
lib/nat/service.go

@@ -23,7 +23,7 @@ import (
 // setup/renewal of a port mapping.
 type Service struct {
 	id   protocol.DeviceID
-	cfg  *config.Wrapper
+	cfg  config.Wrapper
 	stop chan struct{}
 
 	mappings []*Mapping
@@ -31,7 +31,7 @@ type Service struct {
 	mut      sync.RWMutex
 }
 
-func NewService(id protocol.DeviceID, cfg *config.Wrapper) *Service {
+func NewService(id protocol.DeviceID, cfg config.Wrapper) *Service {
 	return &Service{
 		id:  id,
 		cfg: cfg,

+ 2 - 2
lib/watchaggregator/aggregator.go

@@ -125,14 +125,14 @@ func newAggregator(folderCfg config.FolderConfiguration, ctx context.Context) *a
 	return a
 }
 
-func Aggregate(in <-chan fs.Event, out chan<- []string, folderCfg config.FolderConfiguration, cfg *config.Wrapper, ctx context.Context) {
+func Aggregate(in <-chan fs.Event, out chan<- []string, folderCfg config.FolderConfiguration, cfg config.Wrapper, ctx context.Context) {
 	a := newAggregator(folderCfg, ctx)
 
 	// Necessary for unit tests where the backend is mocked
 	go a.mainLoop(in, out, cfg)
 }
 
-func (a *aggregator) mainLoop(in <-chan fs.Event, out chan<- []string, cfg *config.Wrapper) {
+func (a *aggregator) mainLoop(in <-chan fs.Event, out chan<- []string, cfg config.Wrapper) {
 	a.notifyTimer = time.NewTimer(a.notifyDelay)
 	defer a.notifyTimer.Stop()