From b50039a920e2b6c4e215dfb70925626892966b74 Mon Sep 17 00:00:00 2001 From: Simon Frei Date: Tue, 26 Mar 2019 20:53:58 +0100 Subject: [PATCH] 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 --- cmd/syncthing/debug.go | 8 +- cmd/syncthing/main.go | 54 +-- cmd/syncthing/gui.go => lib/api/api.go | 387 ++++++++---------- .../gui_auth.go => lib/api/api_auth.go | 4 +- .../api/api_auth_test.go | 2 +- .../gui_csrf.go => lib/api/api_csrf.go | 4 +- .../gui_statics.go => lib/api/api_statics.go | 2 +- .../gui_test.go => lib/api/api_test.go | 39 +- lib/api/debug.go | 28 ++ .../api}/mocked_config_test.go | 2 +- .../api}/mocked_connections_test.go | 2 +- .../api}/mocked_cpuusage_test.go | 2 +- .../api}/mocked_discovery_test.go | 2 +- .../api}/mocked_events_test.go | 2 +- .../api}/mocked_logger_test.go | 2 +- .../api}/mocked_model_test.go | 2 +- {cmd/syncthing => lib/api}/support_bundle.go | 4 +- {cmd/syncthing => lib/api}/testdata/.stfolder | 0 lib/api/testdata/config/cert.pem | 23 ++ lib/api/testdata/config/config.xml | 134 ++++++ lib/api/testdata/config/https-cert.pem | 23 ++ lib/api/testdata/config/https-key.pem | 39 ++ lib/api/testdata/config/key.pem | 39 ++ {cmd/syncthing => lib/api}/testdata/default/a | 0 {cmd/syncthing => lib/api}/testdata/default/b | 0 {cmd/syncthing => lib/api}/testdata/default/d | 0 {cmd/syncthing => lib/api}/testdata/foo/a | 0 .../api}/testdata/testfolder/.stfolder | 0 .../model/folder_summary.go | 107 ++++- lib/model/model.go | 11 + lib/ur/debug.go | 22 + {cmd/syncthing => lib/ur}/memsize_darwin.go | 2 +- {cmd/syncthing => lib/ur}/memsize_linux.go | 2 +- {cmd/syncthing => lib/ur}/memsize_netbsd.go | 2 +- {cmd/syncthing => lib/ur}/memsize_solaris.go | 2 +- {cmd/syncthing => lib/ur}/memsize_unimpl.go | 2 +- {cmd/syncthing => lib/ur}/memsize_windows.go | 2 +- {cmd/syncthing => lib/ur}/usage_report.go | 132 +++--- 38 files changed, 728 insertions(+), 360 deletions(-) rename cmd/syncthing/gui.go => lib/api/api.go (79%) rename cmd/syncthing/gui_auth.go => lib/api/api_auth.go (97%) rename cmd/syncthing/gui_auth_test.go => lib/api/api_auth_test.go (98%) rename cmd/syncthing/gui_csrf.go => lib/api/api_csrf.go (97%) rename cmd/syncthing/gui_statics.go => lib/api/api_statics.go (99%) rename cmd/syncthing/gui_test.go => lib/api/api_test.go (95%) create mode 100644 lib/api/debug.go rename {cmd/syncthing => lib/api}/mocked_config_test.go (99%) rename {cmd/syncthing => lib/api}/mocked_connections_test.go (97%) rename {cmd/syncthing => lib/api}/mocked_cpuusage_test.go (96%) rename {cmd/syncthing => lib/api}/mocked_discovery_test.go (98%) rename {cmd/syncthing => lib/api}/mocked_events_test.go (97%) rename {cmd/syncthing => lib/api}/mocked_logger_test.go (97%) rename {cmd/syncthing => lib/api}/mocked_model_test.go (99%) rename {cmd/syncthing => lib/api}/support_bundle.go (93%) rename {cmd/syncthing => lib/api}/testdata/.stfolder (100%) create mode 100644 lib/api/testdata/config/cert.pem create mode 100644 lib/api/testdata/config/config.xml create mode 100644 lib/api/testdata/config/https-cert.pem create mode 100644 lib/api/testdata/config/https-key.pem create mode 100644 lib/api/testdata/config/key.pem rename {cmd/syncthing => lib/api}/testdata/default/a (100%) rename {cmd/syncthing => lib/api}/testdata/default/b (100%) rename {cmd/syncthing => lib/api}/testdata/default/d (100%) rename {cmd/syncthing => lib/api}/testdata/foo/a (100%) rename {cmd/syncthing => lib/api}/testdata/testfolder/.stfolder (100%) rename cmd/syncthing/summaryservice.go => lib/model/folder_summary.go (65%) create mode 100644 lib/ur/debug.go rename {cmd/syncthing => lib/ur}/memsize_darwin.go (98%) rename {cmd/syncthing => lib/ur}/memsize_linux.go (98%) rename {cmd/syncthing => lib/ur}/memsize_netbsd.go (98%) rename {cmd/syncthing => lib/ur}/memsize_solaris.go (97%) rename {cmd/syncthing => lib/ur}/memsize_unimpl.go (96%) rename {cmd/syncthing => lib/ur}/memsize_windows.go (98%) rename {cmd/syncthing => lib/ur}/usage_report.go (83%) diff --git a/cmd/syncthing/debug.go b/cmd/syncthing/debug.go index e65b68586..2861dc9b6 100644 --- a/cmd/syncthing/debug.go +++ b/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") } diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 010d61c84..674aab173 100644 --- a/cmd/syncthing/main.go +++ b/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) - if err := api.WaitForStart(); err != nil { + 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 := apiSvc.WaitForStart(); err != nil { l.Warnln("Failed starting API:", err) os.Exit(exitError) } diff --git a/cmd/syncthing/gui.go b/lib/api/api.go similarity index 79% rename from cmd/syncthing/gui.go rename to lib/api/api.go index bf7582e5d..5146f09a6 100644 --- a/cmd/syncthing/gui.go +++ b/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, - } - - return service +type Controller interface { + ExitUpgrading() + Restart() + Shutdown() } -func (s *apiService) WaitForStart() error { +type Service interface { + suture.Service + config.Committer + 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") diff --git a/cmd/syncthing/gui_auth.go b/lib/api/api_auth.go similarity index 97% rename from cmd/syncthing/gui_auth.go rename to lib/api/api_auth.go index c84d3c7ff..b9a313aba 100644 --- a/cmd/syncthing/gui_auth.go +++ b/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) diff --git a/cmd/syncthing/gui_auth_test.go b/lib/api/api_auth_test.go similarity index 98% rename from cmd/syncthing/gui_auth_test.go rename to lib/api/api_auth_test.go index eba6ced77..dc41f8804 100644 --- a/cmd/syncthing/gui_auth_test.go +++ b/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" diff --git a/cmd/syncthing/gui_csrf.go b/lib/api/api_csrf.go similarity index 97% rename from cmd/syncthing/gui_csrf.go rename to lib/api/api_csrf.go index 7590d5d21..b0f1d4032 100644 --- a/cmd/syncthing/gui_csrf.go +++ b/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(), diff --git a/cmd/syncthing/gui_statics.go b/lib/api/api_statics.go similarity index 99% rename from cmd/syncthing/gui_statics.go rename to lib/api/api_statics.go index 1b788ba61..39f2a9590 100644 --- a/cmd/syncthing/gui_statics.go +++ b/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" diff --git a/cmd/syncthing/gui_test.go b/lib/api/api_test.go similarity index 95% rename from cmd/syncthing/gui_test.go rename to lib/api/api_test.go index 0904412dd..bafefda40 100644 --- a/cmd/syncthing/gui_test.go +++ b/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 { diff --git a/lib/api/debug.go b/lib/api/debug.go new file mode 100644 index 000000000..1c62f6dab --- /dev/null +++ b/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") +} diff --git a/cmd/syncthing/mocked_config_test.go b/lib/api/mocked_config_test.go similarity index 99% rename from cmd/syncthing/mocked_config_test.go rename to lib/api/mocked_config_test.go index 8bdb8ebe1..cbf8c7223 100644 --- a/cmd/syncthing/mocked_config_test.go +++ b/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" diff --git a/cmd/syncthing/mocked_connections_test.go b/lib/api/mocked_connections_test.go similarity index 97% rename from cmd/syncthing/mocked_connections_test.go rename to lib/api/mocked_connections_test.go index aaf070293..54aee73f8 100644 --- a/cmd/syncthing/mocked_connections_test.go +++ b/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{} diff --git a/cmd/syncthing/mocked_cpuusage_test.go b/lib/api/mocked_cpuusage_test.go similarity index 96% rename from cmd/syncthing/mocked_cpuusage_test.go rename to lib/api/mocked_cpuusage_test.go index b45a44a72..c89a1dbad 100644 --- a/cmd/syncthing/mocked_cpuusage_test.go +++ b/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{} diff --git a/cmd/syncthing/mocked_discovery_test.go b/lib/api/mocked_discovery_test.go similarity index 98% rename from cmd/syncthing/mocked_discovery_test.go rename to lib/api/mocked_discovery_test.go index c367026ca..885dfc804 100644 --- a/cmd/syncthing/mocked_discovery_test.go +++ b/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" diff --git a/cmd/syncthing/mocked_events_test.go b/lib/api/mocked_events_test.go similarity index 97% rename from cmd/syncthing/mocked_events_test.go rename to lib/api/mocked_events_test.go index 625ffb921..4bf079dca 100644 --- a/cmd/syncthing/mocked_events_test.go +++ b/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" diff --git a/cmd/syncthing/mocked_logger_test.go b/lib/api/mocked_logger_test.go similarity index 97% rename from cmd/syncthing/mocked_logger_test.go rename to lib/api/mocked_logger_test.go index 3d33a0890..e678c4047 100644 --- a/cmd/syncthing/mocked_logger_test.go +++ b/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" diff --git a/cmd/syncthing/mocked_model_test.go b/lib/api/mocked_model_test.go similarity index 99% rename from cmd/syncthing/mocked_model_test.go rename to lib/api/mocked_model_test.go index c01d59d95..c3224a5dc 100644 --- a/cmd/syncthing/mocked_model_test.go +++ b/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" diff --git a/cmd/syncthing/support_bundle.go b/lib/api/support_bundle.go similarity index 93% rename from cmd/syncthing/support_bundle.go rename to lib/api/support_bundle.go index 78b4aa986..102c38312 100644 --- a/cmd/syncthing/support_bundle.go +++ b/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 != "" { diff --git a/cmd/syncthing/testdata/.stfolder b/lib/api/testdata/.stfolder similarity index 100% rename from cmd/syncthing/testdata/.stfolder rename to lib/api/testdata/.stfolder diff --git a/lib/api/testdata/config/cert.pem b/lib/api/testdata/config/cert.pem new file mode 100644 index 000000000..3af840e15 --- /dev/null +++ b/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----- diff --git a/lib/api/testdata/config/config.xml b/lib/api/testdata/config/config.xml new file mode 100644 index 000000000..5bd47cc87 --- /dev/null +++ b/lib/api/testdata/config/config.xml @@ -0,0 +1,134 @@ + + + basic + + + + + 1 + + 1 + 0 + 0 + random + false + 0 + 0 + -1 + false + false + false + 25 + .stfolder + true + + + basic + + + 1 + + 1 + 0 + 0 + random + false + 0 + 0 + -1 + false + false + false + 25 + .stfolder + true + + +
tcp://127.0.0.1:22004
+ false + false + 0 + 0 + 0 +
+ +
tcp://127.0.0.1:22001
+ false + false + 0 + 0 + 0 +
+ +
tcp://127.0.0.1:22002
+ false + false + 0 + 0 + 0 +
+ +
tcp://127.0.0.1:22003
+ false + false + 0 + 0 + 0 +
+ +
tcp://127.0.0.1:22004
+ false + false + 0 + 0 + 0 +
+ +
127.0.0.1:8081
+ testuser + $2a$10$7tKL5uvLDGn5s2VLPM2yWOK/II45az0mTel8hxAUJDRQN1Tk2QYwu + abc123 + default +
+ + + tcp://127.0.0.1:22001 + default + false + true + 21027 + [ff12::8384]:21027 + 0 + 0 + 5 + false + 10 + false + true + 0 + 30 + 10 + 3 + 2 + tmwxxCqi + https://data.syncthing.net/newdata + false + 1800 + true + 12 + false + 24 + false + 5 + false + 1 + https://upgrades.syncthing.net/meta.json + false + 10 + 0 + ~ + true + 0 + 0 + +
diff --git a/lib/api/testdata/config/https-cert.pem b/lib/api/testdata/config/https-cert.pem new file mode 100644 index 000000000..ac14760f0 --- /dev/null +++ b/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----- diff --git a/lib/api/testdata/config/https-key.pem b/lib/api/testdata/config/https-key.pem new file mode 100644 index 000000000..ac3d2fafc --- /dev/null +++ b/lib/api/testdata/config/https-key.pem @@ -0,0 +1,39 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/lib/api/testdata/config/key.pem b/lib/api/testdata/config/key.pem new file mode 100644 index 000000000..4c40deb8b --- /dev/null +++ b/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----- diff --git a/cmd/syncthing/testdata/default/a b/lib/api/testdata/default/a similarity index 100% rename from cmd/syncthing/testdata/default/a rename to lib/api/testdata/default/a diff --git a/cmd/syncthing/testdata/default/b b/lib/api/testdata/default/b similarity index 100% rename from cmd/syncthing/testdata/default/b rename to lib/api/testdata/default/b diff --git a/cmd/syncthing/testdata/default/d b/lib/api/testdata/default/d similarity index 100% rename from cmd/syncthing/testdata/default/d rename to lib/api/testdata/default/d diff --git a/cmd/syncthing/testdata/foo/a b/lib/api/testdata/foo/a similarity index 100% rename from cmd/syncthing/testdata/foo/a rename to lib/api/testdata/foo/a diff --git a/cmd/syncthing/testdata/testfolder/.stfolder b/lib/api/testdata/testfolder/.stfolder similarity index 100% rename from cmd/syncthing/testdata/testfolder/.stfolder rename to lib/api/testdata/testfolder/.stfolder diff --git a/cmd/syncthing/summaryservice.go b/lib/model/folder_summary.go similarity index 65% rename from cmd/syncthing/summaryservice.go rename to lib/model/folder_summary.go index ec12fd790..195afb0d6 100644 --- a/cmd/syncthing/summaryservice.go +++ b/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() diff --git a/lib/model/model.go b/lib/model/model.go index 22c74abe8..ddd4e074f 100644 --- a/lib/model/model.go +++ b/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 { diff --git a/lib/ur/debug.go b/lib/ur/debug.go new file mode 100644 index 000000000..c169851ee --- /dev/null +++ b/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") +} diff --git a/cmd/syncthing/memsize_darwin.go b/lib/ur/memsize_darwin.go similarity index 98% rename from cmd/syncthing/memsize_darwin.go rename to lib/ur/memsize_darwin.go index 3c04ba0d0..4b45d3456 100644 --- a/cmd/syncthing/memsize_darwin.go +++ b/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" diff --git a/cmd/syncthing/memsize_linux.go b/lib/ur/memsize_linux.go similarity index 98% rename from cmd/syncthing/memsize_linux.go rename to lib/ur/memsize_linux.go index 9e7ae4b2d..ca6d9edb7 100644 --- a/cmd/syncthing/memsize_linux.go +++ b/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" diff --git a/cmd/syncthing/memsize_netbsd.go b/lib/ur/memsize_netbsd.go similarity index 98% rename from cmd/syncthing/memsize_netbsd.go rename to lib/ur/memsize_netbsd.go index 464a40e91..3fd4a77a6 100644 --- a/cmd/syncthing/memsize_netbsd.go +++ b/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" diff --git a/cmd/syncthing/memsize_solaris.go b/lib/ur/memsize_solaris.go similarity index 97% rename from cmd/syncthing/memsize_solaris.go rename to lib/ur/memsize_solaris.go index b1370dbc6..23965fd05 100644 --- a/cmd/syncthing/memsize_solaris.go +++ b/lib/ur/memsize_solaris.go @@ -6,7 +6,7 @@ // +build solaris -package main +package ur import ( "os/exec" diff --git a/cmd/syncthing/memsize_unimpl.go b/lib/ur/memsize_unimpl.go similarity index 96% rename from cmd/syncthing/memsize_unimpl.go rename to lib/ur/memsize_unimpl.go index f4337b95f..0b4d792f7 100644 --- a/cmd/syncthing/memsize_unimpl.go +++ b/lib/ur/memsize_unimpl.go @@ -6,7 +6,7 @@ // +build freebsd openbsd dragonfly -package main +package ur import "errors" diff --git a/cmd/syncthing/memsize_windows.go b/lib/ur/memsize_windows.go similarity index 98% rename from cmd/syncthing/memsize_windows.go rename to lib/ur/memsize_windows.go index 553078663..92c780829 100644 --- a/cmd/syncthing/memsize_windows.go +++ b/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" diff --git a/cmd/syncthing/usage_report.go b/lib/ur/usage_report.go similarity index 83% rename from cmd/syncthing/usage_report.go rename to lib/ur/usage_report.go index 7b33a01f0..703b7be5a 100644 --- a/cmd/syncthing/usage_report.go +++ b/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 (s *Service) UptimeS() int { + return int(time.Since(StartTime).Seconds()) } -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 *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)