Browse Source

cmd/syncthing, lib/api: Separate api/gui into own package (ref #4085) (#5529)

* cmd/syncthing, lib/gui: Separate gui into own package (ref #4085)

* fix tests

* Don't use main as interface name (make old go happy)

* gui->api

* don't leak state via locations and use in-tree config

* let api (un-)subscribe to config

* interface naming and exporting

* lib/ur

* fix tests and lib/foldersummary

* shorter URVersion and ur debug fix

* review

* model.JsonCompletion(FolderCompletion) -> FolderCompletion.Map()

* rename debug facility https -> api

* folder summaries in model

* disassociate unrelated constants

* fix merge fail

* missing id assignement
Simon Frei 6 years ago
parent
commit
b50039a920

+ 1 - 7
cmd/syncthing/debug.go

@@ -14,15 +14,9 @@ import (
 )
 
 var (
-	l     = logger.DefaultLogger.NewFacility("main", "Main package")
-	httpl = logger.DefaultLogger.NewFacility("http", "REST API")
+	l = logger.DefaultLogger.NewFacility("main", "Main package")
 )
 
-func shouldDebugHTTP() bool {
-	return l.ShouldDebug("http")
-}
-
 func init() {
 	l.SetDebug("main", strings.Contains(os.Getenv("STTRACE"), "main") || os.Getenv("STTRACE") == "all")
-	l.SetDebug("http", strings.Contains(os.Getenv("STTRACE"), "http") || os.Getenv("STTRACE") == "all")
 }

+ 29 - 25
cmd/syncthing/main.go

@@ -29,6 +29,7 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/syncthing/syncthing/lib/api"
 	"github.com/syncthing/syncthing/lib/build"
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/connections"
@@ -46,6 +47,7 @@ import (
 	"github.com/syncthing/syncthing/lib/sha256"
 	"github.com/syncthing/syncthing/lib/tlsutil"
 	"github.com/syncthing/syncthing/lib/upgrade"
+	"github.com/syncthing/syncthing/lib/ur"
 
 	"github.com/pkg/errors"
 	"github.com/thejerf/suture"
@@ -62,7 +64,6 @@ const (
 const (
 	bepProtocolName      = "bep/1.0"
 	tlsDefaultCommonName = "syncthing"
-	defaultEventTimeout  = time.Minute
 	maxSystemErrors      = 5
 	initialSystemLog     = 10
 	maxSystemLog         = 250
@@ -263,6 +264,7 @@ func parseCommandLineOptions() RuntimeOptions {
 	return options
 }
 
+// exiter implements api.Controller
 type exiter struct {
 	stop chan int
 }
@@ -287,7 +289,7 @@ func (e *exiter) waitForExit() int {
 	return <-e.stop
 }
 
-var exit = exiter{make(chan int)}
+var exit = &exiter{make(chan int)}
 
 func main() {
 	options := parseCommandLineOptions()
@@ -621,8 +623,8 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 	// Event subscription for the API; must start early to catch the early
 	// events. The LocalChangeDetected event might overwhelm the event
 	// receiver in some situations so we will not subscribe to it here.
-	defaultSub := events.NewBufferedSubscription(events.Default.Subscribe(defaultEventMask), eventSubBufferSize)
-	diskSub := events.NewBufferedSubscription(events.Default.Subscribe(diskEventMask), eventSubBufferSize)
+	defaultSub := events.NewBufferedSubscription(events.Default.Subscribe(api.DefaultEventMask), api.EventSubBufferSize)
+	diskSub := events.NewBufferedSubscription(events.Default.Subscribe(api.DiskEventMask), api.EventSubBufferSize)
 
 	if len(os.Getenv("GOMAXPROCS")) == 0 {
 		runtime.GOMAXPROCS(runtime.NumCPU())
@@ -692,7 +694,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 		}()
 	}
 
-	perf := cpuBench(3, 150*time.Millisecond, true)
+	perf := ur.CpuBench(3, 150*time.Millisecond, true)
 	l.Infof("Hashing performance is %.02f MB/s", perf)
 
 	dbFile := locations.Get(locations.Database)
@@ -832,10 +834,6 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 		}
 	}
 
-	// GUI
-
-	setupGUI(mainService, cfg, m, defaultSub, diskSub, cachedDiscovery, connectionsService, errors, systemLog, runtimeOptions)
-
 	if runtimeOptions.cpuProfile {
 		f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
 		if err != nil {
@@ -848,20 +846,12 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 		}
 	}
 
-	myDev, _ := cfg.Device(myID)
-	l.Infof(`My name is "%v"`, myDev.Name)
-	for _, device := range cfg.Devices() {
-		if device.DeviceID != myID {
-			l.Infof(`Device %s is "%v" at %v`, device.DeviceID, device.Name, device.Addresses)
-		}
-	}
-
 	// Candidate builds always run with usage reporting.
 
 	if opts := cfg.Options(); build.IsCandidate {
 		l.Infoln("Anonymous usage reporting is always enabled for candidate releases.")
-		if opts.URAccepted != usageReportVersion {
-			opts.URAccepted = usageReportVersion
+		if opts.URAccepted != ur.Version {
+			opts.URAccepted = ur.Version
 			cfg.SetOptions(opts)
 			cfg.Save()
 			// Unique ID will be set and config saved below if necessary.
@@ -875,9 +865,21 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 		cfg.Save()
 	}
 
-	usageReportingSvc := newUsageReportingService(cfg, m, connectionsService)
+	usageReportingSvc := ur.New(cfg, m, connectionsService, noUpgradeFromEnv)
 	mainService.Add(usageReportingSvc)
 
+	// GUI
+
+	setupGUI(mainService, cfg, m, defaultSub, diskSub, cachedDiscovery, connectionsService, usageReportingSvc, errors, systemLog, runtimeOptions)
+
+	myDev, _ := cfg.Device(myID)
+	l.Infof(`My name is "%v"`, myDev.Name)
+	for _, device := range cfg.Devices() {
+		if device.DeviceID != myID {
+			l.Infof(`Device %s is "%v" at %v`, device.DeviceID, device.Name, device.Addresses)
+		}
+	}
+
 	if opts := cfg.Options(); opts.RestartOnWakeup {
 		go standbyMonitor()
 	}
@@ -1069,7 +1071,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, urService *ur.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) {
 	guiCfg := cfg.GUI()
 
 	if !guiCfg.Enabled {
@@ -1083,11 +1085,13 @@ func setupGUI(mainService *suture.Supervisor, cfg config.Wrapper, m model.Model,
 	cpu := newCPUService()
 	mainService.Add(cpu)
 
-	api := newAPIService(myID, cfg, locations.Get(locations.HTTPSCertFile), locations.Get(locations.HTTPSKeyFile), runtimeOptions.assetDir, m, defaultSub, diskSub, discoverer, connectionsService, errors, systemLog, cpu)
-	cfg.Subscribe(api)
-	mainService.Add(api)
+	summaryService := model.NewFolderSummaryService(cfg, m, myID)
+	mainService.Add(summaryService)
+
+	apiSvc := api.New(myID, cfg, runtimeOptions.assetDir, tlsDefaultCommonName, m, defaultSub, diskSub, discoverer, connectionsService, urService, summaryService, errors, systemLog, cpu, exit, noUpgradeFromEnv)
+	mainService.Add(apiSvc)
 
-	if err := api.WaitForStart(); err != nil {
+	if err := apiSvc.WaitForStart(); err != nil {
 		l.Warnln("Failed starting API:", err)
 		os.Exit(exitError)
 	}

+ 163 - 222
cmd/syncthing/gui.go → lib/api/api.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"bytes"
@@ -43,85 +43,101 @@ import (
 	"github.com/syncthing/syncthing/lib/sync"
 	"github.com/syncthing/syncthing/lib/tlsutil"
 	"github.com/syncthing/syncthing/lib/upgrade"
+	"github.com/syncthing/syncthing/lib/ur"
+	"github.com/thejerf/suture"
 	"github.com/vitrun/qart/qr"
 	"golang.org/x/crypto/bcrypt"
 )
 
-var (
-	startTime = time.Now()
-
-	// matches a bcrypt hash and not too much else
-	bcryptExpr = regexp.MustCompile(`^\$2[aby]\$\d+\$.{50,}`)
-)
+// matches a bcrypt hash and not too much else
+var bcryptExpr = regexp.MustCompile(`^\$2[aby]\$\d+\$.{50,}`)
 
 const (
-	defaultEventMask   = events.AllEvents &^ events.LocalChangeDetected &^ events.RemoteChangeDetected
-	diskEventMask      = events.LocalChangeDetected | events.RemoteChangeDetected
-	eventSubBufferSize = 1000
+	DefaultEventMask    = events.AllEvents &^ events.LocalChangeDetected &^ events.RemoteChangeDetected
+	DiskEventMask       = events.LocalChangeDetected | events.RemoteChangeDetected
+	EventSubBufferSize  = 1000
+	defaultEventTimeout = time.Minute
 )
 
-type apiService struct {
-	id                 protocol.DeviceID
-	cfg                config.Wrapper
-	httpsCertFile      string
-	httpsKeyFile       string
-	statics            *staticsServer
-	model              model.Model
-	eventSubs          map[events.EventType]events.BufferedSubscription
-	eventSubsMut       sync.Mutex
-	discoverer         discover.CachingMux
-	connectionsService connections.Service
-	fss                *folderSummaryService
-	systemConfigMut    sync.Mutex    // serializes posts to /rest/system/config
-	stop               chan struct{} // signals intentional stop
-	configChanged      chan struct{} // signals intentional listener close due to config change
-	started            chan string   // signals startup complete by sending the listener address, for testing only
-	startedOnce        chan struct{} // the service has started at least once
-	startupErr         error
-	cpu                rater
+type service struct {
+	id                   protocol.DeviceID
+	cfg                  config.Wrapper
+	statics              *staticsServer
+	model                model.Model
+	eventSubs            map[events.EventType]events.BufferedSubscription
+	eventSubsMut         sync.Mutex
+	discoverer           discover.CachingMux
+	connectionsService   connections.Service
+	fss                  model.FolderSummaryService
+	urService            *ur.Service
+	systemConfigMut      sync.Mutex // serializes posts to /rest/system/config
+	cpu                  Rater
+	contr                Controller
+	noUpgrade            bool
+	tlsDefaultCommonName string
+	stop                 chan struct{} // signals intentional stop
+	configChanged        chan struct{} // signals intentional listener close due to config change
+	started              chan string   // signals startup complete by sending the listener address, for testing only
+	startedOnce          chan struct{} // the service has started successfully at least once
+	startupErr           error
 
 	guiErrors logger.Recorder
 	systemLog logger.Recorder
 }
 
-type rater interface {
+type Rater interface {
 	Rate() float64
 }
 
-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,
-		httpsCertFile: httpsCertFile,
-		httpsKeyFile:  httpsKeyFile,
-		statics:       newStaticsServer(cfg.GUI().Theme, assetDir),
-		model:         m,
-		eventSubs: map[events.EventType]events.BufferedSubscription{
-			defaultEventMask: defaultSub,
-			diskEventMask:    diskSub,
-		},
-		eventSubsMut:       sync.NewMutex(),
-		discoverer:         discoverer,
-		connectionsService: connectionsService,
-		systemConfigMut:    sync.NewMutex(),
-		stop:               make(chan struct{}),
-		configChanged:      make(chan struct{}),
-		startedOnce:        make(chan struct{}),
-		guiErrors:          errors,
-		systemLog:          systemLog,
-		cpu:                cpu,
-	}
+type Controller interface {
+	ExitUpgrading()
+	Restart()
+	Shutdown()
+}
 
-	return service
+type Service interface {
+	suture.Service
+	config.Committer
+	WaitForStart() error
 }
 
-func (s *apiService) WaitForStart() error {
+func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, cpu Rater, contr Controller, noUpgrade bool) Service {
+	return &service{
+		id:      id,
+		cfg:     cfg,
+		statics: newStaticsServer(cfg.GUI().Theme, assetDir),
+		model:   m,
+		eventSubs: map[events.EventType]events.BufferedSubscription{
+			DefaultEventMask: defaultSub,
+			DiskEventMask:    diskSub,
+		},
+		eventSubsMut:         sync.NewMutex(),
+		discoverer:           discoverer,
+		connectionsService:   connectionsService,
+		fss:                  fss,
+		urService:            urService,
+		systemConfigMut:      sync.NewMutex(),
+		guiErrors:            errors,
+		systemLog:            systemLog,
+		cpu:                  cpu,
+		contr:                contr,
+		noUpgrade:            noUpgrade,
+		tlsDefaultCommonName: tlsDefaultCommonName,
+		stop:                 make(chan struct{}),
+		configChanged:        make(chan struct{}),
+		startedOnce:          make(chan struct{}),
+	}
+}
+
+func (s *service) WaitForStart() error {
 	<-s.startedOnce
 	return s.startupErr
 }
 
-func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
-	cert, err := tls.LoadX509KeyPair(s.httpsCertFile, s.httpsKeyFile)
+func (s *service) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
+	httpsCertFile := locations.Get(locations.HTTPSCertFile)
+	httpsKeyFile := locations.Get(locations.HTTPSKeyFile)
+	cert, err := tls.LoadX509KeyPair(httpsCertFile, httpsKeyFile)
 	if err != nil {
 		l.Infoln("Loading HTTPS certificate:", err)
 		l.Infoln("Creating new HTTPS certificate")
@@ -131,10 +147,10 @@ func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener,
 		var name string
 		name, err = os.Hostname()
 		if err != nil {
-			name = tlsDefaultCommonName
+			name = s.tlsDefaultCommonName
 		}
 
-		cert, err = tlsutil.NewCertificate(s.httpsCertFile, s.httpsKeyFile, name)
+		cert, err = tlsutil.NewCertificate(httpsCertFile, httpsKeyFile, name)
 	}
 	if err != nil {
 		return nil, err
@@ -174,7 +190,7 @@ func sendJSON(w http.ResponseWriter, jsonObject interface{}) {
 	fmt.Fprintf(w, "%s\n", bs)
 }
 
-func (s *apiService) Serve() {
+func (s *service) Serve() {
 	listener, err := s.getListener(s.cfg.GUI())
 	if err != nil {
 		select {
@@ -201,6 +217,9 @@ func (s *apiService) Serve() {
 
 	defer listener.Close()
 
+	s.cfg.Subscribe(s)
+	defer s.cfg.Unsubscribe(s)
+
 	// The GET handlers
 	getRestMux := http.NewServeMux()
 	getRestMux.HandleFunc("/rest/db/completion", s.getDBCompletion)              // device folder
@@ -316,10 +335,6 @@ func (s *apiService) Serve() {
 		ReadTimeout: 15 * time.Second,
 	}
 
-	s.fss = newFolderSummaryService(s.cfg, s.model)
-	defer s.fss.Stop()
-	s.fss.ServeBackground()
-
 	l.Infoln("GUI and API listening on", listener.Addr())
 	l.Infoln("Access the GUI via the following URL:", guiCfg.URL())
 	if s.started != nil {
@@ -359,7 +374,7 @@ func (s *apiService) Serve() {
 
 // Complete implements suture.IsCompletable, which signifies to the supervisor
 // whether to stop restarting the service.
-func (s *apiService) Complete() bool {
+func (s *service) Complete() bool {
 	select {
 	case <-s.startedOnce:
 		return s.startupErr != nil
@@ -370,15 +385,15 @@ func (s *apiService) Complete() bool {
 	return false
 }
 
-func (s *apiService) Stop() {
+func (s *service) Stop() {
 	close(s.stop)
 }
 
-func (s *apiService) String() string {
-	return fmt.Sprintf("apiService@%p", s)
+func (s *service) String() string {
+	return fmt.Sprintf("api.service@%p", s)
 }
 
-func (s *apiService) VerifyConfiguration(from, to config.Configuration) error {
+func (s *service) VerifyConfiguration(from, to config.Configuration) error {
 	if to.GUI.Network() != "tcp" {
 		return nil
 	}
@@ -386,7 +401,7 @@ func (s *apiService) VerifyConfiguration(from, to config.Configuration) error {
 	return err
 }
 
-func (s *apiService) CommitConfiguration(from, to config.Configuration) bool {
+func (s *service) CommitConfiguration(from, to config.Configuration) bool {
 	// No action required when this changes, so mask the fact that it changed at all.
 	from.GUI.Debugging = to.GUI.Debugging
 
@@ -438,7 +453,7 @@ func debugMiddleware(h http.Handler) http.Handler {
 					written = rf.Int()
 				}
 			}
-			httpl.Debugf("http: %s %q: status %d, %d bytes in %.02f ms", r.Method, r.URL.String(), status, written, ms)
+			l.Debugf("http: %s %q: status %d, %d bytes in %.02f ms", r.Method, r.URL.String(), status, written, ms)
 		}
 	})
 }
@@ -546,7 +561,7 @@ func localhostMiddleware(h http.Handler) http.Handler {
 	})
 }
 
-func (s *apiService) whenDebugging(h http.Handler) http.Handler {
+func (s *service) whenDebugging(h http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		if s.cfg.GUI().Debugging {
 			h.ServeHTTP(w, r)
@@ -557,11 +572,11 @@ func (s *apiService) whenDebugging(h http.Handler) http.Handler {
 	})
 }
 
-func (s *apiService) restPing(w http.ResponseWriter, r *http.Request) {
+func (s *service) restPing(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, map[string]string{"ping": "pong"})
 }
 
-func (s *apiService) getJSMetadata(w http.ResponseWriter, r *http.Request) {
+func (s *service) getJSMetadata(w http.ResponseWriter, r *http.Request) {
 	meta, _ := json.Marshal(map[string]string{
 		"deviceID": s.id.String(),
 	})
@@ -569,7 +584,7 @@ func (s *apiService) getJSMetadata(w http.ResponseWriter, r *http.Request) {
 	fmt.Fprintf(w, "var metadata = %s;\n", meta)
 }
 
-func (s *apiService) getSystemVersion(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemVersion(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, map[string]interface{}{
 		"version":     build.Version,
 		"codename":    build.Codename,
@@ -582,7 +597,7 @@ func (s *apiService) getSystemVersion(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
-func (s *apiService) getSystemDebug(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemDebug(w http.ResponseWriter, r *http.Request) {
 	names := l.Facilities()
 	enabled := l.FacilityDebugging()
 	sort.Strings(enabled)
@@ -592,7 +607,7 @@ func (s *apiService) getSystemDebug(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
-func (s *apiService) postSystemDebug(w http.ResponseWriter, r *http.Request) {
+func (s *service) postSystemDebug(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json; charset=utf-8")
 	q := r.URL.Query()
 	for _, f := range strings.Split(q.Get("enable"), ",") {
@@ -611,7 +626,7 @@ func (s *apiService) postSystemDebug(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func (s *apiService) getDBBrowse(w http.ResponseWriter, r *http.Request) {
+func (s *service) getDBBrowse(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	folder := qs.Get("folder")
 	prefix := qs.Get("prefix")
@@ -625,7 +640,7 @@ func (s *apiService) getDBBrowse(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, s.model.GlobalDirectoryTree(folder, prefix, levels, dirsonly))
 }
 
-func (s *apiService) getDBCompletion(w http.ResponseWriter, r *http.Request) {
+func (s *service) getDBCompletion(w http.ResponseWriter, r *http.Request) {
 	var qs = r.URL.Query()
 	var folder = qs.Get("folder")
 	var deviceStr = qs.Get("device")
@@ -636,100 +651,26 @@ func (s *apiService) getDBCompletion(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	sendJSON(w, jsonCompletion(s.model.Completion(device, folder)))
+	sendJSON(w, s.model.Completion(device, folder).Map())
 }
 
-func jsonCompletion(comp model.FolderCompletion) map[string]interface{} {
-	return map[string]interface{}{
-		"completion":  comp.CompletionPct,
-		"needBytes":   comp.NeedBytes,
-		"needItems":   comp.NeedItems,
-		"globalBytes": comp.GlobalBytes,
-		"needDeletes": comp.NeedDeletes,
-	}
-}
-
-func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
+func (s *service) getDBStatus(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	folder := qs.Get("folder")
-	if sum, err := folderSummary(s.cfg, s.model, folder); err != nil {
+	if sum, err := s.fss.Summary(folder); err != nil {
 		http.Error(w, err.Error(), http.StatusNotFound)
 	} else {
 		sendJSON(w, sum)
 	}
 }
 
-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)
-	if err != nil && err != model.ErrFolderPaused {
-		// Stats from the db can still be obtained if the folder is just paused
-		return nil, err
-	}
-	res["errors"] = len(errors)
-	res["pullErrors"] = len(errors) // deprecated
-
-	res["invalid"] = "" // Deprecated, retains external API for now
-
-	global := m.GlobalSize(folder)
-	res["globalFiles"], res["globalDirectories"], res["globalSymlinks"], res["globalDeleted"], res["globalBytes"], res["globalTotalItems"] = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes, global.TotalItems()
-
-	local := m.LocalSize(folder)
-	res["localFiles"], res["localDirectories"], res["localSymlinks"], res["localDeleted"], res["localBytes"], res["localTotalItems"] = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes, local.TotalItems()
-
-	need := m.NeedSize(folder)
-	res["needFiles"], res["needDirectories"], res["needSymlinks"], res["needDeletes"], res["needBytes"], res["needTotalItems"] = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes, need.TotalItems()
-
-	if cfg.Folders()[folder].Type == config.FolderTypeReceiveOnly {
-		// Add statistics for things that have changed locally in a receive
-		// only folder.
-		ro := m.ReceiveOnlyChangedSize(folder)
-		res["receiveOnlyChangedFiles"] = ro.Files
-		res["receiveOnlyChangedDirectories"] = ro.Directories
-		res["receiveOnlyChangedSymlinks"] = ro.Symlinks
-		res["receiveOnlyChangedDeletes"] = ro.Deleted
-		res["receiveOnlyChangedBytes"] = ro.Bytes
-		res["receiveOnlyTotalItems"] = ro.TotalItems()
-	}
-
-	res["inSyncFiles"], res["inSyncBytes"] = global.Files-need.Files, global.Bytes-need.Bytes
-
-	res["state"], res["stateChanged"], err = m.State(folder)
-	if err != nil {
-		res["error"] = err.Error()
-	}
-
-	ourSeq, _ := m.CurrentSequence(folder)
-	remoteSeq, _ := m.RemoteSequence(folder)
-
-	res["version"] = ourSeq + remoteSeq  // legacy
-	res["sequence"] = ourSeq + remoteSeq // new name
-
-	ignorePatterns, _, _ := m.GetIgnores(folder)
-	res["ignorePatterns"] = false
-	for _, line := range ignorePatterns {
-		if len(line) > 0 && !strings.HasPrefix(line, "//") {
-			res["ignorePatterns"] = true
-			break
-		}
-	}
-
-	err = m.WatchError(folder)
-	if err != nil {
-		res["watchError"] = err.Error()
-	}
-
-	return res, nil
-}
-
-func (s *apiService) postDBOverride(w http.ResponseWriter, r *http.Request) {
+func (s *service) postDBOverride(w http.ResponseWriter, r *http.Request) {
 	var qs = r.URL.Query()
 	var folder = qs.Get("folder")
 	go s.model.Override(folder)
 }
 
-func (s *apiService) postDBRevert(w http.ResponseWriter, r *http.Request) {
+func (s *service) postDBRevert(w http.ResponseWriter, r *http.Request) {
 	var qs = r.URL.Query()
 	var folder = qs.Get("folder")
 	go s.model.Revert(folder)
@@ -747,7 +688,7 @@ func getPagingParams(qs url.Values) (int, int) {
 	return page, perpage
 }
 
-func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
+func (s *service) getDBNeed(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 
 	folder := qs.Get("folder")
@@ -766,7 +707,7 @@ func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
-func (s *apiService) getDBRemoteNeed(w http.ResponseWriter, r *http.Request) {
+func (s *service) getDBRemoteNeed(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 
 	folder := qs.Get("folder")
@@ -790,7 +731,7 @@ func (s *apiService) getDBRemoteNeed(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func (s *apiService) getDBLocalChanged(w http.ResponseWriter, r *http.Request) {
+func (s *service) getDBLocalChanged(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 
 	folder := qs.Get("folder")
@@ -806,19 +747,19 @@ func (s *apiService) getDBLocalChanged(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
-func (s *apiService) getSystemConnections(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemConnections(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, s.model.ConnectionStats())
 }
 
-func (s *apiService) getDeviceStats(w http.ResponseWriter, r *http.Request) {
+func (s *service) getDeviceStats(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, s.model.DeviceStatistics())
 }
 
-func (s *apiService) getFolderStats(w http.ResponseWriter, r *http.Request) {
+func (s *service) getFolderStats(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, s.model.FolderStatistics())
 }
 
-func (s *apiService) getDBFile(w http.ResponseWriter, r *http.Request) {
+func (s *service) getDBFile(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	folder := qs.Get("folder")
 	file := qs.Get("file")
@@ -839,15 +780,15 @@ func (s *apiService) getDBFile(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
-func (s *apiService) getSystemConfig(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemConfig(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, s.cfg.RawCopy())
 }
 
-func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
+func (s *service) postSystemConfig(w http.ResponseWriter, r *http.Request) {
 	s.systemConfigMut.Lock()
 	defer s.systemConfigMut.Unlock()
 
-	to, err := config.ReadJSON(r.Body, myID)
+	to, err := config.ReadJSON(r.Body, s.id)
 	r.Body.Close()
 	if err != nil {
 		l.Warnln("Decoding posted config:", err)
@@ -886,16 +827,16 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func (s *apiService) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, map[string]bool{"configInSync": !s.cfg.RequiresRestart()})
 }
 
-func (s *apiService) postSystemRestart(w http.ResponseWriter, r *http.Request) {
+func (s *service) postSystemRestart(w http.ResponseWriter, r *http.Request) {
 	s.flushResponse(`{"ok": "restarting"}`, w)
-	go exit.Restart()
+	go s.contr.Restart()
 }
 
-func (s *apiService) postSystemReset(w http.ResponseWriter, r *http.Request) {
+func (s *service) postSystemReset(w http.ResponseWriter, r *http.Request) {
 	var qs = r.URL.Query()
 	folder := qs.Get("folder")
 
@@ -918,27 +859,27 @@ func (s *apiService) postSystemReset(w http.ResponseWriter, r *http.Request) {
 		s.flushResponse(`{"ok": "resetting folder `+folder+`"}`, w)
 	}
 
-	go exit.Restart()
+	go s.contr.Restart()
 }
 
-func (s *apiService) postSystemShutdown(w http.ResponseWriter, r *http.Request) {
+func (s *service) postSystemShutdown(w http.ResponseWriter, r *http.Request) {
 	s.flushResponse(`{"ok": "shutting down"}`, w)
-	go exit.Shutdown()
+	go s.contr.Shutdown()
 }
 
-func (s *apiService) flushResponse(resp string, w http.ResponseWriter) {
+func (s *service) flushResponse(resp string, w http.ResponseWriter) {
 	w.Write([]byte(resp + "\n"))
 	f := w.(http.Flusher)
 	f.Flush()
 }
 
-func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemStatus(w http.ResponseWriter, r *http.Request) {
 	var m runtime.MemStats
 	runtime.ReadMemStats(&m)
 
 	tilde, _ := fs.ExpandTilde("~")
 	res := make(map[string]interface{})
-	res["myID"] = myID.String()
+	res["myID"] = s.id.String()
 	res["goroutines"] = runtime.NumGoroutine()
 	res["alloc"] = m.Alloc
 	res["sys"] = m.Sys - m.HeapReleased
@@ -962,31 +903,31 @@ func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
 	// gives us percent
 	res["cpuPercent"] = s.cpu.Rate() / 10 / float64(runtime.NumCPU())
 	res["pathSeparator"] = string(filepath.Separator)
-	res["urVersionMax"] = usageReportVersion
-	res["uptime"] = int(time.Since(startTime).Seconds())
-	res["startTime"] = startTime
+	res["urVersionMax"] = ur.Version
+	res["uptime"] = s.urService.UptimeS()
+	res["startTime"] = ur.StartTime
 	res["guiAddressOverridden"] = s.cfg.GUI().IsOverridden()
 
 	sendJSON(w, res)
 }
 
-func (s *apiService) getSystemError(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemError(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, map[string][]logger.Line{
 		"errors": s.guiErrors.Since(time.Time{}),
 	})
 }
 
-func (s *apiService) postSystemError(w http.ResponseWriter, r *http.Request) {
+func (s *service) postSystemError(w http.ResponseWriter, r *http.Request) {
 	bs, _ := ioutil.ReadAll(r.Body)
 	r.Body.Close()
 	l.Warnln(string(bs))
 }
 
-func (s *apiService) postSystemErrorClear(w http.ResponseWriter, r *http.Request) {
+func (s *service) postSystemErrorClear(w http.ResponseWriter, r *http.Request) {
 	s.guiErrors.Clear()
 }
 
-func (s *apiService) getSystemLog(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemLog(w http.ResponseWriter, r *http.Request) {
 	q := r.URL.Query()
 	since, err := time.Parse(time.RFC3339, q.Get("since"))
 	if err != nil {
@@ -997,7 +938,7 @@ func (s *apiService) getSystemLog(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
-func (s *apiService) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
 	q := r.URL.Query()
 	since, err := time.Parse(time.RFC3339, q.Get("since"))
 	if err != nil {
@@ -1015,7 +956,7 @@ type fileEntry struct {
 	data []byte
 }
 
-func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSupportBundle(w http.ResponseWriter, r *http.Request) {
 	var files []fileEntry
 
 	// Redacted configuration as a JSON
@@ -1072,7 +1013,7 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
 	}
 
 	// Report Data as a JSON
-	if usageReportingData, err := json.MarshalIndent(reportData(s.cfg, s.model, s.connectionsService, usageReportVersion, true), "", "  "); err != nil {
+	if usageReportingData, err := json.MarshalIndent(s.urService.ReportData(), "", "  "); err != nil {
 		l.Warnln("Support bundle: failed to create versionPlatform.json:", err)
 	} else {
 		files = append(files, fileEntry{name: "usage-reporting.json.txt", data: usageReportingData})
@@ -1117,7 +1058,7 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
 	io.Copy(w, &zipFilesBuffer)
 }
 
-func (s *apiService) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request) {
 	stats := make(map[string]interface{})
 	metrics.Each(func(name string, intf interface{}) {
 		if m, ok := intf.(*metrics.StandardTimer); ok {
@@ -1137,7 +1078,7 @@ func (s *apiService) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request
 	w.Write(bs)
 }
 
-func (s *apiService) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
 	devices := make(map[string]discover.CacheEntry)
 
 	if s.discoverer != nil {
@@ -1152,15 +1093,15 @@ func (s *apiService) getSystemDiscovery(w http.ResponseWriter, r *http.Request)
 	sendJSON(w, devices)
 }
 
-func (s *apiService) getReport(w http.ResponseWriter, r *http.Request) {
-	version := usageReportVersion
+func (s *service) getReport(w http.ResponseWriter, r *http.Request) {
+	version := ur.Version
 	if val, _ := strconv.Atoi(r.URL.Query().Get("version")); val > 0 {
 		version = val
 	}
-	sendJSON(w, reportData(s.cfg, s.model, s.connectionsService, version, true))
+	sendJSON(w, s.urService.ReportDataPreview(version))
 }
 
-func (s *apiService) getRandomString(w http.ResponseWriter, r *http.Request) {
+func (s *service) getRandomString(w http.ResponseWriter, r *http.Request) {
 	length := 32
 	if val, _ := strconv.Atoi(r.URL.Query().Get("length")); val > 0 {
 		length = val
@@ -1170,7 +1111,7 @@ func (s *apiService) getRandomString(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, map[string]string{"random": str})
 }
 
-func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
+func (s *service) getDBIgnores(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 
 	folder := qs.Get("folder")
@@ -1187,7 +1128,7 @@ func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
-func (s *apiService) postDBIgnores(w http.ResponseWriter, r *http.Request) {
+func (s *service) postDBIgnores(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 
 	bs, err := ioutil.ReadAll(r.Body)
@@ -1213,19 +1154,19 @@ func (s *apiService) postDBIgnores(w http.ResponseWriter, r *http.Request) {
 	s.getDBIgnores(w, r)
 }
 
-func (s *apiService) getIndexEvents(w http.ResponseWriter, r *http.Request) {
-	s.fss.gotEventRequest()
+func (s *service) getIndexEvents(w http.ResponseWriter, r *http.Request) {
+	s.fss.OnEventRequest()
 	mask := s.getEventMask(r.URL.Query().Get("events"))
 	sub := s.getEventSub(mask)
 	s.getEvents(w, r, sub)
 }
 
-func (s *apiService) getDiskEvents(w http.ResponseWriter, r *http.Request) {
-	sub := s.getEventSub(diskEventMask)
+func (s *service) getDiskEvents(w http.ResponseWriter, r *http.Request) {
+	sub := s.getEventSub(DiskEventMask)
 	s.getEvents(w, r, sub)
 }
 
-func (s *apiService) getEvents(w http.ResponseWriter, r *http.Request, eventSub events.BufferedSubscription) {
+func (s *service) getEvents(w http.ResponseWriter, r *http.Request, eventSub events.BufferedSubscription) {
 	qs := r.URL.Query()
 	sinceStr := qs.Get("since")
 	limitStr := qs.Get("limit")
@@ -1254,8 +1195,8 @@ func (s *apiService) getEvents(w http.ResponseWriter, r *http.Request, eventSub
 	sendJSON(w, evs)
 }
 
-func (s *apiService) getEventMask(evs string) events.EventType {
-	eventMask := defaultEventMask
+func (s *service) getEventMask(evs string) events.EventType {
+	eventMask := DefaultEventMask
 	if evs != "" {
 		eventList := strings.Split(evs, ",")
 		eventMask = 0
@@ -1266,12 +1207,12 @@ func (s *apiService) getEventMask(evs string) events.EventType {
 	return eventMask
 }
 
-func (s *apiService) getEventSub(mask events.EventType) events.BufferedSubscription {
+func (s *service) getEventSub(mask events.EventType) events.BufferedSubscription {
 	s.eventSubsMut.Lock()
 	bufsub, ok := s.eventSubs[mask]
 	if !ok {
 		evsub := events.Default.Subscribe(mask)
-		bufsub = events.NewBufferedSubscription(evsub, eventSubBufferSize)
+		bufsub = events.NewBufferedSubscription(evsub, EventSubBufferSize)
 		s.eventSubs[mask] = bufsub
 	}
 	s.eventSubsMut.Unlock()
@@ -1279,8 +1220,8 @@ func (s *apiService) getEventSub(mask events.EventType) events.BufferedSubscript
 	return bufsub
 }
 
-func (s *apiService) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
-	if noUpgradeFromEnv {
+func (s *service) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
+	if s.noUpgrade {
 		http.Error(w, upgrade.ErrUpgradeUnsupported.Error(), 500)
 		return
 	}
@@ -1299,7 +1240,7 @@ func (s *apiService) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, res)
 }
 
-func (s *apiService) getDeviceID(w http.ResponseWriter, r *http.Request) {
+func (s *service) getDeviceID(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	idStr := qs.Get("id")
 	id, err := protocol.DeviceIDFromString(idStr)
@@ -1315,7 +1256,7 @@ func (s *apiService) getDeviceID(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func (s *apiService) getLang(w http.ResponseWriter, r *http.Request) {
+func (s *service) getLang(w http.ResponseWriter, r *http.Request) {
 	lang := r.Header.Get("Accept-Language")
 	var langs []string
 	for _, l := range strings.Split(lang, ",") {
@@ -1325,7 +1266,7 @@ func (s *apiService) getLang(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, langs)
 }
 
-func (s *apiService) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
+func (s *service) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
 	opts := s.cfg.Options()
 	rel, err := upgrade.LatestRelease(opts.ReleasesURL, build.Version, opts.UpgradeToPreReleases)
 	if err != nil {
@@ -1343,11 +1284,11 @@ func (s *apiService) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
 		}
 
 		s.flushResponse(`{"ok": "restarting"}`, w)
-		exit.ExitUpgrading()
+		s.contr.ExitUpgrading()
 	}
 }
 
-func (s *apiService) makeDevicePauseHandler(paused bool) http.HandlerFunc {
+func (s *service) makeDevicePauseHandler(paused bool) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		var qs = r.URL.Query()
 		var deviceStr = qs.Get("device")
@@ -1382,7 +1323,7 @@ func (s *apiService) makeDevicePauseHandler(paused bool) http.HandlerFunc {
 	}
 }
 
-func (s *apiService) postDBScan(w http.ResponseWriter, r *http.Request) {
+func (s *service) postDBScan(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	folder := qs.Get("folder")
 	if folder != "" {
@@ -1407,7 +1348,7 @@ func (s *apiService) postDBScan(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func (s *apiService) postDBPrio(w http.ResponseWriter, r *http.Request) {
+func (s *service) postDBPrio(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	folder := qs.Get("folder")
 	file := qs.Get("file")
@@ -1415,7 +1356,7 @@ func (s *apiService) postDBPrio(w http.ResponseWriter, r *http.Request) {
 	s.getDBNeed(w, r)
 }
 
-func (s *apiService) getQR(w http.ResponseWriter, r *http.Request) {
+func (s *service) getQR(w http.ResponseWriter, r *http.Request) {
 	var qs = r.URL.Query()
 	var text = qs.Get("text")
 	code, err := qr.Encode(text, qr.M)
@@ -1428,7 +1369,7 @@ func (s *apiService) getQR(w http.ResponseWriter, r *http.Request) {
 	w.Write(code.PNG())
 }
 
-func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
+func (s *service) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
 	tot := map[string]float64{}
 	count := map[string]float64{}
 
@@ -1452,7 +1393,7 @@ func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, comp)
 }
 
-func (s *apiService) getFolderVersions(w http.ResponseWriter, r *http.Request) {
+func (s *service) getFolderVersions(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	versions, err := s.model.GetFolderVersions(qs.Get("folder"))
 	if err != nil {
@@ -1462,7 +1403,7 @@ func (s *apiService) getFolderVersions(w http.ResponseWriter, r *http.Request) {
 	sendJSON(w, versions)
 }
 
-func (s *apiService) postFolderVersionsRestore(w http.ResponseWriter, r *http.Request) {
+func (s *service) postFolderVersionsRestore(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 
 	bs, err := ioutil.ReadAll(r.Body)
@@ -1487,7 +1428,7 @@ func (s *apiService) postFolderVersionsRestore(w http.ResponseWriter, r *http.Re
 	sendJSON(w, ferr)
 }
 
-func (s *apiService) getFolderErrors(w http.ResponseWriter, r *http.Request) {
+func (s *service) getFolderErrors(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	folder := qs.Get("folder")
 	page, perpage := getPagingParams(qs)
@@ -1517,7 +1458,7 @@ func (s *apiService) getFolderErrors(w http.ResponseWriter, r *http.Request) {
 	})
 }
 
-func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
+func (s *service) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
 	qs := r.URL.Query()
 	current := qs.Get("current")
 
@@ -1596,7 +1537,7 @@ func browseFiles(current string, fsType fs.FilesystemType) []string {
 	return append(exactMatches, caseInsMatches...)
 }
 
-func (s *apiService) getCPUProf(w http.ResponseWriter, r *http.Request) {
+func (s *service) getCPUProf(w http.ResponseWriter, r *http.Request) {
 	duration, err := time.ParseDuration(r.FormValue("duration"))
 	if err != nil {
 		duration = 30 * time.Second
@@ -1613,7 +1554,7 @@ func (s *apiService) getCPUProf(w http.ResponseWriter, r *http.Request) {
 	}
 }
 
-func (s *apiService) getHeapProf(w http.ResponseWriter, r *http.Request) {
+func (s *service) getHeapProf(w http.ResponseWriter, r *http.Request) {
 	filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, build.Version, time.Now().Format("150405")) // hhmmss
 
 	w.Header().Set("Content-Type", "application/octet-stream")

+ 2 - 2
cmd/syncthing/gui_auth.go → lib/api/api_auth.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"bytes"
@@ -53,7 +53,7 @@ func basicAuthAndSessionMiddleware(cookieName string, guiCfg config.GUIConfigura
 			}
 		}
 
-		httpl.Debugln("Sessionless HTTP request with authentication; this is expensive.")
+		l.Debugln("Sessionless HTTP request with authentication; this is expensive.")
 
 		error := func() {
 			time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)

+ 1 - 1
cmd/syncthing/gui_auth_test.go → lib/api/api_auth_test.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"testing"

+ 2 - 2
cmd/syncthing/gui_csrf.go → lib/api/api_csrf.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"bufio"
@@ -57,7 +57,7 @@ func csrfMiddleware(unique string, prefix string, cfg config.GUIConfiguration, n
 		if !strings.HasPrefix(r.URL.Path, prefix) {
 			cookie, err := r.Cookie("CSRF-Token-" + unique)
 			if err != nil || !validCsrfToken(cookie.Value) {
-				httpl.Debugln("new CSRF cookie in response to request for", r.URL)
+				l.Debugln("new CSRF cookie in response to request for", r.URL)
 				cookie = &http.Cookie{
 					Name:  "CSRF-Token-" + unique,
 					Value: newCsrfToken(),

+ 1 - 1
cmd/syncthing/gui_statics.go → lib/api/api_statics.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"bytes"

+ 26 - 13
cmd/syncthing/gui_test.go → lib/api/api_test.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"bytes"
@@ -27,11 +27,25 @@ import (
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
+	"github.com/syncthing/syncthing/lib/locations"
+	"github.com/syncthing/syncthing/lib/model"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/sync"
+	"github.com/syncthing/syncthing/lib/ur"
 	"github.com/thejerf/suture"
 )
 
+func TestMain(m *testing.M) {
+	orig := locations.GetBaseDir(locations.ConfigBaseDir)
+	locations.SetBaseDir(locations.ConfigBaseDir, "testdata/config")
+
+	exitCode := m.Run()
+
+	locations.SetBaseDir(locations.ConfigBaseDir, orig)
+
+	os.Exit(exitCode)
+}
+
 func TestCSRFToken(t *testing.T) {
 	t1 := newCsrfToken()
 	t2 := newCsrfToken()
@@ -74,7 +88,7 @@ func TestStopAfterBrokenConfig(t *testing.T) {
 	}
 	w := config.Wrap("/dev/null", cfg)
 
-	srv := newAPIService(protocol.LocalDeviceID, w, "../../test/h1/https-cert.pem", "../../test/h1/https-key.pem", "", nil, nil, nil, nil, nil, nil, nil, nil)
+	srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, false).(*service)
 	srv.started = make(chan string)
 
 	sup := suture.New("test", suture.Spec{
@@ -180,7 +194,7 @@ func expectURLToContain(t *testing.T, url, exp string) {
 
 func TestDirNames(t *testing.T) {
 	names := dirNames("testdata")
-	expected := []string{"default", "foo", "testfolder"}
+	expected := []string{"config", "default", "foo", "testfolder"}
 	if diff, equal := messagediff.PrettyDiff(expected, names); !equal {
 		t.Errorf("Unexpected dirNames return: %#v\n%s", names, diff)
 	}
@@ -470,9 +484,7 @@ func TestHTTPLogin(t *testing.T) {
 }
 
 func startHTTP(cfg *mockedConfig) (string, error) {
-	model := new(mockedModel)
-	httpsCertFile := "../../test/h1/https-cert.pem"
-	httpsKeyFile := "../../test/h1/https-key.pem"
+	m := new(mockedModel)
 	assetDir := "../../gui"
 	eventSub := new(mockedEventSub)
 	diskEventSub := new(mockedEventSub)
@@ -484,8 +496,9 @@ func startHTTP(cfg *mockedConfig) (string, error) {
 	addrChan := make(chan string)
 
 	// Instantiate the API service
-	svc := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
-		eventSub, diskEventSub, discoverer, connections, errorLog, systemLog, cpu)
+	urService := ur.New(cfg, m, connections, false)
+	summaryService := model.NewFolderSummaryService(cfg, m, protocol.LocalDeviceID)
+	svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, discoverer, connections, urService, summaryService, errorLog, systemLog, cpu, nil, false).(*service)
 	svc.started = addrChan
 
 	// Actually start the API service
@@ -946,10 +959,10 @@ func TestEventMasks(t *testing.T) {
 	cfg := new(mockedConfig)
 	defSub := new(mockedEventSub)
 	diskSub := new(mockedEventSub)
-	svc := newAPIService(protocol.LocalDeviceID, cfg, "", "", "", nil, defSub, diskSub, nil, nil, nil, nil, nil)
+	svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, nil, nil, nil, nil, nil, nil, nil, nil, false).(*service)
 
-	if mask := svc.getEventMask(""); mask != defaultEventMask {
-		t.Errorf("incorrect default mask %x != %x", int64(mask), int64(defaultEventMask))
+	if mask := svc.getEventMask(""); mask != DefaultEventMask {
+		t.Errorf("incorrect default mask %x != %x", int64(mask), int64(DefaultEventMask))
 	}
 
 	expected := events.FolderSummary | events.LocalChangeDetected
@@ -962,10 +975,10 @@ func TestEventMasks(t *testing.T) {
 		t.Errorf("incorrect parsed mask %x != %x", int64(mask), int64(expected))
 	}
 
-	if res := svc.getEventSub(defaultEventMask); res != defSub {
+	if res := svc.getEventSub(DefaultEventMask); res != defSub {
 		t.Errorf("should have returned the given default event sub")
 	}
-	if res := svc.getEventSub(diskEventMask); res != diskSub {
+	if res := svc.getEventSub(DiskEventMask); res != diskSub {
 		t.Errorf("should have returned the given disk event sub")
 	}
 	if res := svc.getEventSub(events.LocalIndexUpdated); res == nil || res == defSub || res == diskSub {

+ 28 - 0
lib/api/debug.go

@@ -0,0 +1,28 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package api
+
+import (
+	"os"
+	"strings"
+
+	"github.com/syncthing/syncthing/lib/logger"
+)
+
+var (
+	l = logger.DefaultLogger.NewFacility("api", "REST API")
+)
+
+func shouldDebugHTTP() bool {
+	return l.ShouldDebug("api")
+}
+
+func init() {
+	// The debug facility was originally named "http", changed in:
+	// https://github.com/syncthing/syncthing/pull/5548
+	l.SetDebug("api", strings.Contains(os.Getenv("STTRACE"), "api") || strings.Contains(os.Getenv("STTRACE"), "http") || os.Getenv("STTRACE") == "all")
+}

+ 1 - 1
cmd/syncthing/mocked_config_test.go → lib/api/mocked_config_test.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"github.com/syncthing/syncthing/lib/config"

+ 1 - 1
cmd/syncthing/mocked_connections_test.go → lib/api/mocked_connections_test.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 type mockedConnections struct{}
 

+ 1 - 1
cmd/syncthing/mocked_cpuusage_test.go → lib/api/mocked_cpuusage_test.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 type mockedCPUService struct{}
 

+ 1 - 1
cmd/syncthing/mocked_discovery_test.go → lib/api/mocked_discovery_test.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"time"

+ 1 - 1
cmd/syncthing/mocked_events_test.go → lib/api/mocked_events_test.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"time"

+ 1 - 1
cmd/syncthing/mocked_logger_test.go → lib/api/mocked_logger_test.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"time"

+ 1 - 1
cmd/syncthing/mocked_model_test.go → lib/api/mocked_model_test.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"net"

+ 2 - 2
cmd/syncthing/support_bundle.go → lib/api/support_bundle.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package api
 
 import (
 	"archive/zip"
@@ -14,7 +14,7 @@ import (
 )
 
 // getRedactedConfig redacting some parts of config
-func getRedactedConfig(s *apiService) config.Configuration {
+func getRedactedConfig(s *service) config.Configuration {
 	rawConf := s.cfg.RawCopy()
 	rawConf.GUI.APIKey = "REDACTED"
 	if rawConf.GUI.Password != "" {

+ 0 - 0
cmd/syncthing/testdata/.stfolder → lib/api/testdata/.stfolder


+ 23 - 0
lib/api/testdata/config/cert.pem

@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID3jCCAkigAwIBAgIBADALBgkqhkiG9w0BAQUwFDESMBAGA1UEAxMJc3luY3Ro
+aW5nMB4XDTE0MDMxNDA3MDA1M1oXDTQ5MTIzMTIzNTk1OVowFDESMBAGA1UEAxMJ
+c3luY3RoaW5nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEArDOcd5ft
+R7SnalxF1ckU3lDQpgfMIPhFDU//4dvdSSFevrMuVDTbUYhyCfGtg/g+F5TmKhZg
+E2peYhllITupz5MP7OHGaO2GHf2XnUDD4QUO3E+KVAUw7dyFSwy09esqApVLzH3+
+ov+QXyyzmRWPsJe9u18BHU1Hob/RmBhS9m2CAJgzN6EJ8KGjApiW3iR8lD/hjVyi
+IVde8IRD6qYHEJYiPJuziTVcQpCblVYxTz3ScmmT190/O9UvViIpcOPQdwgOdewP
+NNMK35c9Edt0AH5flYp6jgrja9NkLQJ3+KOiro6yl9IUS5w87GMxI8qzI8SgCAZZ
+pYSoLbu1FJPvxV4p5eHwuprBCwmFYZWw6Y7rqH0sN52C+3TeObJCMNP9ilPadqRI
++G0Q99TCaloeR022x33r/8D8SIn3FP35zrlFM+DvqlxoS6glbNb/Bj3p9vN0XONO
+RCuynOGe9F/4h/DaNnrbrRWqJOxBsZTsbbcJaKATfWU/Z9GcC+pUpPRhAgMBAAGj
+PzA9MA4GA1UdDwEB/wQEAwIAoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
+AwIwDAYDVR0TAQH/BAIwADALBgkqhkiG9w0BAQUDggGBAFF8dklGoC43fMrUZfb4
+6areRWG8quO6cSX6ATzRQVJ8WJ5VcC7OJk8/FeiYA+wcvUJ/1Zm/VHMYugtOz5M8
+CrWAF1r9D3Xfe5D8qfrEOYG2XjxD2nFHCnkbY4fP+SMSuXaDs7ixQnzw0UFh1wsV
+9Jy/QrgXFAIFZtu1Nz+rrvoAgw24gkDhY3557MbmYfmfPsJ8cw+WJ845sxGMPFF2
+c+5EN0jiSm0AwZK11BMJda36ke829UZctDkopbGEg1peydDR5LiyhiTAPtWn7uT/
+PkzHYLuaECAkVbWC3bZLocMGOP6F1pG+BMr00NJgVy05ASQzi4FPjcZQNNY8s69R
+ZgoCIBaJZq3ti1EsZQ1H0Ynm2c2NMVKdj4czoy8a9ZC+DCuhG7EV5Foh20VhCWgA
+RfPhlHVJthuimsWBx39X85gjSBR017uk0AxOJa6pzh/b/RPCRtUfX8EArInS3XCf
+RvRtdrnBZNI3tiREopZGt0SzgDZUs4uDVBUX8HnHzyFJrg==
+-----END CERTIFICATE-----

+ 134 - 0
lib/api/testdata/config/config.xml

@@ -0,0 +1,134 @@
+<configuration version="28">
+    <folder id="default" label="" path="s1/" type="sendreceive" rescanIntervalS="10" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
+        <filesystemType>basic</filesystemType>
+        <device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy=""></device>
+        <device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" introducedBy=""></device>
+        <device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" introducedBy=""></device>
+        <device id="7PBCTLL-JJRYBSA-MOWZRKL-MSDMN4N-4US4OMX-SYEXUS4-HSBGNRY-CZXRXAT" introducedBy=""></device>
+        <minDiskFree unit="%">1</minDiskFree>
+        <versioning></versioning>
+        <copiers>1</copiers>
+        <pullerMaxPendingKiB>0</pullerMaxPendingKiB>
+        <hashers>0</hashers>
+        <order>random</order>
+        <ignoreDelete>false</ignoreDelete>
+        <scanProgressIntervalS>0</scanProgressIntervalS>
+        <pullerPauseS>0</pullerPauseS>
+        <maxConflicts>-1</maxConflicts>
+        <disableSparseFiles>false</disableSparseFiles>
+        <disableTempIndexes>false</disableTempIndexes>
+        <paused>false</paused>
+        <weakHashThresholdPct>25</weakHashThresholdPct>
+        <markerName>.stfolder</markerName>
+        <useLargeBlocks>true</useLargeBlocks>
+    </folder>
+    <folder id="¯\_(ツ)_/¯ Räksmörgås 动作 Адрес" label="" path="s12-1/" type="sendreceive" rescanIntervalS="10" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
+        <filesystemType>basic</filesystemType>
+        <device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" introducedBy=""></device>
+        <device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" introducedBy=""></device>
+        <minDiskFree unit="%">1</minDiskFree>
+        <versioning></versioning>
+        <copiers>1</copiers>
+        <pullerMaxPendingKiB>0</pullerMaxPendingKiB>
+        <hashers>0</hashers>
+        <order>random</order>
+        <ignoreDelete>false</ignoreDelete>
+        <scanProgressIntervalS>0</scanProgressIntervalS>
+        <pullerPauseS>0</pullerPauseS>
+        <maxConflicts>-1</maxConflicts>
+        <disableSparseFiles>false</disableSparseFiles>
+        <disableTempIndexes>false</disableTempIndexes>
+        <paused>false</paused>
+        <weakHashThresholdPct>25</weakHashThresholdPct>
+        <markerName>.stfolder</markerName>
+        <useLargeBlocks>true</useLargeBlocks>
+    </folder>
+    <device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK" name="s4" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
+        <address>tcp://127.0.0.1:22004</address>
+        <paused>false</paused>
+        <autoAcceptFolders>false</autoAcceptFolders>
+        <maxSendKbps>0</maxSendKbps>
+        <maxRecvKbps>0</maxRecvKbps>
+        <maxRequestKiB>0</maxRequestKiB>
+    </device>
+    <device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
+        <address>tcp://127.0.0.1:22001</address>
+        <paused>false</paused>
+        <autoAcceptFolders>false</autoAcceptFolders>
+        <maxSendKbps>0</maxSendKbps>
+        <maxRecvKbps>0</maxRecvKbps>
+        <maxRequestKiB>0</maxRequestKiB>
+    </device>
+    <device id="MRIW7OK-NETT3M4-N6SBWME-N25O76W-YJKVXPH-FUMQJ3S-P57B74J-GBITBAC" name="s2" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
+        <address>tcp://127.0.0.1:22002</address>
+        <paused>false</paused>
+        <autoAcceptFolders>false</autoAcceptFolders>
+        <maxSendKbps>0</maxSendKbps>
+        <maxRecvKbps>0</maxRecvKbps>
+        <maxRequestKiB>0</maxRequestKiB>
+    </device>
+    <device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
+        <address>tcp://127.0.0.1:22003</address>
+        <paused>false</paused>
+        <autoAcceptFolders>false</autoAcceptFolders>
+        <maxSendKbps>0</maxSendKbps>
+        <maxRecvKbps>0</maxRecvKbps>
+        <maxRequestKiB>0</maxRequestKiB>
+    </device>
+    <device id="7PBCTLL-JJRYBSA-MOWZRKL-MSDMN4N-4US4OMX-SYEXUS4-HSBGNRY-CZXRXAT" name="s4" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
+        <address>tcp://127.0.0.1:22004</address>
+        <paused>false</paused>
+        <autoAcceptFolders>false</autoAcceptFolders>
+        <maxSendKbps>0</maxSendKbps>
+        <maxRecvKbps>0</maxRecvKbps>
+        <maxRequestKiB>0</maxRequestKiB>
+    </device>
+    <gui enabled="true" tls="false" debugging="true">
+        <address>127.0.0.1:8081</address>
+        <user>testuser</user>
+        <password>$2a$10$7tKL5uvLDGn5s2VLPM2yWOK/II45az0mTel8hxAUJDRQN1Tk2QYwu</password>
+        <apikey>abc123</apikey>
+        <theme>default</theme>
+    </gui>
+    <ldap></ldap>
+    <options>
+        <listenAddress>tcp://127.0.0.1:22001</listenAddress>
+        <globalAnnounceServer>default</globalAnnounceServer>
+        <globalAnnounceEnabled>false</globalAnnounceEnabled>
+        <localAnnounceEnabled>true</localAnnounceEnabled>
+        <localAnnouncePort>21027</localAnnouncePort>
+        <localAnnounceMCAddr>[ff12::8384]:21027</localAnnounceMCAddr>
+        <maxSendKbps>0</maxSendKbps>
+        <maxRecvKbps>0</maxRecvKbps>
+        <reconnectionIntervalS>5</reconnectionIntervalS>
+        <relaysEnabled>false</relaysEnabled>
+        <relayReconnectIntervalM>10</relayReconnectIntervalM>
+        <startBrowser>false</startBrowser>
+        <natEnabled>true</natEnabled>
+        <natLeaseMinutes>0</natLeaseMinutes>
+        <natRenewalMinutes>30</natRenewalMinutes>
+        <natTimeoutSeconds>10</natTimeoutSeconds>
+        <urAccepted>3</urAccepted>
+        <urSeen>2</urSeen>
+        <urUniqueID>tmwxxCqi</urUniqueID>
+        <urURL>https://data.syncthing.net/newdata</urURL>
+        <urPostInsecurely>false</urPostInsecurely>
+        <urInitialDelayS>1800</urInitialDelayS>
+        <restartOnWakeup>true</restartOnWakeup>
+        <autoUpgradeIntervalH>12</autoUpgradeIntervalH>
+        <upgradeToPreReleases>false</upgradeToPreReleases>
+        <keepTemporariesH>24</keepTemporariesH>
+        <cacheIgnoredFiles>false</cacheIgnoredFiles>
+        <progressUpdateIntervalS>5</progressUpdateIntervalS>
+        <limitBandwidthInLan>false</limitBandwidthInLan>
+        <minHomeDiskFree unit="%">1</minHomeDiskFree>
+        <releasesURL>https://upgrades.syncthing.net/meta.json</releasesURL>
+        <overwriteRemoteDeviceNamesOnConnect>false</overwriteRemoteDeviceNamesOnConnect>
+        <tempIndexMinBlocks>10</tempIndexMinBlocks>
+        <trafficClass>0</trafficClass>
+        <defaultFolderPath>~</defaultFolderPath>
+        <setLowPriority>true</setLowPriority>
+        <maxConcurrentScans>0</maxConcurrentScans>
+        <minHomeDiskFreePct>0</minHomeDiskFreePct>
+    </options>
+</configuration>

+ 23 - 0
lib/api/testdata/config/https-cert.pem

@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID5TCCAk+gAwIBAgIIBYqoKiSgB+owCwYJKoZIhvcNAQELMBQxEjAQBgNVBAMT
+CXN5bmN0aGluZzAeFw0xNDA5MTQyMjIzMzVaFw00OTEyMzEyMzU5NTlaMBQxEjAQ
+BgNVBAMTCXN5bmN0aGluZzCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGB
+AKZK/sjb6ZuVVHPvo77Cp5E8LfiznfoIWJRoX/MczE99iDyFZm1Wf9GFT8WhXICM
+C2kgGbr/gAxhkeEcZ500vhA2C+aois1DGcb+vNY53I0qp3vSUl4ow55R0xJ4UjpJ
+nJWF8p9iPDMwMP6WQ/E/ekKRKCOt0TFj4xqtiSt0pxPLeHfKVpWXxqIVDhnsoGQ+
+NWuUjM3FkmEmhp5DdRtwskiZZYz1zCgoHkFzKt/+IxjCuzbO0+Ti8R3b/d0A+WLN
+LHr0SjatajLbHebA+9c3ts6t3V5YzcMqDJ4MyxFtRoXFJjEbcM9IqKQE8t8TIhv8
+a302yRikJ2uPx+fXJGospnmWCbaK2rViPbvICSgvSBA3As0f3yPzXsEt+aW5NmDV
+fLBX1DU7Ow6oBqZTlI+STrzZR1qfvIuweIWoPqnPNd4sxuoxAK50ViUKdOtSYL/a
+F0eM3bqbp2ozhct+Bfmqu2oI/RHXe+RUfAXrlFQ8p6jcISW2ip+oiBtR4GZkncI9
+YQIDAQABoz8wPTAOBgNVHQ8BAf8EBAMCAKAwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
+CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJKoZIhvcNAQELA4IBgQBsYc5XVQy5
+aJVdwx+mAKiuCs5ZCvV4H4VWY9XUwEJuUUD3yXw2xyzuQl5+lOxfiQcaudhVwARC
+Dao75MUctXmx1YU+J5G31cGdC9kbxWuo1xypkK+2Zl+Kwh65aod3OkHVz9oNkKpf
+JnXbdph4UiFJzijSruXDDaerrQdABUvlusPozZn8vMwZ21Ls/eNIOJvA0S2d2jep
+fvmu7yQPejDp7zcgPdmneuZqmUyXLxxFopYqHqFQVM8f+Y8iZ8HnMiAJgLKQcmro
+pp1z/NY0Xr0pLyBY5d/sO+tZmQkyUEWegHtEtQQOO+x8BWinDEAurej/YvZTWTmN
++YoUvGdKyV6XfC6WPFcUDFHY4KPSqS3xoLmoVV4xNjJU3aG/xL4uDencNZR/UFNw
+wKsdvm9SX4TpSLlQa0wu1iNv7QyeR4ZKgaBNSwp2rxpatOi7TTs9KRPfjLFLpYAg
+bIons/a890SIxpuneuhQZkH63t930EXIZ+9GkU0aUs7MFg5cCmwmlvE=
+-----END CERTIFICATE-----

+ 39 - 0
lib/api/testdata/config/https-key.pem

@@ -0,0 +1,39 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----

+ 39 - 0
lib/api/testdata/config/key.pem

@@ -0,0 +1,39 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIG5AIBAAKCAYEArDOcd5ftR7SnalxF1ckU3lDQpgfMIPhFDU//4dvdSSFevrMu
+VDTbUYhyCfGtg/g+F5TmKhZgE2peYhllITupz5MP7OHGaO2GHf2XnUDD4QUO3E+K
+VAUw7dyFSwy09esqApVLzH3+ov+QXyyzmRWPsJe9u18BHU1Hob/RmBhS9m2CAJgz
+N6EJ8KGjApiW3iR8lD/hjVyiIVde8IRD6qYHEJYiPJuziTVcQpCblVYxTz3ScmmT
+190/O9UvViIpcOPQdwgOdewPNNMK35c9Edt0AH5flYp6jgrja9NkLQJ3+KOiro6y
+l9IUS5w87GMxI8qzI8SgCAZZpYSoLbu1FJPvxV4p5eHwuprBCwmFYZWw6Y7rqH0s
+N52C+3TeObJCMNP9ilPadqRI+G0Q99TCaloeR022x33r/8D8SIn3FP35zrlFM+Dv
+qlxoS6glbNb/Bj3p9vN0XONORCuynOGe9F/4h/DaNnrbrRWqJOxBsZTsbbcJaKAT
+fWU/Z9GcC+pUpPRhAgMBAAECggGAL8+Unc/c3Y/W+7zq1tShqqgdhjub/XtxEKUp
+kngNFITjXWc6cb7LNfQAVap4Vq/R7ZI15XGY80sRMYODhJqgJzXZshdtkyx/lEwY
+kFyvBgb1fU3IRlO6phAYIiJBDBZi75ysEvbYgEEcwJAUvWgzIQDAeQmDsbMHNG2h
+r+zw++Kjua6IaeWYcOsv60Safsr6m96wrSMPENrFTVor0TaPt5c3okRIsMvT9ddY
+mzn3Lt0nVQTjO4f+SoqCPhP2FZXqksfKlZlKlr6BLxXGt6b49OrLSXM5eQXIcIZn
+ZDRsO24X5z8156qPgM9cA8oNEjuSdnArUTreBOsTwNoSpf24Qadsv/uTZlaHM19V
+q6zQvkjH3ERcOpixmg48TKdIj8cPYxezvcbNqSbZmdyQuaVlgDbUxwYI8A4IhhWl
+6xhwpX3qPDgw/QHIEngFIWfiIfCk11EPY0SN4cGO6f1rLYug8kqxMPuIQ5Jz9Hhx
+eFSRnr/fWoJcVYG6bMDKn9YWObQBAoHBAM8NahsLbjl8mdT43LH1Od1tDmDch+0Y
+JM7TgiIN/GM3piZSpGMOFqToLAqvY+Gf3l4sPgNs10cqdPAEpMk8MJ/IXGmbKq38
+iVmMaqHTQorCxyUbc54q9AbFU4HKv//F6ZN6K1wSaJt2RBeZpYI+MyBXr5baFiBZ
+ddXtXlqoEcCFyNR0DhlXrlZPs+cnyM2ZDp++lpn9Wfy+zkv36+NWpAkXVnARjxdF
+l6M+L7OlurYAWiyJE4uHUjawAM82i5+w8QKBwQDU6RCN6/AMmVrYqPy+7QcnAq67
+tPDv25gzVExeMKLBAMoz1TkMS+jIF1NMp3cYg5GbLqvx8Qd27fjFbWe/GPeZvlgL
+qdQI/T8J60dHAySMeOFOB2QWXhI1kwh0b2X0SDkTgfdJBKGdrKVcLTuLyVE24exu
+yRc8cXpYwBtVkXNBYFd7XEM+tC4b1khO23OJXHJUen9+hgsmn8/zUjASAoq3+Zly
+J+OHwwXcDcTFLeok3kX3A9NuqIV/Fa9DOGYlenECgcEAvO1onDTZ5uqjE4nhFyDE
+JB+WtxuDi/wz2eV1IM3SNlZY7S8LgLciQmb3iOhxIzdVGGkWTNnLtcwv17LlCho5
+5BJXAKXtU8TTLzrJMdArL6J7RIi//tsCwAreH9h5SVG1yDP5zJGfkftgNoikVSuc
+Sy63sdZdyjbXJtTo+5/QUvPARNuA4e73zRn89jd/Kts2VNz7XpemvND+PKOEQnSU
+SRdab/gVsQ53RyU/MZVPwTKhFXIeu3pGsk/27RzAWn6BAoHBAMIRYwaKDffd/SHJ
+/v+lHEThvBXa21c26ae36hhc6q1UI/tVGrfrpVZldIdFilgs7RbvVsmksvIj/gMv
+M0bL4j0gdC7FcUF0XPaUoBbJdZIZSP0P3ZpJyv1MdYN0WxFsl6IBcD79WrdXPC8m
+B8XmDgIhsppU77onkaa+DOxVNSJdR8BpG95W7ERxcN14SPrm6ku4kOfqFNXzC+C1
+hJ2V9Y22lLiqRUplaLzpS/eTX36VoF6E/T87mtt5D5UNHoaA8QKBwH5sRqZXoatU
+X+vw1MHU5eptMwG7LXR0gw2xmvG3cCN4hbnnBp5YaXlWPiIMmaWhpvschgBIo1TP
+qGWUpMEETGES18NenLBym+tWIXlfuyZH3B4NUi4kItiZaKb09LzmTjFvzdfQzun4
+HzIeigTNBDHdS0rdicNIn83QLZ4pJaOZJHq79+mFYkp+9It7UUoWsws6DGl/qX8o
+0cj4NmJB6QiJa1QCzrGkaajbtThbFoQal9Twk2h3jHgJzX3FbwCpLw==
+-----END RSA PRIVATE KEY-----

+ 0 - 0
cmd/syncthing/testdata/default/a → lib/api/testdata/default/a


+ 0 - 0
cmd/syncthing/testdata/default/b → lib/api/testdata/default/b


+ 0 - 0
cmd/syncthing/testdata/default/d → lib/api/testdata/default/d


+ 0 - 0
cmd/syncthing/testdata/foo/a → lib/api/testdata/foo/a


+ 0 - 0
cmd/syncthing/testdata/testfolder/.stfolder → lib/api/testdata/testfolder/.stfolder


+ 93 - 14
cmd/syncthing/summaryservice.go → lib/model/folder_summary.go

@@ -4,26 +4,36 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package model
 
 import (
+	"fmt"
+	"strings"
 	"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"
 )
 
+const minSummaryInterval = time.Minute
+
+type FolderSummaryService interface {
+	suture.Service
+	Summary(folder string) (map[string]interface{}, error)
+	OnEventRequest()
+}
+
 // The folderSummaryService adds summary information events (FolderSummary and
 // FolderCompletion) into the event stream at certain intervals.
 type folderSummaryService struct {
 	*suture.Supervisor
 
 	cfg       config.Wrapper
-	model     model.Model
+	model     Model
+	id        protocol.DeviceID
 	stop      chan struct{}
 	immediate chan string
 
@@ -36,13 +46,14 @@ type folderSummaryService struct {
 	lastEventReqMut sync.Mutex
 }
 
-func newFolderSummaryService(cfg config.Wrapper, m model.Model) *folderSummaryService {
+func NewFolderSummaryService(cfg config.Wrapper, m Model, id protocol.DeviceID) FolderSummaryService {
 	service := &folderSummaryService{
 		Supervisor: suture.New("folderSummaryService", suture.Spec{
 			PassThroughPanics: true,
 		}),
 		cfg:             cfg,
 		model:           m,
+		id:              id,
 		stop:            make(chan struct{}),
 		immediate:       make(chan string),
 		folders:         make(map[string]struct{}),
@@ -61,6 +72,80 @@ func (c *folderSummaryService) Stop() {
 	close(c.stop)
 }
 
+func (c *folderSummaryService) String() string {
+	return fmt.Sprintf("FolderSummaryService@%p", c)
+}
+
+func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, error) {
+	var res = make(map[string]interface{})
+
+	errors, err := c.model.FolderErrors(folder)
+	if err != nil && err != ErrFolderPaused {
+		// Stats from the db can still be obtained if the folder is just paused
+		return nil, err
+	}
+	res["errors"] = len(errors)
+	res["pullErrors"] = len(errors) // deprecated
+
+	res["invalid"] = "" // Deprecated, retains external API for now
+
+	global := c.model.GlobalSize(folder)
+	res["globalFiles"], res["globalDirectories"], res["globalSymlinks"], res["globalDeleted"], res["globalBytes"], res["globalTotalItems"] = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes, global.TotalItems()
+
+	local := c.model.LocalSize(folder)
+	res["localFiles"], res["localDirectories"], res["localSymlinks"], res["localDeleted"], res["localBytes"], res["localTotalItems"] = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes, local.TotalItems()
+
+	need := c.model.NeedSize(folder)
+	res["needFiles"], res["needDirectories"], res["needSymlinks"], res["needDeletes"], res["needBytes"], res["needTotalItems"] = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes, need.TotalItems()
+
+	if c.cfg.Folders()[folder].Type == config.FolderTypeReceiveOnly {
+		// Add statistics for things that have changed locally in a receive
+		// only folder.
+		ro := c.model.ReceiveOnlyChangedSize(folder)
+		res["receiveOnlyChangedFiles"] = ro.Files
+		res["receiveOnlyChangedDirectories"] = ro.Directories
+		res["receiveOnlyChangedSymlinks"] = ro.Symlinks
+		res["receiveOnlyChangedDeletes"] = ro.Deleted
+		res["receiveOnlyChangedBytes"] = ro.Bytes
+		res["receiveOnlyTotalItems"] = ro.TotalItems()
+	}
+
+	res["inSyncFiles"], res["inSyncBytes"] = global.Files-need.Files, global.Bytes-need.Bytes
+
+	res["state"], res["stateChanged"], err = c.model.State(folder)
+	if err != nil {
+		res["error"] = err.Error()
+	}
+
+	ourSeq, _ := c.model.CurrentSequence(folder)
+	remoteSeq, _ := c.model.RemoteSequence(folder)
+
+	res["version"] = ourSeq + remoteSeq  // legacy
+	res["sequence"] = ourSeq + remoteSeq // new name
+
+	ignorePatterns, _, _ := c.model.GetIgnores(folder)
+	res["ignorePatterns"] = false
+	for _, line := range ignorePatterns {
+		if len(line) > 0 && !strings.HasPrefix(line, "//") {
+			res["ignorePatterns"] = true
+			break
+		}
+	}
+
+	err = c.model.WatchError(folder)
+	if err != nil {
+		res["watchError"] = err.Error()
+	}
+
+	return res, nil
+}
+
+func (c *folderSummaryService) OnEventRequest() {
+	c.lastEventReqMut.Lock()
+	c.lastEventReq = time.Now()
+	c.lastEventReqMut.Unlock()
+}
+
 // listenForUpdates subscribes to the event bus and makes note of folders that
 // need their data recalculated.
 func (c *folderSummaryService) listenForUpdates() {
@@ -173,7 +258,7 @@ func (c *folderSummaryService) foldersToHandle() []string {
 	c.lastEventReqMut.Lock()
 	last := c.lastEventReq
 	c.lastEventReqMut.Unlock()
-	if time.Since(last) > defaultEventTimeout {
+	if time.Since(last) > minSummaryInterval {
 		return nil
 	}
 
@@ -191,7 +276,7 @@ func (c *folderSummaryService) foldersToHandle() []string {
 func (c *folderSummaryService) sendSummary(folder string) {
 	// The folder summary contains how many bytes, files etc
 	// are in the folder and how in sync we are.
-	data, err := folderSummary(c.cfg, c.model, folder)
+	data, err := c.Summary(folder)
 	if err != nil {
 		return
 	}
@@ -201,7 +286,7 @@ func (c *folderSummaryService) sendSummary(folder string) {
 	})
 
 	for _, devCfg := range c.cfg.Folders()[folder].Devices {
-		if devCfg.DeviceID.Equals(myID) {
+		if devCfg.DeviceID.Equals(c.id) {
 			// We already know about ourselves.
 			continue
 		}
@@ -212,19 +297,13 @@ func (c *folderSummaryService) sendSummary(folder string) {
 
 		// Get completion percentage of this folder for the
 		// remote device.
-		comp := jsonCompletion(c.model.Completion(devCfg.DeviceID, folder))
+		comp := c.model.Completion(devCfg.DeviceID, folder).Map()
 		comp["folder"] = folder
 		comp["device"] = devCfg.DeviceID.String()
 		events.Default.Log(events.FolderCompletion, comp)
 	}
 }
 
-func (c *folderSummaryService) gotEventRequest() {
-	c.lastEventReqMut.Lock()
-	c.lastEventReq = time.Now()
-	c.lastEventReqMut.Unlock()
-}
-
 // serviceFunc wraps a function to create a suture.Service without stop
 // functionality.
 type serviceFunc func()

+ 11 - 0
lib/model/model.go

@@ -665,6 +665,17 @@ type FolderCompletion struct {
 	NeedDeletes   int64
 }
 
+// Map returns the members as a map, e.g. used in api to serialize as Json.
+func (comp FolderCompletion) Map() map[string]interface{} {
+	return map[string]interface{}{
+		"completion":  comp.CompletionPct,
+		"needBytes":   comp.NeedBytes,
+		"needItems":   comp.NeedItems,
+		"globalBytes": comp.GlobalBytes,
+		"needDeletes": comp.NeedDeletes,
+	}
+}
+
 // Completion returns the completion status, in percent, for the given device
 // and folder.
 func (m *model) Completion(device protocol.DeviceID, folder string) FolderCompletion {

+ 22 - 0
lib/ur/debug.go

@@ -0,0 +1,22 @@
+// Copyright (C) 2014 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package ur
+
+import (
+	"os"
+	"strings"
+
+	"github.com/syncthing/syncthing/lib/logger"
+)
+
+var (
+	l = logger.DefaultLogger.NewFacility("ur", "Usage reporting")
+)
+
+func init() {
+	l.SetDebug("ur", strings.Contains(os.Getenv("STTRACE"), "ur") || os.Getenv("STTRACE") == "all")
+}

+ 1 - 1
cmd/syncthing/memsize_darwin.go → lib/ur/memsize_darwin.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package ur
 
 import (
 	"errors"

+ 1 - 1
cmd/syncthing/memsize_linux.go → lib/ur/memsize_linux.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package ur
 
 import (
 	"bufio"

+ 1 - 1
cmd/syncthing/memsize_netbsd.go → lib/ur/memsize_netbsd.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package ur
 
 import (
 	"errors"

+ 1 - 1
cmd/syncthing/memsize_solaris.go → lib/ur/memsize_solaris.go

@@ -6,7 +6,7 @@
 
 // +build solaris
 
-package main
+package ur
 
 import (
 	"os/exec"

+ 1 - 1
cmd/syncthing/memsize_unimpl.go → lib/ur/memsize_unimpl.go

@@ -6,7 +6,7 @@
 
 // +build freebsd openbsd dragonfly
 
-package main
+package ur
 
 import "errors"
 

+ 1 - 1
cmd/syncthing/memsize_windows.go → lib/ur/memsize_windows.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package ur
 
 import (
 	"encoding/binary"

+ 75 - 57
cmd/syncthing/usage_report.go → lib/ur/usage_report.go

@@ -4,7 +4,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this file,
 // You can obtain one at https://mozilla.org/MPL/2.0/.
 
-package main
+package ur
 
 import (
 	"bytes"
@@ -33,25 +33,63 @@ import (
 // Current version number of the usage report, for acceptance purposes. If
 // fields are added or changed this integer must be incremented so that users
 // are prompted for acceptance of the new report.
-const usageReportVersion = 3
+const Version = 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 config.Wrapper, m model.Model, connectionsService connections.Service, version int, preview bool) map[string]interface{} {
-	opts := cfg.Options()
+var StartTime = time.Now()
+
+type Service struct {
+	cfg                config.Wrapper
+	model              model.Model
+	connectionsService connections.Service
+	noUpgrade          bool
+	forceRun           chan struct{}
+	stop               chan struct{}
+	stopped            chan struct{}
+	stopMut            sync.RWMutex
+}
+
+func New(cfg config.Wrapper, m model.Model, connectionsService connections.Service, noUpgrade bool) *Service {
+	svc := &Service{
+		cfg:                cfg,
+		model:              m,
+		connectionsService: connectionsService,
+		noUpgrade:          noUpgrade,
+		forceRun:           make(chan struct{}),
+		stop:               make(chan struct{}),
+		stopped:            make(chan struct{}),
+	}
+	close(svc.stopped) // Not yet running, dont block on Stop()
+	cfg.Subscribe(svc)
+	return svc
+}
+
+// ReportData returns the data to be sent in a usage report with the currently
+// configured usage reporting version.
+func (s *Service) ReportData() map[string]interface{} {
+	return s.reportData(Version, false)
+}
+
+// ReportDataPreview returns a preview of the data to be sent in a usage report
+// with the given version.
+func (s *Service) ReportDataPreview(urVersion int) map[string]interface{} {
+	return s.reportData(urVersion, true)
+}
+
+func (s *Service) reportData(urVersion int, preview bool) map[string]interface{} {
+	opts := s.cfg.Options()
 	res := make(map[string]interface{})
-	res["urVersion"] = version
+	res["urVersion"] = urVersion
 	res["uniqueID"] = opts.URUniqueID
 	res["version"] = build.Version
 	res["longVersion"] = build.LongVersion
 	res["platform"] = runtime.GOOS + "-" + runtime.GOARCH
-	res["numFolders"] = len(cfg.Folders())
-	res["numDevices"] = len(cfg.Devices())
+	res["numFolders"] = len(s.cfg.Folders())
+	res["numDevices"] = len(s.cfg.Devices())
 
 	var totFiles, maxFiles int
 	var totBytes, maxBytes int64
-	for folderID := range cfg.Folders() {
-		global := m.GlobalSize(folderID)
+	for folderID := range s.cfg.Folders() {
+		global := s.model.GlobalSize(folderID)
 		totFiles += int(global.Files)
 		totBytes += global.Bytes
 		if int(global.Files) > maxFiles {
@@ -70,8 +108,8 @@ func reportData(cfg config.Wrapper, m model.Model, connectionsService connection
 	var mem runtime.MemStats
 	runtime.ReadMemStats(&mem)
 	res["memoryUsageMiB"] = (mem.Sys - mem.HeapReleased) / 1024 / 1024
-	res["sha256Perf"] = cpuBench(5, 125*time.Millisecond, false)
-	res["hashPerf"] = cpuBench(5, 125*time.Millisecond, true)
+	res["sha256Perf"] = CpuBench(5, 125*time.Millisecond, false)
+	res["hashPerf"] = CpuBench(5, 125*time.Millisecond, true)
 
 	bytes, err := memorySize()
 	if err == nil {
@@ -92,7 +130,7 @@ func reportData(cfg config.Wrapper, m model.Model, connectionsService connection
 		"staggeredVersioning": 0,
 		"trashcanVersioning":  0,
 	}
-	for _, cfg := range cfg.Folders() {
+	for _, cfg := range s.cfg.Folders() {
 		rescanIntvs = append(rescanIntvs, cfg.RescanIntervalS)
 
 		switch cfg.Type {
@@ -129,7 +167,7 @@ func reportData(cfg config.Wrapper, m model.Model, connectionsService connection
 		"dynamicAddr":      0,
 		"staticAddr":       0,
 	}
-	for _, cfg := range cfg.Devices() {
+	for _, cfg := range s.cfg.Devices() {
 		if cfg.Introducer {
 			deviceUses["introducer"]++
 		}
@@ -170,7 +208,7 @@ func reportData(cfg config.Wrapper, m model.Model, connectionsService connection
 	}
 
 	defaultRelayServers, otherRelayServers := 0, 0
-	for _, addr := range cfg.ListenAddresses() {
+	for _, addr := range s.cfg.ListenAddresses() {
 		switch {
 		case addr == "dynamic+https://relays.syncthing.net/endpoint":
 			defaultRelayServers++
@@ -186,13 +224,13 @@ func reportData(cfg config.Wrapper, m model.Model, connectionsService connection
 
 	res["usesRateLimit"] = opts.MaxRecvKbps > 0 || opts.MaxSendKbps > 0
 
-	res["upgradeAllowedManual"] = !(upgrade.DisabledByCompilation || noUpgradeFromEnv)
-	res["upgradeAllowedAuto"] = !(upgrade.DisabledByCompilation || noUpgradeFromEnv) && opts.AutoUpgradeIntervalH > 0
-	res["upgradeAllowedPre"] = !(upgrade.DisabledByCompilation || noUpgradeFromEnv) && opts.AutoUpgradeIntervalH > 0 && opts.UpgradeToPreReleases
+	res["upgradeAllowedManual"] = !(upgrade.DisabledByCompilation || s.noUpgrade)
+	res["upgradeAllowedAuto"] = !(upgrade.DisabledByCompilation || s.noUpgrade) && opts.AutoUpgradeIntervalH > 0
+	res["upgradeAllowedPre"] = !(upgrade.DisabledByCompilation || s.noUpgrade) && opts.AutoUpgradeIntervalH > 0 && opts.UpgradeToPreReleases
 
-	if version >= 3 {
-		res["uptime"] = int(time.Since(startTime).Seconds())
-		res["natType"] = connectionsService.NATType()
+	if urVersion >= 3 {
+		res["uptime"] = s.UptimeS()
+		res["natType"] = s.connectionsService.NATType()
 		res["alwaysLocalNets"] = len(opts.AlwaysLocalNets) > 0
 		res["cacheIgnoredFiles"] = opts.CacheIgnoredFiles
 		res["overwriteRemoteDeviceNames"] = opts.OverwriteRemoteDevNames
@@ -220,7 +258,7 @@ func reportData(cfg config.Wrapper, m model.Model, connectionsService connection
 		pullOrder := make(map[string]int)
 		filesystemType := make(map[string]int)
 		var fsWatcherDelays []int
-		for _, cfg := range cfg.Folders() {
+		for _, cfg := range s.cfg.Folders() {
 			if cfg.ScanProgressIntervalS < 0 {
 				folderUsesV3["scanProgressDisabled"]++
 			}
@@ -260,7 +298,7 @@ func reportData(cfg config.Wrapper, m model.Model, connectionsService connection
 		}
 		res["folderUsesV3"] = folderUsesV3Interface
 
-		guiCfg := cfg.GUI()
+		guiCfg := s.cfg.GUI()
 		// Anticipate multiple GUI configs in the future, hence store counts.
 		guiStats := map[string]int{
 			"enabled":                   0,
@@ -315,39 +353,19 @@ func reportData(cfg config.Wrapper, m model.Model, connectionsService connection
 		res["guiStats"] = guiStatsInterface
 	}
 
-	for key, value := range m.UsageReportingStats(version, preview) {
+	for key, value := range s.model.UsageReportingStats(urVersion, preview) {
 		res[key] = value
 	}
 
 	return res
 }
 
-type usageReportingService struct {
-	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 {
-	svc := &usageReportingService{
-		cfg:                cfg,
-		model:              model,
-		connectionsService: connectionsService,
-		forceRun:           make(chan struct{}),
-		stop:               make(chan struct{}),
-		stopped:            make(chan struct{}),
-	}
-	close(svc.stopped) // Not yet running, dont block on Stop()
-	cfg.Subscribe(svc)
-	return svc
+func (s *Service) UptimeS() int {
+	return int(time.Since(StartTime).Seconds())
 }
 
-func (s *usageReportingService) sendUsageReport() error {
-	d := reportData(s.cfg, s.model, s.connectionsService, s.cfg.Options().URAccepted, false)
+func (s *Service) sendUsageReport() error {
+	d := s.ReportData()
 	var b bytes.Buffer
 	if err := json.NewEncoder(&b).Encode(d); err != nil {
 		return err
@@ -366,7 +384,7 @@ func (s *usageReportingService) sendUsageReport() error {
 	return err
 }
 
-func (s *usageReportingService) Serve() {
+func (s *Service) Serve() {
 	s.stopMut.Lock()
 	s.stop = make(chan struct{})
 	s.stopped = make(chan struct{})
@@ -397,11 +415,11 @@ func (s *usageReportingService) Serve() {
 	}
 }
 
-func (s *usageReportingService) VerifyConfiguration(from, to config.Configuration) error {
+func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
 	return nil
 }
 
-func (s *usageReportingService) CommitConfiguration(from, to config.Configuration) bool {
+func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
 	if from.Options.URAccepted != to.Options.URAccepted || from.Options.URUniqueID != to.Options.URUniqueID || from.Options.URURL != to.Options.URURL {
 		s.stopMut.RLock()
 		select {
@@ -413,19 +431,19 @@ func (s *usageReportingService) CommitConfiguration(from, to config.Configuratio
 	return true
 }
 
-func (s *usageReportingService) Stop() {
+func (s *Service) Stop() {
 	s.stopMut.RLock()
 	close(s.stop)
 	<-s.stopped
 	s.stopMut.RUnlock()
 }
 
-func (*usageReportingService) String() string {
-	return "usageReportingService"
+func (*Service) String() string {
+	return "ur.Service"
 }
 
-// cpuBench returns CPU performance as a measure of single threaded SHA-256 MiB/s
-func cpuBench(iterations int, duration time.Duration, useWeakHash bool) float64 {
+// CpuBench returns CPU performance as a measure of single threaded SHA-256 MiB/s
+func CpuBench(iterations int, duration time.Duration, useWeakHash bool) float64 {
 	dataSize := 16 * protocol.MinBlockSize
 	bs := make([]byte, dataSize)
 	rand.Reader.Read(bs)