mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-08 22:31:04 +00:00
* 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
This commit is contained in:
parent
d4e81fff8a
commit
b50039a920
@ -15,14 +15,8 @@ import (
|
||||
|
||||
var (
|
||||
l = logger.DefaultLogger.NewFacility("main", "Main package")
|
||||
httpl = logger.DefaultLogger.NewFacility("http", "REST API")
|
||||
)
|
||||
|
||||
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,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)
|
||||
}
|
||||
|
@ -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,}`)
|
||||
)
|
||||
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 {
|
||||
type service 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
|
||||
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 at least once
|
||||
startedOnce chan struct{} // the service has started successfully at least once
|
||||
startupErr error
|
||||
cpu rater
|
||||
|
||||
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{
|
||||
type Controller interface {
|
||||
ExitUpgrading()
|
||||
Restart()
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
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,
|
||||
httpsCertFile: httpsCertFile,
|
||||
httpsKeyFile: httpsKeyFile,
|
||||
statics: newStaticsServer(cfg.GUI().Theme, assetDir),
|
||||
model: m,
|
||||
eventSubs: map[events.EventType]events.BufferedSubscription{
|
||||
defaultEventMask: defaultSub,
|
||||
diskEventMask: diskSub,
|
||||
DefaultEventMask: defaultSub,
|
||||
DiskEventMask: diskSub,
|
||||
},
|
||||
eventSubsMut: sync.NewMutex(),
|
||||
discoverer: discoverer,
|
||||
connectionsService: connectionsService,
|
||||
fss: fss,
|
||||
urService: urService,
|
||||
systemConfigMut: sync.NewMutex(),
|
||||
stop: make(chan struct{}),
|
||||
configChanged: make(chan struct{}),
|
||||
startedOnce: make(chan struct{}),
|
||||
guiErrors: errors,
|
||||
systemLog: systemLog,
|
||||
cpu: cpu,
|
||||
contr: contr,
|
||||
noUpgrade: noUpgrade,
|
||||
tlsDefaultCommonName: tlsDefaultCommonName,
|
||||
stop: make(chan struct{}),
|
||||
configChanged: make(chan struct{}),
|
||||
startedOnce: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *apiService) WaitForStart() error {
|
||||
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")
|
@ -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)
|
@ -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"
|
@ -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(),
|
@ -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"
|
@ -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
lib/api/debug.go
Normal file
28
lib/api/debug.go
Normal file
@ -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")
|
||||
}
|
@ -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"
|
@ -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{}
|
||||
|
@ -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{}
|
||||
|
@ -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"
|
@ -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"
|
@ -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"
|
@ -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"
|
@ -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 != "" {
|
23
lib/api/testdata/config/cert.pem
vendored
Normal file
23
lib/api/testdata/config/cert.pem
vendored
Normal file
@ -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
lib/api/testdata/config/config.xml
vendored
Normal file
134
lib/api/testdata/config/config.xml
vendored
Normal file
@ -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
lib/api/testdata/config/https-cert.pem
vendored
Normal file
23
lib/api/testdata/config/https-cert.pem
vendored
Normal file
@ -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
lib/api/testdata/config/https-key.pem
vendored
Normal file
39
lib/api/testdata/config/https-key.pem
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
39
lib/api/testdata/config/key.pem
vendored
Normal file
39
lib/api/testdata/config/key.pem
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
@ -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()
|
@ -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
lib/ur/debug.go
Normal file
22
lib/ur/debug.go
Normal file
@ -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")
|
||||
}
|
@ -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"
|
@ -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"
|
@ -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"
|
@ -6,7 +6,7 @@
|
||||
|
||||
// +build solaris
|
||||
|
||||
package main
|
||||
package ur
|
||||
|
||||
import (
|
||||
"os/exec"
|
@ -6,7 +6,7 @@
|
||||
|
||||
// +build freebsd openbsd dragonfly
|
||||
|
||||
package main
|
||||
package ur
|
||||
|
||||
import "errors"
|
||||
|
@ -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"
|
@ -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)
|
Loading…
Reference in New Issue
Block a user