mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-05 04:19:10 -05:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a54d7a32d1 | ||
|
|
ca823bd591 | ||
|
|
eabd972667 | ||
|
|
395e524e2d | ||
|
|
48b1a2b264 | ||
|
|
58953d799c | ||
|
|
f0f8bf7784 | ||
|
|
bf3834e367 | ||
|
|
8d1eff7e41 | ||
|
|
0cff66fcbc | ||
|
|
d23e8be39f | ||
|
|
43a5be1c4b | ||
|
|
b50039a920 | ||
|
|
d4e81fff8a | ||
|
|
3a557a43cd | ||
|
|
bc53782f88 | ||
|
|
e31a116e6e | ||
|
|
675f289aef | ||
|
|
e7ae851900 | ||
|
|
73fa7a6e5b | ||
|
|
1a6d023ba8 | ||
|
|
9207535028 | ||
|
|
24967e99a7 |
1
AUTHORS
1
AUTHORS
@@ -76,6 +76,7 @@ Felix Ableitner (Nutomic) <me@nutomic.com>
|
||||
Felix Unterpaintner (bigbear2nd) <bigbear2nd@gmail.com>
|
||||
Francois-Xavier Gsell (zukoo) <fxgsell@gmail.com>
|
||||
Frank Isemann (fti7) <frank@isemann.name>
|
||||
georgespatton <georgespatton@users.noreply.github.com>
|
||||
Gilli Sigurdsson (gillisig) <gilli@vx.is>
|
||||
Graham Miln (grahammiln) <graham.miln@dssw.co.uk> <graham.miln@miln.eu>
|
||||
Han Boetes <han@boetes.org>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.11 AS builder
|
||||
FROM golang:1.12 AS builder
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
@@ -14,15 +14,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
l = logger.DefaultLogger.NewFacility("main", "Main package")
|
||||
httpl = logger.DefaultLogger.NewFacility("http", "REST API")
|
||||
l = logger.DefaultLogger.NewFacility("main", "Main package")
|
||||
)
|
||||
|
||||
func shouldDebugHTTP() bool {
|
||||
return l.ShouldDebug("http")
|
||||
}
|
||||
|
||||
func init() {
|
||||
l.SetDebug("main", strings.Contains(os.Getenv("STTRACE"), "main") || os.Getenv("STTRACE") == "all")
|
||||
l.SetDebug("http", strings.Contains(os.Getenv("STTRACE"), "http") || os.Getenv("STTRACE") == "all")
|
||||
}
|
||||
|
||||
@@ -29,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)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Unit]
|
||||
Description=Syncthing - Open Source Continuous File Synchronization for %I
|
||||
Documentation=man:syncthing(1)
|
||||
After=multi-user.target network.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=%i
|
||||
|
||||
3
go.mod
3
go.mod
@@ -36,9 +36,8 @@ require (
|
||||
github.com/thejerf/suture v3.0.2+incompatible
|
||||
github.com/urfave/cli v1.20.0
|
||||
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
|
||||
golang.org/x/crypto v0.0.0-20171231215028-0fcca4842a8d
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
|
||||
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 // indirect
|
||||
golang.org/x/text v0.0.0-20171227012246-e19ae1496984
|
||||
golang.org/x/time v0.0.0-20170927054726-6dc17368e09b
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -85,6 +85,8 @@ github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0 h1:okhMind4q9H1OxF44gN
|
||||
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0/go.mod h1:TTbGUfE+cXXceWtbTHq6lqcTvYPBKLNejBEbnUsQJtU=
|
||||
golang.org/x/crypto v0.0.0-20171231215028-0fcca4842a8d h1:GrqEEc3+MtHKTsZrdIGVoYDgLpbSRzW1EF+nLu0PcHE=
|
||||
golang.org/x/crypto v0.0.0-20171231215028-0fcca4842a8d/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
|
||||
@@ -92,6 +94,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM=
|
||||
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.0.0-20171227012246-e19ae1496984 h1:ulYJn/BqO4fMRe1xAQzWjokgjsQLPpb21GltxXHI3fQ=
|
||||
golang.org/x/text v0.0.0-20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20170927054726-6dc17368e09b h1:3X+R0qq1+64izd8es+EttB6qcY+JDlVmAhpRXl7gpzU=
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Копиран от оригинала",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Всички правата запазени © 2014-2016 Сътрудници:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Всички правата запазени © 2014-2017. Сътрудници:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Създаване на шаблони за игнориране, презаписване на съществуващ файл в {{path}}.",
|
||||
"Danger!": "Опасност!",
|
||||
"Debugging Facilities": "Дебъг функционалност",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Copiat de l'original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 els següents Col·laboradors:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 els següents Col·laboradors:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creant patrons a ignorar, sobreescriguent un fitxer que ja existeix a {{path}}.",
|
||||
"Danger!": "Perill!",
|
||||
"Debugging Facilities": "Utilitats de Depuració",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Zkopírováno z originálu",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následující přispěvatelé:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 následující přispěvatelé:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Vytváření ignorovaných vzorů, přepisování existujícího souboru v {{path}}.",
|
||||
"Danger!": "Pozor!",
|
||||
"Debugging Facilities": "Nástroje pro ladění",
|
||||
|
||||
@@ -56,12 +56,13 @@
|
||||
"Copied from original": "Kopieret fra originalen",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 de følgende bidragsydere:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 de følgende bidragsydere:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 de følgende bidragsydere:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Opretter ignoreringsmønstre; overskriver en eksisterende fil på {{path}}.",
|
||||
"Danger!": "Fare!",
|
||||
"Debugging Facilities": "Faciliteter til fejlretning",
|
||||
"Default Folder Path": "Standardmappesti",
|
||||
"Deleted": "Slettet",
|
||||
"Deselect All": "Deselect All",
|
||||
"Deselect All": "Fravælg alle",
|
||||
"Device": "Enhed",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enheden “{{name}}” ({{device}} på {{address}}) vil gerne forbinde. Tilføj denne enhed?",
|
||||
"Device ID": "Enheds-ID",
|
||||
@@ -74,11 +75,11 @@
|
||||
"Disabled periodic scanning and disabled watching for changes": "Deaktiverede periodisk skanning og deaktiverede overvågning af ændringer",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Deaktiverede periodisk skanning og aktiverede overvågning af ændringer",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Deaktiverede periodisk skanning fra og lykkedes ikke med at opsætte overvågning af ændringer; prøver igen hvert minut:",
|
||||
"Discard": "Discard",
|
||||
"Discard": "Behold ikke",
|
||||
"Disconnected": "Ikke tilsluttet",
|
||||
"Discovered": "Opdaget",
|
||||
"Discovery": "Opslag",
|
||||
"Discovery Failures": "Opdagelses Fejl ",
|
||||
"Discovery Failures": "Fejl ved opdagelse",
|
||||
"Do not restore": "Genskab ikke",
|
||||
"Do not restore all": "Genskab ikke alle",
|
||||
"Do you want to enable watching for changes for all your folders?": "Vil du aktivere løbende overvågning af ændringer for alle dine mapper?",
|
||||
@@ -97,7 +98,7 @@
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Indtast et ikke-negativt tal (fx “2,35”) og vælg en enhed. Procentsatser er ud fra total diskstørrelse.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Indtast et ikke-priviligeret portnummer (1024–65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv kommaseparerede adresser (“tcp://ip:port”, “tcp://host:port”) eller “dynamic” for at benytte automatisk opdagelse af adressen.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv en kommaadskilt adresseliste (\"tcp://ip:port\", \"tcp://host:port\") eller \"dynamic\" for automatisk at opdage adressen.",
|
||||
"Enter ignore patterns, one per line.": "Indtast ignoreringsmønstre, ét per linje.",
|
||||
"Error": "Fejl",
|
||||
"External File Versioning": "Ekstern filversionering",
|
||||
@@ -143,9 +144,9 @@
|
||||
"Ignore": "Ignorér",
|
||||
"Ignore Patterns": "Ignoreringsmønstre",
|
||||
"Ignore Permissions": "Ignorér rettigheder",
|
||||
"Ignored Devices": "Ignored Devices",
|
||||
"Ignored Folders": "Ignored Folders",
|
||||
"Ignored at": "Ignored at",
|
||||
"Ignored Devices": "Ignorerede enheder",
|
||||
"Ignored Folders": "Ignorerede mapper",
|
||||
"Ignored at": "Ignoreret på",
|
||||
"Incoming Rate Limit (KiB/s)": "Indgående hastighedsbegrænsning (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Ukorrekt opsætning kan skade dine data og gøre Syncthing ude af stand til at fungere.",
|
||||
"Introduced By": "Introduceret af",
|
||||
@@ -159,14 +160,14 @@
|
||||
"Later": "Senere",
|
||||
"Latest Change": "Seneste ændring",
|
||||
"Learn more": "Lær mere",
|
||||
"Limit": "Limit",
|
||||
"Limit": "Grænse",
|
||||
"Listeners": "Lyttere",
|
||||
"Loading data...": "Indlæser data…",
|
||||
"Loading...": "Indlæser…",
|
||||
"Loading data...": "Indlæser data ...",
|
||||
"Loading...": "Indlæser ...",
|
||||
"Local Discovery": "Lokal opslag",
|
||||
"Local State": "Lokal tilstand",
|
||||
"Local State (Total)": "Lokal tilstand (total)",
|
||||
"Locally Changed Items": "Locally Changed Items",
|
||||
"Locally Changed Items": "Lokalt ændrede filer",
|
||||
"Log": "Logbog",
|
||||
"Log tailing paused. Click here to continue.": "Logfølgning på pause. Klik her for at fortsætte.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Logfølgning på pause. Rul til bunden for at fortsætte.",
|
||||
@@ -208,7 +209,7 @@
|
||||
"Pause": "Pause",
|
||||
"Pause All": "Sæt alt på pause",
|
||||
"Paused": "På pause",
|
||||
"Pending changes": "Pending changes",
|
||||
"Pending changes": "Ventende ændringer",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodisk skanning med et givent interval og deaktiveret overvågning af ændringer",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodisk skanning med et givent interval og aktiveret overvågning af ændringer",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning med et givent interval og lykkedes ikke med at opsætte overvågning af ændringer; prøver igen hvert minut:",
|
||||
@@ -252,7 +253,7 @@
|
||||
"Scanning": "Skanner",
|
||||
"See external versioner help for supported templated command line parameters.": "Se hjælp til ekstern versionering for understøttede kommandolinjeparametre.",
|
||||
"See external versioning help for supported templated command line parameters.": "Se hjælp til ekstern versionering for understøttede kommandolinjeparametre.",
|
||||
"Select All": "Select All",
|
||||
"Select All": "Vælg alle",
|
||||
"Select a version": "Vælg en version",
|
||||
"Select latest version": "Vælg seneste version",
|
||||
"Select oldest version": "Vælg ældste version",
|
||||
@@ -289,7 +290,7 @@
|
||||
"Statistics": "Statistikker",
|
||||
"Stopped": "Stoppet",
|
||||
"Support": "Støt",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Support Bundle": "Støttepakke",
|
||||
"Sync Protocol Listen Addresses": "Lytteadresser for synkroniseringsprotokol",
|
||||
"Syncing": "Synkroniserer",
|
||||
"Syncthing has been shut down.": "Syncthing er lukket ned.",
|
||||
@@ -298,7 +299,7 @@
|
||||
"Syncthing is upgrading.": "Syncthing opgraderer.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ud til at være stoppet eller oplever problemer med din internetforbindelse. Prøver igen…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Det ser ud til, at Syncthing har problemer med at udføre opgaven. Prøv at genindlæse siden eller genstarte Synching, hvis problemet vedbliver.",
|
||||
"Take me back": "Take me back",
|
||||
"Take me back": "Tag mig tilbage",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing-administationsfladen er sat op til at kunne fjernstyres uden adgangskode.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Den indsamlede statistik er offentligt tilgængelig på den nedenstående URL.",
|
||||
@@ -313,7 +314,7 @@
|
||||
"The folder path cannot be blank.": "Mappestien må ikke være tom.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De følgende intervaller er brugt: Inden for den første time bliver en version gemt hvert 30. sekund, inden for den første dag bliver en version gemt hver time, inden for de første 30 dage bliver en version gemt hver dag, og indtil den maksimale alder bliver en version gemt hver uge.",
|
||||
"The following items could not be synchronized.": "Følgende filer kunne ikke synkroniseres.",
|
||||
"The following items were changed locally.": "The following items were changed locally.",
|
||||
"The following items were changed locally.": "De følgende filer er ændret lokalt.",
|
||||
"The maximum age must be a number and cannot be blank.": "Maksimal alder skal være et tal og feltet må ikke være tomt.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den maksimale tid, en version skal gemmes (i dage; sæt lig med 0 for at beholde gamle versioner for altid).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Procentsatsen for mindst ledig diskplads skal være et ikke-negativt tal mellem 0 og 100 (inklusive).",
|
||||
@@ -336,44 +337,44 @@
|
||||
"Unavailable": "Ikke tilgængelig",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Ikke tilgængelig / deaktiveret af administrator eller vedligeholder",
|
||||
"Undecided (will prompt)": "Ubestemt (du bliver spurgt)",
|
||||
"Unignore": "Unignore",
|
||||
"Unignore": "Fjern ignorering",
|
||||
"Unknown": "Ukendt",
|
||||
"Unshared": "Ikke delt",
|
||||
"Unused": "Ubrugt",
|
||||
"Up to Date": "Fuldt opdateret",
|
||||
"Updated": "Opdateret",
|
||||
"Upgrade": "Upgradér",
|
||||
"Upgrade": "Opgradér",
|
||||
"Upgrade To {%version%}": "Opgradér til {{version}}",
|
||||
"Upgrading": "Opgraderer",
|
||||
"Upload Rate": "Uploadhastighed",
|
||||
"Uptime": "Oppetid",
|
||||
"Usage reporting is always enabled for candidate releases.": "Forbrugsraportering er altid aktiveret for udgivelseskandidater.",
|
||||
"Use HTTPS for GUI": "Anvend HTTPS til GUI-adgang",
|
||||
"Use notifications from the filesystem to detect changed items.": "Use notifications from the filesystem to detect changed items.",
|
||||
"Variable Size Blocks": "Variable Size Blocks",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Variable size blocks (also \"large blocks\") are more efficient for large files.",
|
||||
"Use notifications from the filesystem to detect changed items.": "Benyt notifikationer fra filsystemet til at finde filændringer.",
|
||||
"Variable Size Blocks": "Skiftende blokstørrelse",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Skiftende blokstørrelse (også \"store blokke\") er mere effektivt for større filer.",
|
||||
"Version": "Version",
|
||||
"Versions": "Versioner",
|
||||
"Versions Path": "Versionssti",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner slettes automatisk, hvis de er ældre end den givne maksimum alder eller overstiger det tilladte antal filer i et interval.",
|
||||
"Waiting to scan": "Waiting to scan",
|
||||
"Waiting to scan": "Venter på at skanne",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Advarsel: Denne sti er en forældermappe til den eksisterende mappe “{{otherFolder}}”.",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Advarsel: Denne sti er en forældermappe til den eksisterende mappe “{{otherFolderLabel}}” ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Advarsel: Denne sti er en undermappe til den eksisterende mappe “{{otherFolder}}”.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Advarsel: Denne sti er en undermappe til den eksisterende mappe “{{otherFolderLabel}}” ({{otherFolder}}).",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Advarsel: Hvis du bruger en ekstern overvågning så som {{syncthingInotify}}, bør du være sikker på, at den er deaktiveret.",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Advarsel: Hvis du bruger en ekstern overvågning såsom {{syncthingInotify}}, bør du være sikker på, at den er deaktiveret.",
|
||||
"Watch for Changes": "Overvåg ændringer",
|
||||
"Watching for Changes": "Overvågning af ændringer",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Overvågning af ændringer finder ændringer uden at skanne fra tid til anden.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Når der tilføjes en ny enhed, vær da opmærksom på, at denne enhed også skal tilføjes i den anden ende.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Når der tilføjes en ny enhed, vær da opmærksom på at samme mappe-ID bruges til at forbinde mapper på de forskellige enheder. Der er forskel på store og små bogstaver, og ID skal være fuldstændig identisk på alle enheder.",
|
||||
"Yes": "Ja",
|
||||
"You can also select one of these nearby devices:": "Du kan også vælge en af disse enheder i nærheden:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Du kan altid ændre dit valg under indstillinger.",
|
||||
"You can read more about the two release channels at the link below.": "Du kan læse mere om de to udgivelseskanaler på linket herunder.",
|
||||
"You have no ignored devices.": "You have no ignored devices.",
|
||||
"You have no ignored folders.": "You have no ignored folders.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
|
||||
"You have no ignored devices.": "Du har ingen ignorerede enheder.",
|
||||
"You have no ignored folders.": "Du har ingen ignorerede mapper.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Du har ændringer, som ikke er gemt. Er du sikker på, at du ikke vil beholde dem?",
|
||||
"You must keep at least one version.": "Du skal beholde mindst én version.",
|
||||
"days": "dage",
|
||||
"directories": "mapper",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Vom Original kopiert",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 der folgenden Unterstützer:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 der folgenden Unterstützer:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Erstelle Ignoriermuster, welche die existierende Datei {{path}} überschreiben.",
|
||||
"Danger!": "Achtung!",
|
||||
"Debugging Facilities": "Debugging-Möglichkeiten",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Έχει αντιγραφεί από το πρωτότυπο",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 για τους παρακάτω συνεισφέροντες:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 για τους παρακάτω συνεισφέροντες:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Δημιουργία προτύπων αγνόησης, αντικατάσταση του υπάρχοντος αρχείου στο {{path}}.",
|
||||
"Danger!": "Προσοχή!",
|
||||
"Debugging Facilities": "Εργαλεία αποσφαλμάτωσης",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Copied from original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
|
||||
"Danger!": "Danger!",
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
|
||||
@@ -295,6 +295,7 @@
|
||||
"Syncing": "Syncing",
|
||||
"Syncthing has been shut down.": "Syncthing has been shut down.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing includes the following software or portions thereof:",
|
||||
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
|
||||
"Syncthing is restarting.": "Syncthing is restarting.",
|
||||
"Syncthing is upgrading.": "Syncthing is upgrading.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
|
||||
|
||||
386
gui/default/assets/lang/lang-eo.json
Normal file
386
gui/default/assets/lang/lang-eo.json
Normal file
@@ -0,0 +1,386 @@
|
||||
{
|
||||
"A device with that ID is already added.": "Aparato kun samtia ID estis jam aldonita.",
|
||||
"A negative number of days doesn't make sense.": "Negativa numero de tagoj ne havas sencon.",
|
||||
"A new major version may not be compatible with previous versions.": "Nova ĉefa versio eble ne kongruanta kun antaŭaj versioj.",
|
||||
"API Key": "API Ŝlosilo",
|
||||
"About": "Pri",
|
||||
"Action": "Ago",
|
||||
"Actions": "Agoj",
|
||||
"Add": "Aldoni",
|
||||
"Add Device": "Aldoni Aparaton",
|
||||
"Add Folder": "Aldoni Dosierujon",
|
||||
"Add Remote Device": "Aldoni Foran Aparaton",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Aldoni aparatojn de la enkondukanto ĝis nia aparatlisto, por reciproke komunigitaj dosierujoj.",
|
||||
"Add new folder?": "Aldoni novan dosierujon?",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Aldone, plena reskana intervalo estos pliigita (60-oble, t.e. nova defaŭlto estas 1h). Vi povas ankaŭ agordi ĝin permane por ĉiu dosierujo poste post elekto de Ne.",
|
||||
"Address": "Adreso",
|
||||
"Addresses": "Adresoj",
|
||||
"Advanced": "Altnivela",
|
||||
"Advanced Configuration": "Altnivela Agordo",
|
||||
"Advanced settings": "Altnivelaj agordoj",
|
||||
"All Data": "Ĉiuj Datumoj",
|
||||
"Allow Anonymous Usage Reporting?": "Permesi Anoniman Raporton de Uzado?",
|
||||
"Allowed Networks": "Permesitaj Retoj",
|
||||
"Alphabetic": "Alfabeta",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder.": "Ekstera komando manipulas la version. Ĝi devas forigi la dosieron el la komunigita dosierujo.",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Ekstera komando manipulas la version. Ĝi devas forigi la dosieron el la komunigita dosierujo. Se la vojo al la apliko elhavas blankoj, ĝi devas esti inter citiloj.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ekstera komando manipulas la version. Ĝi devas forigi la dosieron el la sinkronigita dosierujo.",
|
||||
"Anonymous Usage Reporting": "Anonima Raporto de Uzado",
|
||||
"Anonymous usage report format has changed. Would you like to move to the new format?": "Formato de anonima raporto de uzado ŝanĝis. Ĉu vi ŝatus transiri al la nova formato?",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Ajnaj aparatoj agorditaj sur enkondukanta aparato estos ankaŭ aldonita al ĉi tiu aparato.",
|
||||
"Are you sure you want to remove device {%name%}?": "Ĉu vi certas, ke vi volas forigi aparaton {{name}}?",
|
||||
"Are you sure you want to remove folder {%label%}?": "Ĉu vi certas, ke vi volas forigi dosierujon {{label}}?",
|
||||
"Are you sure you want to restore {%count%} files?": "Ĉu vi certas, ke vi volas restarigi {{count}} dosierojn?",
|
||||
"Auto Accept": "Akcepti Aŭtomate",
|
||||
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Aŭtomata ĝisdatigo nun proponas la elekton inter stabilaj eldonoj kaj kandidataj eldonoj.",
|
||||
"Automatic upgrades": "Aŭtomataj ĝisdatigoj",
|
||||
"Automatic upgrades are always enabled for candidate releases.": "Aŭtomataj ĝisdatigoj ĉiam ŝaltitaj por kandidataj eldonoj.",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Aŭtomate krei aŭ komunigi dosierujojn, kiujn ĉi tiu aparato anoncas, ĉe la defaŭlta vojo.",
|
||||
"Available debug logging facilities:": "Disponeblaj elpurigadaj protokoliloj:",
|
||||
"Be careful!": "Atentu!",
|
||||
"Bugs": "Cimoj",
|
||||
"CPU Utilization": "Ĉefprocesoro Uzo",
|
||||
"Changelog": "Ŝanĝoprotokolo",
|
||||
"Clean out after": "Purigi poste",
|
||||
"Click to see discovery failures": "Alklaku por vidi malsukcesajn malkovrojn",
|
||||
"Close": "Fermi",
|
||||
"Command": "Komando",
|
||||
"Comment, when used at the start of a line": "Komento, kiam uzita ĉe la komenco de lineo",
|
||||
"Compression": "Densigo",
|
||||
"Configured": "Agordita",
|
||||
"Connection Error": "Eraro de Konekto",
|
||||
"Connection Type": "Tipo de Konekto",
|
||||
"Connections": "Konektoj",
|
||||
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Daŭra rigardado je ŝanĝoj estas nun havebla ene Syncthing. Ĉi tio detektos ŝangoj sur disko kaj skanos nur modifitajn vojojn. La avantaĝo estas en pli rapifa propagiĝo de ŝanĝoj kaj bezono je malpli plenaj skanoj.",
|
||||
"Copied from elsewhere": "Kopiita el aliloke",
|
||||
"Copied from original": "Kopiita el la originalo",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Kopirajto © 2014-2016 por la sekvantaj Kontribuantoj:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Kopirajto © 2014-2017 por la sekvantaj Kontribuantoj:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Kopirajto © 2014-2019 por la sekvantaj Kontribuantoj:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Kreante ignorantajn ŝablonojn, anstataŭige ekzistantan dosieron ĉe {{path}}.",
|
||||
"Danger!": "Danĝero!",
|
||||
"Debugging Facilities": "Elpurigadiloj",
|
||||
"Default Folder Path": "Defaŭlta Dosieruja Vojo",
|
||||
"Deleted": "Forigita",
|
||||
"Deselect All": "Malelekti Ĉiujn",
|
||||
"Device": "Aparato",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Aparato \"{{name}}\" ({{device}} ĉe {{address}}) volas konekti. Aldoni la novan aparaton?",
|
||||
"Device ID": "Aparato ID",
|
||||
"Device Identification": "Identigo de Aparato",
|
||||
"Device Name": "Nomo de Aparato",
|
||||
"Device rate limits": "Limoj de rapideco de aparato",
|
||||
"Device that last modified the item": "Aparato kiu laste modifis la eron",
|
||||
"Devices": "Aparatoj",
|
||||
"Disabled": "Malebligita",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Malebligita perioda skanado kaj malebligita rigardado je ŝanĝoj",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Malebligita perioda skanado kaj ebligita rigardado je ŝanĝoj",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Malebligita perioda skanado kaj malsukcesis agordi rigardadon je ŝanĝoj. Provante denove ĉiuminute:",
|
||||
"Discard": "Forĵeti",
|
||||
"Disconnected": "Malkonektita",
|
||||
"Discovered": "Malkovrita",
|
||||
"Discovery": "Malkovro",
|
||||
"Discovery Failures": "Malsukcesoj de Malkovro",
|
||||
"Do not restore": "Ne restarigu",
|
||||
"Do not restore all": "Ne restarigu ĉion",
|
||||
"Do you want to enable watching for changes for all your folders?": "Ĉu vi volas ebligi rigardado je ŝanĝoj por ĉiuj viaj dosierujoj?",
|
||||
"Documentation": "Dokumentado",
|
||||
"Download Rate": "Elŝutrapideco",
|
||||
"Downloaded": "Elŝutita",
|
||||
"Downloading": "Elŝutado",
|
||||
"Edit": "Redakti",
|
||||
"Edit Device": "Redakti Aparaton",
|
||||
"Edit Folder": "Redakti Dosierujon",
|
||||
"Editing": "Redaktado",
|
||||
"Editing {%path%}.": "Redaktado de {{path}}.",
|
||||
"Enable NAT traversal": "Ŝaltu trairan NAT",
|
||||
"Enable Relaying": "Ŝaltu Relajsadon",
|
||||
"Enabled": "Ebligita",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Enigu ne negativan nombron (ekz. \"2.35\") kaj elektu uniton. Procentoj estas kiel parto de tuta grandeco de disko.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Enigu ne privilegiitan numeron de pordo (1024- 65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enigu adresojn dividitajn per komoj (\"tcp://ip:port\", \"tcp://host:port\") aŭ \"dynamic\" por elfari aŭtomatan malkovradon de la adreso.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enigu adresojn dividitajn per komoj (\"tcp://ip:port\", \"tcp://host:port\") aŭ \"dynamic\" por elfari aŭtomatan malkovradon de la adreso.",
|
||||
"Enter ignore patterns, one per line.": "Enigu ignorantajn ŝablonojn, unu po linio.",
|
||||
"Error": "Eraro",
|
||||
"External File Versioning": "Ekstera Versionado de Dosiero",
|
||||
"Failed Items": "Malsukcesaj Eroj",
|
||||
"Failed to load ignore patterns": "Malsukcesis ŝarĝi ignorantajn ŝablonojn",
|
||||
"Failed to setup, retrying": "Malsukcesis agordi, provante denove",
|
||||
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Malsukceso por konekti al IPv6 serviloj atendante se ekzistas neniu IPv6 konektebleco.",
|
||||
"File Pull Order": "Ordo por Tiri Dosieron",
|
||||
"File Versioning": "Versionado de Dosieroj",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Permesoj bitaj de dosieroj estas ignorita dum la serĉado por ŝanĝoj. Uzi en FAT dosiersistemoj.",
|
||||
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Dosieroj estas movigitaj al .stversions dosierujo kiam anstataŭigitaj aŭ forigitaj en Syncthing.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Dosieroj estas movigitaj al .stversions dosierujo kiam anstataŭigitaj aŭ forigitaj en Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Dosieroj estas movigitaj al date stampitaj versioj en .stversions dosierujo kiam ili estas anstataŭigitaj aŭ forigitaj en Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dosieroj estas movigitaj al datostampitaj versioj en .stversions dosierujo kiam ili estas anstataŭigitaj aŭ forigitaj en Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosieroj estas protektataj kontraŭ ŝanĝoj faritaj en aliaj aparatoj, sed ŝanĝoj faritaj en ĉi tiu aparato estos senditaj al cetera parto de la grupo.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Dosieroj estas sinkronigitaj de la grupo, sed ajnaj ŝanĝoj faritaj loke ne estis senditaj al aliaj aparatoj.",
|
||||
"Filesystem Notifications": "Dosiersistemaj Sciigoj",
|
||||
"Filesystem Watcher Errors": "Eraroj de Rigardanto de Dosiersistemo",
|
||||
"Filter by date": "Filtri per daton",
|
||||
"Filter by name": "Filtri per nomon",
|
||||
"Folder": "Dosierujo",
|
||||
"Folder ID": "Dosieruja ID",
|
||||
"Folder Label": "Dosieruja Etikedo",
|
||||
"Folder Path": "Dosieruja Vojo",
|
||||
"Folder Type": "Dosieruja Tipo",
|
||||
"Folders": "Dosierujoj",
|
||||
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "Por la sekvantaj dosierujoj eraro okazis dum komencado de rigardado je ŝanĝoj. Provante denove ĉiuminute, do eraroj eble foriros baldaŭ. Se ili persistas, provu ripari subkuŝantan problemon kaj petu helpon, se vi ne povas.",
|
||||
"Full Rescan Interval (s)": "Plena Reskana Intervalo (s)",
|
||||
"GUI": "Grafika Interfaco",
|
||||
"GUI Authentication Password": "Pasvorta Aŭtentigo en Grafika Interfaco",
|
||||
"GUI Authentication User": "Uzanta Aŭtentigo en Grafika Interfaco",
|
||||
"GUI Listen Address": "Adreso de Aŭskultado en Grafika Interfaco",
|
||||
"GUI Listen Addresses": "Adresoj de Aŭskultado en Grafika Interfaco",
|
||||
"GUI Theme": "Etoso de Grafika Interfaco",
|
||||
"General": "Ĝenerala",
|
||||
"Generate": "Generi",
|
||||
"Global Changes": "Mallokaj Ŝanĝoj",
|
||||
"Global Discovery": "Malloka Malkovro",
|
||||
"Global Discovery Servers": "Serviloj de Malloka Malkovro",
|
||||
"Global State": "Malloka Stato",
|
||||
"Help": "Helpo",
|
||||
"Home page": "Hejma paĝo",
|
||||
"Ignore": "Ignoru",
|
||||
"Ignore Patterns": "Ignorantaj Ŝablonoj",
|
||||
"Ignore Permissions": "Ignori Permesojn",
|
||||
"Ignored Devices": "Ignoritaj Aparatoj",
|
||||
"Ignored Folders": "Ignoritaj Dosierujoj",
|
||||
"Ignored at": "Ignorita ĉe",
|
||||
"Incoming Rate Limit (KiB/s)": "Alvenanta Rapideco Limo (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Erara agordo povas difekti viajn dosierujajn enhavojn kaj senefikigi Syncthing-n.",
|
||||
"Introduced By": "Enkondukita Per",
|
||||
"Introducer": "Enkondukanto",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversigo de la donita kondiĉo (t.e. ne ekskludi)",
|
||||
"Keep Versions": "Konservi Versiojn",
|
||||
"Largest First": "Plej Granda Unue",
|
||||
"Last File Received": "Lasta Dosiero Ricevita",
|
||||
"Last Scan": "Lasta Skano",
|
||||
"Last seen": "Lasta vidita",
|
||||
"Later": "Poste",
|
||||
"Latest Change": "Lasta Ŝanĝo",
|
||||
"Learn more": "Lerni pli",
|
||||
"Limit": "Limo",
|
||||
"Listeners": "Aŭskultantoj",
|
||||
"Loading data...": "Ŝarĝas datumojn...",
|
||||
"Loading...": "Ŝarĝas...",
|
||||
"Local Discovery": "Loka Malkovro",
|
||||
"Local State": "Loka Stato",
|
||||
"Local State (Total)": "Loka Stato (Tuta)",
|
||||
"Locally Changed Items": "Loke Ŝanĝitaj Eroj",
|
||||
"Log": "Protokolo",
|
||||
"Log tailing paused. Click here to continue.": "Vostado de protokolo paŭzis. Alklaku ĉi tie por daŭrigi.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Vostado de protokolo paŭzis. Rulumu malsupre por daŭrigi.",
|
||||
"Logs": "Protokoloj",
|
||||
"Major Upgrade": "Ĉefa Ĝisdatigo",
|
||||
"Mass actions": "Amasa agoj",
|
||||
"Master": "Ĉefa",
|
||||
"Maximum Age": "Maksimuma Aĝo",
|
||||
"Metadata Only": "Nur Metadatumoj",
|
||||
"Minimum Free Disk Space": "Minimuma Libera Diskospaco",
|
||||
"Mod. Device": "Mod. Aparato",
|
||||
"Mod. Time": "Mod. Tempo",
|
||||
"Move to top of queue": "Movi al la supro de atendovico",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Multnivela ĵokero (egalas multoblajn dosierujaj niveloj)",
|
||||
"Never": "Neniam",
|
||||
"New Device": "Nova Aparato",
|
||||
"New Folder": "Nova Dosierujo",
|
||||
"Newest First": "Plejnova Unue",
|
||||
"No": "Ne",
|
||||
"No File Versioning": "Sen Dosiera Versionado",
|
||||
"No files will be deleted as a result of this operation.": "Neniuj dosieroj estos forigitaj rezulte de ĉi tiu ago.",
|
||||
"No upgrades": "Sen ĝisdatigoj",
|
||||
"Normal": "Normala",
|
||||
"Notice": "Avizo",
|
||||
"OK": "Bone",
|
||||
"Off": "Malŝata",
|
||||
"Oldest First": "Malnova Unue",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Laŭvola priskriba etikedo por la dosierujo. Povas esti malsama en ĉiu aparato.",
|
||||
"Options": "Opcioj",
|
||||
"Out of Sync": "Elsinkronigita",
|
||||
"Out of Sync Items": "Elsinkronigitaj Eroj",
|
||||
"Outgoing Rate Limit (KiB/s)": " Eliranta Rapideco Limo (KiB/s)",
|
||||
"Override Changes": "Transpasi Ŝanĝojn",
|
||||
"Path": "Vojo",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Vojo de la dosierujo en la loka komputilo. Kreiĝos se ne ekzistas. La tilda signo (~) povas esti uzata kiel mallongigilo por",
|
||||
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "La vojo, kie novaj aŭtomate akceptitaj dosierujoj estos kreita, kaj ankaŭ la defaŭlta sugestita vojo aldoninte novajn dosierujojn per uzinterfaco. Tildo (~) malvolvas al {{tilde}}.",
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Vojo kies versioj devus esti stokitaj (lasu malplena por la defaŭlta .stversions dosierujo en la komunigita dosierujo).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Vojo kies vesioj estas konservitaj (lasi malplena por la defaŭlta .stversions dosierujo en la dosierujo).",
|
||||
"Pause": "Paŭzu",
|
||||
"Pause All": "Paŭzu Ĉion",
|
||||
"Paused": "Paŭzita",
|
||||
"Pending changes": "Pritraktataj ŝanĝoj",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Perioda skanado ĉe donita intervalo kaj malebligita rigardado je ŝanĝoj",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Perioda skanado ĉe donita intervalo kaj ebligita rigardado je ŝanĝoj",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Perioda skanado ĉe donita intervalo kaj malsukcesis agordi rigardadon je ŝanĝoj. Provante denove ĉiuminute:",
|
||||
"Permissions": "Permesoj",
|
||||
"Please consult the release notes before performing a major upgrade.": "Bonvolu konsulti la notojn de eldono antaŭ elfari ĉefan ĝisdatigon.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Bonvolu agordi GUI Authentication Uzanto kaj Pasvorto en la agordoj dialogo.",
|
||||
"Please wait": "Bonvolu atendi",
|
||||
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefikso indikanta, ke la dosiero povas esti forigita, se ĝi malhelpas forigi dosierujon",
|
||||
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefikso indikanta, ke la ŝablono devus esti egalita usklecoblinde.",
|
||||
"Preview": "Antaŭrigardo",
|
||||
"Preview Usage Report": "Antaŭrigardo Uzada Raporto",
|
||||
"Quick guide to supported patterns": "Rapida gvidilo pri subtenata ŝablonoj",
|
||||
"RAM Utilization": "Labormemoro Uzo",
|
||||
"Random": "Hazarda",
|
||||
"Receive Only": "Nur Ricevi",
|
||||
"Recent Changes": "Lastatempaj Ŝanĝoj",
|
||||
"Reduced by ignore patterns": "Malpliigita per ignorantaj ŝablonoj",
|
||||
"Release Notes": "Notoj de Eldono",
|
||||
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Kandidataj eldonoj enhavas la lastajn trajtojn kaj korektojn. Ili estas similaj al la tradiciaj dusemajnaj Syncthing eldonoj.",
|
||||
"Remote Devices": "Foraj Aparatoj",
|
||||
"Remove": "Forigu",
|
||||
"Remove Device": "Forigi Aparaton",
|
||||
"Remove Folder": "Forigi Dosierujon",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Nepra identigilo por la dosierujo. Devas esti la sama en ĉiuj aparatoj de la grupo.",
|
||||
"Rescan": "Reskanu",
|
||||
"Rescan All": "Reskanu Ĉion",
|
||||
"Rescan Interval": "Reskana Intervalo",
|
||||
"Rescans": "Reskanoj",
|
||||
"Restart": "Restartu",
|
||||
"Restart Needed": "Restarto Bezonata",
|
||||
"Restarting": "Restartado",
|
||||
"Restore": "Restarigi",
|
||||
"Restore Versions": "Restarigi Versiojn",
|
||||
"Resume": "Daŭrigu",
|
||||
"Resume All": "Daŭrigu Ĉion",
|
||||
"Reused": "Reuzita",
|
||||
"Revert Local Changes": "Reverti Lokajn Ŝangojn",
|
||||
"Running": "Kurante",
|
||||
"Save": "Konservu",
|
||||
"Scan Time Remaining": "Restanta Tempo de Skano",
|
||||
"Scanning": "Skanado",
|
||||
"See external versioner help for supported templated command line parameters.": "Vidu informlibron de ekstera versionilo por subtenata ŝablona parametroj de komandlinio.",
|
||||
"See external versioning help for supported templated command line parameters.": "Vidu informlibron de ekstera versionado por subtenata ŝablona parametroj de komandlinio.",
|
||||
"Select All": "Elekti Ĉiujn",
|
||||
"Select a version": "Elekti version",
|
||||
"Select latest version": "Elekti plej novan version",
|
||||
"Select oldest version": "Elekti plej malnovan version",
|
||||
"Select the devices to share this folder with.": "Elekti la aparatojn por komunigi ĉi tiun dosierujon.",
|
||||
"Select the folders to share with this device.": "Elekti la dosierujojn por komunigi kun ĉi tiu aparato.",
|
||||
"Send & Receive": "Sendi kaj Ricevi",
|
||||
"Send Only": "Nur Sendi",
|
||||
"Settings": "Agordoj",
|
||||
"Share": "Komunigi",
|
||||
"Share Folder": "Komunigu Dosierujon",
|
||||
"Share Folders With Device": "Dosierujoj Komunigitaj Kun Aparato",
|
||||
"Share With Devices": "Komunigu Kun Aparatoj",
|
||||
"Share this folder?": "Komunigi ĉi tiun dosierujon?",
|
||||
"Shared With": "Komunigita Kun",
|
||||
"Sharing": "Komunigo",
|
||||
"Show ID": "Montru ID",
|
||||
"Show QR": "Montru QR",
|
||||
"Show diff with previous version": "Montri diferenco kun antaŭa versio",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Montrita anstataŭ ID de Aparato en la statuso de la grupo. Estos anoncita al aliaj aparatoj kiel laŭvola defaŭlta nomo.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Montri anstataŭ ID de Aparato en la statuso de la grupo. Estos ĝisdatigita al la nomo de la aparato sciigante se ĝi estas lasita malplena.",
|
||||
"Shutdown": "Sistemfermo",
|
||||
"Shutdown Complete": "Sistemfermo Tuta",
|
||||
"Simple File Versioning": "Simpla Versionado de Dosieroj",
|
||||
"Single level wildcard (matches within a directory only)": "Ununivela ĵokero (egalas nur ene de dosierujo)",
|
||||
"Size": "Grandeco",
|
||||
"Smallest First": "Plej Malgranda Unue",
|
||||
"Some items could not be restored:": "Iuj eroj ne povis esti restarigitaj:",
|
||||
"Source Code": "Fontkodo",
|
||||
"Stable releases and release candidates": "Stabilaj eldonoj kaj kandidataj eldonoj",
|
||||
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabilaj eldonoj prokrastas je ĉirkaŭ du semjanoj. Dum tiu tempo ili estos testataj kiel kandidataj eldonoj.",
|
||||
"Stable releases only": "Nur stabilaj eldonoj",
|
||||
"Staggered File Versioning": "Gradigita Dosiera Versionado",
|
||||
"Start Browser": "Startu Retumilon",
|
||||
"Statistics": "Statistikoj",
|
||||
"Stopped": "Haltita",
|
||||
"Support": "Subteno",
|
||||
"Support Bundle": "Pakaĵo por subteno",
|
||||
"Sync Protocol Listen Addresses": "Aŭskultado Adresoj de Sinkprotokolo",
|
||||
"Syncing": "Sinkronigas",
|
||||
"Syncthing has been shut down.": "Syncthing estis malŝaltita.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing inkluzivas la jenajn programarojn aŭ porciojn ĝiajn:",
|
||||
"Syncthing is restarting.": "Syncthing estas restartanta.",
|
||||
"Syncthing is upgrading.": "Syncthing estas ĝisdatigita.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ŝajnas nefunkcii, aŭ estas problemo kun via retkonekto. Reprovado...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ŝajnas renkonti problemon kun la traktado de via peto. Bonvolu refreŝigi la paĝon aŭ restarti Syncthing se la problemo daŭras.",
|
||||
"Take me back": "Prenu min reen",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "La adreso de grafika interfaco estas superregita per startigaj agordoj. Ŝanĝoj ĉi tie ne efektiviĝas dum la superrego estas aktuala.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "La administra interfaco de Syncthing estas agordita por permesi foran atingon sen pasvorto.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "La agregita statistikoj estas publike disponebla ĉe la URL malsupre.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La agordo estis registrita sed ne aktivigita. Syncthing devas restarti por aktivigi la novan agordon.",
|
||||
"The device ID cannot be blank.": "La aparato ID ne povas esti malplena.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "La aparato ID por eniri ĉi tie estas trovebla per \"Agoj > Montru ID\" dialogo en la alia aparato. Interspacoj kaj streketoj estas opcio (ignorigita).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "La ĉifrita raporto de uzado estas sendata ĉiutage. Ĝi estas uzata por sekvi komunajn platformojn, dosierujajn grandojn kaj aplikaĵajn versiojn. Se la raporto datumaro ŝanĝis, vi estos avertata per ĉi tiu dialogo denove.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "La enigita aparato ID ne ŝajnas valida. Ĝi devas esti signoĉeno el 52 aŭ 56 karaktroj longa enhavanta leterojn kaj nombrojn, kun interspacoj kaj streketoj opciaj.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "La unua komandlinia parametro estas la vojo de la dosierujo kaj la dua parametro estas la relativa vojo en la dosierujo.",
|
||||
"The folder ID cannot be blank.": "La dosierujo ID ne povas esti malplena.",
|
||||
"The folder ID must be unique.": "La dosierujo ID devas esti unika.",
|
||||
"The folder path cannot be blank.": "La vojo de dosierujo ne povas esti malplena.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "La jenaj intervaloj estas uzataj: dum la unua horo version restas dum ĉiuj 30 sekundoj, dum la unua tago versio restas konservita dum ĉiu horo, dum la unuaj 30 tagoj versio estas konservita dum ĉiu tago, ĝis la maksimume aĝa versio restas konservita dum ĉiu semajno.",
|
||||
"The following items could not be synchronized.": "La sekvantaj eroj ne povas esti sinkronigitaj.",
|
||||
"The following items were changed locally.": "La sekvantaj eroj estis ŝanĝitaj loke.",
|
||||
"The maximum age must be a number and cannot be blank.": "La maksimuma aĝo devas esti nombro kaj ne povas esti malplena.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La maksimuma tempo por konservi version (en tagoj, agordi je 0 por konservi versiojn eterne).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "La minimuma procento de libera diskospaco devas esti pozitiva nombro inter 0 kaj 100 (inkluziva).",
|
||||
"The number of days must be a number and cannot be blank.": "La nombro da tagoj devas esti nombro kaj ne povas esti malplena.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "La nombro da tagoj por konservi dosierojn en la rubujo. Nulo signifas eterne.",
|
||||
"The number of old versions to keep, per file.": "La nombro da malnovaj versioj por konservi, po ĉiu dosiero.",
|
||||
"The number of versions must be a number and cannot be blank.": "La nombro da versioj devas esti nombro kaj ne povas esti malplena.",
|
||||
"The path cannot be blank.": "La vojo ne povas esti malplena.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "La rapideca limo devas esti pozitiva nombro (0: senlimo)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "La intervalo de reskano devas esti pozitiva nombro da sekundoj.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Ili estas reprovitaj aŭtomate kaj estos sinkronigitaj kiam la eraro estas solvita.",
|
||||
"This Device": "Ĉi Tiu Aparato",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Ĉi tio povas facile doni al kodumuloj atingon por legi kaj ŝanĝi ajnajn dosierojn en via komputilo.",
|
||||
"This is a major version upgrade.": "Ĉi tio estas ĉefversio ĝisdatigita.",
|
||||
"This setting controls the free space required on the home (i.e., index database) disk.": "Ĉi tiu agordo regas la libera spaco postulita sur la hejma (t.e. indeksa datumbaza) disko.",
|
||||
"Time": "Tempo",
|
||||
"Time the item was last modified": "Tempo de lasta modifo de la ero",
|
||||
"Trash Can File Versioning": "Rubuja Dosiera Versionado",
|
||||
"Type": "Tipo",
|
||||
"Unavailable": "Ne disponebla",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Ne disponebla/Malebligita de administranto aŭ subtenanto",
|
||||
"Undecided (will prompt)": "Hezitema (demandos)",
|
||||
"Unignore": "Malignoru",
|
||||
"Unknown": "Nekonata",
|
||||
"Unshared": "Nekomunigita",
|
||||
"Unused": "Neuzita",
|
||||
"Up to Date": "Ĝisdata",
|
||||
"Updated": "Ĝisdatigita",
|
||||
"Upgrade": "Altgradigo",
|
||||
"Upgrade To {%version%}": "Altgradigi Al {{version}}",
|
||||
"Upgrading": "Altgradigata",
|
||||
"Upload Rate": "Alŝutrapideco",
|
||||
"Uptime": "Daŭro de funkciado",
|
||||
"Usage reporting is always enabled for candidate releases.": "Uzada raportado ĉiam ŝaltita por kandidataj eldonoj.",
|
||||
"Use HTTPS for GUI": "Uzi HTTPS por grafika interfaco.",
|
||||
"Use notifications from the filesystem to detect changed items.": "Uzi sciigoj de la dosiersistemo por detekti ŝanĝitajn erojn.",
|
||||
"Variable Size Blocks": "Blokoj de Variaj Grandecoj",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Blokoj de Variaj Grandecoj (ankaŭ \"grandaj blokoj\") estas pli efika por grandaj dosieroj.",
|
||||
"Version": "Versio",
|
||||
"Versions": "Versioj",
|
||||
"Versions Path": "Vojo de Versioj",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioj estas aŭtomate forigita se ili estas pli malnovaj ol la maksimuma aĝo aŭ superas la nombron da dosieroj permesita en intervalo.",
|
||||
"Waiting to scan": "Atendante scanadon",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Averto, ĉi tiu vojo estas parenta dosierujo de ekzistanta dosierujo \"{{otherFolder}}\".",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Averto, ĉi tiu vojo estas parenta dosierujo de ekzistanta dosierujo \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Averto, ĉi tiu vojo estas subdosierujo de ekzistanta dosierujo \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Averto, ĉi tiu vojo estas subdosierujo de ekzistanta dosierujo \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Averto: se vi uzas ekstera rigardanto simila al {{syncthingInotify}}, vi devas certiĝi ĝi estas senaktivita.",
|
||||
"Watch for Changes": "Rigardi Ŝanĝojn",
|
||||
"Watching for Changes": "Rigardado je Ŝanĝoj",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Rigardado je ŝanĝoj malkovras plejparton de la ŝanĝoj sen perioda skanado.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Dum la aldonado de nova aparato, memoru ke ĉi tiu aparato devas esti aldonita en la alia flanko ankaŭ.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Dum la aldonado de nova dosierujo, memoru ke la Dosieruja ID estas uzita por ligi la dosierujojn kune inter aparatoj. Ili estas literfakodistingaj kaj devas kongrui precize inter ĉiuj aparatoj.",
|
||||
"Yes": "Jes",
|
||||
"You can also select one of these nearby devices:": "Vi povas ankaŭ elekti unu el ĉi tiuj proksimaj aparatoj:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Vi povas ŝanĝi vian elekton iam ajn en la Agorda dialogo.",
|
||||
"You can read more about the two release channels at the link below.": "Vi povas legi plu pri la du eldonkanaloj per la malsupra ligilo.",
|
||||
"You have no ignored devices.": "Vi havas neniujn ignoritajn aparatojn.",
|
||||
"You have no ignored folders.": "Vi havas neniujn ignoritajn dosierujojn.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Vi havas ne konservitaj ŝanĝoj. Ĉu vi vere volas forĵeti ilin?",
|
||||
"You must keep at least one version.": "Vi devas konservi almenaŭ unu version.",
|
||||
"days": "tagoj",
|
||||
"directories": "dosierujoj",
|
||||
"files": "dosieroj",
|
||||
"full documentation": "tuta dokumentado",
|
||||
"items": "eroj",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} volas komunigi dosierujon \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} volas komunigi dosierujon \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Copiado del original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes Colaboradores:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 Los siguientes colaboradores:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Crear patrones a ignorar, sobreescribiendo un fichero existente en {{path}}.",
|
||||
"Danger!": "¡Peligro!",
|
||||
"Debugging Facilities": "Ayudas a la depuración",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Copiado del original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes Colaboradores:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 Los siguientes colaboradores:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Crear patrones a ignorar, sobreescribiendo un fichero existente en {{path}}.",
|
||||
"Danger!": "¡Peligro!",
|
||||
"Debugging Facilities": "Servicios de depuración",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Kopioitu alkuperäisestä lähteestä",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 seuraavat avustajat",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 seuraavat avustajat:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Luodaan ohituslausekkeet. Ylikirjoitetaan tiedosto: {{path}}.",
|
||||
"Danger!": "Vaara!",
|
||||
"Debugging Facilities": "Debug -luokat",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Copié depuis l'original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016, les contributeurs sont:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017, les contributeurs sont:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 les Contributeurs suivants :",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Création de masques d'exclusion, remplacement du fichier existant : {{path}}.",
|
||||
"Danger!": "Attention !",
|
||||
"Debugging Facilities": "Outils de débogage",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Oernommen fan orizjineel",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 de folgende bydragers:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 de folgende Bydragers:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Meitsje negear-patroanen dy in besteande triem oerskriuwe yn {{path}}.",
|
||||
"Danger!": "Gefaar!",
|
||||
"Debugging Facilities": "Debug-foarsjennings",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Eredetiről másolva",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Szerzői jog © 2014-2016 az alábbi közreműködők:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Szerzői jog © 2014-2017 az alábbi közreműködők:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Szerzői jog © 2014-2019 az alábbi közreműködők:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Mellőzési minták létrehozása, egy létező fájl felülírása itt: {{path}}.",
|
||||
"Danger!": "Veszély!",
|
||||
"Debugging Facilities": "Hibakeresési képességek",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Copiato dall'originale",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 i seguenti Collaboratori:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 i seguenti Collaboratori:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creazione di schemi di esclusione, sovrascrivendo un file esistente in {{path}}.",
|
||||
"Danger!": "Pericolo!",
|
||||
"Debugging Facilities": "Servizi di Debug",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "元ファイルからコピー済",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "無視パターンを作成中。既存のファイルが {{path}} にある場合は上書きされます。",
|
||||
"Danger!": "危険!",
|
||||
"Debugging Facilities": "デバッグ機能",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "원본에서 복사됨",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "무시 패턴 만들기, {{path}}에 존재하는 파일을 덮어쓰기 합니다",
|
||||
"Danger!": "경고!",
|
||||
"Debugging Facilities": "디버깅 기능",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Nukopijuota iš originalo",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Autorių teisės © 2014-2016 šių bendraautorių:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Autorių teisės © 2014-2017 šių bendraautorių:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Autorių teisės © 2014-2019 šių bendraautorių:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Kuriami nepaisomi šablonai, perrašomas esamas failas, esantis {{path}}.",
|
||||
"Danger!": "Pavojus!",
|
||||
"Debugging Facilities": "Derinimo priemonės",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Kopiert fra original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Opphavsrett © 2014-2016 for følgende bidragsytere:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Opphavsrett © 2014-2017 for følgende bidragsytere:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Oppretter ignoreringsmønster, overskriver eksisterende fil i {{path}}.",
|
||||
"Danger!": "Fare!",
|
||||
"Debugging Facilities": "Feilrettingsverktøy",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Gekopieerd van origineel",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Auteursrecht © 2014-2016 voor de volgende bijdragers:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Auteursrecht © 2014-2017 voor de volgende bijdragers:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Auteursrecht © 2014-2019 voor de volgende bijdragers:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Negeerpatronen worden aangemaakt, bestaand bestand wordt overschreven op {{path}}.",
|
||||
"Danger!": "Let op!",
|
||||
"Debugging Facilities": "Debugmogelijkheden",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Skopiowane z oryginału",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016: ",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Prawa autorskie © 2014-2017 dla następujących autorów:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Ustawienie wzorów ignorowania, nadpisze istniejący plik w {{path}}.",
|
||||
"Danger!": "Niebezpieczne!",
|
||||
"Debugging Facilities": "Odpluskwianie",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Copiado do original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Direitos reservados © 2014-2016 aos seguintes colaboradores:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 dos seguintes Colaboradores:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Criando filtros, sobrescrevendo o arquivo {{path}}.",
|
||||
"Danger!": "Perigo!",
|
||||
"Debugging Facilities": "Facilidades de depuração",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Copiado do original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 os seguintes contribuidores:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 dos seguintes contribuidores:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 dos seguintes contribuidores:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Criando padrões de exclusão, sobrescrevendo um ficheiro existente em {{path}}.",
|
||||
"Danger!": "Perigo!",
|
||||
"Debugging Facilities": "Recursos de depuração",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Скопировано с оригинала",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Авторские права © 2014–2016 принадлежат:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Авторские права © 2014—2017 следующие участники:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Авторские права © 2014–2019 принадлежат:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Создание шаблонов игнорирования, существующий файл {{path}} будет перезаписан.",
|
||||
"Danger!": "Опасно!",
|
||||
"Debugging Facilities": "Средства отладки",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Skopírované z originálu",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následujúci prispivatelia:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 následujúci prispivatelia:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Vytváranie vzorov ignorovania, prepísanie existujúceho súboru v {{path}}.",
|
||||
"Danger!": "Pozor!",
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
|
||||
@@ -56,11 +56,12 @@
|
||||
"Copied from original": "Kopierat från original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragare:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 följande bidragande:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 följande bidragsgivare:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Skapa ignorera mönster, skriver över en existerande fil på {{path}}.",
|
||||
"Danger!": "Fara!",
|
||||
"Debugging Facilities": "Felsökningsanläggningar",
|
||||
"Default Folder Path": "Standard mappsökväg",
|
||||
"Deleted": "Tog bort",
|
||||
"Deleted": "Raderade",
|
||||
"Deselect All": "Avmarkera alla",
|
||||
"Device": "Enhet",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
@@ -108,10 +109,10 @@
|
||||
"File Pull Order": "Filhämtningsprioritering",
|
||||
"File Versioning": "Filversionshantering",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Filrättigheter ignoreras under sökning efter förändringar. Används på FAT-filsystem.",
|
||||
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Filer flyttas till .stversions-mappen vid byte eller tas bort av Syncthing.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till .stversions-mappen när de ersätts eller tas bort av Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Filer flyttas till datumstämplade versioner i en .stversions-mapp när de ersätts eller tas bort av Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till datummärkta versioner i en .stversions mapp när de ersätts eller tas bort av Syncthing.",
|
||||
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Filer flyttas till .stversions-mappen vid byte eller raderas av Syncthing.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till .stversions-mappen när de ersätts eller raderas av Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Filer flyttas till datumstämplade versioner i en .stversions-mapp när de ersätts eller raderas av Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till datummärkta versioner i en .stversions-mapp när de ersätts eller raderas av Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer skyddas från ändringar gjorda på andra enheter, men ändringar som görs på den här noden skickas till de andra klustermedlemmarna.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Filer synkroniseras från klustret, men alla ändringar som görs lokalt skickas inte till andra enheter.",
|
||||
"Filesystem Notifications": "filsystemsnotifieringar",
|
||||
@@ -187,7 +188,7 @@
|
||||
"Newest First": "Nyast först",
|
||||
"No": "Nej",
|
||||
"No File Versioning": "Ingen filversionshantering",
|
||||
"No files will be deleted as a result of this operation.": "Inga filer kommer att tas bort till följd av denna operation.",
|
||||
"No files will be deleted as a result of this operation.": "Inga filer kommer att raderas till följd av denna operation.",
|
||||
"No upgrades": "Inga uppgraderingar",
|
||||
"Normal": "Normal",
|
||||
"Notice": "Observera",
|
||||
@@ -203,7 +204,7 @@
|
||||
"Path": "Sökväg",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till mappen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
||||
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Sökvägen där nya automatiskt accepterade mappar kommer att skapas, liksom den föreslagna sökvägen när du lägger till nya mappar via gränssnittet. Tecknet tilde (~) expanderar till {{tilde}}.",
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Sökväg där versioner ska lagras (lämna tomt för standard .stversions-katalogen i den delade katalogen).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Sökväg där versioner ska lagras (lämna tomt för standard .stversions-mappen i den delade katalogen).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sökväg där versioner sparas (lämna tomt för att använda standard .stversions-mappen i mappen).",
|
||||
"Pause": "Paus",
|
||||
"Pause All": "Pausa alla",
|
||||
@@ -216,7 +217,7 @@
|
||||
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiska gränssnittets användarautentisering och lösenord i inställningsdialogrutan.",
|
||||
"Please wait": "Var god vänta",
|
||||
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix som indikerar att filen kan raderas om det förhindrar borttagning av katalog",
|
||||
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix som indikerar att filen kan raderas om det förhindrar radering av katalog",
|
||||
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefix som indikerar att mönstret ska matchas utan skiftlägeskänslighet",
|
||||
"Preview": "Förhandsgranska",
|
||||
"Preview Usage Report": "Förhandsgranska statistik",
|
||||
@@ -355,7 +356,7 @@
|
||||
"Version": "Version",
|
||||
"Versions": "Versioner",
|
||||
"Versions Path": "Sökväg för versioner",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i intervallet.",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner raderas automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i intervallet.",
|
||||
"Waiting to scan": "Väntar på uppdatering",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en överordnad mapp av en befintlig mapp \"{{otherFolder}}\".",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varning, denna sökväg är en överordnad mapp av en befintlig mapp \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "Скопійовано з оригіналу",
|
||||
"Copyright © 2014-2016 the following Contributors:": "© 2014-2016 Всі права застережено, вклад внесли:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "© 2014-2017 Всі права застережено, вклад внесли:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Створення шаблонів винятків з перезаписом існуючого файлу {{path}}.",
|
||||
"Danger!": "Небезпечно!",
|
||||
"Debugging Facilities": "Засоби відладки",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "从源复制",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 以下贡献者:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 以下贡献者:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "版权所有 © 2014-2019 以下贡献者:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "正在创建忽略模式,覆盖位于 {{path}} 的已有文件。",
|
||||
"Danger!": "危险!",
|
||||
"Debugging Facilities": "调试功能",
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"Copied from original": "從原處複製",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 下列貢獻者:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 下列貢獻者:",
|
||||
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "建立忽略樣式,覆蓋已存在的 {{path}}。",
|
||||
"Danger!": "危險!",
|
||||
"Debugging Facilities": "除錯工具",
|
||||
|
||||
@@ -1 +1 @@
|
||||
var langPrettyprint = {"bg":"Bulgarian","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sk":"Slovak","sv":"Swedish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
|
||||
var langPrettyprint = {"bg":"Bulgarian","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","eo":"Esperanto","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sk":"Slovak","sv":"Swedish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
var validLangs = ["bg","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","fi","fr","fy","hu","it","ja","ko-KR","lt","nb","nl","pl","pt-BR","pt-PT","ru","sk","sv","uk","zh-CN","zh-TW"]
|
||||
var validLangs = ["bg","ca@valencia","cs","da","de","el","en","en-GB","eo","es","es-ES","fi","fr","fy","hu","it","ja","ko-KR","lt","nb","nl","pl","pt-BR","pt-PT","ru","sk","sv","uk","zh-CN","zh-TW"]
|
||||
|
||||
@@ -328,6 +328,7 @@
|
||||
<span ng-show="syncRemaining(folder.id)">({{syncPercentage(folder.id) | percent}}, {{syncRemaining(folder.id) | binary}}B)</span>
|
||||
</span>
|
||||
<span ng-switch-when="outofsync"><span class="hidden-xs" translate>Out of Sync</span><span class="visible-xs" aria-label="{{'Out of Sync' | translate}}"><i class="fas fa-fw fa-exclamation-circle"></i></span></span>
|
||||
<span ng-switch-when="faileditems"><span class="hidden-xs" translate>Failed Items</span><span class="visible-xs" aria-label="{{'Failed Items' | translate}}"><i class="fas fa-fw fa-exclamation-circle"></i></span></span>
|
||||
</div>
|
||||
<div class="panel-title-text">
|
||||
<span tooltip data-original-title="{{folder.label.length != 0 ? folder.id : ''}}">{{folder.label.length != 0 ? folder.label : folder.id}}</span>
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
</h1>
|
||||
<hr />
|
||||
|
||||
<p translate>Syncthing is Free and Open Source Software licensed as MPL v2.0.</p>
|
||||
<hr />
|
||||
<p translate>Copyright © 2014-2019 the following Contributors:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, Aaron Bieber, Adam Piggott, Adel Qalieh, Alessandro G., Andrew Dunham, Andrew Rabert, Andrey D, Antoine Lamielle, Aranjedeath, Arthur Axel fREW Schmidt, BAHADIR YILMAZ, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benno Fünfstück, Benny Ng, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Chris Tonkinson, Colin Kennedy, Cromefire_, Dale Visser, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Erik Meitner, Evgeny Kuznetsov, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Graham Miln, Han Boetes, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan Cross, Jose Manuel Delicado, Jörg Thalheim, Karol Różycki, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Kurt Fitzner, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mark Pulford, Mateusz Naściszewski, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max Schulze, MaximAL, Maxime Thirouin, Michael Jephcote, Michael Tilli, Mike Boone, MikeLund, Nicholas Rishel, Nico Stapelbroek, Nicolas Braud-Santoni, Niels Peter Roest, Nils Jakobi, NoLooseEnds, Oyebanji Jacob Mayowa, Pascal Jungblut, Pawel Palenica, Paweł Rozlach, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Richard Hartmann, Robert Carosi, Roman Zaynetdinov, Ross Smith II, Sacheendra Talluri, Scott Klupfel, Sly_tom_cat, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tomas Cerveny, Tommy Thorn, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, Vladimir Rusinov, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, chucic, derekriemer, desbma, janost, jaseg, klemens, marco-m, otbutz, perewa, rubenbe, wangguoliang, xjtdy888, 佛跳墙
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, Aaron Bieber, Adam Piggott, Adel Qalieh, Alessandro G., Andrew Dunham, Andrew Rabert, Andrey D, Antoine Lamielle, Aranjedeath, Arthur Axel fREW Schmidt, BAHADIR YILMAZ, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benno Fünfstück, Benny Ng, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Chris Tonkinson, Colin Kennedy, Cromefire_, Dale Visser, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Erik Meitner, Evgeny Kuznetsov, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Graham Miln, Han Boetes, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan Cross, Jose Manuel Delicado, Jörg Thalheim, Karol Różycki, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Kurt Fitzner, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mark Pulford, Mateusz Naściszewski, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max Schulze, MaximAL, Maxime Thirouin, Michael Jephcote, Michael Tilli, Mike Boone, MikeLund, Nicholas Rishel, Nico Stapelbroek, Nicolas Braud-Santoni, Niels Peter Roest, Nils Jakobi, NoLooseEnds, Oyebanji Jacob Mayowa, Pascal Jungblut, Pawel Palenica, Paweł Rozlach, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Richard Hartmann, Robert Carosi, Roman Zaynetdinov, Ross Smith II, Sacheendra Talluri, Scott Klupfel, Sly_tom_cat, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tomas Cerveny, Tommy Thorn, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, Vladimir Rusinov, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, chucic, derekriemer, desbma, georgespatton, janost, jaseg, klemens, marco-m, otbutz, perewa, rubenbe, wangguoliang, xjtdy888, 佛跳墙
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
|
||||
@@ -751,6 +751,9 @@ angular.module('syncthing.core')
|
||||
if (state === 'idle' && $scope.model[folderCfg.id].needTotalItems > 0) {
|
||||
return 'outofsync';
|
||||
}
|
||||
if ($scope.hasFailedFiles(folderCfg.id)) {
|
||||
return 'faileditems';
|
||||
}
|
||||
if (state === 'scanning') {
|
||||
return state;
|
||||
}
|
||||
@@ -777,7 +780,7 @@ angular.module('syncthing.core')
|
||||
if (status === 'unknown') {
|
||||
return 'info';
|
||||
}
|
||||
if (status === 'stopped' || status === 'outofsync' || status === 'error') {
|
||||
if (status === 'stopped' || status === 'outofsync' || status === 'error' || status === 'faileditems') {
|
||||
return 'danger';
|
||||
}
|
||||
if (status === 'unshared' || status === 'scan-waiting') {
|
||||
|
||||
@@ -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")
|
||||
@@ -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-----
|
||||
MIIG5AIBAAKCAYEApkr+yNvpm5VUc++jvsKnkTwt+LOd+ghYlGhf8xzMT32IPIVm
|
||||
bVZ/0YVPxaFcgIwLaSAZuv+ADGGR4RxnnTS+EDYL5qiKzUMZxv681jncjSqne9JS
|
||||
XijDnlHTEnhSOkmclYXyn2I8MzAw/pZD8T96QpEoI63RMWPjGq2JK3SnE8t4d8pW
|
||||
lZfGohUOGeygZD41a5SMzcWSYSaGnkN1G3CySJlljPXMKCgeQXMq3/4jGMK7Ns7T
|
||||
5OLxHdv93QD5Ys0sevRKNq1qMtsd5sD71ze2zq3dXljNwyoMngzLEW1GhcUmMRtw
|
||||
z0iopATy3xMiG/xrfTbJGKQna4/H59ckaiymeZYJtoratWI9u8gJKC9IEDcCzR/f
|
||||
I/NewS35pbk2YNV8sFfUNTs7DqgGplOUj5JOvNlHWp+8i7B4hag+qc813izG6jEA
|
||||
rnRWJQp061Jgv9oXR4zdupunajOFy34F+aq7agj9Edd75FR8BeuUVDynqNwhJbaK
|
||||
n6iIG1HgZmSdwj1hAgMBAAECggGAQkd334TPSmStgXwNLrYU5a0vwYWNvJ9g9t3X
|
||||
CGX9BN3K1BxzY7brQQ46alHTNaUb0y2pM8AsQEMPSsLwhVcFPh7chXW9xOwutQLJ
|
||||
LzVms5lBofeFPuROe6avUxhD5dl7IJl/x4j254wYqxAnSlt7llaWwgnAbEgct4Bd
|
||||
QMXA5gHeJRivg/Y3hFiSA0Et+GZXEmbl7AoIOtKJK0FFxscXOBpzwEgjtAmxbXLC
|
||||
rv5y7KaIyeKL0Bmn8rfBKjn+LCQMJt4wZCrNtFLg3aSpkmqZl6r8Q84OwHMp2x8l
|
||||
SFNVi7j1Cv8DC/yhyEOCbHIRZrK/vzt6Cqe+yjr1UG9niwhQJbEvaV26odzvMSNZ
|
||||
1VodN+ltCZRFFEBc+z3CR7SKDZayT93dLxolzQ4DuSfDnk0fBLtOfeISxS/Wg7Yv
|
||||
5q0XF6cTmQEsDbuDswvlHo3k8w3cjz9SmxMasxgHx6jHkSBbkw0iFLT3KdqA8PrG
|
||||
D3uo67fIQEkcncmRLP3I1qUiWX21AoHBAMVQLLgOd3bOrByyVeugA+5dhef0uopJ
|
||||
GadzBlAT4EY7Vuxu1Qu/m876FnhQc3tGTLfZhcnL9VXV+3DSTosfRz+YDm+K5lOh
|
||||
ZRtswuZscm+l26X+2j1h+AGW8SIz5f9M0CnFpqjC8KkopPk/ZKTcDvrNRRxI5EPx
|
||||
TPZaiPhztlcsc7K5jkLJRL0GiadUniOFY7kUA18hs3MEyzkdYbz8WolUyHeSJT2H
|
||||
hmpdsA5tzUKB1NVdsIsjWESQF3Hd2FFHMwKBwQDXwOCUq5KSBKa1BSO1oQxhyHy3
|
||||
ZQ86d5weLNxovwrHd4ivaVPJ46YLjNk+/q685XPUfoDxO1fnyIYIy4ChtkhXmyli
|
||||
LOPfNt0iSW2M1/L1wb6ZwMz+RWpb3zqPgjMlDCEtD5hQ8Cl5do2tyh3sIrLgamVG
|
||||
sY1hx+VD0BmXUUTGjl8nJqQSMYl6IXTKzrFrx+QWdzA0yWN753XiAF5cLkxNahes
|
||||
SKb/ibrMtO/JKt3RBlZPS3wiFRkxtNcS1HrVWRsCgcBaFir0thYxNlc6munDtMFW
|
||||
uXiD2Sa6MHn4C/pb4VdKeZlMRaYbwRYAQAq2T/UJ2aT5Y+VDp02SLSqp7jtSJavA
|
||||
C0q7/qz+jfe9t8Cct/LfqthIR72YvPwgravWs99U2ttH1ygqcSaz9QytiBYJdzeX
|
||||
ptTg/x7JLoi3CcrztNERqAgDF9kuAPrTWwLKVUYGbcaEH/ESJC7sWsn2f8W6JXWo
|
||||
sf79KMq79v6V3cSeMd+/d8uWxzntrOuGEkvB/0negiUCgcEAp0YwGLQJGFKo2XIZ
|
||||
pIkva2SgZSPiMadoj/CiFkf/2HRxseYMg1uPcicKjA+zdFrFejt2RxGGbvsGCC2X
|
||||
FkmYPuvaovZA2d/UhO+/EtKe2TEUUGqtxHoXIxGoenkspA2Kb0BHDIGW9kgXQmWQ
|
||||
23JvkxSKXsvr3KK5uuDN5oaotvTNCzKnRD/J4bmsrkygO/sneM+BvXtiOT9UIxu8
|
||||
DOYMXHzjy7wsVbT38hxaSHKGtbefFS1mGZqYBPS7Rysb7Ot/AoHBAL0SAbt1a2Ol
|
||||
ObrK8vjTHcQHJH74n+6PWRfsBO+UJ1vtOYFzW85BiVZmi8tC4bJ0Hd89TT7AibzP
|
||||
L1Ftrn0XmBfniwV1SsrjVaRy/KbBeUhjruqyQ2oDLEU7DAm5Z2jG4aG2rLbXYAS9
|
||||
yOQITLN5AVraI4Pr1IWjZTzd/zaaWA5nFNthyXSww1II0f1BgX1S/49k4aWjXeMn
|
||||
FrKN5T7BqIh9W6d7YTrzXoH9lEsUPQHV/ci+YRP4mrfrcC9hJZ3O9g==
|
||||
-----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-----
|
||||
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-----
|
||||
@@ -482,7 +482,7 @@ func (w *wrapper) AddOrUpdatePendingDevice(device protocol.DeviceID, name, addre
|
||||
|
||||
for i := range w.cfg.PendingDevices {
|
||||
if w.cfg.PendingDevices[i].ID == device {
|
||||
w.cfg.PendingDevices[i].Time = time.Now()
|
||||
w.cfg.PendingDevices[i].Time = time.Now().Round(time.Second)
|
||||
w.cfg.PendingDevices[i].Name = name
|
||||
w.cfg.PendingDevices[i].Address = address
|
||||
return
|
||||
@@ -490,7 +490,7 @@ func (w *wrapper) AddOrUpdatePendingDevice(device protocol.DeviceID, name, addre
|
||||
}
|
||||
|
||||
w.cfg.PendingDevices = append(w.cfg.PendingDevices, ObservedDevice{
|
||||
Time: time.Now(),
|
||||
Time: time.Now().Round(time.Second),
|
||||
ID: device,
|
||||
Name: name,
|
||||
Address: address,
|
||||
@@ -508,12 +508,12 @@ func (w *wrapper) AddOrUpdatePendingFolder(id, label string, device protocol.Dev
|
||||
for j := range w.cfg.Devices[i].PendingFolders {
|
||||
if w.cfg.Devices[i].PendingFolders[j].ID == id {
|
||||
w.cfg.Devices[i].PendingFolders[j].Label = label
|
||||
w.cfg.Devices[i].PendingFolders[j].Time = time.Now()
|
||||
w.cfg.Devices[i].PendingFolders[j].Time = time.Now().Round(time.Second)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.cfg.Devices[i].PendingFolders = append(w.cfg.Devices[i].PendingFolders, ObservedFolder{
|
||||
Time: time.Now(),
|
||||
Time: time.Now().Round(time.Second),
|
||||
ID: id,
|
||||
Label: label,
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/syncthing/notify"
|
||||
)
|
||||
@@ -32,6 +33,12 @@ func (f *BasicFilesystem) Watch(name string, ignore Matcher, ctx context.Context
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remove `\\?\` prefix if the path is just a drive letter as a dirty
|
||||
// fix for https://github.com/syncthing/syncthing/issues/5578
|
||||
if runtime.GOOS == "windows" && len(absName) <= 7 && len(absName) > 4 && absName[:4] == `\\?\` {
|
||||
absName = absName[4:]
|
||||
}
|
||||
|
||||
outChan := make(chan Event)
|
||||
backendChan := make(chan notify.EventInfo, backendBuffer)
|
||||
|
||||
|
||||
@@ -198,9 +198,9 @@ func (f *folder) Serve() {
|
||||
|
||||
func (f *folder) BringToFront(string) {}
|
||||
|
||||
func (f *folder) Override(fs *db.FileSet, updateFn func([]protocol.FileInfo)) {}
|
||||
func (f *folder) Override() {}
|
||||
|
||||
func (f *folder) Revert(fs *db.FileSet, updateFn func([]protocol.FileInfo)) {}
|
||||
func (f *folder) Revert() {}
|
||||
|
||||
func (f *folder) DelayScan(next time.Duration) {
|
||||
f.Delay(next)
|
||||
@@ -345,7 +345,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
Subs: subDirs,
|
||||
Matcher: f.ignores,
|
||||
TempLifetime: time.Duration(f.model.cfg.Options().KeepTemporariesH) * time.Hour,
|
||||
CurrentFiler: cFiler{f.model, f.ID},
|
||||
CurrentFiler: cFiler{f.fset},
|
||||
Filesystem: mtimefs,
|
||||
IgnorePerms: f.IgnorePerms,
|
||||
AutoNormalize: f.AutoNormalize,
|
||||
@@ -361,7 +361,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
l.Debugf("Stopping scan of folder %s due to: %s", f.Description(), err)
|
||||
return err
|
||||
}
|
||||
f.model.updateLocalsFromScanning(f.ID, fs)
|
||||
f.updateLocalsFromScanning(fs)
|
||||
return nil
|
||||
}
|
||||
// Resolve items which are identical with the global state.
|
||||
@@ -737,6 +737,86 @@ func (f *folder) Errors() []FileError {
|
||||
return append([]FileError{}, f.scanErrors...)
|
||||
}
|
||||
|
||||
// ForceRescan marks the file such that it gets rehashed on next scan and then
|
||||
// immediately executes that scan.
|
||||
func (f *folder) ForceRescan(file protocol.FileInfo) error {
|
||||
file.SetMustRescan(f.shortID)
|
||||
f.fset.Update(protocol.LocalDeviceID, []protocol.FileInfo{file})
|
||||
|
||||
return f.Scan([]string{file.Name})
|
||||
}
|
||||
|
||||
func (f *folder) updateLocalsFromScanning(fs []protocol.FileInfo) {
|
||||
f.updateLocals(fs)
|
||||
|
||||
f.emitDiskChangeEvents(fs, events.LocalChangeDetected)
|
||||
}
|
||||
|
||||
func (f *folder) updateLocalsFromPulling(fs []protocol.FileInfo) {
|
||||
f.updateLocals(fs)
|
||||
|
||||
f.emitDiskChangeEvents(fs, events.RemoteChangeDetected)
|
||||
}
|
||||
|
||||
func (f *folder) updateLocals(fs []protocol.FileInfo) {
|
||||
f.fset.Update(protocol.LocalDeviceID, fs)
|
||||
|
||||
filenames := make([]string, len(fs))
|
||||
for i, file := range fs {
|
||||
filenames[i] = file.Name
|
||||
}
|
||||
|
||||
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
|
||||
"folder": f.ID,
|
||||
"items": len(fs),
|
||||
"filenames": filenames,
|
||||
"version": f.fset.Sequence(protocol.LocalDeviceID),
|
||||
})
|
||||
}
|
||||
|
||||
func (f *folder) emitDiskChangeEvents(fs []protocol.FileInfo, typeOfEvent events.EventType) {
|
||||
for _, file := range fs {
|
||||
if file.IsInvalid() {
|
||||
continue
|
||||
}
|
||||
|
||||
objType := "file"
|
||||
action := "modified"
|
||||
|
||||
switch {
|
||||
case file.IsDeleted():
|
||||
action = "deleted"
|
||||
|
||||
// If our local vector is version 1 AND it is the only version
|
||||
// vector so far seen for this file then it is a new file. Else if
|
||||
// it is > 1 it's not new, and if it is 1 but another shortId
|
||||
// version vector exists then it is new for us but created elsewhere
|
||||
// so the file is still not new but modified by us. Only if it is
|
||||
// truly new do we change this to 'added', else we leave it as
|
||||
// 'modified'.
|
||||
case len(file.Version.Counters) == 1 && file.Version.Counters[0].Value == 1:
|
||||
action = "added"
|
||||
}
|
||||
|
||||
if file.IsSymlink() {
|
||||
objType = "symlink"
|
||||
} else if file.IsDirectory() {
|
||||
objType = "dir"
|
||||
}
|
||||
|
||||
// Two different events can be fired here based on what EventType is passed into function
|
||||
events.Default.Log(typeOfEvent, map[string]string{
|
||||
"folder": f.ID,
|
||||
"folderID": f.ID, // incorrect, deprecated, kept for historical compliance
|
||||
"label": f.Label,
|
||||
"action": action,
|
||||
"type": objType,
|
||||
"path": filepath.FromSlash(file.Name),
|
||||
"modifiedBy": file.ModifiedBy.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The exists function is expected to return true for all known paths
|
||||
// (excluding "" and ".")
|
||||
func unifySubs(dirs []string, exists func(dir string) bool) []string {
|
||||
@@ -770,3 +850,12 @@ func unifySubs(dirs []string, exists func(dir string) bool) []string {
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
type cFiler struct {
|
||||
*db.FileSet
|
||||
}
|
||||
|
||||
// Implements scanner.CurrentFiler
|
||||
func (cf cFiler) CurrentFile(file string) (protocol.FileInfo, bool) {
|
||||
return cf.Get(protocol.LocalDeviceID, file)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func newReceiveOnlyFolder(model *model, fset *db.FileSet, ignores *ignore.Matche
|
||||
return &receiveOnlyFolder{sr}
|
||||
}
|
||||
|
||||
func (f *receiveOnlyFolder) Revert(fs *db.FileSet, updateFn func([]protocol.FileInfo)) {
|
||||
func (f *receiveOnlyFolder) Revert() {
|
||||
f.setState(FolderScanning)
|
||||
defer f.setState(FolderIdle)
|
||||
|
||||
@@ -78,7 +78,7 @@ func (f *receiveOnlyFolder) Revert(fs *db.FileSet, updateFn func([]protocol.File
|
||||
|
||||
batch := make([]protocol.FileInfo, 0, maxBatchSizeFiles)
|
||||
batchSizeBytes := 0
|
||||
fs.WithHave(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
f.fset.WithHave(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
fi := intf.(protocol.FileInfo)
|
||||
if !fi.IsReceiveOnlyChanged() {
|
||||
// We're only interested in files that have changed locally in
|
||||
@@ -124,14 +124,14 @@ func (f *receiveOnlyFolder) Revert(fs *db.FileSet, updateFn func([]protocol.File
|
||||
batchSizeBytes += fi.ProtoSize()
|
||||
|
||||
if len(batch) >= maxBatchSizeFiles || batchSizeBytes >= maxBatchSizeBytes {
|
||||
updateFn(batch)
|
||||
f.updateLocalsFromScanning(batch)
|
||||
batch = batch[:0]
|
||||
batchSizeBytes = 0
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(batch) > 0 {
|
||||
updateFn(batch)
|
||||
f.updateLocalsFromScanning(batch)
|
||||
}
|
||||
batch = batch[:0]
|
||||
batchSizeBytes = 0
|
||||
@@ -153,7 +153,7 @@ func (f *receiveOnlyFolder) Revert(fs *db.FileSet, updateFn func([]protocol.File
|
||||
})
|
||||
}
|
||||
if len(batch) > 0 {
|
||||
updateFn(batch)
|
||||
f.updateLocalsFromScanning(batch)
|
||||
}
|
||||
|
||||
// We will likely have changed our local index, but that won't trigger a
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -22,34 +23,32 @@ import (
|
||||
)
|
||||
|
||||
func TestRecvOnlyRevertDeletes(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
// Make sure that we delete extraneous files and directories when we hit
|
||||
// Revert.
|
||||
|
||||
testOs.RemoveAll("_recvonly")
|
||||
defer testOs.RemoveAll("_recvonly")
|
||||
// Get us a model up and running
|
||||
|
||||
m, f := setupROFolder()
|
||||
ffs := f.Filesystem()
|
||||
defer os.Remove(m.cfg.ConfigPath())
|
||||
defer os.Remove(ffs.URI())
|
||||
defer m.Stop()
|
||||
|
||||
// Create some test data
|
||||
|
||||
testOs.MkdirAll("_recvonly/.stfolder", 0755)
|
||||
testOs.MkdirAll("_recvonly/ignDir", 0755)
|
||||
testOs.MkdirAll("_recvonly/unknownDir", 0755)
|
||||
must(t, ioutil.WriteFile("_recvonly/ignDir/ignFile", []byte("hello\n"), 0644))
|
||||
must(t, ioutil.WriteFile("_recvonly/unknownDir/unknownFile", []byte("hello\n"), 0644))
|
||||
must(t, ioutil.WriteFile("_recvonly/.stignore", []byte("ignDir\n"), 0644))
|
||||
for _, dir := range []string{".stfolder", "ignDir", "unknownDir"} {
|
||||
must(t, ffs.MkdirAll(dir, 0755))
|
||||
}
|
||||
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "ignDir/ignFile"), []byte("hello\n"), 0644))
|
||||
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "unknownDir/unknownFile"), []byte("hello\n"), 0644))
|
||||
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), ".stignore"), []byte("ignDir\n"), 0644))
|
||||
|
||||
knownFiles := setupKnownFiles(t, []byte("hello\n"))
|
||||
|
||||
// Get us a model up and running
|
||||
|
||||
m := setupROFolder()
|
||||
defer m.Stop()
|
||||
knownFiles := setupKnownFiles(t, ffs, []byte("hello\n"))
|
||||
|
||||
// Send and index update for the known stuff
|
||||
|
||||
m.Index(device1, "ro", knownFiles)
|
||||
m.updateLocalsFromScanning("ro", knownFiles)
|
||||
f.updateLocalsFromScanning(knownFiles)
|
||||
|
||||
size := m.GlobalSize("ro")
|
||||
if size.Files != 1 || size.Directories != 1 {
|
||||
@@ -81,17 +80,15 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
|
||||
m.Revert("ro")
|
||||
|
||||
// These should still exist
|
||||
for _, p := range []string{"_recvonly/knownDir/knownFile", "_recvonly/ignDir/ignFile"} {
|
||||
_, err := os.Stat(p)
|
||||
if err != nil {
|
||||
for _, p := range []string{"knownDir/knownFile", "ignDir/ignFile"} {
|
||||
if _, err := ffs.Stat(p); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// These should have been removed
|
||||
for _, p := range []string{"_recvonly/unknownDir", "_recvonly/unknownDir/unknownFile"} {
|
||||
_, err := os.Stat(p)
|
||||
if !os.IsNotExist(err) {
|
||||
for _, p := range []string{"unknownDir", "unknownDir/unknownFile"} {
|
||||
if _, err := ffs.Stat(p); !fs.IsNotExist(err) {
|
||||
t.Error("Unexpected existing thing:", p)
|
||||
}
|
||||
}
|
||||
@@ -109,29 +106,27 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRecvOnlyRevertNeeds(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
// Make sure that a new file gets picked up and considered latest, then
|
||||
// gets considered old when we hit Revert.
|
||||
|
||||
testOs.RemoveAll("_recvonly")
|
||||
defer testOs.RemoveAll("_recvonly")
|
||||
// Get us a model up and running
|
||||
|
||||
m, f := setupROFolder()
|
||||
ffs := f.Filesystem()
|
||||
defer os.Remove(m.cfg.ConfigPath())
|
||||
defer os.Remove(ffs.URI())
|
||||
defer m.Stop()
|
||||
|
||||
// Create some test data
|
||||
|
||||
testOs.MkdirAll("_recvonly/.stfolder", 0755)
|
||||
must(t, ffs.MkdirAll(".stfolder", 0755))
|
||||
oldData := []byte("hello\n")
|
||||
knownFiles := setupKnownFiles(t, oldData)
|
||||
|
||||
// Get us a model up and running
|
||||
|
||||
m := setupROFolder()
|
||||
defer m.Stop()
|
||||
knownFiles := setupKnownFiles(t, ffs, oldData)
|
||||
|
||||
// Send and index update for the known stuff
|
||||
|
||||
m.Index(device1, "ro", knownFiles)
|
||||
m.updateLocalsFromScanning("ro", knownFiles)
|
||||
f.updateLocalsFromScanning(knownFiles)
|
||||
|
||||
// Start the folder. This will cause a scan.
|
||||
|
||||
@@ -160,7 +155,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
|
||||
// Update the file.
|
||||
|
||||
newData := []byte("totally different data\n")
|
||||
must(t, ioutil.WriteFile("_recvonly/knownDir/knownFile", newData, 0644))
|
||||
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "knownDir/knownFile"), newData, 0644))
|
||||
|
||||
// Rescan.
|
||||
|
||||
@@ -207,19 +202,19 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
|
||||
func TestRecvOnlyUndoChanges(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
testOs.RemoveAll("_recvonly")
|
||||
defer testOs.RemoveAll("_recvonly")
|
||||
// Get us a model up and running
|
||||
|
||||
m, f := setupROFolder()
|
||||
ffs := f.Filesystem()
|
||||
defer os.Remove(m.cfg.ConfigPath())
|
||||
defer os.Remove(ffs.URI())
|
||||
defer m.Stop()
|
||||
|
||||
// Create some test data
|
||||
|
||||
testOs.MkdirAll("_recvonly/.stfolder", 0755)
|
||||
must(t, ffs.MkdirAll(".stfolder", 0755))
|
||||
oldData := []byte("hello\n")
|
||||
knownFiles := setupKnownFiles(t, oldData)
|
||||
|
||||
// Get us a model up and running
|
||||
|
||||
m := setupROFolder()
|
||||
defer m.Stop()
|
||||
knownFiles := setupKnownFiles(t, ffs, oldData)
|
||||
|
||||
m.fmut.Lock()
|
||||
fset := m.folderFiles["ro"]
|
||||
@@ -229,7 +224,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
|
||||
// Send and index update for the known stuff
|
||||
|
||||
m.Index(device1, "ro", knownFiles)
|
||||
m.updateLocalsFromScanning("ro", knownFiles)
|
||||
f.updateLocalsFromScanning(knownFiles)
|
||||
|
||||
// Start the folder. This will cause a scan.
|
||||
|
||||
@@ -257,10 +252,10 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
|
||||
|
||||
// Create a file and modify another
|
||||
|
||||
file := "_recvonly/foo"
|
||||
file := filepath.Join(ffs.URI(), "foo")
|
||||
must(t, ioutil.WriteFile(file, []byte("hello\n"), 0644))
|
||||
|
||||
must(t, ioutil.WriteFile("_recvonly/knownDir/knownFile", []byte("bye\n"), 0644))
|
||||
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "knownDir/knownFile"), []byte("bye\n"), 0644))
|
||||
|
||||
m.ScanFolder("ro")
|
||||
|
||||
@@ -272,7 +267,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
|
||||
// Remove the file again and undo the modification
|
||||
|
||||
testOs.Remove(file)
|
||||
must(t, ioutil.WriteFile("_recvonly/knownDir/knownFile", oldData, 0644))
|
||||
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "knownDir/knownFile"), oldData, 0644))
|
||||
folderFs.Chtimes("knownDir/knownFile", knownFiles[1].ModTime(), knownFiles[1].ModTime())
|
||||
|
||||
m.ScanFolder("ro")
|
||||
@@ -283,16 +278,19 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func setupKnownFiles(t *testing.T, data []byte) []protocol.FileInfo {
|
||||
testOs := &fatalOs{t}
|
||||
func setupKnownFiles(t *testing.T, ffs fs.Filesystem, data []byte) []protocol.FileInfo {
|
||||
t.Helper()
|
||||
|
||||
testOs.MkdirAll("_recvonly/knownDir", 0755)
|
||||
must(t, ioutil.WriteFile("_recvonly/knownDir/knownFile", data, 0644))
|
||||
must(t, ffs.MkdirAll("knownDir", 0755))
|
||||
must(t, ioutil.WriteFile(filepath.Join(ffs.URI(), "knownDir/knownFile"), data, 0644))
|
||||
|
||||
t0 := time.Now().Add(-1 * time.Minute)
|
||||
testOs.Chtimes("_recvonly/knownDir/knownFile", t0, t0)
|
||||
must(t, ffs.Chtimes("knownDir/knownFile", t0, t0))
|
||||
|
||||
fi := testOs.Stat("_recvonly/knownDir/knownFile")
|
||||
fi, err := ffs.Stat("knownDir/knownFile")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
blocks, _ := scanner.Blocks(context.TODO(), bytes.NewReader(data), protocol.BlockSize(int64(len(data))), int64(len(data)), nil, true)
|
||||
knownFiles := []protocol.FileInfo{
|
||||
{
|
||||
@@ -318,23 +316,24 @@ func setupKnownFiles(t *testing.T, data []byte) []protocol.FileInfo {
|
||||
return knownFiles
|
||||
}
|
||||
|
||||
func setupROFolder() *model {
|
||||
fcfg := config.NewFolderConfiguration(myID, "ro", "receive only test", fs.FilesystemTypeBasic, "_recvonly")
|
||||
func setupROFolder() (*model, *sendOnlyFolder) {
|
||||
w := createTmpWrapper(defaultCfg)
|
||||
fcfg := testFolderConfigTmp()
|
||||
fcfg.ID = "ro"
|
||||
fcfg.Type = config.FolderTypeReceiveOnly
|
||||
fcfg.Devices = []config.FolderDeviceConfiguration{{DeviceID: device1}}
|
||||
fcfg.FSWatcherEnabled = false
|
||||
fcfg.RescanIntervalS = 86400
|
||||
w.SetFolder(fcfg)
|
||||
|
||||
cfg := defaultCfg.Copy()
|
||||
cfg.Folders = append(cfg.Folders, fcfg)
|
||||
|
||||
wrp := createTmpWrapper(cfg)
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := newModel(wrp, myID, "syncthing", "dev", db, nil)
|
||||
|
||||
m.ServeBackground()
|
||||
m := newModel(w, myID, "syncthing", "dev", db.OpenMemory(), nil)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
return m
|
||||
f := &sendOnlyFolder{
|
||||
folder: folder{
|
||||
fset: m.folderFiles[fcfg.ID],
|
||||
FolderConfiguration: fcfg,
|
||||
},
|
||||
}
|
||||
|
||||
m.ServeBackground()
|
||||
|
||||
return m, f
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func (f *sendOnlyFolder) pull() bool {
|
||||
|
||||
f.fset.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
if len(batch) == maxBatchSizeFiles || batchSizeBytes > maxBatchSizeBytes {
|
||||
f.model.updateLocalsFromPulling(f.folderID, batch)
|
||||
f.updateLocalsFromPulling(batch)
|
||||
batch = batch[:0]
|
||||
batchSizeBytes = 0
|
||||
}
|
||||
@@ -85,25 +85,25 @@ func (f *sendOnlyFolder) pull() bool {
|
||||
})
|
||||
|
||||
if len(batch) > 0 {
|
||||
f.model.updateLocalsFromPulling(f.folderID, batch)
|
||||
f.updateLocalsFromPulling(batch)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *sendOnlyFolder) Override(fs *db.FileSet, updateFn func([]protocol.FileInfo)) {
|
||||
func (f *sendOnlyFolder) Override() {
|
||||
f.setState(FolderScanning)
|
||||
batch := make([]protocol.FileInfo, 0, maxBatchSizeFiles)
|
||||
batchSizeBytes := 0
|
||||
fs.WithNeed(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
|
||||
f.fset.WithNeed(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
|
||||
need := fi.(protocol.FileInfo)
|
||||
if len(batch) == maxBatchSizeFiles || batchSizeBytes > maxBatchSizeBytes {
|
||||
updateFn(batch)
|
||||
f.updateLocalsFromScanning(batch)
|
||||
batch = batch[:0]
|
||||
batchSizeBytes = 0
|
||||
}
|
||||
|
||||
have, ok := fs.Get(protocol.LocalDeviceID, need.Name)
|
||||
have, ok := f.fset.Get(protocol.LocalDeviceID, need.Name)
|
||||
// Don't override files that are in a bad state (ignored,
|
||||
// unsupported, must rescan, ...).
|
||||
if ok && have.IsInvalid() {
|
||||
@@ -126,7 +126,7 @@ func (f *sendOnlyFolder) Override(fs *db.FileSet, updateFn func([]protocol.FileI
|
||||
return true
|
||||
})
|
||||
if len(batch) > 0 {
|
||||
updateFn(batch)
|
||||
f.updateLocalsFromScanning(batch)
|
||||
}
|
||||
f.setState(FolderIdle)
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ func (f *sendReceiveFolder) pullerIteration(scanChan chan<- string) int {
|
||||
doneWg.Done()
|
||||
}()
|
||||
|
||||
changed, fileDeletions, dirDeletions, err := f.processNeeded(dbUpdateChan, copyChan, finisherChan, scanChan)
|
||||
changed, fileDeletions, dirDeletions, err := f.processNeeded(dbUpdateChan, copyChan, scanChan)
|
||||
|
||||
// Signal copy and puller routines that we are done with the in data for
|
||||
// this iteration. Wait for them to finish.
|
||||
@@ -290,9 +290,10 @@ func (f *sendReceiveFolder) pullerIteration(scanChan chan<- string) int {
|
||||
return changed
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState, scanChan chan<- string) (int, map[string]protocol.FileInfo, []protocol.FileInfo, error) {
|
||||
func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyChan chan<- copyBlocksState, scanChan chan<- string) (int, map[string]protocol.FileInfo, []protocol.FileInfo, error) {
|
||||
defer f.queue.Reset()
|
||||
|
||||
changed := 0
|
||||
var processDirectly []protocol.FileInfo
|
||||
var dirDeletions []protocol.FileInfo
|
||||
fileDeletions := map[string]protocol.FileInfo{}
|
||||
buckets := map[string][]protocol.FileInfo{}
|
||||
@@ -346,8 +347,16 @@ func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyC
|
||||
changed++
|
||||
|
||||
case file.Type == protocol.FileInfoTypeFile:
|
||||
// Queue files for processing after directories and symlinks.
|
||||
f.queue.Push(file.Name, file.Size, file.ModTime())
|
||||
curFile, hasCurFile := f.fset.Get(protocol.LocalDeviceID, file.Name)
|
||||
if _, need := blockDiff(curFile.Blocks, file.Blocks); hasCurFile && len(need) == 0 {
|
||||
// We are supposed to copy the entire file, and then fetch nothing. We
|
||||
// are only updating metadata, so we don't actually *need* to make the
|
||||
// copy.
|
||||
f.shortcutFile(file, curFile, dbUpdateChan)
|
||||
} else {
|
||||
// Queue files for processing after directories and symlinks.
|
||||
f.queue.Push(file.Name, file.Size, file.ModTime())
|
||||
}
|
||||
|
||||
case runtime.GOOS == "windows" && file.IsSymlink():
|
||||
file.SetUnsupported(f.shortID)
|
||||
@@ -355,11 +364,23 @@ func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyC
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateInvalidate}
|
||||
changed++
|
||||
|
||||
default:
|
||||
// Directories, symlinks
|
||||
l.Debugln(f, "to be processed directly", file)
|
||||
processDirectly = append(processDirectly, file)
|
||||
case file.IsDirectory() && !file.IsSymlink():
|
||||
changed++
|
||||
l.Debugln(f, "Handling directory", file.Name)
|
||||
if f.checkParent(file.Name, scanChan) {
|
||||
f.handleDir(file, dbUpdateChan, scanChan)
|
||||
}
|
||||
|
||||
case file.IsSymlink():
|
||||
changed++
|
||||
l.Debugln(f, "Handling symlink", file.Name)
|
||||
if f.checkParent(file.Name, scanChan) {
|
||||
f.handleSymlink(file, dbUpdateChan, scanChan)
|
||||
}
|
||||
|
||||
default:
|
||||
l.Warnln(file)
|
||||
panic("unhandleable item type, can't happen")
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -371,39 +392,6 @@ func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyC
|
||||
default:
|
||||
}
|
||||
|
||||
// Sort the "process directly" pile by number of path components. This
|
||||
// ensures that we handle parents before children.
|
||||
|
||||
sort.Sort(byComponentCount(processDirectly))
|
||||
|
||||
// Process the list.
|
||||
|
||||
for _, fi := range processDirectly {
|
||||
select {
|
||||
case <-f.ctx.Done():
|
||||
return changed, fileDeletions, dirDeletions, f.ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
if !f.checkParent(fi.Name, scanChan) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case fi.IsDirectory() && !fi.IsSymlink():
|
||||
l.Debugln(f, "Handling directory", fi.Name)
|
||||
f.handleDir(fi, dbUpdateChan, scanChan)
|
||||
|
||||
case fi.IsSymlink():
|
||||
l.Debugln(f, "Handling symlink", fi.Name)
|
||||
f.handleSymlink(fi, dbUpdateChan, scanChan)
|
||||
|
||||
default:
|
||||
l.Warnln(fi)
|
||||
panic("unhandleable item type, can't happen")
|
||||
}
|
||||
}
|
||||
|
||||
// Now do the file queue. Reorder it according to configuration.
|
||||
|
||||
switch f.Order {
|
||||
@@ -478,6 +466,7 @@ nextFile:
|
||||
// Remove the pending deletion (as we performed it by renaming)
|
||||
delete(fileDeletions, candidate.Name)
|
||||
|
||||
changed++
|
||||
f.queue.Done(fileName)
|
||||
continue nextFile
|
||||
}
|
||||
@@ -488,11 +477,12 @@ nextFile:
|
||||
if _, ok := f.model.Connection(dev); ok {
|
||||
changed++
|
||||
// Handle the file normally, by coping and pulling, etc.
|
||||
f.handleFile(fi, copyChan, finisherChan, dbUpdateChan)
|
||||
f.handleFile(fi, copyChan, dbUpdateChan)
|
||||
continue nextFile
|
||||
}
|
||||
}
|
||||
f.newPullError(fileName, errNotAvailable)
|
||||
f.queue.Done(fileName)
|
||||
}
|
||||
|
||||
return changed, fileDeletions, dirDeletions, nil
|
||||
@@ -1017,18 +1007,10 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, db
|
||||
|
||||
// handleFile queues the copies and pulls as necessary for a single new or
|
||||
// changed file.
|
||||
func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState, dbUpdateChan chan<- dbUpdateJob) {
|
||||
func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, dbUpdateChan chan<- dbUpdateJob) {
|
||||
curFile, hasCurFile := f.fset.Get(protocol.LocalDeviceID, file.Name)
|
||||
|
||||
have, need := blockDiff(curFile.Blocks, file.Blocks)
|
||||
|
||||
if hasCurFile && len(need) == 0 {
|
||||
// We are supposed to copy the entire file, and then fetch nothing. We
|
||||
// are only updating metadata, so we don't actually *need* to make the
|
||||
// copy.
|
||||
f.shortcutFile(file, curFile, dbUpdateChan)
|
||||
return
|
||||
}
|
||||
have, _ := blockDiff(curFile.Blocks, file.Blocks)
|
||||
|
||||
tempName := fs.TempName(file.Name)
|
||||
|
||||
@@ -1568,6 +1550,10 @@ func (f *sendReceiveFolder) finisherRoutine(in <-chan *sharedPullerState, dbUpda
|
||||
blockStatsMut.Unlock()
|
||||
}
|
||||
|
||||
if f.model.progressEmitter != nil {
|
||||
f.model.progressEmitter.Deregister(state)
|
||||
}
|
||||
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
"item": state.file.Name,
|
||||
@@ -1575,10 +1561,6 @@ func (f *sendReceiveFolder) finisherRoutine(in <-chan *sharedPullerState, dbUpda
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
if f.model.progressEmitter != nil {
|
||||
f.model.progressEmitter.Deregister(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1622,7 +1604,7 @@ func (f *sendReceiveFolder) dbUpdaterRoutine(dbUpdateChan <-chan dbUpdateJob) {
|
||||
|
||||
// All updates to file/folder objects that originated remotely
|
||||
// (across the network) use this call to updateLocals
|
||||
f.model.updateLocalsFromPulling(f.folderID, files)
|
||||
f.updateLocalsFromPulling(files)
|
||||
|
||||
if found {
|
||||
f.ReceivedFile(lastFile.Name, lastFile.IsDeleted())
|
||||
@@ -1974,32 +1956,6 @@ func (l fileErrorList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
|
||||
// byComponentCount sorts by the number of path components in Name, that is
|
||||
// "x/y" sorts before "foo/bar/baz".
|
||||
type byComponentCount []protocol.FileInfo
|
||||
|
||||
func (l byComponentCount) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l byComponentCount) Less(a, b int) bool {
|
||||
return componentCount(l[a].Name) < componentCount(l[b].Name)
|
||||
}
|
||||
|
||||
func (l byComponentCount) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
|
||||
func componentCount(name string) int {
|
||||
count := 0
|
||||
for _, codepoint := range name {
|
||||
if codepoint == fs.PathSeparator {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func conflictName(name, lastModBy string) string {
|
||||
ext := filepath.Ext(name)
|
||||
return name[:len(name)-len(ext)] + time.Now().Format(".sync-conflict-20060102-150405-") + lastModBy + ext
|
||||
|
||||
@@ -94,11 +94,6 @@ func setupSendReceiveFolder(files ...protocol.FileInfo) (*model, *sendReceiveFol
|
||||
fcfg := testFolderConfigTmp()
|
||||
model.AddFolder(fcfg)
|
||||
|
||||
// Update index
|
||||
if files != nil {
|
||||
model.updateLocalsFromScanning("default", files)
|
||||
}
|
||||
|
||||
f := &sendReceiveFolder{
|
||||
folder: folder{
|
||||
stateTracker: newStateTracker("default"),
|
||||
@@ -115,6 +110,11 @@ func setupSendReceiveFolder(files ...protocol.FileInfo) (*model, *sendReceiveFol
|
||||
}
|
||||
f.fs = fs.NewMtimeFS(f.Filesystem(), db.NewNamespacedKV(model.db, "mtime"))
|
||||
|
||||
// Update index
|
||||
if files != nil {
|
||||
f.updateLocalsFromScanning(files)
|
||||
}
|
||||
|
||||
// Folders are never actually started, so no initial scan will be done
|
||||
close(f.initialScanFinished)
|
||||
|
||||
@@ -145,7 +145,7 @@ func TestHandleFile(t *testing.T) {
|
||||
copyChan := make(chan copyBlocksState, 1)
|
||||
dbUpdateChan := make(chan dbUpdateJob, 1)
|
||||
|
||||
f.handleFile(requiredFile, copyChan, nil, dbUpdateChan)
|
||||
f.handleFile(requiredFile, copyChan, dbUpdateChan)
|
||||
|
||||
// Receive the results
|
||||
toCopy := <-copyChan
|
||||
@@ -195,7 +195,7 @@ func TestHandleFileWithTemp(t *testing.T) {
|
||||
copyChan := make(chan copyBlocksState, 1)
|
||||
dbUpdateChan := make(chan dbUpdateJob, 1)
|
||||
|
||||
f.handleFile(requiredFile, copyChan, nil, dbUpdateChan)
|
||||
f.handleFile(requiredFile, copyChan, dbUpdateChan)
|
||||
|
||||
// Receive the results
|
||||
toCopy := <-copyChan
|
||||
@@ -253,7 +253,7 @@ func TestCopierFinder(t *testing.T) {
|
||||
// Run a single fetcher routine
|
||||
go f.copierRoutine(copyChan, pullChan, finisherChan)
|
||||
|
||||
f.handleFile(requiredFile, copyChan, finisherChan, dbUpdateChan)
|
||||
f.handleFile(requiredFile, copyChan, dbUpdateChan)
|
||||
|
||||
pulls := []pullBlockState{<-pullChan, <-pullChan, <-pullChan, <-pullChan}
|
||||
finish := <-finisherChan
|
||||
@@ -362,7 +362,7 @@ func TestWeakHash(t *testing.T) {
|
||||
ModifiedS: info.ModTime().Unix() + 1,
|
||||
}
|
||||
|
||||
model.updateLocalsFromScanning("default", []protocol.FileInfo{existingFile})
|
||||
fo.updateLocalsFromScanning([]protocol.FileInfo{existingFile})
|
||||
|
||||
copyChan := make(chan copyBlocksState)
|
||||
pullChan := make(chan pullBlockState, expectBlocks)
|
||||
@@ -374,7 +374,7 @@ func TestWeakHash(t *testing.T) {
|
||||
|
||||
// Test 1 - no weak hashing, file gets fully repulled (`expectBlocks` pulls).
|
||||
fo.WeakHashThresholdPct = 101
|
||||
fo.handleFile(desiredFile, copyChan, finisherChan, dbUpdateChan)
|
||||
fo.handleFile(desiredFile, copyChan, dbUpdateChan)
|
||||
|
||||
var pulls []pullBlockState
|
||||
for len(pulls) < expectBlocks {
|
||||
@@ -402,7 +402,7 @@ func TestWeakHash(t *testing.T) {
|
||||
|
||||
// Test 2 - using weak hash, expectPulls blocks pulled.
|
||||
fo.WeakHashThresholdPct = -1
|
||||
fo.handleFile(desiredFile, copyChan, finisherChan, dbUpdateChan)
|
||||
fo.handleFile(desiredFile, copyChan, dbUpdateChan)
|
||||
|
||||
pulls = pulls[:0]
|
||||
for len(pulls) < expectPulls {
|
||||
@@ -440,7 +440,7 @@ func TestCopierCleanup(t *testing.T) {
|
||||
file.Blocks = []protocol.BlockInfo{blocks[1]}
|
||||
file.Version = file.Version.Update(myID.Short())
|
||||
// Update index (removing old blocks)
|
||||
m.updateLocalsFromScanning("default", []protocol.FileInfo{file})
|
||||
f.updateLocalsFromScanning([]protocol.FileInfo{file})
|
||||
|
||||
if m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
|
||||
t.Error("Unexpected block found")
|
||||
@@ -453,7 +453,7 @@ func TestCopierCleanup(t *testing.T) {
|
||||
file.Blocks = []protocol.BlockInfo{blocks[0]}
|
||||
file.Version = file.Version.Update(myID.Short())
|
||||
// Update index (removing old blocks)
|
||||
m.updateLocalsFromScanning("default", []protocol.FileInfo{file})
|
||||
f.updateLocalsFromScanning([]protocol.FileInfo{file})
|
||||
|
||||
if !m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
|
||||
t.Error("Unexpected block found")
|
||||
@@ -493,7 +493,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
go f.copierRoutine(copyChan, pullChan, finisherBufferChan)
|
||||
go f.finisherRoutine(finisherChan, dbUpdateChan, make(chan string))
|
||||
|
||||
f.handleFile(file, copyChan, finisherChan, dbUpdateChan)
|
||||
f.handleFile(file, copyChan, dbUpdateChan)
|
||||
|
||||
// Receive a block at puller, to indicate that at least a single copier
|
||||
// loop has been performed.
|
||||
@@ -584,7 +584,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
go f.pullerRoutine(pullChan, finisherBufferChan)
|
||||
go f.finisherRoutine(finisherChan, dbUpdateChan, make(chan string))
|
||||
|
||||
f.handleFile(file, copyChan, finisherChan, dbUpdateChan)
|
||||
f.handleFile(file, copyChan, dbUpdateChan)
|
||||
|
||||
// Receive at finisher, we should error out as puller has nowhere to pull
|
||||
// from.
|
||||
@@ -832,7 +832,7 @@ func TestCopyOwner(t *testing.T) {
|
||||
defer close(copierChan)
|
||||
go f.copierRoutine(copierChan, nil, finisherChan)
|
||||
go f.finisherRoutine(finisherChan, dbUpdateChan, nil)
|
||||
f.handleFile(file, copierChan, nil, nil)
|
||||
f.handleFile(file, copierChan, nil)
|
||||
<-dbUpdateChan
|
||||
|
||||
info, err = f.fs.Lstat("foo/bar/baz")
|
||||
@@ -878,7 +878,7 @@ func TestSRConflictReplaceFileByDir(t *testing.T) {
|
||||
// create local file
|
||||
file := createFile(t, name, ffs)
|
||||
file.Version = protocol.Vector{}.Update(myID.Short())
|
||||
m.updateLocalsFromScanning(f.ID, []protocol.FileInfo{file})
|
||||
f.updateLocalsFromScanning([]protocol.FileInfo{file})
|
||||
|
||||
// Simulate remote creating a dir with the same name
|
||||
file.Type = protocol.FileInfoTypeDirectory
|
||||
@@ -913,7 +913,7 @@ func TestSRConflictReplaceFileByLink(t *testing.T) {
|
||||
// create local file
|
||||
file := createFile(t, name, ffs)
|
||||
file.Version = protocol.Vector{}.Update(myID.Short())
|
||||
m.updateLocalsFromScanning(f.ID, []protocol.FileInfo{file})
|
||||
f.updateLocalsFromScanning([]protocol.FileInfo{file})
|
||||
|
||||
// Simulate remote creating a symlink with the same name
|
||||
file.Type = protocol.FileInfoTypeSymlink
|
||||
|
||||
@@ -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()
|
||||
@@ -54,8 +54,8 @@ const (
|
||||
|
||||
type service interface {
|
||||
BringToFront(string)
|
||||
Override(*db.FileSet, func([]protocol.FileInfo))
|
||||
Revert(*db.FileSet, func([]protocol.FileInfo))
|
||||
Override()
|
||||
Revert()
|
||||
DelayScan(d time.Duration)
|
||||
SchedulePull() // something relevant changed, we should try a pull
|
||||
Jobs() ([]string, []string) // In progress, Queued
|
||||
@@ -65,6 +65,7 @@ type service interface {
|
||||
CheckHealth() error
|
||||
Errors() []FileError
|
||||
WatchError() error
|
||||
ForceRescan(file protocol.FileInfo) error
|
||||
GetStatistics() stats.FolderStatistics
|
||||
|
||||
getState() (folderState, time.Time, error)
|
||||
@@ -665,6 +666,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 {
|
||||
@@ -1600,17 +1612,19 @@ func (m *model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem,
|
||||
// The hashes provided part of the request match what we expect to find according
|
||||
// to what we have in the database, yet the content we've read off the filesystem doesn't
|
||||
// Something is fishy, invalidate the file and rescan it.
|
||||
cf.SetMustRescan(m.shortID)
|
||||
|
||||
// Update the index and tell others
|
||||
// The file will temporarily become invalid, which is ok as the content is messed up.
|
||||
m.updateLocalsFromScanning(folder, []protocol.FileInfo{cf})
|
||||
|
||||
if err := m.ScanFolderSubdirs(folder, []string{name}); err != nil {
|
||||
l.Debugf("%v recheckFile: %s: %q / %q rescan: %s", m, deviceID, folder, name, err)
|
||||
} else {
|
||||
l.Debugf("%v recheckFile: %s: %q / %q", m, deviceID, folder, name)
|
||||
m.fmut.Lock()
|
||||
runner, ok := m.folderRunners[folder]
|
||||
m.fmut.Unlock()
|
||||
if !ok {
|
||||
l.Debugf("%v recheckFile: %s: %q / %q: Folder stopped before rescan could be scheduled", m, deviceID, folder, name)
|
||||
return
|
||||
}
|
||||
if err := runner.ForceRescan(cf); err != nil {
|
||||
l.Debugf("%v recheckFile: %s: %q / %q rescan: %s", m, deviceID, folder, name, err)
|
||||
return
|
||||
}
|
||||
l.Debugf("%v recheckFile: %s: %q / %q", m, deviceID, folder, name)
|
||||
}
|
||||
|
||||
func (m *model) CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
@@ -1633,16 +1647,6 @@ func (m *model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo
|
||||
return fs.GetGlobal(file)
|
||||
}
|
||||
|
||||
type cFiler struct {
|
||||
m Model
|
||||
r string
|
||||
}
|
||||
|
||||
// Implements scanner.CurrentFiler
|
||||
func (cf cFiler) CurrentFile(file string) (protocol.FileInfo, bool) {
|
||||
return cf.m.CurrentFolderFile(cf.r, file)
|
||||
}
|
||||
|
||||
// Connection returns the current connection for device, and a boolean whether a connection was found.
|
||||
func (m *model) Connection(deviceID protocol.DeviceID) (connections.Connection, bool) {
|
||||
m.pmut.RLock()
|
||||
@@ -1940,15 +1944,6 @@ func sendIndexTo(prevSequence int64, conn protocol.Connection, folder string, fs
|
||||
}
|
||||
}
|
||||
|
||||
if shouldDebug() {
|
||||
if fi.SequenceNo() < prevSequence+1 {
|
||||
panic(fmt.Sprintln("sequence lower than requested, got:", fi.SequenceNo(), ", asked to start at:", prevSequence+1))
|
||||
}
|
||||
if f.Sequence > 0 && fi.SequenceNo() <= f.Sequence {
|
||||
panic(fmt.Sprintln("non-increasing sequence, current:", fi.SequenceNo(), "<= previous:", f.Sequence))
|
||||
}
|
||||
}
|
||||
|
||||
f = fi.(protocol.FileInfo)
|
||||
|
||||
// Mark the file as invalid if any of the local bad stuff flags are set.
|
||||
@@ -1986,92 +1981,6 @@ func sendIndexTo(prevSequence int64, conn protocol.Connection, folder string, fs
|
||||
return f.Sequence, err
|
||||
}
|
||||
|
||||
func (m *model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo) {
|
||||
m.updateLocals(folder, fs)
|
||||
|
||||
m.fmut.RLock()
|
||||
folderCfg := m.folderCfgs[folder]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
m.diskChangeDetected(folderCfg, fs, events.LocalChangeDetected)
|
||||
}
|
||||
|
||||
func (m *model) updateLocalsFromPulling(folder string, fs []protocol.FileInfo) {
|
||||
m.updateLocals(folder, fs)
|
||||
|
||||
m.fmut.RLock()
|
||||
folderCfg := m.folderCfgs[folder]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
m.diskChangeDetected(folderCfg, fs, events.RemoteChangeDetected)
|
||||
}
|
||||
|
||||
func (m *model) updateLocals(folder string, fs []protocol.FileInfo) {
|
||||
m.fmut.RLock()
|
||||
files := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
if files == nil {
|
||||
// The folder doesn't exist.
|
||||
return
|
||||
}
|
||||
files.Update(protocol.LocalDeviceID, fs)
|
||||
|
||||
filenames := make([]string, len(fs))
|
||||
for i, file := range fs {
|
||||
filenames[i] = file.Name
|
||||
}
|
||||
|
||||
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
|
||||
"folder": folder,
|
||||
"items": len(fs),
|
||||
"filenames": filenames,
|
||||
"version": files.Sequence(protocol.LocalDeviceID),
|
||||
})
|
||||
}
|
||||
|
||||
func (m *model) diskChangeDetected(folderCfg config.FolderConfiguration, files []protocol.FileInfo, typeOfEvent events.EventType) {
|
||||
for _, file := range files {
|
||||
if file.IsInvalid() {
|
||||
continue
|
||||
}
|
||||
|
||||
objType := "file"
|
||||
action := "modified"
|
||||
|
||||
switch {
|
||||
case file.IsDeleted():
|
||||
action = "deleted"
|
||||
|
||||
// If our local vector is version 1 AND it is the only version
|
||||
// vector so far seen for this file then it is a new file. Else if
|
||||
// it is > 1 it's not new, and if it is 1 but another shortId
|
||||
// version vector exists then it is new for us but created elsewhere
|
||||
// so the file is still not new but modified by us. Only if it is
|
||||
// truly new do we change this to 'added', else we leave it as
|
||||
// 'modified'.
|
||||
case len(file.Version.Counters) == 1 && file.Version.Counters[0].Value == 1:
|
||||
action = "added"
|
||||
}
|
||||
|
||||
if file.IsSymlink() {
|
||||
objType = "symlink"
|
||||
} else if file.IsDirectory() {
|
||||
objType = "dir"
|
||||
}
|
||||
|
||||
// Two different events can be fired here based on what EventType is passed into function
|
||||
events.Default.Log(typeOfEvent, map[string]string{
|
||||
"folder": folderCfg.ID,
|
||||
"folderID": folderCfg.ID, // incorrect, deprecated, kept for historical compliance
|
||||
"label": folderCfg.Label,
|
||||
"action": action,
|
||||
"type": objType,
|
||||
"path": filepath.FromSlash(file.Name),
|
||||
"modifiedBy": file.ModifiedBy.String(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (m *model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
m.pmut.RLock()
|
||||
nc, ok := m.conn[deviceID]
|
||||
@@ -2274,36 +2183,30 @@ func (m *model) Override(folder string) {
|
||||
// Grab the runner and the file set.
|
||||
|
||||
m.fmut.RLock()
|
||||
fs, fsOK := m.folderFiles[folder]
|
||||
runner, runnerOK := m.folderRunners[folder]
|
||||
runner, ok := m.folderRunners[folder]
|
||||
m.fmut.RUnlock()
|
||||
if !fsOK || !runnerOK {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Run the override, taking updates as if they came from scanning.
|
||||
|
||||
runner.Override(fs, func(files []protocol.FileInfo) {
|
||||
m.updateLocalsFromScanning(folder, files)
|
||||
})
|
||||
runner.Override()
|
||||
}
|
||||
|
||||
func (m *model) Revert(folder string) {
|
||||
// Grab the runner and the file set.
|
||||
|
||||
m.fmut.RLock()
|
||||
fs, fsOK := m.folderFiles[folder]
|
||||
runner, runnerOK := m.folderRunners[folder]
|
||||
runner, ok := m.folderRunners[folder]
|
||||
m.fmut.RUnlock()
|
||||
if !fsOK || !runnerOK {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Run the revert, taking updates as if they came from scanning.
|
||||
|
||||
runner.Revert(fs, func(files []protocol.FileInfo) {
|
||||
m.updateLocalsFromScanning(folder, files)
|
||||
})
|
||||
runner.Revert()
|
||||
}
|
||||
|
||||
// CurrentSequence returns the change version for the given folder.
|
||||
|
||||
@@ -110,6 +110,13 @@ func (q *jobQueue) Shuffle() {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *jobQueue) Reset() {
|
||||
q.mut.Lock()
|
||||
defer q.mut.Unlock()
|
||||
q.progress = nil
|
||||
q.queued = nil
|
||||
}
|
||||
|
||||
func (q *jobQueue) lenQueued() int {
|
||||
q.mut.Lock()
|
||||
defer q.mut.Unlock()
|
||||
|
||||
@@ -370,14 +370,12 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
|
||||
|
||||
fc.sendIndexUpdate()
|
||||
|
||||
timeout := time.NewTimer(5 * time.Second)
|
||||
select {
|
||||
case ev := <-sub.C():
|
||||
t.Fatalf("Errors while pulling: %v", ev)
|
||||
case <-timeout.C:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("timed out before index was received")
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
|
||||
fc.mut.Lock()
|
||||
@@ -421,12 +419,10 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
timeout = time.NewTimer(5 * time.Second)
|
||||
select {
|
||||
case <-timeout.C:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("timed out before index was received")
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +448,8 @@ func TestIssue4841(t *testing.T) {
|
||||
fc.mut.Unlock()
|
||||
|
||||
// Setup file from remote that was ignored locally
|
||||
m.updateLocals(defaultFolderConfig.ID, []protocol.FileInfo{{
|
||||
folder := m.folderRunners[defaultFolderConfig.ID].(*sendReceiveFolder)
|
||||
folder.updateLocals([]protocol.FileInfo{{
|
||||
Name: "foo",
|
||||
Type: protocol.FileInfoTypeFile,
|
||||
LocalFlags: protocol.FlagLocalIgnored,
|
||||
|
||||
@@ -41,12 +41,23 @@ const (
|
||||
var BlockSizes []int
|
||||
|
||||
// For each block size, the hash of a block of all zeroes
|
||||
var sha256OfEmptyBlock = make(map[int][sha256.Size]byte)
|
||||
var sha256OfEmptyBlock = map[int][sha256.Size]byte{
|
||||
128 << KiB: {0xfa, 0x43, 0x23, 0x9b, 0xce, 0xe7, 0xb9, 0x7c, 0xa6, 0x2f, 0x0, 0x7c, 0xc6, 0x84, 0x87, 0x56, 0xa, 0x39, 0xe1, 0x9f, 0x74, 0xf3, 0xdd, 0xe7, 0x48, 0x6d, 0xb3, 0xf9, 0x8d, 0xf8, 0xe4, 0x71},
|
||||
256 << KiB: {0x8a, 0x39, 0xd2, 0xab, 0xd3, 0x99, 0x9a, 0xb7, 0x3c, 0x34, 0xdb, 0x24, 0x76, 0x84, 0x9c, 0xdd, 0xf3, 0x3, 0xce, 0x38, 0x9b, 0x35, 0x82, 0x68, 0x50, 0xf9, 0xa7, 0x0, 0x58, 0x9b, 0x4a, 0x90},
|
||||
512 << KiB: {0x7, 0x85, 0x4d, 0x2f, 0xef, 0x29, 0x7a, 0x6, 0xba, 0x81, 0x68, 0x5e, 0x66, 0xc, 0x33, 0x2d, 0xe3, 0x6d, 0x5d, 0x18, 0xd5, 0x46, 0x92, 0x7d, 0x30, 0xda, 0xad, 0x6d, 0x7f, 0xda, 0x15, 0x41},
|
||||
1 << MiB: {0x30, 0xe1, 0x49, 0x55, 0xeb, 0xf1, 0x35, 0x22, 0x66, 0xdc, 0x2f, 0xf8, 0x6, 0x7e, 0x68, 0x10, 0x46, 0x7, 0xe7, 0x50, 0xab, 0xb9, 0xd3, 0xb3, 0x65, 0x82, 0xb8, 0xaf, 0x90, 0x9f, 0xcb, 0x58},
|
||||
2 << MiB: {0x56, 0x47, 0xf0, 0x5e, 0xc1, 0x89, 0x58, 0x94, 0x7d, 0x32, 0x87, 0x4e, 0xeb, 0x78, 0x8f, 0xa3, 0x96, 0xa0, 0x5d, 0xb, 0xab, 0x7c, 0x1b, 0x71, 0xf1, 0x12, 0xce, 0xb7, 0xe9, 0xb3, 0x1e, 0xee},
|
||||
4 << MiB: {0xbb, 0x9f, 0x8d, 0xf6, 0x14, 0x74, 0xd2, 0x5e, 0x71, 0xfa, 0x0, 0x72, 0x23, 0x18, 0xcd, 0x38, 0x73, 0x96, 0xca, 0x17, 0x36, 0x60, 0x5e, 0x12, 0x48, 0x82, 0x1c, 0xc0, 0xde, 0x3d, 0x3a, 0xf8},
|
||||
8 << MiB: {0x2d, 0xae, 0xb1, 0xf3, 0x60, 0x95, 0xb4, 0x4b, 0x31, 0x84, 0x10, 0xb3, 0xf4, 0xe8, 0xb5, 0xd9, 0x89, 0xdc, 0xc7, 0xbb, 0x2, 0x3d, 0x14, 0x26, 0xc4, 0x92, 0xda, 0xb0, 0xa3, 0x5, 0x3e, 0x74},
|
||||
16 << MiB: {0x8, 0xa, 0xcf, 0x35, 0xa5, 0x7, 0xac, 0x98, 0x49, 0xcf, 0xcb, 0xa4, 0x7d, 0xc2, 0xad, 0x83, 0xe0, 0x1b, 0x75, 0x66, 0x3a, 0x51, 0x62, 0x79, 0xc8, 0xb9, 0xd2, 0x43, 0xb7, 0x19, 0x64, 0x3e},
|
||||
}
|
||||
|
||||
func init() {
|
||||
for blockSize := MinBlockSize; blockSize <= MaxBlockSize; blockSize *= 2 {
|
||||
BlockSizes = append(BlockSizes, blockSize)
|
||||
sha256OfEmptyBlock[blockSize] = sha256.Sum256(make([]byte, blockSize))
|
||||
if _, ok := sha256OfEmptyBlock[blockSize]; !ok {
|
||||
panic("missing hard coded value for sha256 of empty block")
|
||||
}
|
||||
}
|
||||
BufferPool = newBufferPool()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -610,3 +611,13 @@ func TestIsEquivalent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSha256OfEmptyBlock(t *testing.T) {
|
||||
// every block size should have a correct entry in sha256OfEmptyBlock
|
||||
for blockSize := MinBlockSize; blockSize <= MaxBlockSize; blockSize *= 2 {
|
||||
expected := sha256.Sum256(make([]byte, blockSize))
|
||||
if sha256OfEmptyBlock[blockSize] != expected {
|
||||
t.Error("missing or wrong hash for block of size", blockSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "STDISCOSRV" "1" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.TH "STDISCOSRV" "1" "Mar 22, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
stdiscosrv \- Syncthing Discovery Server
|
||||
.
|
||||
@@ -52,12 +52,12 @@ Syncthing project also maintains a global cluster for public use.
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-cert=<file>
|
||||
Certificate file (default "./cert.pem").
|
||||
Certificate file (default “./cert.pem”).
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-db\-dir=<string>
|
||||
Database directory, where data is stored (default "./discovery.db").
|
||||
Database directory, where data is stored (default “./discovery.db”).
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@@ -72,12 +72,12 @@ Listen on HTTP (behind an HTTPS proxy).
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-key=<file>
|
||||
Key file (default "./key.pem").
|
||||
Key file (default “./key.pem”).
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-listen=<address>
|
||||
Listen address (default ":8443").
|
||||
Listen address (default “:8443”).
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@@ -92,14 +92,14 @@ Replication peers, \fI\%id@address\fP <\fBid@address\fP>, comma separated
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-replication\-listen=<address>
|
||||
Listen address for incoming replication connections (default ":19200").
|
||||
Listen address for incoming replication connections (default “:19200”).
|
||||
.UNINDENT
|
||||
.SH POINTING SYNCTHING AT YOUR DISCOVERY SERVER
|
||||
.sp
|
||||
By default, Syncthing uses a number of global discovery servers, signified by
|
||||
the entry \fBdefault\fP in the list of discovery servers. To make Syncthing use
|
||||
your own instance of stdiscosrv, open up Syncthing\(aqs web GUI. Go to settings,
|
||||
Global Discovery Server and add stdiscosrv\(aqs host address to the comma\-separated
|
||||
your own instance of stdiscosrv, open up Syncthing’s web GUI. Go to settings,
|
||||
Global Discovery Server and add stdiscosrv’s host address to the comma\-separated
|
||||
list, e.g. \fBhttps://disco.example.com:8443/\fP\&. Note that stdiscosrv uses port
|
||||
8443 by default. For stdiscosrv to be available over the internet with a dynamic
|
||||
IP address, you will need a dynamic DNS service.
|
||||
@@ -114,7 +114,7 @@ entry from the list.
|
||||
.SS Description
|
||||
.sp
|
||||
This guide assumes that you have already set up Syncthing. If you
|
||||
haven\(aqt yet, head over to getting\-started first.
|
||||
haven’t yet, head over to getting\-started first.
|
||||
.SS Installing
|
||||
.sp
|
||||
Go to \fI\%releases\fP <\fBhttps://github.com/syncthing/discosrv/releases\fP> and
|
||||
@@ -146,7 +146,7 @@ discovery server. This is like any other HTTPS website; clients will
|
||||
authenticate the server based on its certificate and domain name.
|
||||
.IP \(bu 2
|
||||
Use any certificate pair and let clients authenticate the server based on
|
||||
its "device ID" (similar to Syncthing\-to\-Syncthing authentication). This
|
||||
its “device ID” (similar to Syncthing\-to\-Syncthing authentication). This
|
||||
option can be used with the certificate automatically generated by the
|
||||
discovery server.
|
||||
.IP \(bu 2
|
||||
@@ -155,7 +155,7 @@ reverse proxy. See below for configuration.
|
||||
.UNINDENT
|
||||
.sp
|
||||
For the first two options, the discovery server must be given the paths to
|
||||
the certificate and key at startup. This isn\(aqt necessary with the \fBhttp\fP flag:
|
||||
the certificate and key at startup. This isn’t necessary with the \fBhttp\fP flag:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
@@ -218,9 +218,9 @@ sender and listener.
|
||||
As an example, lets assume two discovery servers:
|
||||
.INDENT 0.0
|
||||
.IP \(bu 2
|
||||
Server one is on 192.0.2.20 and has certificate ID I6K...H76
|
||||
Server one is on 192.0.2.20 and has certificate ID I6K…H76
|
||||
.IP \(bu 2
|
||||
Server two is on 192.0.2.55 and has certificate ID MRI...7OK
|
||||
Server two is on 192.0.2.55 and has certificate ID MRI…7OK
|
||||
.UNINDENT
|
||||
.sp
|
||||
In order for both to replicate to the other and thus form a redundant pair,
|
||||
@@ -257,7 +257,7 @@ port must be specified in peer addresses.
|
||||
.sp
|
||||
It is possible to only allow incoming connections from a peer without
|
||||
establishing an outgoing replication connection. To do so, give only the
|
||||
device ID without "@ip:port" address:
|
||||
device ID without “@ip:port” address:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
@@ -270,7 +270,7 @@ $ stdiscosrv \-replicate=I6K...H76 <other options>
|
||||
.UNINDENT
|
||||
.sp
|
||||
Discosrv will listen on the replication port only when \fB\-replicate\fP is
|
||||
given. The default replication listen address is ":19200".
|
||||
given. The default replication listen address is “:19200”.
|
||||
.sp
|
||||
To achieve load balancing over two mutually replicating discovery server
|
||||
instances, add multiple A / AAAA DNS records for a given name and point
|
||||
@@ -293,10 +293,10 @@ Run the discovery server using the \-http flag \fBstdiscosrv \-http\fP\&.
|
||||
.IP \(bu 2
|
||||
SSL certificate/key configured for the reverse proxy
|
||||
.IP \(bu 2
|
||||
The "X\-Forwarded\-For" http header must be passed through with the client\(aqs
|
||||
The “X\-Forwarded\-For” http header must be passed through with the client’s
|
||||
real IP address
|
||||
.IP \(bu 2
|
||||
The "X\-SSL\-Cert" must be passed through with the PEM\-encoded client SSL
|
||||
The “X\-SSL\-Cert” must be passed through with the PEM\-encoded client SSL
|
||||
certificate
|
||||
.IP \(bu 2
|
||||
The proxy must request the client SSL certificate but not require it to be
|
||||
@@ -371,7 +371,7 @@ server {
|
||||
.UNINDENT
|
||||
.sp
|
||||
An example of automating the SSL certificates and reverse\-proxying the Discovery
|
||||
Server and Syncthing using Nginx, \fI\%Let\(aqs Encrypt\fP <\fBhttps://letsencrypt.org/\fP> and Docker can be found \fI\%here\fP <\fBhttps://forum.syncthing.net/t/docker-syncthing-and-syncthing-discovery-behind-nginx-reverse-proxy-with-lets-encrypt/6880\fP>\&.
|
||||
Server and Syncthing using Nginx, \fI\%Let’s Encrypt\fP <\fBhttps://letsencrypt.org/\fP> and Docker can be found \fI\%here\fP <\fBhttps://forum.syncthing.net/t/docker-syncthing-and-syncthing-discovery-behind-nginx-reverse-proxy-with-lets-encrypt/6880\fP>\&.
|
||||
.SH SEE ALSO
|
||||
.sp
|
||||
\fBsyncthing\-networking(7)\fP, \fBsyncthing\-faq(7)\fP
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "STRELAYSRV" "1" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.TH "STRELAYSRV" "1" "Mar 22, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
strelaysrv \- Syncthing Relay Server
|
||||
.
|
||||
@@ -72,12 +72,12 @@ Global rate limit, in bytes/s.
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-keys=<dir>
|
||||
Directory where cert.pem and key.pem is stored (default ".").
|
||||
Directory where cert.pem and key.pem is stored (default “.”).
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-listen=<listen addr>
|
||||
Protocol listen address (default ":22067").
|
||||
Protocol listen address (default “:22067”).
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@@ -127,13 +127,13 @@ How often pings are sent (default 1m0s).
|
||||
.TP
|
||||
.B \-pools=<pool addresses>
|
||||
Comma separated list of relay pool addresses to join (default
|
||||
"\fI\%http://relays.syncthing.net/endpoint\fP"). Blank to disable announcement to
|
||||
“\fI\%http://relays.syncthing.net/endpoint\fP”). Blank to disable announcement to
|
||||
a pool, thereby remaining a private relay.
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-protocol=<string>
|
||||
Protocol used for listening. \(aqtcp\(aq for IPv4 and IPv6, \(aqtcp4\(aq for IPv4, \(aqtcp6\(aq for IPv6 (default "tcp").
|
||||
Protocol used for listening. ‘tcp’ for IPv4 and IPv6, ‘tcp4’ for IPv4, ‘tcp6’ for IPv6 (default “tcp”).
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@@ -143,7 +143,7 @@ An optional description about who provides the relay.
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B \-status\-srv=<listen addr>
|
||||
Listen address for status service (blank to disable) (default ":22070").
|
||||
Listen address for status service (blank to disable) (default “:22070”).
|
||||
Status service is used by the relay pool server UI for displaying stats (data transfered, number of clients, etc.)
|
||||
.UNINDENT
|
||||
.SH SETTING UP
|
||||
@@ -191,7 +191,7 @@ relay://private\-relay\-1.example.com:443/?id=ITZRNXE\-YNROGBZ\-HXTH5P7\-VK5NYE5
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
The relay\(aqs device ID is output on start\-up.
|
||||
The relay’s device ID is output on start\-up.
|
||||
.SS Running on port 443 as an unprivileged user
|
||||
.sp
|
||||
It is recommended that you run the relay on port 443 (or another port which is
|
||||
@@ -213,7 +213,7 @@ iptables \-t nat \-A PREROUTING \-i eth0 \-p tcp \-\-dport 443 \-j REDIRECT \-\-
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
Or, if you\(aqre using \fBufw\fP, add the following to \fB/etc/ufw/before.rules\fP:
|
||||
Or, if you’re using \fBufw\fP, add the following to \fB/etc/ufw/before.rules\fP:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-BEP" "7" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "Mar 22, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-bep \- Block Exchange Protocol v1
|
||||
.
|
||||
@@ -46,8 +46,8 @@ File data is described and transferred in units of \fIblocks\fP, each being from
|
||||
block size may vary between files but is constant in any given file, except
|
||||
for the last block which may be smaller.
|
||||
.sp
|
||||
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
||||
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
|
||||
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”,
|
||||
“SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this
|
||||
document are to be interpreted as described in RFC 2119.
|
||||
.SH TRANSPORT AND AUTHENTICATION
|
||||
.sp
|
||||
@@ -72,8 +72,8 @@ v ... v
|
||||
.UNINDENT
|
||||
.sp
|
||||
The encryption and authentication layer SHALL use TLS 1.2 or a higher
|
||||
revision. A strong cipher suite SHALL be used, with "strong cipher
|
||||
suite" being defined as being without known weaknesses and providing
|
||||
revision. A strong cipher suite SHALL be used, with “strong cipher
|
||||
suite” being defined as being without known weaknesses and providing
|
||||
Perfect Forward Secrecy (PFS). Examples of strong cipher suites are
|
||||
given at the end of this document. This is not to be taken as an
|
||||
exhaustive list of allowed cipher suites but represents best practices
|
||||
@@ -85,7 +85,7 @@ connection. Possibilities include certificates signed by a common
|
||||
trusted CA, preshared certificates, preshared certificate fingerprints
|
||||
or certificate pinning combined with some out of band first
|
||||
verification. The reference implementation uses preshared certificate
|
||||
fingerprints (SHA\-256) referred to as "Device IDs".
|
||||
fingerprints (SHA\-256) referred to as “Device IDs”.
|
||||
.sp
|
||||
There is no required order or synchronization among BEP messages except
|
||||
as noted per message type \- any message type may be sent at any time and
|
||||
@@ -94,9 +94,9 @@ another.
|
||||
.sp
|
||||
The underlying transport protocol MUST guarantee reliable packet delivery.
|
||||
.sp
|
||||
In this document, in diagrams and text, "bit 0" refers to the \fImost
|
||||
significant\fP bit of a word; "bit 15" is thus the least significant bit of a
|
||||
16 bit word (int16) and "bit 31" is the least significant bit of a 32 bit
|
||||
In this document, in diagrams and text, “bit 0” refers to the \fImost
|
||||
significant\fP bit of a word; “bit 15” is thus the least significant bit of a
|
||||
16 bit word (int16) and “bit 31” is the least significant bit of a 32 bit
|
||||
word (int32). Non protocol buffer integers are always represented in network
|
||||
byte order (i.e., big endian) and are signed unless stated otherwise, but
|
||||
when describing message lengths negative values do not make sense and the
|
||||
@@ -109,7 +109,7 @@ message is \fIvalid\fP with all fields empty \- for example, an index entry for
|
||||
file that does not have a name is not useful and MAY be rejected by the
|
||||
implementation. However the folder label is for human consumption only so an
|
||||
empty label should be accepted \- the implementation will have to choose some
|
||||
way to represent the folder, perhaps by using the ID in it\(aqs place or
|
||||
way to represent the folder, perhaps by using the ID in it’s place or
|
||||
automatically generating a label.
|
||||
.SH PRE-AUTHENTICATION MESSAGES
|
||||
.sp
|
||||
@@ -171,7 +171,7 @@ name or host name, for the remote device.
|
||||
The \fBclient_name\fP and \fBclient_version\fP identifies the implementation. The
|
||||
values SHOULD be simple strings identifying the implementation name, as a
|
||||
user would expect to see it, and the version string in the same manner. An
|
||||
example client name is "syncthing" and an example client version is "v0.7.2".
|
||||
example client name is “syncthing” and an example client version is “v0.7.2”.
|
||||
The client version field SHOULD follow the patterns laid out in the \fI\%Semantic
|
||||
Versioning\fP <\fBhttp://semver.org/\fP> standard.
|
||||
.sp
|
||||
@@ -467,7 +467,7 @@ The \fBfiles\fP field is a list of files making up the index information.
|
||||
The \fBname\fP is the file name path relative to the folder root. Like all
|
||||
strings in BEP, the Name is always in UTF\-8 NFC regardless of operating
|
||||
system or file system specific conventions. The name field uses the slash
|
||||
character ("/") as path separator, regardless of the implementation\(aqs
|
||||
character (“/”) as path separator, regardless of the implementation’s
|
||||
operating system conventions. The combination of folder and name uniquely
|
||||
identifies each file in a cluster.
|
||||
.sp
|
||||
@@ -532,7 +532,7 @@ symlink type. It is empty for all other entry types.
|
||||
.SS Request
|
||||
.sp
|
||||
The Request message expresses the desire to receive a data block
|
||||
corresponding to a part of a certain file in the peer\(aqs folder.
|
||||
corresponding to a part of a certain file in the peer’s folder.
|
||||
.SS Protocol Buffer Schema
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
@@ -569,7 +569,7 @@ requested hash. The other device MAY reuse a block from a different file and
|
||||
offset having the same size and hash, if one exists.
|
||||
.sp
|
||||
The \fBfrom temporary\fP field is set to indicate that the read should be
|
||||
performed from the temporary file (converting name to it\(aqs temporary form)
|
||||
performed from the temporary file (converting name to it’s temporary form)
|
||||
and falling back to the non temporary file if any error occurs. Knowledge of
|
||||
contents of temporary files comes from DownloadProgress messages.
|
||||
.SS Response
|
||||
@@ -812,7 +812,7 @@ index data.
|
||||
For situations with large indexes or frequent reconnects this can be quite
|
||||
inefficient. A mechanism can then be used to retain index data between
|
||||
connections and only transmit any changes since that data on connection
|
||||
start. This is called "delta indexes". To enable this mechanism the
|
||||
start. This is called “delta indexes”. To enable this mechanism the
|
||||
\fBsequence\fP and \fBindex ID\fP fields are used.
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@@ -861,7 +861,7 @@ Update messages rather than sending a very large Index message.
|
||||
The Syncthing implementation imposes a hard limit of 500,000,000 bytes on
|
||||
all messages. Attempting to send or receive a larger message will result in
|
||||
a connection close. This size was chosen to accommodate Index messages
|
||||
containing a large block list. It\(aqs intended that the limit may be further
|
||||
containing a large block list. It’s intended that the limit may be further
|
||||
reduced in a future protocol update supporting variable block sizes (and
|
||||
thus shorter block lists for large files).
|
||||
.SH SELECTION OF BLOCK SIZE
|
||||
@@ -1049,7 +1049,7 @@ T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
\&...
|
||||
…
|
||||
T} T{
|
||||
T} T{
|
||||
T}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "Mar 22, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.
|
||||
@@ -58,7 +58,7 @@ directory the following files are located:
|
||||
The configuration file, in XML format.
|
||||
.TP
|
||||
.B \fBcert.pem\fP, \fBkey.pem\fP
|
||||
The device\(aqs RSA public and private key. These form the basis for the
|
||||
The device’s RSA public and private key. These form the basis for the
|
||||
device ID. The key must be kept private.
|
||||
.TP
|
||||
.B \fBhttps\-cert.pem\fP, \fBhttps\-key.pem\fP
|
||||
@@ -257,17 +257,17 @@ Controls how the folder is handled by Syncthing. Possible values are:
|
||||
.TP
|
||||
.B sendreceive
|
||||
The folder is in default mode. Sending local and accepting remote changes.
|
||||
Note that this type was previously called "readwrite" which is deprecated
|
||||
Note that this type was previously called “readwrite” which is deprecated
|
||||
but still accepted in incoming configs.
|
||||
.TP
|
||||
.B sendonly
|
||||
The folder is in "send only" mode \-\- it will not be modified by
|
||||
The folder is in “send only” mode – it will not be modified by
|
||||
Syncthing on this device.
|
||||
Note that this type was previously called "readonly" which is deprecated
|
||||
Note that this type was previously called “readonly” which is deprecated
|
||||
but still accepted in incoming configs.
|
||||
.TP
|
||||
.B receiveonly
|
||||
The folder is in "receive only" mode \-\- it will not propagate
|
||||
The folder is in “receive only” mode – it will not propagate
|
||||
changes to other devices.
|
||||
.UNINDENT
|
||||
.TP
|
||||
@@ -325,7 +325,7 @@ versioning
|
||||
.B copiers, pullers, hashers
|
||||
The number of copier, puller and hasher routines to use, or zero for the
|
||||
system determined optimum. These are low level performance options for
|
||||
advanced users only; do not change unless requested to or you\(aqve actually
|
||||
advanced users only; do not change unless requested to or you’ve actually
|
||||
read and understood the code yourself. :)
|
||||
.TP
|
||||
.B order
|
||||
@@ -362,8 +362,8 @@ The interval with which scan progress information is sent to the GUI. Zero
|
||||
means the default value (two seconds).
|
||||
.TP
|
||||
.B pullerPauseS
|
||||
Tweak for rate limiting the puller when it retries pulling files. Don\(aqt
|
||||
change these unless you know what you\(aqre doing.
|
||||
Tweak for rate limiting the puller when it retries pulling files. Don’t
|
||||
change these unless you know what you’re doing.
|
||||
.TP
|
||||
.B maxConflicts
|
||||
The maximum number of conflict copies to keep around for any given file.
|
||||
@@ -389,7 +389,7 @@ to \-1 to always use weak hash. Default value is 25.
|
||||
.TP
|
||||
.B markerName
|
||||
Name of a directory or file in the folder root to be used as
|
||||
marker\-faq\&. Default is ".stfolder".
|
||||
marker\-faq\&. Default is “.stfolder”.
|
||||
.TP
|
||||
.B fsync
|
||||
Deprecated since version v0.14.37.
|
||||
@@ -402,8 +402,8 @@ committing the changes to the internal database.
|
||||
Deprecated since version v0.14.41.
|
||||
|
||||
.sp
|
||||
Tweak for rate limiting the puller. Don\(aqt change these unless you know
|
||||
what you\(aqre doing.
|
||||
Tweak for rate limiting the puller. Don’t change these unless you know
|
||||
what you’re doing.
|
||||
.UNINDENT
|
||||
.SH DEVICE ELEMENT
|
||||
.INDENT 0.0
|
||||
@@ -478,7 +478,7 @@ to even if the original introducer is no longer listing the remote device as kno
|
||||
Defines which device has introduced us to this device. Used only for following de\-introductions.
|
||||
.TP
|
||||
.B certName
|
||||
The device certificate common name, if it is not the default "syncthing".
|
||||
The device certificate common name, if it is not the default “syncthing”.
|
||||
.UNINDENT
|
||||
.sp
|
||||
From following child elements at least one \fBaddress\fP child must exist.
|
||||
@@ -967,9 +967,9 @@ that the files you are backing up are in a folder\-sendonly to prevent other
|
||||
devices from overwriting the per device configuration. The folder on the remote
|
||||
device(s) should not be used as configuration for the remote devices.
|
||||
.sp
|
||||
If you\(aqd like to sync your home folder in non\-send only mode, you may add the
|
||||
If you’d like to sync your home folder in non\-send only mode, you may add the
|
||||
folder that stores the configuration files to the ignore list\&.
|
||||
If you\(aqd also like to backup your configuration files, add another folder in
|
||||
If you’d also like to backup your configuration files, add another folder in
|
||||
send only mode for just the configuration folder.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "Mar 22, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.
|
||||
@@ -32,8 +32,8 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
..
|
||||
.sp
|
||||
Every device is identified by a device ID. The device ID is used for address
|
||||
resolution, authentication and authorization. The term "device ID" could
|
||||
interchangeably have been "key ID" since the device ID is a direct property of
|
||||
resolution, authentication and authorization. The term “device ID” could
|
||||
interchangeably have been “key ID” since the device ID is a direct property of
|
||||
the public key in use.
|
||||
.SH KEYS
|
||||
.sp
|
||||
@@ -43,7 +43,7 @@ startup, Syncthing will create a public/private keypair.
|
||||
Currently this is a 384 bit ECDSA key (3072 bit RSA prior to v0.12.5,
|
||||
which is what is used as an example in this article). The keys are saved in
|
||||
the form of the private key (\fBkey.pem\fP) and a self signed certificate
|
||||
(\fBcert.pem\fP). The self signing part doesn\(aqt actually add any security or
|
||||
(\fBcert.pem\fP). The self signing part doesn’t actually add any security or
|
||||
functionality as far as Syncthing is concerned but it enables the use of the
|
||||
keys in a standard TLS exchange.
|
||||
.sp
|
||||
@@ -94,7 +94,7 @@ Certificate:
|
||||
.sp
|
||||
We can see here that the certificate is little more than a container for the
|
||||
public key; the serial number is zero and the Issuer and Subject are both
|
||||
"syncthing" where a qualified name might otherwise be expected.
|
||||
“syncthing” where a qualified name might otherwise be expected.
|
||||
.sp
|
||||
An advanced user could replace the \fBkey.pem\fP and \fBcert.pem\fP files with a
|
||||
keypair generated directly by the \fBopenssl\fP utility or other mechanism.
|
||||
@@ -138,7 +138,7 @@ MFZWI3D\-BONSGYC\-YLTMRWG\-C43ENR5\-QXGZDMM\-FZWI3DP\-BONSGYY\-LTMRWAD
|
||||
.UNINDENT
|
||||
.SS Connection Establishment
|
||||
.sp
|
||||
Now we know what device IDs are, here\(aqs how they are used in Syncthing. When
|
||||
Now we know what device IDs are, here’s how they are used in Syncthing. When
|
||||
you add a device ID to the configuration, Syncthing will attempt to
|
||||
connect to that device. The first thing we need to do is figure out the IP and
|
||||
port to connect to. There are three possibilities here:
|
||||
@@ -150,13 +150,13 @@ dynamic DNS setup this might be a good option.
|
||||
.IP \(bu 2
|
||||
Using local discovery, if enabled. Every Syncthing instance on a LAN
|
||||
periodically broadcasts information about itself (device ID, address,
|
||||
port number). If we\(aqve seen one of these broadcasts for a given
|
||||
device ID that\(aqs where we try to connect.
|
||||
port number). If we’ve seen one of these broadcasts for a given
|
||||
device ID that’s where we try to connect.
|
||||
.IP \(bu 2
|
||||
Using global discovery, if enabled. Every Syncthing instance
|
||||
announces itself to the global discovery service (device ID and
|
||||
external port number \- the internal address is not announced to the
|
||||
global server). If we don\(aqt have a static address and haven\(aqt seen
|
||||
global server). If we don’t have a static address and haven’t seen
|
||||
any local announcements the global discovery server will be queried
|
||||
for an address.
|
||||
.UNINDENT
|
||||
@@ -188,11 +188,11 @@ The SHA\-256 hash is cryptographically collision resistant. This means
|
||||
that there is no way that we know of to create two different messages
|
||||
with the same hash.
|
||||
.sp
|
||||
You can argue that of course there are collisions \- there\(aqs an infinite
|
||||
You can argue that of course there are collisions \- there’s an infinite
|
||||
amount of inputs and a finite amount of outputs \- so by definition there
|
||||
are infinitely many messages that result in the same hash.
|
||||
.sp
|
||||
I\(aqm going to quote \fI\%stack
|
||||
I’m going to quote \fI\%stack
|
||||
overflow\fP <\fBhttps://stackoverflow.com/questions/4014090/is-it-safe-to-ignore-the-possibility-of-sha-collisions-in-practice\fP>
|
||||
here:
|
||||
.INDENT 0.0
|
||||
@@ -203,28 +203,28 @@ civilization\-as\-we\- know\-it, and killing off a few billion people ?
|
||||
It can be argued that any unlucky event with a probability lower
|
||||
than that is not actually very important.
|
||||
.sp
|
||||
If we have a "perfect" hash function with output size n, and we have
|
||||
If we have a “perfect” hash function with output size n, and we have
|
||||
p messages to hash (individual message length is not important),
|
||||
then probability of collision is about p2/2n+1 (this is an
|
||||
approximation which is valid for "small" p, i.e. substantially
|
||||
approximation which is valid for “small” p, i.e. substantially
|
||||
smaller than 2n/2). For instance, with SHA\-256 (n=256) and one
|
||||
billion messages (p=10^9) then the probability is about 4.3*10^\-60.
|
||||
.sp
|
||||
A mass\-murderer space rock happens about once every 30 million years
|
||||
on average. This leads to a probability of such an event occurring
|
||||
in the next second to about 10^\-15. That\(aqs 45 orders of magnitude
|
||||
in the next second to about 10^\-15. That’s 45 orders of magnitude
|
||||
more probable than the SHA\-256 collision. Briefly stated, if you
|
||||
find SHA\-256 collisions scary then your priorities are wrong.
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
It\(aqs also worth noting that the property of SHA\-256 that we are using is not
|
||||
It’s also worth noting that the property of SHA\-256 that we are using is not
|
||||
simply collision resistance but resistance to a preimage attack, i.e. even if
|
||||
you can find two messages that result in a hash collision that doesn\(aqt help you
|
||||
you can find two messages that result in a hash collision that doesn’t help you
|
||||
attack Syncthing (or TLS in general). You need to create a message that hashes
|
||||
to exactly the hash that my certificate already has or you won\(aqt get in.
|
||||
to exactly the hash that my certificate already has or you won’t get in.
|
||||
.sp
|
||||
Note also that it\(aqs not good enough to find a random blob of bits that happen to
|
||||
Note also that it’s not good enough to find a random blob of bits that happen to
|
||||
have the same hash as my certificate. You need to create a valid DER\-encoded,
|
||||
signed certificate that has the same hash as mine. The difficulty of this is
|
||||
staggeringly far beyond the already staggering difficulty of finding a SHA\-256
|
||||
@@ -235,22 +235,22 @@ As far as I know, these are the issues or potential issues with the
|
||||
above mechanism.
|
||||
.SS Discovery Spoofing
|
||||
.sp
|
||||
Currently, the local discovery mechanism isn\(aqt protected by crypto. This
|
||||
Currently, the local discovery mechanism isn’t protected by crypto. This
|
||||
means that any device can in theory announce itself for any device ID and
|
||||
potentially receive connections for that device from the local network.
|
||||
.SS Long Device IDs are Painful
|
||||
.sp
|
||||
It\(aqs a mouthful to read over the phone, annoying to type into an SMS or even
|
||||
It’s a mouthful to read over the phone, annoying to type into an SMS or even
|
||||
into a computer. And it needs to be done twice, once for each side.
|
||||
.sp
|
||||
This isn\(aqt a vulnerability as such, but a user experience problem. There are
|
||||
This isn’t a vulnerability as such, but a user experience problem. There are
|
||||
various possible solutions:
|
||||
.INDENT 0.0
|
||||
.IP \(bu 2
|
||||
Use shorter device IDs with verification based on the full ID ("You
|
||||
Use shorter device IDs with verification based on the full ID (“You
|
||||
entered MFZWI3; I found and connected to a device with the ID
|
||||
MFZWI3\-DBONSG\-YYLTMR\-WGC43E\-NRQXGZ\-DMMFZW\-I3DBON\-SGYYLT\-MRWA, please
|
||||
confirm that this is correct").
|
||||
confirm that this is correct”).
|
||||
.IP \(bu 2
|
||||
Use shorter device IDs with an out of band authentication, a la
|
||||
Bluetooth pairing. You enter a one time PIN into Syncthing and give
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-EVENT-API" "7" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "Mar 22, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.
|
||||
@@ -162,8 +162,8 @@ Generated each time a connection to a device has been terminated.
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
The error key contains the cause for disconnection, which might not
|
||||
necessarily be an error as such. Specifically, "EOF" and "unexpected
|
||||
EOF" both signify TCP connection termination, either due to the other
|
||||
necessarily be an error as such. Specifically, “EOF” and “unexpected
|
||||
EOF” both signify TCP connection termination, either due to the other
|
||||
device restarting or going offline or due to a network change.
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-FAQ" "7" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "Mar 22, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.
|
||||
@@ -38,10 +38,10 @@ machine will automatically be replicated to your other devices. We believe your
|
||||
data is your data alone and you deserve to choose where it is stored. Therefore
|
||||
Syncthing does not upload your data to the cloud but exchanges your data across
|
||||
your machines as soon as they are online at the same time.
|
||||
.SH IS IT "SYNCTHING", "SYNCTHING" OR "SYNCTHING"?
|
||||
.SH IS IT “SYNCTHING”, “SYNCTHING” OR “SYNCTHING”?
|
||||
.sp
|
||||
It\(aqs \fBSyncthing\fP, although the command and source repository is spelled
|
||||
\fBsyncthing\fP so it may be referred to in that way as well. It\(aqs definitely not
|
||||
It’s \fBSyncthing\fP, although the command and source repository is spelled
|
||||
\fBsyncthing\fP so it may be referred to in that way as well. It’s definitely not
|
||||
SyncThing, even though the abbreviation \fBst\fP is used in some
|
||||
circumstances and file names.
|
||||
.SH HOW DOES SYNCTHING DIFFER FROM BITTORRENT/RESILIO SYNC?
|
||||
@@ -116,9 +116,9 @@ in the configuration file (24 hours by default).
|
||||
.sp
|
||||
When troubleshooting a slow sync, there are a number of things to check.
|
||||
.sp
|
||||
First of all, verify that you are not connected via a relay. In the "Remote
|
||||
Devices" list on the right side of the GUI, double check that you see
|
||||
"Address: <some address>" and \fInot\fP "Relay: <some address>".
|
||||
First of all, verify that you are not connected via a relay. In the “Remote
|
||||
Devices” list on the right side of the GUI, double check that you see
|
||||
“Address: <some address>” and \fInot\fP “Relay: <some address>”.
|
||||
[image]
|
||||
.sp
|
||||
If you are connected via a relay, this is because a direct connection could
|
||||
@@ -151,8 +151,8 @@ There is a certain amount of housekeeping that must be done to track the
|
||||
current and available versions of each file in the index database.
|
||||
.IP 4. 3
|
||||
By default Syncthing uses periodic scanning every hour when watching for
|
||||
changes or every minute if that\(aqs disabled to detect
|
||||
file changes. This means checking every file\(aqs modification time and
|
||||
changes or every minute if that’s disabled to detect
|
||||
file changes. This means checking every file’s modification time and
|
||||
comparing it to the database. This can cause spikes of CPU usage for large
|
||||
folders.
|
||||
.UNINDENT
|
||||
@@ -169,16 +169,16 @@ To further limit the amount of CPU used when syncing and scanning, set the
|
||||
environment variable \fBGOMAXPROCS\fP to the maximum number of CPU cores
|
||||
Syncthing should use at any given moment. For example, \fBGOMAXPROCS=2\fP on a
|
||||
machine with four cores will limit Syncthing to no more than half the
|
||||
system\(aqs CPU power.
|
||||
system’s CPU power.
|
||||
.SH SHOULD I KEEP MY DEVICE IDS SECRET?
|
||||
.sp
|
||||
No. The IDs are not sensitive. Given a device ID it\(aqs possible to find the IP
|
||||
No. The IDs are not sensitive. Given a device ID it’s possible to find the IP
|
||||
address for that device, if global discovery is enabled on it. Knowing the device
|
||||
ID doesn\(aqt help you actually establish a connection to that device or get a list
|
||||
ID doesn’t help you actually establish a connection to that device or get a list
|
||||
of files, etc.
|
||||
.sp
|
||||
For a connection to be established, both devices need to know about the other\(aqs
|
||||
device ID. It\(aqs not possible (in practice) to forge a device ID. (To forge a
|
||||
For a connection to be established, both devices need to know about the other’s
|
||||
device ID. It’s not possible (in practice) to forge a device ID. (To forge a
|
||||
device ID you need to create a TLS certificate with that specific SHA\-256 hash.
|
||||
If you can do that, you can spoof any TLS certificate. The world is your
|
||||
oyster!)
|
||||
@@ -205,14 +205,14 @@ device where it was deleted.
|
||||
Beware that the \fB<filename>.sync\-conflict\-<date>\-<time>\-<modifiedBy>.<ext>\fP files are
|
||||
treated as normal files after they are created, so they are propagated between
|
||||
devices. We do this because the conflict is detected and resolved on one device,
|
||||
creating the \fBsync\-conflict\fP file, but it\(aqs just as much of a conflict
|
||||
everywhere else and we don\(aqt know which of the conflicting files is the "best"
|
||||
creating the \fBsync\-conflict\fP file, but it’s just as much of a conflict
|
||||
everywhere else and we don’t know which of the conflicting files is the “best”
|
||||
from the user point of view.
|
||||
.SH HOW DO I SERVE A FOLDER FROM A READ ONLY FILESYSTEM?
|
||||
.sp
|
||||
Syncthing requires a "folder marker" to indicate that the folder is present
|
||||
Syncthing requires a “folder marker” to indicate that the folder is present
|
||||
and healthy. By default this is a directory called \fB\&.stfolder\fP that is
|
||||
created by Syncthing when the folder is added. If this folder can\(aqt be
|
||||
created by Syncthing when the folder is added. If this folder can’t be
|
||||
created (you are serving files from a CD or something) you can instead set
|
||||
the advanced config \fBMarker Name\fP to the name of some file or folder that
|
||||
you know will always exist in the folder.
|
||||
@@ -225,11 +225,11 @@ Sharing a folder that is within an already shared folder is possible, but it has
|
||||
its caveats. What you must absolutely avoid are circular shares. This is just
|
||||
one example, there may be other undesired effects. Nesting shared folders is not
|
||||
supported, recommended or coded for, but it can be done successfully when you
|
||||
know what you\(aqre doing \- you have been warned.
|
||||
know what you’re doing \- you have been warned.
|
||||
.SH HOW DO I RENAME/MOVE A SYNCED FOLDER?
|
||||
.sp
|
||||
Syncthing doesn\(aqt have a direct way to do this, as it\(aqs potentially
|
||||
dangerous to do so if you\(aqre not careful \- it may result in data loss if
|
||||
Syncthing doesn’t have a direct way to do this, as it’s potentially
|
||||
dangerous to do so if you’re not careful \- it may result in data loss if
|
||||
something goes wrong during the move and is synchronized to your other
|
||||
devices.
|
||||
.sp
|
||||
@@ -237,8 +237,8 @@ The easy way to rename or move a synced folder on the local system is to
|
||||
remove the folder in the Syncthing UI, move it on disk, then re\-add it using
|
||||
the new path.
|
||||
.sp
|
||||
It\(aqs best to do this when the folder is already in sync between your
|
||||
devices, as it is otherwise unpredictable which changes will "win" after the
|
||||
It’s best to do this when the folder is already in sync between your
|
||||
devices, as it is otherwise unpredictable which changes will “win” after the
|
||||
move. Changes made on other devices may be overwritten, or changes made
|
||||
locally may be overwritten by those on other devices.
|
||||
.sp
|
||||
@@ -252,7 +252,7 @@ to configure listening ports such that they do not overlap (see config).
|
||||
.SH DOES SYNCTHING SUPPORT SYNCING BETWEEN FOLDERS ON THE SAME SYSTEM?
|
||||
.sp
|
||||
No. Syncthing is not designed to sync locally and the overhead involved in
|
||||
doing so using Syncthing\(aqs method would be wasteful. There are better
|
||||
doing so using Syncthing’s method would be wasteful. There are better
|
||||
programs to achieve this such as rsync or Unison.
|
||||
.SH WHEN I DO HAVE TWO DISTINCT SYNCTHING-MANAGED FOLDERS ON TWO HOSTS, HOW DOES SYNCTHING HANDLE MOVING FILES BETWEEN THEM?
|
||||
.sp
|
||||
@@ -289,7 +289,7 @@ The patterns in .stignore are glob patterns, where brackets are used to
|
||||
denote character ranges. That is, the pattern \fBq[abc]x\fP will match the
|
||||
files \fBqax\fP, \fBqbx\fP and \fBqcx\fP\&.
|
||||
.sp
|
||||
To match an actual file \fIcalled\fP \fBq[abc]x\fP the pattern needs to "escape"
|
||||
To match an actual file \fIcalled\fP \fBq[abc]x\fP the pattern needs to “escape”
|
||||
the brackets, like so: \fBq\e[abc\e]x\fP\&.
|
||||
.sp
|
||||
On Windows, escaping special characters is not supported as the \fB\e\fP
|
||||
@@ -298,7 +298,7 @@ such as \fB[\fP and \fB?\fP are not allowed in file names on Windows.
|
||||
.SH WHY IS THE SETUP MORE COMPLICATED THAN BITTORRENT/RESILIO SYNC?
|
||||
.sp
|
||||
Security over convenience. In Syncthing you have to setup both sides to
|
||||
connect two devices. An attacker can\(aqt do much with a stolen device ID, because
|
||||
connect two devices. An attacker can’t do much with a stolen device ID, because
|
||||
you have to add the device on the other side too. You have better control
|
||||
where your files are transferred.
|
||||
.sp
|
||||
@@ -354,7 +354,7 @@ $ ssh \-L 9090:127.0.0.1:8384 user@othercomputer.example.com
|
||||
will log you into othercomputer.example.com, and present the \fIremote\fP
|
||||
Syncthing GUI on \fI\%http://localhost:9090\fP on your \fIlocal\fP computer.
|
||||
.sp
|
||||
If you only want to access the remote gui and don\(aqt want the terminal
|
||||
If you only want to access the remote gui and don’t want the terminal
|
||||
session, use this example,
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
@@ -379,7 +379,7 @@ Another Windows way to run ssh is to install gow.
|
||||
.sp
|
||||
The easiest way to install gow is with chocolatey.
|
||||
\fI\%https://chocolatey.org/\fP
|
||||
.SH WHY DO I GET "HOST CHECK ERROR" IN THE GUI/API?
|
||||
.SH WHY DO I GET “HOST CHECK ERROR” IN THE GUI/API?
|
||||
.sp
|
||||
Since version 0.14.6 Syncthing does an extra security check when the GUI/API
|
||||
is bound to localhost \- namely that the browser is talking to localhost.
|
||||
@@ -404,8 +404,8 @@ Bind the GUI/API to a non\-localhost listen port.
|
||||
In all cases, username/password authentication and HTTPS should be used.
|
||||
.SH MY SYNCTHING DATABASE IS CORRUPT
|
||||
.sp
|
||||
This is almost always a result of bad RAM, storage device or other hardware. When the index database is found to be corrupt Syncthing cannot operate and will note this in the logs and exit. To overcome this delete the \fI\%database folder\fP <\fBhttps://docs.syncthing.net/users/config.html#description\fP> inside Syncthing\(aqs home directory and re\-start Syncthing. It will then need to perform a full re\-hashing of all shared folders. You should check your system in case the underlying cause is indeed faulty hardware which may put the system at risk of further data loss.
|
||||
.SH I DON'T LIKE THE GUI OR THE THEME. CAN IT BE CHANGED?
|
||||
This is almost always a result of bad RAM, storage device or other hardware. When the index database is found to be corrupt Syncthing cannot operate and will note this in the logs and exit. To overcome this delete the \fI\%database folder\fP <\fBhttps://docs.syncthing.net/users/config.html#description\fP> inside Syncthing’s home directory and re\-start Syncthing. It will then need to perform a full re\-hashing of all shared folders. You should check your system in case the underlying cause is indeed faulty hardware which may put the system at risk of further data loss.
|
||||
.SH I DON’T LIKE THE GUI OR THE THEME. CAN IT BE CHANGED?
|
||||
.sp
|
||||
You can change the theme in the settings. Syncthing ships with other themes
|
||||
than the default.
|
||||
@@ -416,7 +416,7 @@ By default, Syncthing will look for a directory \fBgui\fP inside the Syncthing
|
||||
home folder. To change the directory to look for themes, you need to set the
|
||||
STGUIASSETS environment variable. To get the concrete directory, run
|
||||
syncthing with the \fB\-paths\fP parameter. It will print all the relevant paths,
|
||||
including the "GUI override directory".
|
||||
including the “GUI override directory”.
|
||||
.sp
|
||||
To add e.g. a red theme, you can create the file \fBred/assets/css/theme.css\fP
|
||||
inside the GUI override directory to override the default CSS styles.
|
||||
@@ -433,7 +433,7 @@ crashes and other bugs.
|
||||
.SH WHERE DO SYNCTHING LOGS GO TO?
|
||||
.sp
|
||||
Syncthing logs to stdout by default. On Windows Syncthing by default also
|
||||
creates \fBsyncthing.log\fP in Syncthing\(aqs home directory (run \fBsyncthing
|
||||
creates \fBsyncthing.log\fP in Syncthing’s home directory (run \fBsyncthing
|
||||
\-paths\fP to see where that is). Command line option \fB\-logfile\fP can be used
|
||||
to specify a user\-defined logfile.
|
||||
.SH HOW CAN I VIEW THE HISTORY OF CHANGES?
|
||||
@@ -460,7 +460,7 @@ it initiates the conflict resolution procedure, which in the end results in a co
|
||||
up\-to\-date state with all the neighbours.
|
||||
.SH HOW DO I UPGRADE SYNCTHING?
|
||||
.sp
|
||||
If you use a package manager such as Debian\(aqs apt\-get, you should upgrade
|
||||
If you use a package manager such as Debian’s apt\-get, you should upgrade
|
||||
using the package manager. If you use the binary packages linked from
|
||||
Syncthing.net, you can use Syncthing built in automatic upgrades.
|
||||
.INDENT 0.0
|
||||
@@ -488,14 +488,14 @@ version. We suggest to use the GitHub API at
|
||||
the JSON response.
|
||||
.SH HOW DO I RUN SYNCTHING AS A DAEMON PROCESS ON LINUX?
|
||||
.sp
|
||||
If you\(aqre using systemd, runit, or upstart, we already ship examples, check
|
||||
If you’re using systemd, runit, or upstart, we already ship examples, check
|
||||
\fI\%https://github.com/syncthing/syncthing/tree/master/etc\fP for example
|
||||
configurations.
|
||||
.sp
|
||||
If however you\(aqre not using one of these tools, you have a couple of options.
|
||||
If your system has a tool called \fBstart\-stop\-daemon\fP installed (that\(aqs the name
|
||||
If however you’re not using one of these tools, you have a couple of options.
|
||||
If your system has a tool called \fBstart\-stop\-daemon\fP installed (that’s the name
|
||||
of the command, not the package), look into the local documentation for that, it
|
||||
will almost certainly cover 100% of what you want to do. If you don\(aqt have
|
||||
will almost certainly cover 100% of what you want to do. If you don’t have
|
||||
\fBstart\-stop\-daemon\fP, there are a bunch of other software packages you could use
|
||||
to do this. The most well known is called daemontools, and can be found in the
|
||||
standard package repositories for almost every modern Linux distribution.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "Mar 22, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.
|
||||
@@ -34,7 +34,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.sp
|
||||
A device should announce itself at startup. It does this by an HTTPS POST to
|
||||
the announce server URL. Standard discovery currently requires the path to be
|
||||
"/v2/", yet this can be up to the discovery server. The POST has a JSON payload
|
||||
“/v2/”, yet this can be up to the discovery server. The POST has a JSON payload
|
||||
listing connection addresses (if any):
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
@@ -49,9 +49,9 @@ listing connection addresses (if any):
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
It\(aqs OK for the "addresses" field to be either the empty list (\fB[]\fP),
|
||||
It’s OK for the “addresses” field to be either the empty list (\fB[]\fP),
|
||||
\fBnull\fP, or missing entirely. An announcement with the field missing
|
||||
or empty is however not useful...
|
||||
or empty is however not useful…
|
||||
.sp
|
||||
Any empty or unspecified IP addresses (i.e. addresses like \fBtcp://:22000\fP,
|
||||
\fBtcp://0.0.0.0:22000\fP, \fBtcp://[::]:22000\fP) are interpreted as referring to
|
||||
@@ -63,7 +63,7 @@ authentication. The device ID is deduced from the presented certificate.
|
||||
.sp
|
||||
The server response is empty, with code \fB204\fP (No Content) on success. If no
|
||||
certificate was presented, status \fB403\fP (Forbidden) is returned. If the
|
||||
posted data doesn\(aqt conform to the expected format, \fB400\fP (Bad Request) is
|
||||
posted data doesn’t conform to the expected format, \fB400\fP (Bad Request) is
|
||||
returned.
|
||||
.sp
|
||||
In successful responses, the server may return a \fBReannounce\-After\fP header
|
||||
@@ -80,14 +80,14 @@ Many Requests).
|
||||
.SH QUERIES
|
||||
.sp
|
||||
Queries are performed as HTTPS GET requests to the announce server URL. The
|
||||
requested device ID is passed as the query parameter "device", in canonical
|
||||
requested device ID is passed as the query parameter “device”, in canonical
|
||||
string form, i.e. \fBhttps://discovery.syncthing.net/?device=ABC12345\-....\fP
|
||||
.sp
|
||||
Successful responses will have status code \fB200\fP (OK) and carry a JSON payload
|
||||
of the same format as the announcement above. The response will not contain
|
||||
empty or unspecified addresses.
|
||||
.sp
|
||||
If the "device" query parameter is missing or malformed, the status code 400
|
||||
If the “device” query parameter is missing or malformed, the status code 400
|
||||
(Bad Request) is returned.
|
||||
.sp
|
||||
If the device ID is of a valid format but not found in the registry, 404 (Not
|
||||
@@ -109,9 +109,9 @@ signed certificate, Syncthing often runs in environments with outdated or
|
||||
simply nonexistent root CA bundles. Instead, Syncthing can verify the
|
||||
discovery server certificate fingerprint using the device ID mechanism. This
|
||||
is certificate pinning and conveyed in the Syncthing configuration as a
|
||||
synthetic "id" parameter on the discovery server URL:
|
||||
\fBhttps://discovery.syncthing.net/?id=...\fP\&. The "id" parameter is not, in
|
||||
fact, sent to the discovery server \- it\(aqs used by Syncthing itself to know
|
||||
synthetic “id” parameter on the discovery server URL:
|
||||
\fBhttps://discovery.syncthing.net/?id=...\fP\&. The “id” parameter is not, in
|
||||
fact, sent to the discovery server \- it’s used by Syncthing itself to know
|
||||
which certificate to expect on the server side.
|
||||
.sp
|
||||
The public discovery network uses this authentication mechanism instead of
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "Mar 22, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v4
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-NETWORKING" "7" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "Mar 22, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.
|
||||
@@ -72,7 +72,7 @@ Port \fB21027/UDP\fP (for discovery broadcasts on IPv4 and multicasts on IPv6)
|
||||
.UNINDENT
|
||||
.SS Uncomplicated Firewall (ufw)
|
||||
.sp
|
||||
If you\(aqre using \fBufw\fP on Linux and have installed the \fI\%Syncthing package\fP <\fBhttps://apt.syncthing.net/\fP>, you can allow the necessary ports by running:
|
||||
If you’re using \fBufw\fP on Linux and have installed the \fI\%Syncthing package\fP <\fBhttps://apt.syncthing.net/\fP>, you can allow the necessary ports by running:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user