Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4ab9d3312 | |||
| 24f41e169a | |||
| 50d8c43e7c | |||
| 90d9b2de2b | |||
| 04f05f102d | |||
| 289a02e994 | |||
| 84fe285659 | |||
| 445637ebec | |||
| 3f3d2c814b | |||
| 189e44488e | |||
| 27ff20faa3 | |||
| b1564e53e4 | |||
| 3a75b63776 | |||
| 8e238c8e48 | |||
| 3d5af675db | |||
| 9da3273eb8 | |||
| bd37f6da17 | |||
| 6940d79f5b | |||
| 0f80318ef6 | |||
| d6622b1f68 | |||
| 43bcb3d5a5 | |||
| e2e8f6e940 | |||
| 88b0ce892d | |||
| 55cd4b3d9b | |||
| f24676ba5a | |||
| 722b3fce6a | |||
| 8a05492622 | |||
| 63c4e7f6d0 | |||
| fafd30f804 | |||
| ad5a046843 | |||
| 4b8853bfde | |||
| 648fcf2c45 | |||
| ca3ae64bbf | |||
| e2204d0071 | |||
| d5ff2c41dc | |||
| 7a40c42e8b | |||
| 905c3594b0 | |||
| 5fd333e4f7 | |||
| 225c0dda80 | |||
| 5fd2cab102 |
@@ -0,0 +1,5 @@
|
||||
coverage:
|
||||
range: "40...100"
|
||||
|
||||
ignore:
|
||||
- "**.pb.go"
|
||||
@@ -70,6 +70,7 @@ Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
|
||||
Elliot Huffman <thelich2@gmail.com>
|
||||
Emil Hessman (ceh) <emil@hessman.se>
|
||||
Erik Meitner (WSGCSysadmin) <e.meitner@willystreet.coop>
|
||||
Evgeny Kuznetsov <evgeny@kuznetsov.md>
|
||||
Federico Castagnini (facastagnini) <federico.castagnini@gmail.com>
|
||||
Felix Ableitner (Nutomic) <me@nutomic.com>
|
||||
Felix Unterpaintner (bigbear2nd) <bigbear2nd@gmail.com>
|
||||
@@ -83,7 +84,7 @@ Heiko Zuerker (Smiley73) <heiko@zuerker.org>
|
||||
Hugo Locurcio <hugo.locurcio@hugo.pro>
|
||||
Iain Barnett <iainspeed@gmail.com>
|
||||
Ian Johnson (anonymouse64) <ian.johnson@canonical.com> <person.uwsome@gmail.com>
|
||||
Iskander (Alex) Sharipov <quasilyte@gmail.com>
|
||||
Iskander Sharipov (Alex) <quasilyte@gmail.com>
|
||||
Jaakko Hannikainen (jgke) <jgke@jgke.fi>
|
||||
Jacek Szafarkiewicz (hadogenes) <szafar@linux.pl>
|
||||
Jake Peterson (acogdev) <jake@acogdev.com>
|
||||
@@ -99,6 +100,7 @@ Jochen Voss (seehuhn) <voss@seehuhn.de>
|
||||
Johan Andersson <j@i19.se>
|
||||
Johan Vromans (sciurius) <jvromans@squirrel.nl>
|
||||
John Rinehart (fuzzybear3965) <johnrichardrinehart@gmail.com>
|
||||
Jonas Thelemann <e-mail@jonas-thelemann.de>
|
||||
Jonathan Cross <jcross@gmail.com>
|
||||
Jose Manuel Delicado (jmdaweb) <jmdaweb@hotmail.com> <jmdaweb@users.noreply.github.com>
|
||||
Jörg Thalheim <Mic92@users.noreply.github.com>
|
||||
@@ -126,6 +128,7 @@ Mark Pulford (mpx) <mark@kyne.com.au>
|
||||
Mateusz Naściszewski (mateon1) <matin1111@wp.pl>
|
||||
Matic Potočnik <hairyfotr@gmail.com>
|
||||
Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
|
||||
Matt Robenolt <matt@ydekproductions.com>
|
||||
Matteo Ruina <matteo.ruina@gmail.com>
|
||||
Maurizio Tomasi <ziotom78@gmail.com>
|
||||
Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com>
|
||||
@@ -143,6 +146,7 @@ Nicolas Braud-Santoni <nicolas@braud-santoni.eu>
|
||||
Niels Peter Roest (Niller303) <nielsproest@hotmail.com> <seje.niels@hotmail.com>
|
||||
Nils Jakobi (thunderstorm99) <jakobi.nils@gmail.com>
|
||||
NoLooseEnds <jon.koslung@gmail.com>
|
||||
otbutz <tbutz@optitool.de>
|
||||
Oyebanji Jacob Mayowa <oyebanji05@gmail.com>
|
||||
Pascal Jungblut (pascalj) <github@pascalj.com> <mail@pascal-jungblut.com>
|
||||
Pawel Palenica (qepasa) <pawelpalenica11@gmail.com>
|
||||
|
||||
+5
-14
@@ -1,39 +1,30 @@
|
||||
# Docker Container for Syncthing
|
||||
|
||||
Use the Dockerfile in this repo, or pull the `syncthing/syncthing` image
|
||||
from Docker Hub. Use volumes to have the synchronized files available on the
|
||||
host.
|
||||
from Docker Hub.
|
||||
|
||||
The exposed volumes are by default:
|
||||
|
||||
/var/syncthing/config - the configuration and index directory into the Container
|
||||
/var/syncthing - the default sync folder into the Container
|
||||
|
||||
You can add more folders and map them as you prefer.
|
||||
Use the `/var/syncthing` volume to have the synchronized files available on the
|
||||
host. You can add more folders and map them as you prefer.
|
||||
|
||||
Note that Syncthing runs as UID 1000 and GID 1000 by default. These may be
|
||||
altered with the ``PUID`` and ``PGID`` environment variables.
|
||||
|
||||
Example usage:
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
$ docker pull syncthing/syncthing
|
||||
$ docker run -p 8384:8384 -p 22000:22000 \
|
||||
-v /wherever/st-cfg:/var/syncthing/config \
|
||||
-v /wherever/st-sync:/var/syncthing \
|
||||
syncthing/syncthing:latest
|
||||
```
|
||||
|
||||
Note that local device discovery will not work with the above command resulting
|
||||
in poor local transfer rates if local device addresses are not manually
|
||||
configured.
|
||||
Note that local device discovery will not work with the above command, resulting in poor local transfer rates if local device addresses are not manually configured.
|
||||
|
||||
To allow local discovery, the docker host network can be used instead:
|
||||
|
||||
```
|
||||
$ docker pull syncthing/syncthing
|
||||
$ docker run --network=host \
|
||||
-v /wherever/st-cfg:/var/syncthing/config \
|
||||
-v /wherever/st-sync:/var/syncthing \
|
||||
syncthing/syncthing:latest
|
||||
```
|
||||
|
||||
@@ -48,6 +48,7 @@ var (
|
||||
pkgdir string
|
||||
cc string
|
||||
debugBinary bool
|
||||
coverage bool
|
||||
timeout = "120s"
|
||||
gogoProtoVersion = "v1.2.0"
|
||||
)
|
||||
@@ -330,24 +331,27 @@ func parseFlags() {
|
||||
flag.StringVar(&pkgdir, "pkgdir", "", "Set -pkgdir parameter for `go build`")
|
||||
flag.StringVar(&cc, "cc", os.Getenv("CC"), "Set CC environment variable for `go build`")
|
||||
flag.BoolVar(&debugBinary, "debug-binary", debugBinary, "Create unoptimized binary to use with delve, set -gcflags='-N -l' and omit -ldflags")
|
||||
flag.BoolVar(&coverage, "coverage", coverage, "Write coverage profile of tests to coverage.txt")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
func test(pkgs ...string) {
|
||||
lazyRebuildAssets()
|
||||
|
||||
useRace := runtime.GOARCH == "amd64"
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "linux", "freebsd": // , "windows": # See https://github.com/golang/go/issues/27089
|
||||
default:
|
||||
useRace = false
|
||||
args := []string{"test", "-short", "-timeout", timeout, "-tags", "purego"}
|
||||
|
||||
if runtime.GOARCH == "amd64" {
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "linux", "freebsd": // , "windows": # See https://github.com/golang/go/issues/27089
|
||||
args = append(args, "-race")
|
||||
}
|
||||
}
|
||||
|
||||
if useRace {
|
||||
runPrint(goCmd, append([]string{"test", "-short", "-race", "-timeout", timeout, "-tags", "purego"}, pkgs...)...)
|
||||
} else {
|
||||
runPrint(goCmd, append([]string{"test", "-short", "-timeout", timeout, "-tags", "purego"}, pkgs...)...)
|
||||
if coverage {
|
||||
args = append(args, "-covermode", "atomic", "-coverprofile", "coverage.txt")
|
||||
}
|
||||
|
||||
runPrint(goCmd, append(args, pkgs...)...)
|
||||
}
|
||||
|
||||
func bench(pkgs ...string) {
|
||||
|
||||
+32
-73
@@ -40,11 +40,9 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/syncthing/syncthing/lib/stats"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
"github.com/syncthing/syncthing/lib/versioner"
|
||||
"github.com/vitrun/qart/qr"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -64,90 +62,33 @@ const (
|
||||
|
||||
type apiService struct {
|
||||
id protocol.DeviceID
|
||||
cfg configIntf
|
||||
cfg config.Wrapper
|
||||
httpsCertFile string
|
||||
httpsKeyFile string
|
||||
statics *staticsServer
|
||||
model modelIntf
|
||||
model model.Model
|
||||
eventSubs map[events.EventType]events.BufferedSubscription
|
||||
eventSubsMut sync.Mutex
|
||||
discoverer discover.CachingMux
|
||||
connectionsService connectionsIntf
|
||||
connectionsService connections.Service
|
||||
fss *folderSummaryService
|
||||
systemConfigMut sync.Mutex // serializes posts to /rest/system/config
|
||||
stop chan struct{} // signals intentional stop
|
||||
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
|
||||
startedOnce chan struct{} // the service has started at least once
|
||||
startupErr error
|
||||
cpu rater
|
||||
|
||||
guiErrors logger.Recorder
|
||||
systemLog logger.Recorder
|
||||
}
|
||||
|
||||
type modelIntf interface {
|
||||
GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
|
||||
Completion(device protocol.DeviceID, folder string) model.FolderCompletion
|
||||
Override(folder string)
|
||||
Revert(folder string)
|
||||
NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated)
|
||||
RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error)
|
||||
LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated
|
||||
NeedSize(folder string) db.Counts
|
||||
ConnectionStats() map[string]interface{}
|
||||
DeviceStatistics() map[string]stats.DeviceStatistics
|
||||
FolderStatistics() map[string]stats.FolderStatistics
|
||||
CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool)
|
||||
CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool)
|
||||
ResetFolder(folder string)
|
||||
Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []model.Availability
|
||||
GetIgnores(folder string) ([]string, []string, error)
|
||||
GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error)
|
||||
RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error)
|
||||
SetIgnores(folder string, content []string) error
|
||||
DelayScan(folder string, next time.Duration)
|
||||
ScanFolder(folder string) error
|
||||
ScanFolders() map[string]error
|
||||
ScanFolderSubdirs(folder string, subs []string) error
|
||||
BringToFront(folder, file string)
|
||||
Connection(deviceID protocol.DeviceID) (connections.Connection, bool)
|
||||
GlobalSize(folder string) db.Counts
|
||||
LocalSize(folder string) db.Counts
|
||||
ReceiveOnlyChangedSize(folder string) db.Counts
|
||||
CurrentSequence(folder string) (int64, bool)
|
||||
RemoteSequence(folder string) (int64, bool)
|
||||
State(folder string) (string, time.Time, error)
|
||||
UsageReportingStats(version int, preview bool) map[string]interface{}
|
||||
FolderErrors(folder string) ([]model.FileError, error)
|
||||
WatchError(folder string) error
|
||||
}
|
||||
|
||||
type configIntf interface {
|
||||
GUI() config.GUIConfiguration
|
||||
LDAP() config.LDAPConfiguration
|
||||
RawCopy() config.Configuration
|
||||
Options() config.OptionsConfiguration
|
||||
Replace(cfg config.Configuration) (config.Waiter, error)
|
||||
Subscribe(c config.Committer)
|
||||
Folders() map[string]config.FolderConfiguration
|
||||
Devices() map[protocol.DeviceID]config.DeviceConfiguration
|
||||
SetDevice(config.DeviceConfiguration) (config.Waiter, error)
|
||||
SetDevices([]config.DeviceConfiguration) (config.Waiter, error)
|
||||
Save() error
|
||||
ListenAddresses() []string
|
||||
RequiresRestart() bool
|
||||
}
|
||||
|
||||
type connectionsIntf interface {
|
||||
Status() map[string]interface{}
|
||||
NATType() string
|
||||
}
|
||||
|
||||
type rater interface {
|
||||
Rate() float64
|
||||
}
|
||||
|
||||
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, defaultSub, diskSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder, cpu rater) *apiService {
|
||||
func newAPIService(id protocol.DeviceID, cfg config.Wrapper, httpsCertFile, httpsKeyFile, assetDir string, m model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connections.Service, errors, systemLog logger.Recorder, cpu rater) *apiService {
|
||||
service := &apiService{
|
||||
id: id,
|
||||
cfg: cfg,
|
||||
@@ -174,6 +115,11 @@ func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKey
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *apiService) 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)
|
||||
if err != nil {
|
||||
@@ -236,14 +182,15 @@ func (s *apiService) Serve() {
|
||||
// We let this be a loud user-visible warning as it may be the only
|
||||
// indication they get that the GUI won't be available.
|
||||
l.Warnln("Starting API/GUI:", err)
|
||||
return
|
||||
|
||||
default:
|
||||
// This is during initialization. A failure here should be fatal
|
||||
// as there will be no way for the user to communicate with us
|
||||
// otherwise anyway.
|
||||
l.Fatalln("Starting API/GUI:", err)
|
||||
s.startupErr = err
|
||||
close(s.startedOnce)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if listener == nil {
|
||||
@@ -410,6 +357,19 @@ func (s *apiService) Serve() {
|
||||
}
|
||||
}
|
||||
|
||||
// Complete implements suture.IsCompletable, which signifies to the supervisor
|
||||
// whether to stop restarting the service.
|
||||
func (s *apiService) Complete() bool {
|
||||
select {
|
||||
case <-s.startedOnce:
|
||||
return s.startupErr != nil
|
||||
case <-s.stop:
|
||||
return true
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *apiService) Stop() {
|
||||
close(s.stop)
|
||||
}
|
||||
@@ -699,7 +659,7 @@ func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func folderSummary(cfg configIntf, m modelIntf, folder string) (map[string]interface{}, error) {
|
||||
func folderSummary(cfg config.Wrapper, m model.Model, folder string) (map[string]interface{}, error) {
|
||||
var res = make(map[string]interface{})
|
||||
|
||||
errors, err := m.FolderErrors(folder)
|
||||
@@ -932,7 +892,7 @@ func (s *apiService) getSystemConfigInsync(w http.ResponseWriter, r *http.Reques
|
||||
|
||||
func (s *apiService) postSystemRestart(w http.ResponseWriter, r *http.Request) {
|
||||
s.flushResponse(`{"ok": "restarting"}`, w)
|
||||
go restart()
|
||||
go exit.Restart()
|
||||
}
|
||||
|
||||
func (s *apiService) postSystemReset(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -958,12 +918,12 @@ func (s *apiService) postSystemReset(w http.ResponseWriter, r *http.Request) {
|
||||
s.flushResponse(`{"ok": "resetting folder `+folder+`"}`, w)
|
||||
}
|
||||
|
||||
go restart()
|
||||
go exit.Restart()
|
||||
}
|
||||
|
||||
func (s *apiService) postSystemShutdown(w http.ResponseWriter, r *http.Request) {
|
||||
s.flushResponse(`{"ok": "shutting down"}`, w)
|
||||
go shutdown()
|
||||
go exit.Shutdown()
|
||||
}
|
||||
|
||||
func (s *apiService) flushResponse(resp string, w http.ResponseWriter) {
|
||||
@@ -1383,8 +1343,7 @@ func (s *apiService) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
s.flushResponse(`{"ok": "restarting"}`, w)
|
||||
l.Infoln("Upgrading")
|
||||
stop <- exitUpgrading
|
||||
exit.ExitUpgrading()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+180
-103
@@ -9,7 +9,6 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -48,6 +47,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
@@ -68,10 +68,7 @@ const (
|
||||
maxSystemLog = 250
|
||||
)
|
||||
|
||||
var (
|
||||
myID protocol.DeviceID
|
||||
stop = make(chan int)
|
||||
)
|
||||
var myID protocol.DeviceID
|
||||
|
||||
const (
|
||||
usage = "syncthing [options]"
|
||||
@@ -166,34 +163,35 @@ var (
|
||||
)
|
||||
|
||||
type RuntimeOptions struct {
|
||||
confDir string
|
||||
resetDatabase bool
|
||||
resetDeltaIdxs bool
|
||||
showVersion bool
|
||||
showPaths bool
|
||||
showDeviceId bool
|
||||
doUpgrade bool
|
||||
doUpgradeCheck bool
|
||||
upgradeTo string
|
||||
noBrowser bool
|
||||
browserOnly bool
|
||||
hideConsole bool
|
||||
logFile string
|
||||
auditEnabled bool
|
||||
auditFile string
|
||||
verbose bool
|
||||
paused bool
|
||||
unpaused bool
|
||||
guiAddress string
|
||||
guiAPIKey string
|
||||
generateDir string
|
||||
noRestart bool
|
||||
profiler string
|
||||
assetDir string
|
||||
cpuProfile bool
|
||||
stRestarting bool
|
||||
logFlags int
|
||||
showHelp bool
|
||||
confDir string
|
||||
resetDatabase bool
|
||||
resetDeltaIdxs bool
|
||||
showVersion bool
|
||||
showPaths bool
|
||||
showDeviceId bool
|
||||
doUpgrade bool
|
||||
doUpgradeCheck bool
|
||||
upgradeTo string
|
||||
noBrowser bool
|
||||
browserOnly bool
|
||||
hideConsole bool
|
||||
logFile string
|
||||
auditEnabled bool
|
||||
auditFile string
|
||||
verbose bool
|
||||
paused bool
|
||||
unpaused bool
|
||||
guiAddress string
|
||||
guiAPIKey string
|
||||
generateDir string
|
||||
noRestart bool
|
||||
profiler string
|
||||
assetDir string
|
||||
cpuProfile bool
|
||||
stRestarting bool
|
||||
logFlags int
|
||||
showHelp bool
|
||||
allowNewerConfig bool
|
||||
}
|
||||
|
||||
func defaultRuntimeOptions() RuntimeOptions {
|
||||
@@ -247,6 +245,7 @@ func parseCommandLineOptions() RuntimeOptions {
|
||||
flag.BoolVar(&options.unpaused, "unpaused", false, "Start with all devices and folders unpaused")
|
||||
flag.StringVar(&options.logFile, "logfile", options.logFile, "Log file name (still always logs to stdout). Cannot be used together with -no-restart/STNORESTART environment variable.")
|
||||
flag.StringVar(&options.auditFile, "auditfile", options.auditFile, "Specify audit file (use \"-\" for stdout, \"--\" for stderr)")
|
||||
flag.BoolVar(&options.allowNewerConfig, "allow-newer-config", false, "Allow loading newer than current config version")
|
||||
if runtime.GOOS == "windows" {
|
||||
// Allow user to hide the console window
|
||||
flag.BoolVar(&options.hideConsole, "no-console", false, "Hide console window")
|
||||
@@ -264,6 +263,32 @@ func parseCommandLineOptions() RuntimeOptions {
|
||||
return options
|
||||
}
|
||||
|
||||
type exiter struct {
|
||||
stop chan int
|
||||
}
|
||||
|
||||
func (e *exiter) Restart() {
|
||||
l.Infoln("Restarting")
|
||||
e.stop <- exitRestarting
|
||||
}
|
||||
|
||||
func (e *exiter) Shutdown() {
|
||||
l.Infoln("Shutting down")
|
||||
e.stop <- exitSuccess
|
||||
}
|
||||
|
||||
func (e *exiter) ExitUpgrading() {
|
||||
l.Infoln("Shutting down after upgrade")
|
||||
e.stop <- exitUpgrading
|
||||
}
|
||||
|
||||
// waitForExit must be called synchronously.
|
||||
func (e *exiter) waitForExit() int {
|
||||
return <-e.stop
|
||||
}
|
||||
|
||||
var exit = exiter{make(chan int)}
|
||||
|
||||
func main() {
|
||||
options := parseCommandLineOptions()
|
||||
l.SetFlags(options.logFlags)
|
||||
@@ -282,7 +307,8 @@ func main() {
|
||||
// to complain if they set -logfile explicitly, not if it's set to its
|
||||
// default location
|
||||
if options.noRestart && (options.logFile != "" && options.logFile != "-") {
|
||||
l.Fatalln("-logfile may not be used with -no-restart or STNORESTART")
|
||||
l.Warnln("-logfile may not be used with -no-restart or STNORESTART")
|
||||
os.Exit(exitError)
|
||||
}
|
||||
|
||||
if options.hideConsole {
|
||||
@@ -295,11 +321,13 @@ func main() {
|
||||
var err error
|
||||
options.confDir, err = filepath.Abs(options.confDir)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
l.Warnln("Failed to make options path absolute:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
}
|
||||
if err := locations.SetBaseDir(locations.ConfigBaseDir, options.confDir); err != nil {
|
||||
l.Fatalln(err)
|
||||
l.Warnln(err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +364,8 @@ func main() {
|
||||
locations.Get(locations.KeyFile),
|
||||
)
|
||||
if err != nil {
|
||||
l.Fatalln("Error reading device ID:", err)
|
||||
l.Warnln("Error reading device ID:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
|
||||
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||
@@ -345,22 +374,32 @@ func main() {
|
||||
}
|
||||
|
||||
if options.browserOnly {
|
||||
openGUI()
|
||||
if err := openGUI(); err != nil {
|
||||
l.Warnln("Failed to open web UI:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if options.generateDir != "" {
|
||||
generate(options.generateDir)
|
||||
if err := generate(options.generateDir); err != nil {
|
||||
l.Warnln("Failed to generate config and keys:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure that our home directory exists.
|
||||
ensureDir(locations.GetBaseDir(locations.ConfigBaseDir), 0700)
|
||||
if err := ensureDir(locations.GetBaseDir(locations.ConfigBaseDir), 0700); err != nil {
|
||||
l.Warnln("Failure on home directory:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
|
||||
if options.upgradeTo != "" {
|
||||
err := upgrade.ToURL(options.upgradeTo)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err) // exits 1
|
||||
l.Warnln("Error while Upgrading:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
l.Infoln("Upgraded from", options.upgradeTo)
|
||||
return
|
||||
@@ -379,7 +418,8 @@ func main() {
|
||||
|
||||
if options.resetDatabase {
|
||||
if err := resetDB(); err != nil {
|
||||
l.Fatalln("Resetting database:", err)
|
||||
l.Warnln("Resetting database:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -391,23 +431,30 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func openGUI() {
|
||||
cfg, _ := loadOrDefaultConfig()
|
||||
func openGUI() error {
|
||||
cfg, err := loadOrDefaultConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.GUI().Enabled {
|
||||
if err := openURL(cfg.GUI().URL()); err != nil {
|
||||
l.Fatalln("Open URL:", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
l.Warnln("Browser: GUI is currently disabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generate(generateDir string) {
|
||||
func generate(generateDir string) error {
|
||||
dir, err := fs.ExpandTilde(generateDir)
|
||||
if err != nil {
|
||||
l.Fatalln("generate:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ensureDir(dir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
ensureDir(dir, 0700)
|
||||
|
||||
certFile, keyFile := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
@@ -417,11 +464,11 @@ func generate(generateDir string) {
|
||||
} else {
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, tlsDefaultCommonName)
|
||||
if err != nil {
|
||||
l.Fatalln("Create certificate:", err)
|
||||
return errors.Wrap(err, "create certificate")
|
||||
}
|
||||
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||
if err != nil {
|
||||
l.Fatalln("Load certificate:", err)
|
||||
return errors.Wrap(err, "load certificate")
|
||||
}
|
||||
if err == nil {
|
||||
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
|
||||
@@ -431,13 +478,17 @@ func generate(generateDir string) {
|
||||
cfgFile := filepath.Join(dir, "config.xml")
|
||||
if _, err := os.Stat(cfgFile); err == nil {
|
||||
l.Warnln("Config exists; will not overwrite.")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
cfg, err := defaultConfig(cfgFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var cfg = defaultConfig(cfgFile)
|
||||
err = cfg.Save()
|
||||
if err != nil {
|
||||
l.Warnln("Failed to save config", err)
|
||||
return errors.Wrap(err, "save config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func debugFacilities() string {
|
||||
@@ -467,7 +518,8 @@ func checkUpgrade() upgrade.Release {
|
||||
opts := cfg.Options()
|
||||
release, err := upgrade.LatestRelease(opts.ReleasesURL, build.Version, opts.UpgradeToPreReleases)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err)
|
||||
l.Warnln("Upgrade:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
|
||||
if upgrade.CompareVersions(release.Tag, build.Version) <= 0 {
|
||||
@@ -486,14 +538,16 @@ func performUpgrade(release upgrade.Release) {
|
||||
if err == nil {
|
||||
err = upgrade.To(release)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err)
|
||||
l.Warnln("Upgrade:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
l.Infof("Upgraded to %q", release.Tag)
|
||||
} else {
|
||||
l.Infoln("Attempting upgrade through running Syncthing...")
|
||||
err = upgradeViaRest()
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err)
|
||||
l.Warnln("Upgrade:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
l.Infoln("Syncthing upgrading")
|
||||
os.Exit(exitUpgrading)
|
||||
@@ -592,7 +646,8 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
tlsDefaultCommonName,
|
||||
)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
l.Infoln("Failed to generate certificate:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,10 +669,15 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
"myID": myID.String(),
|
||||
})
|
||||
|
||||
cfg := loadConfigAtStartup()
|
||||
cfg, err := loadConfigAtStartup(runtimeOptions.allowNewerConfig)
|
||||
if err != nil {
|
||||
l.Warnln("Failed to initialize config:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
|
||||
if err := checkShortIDs(cfg); err != nil {
|
||||
l.Fatalln("Short device IDs are in conflict. Unlucky!\n Regenerate the device ID of one of the following:\n ", err)
|
||||
l.Warnln("Short device IDs are in conflict. Unlucky!\n Regenerate the device ID of one of the following:\n ", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
|
||||
if len(runtimeOptions.profiler) > 0 {
|
||||
@@ -626,7 +686,8 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
runtime.SetBlockProfileRate(1)
|
||||
err := http.ListenAndServe(runtimeOptions.profiler, nil)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
l.Warnln(err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -637,10 +698,12 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
dbFile := locations.Get(locations.Database)
|
||||
ldb, err := db.Open(dbFile)
|
||||
if err != nil {
|
||||
l.Fatalln("Error opening database:", err)
|
||||
l.Warnln("Error opening database:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
if err := db.UpdateSchema(ldb); err != nil {
|
||||
l.Fatalln("Database schema:", err)
|
||||
l.Warnln("Database schema:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
|
||||
if runtimeOptions.resetDeltaIdxs {
|
||||
@@ -776,10 +839,12 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
if runtimeOptions.cpuProfile {
|
||||
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
|
||||
if err != nil {
|
||||
l.Fatalln("Creating profile:", err)
|
||||
l.Warnln("Creating profile:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
l.Fatalln("Starting profile:", err)
|
||||
l.Warnln("Starting profile:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -858,7 +923,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
code := <-stop
|
||||
code := exit.waitForExit()
|
||||
|
||||
mainService.Stop()
|
||||
|
||||
@@ -879,7 +944,7 @@ func setupSignalHandling() {
|
||||
signal.Notify(restartSign, sigHup)
|
||||
go func() {
|
||||
<-restartSign
|
||||
stop <- exitRestarting
|
||||
exit.Restart()
|
||||
}()
|
||||
|
||||
// Exit with "success" code (no restart) on INT/TERM
|
||||
@@ -889,45 +954,57 @@ func setupSignalHandling() {
|
||||
signal.Notify(stopSign, os.Interrupt, sigTerm)
|
||||
go func() {
|
||||
<-stopSign
|
||||
stop <- exitSuccess
|
||||
exit.Shutdown()
|
||||
}()
|
||||
}
|
||||
|
||||
func loadOrDefaultConfig() (*config.Wrapper, error) {
|
||||
func loadOrDefaultConfig() (config.Wrapper, error) {
|
||||
cfgFile := locations.Get(locations.ConfigFile)
|
||||
cfg, err := config.Load(cfgFile, myID)
|
||||
|
||||
if err != nil {
|
||||
cfg = defaultConfig(cfgFile)
|
||||
cfg, err = defaultConfig(cfgFile)
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func loadConfigAtStartup() *config.Wrapper {
|
||||
func loadConfigAtStartup(allowNewerConfig bool) (config.Wrapper, error) {
|
||||
cfgFile := locations.Get(locations.ConfigFile)
|
||||
cfg, err := config.Load(cfgFile, myID)
|
||||
if os.IsNotExist(err) {
|
||||
cfg = defaultConfig(cfgFile)
|
||||
cfg.Save()
|
||||
l.Infof("Default config saved. Edit %s to taste or use the GUI\n", cfg.ConfigPath())
|
||||
cfg, err = defaultConfig(cfgFile)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate default config")
|
||||
}
|
||||
err = cfg.Save()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to save default config")
|
||||
}
|
||||
l.Infof("Default config saved. Edit %s to taste (with Syncthing stopped) or use the GUI", cfg.ConfigPath())
|
||||
} else if err == io.EOF {
|
||||
l.Fatalln("Failed to load config: unexpected end of file. Truncated or empty configuration?")
|
||||
return nil, errors.New("Failed to load config: unexpected end of file. Truncated or empty configuration?")
|
||||
} else if err != nil {
|
||||
l.Fatalln("Failed to load config:", err)
|
||||
return nil, errors.Wrap(err, "failed to load config")
|
||||
}
|
||||
|
||||
if cfg.RawCopy().OriginalVersion != config.CurrentVersion {
|
||||
if cfg.RawCopy().OriginalVersion == config.CurrentVersion+1101 {
|
||||
l.Infof("Now, THAT's what we call a config from the future! Don't worry. As long as you hit that wire with the connecting hook at precisely eighty-eight miles per hour the instant the lightning strikes the tower... everything will be fine.")
|
||||
}
|
||||
if cfg.RawCopy().OriginalVersion > config.CurrentVersion && !allowNewerConfig {
|
||||
return nil, fmt.Errorf("Config file version (%d) is newer than supported version (%d). If this is expected, use -allow-newer-config to override.", cfg.RawCopy().OriginalVersion, config.CurrentVersion)
|
||||
}
|
||||
err = archiveAndSaveConfig(cfg)
|
||||
if err != nil {
|
||||
l.Fatalln("Config archive:", err)
|
||||
return nil, errors.Wrap(err, "config archive")
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func archiveAndSaveConfig(cfg *config.Wrapper) error {
|
||||
func archiveAndSaveConfig(cfg config.Wrapper) error {
|
||||
// Copy the existing config to an archive copy
|
||||
archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", cfg.RawCopy().OriginalVersion)
|
||||
l.Infoln("Archiving a copy of old config file format at:", archivePath)
|
||||
@@ -976,7 +1053,8 @@ func startAuditing(mainService *suture.Supervisor, auditFile string) {
|
||||
}
|
||||
fd, err = os.OpenFile(auditFile, auditFlags, 0600)
|
||||
if err != nil {
|
||||
l.Fatalln("Audit:", err)
|
||||
l.Warnln("Audit:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
auditDest = auditFile
|
||||
}
|
||||
@@ -991,7 +1069,7 @@ func startAuditing(mainService *suture.Supervisor, auditFile string) {
|
||||
l.Infoln("Audit log in", auditDest)
|
||||
}
|
||||
|
||||
func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService *connections.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) {
|
||||
func setupGUI(mainService *suture.Supervisor, cfg config.Wrapper, m model.Model, defaultSub, diskSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connections.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) {
|
||||
guiCfg := cfg.GUI()
|
||||
|
||||
if !guiCfg.Enabled {
|
||||
@@ -1009,46 +1087,43 @@ func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Mode
|
||||
cfg.Subscribe(api)
|
||||
mainService.Add(api)
|
||||
|
||||
if err := api.WaitForStart(); err != nil {
|
||||
l.Warnln("Failed starting API:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
|
||||
if cfg.Options().StartBrowser && !runtimeOptions.noBrowser && !runtimeOptions.stRestarting {
|
||||
// Can potentially block if the utility we are invoking doesn't
|
||||
// fork, and just execs, hence keep it in its own routine.
|
||||
<-api.startedOnce
|
||||
go func() { _ = openURL(guiCfg.URL()) }()
|
||||
}
|
||||
}
|
||||
|
||||
func defaultConfig(cfgFile string) *config.Wrapper {
|
||||
newCfg := config.NewWithFreePorts(myID)
|
||||
func defaultConfig(cfgFile string) (config.Wrapper, error) {
|
||||
newCfg, err := config.NewWithFreePorts(myID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if noDefaultFolder {
|
||||
l.Infoln("We will skip creation of a default folder on first start since the proper envvar is set")
|
||||
return config.Wrap(cfgFile, newCfg)
|
||||
return config.Wrap(cfgFile, newCfg), nil
|
||||
}
|
||||
|
||||
newCfg.Folders = append(newCfg.Folders, config.NewFolderConfiguration(myID, "default", "Default Folder", fs.FilesystemTypeBasic, locations.Get(locations.DefFolder)))
|
||||
l.Infoln("Default folder created and/or linked to new config")
|
||||
return config.Wrap(cfgFile, newCfg)
|
||||
return config.Wrap(cfgFile, newCfg), nil
|
||||
}
|
||||
|
||||
func resetDB() error {
|
||||
return os.RemoveAll(locations.Get(locations.Database))
|
||||
}
|
||||
|
||||
func restart() {
|
||||
l.Infoln("Restarting")
|
||||
stop <- exitRestarting
|
||||
}
|
||||
|
||||
func shutdown() {
|
||||
l.Infoln("Shutting down")
|
||||
stop <- exitSuccess
|
||||
}
|
||||
|
||||
func ensureDir(dir string, mode fs.FileMode) {
|
||||
func ensureDir(dir string, mode fs.FileMode) error {
|
||||
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
|
||||
err := fs.MkdirAll(".", mode)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
return err
|
||||
}
|
||||
|
||||
if fi, err := fs.Stat("."); err == nil {
|
||||
@@ -1064,6 +1139,7 @@ func ensureDir(dir string, mode fs.FileMode) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func standbyMonitor() {
|
||||
@@ -1079,14 +1155,14 @@ func standbyMonitor() {
|
||||
// things a moment to stabilize.
|
||||
time.Sleep(restartDelay)
|
||||
|
||||
restart()
|
||||
exit.Restart()
|
||||
return
|
||||
}
|
||||
now = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func autoUpgrade(cfg *config.Wrapper) {
|
||||
func autoUpgrade(cfg config.Wrapper) {
|
||||
timer := time.NewTimer(0)
|
||||
sub := events.Default.Subscribe(events.DeviceConnected)
|
||||
for {
|
||||
@@ -1137,7 +1213,7 @@ func autoUpgrade(cfg *config.Wrapper) {
|
||||
events.Default.Unsubscribe(sub)
|
||||
l.Warnf("Automatically upgraded to version %q. Restarting in 1 minute.", rel.Tag)
|
||||
time.Sleep(time.Minute)
|
||||
stop <- exitUpgrading
|
||||
exit.ExitUpgrading()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1188,7 +1264,7 @@ func cleanConfigDirectory() {
|
||||
// checkShortIDs verifies that the configuration won't result in duplicate
|
||||
// short ID:s; that is, that the devices in the cluster all have unique
|
||||
// initial 64 bits.
|
||||
func checkShortIDs(cfg *config.Wrapper) error {
|
||||
func checkShortIDs(cfg config.Wrapper) error {
|
||||
exists := make(map[protocol.ShortID]protocol.DeviceID)
|
||||
for deviceID := range cfg.Devices() {
|
||||
shortID := deviceID.Short()
|
||||
@@ -1210,7 +1286,7 @@ func showPaths(options RuntimeOptions) {
|
||||
fmt.Printf("Default sync folder directory:\n\t%s\n\n", locations.Get(locations.DefFolder))
|
||||
}
|
||||
|
||||
func setPauseState(cfg *config.Wrapper, paused bool) {
|
||||
func setPauseState(cfg config.Wrapper, paused bool) {
|
||||
raw := cfg.RawCopy()
|
||||
for i := range raw.Devices {
|
||||
raw.Devices[i].Paused = paused
|
||||
@@ -1219,6 +1295,7 @@ func setPauseState(cfg *config.Wrapper, paused bool) {
|
||||
raw.Folders[i].Paused = paused
|
||||
}
|
||||
if _, err := cfg.Replace(raw); err != nil {
|
||||
l.Fatalln("Cannot adjust paused state:", err)
|
||||
l.Warnln("Cannot adjust paused state:", err)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ func (c *mockedConfig) Replace(cfg config.Configuration) (config.Waiter, error)
|
||||
|
||||
func (c *mockedConfig) Subscribe(cm config.Committer) {}
|
||||
|
||||
func (c *mockedConfig) Unsubscribe(cm config.Committer) {}
|
||||
|
||||
func (c *mockedConfig) Folders() map[string]config.FolderConfiguration {
|
||||
return nil
|
||||
}
|
||||
@@ -68,6 +70,58 @@ func (c *mockedConfig) RequiresRestart() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *mockedConfig) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) {}
|
||||
|
||||
func (c *mockedConfig) AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID) {}
|
||||
|
||||
func (m *mockedConfig) MyName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockedConfig) ConfigPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockedConfig) SetGUI(gui config.GUIConfiguration) (config.Waiter, error) {
|
||||
return noopWaiter{}, nil
|
||||
}
|
||||
|
||||
func (m *mockedConfig) SetOptions(opts config.OptionsConfiguration) (config.Waiter, error) {
|
||||
return noopWaiter{}, nil
|
||||
}
|
||||
|
||||
func (m *mockedConfig) Folder(id string) (config.FolderConfiguration, bool) {
|
||||
return config.FolderConfiguration{}, false
|
||||
}
|
||||
|
||||
func (m *mockedConfig) FolderList() []config.FolderConfiguration {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedConfig) SetFolder(fld config.FolderConfiguration) (config.Waiter, error) {
|
||||
return noopWaiter{}, nil
|
||||
}
|
||||
|
||||
func (m *mockedConfig) Device(id protocol.DeviceID) (config.DeviceConfiguration, bool) {
|
||||
return config.DeviceConfiguration{}, false
|
||||
}
|
||||
|
||||
func (m *mockedConfig) RemoveDevice(id protocol.DeviceID) (config.Waiter, error) {
|
||||
return noopWaiter{}, nil
|
||||
}
|
||||
|
||||
func (m *mockedConfig) IgnoredDevice(id protocol.DeviceID) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *mockedConfig) IgnoredFolder(device protocol.DeviceID, folder string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *mockedConfig) GlobalDiscoveryServers() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
type noopWaiter struct{}
|
||||
|
||||
func (noopWaiter) Wait() {}
|
||||
|
||||
@@ -15,3 +15,7 @@ func (m *mockedConnections) Status() map[string]interface{} {
|
||||
func (m *mockedConnections) NATType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *mockedConnections) Serve() {}
|
||||
|
||||
func (m *mockedConnections) Stop() {}
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/connections"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
@@ -150,3 +152,38 @@ func (m *mockedModel) WatchError(folder string) error {
|
||||
func (m *mockedModel) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) Serve() {}
|
||||
func (m *mockedModel) Stop() {}
|
||||
func (m *mockedModel) Index(deviceID protocol.DeviceID, folder string, files []protocol.FileInfo) {}
|
||||
func (m *mockedModel) IndexUpdate(deviceID protocol.DeviceID, folder string, files []protocol.FileInfo) {
|
||||
}
|
||||
|
||||
func (m *mockedModel) Request(deviceID protocol.DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (protocol.RequestResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) ClusterConfig(deviceID protocol.DeviceID, config protocol.ClusterConfig) {}
|
||||
|
||||
func (m *mockedModel) Closed(conn protocol.Connection, err error) {}
|
||||
|
||||
func (m *mockedModel) DownloadProgress(deviceID protocol.DeviceID, folder string, updates []protocol.FileDownloadProgressUpdate) {
|
||||
}
|
||||
|
||||
func (m *mockedModel) AddConnection(conn connections.Connection, hello protocol.HelloResult) {}
|
||||
|
||||
func (m *mockedModel) OnHello(protocol.DeviceID, net.Addr, protocol.HelloResult) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) GetHello(protocol.DeviceID) protocol.HelloIntf {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) AddFolder(cfg config.FolderConfiguration) {}
|
||||
|
||||
func (m *mockedModel) RestartFolder(from, to config.FolderConfiguration) {}
|
||||
|
||||
func (m *mockedModel) StartFolder(folder string) {}
|
||||
|
||||
func (m *mockedModel) StartDeadlockDetector(timeout time.Duration) {}
|
||||
|
||||
@@ -85,18 +85,18 @@ func monitorMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
l.Fatalln("stderr:", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
l.Fatalln("stdout:", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
l.Infoln("Starting syncthing")
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
stdoutMut.Lock()
|
||||
|
||||
@@ -9,7 +9,9 @@ package main
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/thejerf/suture"
|
||||
@@ -20,8 +22,8 @@ import (
|
||||
type folderSummaryService struct {
|
||||
*suture.Supervisor
|
||||
|
||||
cfg configIntf
|
||||
model modelIntf
|
||||
cfg config.Wrapper
|
||||
model model.Model
|
||||
stop chan struct{}
|
||||
immediate chan string
|
||||
|
||||
@@ -34,7 +36,7 @@ type folderSummaryService struct {
|
||||
lastEventReqMut sync.Mutex
|
||||
}
|
||||
|
||||
func newFolderSummaryService(cfg configIntf, m modelIntf) *folderSummaryService {
|
||||
func newFolderSummaryService(cfg config.Wrapper, m model.Model) *folderSummaryService {
|
||||
service := &folderSummaryService{
|
||||
Supervisor: suture.New("folderSummaryService", suture.Spec{
|
||||
PassThroughPanics: true,
|
||||
|
||||
@@ -37,7 +37,7 @@ const usageReportVersion = 3
|
||||
|
||||
// reportData returns the data to be sent in a usage report. It's used in
|
||||
// various places, so not part of the usageReportingManager object.
|
||||
func reportData(cfg configIntf, m modelIntf, connectionsService connectionsIntf, version int, preview bool) map[string]interface{} {
|
||||
func reportData(cfg config.Wrapper, m model.Model, connectionsService connections.Service, version int, preview bool) map[string]interface{} {
|
||||
opts := cfg.Options()
|
||||
res := make(map[string]interface{})
|
||||
res["urVersion"] = version
|
||||
@@ -323,16 +323,16 @@ func reportData(cfg configIntf, m modelIntf, connectionsService connectionsIntf,
|
||||
}
|
||||
|
||||
type usageReportingService struct {
|
||||
cfg *config.Wrapper
|
||||
model *model.Model
|
||||
connectionsService *connections.Service
|
||||
cfg config.Wrapper
|
||||
model model.Model
|
||||
connectionsService connections.Service
|
||||
forceRun chan struct{}
|
||||
stop chan struct{}
|
||||
stopped chan struct{}
|
||||
stopMut sync.RWMutex
|
||||
}
|
||||
|
||||
func newUsageReportingService(cfg *config.Wrapper, model *model.Model, connectionsService *connections.Service) *usageReportingService {
|
||||
func newUsageReportingService(cfg config.Wrapper, model model.Model, connectionsService connections.Service) *usageReportingService {
|
||||
svc := &usageReportingService{
|
||||
cfg: cfg,
|
||||
model: model,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Desktop Entry]
|
||||
Name=Syncthing Web UI
|
||||
GenericName=File synchronization UI
|
||||
Comment="Opens Syncthing's Web UI in the default browser (Syncthing must already be started)."
|
||||
Comment=Opens Syncthing's Web UI in the default browser (Syncthing must already be started).
|
||||
Exec=/usr/bin/syncthing -browser-only
|
||||
Icon=syncthing
|
||||
Terminal=false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[Unit]
|
||||
Description=Syncthing - Open Source Continuous File Synchronization for %I
|
||||
Documentation=man:syncthing(1)
|
||||
After=network.target
|
||||
After=multi-user.target network.target
|
||||
|
||||
[Service]
|
||||
User=%i
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
"Reduced by ignore patterns": "Намалено посредством шаблон за игнориране",
|
||||
"Release Notes": "Бележки по обновяването",
|
||||
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Кандидат версиите съдържат най-новата функционалност и поправки. Те са близки до традиционните дву-седмични Synchthing обновления.",
|
||||
"Remote Devices": "Чужди устройства",
|
||||
"Remote Devices": "Устройства",
|
||||
"Remove": "Премахни",
|
||||
"Remove Device": "Премахване на устройство",
|
||||
"Remove Folder": "Премахване на папка",
|
||||
@@ -300,7 +300,7 @@
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing има проблем при обработването на заявката. Моля, презаредете браузъра или рестартирайте Syncthing ако проблемът продължи.",
|
||||
"Take me back": "Take me back",
|
||||
"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 е разрешава дистанционен достъп без да изисква парола.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Администраторският панел на Syncthing разрешава дистанционен достъп без да изисква парола.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Обобщение на събраните статистически ще намерите на долния URL адрес.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурацията е запазена, но не е активирана. Syncthing трябва да рестартира, за да се активира новата конфигурация.",
|
||||
"The device ID cannot be blank.": "Полето идентификатор на устройство не може да бъде празно.",
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Работи от",
|
||||
"Usage reporting is always enabled for candidate releases.": "Докладът за ползването е винаги включен за кандидат нови версии.",
|
||||
"Use HTTPS for GUI": "Използвай HTTPS за потребителския интерфейс",
|
||||
"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.",
|
||||
"Version": "Версия",
|
||||
"Versions": "Версии",
|
||||
"Versions Path": "Път до версиите",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Предупреждение: Ако използвате външна програма за наблюдение като {{syncthingInotify}}, трябва да я деактивирате.",
|
||||
"Watch for Changes": "Мониторинг за промени",
|
||||
"Watching for Changes": "Мониторинг за промени",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Когато добавяте ново устройство имайте предвид, че това устройство също трябва да бъде добавено от другата страна.",
|
||||
"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.": "Когато добавяте нов идентификатор на папка имайте предвид, че той се използва за свързване на папките между отделните устройства. Идентификатора разграничава главни/малки букви.",
|
||||
"Yes": "Да",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Temps de funcionament",
|
||||
"Usage reporting is always enabled for candidate releases.": "Els informes d'ús sempre estan activats per a les versions candidates.",
|
||||
"Use HTTPS for GUI": "Utilitzar HTTPS per a l'Interfície Gràfica d'Usuari (GUI)",
|
||||
"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.",
|
||||
"Version": "Versió",
|
||||
"Versions": "Versions",
|
||||
"Versions Path": "Ruta de les versions",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "AVÍS: Si estàs utilitzant un observador extern com {{syncthingInotify}}, deus assegurar-te de que està desactivat.",
|
||||
"Watch for Changes": "Vigilar els Canvis",
|
||||
"Watching for Changes": "Vigilant els Canvis",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quant s'afig un nou dispositiu, hi ha que tindre en compte que aquest dispositiu deu ser afegit també en l'altre costat.",
|
||||
"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.": "Quant s'afig una nova carpeta, hi ha que tindre en compte que l'ID de la carpeta s'utilitza per a juntar les carpetes entre dispositius. Són sensibles a les majúscules i deuen coincidir exactament entre tots els dispositius.",
|
||||
"Yes": "Sí",
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"Connection Error": "Chyba připojení",
|
||||
"Connection Type": "Typ připojení",
|
||||
"Connections": "Připojení",
|
||||
"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.": "Syncthing nyní umožňuje nepřetržité sledování změn. To zachytí změny na disku na spustí sken pouze pro změněné cesty. Výhody jsou rychlejší propagace změn a méně plných skenů.",
|
||||
"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.": "Syncthing nyní umožňuje nepřetržité sledování změn. To zachytí změny na disku a spustí sken pouze pro změněné cesty. Výhody jsou rychlejší propagace změn a méně plných skenů.",
|
||||
"Copied from elsewhere": "Zkopírováno odjinud",
|
||||
"Copied from original": "Zkopírováno z originálu",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následující přispěvatelé:",
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Celkový čas běhu",
|
||||
"Usage reporting is always enabled for candidate releases.": "Hlášení o používání je pro kandidáty na vydání vždy zapnuto.",
|
||||
"Use HTTPS for GUI": "Použít HTTPS pro grafické rozhraní",
|
||||
"Use notifications from the filesystem to detect changed items.": "Použít oznamování soubor. systému pro nalezení změněných položek.",
|
||||
"Variable Size Blocks": "Bloky variabilní velikosti",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Bloky variabilní velikosti (také \"velké bloky\") jsou účinnější pro velké soubory.",
|
||||
"Version": "Verze",
|
||||
"Versions": "Verze",
|
||||
"Versions Path": "Cesta k verzím",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Pozor: Pokud používáte externí sledování změn jako {{syncthingInotify}}, měly byste se ujistit, že je toto sledování vypnuto.",
|
||||
"Watch for Changes": "Sledovat změny",
|
||||
"Watching for Changes": "Sledování změn",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Sledování změn odhalí většinu změn bez periodického skenování.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Při přidávání nového zařízení mějte na paměti, že je ho třeba také zadat na druhé straně.",
|
||||
"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.": "Při přidávání nového adresáře mějte na paměti, že jeho ID je použito ke svázání adresářů napříč zařízeními. Rozlišují se malá a velká písmena a musí přesně souhlasit mezi všemi zařízeními.",
|
||||
"Yes": "Ano",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"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.",
|
||||
"Version": "Version",
|
||||
"Versions": "Versioner",
|
||||
"Versions Path": "Versionssti",
|
||||
@@ -361,6 +364,7 @@
|
||||
"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.",
|
||||
"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",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Betriebszeit",
|
||||
"Usage reporting is always enabled for candidate releases.": "Nutzungsbericht ist für Veröffentlichungskandidaten immer aktiviert.",
|
||||
"Use HTTPS for GUI": "HTTPS für Benutzeroberfläche verwenden",
|
||||
"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.",
|
||||
"Version": "Version",
|
||||
"Versions": "Versionen",
|
||||
"Versions Path": "Versionierungspfad",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Achtung: Wenn Sie einen externen Beobachter wie {{syncthingInotify}} benutzen, sollten sie sicher sein das dieser deaktiviert ist.",
|
||||
"Watch for Changes": "Auf Änderungen achten",
|
||||
"Watching for Changes": "Auf Änderungen achten",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Gerätes, dass dieses Gerät auch auf den anderen Geräten hinzugefügt werden muss.",
|
||||
"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.": "Beachte bitte beim Hinzufügen eines neuen Ordners, dass die Ordnerkennung dazu verwendet wird, Ordner zwischen Geräten zu verbinden. Die Kennung muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
|
||||
"Yes": "Ja",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Χρόνος απρόσκοπτης λειτουργίας",
|
||||
"Usage reporting is always enabled for candidate releases.": "Η αποστολή αναφορών χρήσης είναι πάντα ενεργοποιημένη στις υποψήφιες εκδόσεις.",
|
||||
"Use HTTPS for GUI": "Χρήση HTTPS για τη διεπαφή",
|
||||
"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.",
|
||||
"Version": "Έκδοση",
|
||||
"Versions": "Εκδόσεις",
|
||||
"Versions Path": "Φάκελος τήρησης εκδόσεων",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Προσοχή: αν χρησιμοποιείτε ένα εξωτερικό εργαλείο επιτήρησης, όπως το {{syncthingInotify}}, σιγουρευτείτε ότι έχει απενεργοποιηθεί.",
|
||||
"Watch for Changes": "Επιτήρηση αλλαγών",
|
||||
"Watching for Changes": "Εκτελείται η επιτήρηση αλλαγών",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Θυμήσου πως όταν προσθέτεις μια νέα συσκευή, ετούτη η συσκευή θα πρέπει να προστεθεί και στην άλλη πλευρά.",
|
||||
"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.": "Όταν προσθέτεις έναν νέο φάκελο, θυμήσου πως η ταυτότητα ενός φακέλου χρησιμοποιείται για να να συσχετίσει φακέλους μεταξύ συσκευών. Η ταυτότητα του φακέλου θα πρέπει να είναι η ίδια σε όλες τις συσκευές και έχουν σημασία τα πεζά ή κεφαλαία γράμματα.",
|
||||
"Yes": "Ναι",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Uptime",
|
||||
"Usage reporting is always enabled for candidate releases.": "Usage reporting is always enabled for candidate releases.",
|
||||
"Use HTTPS for GUI": "Use HTTPS for GUI",
|
||||
"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.",
|
||||
"Version": "Version",
|
||||
"Versions": "Versions",
|
||||
"Versions Path": "Versions Path",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Warning: If you are using an external watcher like {{syncthingInotify}}, you should make sure it is deactivated.",
|
||||
"Watch for Changes": "Watch for Changes",
|
||||
"Watching for Changes": "Watching for Changes",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "When adding a new device, keep in mind that this device must be added on the other side too.",
|
||||
"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.": "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.",
|
||||
"Yes": "Yes",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Tiempo de funcionamiento",
|
||||
"Usage reporting is always enabled for candidate releases.": "El informe de uso está siempre habilitado en las versiones candidatas.",
|
||||
"Use HTTPS for GUI": "Usar HTTPS para la Interfaz Gráfica de Usuario (GUI)",
|
||||
"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.",
|
||||
"Version": "Versión",
|
||||
"Versions": "Versiones",
|
||||
"Versions Path": "Ruta de las versiones",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Advertencia: Si estás usando un vigilante externo como {{syncthingInotify}}, deberías asegurarte de que está desactivado.",
|
||||
"Watch for Changes": "Vigilar los cambios",
|
||||
"Watching for Changes": "Vigilando los cambios",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Cuando añada un nuevo dispositivo, tenga en cuenta que este debe añadirse también en el otro lado.",
|
||||
"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.": "Cuando añada una nueva carpeta, tenga en cuenta que su ID se usa para unir carpetas entre dispositivos. Son sensibles a las mayúsculas y deben coincidir exactamente entre todos los dispositivos.",
|
||||
"Yes": "Si",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Tiempo de funcionamiento",
|
||||
"Usage reporting is always enabled for candidate releases.": "El informe de uso está siempre habilitado en las versiones candidatas.",
|
||||
"Use HTTPS for GUI": "Usar HTTPS para la Interfaz Gráfica de Usuario (GUI)",
|
||||
"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.",
|
||||
"Version": "Versión",
|
||||
"Versions": "Versiones",
|
||||
"Versions Path": "Ruta de las versiones",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Advertencia: Si estás utilizando un observador externo como {{syncthingInotify}}, debes asegurarte de que está desactivado.",
|
||||
"Watch for Changes": "Vigila los cambios",
|
||||
"Watching for Changes": "Vigilando los cambios",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Cuando añada un nuevo dispositivo, tenga en cuenta que este debe añadirse también en el otro lado.",
|
||||
"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.": "Cuando añada una nueva carpeta, tenga en cuenta que su ID se usa para unir carpetas entre dispositivos. Son sensibles a las mayúsculas y deben coincidir exactamente entre todos los dispositivos.",
|
||||
"Yes": "Si",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Päälläoloaika",
|
||||
"Usage reporting is always enabled for candidate releases.": "Käytön raportointi on aina käytössä testiversioissa.",
|
||||
"Use HTTPS for GUI": "Käytä HTTPS:ää GUI:n kanssa",
|
||||
"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.",
|
||||
"Version": "Versio",
|
||||
"Versions": "Versiot",
|
||||
"Versions Path": "Versioiden polku",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Varoitus: jos käytät ulkopuolista tiedostojärjestelmän muutosten valvojaa, kuten {{syncthingInotify}} varmista, että se ei ole aktiivinen.",
|
||||
"Watch for Changes": "Seuraa muutoksia",
|
||||
"Watching for Changes": "Seuraa muutoksia",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Lisättäessä laitetta, muista että tämä laite tulee myös lisätä toiseen laitteeseen.",
|
||||
"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.": "Lisättäessä uutta kansiota, muista että kansion ID:tä käytetään solmimaan kansiot yhteen laitteiden välillä. Ne ovat riippuvaisia kirjankoosta ja niiden tulee täsmätä kaikkien laitteiden välillä.",
|
||||
"Yes": "Kyllä",
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
"Global Discovery": "Découverte globale",
|
||||
"Global Discovery Servers": "Serveurs de découverte globale",
|
||||
"Global State": "État global",
|
||||
"Help": "Aide",
|
||||
"Help": "Aide (anglais)",
|
||||
"Home page": "Page d'accueil",
|
||||
"Ignore": "Refuser",
|
||||
"Ignore Patterns": "Exclusions...",
|
||||
@@ -298,7 +298,7 @@
|
||||
"Syncthing is upgrading.": "Syncthing se met à jour.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être arrêté, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. Rafraîchissez la page (F5 sur PC) ou redémarrez Syncthing si le problème persiste.",
|
||||
"Take me back": "Reprends moi",
|
||||
"Take me back": "Vérifier ...",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "L'adresse de l'interface graphique est remplacée par une ou des options de lancement. Les modifications apportées ici ne seront pas effectives tant que ces options seront utilisées.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interface d'administration de Syncthing est paramétrée pour autoriser les accès à distance sans mot de passe !!!",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Les statistiques agrégées sont disponibles publiquement à l'adresse ci-dessous.",
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Durée de fonctionnement",
|
||||
"Usage reporting is always enabled for candidate releases.": "L'envoi des statistiques d'utilisation est obligatoirement actif pour les versions préliminaires.",
|
||||
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
|
||||
"Use notifications from the filesystem to detect changed items.": "Utiliser les notifications du système de fichiers pour détecter les éléments modifiés.",
|
||||
"Variable Size Blocks": "Blocs de taille variable",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Les blocs de taille variable (dont les \"gros blocs\") sont plus efficaces pour les gros fichiers.",
|
||||
"Version": "Version",
|
||||
"Versions": "Restauration...",
|
||||
"Versions Path": "Emplacement des versions",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Attention : si vous utilisez un système externe de surveillance tel que {{syncthingInotify}}, vous devez vous assurer qu'il est désactivé.",
|
||||
"Watch for Changes": "Écouter les changements",
|
||||
"Watching for Changes": "Surveillance des changements",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "La surveillance des changements découvre la plupart des changements sans réanalyses périodiques.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Lorsque vous ajoutez un appareil, gardez à l'esprit que le votre doit aussi être ajouté de l'autre coté.",
|
||||
"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.": "Lorsqu'un nouveau partage est ajouté, gardez à l'esprit que son ID est utilisée pour lier les répertoires à travers les appareils. L'ID est sensible à la casse et sera forcément la même sur tous les appareils participant à ce partage.",
|
||||
"Yes": "Oui",
|
||||
@@ -369,7 +373,7 @@
|
||||
"You can read more about the two release channels at the link below.": "Vous pouvez en savoir plus sur les deux canaux de distribution via le lien ci-dessous.",
|
||||
"You have no ignored devices.": "Aucun appareil refusé.",
|
||||
"You have no ignored folders.": "Aucun partage refusé.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Vous avez des réglages non enregistrés. Voulez-vous vraiment les ignorer ?",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Vous avez des réglages non enregistrés. Voulez-vous vraiment les rejeter ?",
|
||||
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
||||
"days": "Jours",
|
||||
"directories": "répertoires",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Rintiid",
|
||||
"Usage reporting is always enabled for candidate releases.": "Brûkersrapportaazje stiet altyd oan foar ferzje kandidaten.",
|
||||
"Use HTTPS for GUI": "Brûk HTTPS foar GUI",
|
||||
"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.",
|
||||
"Version": "Ferzje",
|
||||
"Versions": "Ferzjes",
|
||||
"Versions Path": "Ferzjes-paad",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Warskôging: As jo in eksterne sjogger lykas {{syncthingInotify}} brûke, bin der dan wiis fan dat dizze út stiet.",
|
||||
"Watch for Changes": "Sjoch foar Feroarings",
|
||||
"Watching for Changes": "Sjocht foar Feroarings",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Hâld by it taheakjen fan in nij apparaat yn de holle dat it apparaat oan de oare kant ek taheakke wurde moat. ",
|
||||
"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.": "Hâld by it taheakjen fan in nije map yn de holle dat de map-ID brûkt wurd om de mappen tusken apparaten mei-inoar te ferbinen. Se binne haadlettergefoelich en moatte oer alle apparaten eksakt oerienkomme.",
|
||||
"Yes": "Ja",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Üzemidő",
|
||||
"Usage reporting is always enabled for candidate releases.": "Az előzetes kiadásokban a használati jelentés mindig engedélyezett.",
|
||||
"Use HTTPS for GUI": "HTTPS használata a grafikus felülethez",
|
||||
"Use notifications from the filesystem to detect changed items.": "A fájlrendszer által szolgáltatott értesítések alkalmazása a megváltozott elemek keresésére.",
|
||||
"Variable Size Blocks": "Változó méretű blokkok",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "A változó mérető blokkok (ún. „nagy blokkok”) sokkal hatékonyabbak nagy fájloknál.",
|
||||
"Version": "Verzió",
|
||||
"Versions": "Verziók",
|
||||
"Versions Path": "Verziók útvonala",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Figyelem: Ha eddig külső megfigyelő volt erre a célra alkalmazva, mint pl. a(z) {{syncthingInotify}}, akkor előbb meg kell győződni arról, hogy az ki lett kapcsolva.",
|
||||
"Watch for Changes": "Változásfigyelés",
|
||||
"Watching for Changes": "Változások figyelése",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "A változások keresése felfedezi a legtöbb változást periodikus átnézés nélkül.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Új eszköz hozzáadásakor nem szabad elfeledkezni arról, hogy a másik oldalon ezt az eszközt is hozzá kell adni.",
|
||||
"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.": "Új eszköz hozzáadásakor észben kell tartani, hogy a mappaazonosító arra való, hogy összekösse a mappákat az eszközökön. Az azonosító kisbetű-nagybetű érzékeny és pontosan egyeznie kell az eszközökön.",
|
||||
"Yes": "Igen",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Tempo di Funzionamento",
|
||||
"Usage reporting is always enabled for candidate releases.": "Le segnalazioni di utilizzo sono sempre abilitate le versioni candidate al rilascio.",
|
||||
"Use HTTPS for GUI": "Utilizza HTTPS per l'interfaccia grafica",
|
||||
"Use notifications from the filesystem to detect changed items.": "Usa le notifiche dal filesystem per rilevare gli elementi modificati.",
|
||||
"Variable Size Blocks": "Blocchi di dimensioni variabile",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "I blocchi di dimensioni variabile (anche \"blocchi di grandi dimensioni\") sono più efficienti per file di grandi dimensioni.",
|
||||
"Version": "Versione",
|
||||
"Versions": "Versioni",
|
||||
"Versions Path": "Percorso Cartella Versioni",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Attenzione: se stai usando uno strumento esterno di monitoraggio come {{syncthingInotify}}, dovresti assicurarti che sia disattivato.",
|
||||
"Watch for Changes": "Monitorare i cambiamenti",
|
||||
"Watching for Changes": "Monitoraggio dei cambiamenti",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Guardando le modifiche si scopre la maggior parte delle modifiche senza scansione periodica.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando si aggiunge un nuovo dispositivo, tenere presente che il dispositivo deve essere aggiunto anche dall'altra parte.",
|
||||
"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.": "Quando aggiungi una nuova cartella, ricordati che gli ID vengono utilizzati per collegare le cartelle nei dispositivi. Distinguono maiuscole e minuscole e devono corrispondere esattamente su tutti i dispositivi.",
|
||||
"Yes": "Sì",
|
||||
|
||||
@@ -58,8 +58,8 @@
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 the following Contributors:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "無視パターンを作成中。既存のファイルが {{path}} にある場合は上書きされます。",
|
||||
"Danger!": "危険!",
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
"Default Folder Path": "Default Folder Path",
|
||||
"Debugging Facilities": "デバッグ機能",
|
||||
"Default Folder Path": "デフォルトのフォルダーパス",
|
||||
"Deleted": "削除",
|
||||
"Deselect All": "すべて選択解除",
|
||||
"Device": "デバイス",
|
||||
@@ -202,7 +202,7 @@
|
||||
"Override Changes": "他のデバイスの変更を上書きする",
|
||||
"Path": "パス",
|
||||
"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": "ローカルコンピュータ上のフォルダーパス。フォルダーが存在しない場合は作成されます。チルダ (~) で以下のフォルダーを短縮入力できます:",
|
||||
"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%}.": "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}}.",
|
||||
"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%}.": "新しい自動承諾フォルダーが作成されるパス、およびUIを使用して新しいフォルダーを追加するときにデフォルトでサジェストされるパスです。チルダ文字(~)の場合は {{tilde}} に展開されます。",
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "古いバージョンを保存するパス (空欄の場合、デフォルトで共有フォルダー内の .stversions ディレクトリ)",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "古いバージョンを保存するパス (空欄の場合、デフォルトでフォルダー内の .stversions フォルダー)",
|
||||
"Pause": "一時停止",
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "稼働時間",
|
||||
"Usage reporting is always enabled for candidate releases.": "リリース候補版では常に使用状況レポートが送信されます。",
|
||||
"Use HTTPS for GUI": "GUIにHTTPSを使用する",
|
||||
"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.",
|
||||
"Version": "バージョン",
|
||||
"Versions": "Versions",
|
||||
"Versions Path": "古いバージョンを保存するパス",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Warning: If you are using an external watcher like {{syncthingInotify}}, you should make sure it is deactivated.",
|
||||
"Watch for Changes": "変更の監視",
|
||||
"Watching for Changes": "変更の監視",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "新しいデバイスを追加する際は、相手側デバイスにもこのデバイスを追加してください。",
|
||||
"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.": "新しいフォルダーを追加する際、フォルダーIDはデバイス間でフォルダーの対応づけに使われることに注意してください。フォルダーIDは大文字と小文字が区別され、共有するすべてのデバイスの間で完全に一致しなくてはなりません。",
|
||||
"Yes": "はい",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "가동 시간",
|
||||
"Usage reporting is always enabled for candidate releases.": "출시 후보 버전에서는 사용 보고서가 항상 활성화 됩니다.",
|
||||
"Use HTTPS for GUI": "GUI에서 HTTPS 프로토콜 사용",
|
||||
"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.",
|
||||
"Version": "버전",
|
||||
"Versions": "버전",
|
||||
"Versions Path": "버전 저장 경로",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "경고 : {{syncthingInotify}} 와 같은 외부 감시 도구를 사용하는 경우 비활성화되어 있는지 확인해야 합니다.",
|
||||
"Watch for Changes": "변경 사항 감시",
|
||||
"Watching for Changes": "변경 사항 감시",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "새 장치를 추가할 시 추가한 기기 쪽에서도 이 장치를 추가해야 합니다.",
|
||||
"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.": "새 폴더를 추가할 시 폴더 ID는 장치간에 폴더를 묶을 때 사용됩니다. 대소문자를 구분하며 모든 장치에서 같은 ID를 사용해야 합니다.",
|
||||
"Yes": "예",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Veiksnumo laikas",
|
||||
"Usage reporting is always enabled for candidate releases.": "Naudojimo ataskaita kandidatinėms versijoms visada yra įjungta.",
|
||||
"Use HTTPS for GUI": "Valdymo skydeliui naudoti saugų ryšį ",
|
||||
"Use notifications from the filesystem to detect changed items.": "Naudoti pranešimus iš failų sistemos, norint aptikti pakeistus elementus.",
|
||||
"Variable Size Blocks": "Kintamųjų dydžių blokai",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Kintamųjų dydžių blokai (taip pat \"dideli blokai\") yra efektyvesni dideliems failams.",
|
||||
"Version": "Versija",
|
||||
"Versions": "Versijos",
|
||||
"Versions Path": "Kelias iki versijos",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Įspėjimas: Jeigu naudojate išorinį stebėtoją, tokį kaip {{syncthingInotify}}, tuomet turite įsitikinti, kad jis yra išjungtas.",
|
||||
"Watch for Changes": "Stebėti pakeitimus",
|
||||
"Watching for Changes": "Stebimi pakeitimai",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Pakeitimų stebėjimas aptinka daugumą pakeitimų be periodinio nuskaitymo.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Pridėdami įrenginį, turėkite omeny, kad šis įrenginys taip pat turi būti pridėtas kitoje pusėje.",
|
||||
"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.": "Kai įvedate naują aplanką neužmirškite, kad jis bus naudojamas visuose įrenginiuose. Svarbu visur įvesti visiškai tokį pat aplanko vardą neužmirštant apie didžiąsias ir mažąsias raides.",
|
||||
"Yes": "Taip",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Oppetid",
|
||||
"Usage reporting is always enabled for candidate releases.": "Bruksrapportering er alltid påslått for utgivelseskandidater",
|
||||
"Use HTTPS for GUI": "Bruk HTTPS for GUI",
|
||||
"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.",
|
||||
"Version": "Versjon",
|
||||
"Versions": "Versjoner",
|
||||
"Versions Path": "Plassering av versjoner",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Advarsel: Hvis du bruker en ekstern viser, som {{syncthingInotify}}, burde du forsikre deg om at den avslått.",
|
||||
"Watch for Changes": "Hold oppsyn med endringer",
|
||||
"Watching for Changes": "Holder oppsyn med endringer",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Når du legger til en ny enhet, husk at enheten må legges til på andre siden også.",
|
||||
"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 en ny mappe blir lagt til, husk at Mappe-ID blir brukt til å binde sammen mapper mellom enheter. Det er forskjell på store og små bokstaver, så IDene må være identiske på alle enhetene.",
|
||||
"Yes": "Ja",
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"Connection Type": "Soort verbinding",
|
||||
"Connections": "Verbindingen",
|
||||
"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.": "Continu opvolgen van wijzigingen is nu beschikbaar in Syncthing. Dit zal wijzigingen op een schijf detecteren en alleen een scan uitvoeren op de gewijzigde paden. De voordelen zijn dat wijzigingen sneller doorgevoerd worden en dat minder volledige scans nodig zijn.",
|
||||
"Copied from elsewhere": "Gekopieerd van ergens anders",
|
||||
"Copied from original": "Gekopieerd van het origineel",
|
||||
"Copied from elsewhere": "Gekopieerd van elders",
|
||||
"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:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Negeerpatronen worden aangemaakt, bestaand bestand wordt overschreven op {{path}}.",
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Bedrijfstijd",
|
||||
"Usage reporting is always enabled for candidate releases.": "Gebruiksrapportering is altijd ingeschakeld voor de kandidaat-releases.",
|
||||
"Use HTTPS for GUI": "HTTPS gebruiken voor GUI",
|
||||
"Use notifications from the filesystem to detect changed items.": "Meldingen van het bestandssysteem gebruiken om gewijzigde items te detecteren.",
|
||||
"Variable Size Blocks": "Blokken met variabele grootte",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Blokken met variabele grootte (ook \"grote blokken\") zijn efficiënter voor grote bestanden.",
|
||||
"Version": "Versie",
|
||||
"Versions": "Versies",
|
||||
"Versions Path": "Pad voor versies",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Waarschuwing: als u een externe opvolger gebruikt zoals {{syncthingInotify}}, moet u zich ervan verzekeren dat die uitgeschakeld is.",
|
||||
"Watch for Changes": "Wijzigingen opvolgen",
|
||||
"Watching for Changes": "Wijzigingen opvolgen",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Opvolgen van wijzigingen ontdekt de meeste wijzigingen zonder periodiek scannen.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Houd er bij het toevoegen van een nieuw apparaat rekening mee dat dit apparaat ook aan de andere kant moet toegevoegd worden.",
|
||||
"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.": "Houd er bij het toevoegen van een nieuwe map rekening mee dat de map-ID gebruikt wordt om mappen aan elkaar te koppelen tussen apparaten. Ze zijn hoofdlettergevoelig en moeten exact overeenkomen op alle apparaten.",
|
||||
"Yes": "Ja",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Czas działania",
|
||||
"Usage reporting is always enabled for candidate releases.": "Raportowanie użycia dla wydań kandydujących jest zawsze włączone.",
|
||||
"Use HTTPS for GUI": "Używaj HTTPS",
|
||||
"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.",
|
||||
"Version": "Wersja",
|
||||
"Versions": "Wersje",
|
||||
"Versions Path": "Ścieżka wersji",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Uwaga: Jeśli korzystasz z zewnętrznego obserwatora takiego jak {{syncthingInotify}}, upewnij się, że jest on dezaktywowany.",
|
||||
"Watch for Changes": "Obserwuj zmiany",
|
||||
"Watching for Changes": "Obserwowanie zmian",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Gdy dodajesz nowe urządzenie, pamiętaj że urządzenie musi zostać dodane także po drugiej stronie.",
|
||||
"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.": "Przy dodawaniu nowego folderu, pamiętaj, że ID użyte jest do łączenia folderów pomiędzy urządzeniami. Wielkość liter ciągu ma znaczenie musi zgadzać się na wszystkich urządzeniach.",
|
||||
"Yes": "Tak",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Tempo ligado",
|
||||
"Usage reporting is always enabled for candidate releases.": "O relatório de uso está sempre habilitado em versões candidatas ao lançamento",
|
||||
"Use HTTPS for GUI": "Usar HTTPS para a interface web",
|
||||
"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.",
|
||||
"Version": "Versão",
|
||||
"Versions": "Versões",
|
||||
"Versions Path": "Caminho do versionamento",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Warning: If you are using an external watcher like {{syncthingInotify}}, you should make sure it is deactivated.",
|
||||
"Watch for Changes": "Observar alterações",
|
||||
"Watching for Changes": "Observando alterações",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando estiver adicionando um dispositivo, lembre-se de que este dispositivo deve ser adicionado do outro lado também.",
|
||||
"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.": "Quando adicionar uma nova pasta, lembre-se que o ID da pasta é utilizado para ligar pastas entre dispositivos. Ele é sensível às diferenças entre maiúsculas e minúsculas e deve ser o mesmo em todos os dispositivos.",
|
||||
"Yes": "Sim",
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
"Running": "Em execução",
|
||||
"Save": "Gravar",
|
||||
"Scan Time Remaining": "Tempo restante da verificação",
|
||||
"Scanning": "Verificando",
|
||||
"Scanning": "Verificação de alterações",
|
||||
"See external versioner help for supported templated command line parameters.": "Veja a ajuda do gestor de versões externo para saber que parâmetros da linha de comandos são suportados.",
|
||||
"See external versioning help for supported templated command line parameters.": "Veja a ajuda externa sobre gestão de versões para ver os modelos suportados de parâmetros para a linha de comandos.",
|
||||
"Select All": "Seleccionar tudo",
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Tempo em actividade",
|
||||
"Usage reporting is always enabled for candidate releases.": "O relatório de utilização está sempre activado nas versões candidatas a lançamento.",
|
||||
"Use HTTPS for GUI": "Utilizar HTTPS na interface gráfica",
|
||||
"Use notifications from the filesystem to detect changed items.": "Use notificações do sistema de ficheiros para detectar itens alterados.",
|
||||
"Variable Size Blocks": "Blocos de tamanho variável",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Blocos de tamanho variável (também designados por \"blocos grandes\") são mais eficientes para ficheiros grandes.",
|
||||
"Version": "Versão",
|
||||
"Versions": "Versões",
|
||||
"Versions Path": "Caminho das versões",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Aviso: Se estiver a usar um verificador externo, tal como o {{syncthingInotify}}, deve certificar-se que está desactivado.",
|
||||
"Watch for Changes": "Vigiar alterações",
|
||||
"Watching for Changes": "Vigilância de alterações",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "A vigilância de alterações encontra a maior parte das alterações sem a verificação periódica.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando adicionar um novo dispositivo, lembre-se que este dispositivo tem que ser adicionado do outro lado também.",
|
||||
"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.": "Quando adicionar uma nova pasta, lembre-se que o ID da pasta é utilizado para ligar as pastas entre dispositivos. É sensível às diferenças entre maiúsculas e minúsculas e tem que ter uma correspondência perfeita entre todos os dispositivos.",
|
||||
"Yes": "Sim",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"Add Device": "Добавить устройство",
|
||||
"Add Folder": "Добавить папку",
|
||||
"Add Remote Device": "Добавить удалённое устройство",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Add devices from the introducer to our device list, for mutually shared folders.",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Добавлять устройства, известные рекомендателю, в список устройств, если есть общие с ними папки.",
|
||||
"Add new folder?": "Добавить новую папку?",
|
||||
"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.": "Также будет увеличен интервал полного сканирования (в 60 раз, т.е. новое значение по умолчанию 1 час). Вы можете вручную настроить интервал для каждой папки, выбрав \"Нет\".",
|
||||
"Address": "Адрес",
|
||||
@@ -23,7 +23,7 @@
|
||||
"Allowed Networks": "Разрешённые сети",
|
||||
"Alphabetic": "По алфавиту",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder.": "Для версионирования используется внешняя программа. Ей нужно удалить файл из общей папки.",
|
||||
"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.": "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.",
|
||||
"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.": "Для версионирования используется внешняя программа. Ей нужно удалить файл из общей папки. Если путь к приложению содержит пробелы, его нужно взять в кавычки.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Внешний процесс управляет версиями файлов. Процесс удалит файл из синхронизируемой папки.",
|
||||
"Anonymous Usage Reporting": "Анонимный отчет об использовании",
|
||||
"Anonymous usage report format has changed. Would you like to move to the new format?": "Формат анонимных отчётов изменился. Вы хотите переключиться на новый формат?",
|
||||
@@ -34,8 +34,8 @@
|
||||
"Auto Accept": "Автопринятие",
|
||||
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Автоматическое обновление теперь предлагает выбор между стабильными выпусками и кандидатами в релизы.",
|
||||
"Automatic upgrades": "Автообновление",
|
||||
"Automatic upgrades are always enabled for candidate releases.": "Automatic upgrades are always enabled for candidate releases.",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
|
||||
"Automatic upgrades are always enabled for candidate releases.": "Автоматическое обновление всегда включено для кандидатов в релизы.",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Автоматически создавать, используя путь по умолчанию, или делиться папками, о которых сообщает это устройство.",
|
||||
"Available debug logging facilities:": "Доступные средства отладочного журнала:",
|
||||
"Be careful!": "Будьте осторожны!",
|
||||
"Bugs": "Ошибки",
|
||||
@@ -56,12 +56,12 @@
|
||||
"Copied from original": "Скопировано с оригинала",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Авторские права © 2014–2016 принадлежат:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Авторские права © 2014—2017 следующие участники:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creating ignore patterns, overwriting an existing file at {{path}}.",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Создание шаблонов игнорирования, существующий файл {{path}} будет перезаписан.",
|
||||
"Danger!": "Опасно!",
|
||||
"Debugging Facilities": "Средства отладки",
|
||||
"Default Folder Path": "Путь для папок",
|
||||
"Deleted": "Удалено",
|
||||
"Deselect All": "Deselect All",
|
||||
"Deselect All": "Снять выделение",
|
||||
"Device": "Устройство",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство «{{name}}» ({{device}} на {{address}}) хочет подключиться. Добавить новое устройство?",
|
||||
"Device ID": "ID устройства",
|
||||
@@ -73,8 +73,8 @@
|
||||
"Disabled": "Отключено",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Периодическое сканирование и отслеживание изменений отключено",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Периодическое сканирование отключено, отслеживание изменений включено",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
|
||||
"Discard": "Discard",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Периодическое сканирование отключено, не удалось включить отслеживание изменений, повторная попытка каждую минуту.",
|
||||
"Discard": "Отменить",
|
||||
"Disconnected": "Нет соединения",
|
||||
"Discovered": "Обнаружено",
|
||||
"Discovery": "Обнаружение",
|
||||
@@ -97,7 +97,7 @@
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Введите положительное значение (например, \"2.35\") и выберите единицу измерения, либо процент от общей ёмкости диска.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Введите непривилегированный порт (1024—65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Введите через запятую («tcp://ip:port», «tcp://host:port») адреса, либо «dynamic», чтобы выполнить автоматическое обнаружение адреса.",
|
||||
"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.": "Введите через запятую («tcp://ip:port», «tcp://host:port») адреса, либо «dynamic», чтобы выполнить автоматическое обнаружение адреса.",
|
||||
"Enter ignore patterns, one per line.": "Введите шаблоны игнорирования, по одному на строку.",
|
||||
"Error": "Ошибка",
|
||||
"External File Versioning": "Внешний контроль версий файлов",
|
||||
@@ -115,7 +115,7 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файлы защищены от изменений сделанных на других устройствах, но изменения сделанные на этом устройстве будут отправлены всему кластеру.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Файлы синхронизируются из группы, но изменения, сделанные на этом устройстве, не будут отправлены на другие устройства группы.",
|
||||
"Filesystem Notifications": "Уведомления файловой системы",
|
||||
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
|
||||
"Filesystem Watcher Errors": "Ошибки отслеживания файловой системы",
|
||||
"Filter by date": "Отфильтровать по дате",
|
||||
"Filter by name": "Отфильтровать по имени",
|
||||
"Folder": "Папка",
|
||||
@@ -124,7 +124,7 @@
|
||||
"Folder Path": "Путь к папке",
|
||||
"Folder Type": "Тип папки",
|
||||
"Folders": "Папки",
|
||||
"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.": "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.",
|
||||
"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.": "Для следующих папок произошла ошибка при запуске отслеживания изменений. Попытки будут повторяться раз в минуту, и ошибки скоро могут быть устранены. Если этого не произойдёт, попробуйте разобраться в причинах и попросите поддержки, если у вас не получится.",
|
||||
"Full Rescan Interval (s)": "Интервал полного сканирования (в секундах)",
|
||||
"GUI": "Интерфейс",
|
||||
"GUI Authentication Password": "Пароль для доступа к панели управления",
|
||||
@@ -159,14 +159,14 @@
|
||||
"Later": "Позже",
|
||||
"Latest Change": "Последнее изменение",
|
||||
"Learn more": "Узнать больше",
|
||||
"Limit": "Limit",
|
||||
"Limit": "Ограничение",
|
||||
"Listeners": "Прослушиватель",
|
||||
"Loading data...": "Загрузка данных...",
|
||||
"Loading...": "Загрузка...",
|
||||
"Local Discovery": "Локальное обнаружение",
|
||||
"Local State": "Локальное состояние",
|
||||
"Local State (Total)": "Локальное состояние (всего)",
|
||||
"Locally Changed Items": "Locally Changed Items",
|
||||
"Locally Changed Items": "Объекты, изменённые на этом компьютере",
|
||||
"Log": "Журнал",
|
||||
"Log tailing paused. Click here to continue.": "Вывод журнала приостановлен. Чтобы продолжить, нажмите здесь.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Вывод журнала приостановлен. Чтобы продолжить, прокрутите журнал до конца.",
|
||||
@@ -177,8 +177,8 @@
|
||||
"Maximum Age": "Максимальный срок",
|
||||
"Metadata Only": "Только метаданные",
|
||||
"Minimum Free Disk Space": "Минимальное свободное место на диске",
|
||||
"Mod. Device": "Mod. Device",
|
||||
"Mod. Time": "Mod. Time",
|
||||
"Mod. Device": "Изм. устройство",
|
||||
"Mod. Time": "Посл. изм.",
|
||||
"Move to top of queue": "Поместить в начало очереди",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Многоуровневая маска (поиск совпадений во всех подпапках)",
|
||||
"Never": "Никогда",
|
||||
@@ -208,16 +208,16 @@
|
||||
"Pause": "Пауза",
|
||||
"Pause All": "Приостановить все",
|
||||
"Paused": "Приостановлено",
|
||||
"Pending changes": "Pending changes",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:",
|
||||
"Pending changes": "Несохранённые изменения",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Периодическое сканирование с заданным интервалом, отслеживание изменений отключено",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Периодическое сканирование с заданным интервалом и включено отслеживание изменений",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Периодическое сканирование с заданным интервалом, не удалось включить отслеживание изменений, повторная попытка каждую минуту.",
|
||||
"Permissions": "Разрешения",
|
||||
"Please consult the release notes before performing a major upgrade.": "Перед проведением обновления основной версии ознакомтесь, пожалуйста, с Замечаниями к версии",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Установите имя пользователя и пароль для интерфейса в настройках",
|
||||
"Please wait": "Пожалуйста, подождите",
|
||||
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix indicating that the file can be deleted if preventing directory removal",
|
||||
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefix indicating that the pattern should be matched without case sensitivity",
|
||||
"Prefix indicating that the file can be deleted if preventing directory removal": "Префикс указывает, что файл может быть удалён, если он мешает удалить папку",
|
||||
"Prefix indicating that the pattern should be matched without case sensitivity": "Префикс указывает, что регистр букв (заглавные/строчные) игнорируется",
|
||||
"Preview": "Предварительный просмотр",
|
||||
"Preview Usage Report": "Посмотреть отчёт об использовании",
|
||||
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
|
||||
@@ -245,14 +245,14 @@
|
||||
"Resume": "Возобновить",
|
||||
"Resume All": "Возобновить все",
|
||||
"Reused": "Повторно использовано",
|
||||
"Revert Local Changes": "Revert Local Changes",
|
||||
"Revert Local Changes": "Отменить изменения на этом компьютере",
|
||||
"Running": "Запущено",
|
||||
"Save": "Сохранить",
|
||||
"Scan Time Remaining": "Оставшееся время сканирования",
|
||||
"Scanning": "Сканирование",
|
||||
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
|
||||
"Select All": "Select All",
|
||||
"See external versioner help for supported templated command line parameters.": "Поддерживаемые шаблонные параметры командной строки см. в документации сторонней программы контроля версий",
|
||||
"See external versioning help for supported templated command line parameters.": "Поддерживаемые шаблонные параметры командной строки см. в документации сторонней программы контроля версий",
|
||||
"Select All": "Выбрать все",
|
||||
"Select a version": "Выберите версию",
|
||||
"Select latest version": "Выбрать последнюю версию",
|
||||
"Select oldest version": "Выбрать самую старую версию",
|
||||
@@ -267,7 +267,7 @@
|
||||
"Share With Devices": "Предоставить доступ устройствам",
|
||||
"Share this folder?": "Предоставить доступ к этой папке?",
|
||||
"Shared With": "Доступ предоставлен",
|
||||
"Sharing": "Sharing",
|
||||
"Sharing": "Предоставление доступа",
|
||||
"Show ID": "Показать ID",
|
||||
"Show QR": "Показать QR-код",
|
||||
"Show diff with previous version": "Показать различия с предыдущей версией",
|
||||
@@ -289,7 +289,7 @@
|
||||
"Statistics": "Статистика",
|
||||
"Stopped": "Остановлено",
|
||||
"Support": "Поддержка",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Support Bundle": "Данные для поддержки",
|
||||
"Sync Protocol Listen Addresses": "Адрес протокола синхронизации",
|
||||
"Syncing": "Синхронизация",
|
||||
"Syncthing has been shut down.": "Syncthing был выключен.",
|
||||
@@ -298,7 +298,7 @@
|
||||
"Syncthing is upgrading.": "Обновление Syncthing.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing столкнулся с проблемой при обработке Вашего запроса. Пожалуйста, обновите страницу или перезапустите Syncthing если проблема повторится.",
|
||||
"Take me back": "Take me back",
|
||||
"Take me back": "Вернуться к редактированию",
|
||||
"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 настроен для предоставления удаленного доступа без пароля.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Агрегированные статистические данные общедоступны по ссылке ниже.",
|
||||
@@ -313,7 +313,7 @@
|
||||
"The folder path cannot be blank.": "Путь к папке не должен быть пустым.",
|
||||
"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.": "Используются следующие интервалы: в первый час версия меняется каждые 30 секунд, в первый день - каждый час, первые 30 дней - каждый день, после, до максимального срока - каждую неделю.",
|
||||
"The following items could not be synchronized.": "Невозможно синхронизировать следующие объекты",
|
||||
"The following items were changed locally.": "The following items were changed locally.",
|
||||
"The following items were changed locally.": "Следующие объекты были изменены локально",
|
||||
"The maximum age must be a number and cannot be blank.": "Максимальный срок должен быть числом и не может быть пустым.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максимальный срок хранения версии (в днях, 0 значит вечное хранение).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Минимальное свободное место на диске должно быть в процентах между 0 и 100 включительно.",
|
||||
@@ -334,8 +334,8 @@
|
||||
"Trash Can File Versioning": "Использовать версионность для файлов в Корзине",
|
||||
"Type": "Тип",
|
||||
"Unavailable": "Недоступно",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
|
||||
"Undecided (will prompt)": "Undecided (will prompt)",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Недоступно или отключено администратором",
|
||||
"Undecided (will prompt)": "Запрос каждый раз",
|
||||
"Unignore": "Не игнорировать",
|
||||
"Unknown": "Неизвестно",
|
||||
"Unshared": "Необщедоступно",
|
||||
@@ -349,18 +349,22 @@
|
||||
"Uptime": "Время работы",
|
||||
"Usage reporting is always enabled for candidate releases.": "Отправка отчётов об использовании всегда включена для кандидатов в релизы.",
|
||||
"Use HTTPS for GUI": "Использовать HTTPS для панели управления",
|
||||
"Use notifications from the filesystem to detect changed items.": "Использовать уведомления от файловой системы для обнаружения изменённых объектов.",
|
||||
"Variable Size Blocks": "Блоки переменного размера",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Блоки переменного размера (или \"большие блоки\") более эффективны для файлов большого размера.",
|
||||
"Version": "Версия",
|
||||
"Versions": "Версии",
|
||||
"Versions Path": "Путь к версиям",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версии удаляются автоматически, если они существуют дольше максимального срока или превышают разрешённое количество файлов за интервал.",
|
||||
"Waiting to scan": "Waiting to scan",
|
||||
"Waiting to scan": "Ожидание сканирования",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Внимание! Этот путь — родительская директория уже существующей папки «{{otherFolder}}».",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Внимание! Этот путь — родительская директория уже существующей папки «{{otherFolderLabel}}» ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Осторожно, этот путь является подкаталогом существующей папки «{{otherFolder}}».",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Внимание! Этот путь — поддиректория уже существующей папки «{{otherFolderLabel}}» ({{otherFolder}}).",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Warning: If you are using an external watcher like {{syncthingInotify}}, you should make sure it is deactivated.",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Внимание! Если вы используете внешнюю программу для отслеживания изменений, например, {{syncthingInotify}}, убедитесь, что она отключена.",
|
||||
"Watch for Changes": "Следить за изменениями",
|
||||
"Watching for Changes": "Слежение за изменениями",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Отслеживание изменений обнаруживает большинство изменений без периодического сканирования.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Когда добавляете устройство, помните о том, что это же устройство должно быть добавлено и другой стороной.",
|
||||
"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.": "Когда добавляете новую папку, помните, что ID папок используются для того, чтобы связывать папки между всеми устройствами. Они чувствительны к регистру и должны совпадать на всех используемых устройствах.",
|
||||
"Yes": "Да",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Doba prevádzky",
|
||||
"Usage reporting is always enabled for candidate releases.": "Hlásenia o používaní sú pri kandidátoch na vydanie vždy povolené.",
|
||||
"Use HTTPS for GUI": "Použiť HTTPS pre grafické rozhranie",
|
||||
"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.",
|
||||
"Version": "Verzia",
|
||||
"Versions": "Verzie",
|
||||
"Versions Path": "Cesta k verziám",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Warning: If you are using an external watcher like {{syncthingInotify}}, you should make sure it is deactivated.",
|
||||
"Watch for Changes": "Watch for Changes",
|
||||
"Watching for Changes": "Watching for Changes",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "When adding a new device, keep in mind that this device must be added on the other side too.",
|
||||
"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.": "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.",
|
||||
"Yes": "Áno",
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
"Pending changes": "Väntar på ändringar",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodisk uppdatering i givet intervall och inaktiverad spaning efter ändringar",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodisk uppdatering i givet intervall och aktiverad spaning efter ändringar",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk uppdatering i givet intervall och misslyckades med att ställa in spaning efter ändringar, försök igen var 1m:",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk uppdatering i givet intervall och misslyckades med att ställa in utkik efter ändringar, försök igen var 1m:",
|
||||
"Permissions": "Behörigheter",
|
||||
"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.",
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Drifttid",
|
||||
"Usage reporting is always enabled for candidate releases.": "Användningsrapportering är alltid aktiverad för kandidatutgåvor.",
|
||||
"Use HTTPS for GUI": "Använd HTTPS för gränssnittet",
|
||||
"Use notifications from the filesystem to detect changed items.": "Använd aviseringar från filsystemet för att upptäcka ändrade objekt.",
|
||||
"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.",
|
||||
"Version": "Version",
|
||||
"Versions": "Versioner",
|
||||
"Versions Path": "Sökväg för versioner",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Varning: Om du använder en extern bevakare som {{syncthingInotify}}, bör du se till att den är inaktiverad.",
|
||||
"Watch for Changes": "Håll utkik efter ändringar",
|
||||
"Watching for Changes": "Håller utkik efter ändringar",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Hålla utkik efter ändringar upptäcker de flesta förändringar utan periodisk uppdatering.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att den här enheten måste läggas till på den andra enheten också.",
|
||||
"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 du lägger till ny mapp, tänk på att mapp-ID knyter ihop mappar mellan olika enheter. De skiftlägeskänsliga och måste matcha precis mellan alla enheter.",
|
||||
"Yes": "Ja",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "Тривалість роботи",
|
||||
"Usage reporting is always enabled for candidate releases.": "Звіти про користування завжди увімкнені для реліз-кандидатів.",
|
||||
"Use HTTPS for GUI": "Використовувати HTTPS для доступу до панелі управління",
|
||||
"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.",
|
||||
"Version": "Версія",
|
||||
"Versions": "Версії",
|
||||
"Versions Path": "Шлях до версій",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Warning: If you are using an external watcher like {{syncthingInotify}}, you should make sure it is deactivated.",
|
||||
"Watch for Changes": "Моніторити зміни",
|
||||
"Watching for Changes": "Моніторинг щмін",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Коли додаєте новий вузол, пам’ятайте, що цей вузол повинен бути доданий і на іншій стороні.",
|
||||
"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.": "Коли додаєте нову директорію, пам’ятайте, що ID цієї директорії використовується для того, щоб зв’язувати директорії разом між пристроями. Назви повинні точно співпадати між усіма пристроями, регістр символів має значення.",
|
||||
"Yes": "Так",
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
"Statistics": "统计",
|
||||
"Stopped": "已停止",
|
||||
"Support": "支持",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Support Bundle": "支持捆绑包",
|
||||
"Sync Protocol Listen Addresses": "协议监听地址",
|
||||
"Syncing": "同步中",
|
||||
"Syncthing has been shut down.": "Syncthing 已关闭。",
|
||||
@@ -336,7 +336,7 @@
|
||||
"Unavailable": "无效",
|
||||
"Unavailable/Disabled by administrator or maintainer": "无效/禁用(由管理员或维护者)",
|
||||
"Undecided (will prompt)": "待定(将提示)",
|
||||
"Unignore": "屏蔽",
|
||||
"Unignore": "解除忽略",
|
||||
"Unknown": "未知",
|
||||
"Unshared": "未共享",
|
||||
"Unused": "未使用",
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "已启动",
|
||||
"Usage reporting is always enabled for candidate releases.": "发布候选版总是会启用使用报告。",
|
||||
"Use HTTPS for GUI": "使用加密连接到图形管理页面",
|
||||
"Use notifications from the filesystem to detect changed items.": "使用文件系统的通知来检测更改的项目。",
|
||||
"Variable Size Blocks": "可变大小块",
|
||||
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "可变大小块(既“较大块”)对于大文件更有效率。",
|
||||
"Version": "版本",
|
||||
"Versions": "历史版本",
|
||||
"Versions Path": "历史版本路径",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "警告:如果你在使用外部的监视器如 {{syncthingInotify}},你应该确保它已经取消激活。",
|
||||
"Watch for Changes": "监视更改",
|
||||
"Watching for Changes": "正在监视更改",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "对更改的监视无需定期扫描就可以发现大多数更改。",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "若您在本机添加新设备,记住您也必须在这个设备上添加本机。",
|
||||
"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.": "若你添加了新文件夹,记住文件夹 ID 是用以在不同设备间建立联系的。在不同设备间拥有相同 ID 的文件夹将会被同步。且文件夹 ID 区分大小写。",
|
||||
"Yes": "是",
|
||||
|
||||
@@ -349,6 +349,9 @@
|
||||
"Uptime": "上線時間",
|
||||
"Usage reporting is always enabled for candidate releases.": "發行候選版永遠啟用使用數據回報。",
|
||||
"Use HTTPS for GUI": "為 GUI 使用 HTTPS",
|
||||
"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.",
|
||||
"Version": "版本",
|
||||
"Versions": "版本",
|
||||
"Versions Path": "歷史版本路徑",
|
||||
@@ -361,6 +364,7 @@
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "警告:如果您正在使用外部監視工具,如 {{syncthingInotify}},您應該確認已經將其關閉。",
|
||||
"Watch for Changes": "監視變動",
|
||||
"Watching for Changes": "正在監視變動",
|
||||
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "當新增一個裝置時,務必記住,當前的這個裝置也同樣必須被添加至另一邊。",
|
||||
"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.": "當新增一個資料夾時,請記住,資料夾識別碼是用來將裝置之間的資料夾綁定在一起的。它們有區分大小寫,且必須在所有裝置之間完全相同。",
|
||||
"Yes": "是",
|
||||
|
||||
@@ -709,7 +709,7 @@
|
||||
<tr>
|
||||
<th><span class="fas fa-fw fa-link"></span> <span translate>Address</span></th>
|
||||
<td ng-if="connections[deviceCfg.deviceID].connected" class="text-right">
|
||||
<span tooltip data-original-title="{{ connections[deviceCfg.deviceID].type.indexOf('Relay') > -1 ? '' : connections[deviceCfg.deviceID].type }}">
|
||||
<span tooltip data-original-title="{{ connections[deviceCfg.deviceID].type.indexOf('Relay') > -1 ? '' : connections[deviceCfg.deviceID].type }} {{ connections[deviceCfg.deviceID].crypto }}">
|
||||
{{deviceAddr(deviceCfg)}}
|
||||
</span>
|
||||
</td>
|
||||
@@ -740,7 +740,7 @@
|
||||
<td translate class="text-right">Yes</td>
|
||||
</tr>
|
||||
<tr ng-if="deviceCfg.introducedBy">
|
||||
<th><span class="far fa-fw fa-meh"></span> <span translate>Introduced By</span></th>
|
||||
<th><span class="far fa-fw fa-handshake-o"></span> <span translate>Introduced By</span></th>
|
||||
<td class="text-right">{{ deviceName(findDevice(deviceCfg.introducedBy)) || deviceCfg.introducedBy.substring(0, 5) }}</td>
|
||||
</tr>
|
||||
<tr ng-if="connections[deviceCfg.deviceID].clientVersion">
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
</h1>
|
||||
<hr />
|
||||
|
||||
<p translate>Copyright © 2014-2017 the following Contributors:</p>
|
||||
<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, 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 (Alex) 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, 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, 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, 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, janost, jaseg, klemens, marco-m, otbutz, perewa, rubenbe, wangguoliang, xjtdy888, 佛跳墙
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
@@ -20,21 +20,47 @@ Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, Alexandre Viau, And
|
||||
<p translate>Syncthing includes the following software or portions thereof:</p>
|
||||
<ul class="list-unstyled two-columns" id="copyright-notices">
|
||||
<li><a href="http://getbootstrap.com/">Bootstrap</a>, Copyright © 2011-2016 Twitter, Inc.</li>
|
||||
<li><a href="https://angularjs.org/">AngularJS</a>, Copyright © 2010-2016 Google, Inc.</li>
|
||||
<li><a href="https://angularjs.org/">AngularJS</a>, Copyright © 2010-2014, 2016 Google, Inc.</li>
|
||||
<li><a href="http://www.daterangepicker.com/">Date Range Picker</a>, Copyright © 2012-2018 Dan Grossman.</li>
|
||||
<li><a href="https://github.com/mar10/fancytree">JQuery Fancytree Plugin</a>, Copyright © 2008-2018 Martin Wendt.</li>
|
||||
<li><a href="https://forkaweso.me/Fork-Awesome/">Fork Awesome</a>, Copyright © 2018 Dave Gandy & Fork Awesome.</li>
|
||||
<li><a href="http://jquery.com/">jQuery JavaScript Library</a>, Copyright © jQuery Foundation and other contributors.</li>
|
||||
<li><a href="http://momentjs.com/">moment.js</a>, Copyright © JS Foundation and other contributors.</li>
|
||||
<li><a href="https://prometheus.io/">Prometheus</a>, Copyright © 2012-2015 The Prometheus Authors.</li>
|
||||
<li><a href="https://github.com/AudriusButkevicius/go-nat-pmp">AudriusButkevicius/go-nat-pmp</a>, Copyright © 2013 John Howard Palevich.</li>
|
||||
<li><a href="https://github.com/AudriusButkevicius/recli">AudriusButkevicius/recli</a>, Copyright © 2019 Audrius Butkevicius.</li>
|
||||
<li><a href="https://github.com/beorn7/perks">beorn7/perks</a>, Copyright © 2013 Blake Mizerany.</li>
|
||||
<li><a href="https://github.com/bkaradzic/go-lz4">bkaradzic/go-lz4</a>, Copyright © 2011-2012 Branimir Karadzic, 2013 Damian Gryski.</li>
|
||||
<li><a href="https://github.com/chmduquesne/rollinghash">chmduquesne/rollinghash</a>, Copyright © 2015 Christophe-Marie Duquesne</li>
|
||||
<li><a href="https://github.com/calmh/du">calmh/du</a>, Public domain.</li>
|
||||
<li><a href="https://github.com/calmh/xdr">calmh/xdr</a>, Copyright © 2014 Jakob Borg.</li>
|
||||
<li><a href="https://github.com/chmduquesne/rollinghash">chmduquesne/rollinghash</a>, Copyright © 2015 Christophe-Marie Duquesne.</li>
|
||||
<li><a href="https://github.com/d4l3k/messagediff">d4l3k/messagediff</a>, Copyright © 2015 Tristan Rice.</li>
|
||||
<li><a href="https://github.com/flynn-archive/go-shlex">flynn-archive/go-shlex</a>, Copyright © 2012 Google Inc.</li>
|
||||
<li><a href="https://github.com/gobwas/glob">gobwas/glob</a>, Copyright © 2016 Sergey Kamardin.</li>
|
||||
<li><a href="https://github.com/gogo/protobuf">gogo/protobuf</a>, Copyright © 2013 The GoGo Authors.</li>
|
||||
<li><a href="https://github.com/golang/groupcache">golang/groupcache</a>, Copyright © 2013 Google Inc.</li>
|
||||
<li><a href="https://github.com/golang/protobuf">golang/protobuf</a>, Copyright © 2010 The Go Authors.</li>
|
||||
<li><a href="https://github.com/golang/snappy">golang/snappy</a>, Copyright © 2011 The Snappy-Go Authors.</li>
|
||||
<li><a href="https://github.com/jackpal/gateway">jackpal/gateway</a>, Copyright © 2010 Jack Palevich.</li>
|
||||
<li><a href="https://github.com/minio/sha256-simd">minio/sha256-simd</a>, Copyright © 2016 Minio, Inc.</li>
|
||||
<li><a href="https://github.com/kballard/go-shellquote">kballard/go-shellquote</a>, Copyright © 2014 Kevin Ballard.</li>
|
||||
<li><a href="https://github.com/lib/pq">lib/pq</a>, Copyright © 2011-2013, 'pq' Contributors, portions Copyright © 2011 Blake Mizerany.</li>
|
||||
<li><a href="https://github.com/mattn/go-isatty">mattn/go-isatty</a>, Copyright © Yasuhiro MATSUMOTO.</li>
|
||||
<li><a href="https://github.com/matttproud/golang_protobuf_extensions">matttproud/golang_protobuf_extensions</a>, Copyright © 2012 Matt T. Proud.</li>
|
||||
<li><a href="https://github.com/minio/sha256-simd">minio/sha256-simd</a>, Copyright © 2016-2017 Minio, Inc.</li>
|
||||
<li><a href="https://github.com/oschwald/geoip2-golang">oschwald/geoip2-golang</a>, Copyright © 2015, Gregory J. Oschwald.</li>
|
||||
<li><a href="https://github.com/oschwald/maxminddb-golang">oschwald/maxminddb-golang</a>, Copyright © 2015, Gregory J. Oschwald.</li>
|
||||
<li><a href="https://github.com/petermattis/goid">petermattis/goid</a>, Copyright © 2015-2016 Peter Mattis.</li>
|
||||
<li><a href="https://github.com/pkg/errors">pkg/errors</a>, Copyright © 2015, Dave Cheney.</li>
|
||||
<li><a href="https://github.com/rcrowley/go-metrics">rcrowley/go-metrics</a>, Copyright © 2012 Richard Crowley.</li>
|
||||
<li><a href="https://github.com/sasha-s/go-deadlock">sasha-s/go-deadlock</a>, Copyright © 2016 sasha-s</li>
|
||||
<li><a href="https://github.com/sasha-s/go-deadlock">sasha-s/go-deadlock</a>, Copyright © 2016 sasha-s.</li>
|
||||
<li><a href="https://github.com/syncthing/notify">syncthing/notify</a>, Copyright © 2014-2015 The Notify Authors.</li>
|
||||
<li><a href="https://github.com/syndtr/goleveldb">syndtr/goleveldb</a>, Copyright © 2012 Suryandaru Triandana.</li>
|
||||
<li><a href="https://github.com/thejerf/suture">thejerf/suture</a>, Copyright © 2014-2015 Barracuda Networks, Inc.</li>
|
||||
<li><a href="https://github.com/vitrun/qart">vitrun/qart</a>, Copyright © The Go Authors.</li>
|
||||
<li><a href="https://golang.org">The Go Programming Language</a>, Copyright © 2012 The Go Authors.</li>
|
||||
<li><a href="https://github.com/urfave/cli">urfave/cli</a>, Copyright © 2016 Jeremy Saenz & Contributors.</li>
|
||||
<li><a href="https://github.com/vitrun/qart">vitrun/qart</a>, Copyright © 2010-2011 The Go Authors.</li>
|
||||
<li><a href="https://gopkg.in/asn1-ber.v1">gopkg.in/asn1-ber.v1</a>, Copyright © 2011-2015 Michael Mitton, portions Copyright © 2015-2016 go-asn1-ber Authors.</li>
|
||||
<li><a href="https://gopkg.in/ldap.v2">gopkg.in/ldap.v2</a>, Copyright © 2011-2015 Michael Mitton, portions Copyright © 2015-2016 go-ldap Authors.</li>
|
||||
<li><a href="https://golang.org">The Go Programming Language</a>, Copyright © 2009 The Go Authors.</li>
|
||||
<li>Font Awesome by Dave Gandy - <a href="http://fontawesome.io/">http://fontawesome.io</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -84,18 +84,18 @@ func New(myID protocol.DeviceID) Configuration {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func NewWithFreePorts(myID protocol.DeviceID) Configuration {
|
||||
func NewWithFreePorts(myID protocol.DeviceID) (Configuration, error) {
|
||||
cfg := New(myID)
|
||||
|
||||
port, err := getFreePort("127.0.0.1", DefaultGUIPort)
|
||||
if err != nil {
|
||||
l.Fatalln("get free port (GUI):", err)
|
||||
return Configuration{}, fmt.Errorf("get free port (GUI): %v", err)
|
||||
}
|
||||
cfg.GUI.RawAddress = fmt.Sprintf("127.0.0.1:%d", port)
|
||||
|
||||
port, err = getFreePort("0.0.0.0", DefaultTCPPort)
|
||||
if err != nil {
|
||||
l.Fatalln("get free port (BEP):", err)
|
||||
return Configuration{}, fmt.Errorf("get free port (BEP): %v", err)
|
||||
}
|
||||
if port == DefaultTCPPort {
|
||||
cfg.Options.ListenAddresses = []string{"default"}
|
||||
@@ -106,7 +106,7 @@ func NewWithFreePorts(myID protocol.DeviceID) Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
|
||||
|
||||
@@ -93,7 +93,7 @@ func TestDeviceConfig(t *testing.T) {
|
||||
t.Fatal("Unexpected file")
|
||||
}
|
||||
|
||||
cfg := wr.cfg
|
||||
cfg := wr.(*wrapper).cfg
|
||||
|
||||
expectedFolders := []FolderConfiguration{
|
||||
{
|
||||
@@ -515,7 +515,7 @@ func TestNewSaveLoad(t *testing.T) {
|
||||
cfg := Wrap(path, intCfg)
|
||||
|
||||
// To make the equality pass later
|
||||
cfg.cfg.XMLName.Local = "configuration"
|
||||
cfg.(*wrapper).cfg.XMLName.Local = "configuration"
|
||||
|
||||
if exists(path) {
|
||||
t.Error(path, "exists")
|
||||
@@ -827,7 +827,7 @@ func TestIgnoredFolders(t *testing.T) {
|
||||
|
||||
// 2 for folder2, 1 for folder1, as non-existing device and device the folder is shared with is removed.
|
||||
expectedIgnoredFolders := 3
|
||||
for _, dev := range wrapper.cfg.Devices {
|
||||
for _, dev := range wrapper.Devices() {
|
||||
expectedIgnoredFolders -= len(dev.IgnoredFolders)
|
||||
}
|
||||
if expectedIgnoredFolders != 0 {
|
||||
|
||||
@@ -52,7 +52,7 @@ type FolderConfiguration struct {
|
||||
Paused bool `xml:"paused" json:"paused"`
|
||||
WeakHashThresholdPct int `xml:"weakHashThresholdPct" json:"weakHashThresholdPct"` // Use weak hash if more than X percent of the file has changed. Set to -1 to always use weak hash.
|
||||
MarkerName string `xml:"markerName" json:"markerName"`
|
||||
UseLargeBlocks bool `xml:"useLargeBlocks" json:"useLargeBlocks"`
|
||||
UseLargeBlocks bool `xml:"useLargeBlocks" json:"useLargeBlocks" default:"true"`
|
||||
CopyOwnershipFromParent bool `xml:"copyOwnershipFromParent" json:"copyOwnershipFromParent"`
|
||||
|
||||
cachedFilesystem fs.Filesystem
|
||||
@@ -106,7 +106,7 @@ func (f FolderConfiguration) Versioner() versioner.Versioner {
|
||||
}
|
||||
versionerFactory, ok := versioner.Factories[f.Versioning.Type]
|
||||
if !ok {
|
||||
l.Fatalf("Requested versioning type %q that does not exist", f.Versioning.Type)
|
||||
panic(fmt.Sprintf("Requested versioning type %q that does not exist", f.Versioning.Type))
|
||||
}
|
||||
|
||||
return versionerFactory(f.ID, f.Filesystem(), f.Versioning.Params)
|
||||
|
||||
+75
-37
@@ -52,10 +52,48 @@ type noopWaiter struct{}
|
||||
|
||||
func (noopWaiter) Wait() {}
|
||||
|
||||
// A wrapper around a Configuration that manages loads, saves and published
|
||||
// A Wrapper around a Configuration that manages loads, saves and published
|
||||
// notifications of changes to registered Handlers
|
||||
type Wrapper interface {
|
||||
MyName() string
|
||||
ConfigPath() string
|
||||
|
||||
type Wrapper struct {
|
||||
RawCopy() Configuration
|
||||
Replace(cfg Configuration) (Waiter, error)
|
||||
RequiresRestart() bool
|
||||
Save() error
|
||||
|
||||
GUI() GUIConfiguration
|
||||
SetGUI(gui GUIConfiguration) (Waiter, error)
|
||||
LDAP() LDAPConfiguration
|
||||
|
||||
Options() OptionsConfiguration
|
||||
SetOptions(opts OptionsConfiguration) (Waiter, error)
|
||||
|
||||
Folder(id string) (FolderConfiguration, bool)
|
||||
Folders() map[string]FolderConfiguration
|
||||
FolderList() []FolderConfiguration
|
||||
SetFolder(fld FolderConfiguration) (Waiter, error)
|
||||
|
||||
Device(id protocol.DeviceID) (DeviceConfiguration, bool)
|
||||
Devices() map[protocol.DeviceID]DeviceConfiguration
|
||||
RemoveDevice(id protocol.DeviceID) (Waiter, error)
|
||||
SetDevice(DeviceConfiguration) (Waiter, error)
|
||||
SetDevices([]DeviceConfiguration) (Waiter, error)
|
||||
|
||||
AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string)
|
||||
AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID)
|
||||
IgnoredDevice(id protocol.DeviceID) bool
|
||||
IgnoredFolder(device protocol.DeviceID, folder string) bool
|
||||
|
||||
ListenAddresses() []string
|
||||
GlobalDiscoveryServers() []string
|
||||
|
||||
Subscribe(c Committer)
|
||||
Unsubscribe(c Committer)
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
cfg Configuration
|
||||
path string
|
||||
|
||||
@@ -69,8 +107,8 @@ type Wrapper struct {
|
||||
|
||||
// Wrap wraps an existing Configuration structure and ties it to a file on
|
||||
// disk.
|
||||
func Wrap(path string, cfg Configuration) *Wrapper {
|
||||
w := &Wrapper{
|
||||
func Wrap(path string, cfg Configuration) Wrapper {
|
||||
w := &wrapper{
|
||||
cfg: cfg,
|
||||
path: path,
|
||||
mut: sync.NewMutex(),
|
||||
@@ -80,7 +118,7 @@ func Wrap(path string, cfg Configuration) *Wrapper {
|
||||
|
||||
// Load loads an existing file on disk and returns a new configuration
|
||||
// wrapper.
|
||||
func Load(path string, myID protocol.DeviceID) (*Wrapper, error) {
|
||||
func Load(path string, myID protocol.DeviceID) (Wrapper, error) {
|
||||
fd, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -95,13 +133,13 @@ func Load(path string, myID protocol.DeviceID) (*Wrapper, error) {
|
||||
return Wrap(path, cfg), nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) ConfigPath() string {
|
||||
func (w *wrapper) ConfigPath() string {
|
||||
return w.path
|
||||
}
|
||||
|
||||
// Subscribe registers the given handler to be called on any future
|
||||
// configuration changes.
|
||||
func (w *Wrapper) Subscribe(c Committer) {
|
||||
func (w *wrapper) Subscribe(c Committer) {
|
||||
w.mut.Lock()
|
||||
w.subs = append(w.subs, c)
|
||||
w.mut.Unlock()
|
||||
@@ -109,7 +147,7 @@ func (w *Wrapper) Subscribe(c Committer) {
|
||||
|
||||
// Unsubscribe de-registers the given handler from any future calls to
|
||||
// configuration changes
|
||||
func (w *Wrapper) Unsubscribe(c Committer) {
|
||||
func (w *wrapper) Unsubscribe(c Committer) {
|
||||
w.mut.Lock()
|
||||
for i := range w.subs {
|
||||
if w.subs[i] == c {
|
||||
@@ -123,20 +161,20 @@ func (w *Wrapper) Unsubscribe(c Committer) {
|
||||
}
|
||||
|
||||
// RawCopy returns a copy of the currently wrapped Configuration object.
|
||||
func (w *Wrapper) RawCopy() Configuration {
|
||||
func (w *wrapper) RawCopy() Configuration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
return w.cfg.Copy()
|
||||
}
|
||||
|
||||
// Replace swaps the current configuration object for the given one.
|
||||
func (w *Wrapper) Replace(cfg Configuration) (Waiter, error) {
|
||||
func (w *wrapper) Replace(cfg Configuration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
return w.replaceLocked(cfg.Copy())
|
||||
}
|
||||
|
||||
func (w *Wrapper) replaceLocked(to Configuration) (Waiter, error) {
|
||||
func (w *wrapper) replaceLocked(to Configuration) (Waiter, error) {
|
||||
from := w.cfg
|
||||
|
||||
if err := to.clean(); err != nil {
|
||||
@@ -158,7 +196,7 @@ func (w *Wrapper) replaceLocked(to Configuration) (Waiter, error) {
|
||||
return w.notifyListeners(from.Copy(), to.Copy()), nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) notifyListeners(from, to Configuration) Waiter {
|
||||
func (w *wrapper) notifyListeners(from, to Configuration) Waiter {
|
||||
wg := sync.NewWaitGroup()
|
||||
wg.Add(len(w.subs))
|
||||
for _, sub := range w.subs {
|
||||
@@ -170,7 +208,7 @@ func (w *Wrapper) notifyListeners(from, to Configuration) Waiter {
|
||||
return wg
|
||||
}
|
||||
|
||||
func (w *Wrapper) notifyListener(sub Committer, from, to Configuration) {
|
||||
func (w *wrapper) notifyListener(sub Committer, from, to Configuration) {
|
||||
l.Debugln(sub, "committing configuration")
|
||||
if !sub.CommitConfiguration(from, to) {
|
||||
l.Debugln(sub, "requires restart")
|
||||
@@ -179,7 +217,7 @@ func (w *Wrapper) notifyListener(sub Committer, from, to Configuration) {
|
||||
}
|
||||
|
||||
// Devices returns a map of devices.
|
||||
func (w *Wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
||||
func (w *wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
if w.deviceMap == nil {
|
||||
@@ -193,7 +231,7 @@ func (w *Wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
||||
|
||||
// SetDevices adds new devices to the configuration, or overwrites existing
|
||||
// devices with the same ID.
|
||||
func (w *Wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
|
||||
func (w *wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -218,12 +256,12 @@ func (w *Wrapper) SetDevices(devs []DeviceConfiguration) (Waiter, error) {
|
||||
|
||||
// SetDevice adds a new device to the configuration, or overwrites an existing
|
||||
// device with the same ID.
|
||||
func (w *Wrapper) SetDevice(dev DeviceConfiguration) (Waiter, error) {
|
||||
func (w *wrapper) SetDevice(dev DeviceConfiguration) (Waiter, error) {
|
||||
return w.SetDevices([]DeviceConfiguration{dev})
|
||||
}
|
||||
|
||||
// RemoveDevice removes the device from the configuration
|
||||
func (w *Wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) {
|
||||
func (w *wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -240,7 +278,7 @@ func (w *Wrapper) RemoveDevice(id protocol.DeviceID) (Waiter, error) {
|
||||
|
||||
// Folders returns a map of folders. Folder structures should not be changed,
|
||||
// other than for the purpose of updating via SetFolder().
|
||||
func (w *Wrapper) Folders() map[string]FolderConfiguration {
|
||||
func (w *wrapper) Folders() map[string]FolderConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
if w.folderMap == nil {
|
||||
@@ -253,7 +291,7 @@ func (w *Wrapper) Folders() map[string]FolderConfiguration {
|
||||
}
|
||||
|
||||
// FolderList returns a slice of folders.
|
||||
func (w *Wrapper) FolderList() []FolderConfiguration {
|
||||
func (w *wrapper) FolderList() []FolderConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
return w.cfg.Copy().Folders
|
||||
@@ -261,7 +299,7 @@ func (w *Wrapper) FolderList() []FolderConfiguration {
|
||||
|
||||
// SetFolder adds a new folder to the configuration, or overwrites an existing
|
||||
// folder with the same ID.
|
||||
func (w *Wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {
|
||||
func (w *wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -280,14 +318,14 @@ func (w *Wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {
|
||||
}
|
||||
|
||||
// Options returns the current options configuration object.
|
||||
func (w *Wrapper) Options() OptionsConfiguration {
|
||||
func (w *wrapper) Options() OptionsConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
return w.cfg.Options.Copy()
|
||||
}
|
||||
|
||||
// SetOptions replaces the current options configuration object.
|
||||
func (w *Wrapper) SetOptions(opts OptionsConfiguration) (Waiter, error) {
|
||||
func (w *wrapper) SetOptions(opts OptionsConfiguration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
@@ -295,21 +333,21 @@ func (w *Wrapper) SetOptions(opts OptionsConfiguration) (Waiter, error) {
|
||||
return w.replaceLocked(newCfg)
|
||||
}
|
||||
|
||||
func (w *Wrapper) LDAP() LDAPConfiguration {
|
||||
func (w *wrapper) LDAP() LDAPConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
return w.cfg.LDAP.Copy()
|
||||
}
|
||||
|
||||
// GUI returns the current GUI configuration object.
|
||||
func (w *Wrapper) GUI() GUIConfiguration {
|
||||
func (w *wrapper) GUI() GUIConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
return w.cfg.GUI.Copy()
|
||||
}
|
||||
|
||||
// SetGUI replaces the current GUI configuration object.
|
||||
func (w *Wrapper) SetGUI(gui GUIConfiguration) (Waiter, error) {
|
||||
func (w *wrapper) SetGUI(gui GUIConfiguration) (Waiter, error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
@@ -319,7 +357,7 @@ func (w *Wrapper) SetGUI(gui GUIConfiguration) (Waiter, error) {
|
||||
|
||||
// IgnoredDevice returns whether or not connection attempts from the given
|
||||
// device should be silently ignored.
|
||||
func (w *Wrapper) IgnoredDevice(id protocol.DeviceID) bool {
|
||||
func (w *wrapper) IgnoredDevice(id protocol.DeviceID) bool {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
for _, device := range w.cfg.IgnoredDevices {
|
||||
@@ -332,7 +370,7 @@ func (w *Wrapper) IgnoredDevice(id protocol.DeviceID) bool {
|
||||
|
||||
// IgnoredFolder returns whether or not share attempts for the given
|
||||
// folder should be silently ignored.
|
||||
func (w *Wrapper) IgnoredFolder(device protocol.DeviceID, folder string) bool {
|
||||
func (w *wrapper) IgnoredFolder(device protocol.DeviceID, folder string) bool {
|
||||
dev, ok := w.Device(device)
|
||||
if !ok {
|
||||
return false
|
||||
@@ -341,7 +379,7 @@ func (w *Wrapper) IgnoredFolder(device protocol.DeviceID, folder string) bool {
|
||||
}
|
||||
|
||||
// Device returns the configuration for the given device and an "ok" bool.
|
||||
func (w *Wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
|
||||
func (w *wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
for _, device := range w.cfg.Devices {
|
||||
@@ -353,7 +391,7 @@ func (w *Wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
|
||||
}
|
||||
|
||||
// Folder returns the configuration for the given folder and an "ok" bool.
|
||||
func (w *Wrapper) Folder(id string) (FolderConfiguration, bool) {
|
||||
func (w *wrapper) Folder(id string) (FolderConfiguration, bool) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
for _, folder := range w.cfg.Folders {
|
||||
@@ -365,7 +403,7 @@ func (w *Wrapper) Folder(id string) (FolderConfiguration, bool) {
|
||||
}
|
||||
|
||||
// Save writes the configuration to disk, and generates a ConfigSaved event.
|
||||
func (w *Wrapper) Save() error {
|
||||
func (w *wrapper) Save() error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -390,7 +428,7 @@ func (w *Wrapper) Save() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) GlobalDiscoveryServers() []string {
|
||||
func (w *wrapper) GlobalDiscoveryServers() []string {
|
||||
var servers []string
|
||||
for _, srv := range w.Options().GlobalAnnServers {
|
||||
switch srv {
|
||||
@@ -407,7 +445,7 @@ func (w *Wrapper) GlobalDiscoveryServers() []string {
|
||||
return util.UniqueStrings(servers)
|
||||
}
|
||||
|
||||
func (w *Wrapper) ListenAddresses() []string {
|
||||
func (w *wrapper) ListenAddresses() []string {
|
||||
var addresses []string
|
||||
for _, addr := range w.Options().ListenAddresses {
|
||||
switch addr {
|
||||
@@ -420,15 +458,15 @@ func (w *Wrapper) ListenAddresses() []string {
|
||||
return util.UniqueStrings(addresses)
|
||||
}
|
||||
|
||||
func (w *Wrapper) RequiresRestart() bool {
|
||||
func (w *wrapper) RequiresRestart() bool {
|
||||
return atomic.LoadUint32(&w.requiresRestart) != 0
|
||||
}
|
||||
|
||||
func (w *Wrapper) setRequiresRestart() {
|
||||
func (w *wrapper) setRequiresRestart() {
|
||||
atomic.StoreUint32(&w.requiresRestart, 1)
|
||||
}
|
||||
|
||||
func (w *Wrapper) MyName() string {
|
||||
func (w *wrapper) MyName() string {
|
||||
w.mut.Lock()
|
||||
myID := w.cfg.MyID
|
||||
w.mut.Unlock()
|
||||
@@ -436,7 +474,7 @@ func (w *Wrapper) MyName() string {
|
||||
return cfg.Name
|
||||
}
|
||||
|
||||
func (w *Wrapper) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) {
|
||||
func (w *wrapper) AddOrUpdatePendingDevice(device protocol.DeviceID, name, address string) {
|
||||
defer w.Save()
|
||||
|
||||
w.mut.Lock()
|
||||
@@ -459,7 +497,7 @@ func (w *Wrapper) AddOrUpdatePendingDevice(device protocol.DeviceID, name, addre
|
||||
})
|
||||
}
|
||||
|
||||
func (w *Wrapper) AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID) {
|
||||
func (w *wrapper) AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID) {
|
||||
defer w.Save()
|
||||
|
||||
w.mut.Lock()
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestIsLANHost(t *testing.T) {
|
||||
AlwaysLocalNets: []string{"10.20.30.0/24"},
|
||||
},
|
||||
})
|
||||
s := &Service{cfg: cfg}
|
||||
s := &service{cfg: cfg}
|
||||
|
||||
for _, tc := range cases {
|
||||
res := s.isLANHost(tc.addr)
|
||||
|
||||
@@ -36,7 +36,7 @@ type waiter interface {
|
||||
|
||||
const limiterBurstSize = 4 * 128 << 10
|
||||
|
||||
func newLimiter(cfg *config.Wrapper) *limiter {
|
||||
func newLimiter(cfg config.Wrapper) *limiter {
|
||||
l := &limiter{
|
||||
write: rate.NewLimiter(rate.Inf, limiterBurstSize),
|
||||
read: rate.NewLimiter(rate.Inf, limiterBurstSize),
|
||||
|
||||
@@ -24,7 +24,7 @@ func init() {
|
||||
device4, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
|
||||
}
|
||||
|
||||
func initConfig() *config.Wrapper {
|
||||
func initConfig() config.Wrapper {
|
||||
cfg := config.Wrap("/dev/null", config.New(device1))
|
||||
dev1Conf = config.NewDeviceConfiguration(device1, "device1")
|
||||
dev2Conf = config.NewDeviceConfiguration(device2, "device2")
|
||||
|
||||
@@ -22,7 +22,7 @@ func init() {
|
||||
}
|
||||
|
||||
type relayDialer struct {
|
||||
cfg *config.Wrapper
|
||||
cfg config.Wrapper
|
||||
tlsCfg *tls.Config
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func (d *relayDialer) RedialFrequency() time.Duration {
|
||||
|
||||
type relayDialerFactory struct{}
|
||||
|
||||
func (relayDialerFactory) New(cfg *config.Wrapper, tlsCfg *tls.Config) genericDialer {
|
||||
func (relayDialerFactory) New(cfg config.Wrapper, tlsCfg *tls.Config) genericDialer {
|
||||
return &relayDialer{
|
||||
cfg: cfg,
|
||||
tlsCfg: tlsCfg,
|
||||
|
||||
@@ -29,7 +29,7 @@ type relayListener struct {
|
||||
onAddressesChangedNotifier
|
||||
|
||||
uri *url.URL
|
||||
cfg *config.Wrapper
|
||||
cfg config.Wrapper
|
||||
tlsCfg *tls.Config
|
||||
conns chan internalConn
|
||||
factory listenerFactory
|
||||
@@ -180,7 +180,7 @@ func (t *relayListener) NATType() string {
|
||||
|
||||
type relayListenerFactory struct{}
|
||||
|
||||
func (f *relayListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
|
||||
func (f *relayListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
|
||||
return &relayListener{
|
||||
uri: uri,
|
||||
cfg: cfg,
|
||||
|
||||
+35
-18
@@ -51,6 +51,7 @@ const (
|
||||
|
||||
// From go/src/crypto/tls/cipher_suites.go
|
||||
var tlsCipherSuiteNames = map[uint16]string{
|
||||
// TLS 1.2
|
||||
0x0005: "TLS_RSA_WITH_RC4_128_SHA",
|
||||
0x000a: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
|
||||
0x002f: "TLS_RSA_WITH_AES_128_CBC_SHA",
|
||||
@@ -73,13 +74,29 @@ var tlsCipherSuiteNames = map[uint16]string{
|
||||
0xc02c: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
||||
0xcca8: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
|
||||
0xcca9: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305",
|
||||
|
||||
// TLS 1.3
|
||||
0x1301: "TLS_AES_128_GCM_SHA256",
|
||||
0x1302: "TLS_AES_256_GCM_SHA384",
|
||||
0x1303: "TLS_CHACHA20_POLY1305_SHA256",
|
||||
}
|
||||
|
||||
var tlsVersionNames = map[uint16]string{
|
||||
tls.VersionTLS12: "TLS1.2",
|
||||
772: "TLS1.3", // tls.VersionTLS13 constant available in Go 1.12+
|
||||
}
|
||||
|
||||
// Service listens and dials all configured unconnected devices, via supported
|
||||
// dialers. Successful connections are handed to the model.
|
||||
type Service struct {
|
||||
type Service interface {
|
||||
suture.Service
|
||||
Status() map[string]interface{}
|
||||
NATType() string
|
||||
}
|
||||
|
||||
type service struct {
|
||||
*suture.Supervisor
|
||||
cfg *config.Wrapper
|
||||
cfg config.Wrapper
|
||||
myID protocol.DeviceID
|
||||
model Model
|
||||
tlsCfg *tls.Config
|
||||
@@ -97,10 +114,10 @@ type Service struct {
|
||||
listenerSupervisor *suture.Supervisor
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder,
|
||||
bepProtocolName string, tlsDefaultCommonName string) *Service {
|
||||
func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder,
|
||||
bepProtocolName string, tlsDefaultCommonName string) *service {
|
||||
|
||||
service := &Service{
|
||||
service := &service{
|
||||
Supervisor: suture.New("connections.Service", suture.Spec{
|
||||
Log: func(line string) {
|
||||
l.Infoln(line)
|
||||
@@ -156,7 +173,7 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Service) handle() {
|
||||
func (s *service) handle() {
|
||||
next:
|
||||
for c := range s.conns {
|
||||
cs := c.ConnectionState()
|
||||
@@ -275,14 +292,14 @@ next:
|
||||
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, c.String(), deviceCfg.Compression)
|
||||
modelConn := completeConn{c, protoConn}
|
||||
|
||||
l.Infof("Established secure connection to %s at %s (%s)", remoteID, c, tlsCipherSuiteNames[c.ConnectionState().CipherSuite])
|
||||
l.Infof("Established secure connection to %s at %s", remoteID, c)
|
||||
|
||||
s.model.AddConnection(modelConn, hello)
|
||||
continue next
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) connect() {
|
||||
func (s *service) connect() {
|
||||
nextDial := make(map[string]time.Time)
|
||||
|
||||
// Used as delay for the first few connection attempts, increases
|
||||
@@ -433,7 +450,7 @@ func (s *Service) connect() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) isLANHost(host string) bool {
|
||||
func (s *service) isLANHost(host string) bool {
|
||||
// Probably we are called with an ip:port combo which we can resolve as
|
||||
// a TCP address.
|
||||
if addr, err := net.ResolveTCPAddr("tcp", host); err == nil {
|
||||
@@ -447,7 +464,7 @@ func (s *Service) isLANHost(host string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) isLAN(addr net.Addr) bool {
|
||||
func (s *service) isLAN(addr net.Addr) bool {
|
||||
var ip net.IP
|
||||
|
||||
switch addr := addr.(type) {
|
||||
@@ -488,7 +505,7 @@ func (s *Service) isLAN(addr net.Addr) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) createListener(factory listenerFactory, uri *url.URL) bool {
|
||||
func (s *service) createListener(factory listenerFactory, uri *url.URL) bool {
|
||||
// must be called with listenerMut held
|
||||
|
||||
l.Debugln("Starting listener", uri)
|
||||
@@ -500,7 +517,7 @@ func (s *Service) createListener(factory listenerFactory, uri *url.URL) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Service) logListenAddressesChangedEvent(l genericListener) {
|
||||
func (s *service) logListenAddressesChangedEvent(l genericListener) {
|
||||
events.Default.Log(events.ListenAddressesChanged, map[string]interface{}{
|
||||
"address": l.URI(),
|
||||
"lan": l.LANAddresses(),
|
||||
@@ -508,11 +525,11 @@ func (s *Service) logListenAddressesChangedEvent(l genericListener) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
|
||||
func (s *service) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
|
||||
func (s *service) CommitConfiguration(from, to config.Configuration) bool {
|
||||
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
|
||||
for _, dev := range to.Devices {
|
||||
newDevices[dev.DeviceID] = true
|
||||
@@ -589,7 +606,7 @@ func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Service) AllAddresses() []string {
|
||||
func (s *service) AllAddresses() []string {
|
||||
s.listenersMut.RLock()
|
||||
var addrs []string
|
||||
for _, listener := range s.listeners {
|
||||
@@ -604,7 +621,7 @@ func (s *Service) AllAddresses() []string {
|
||||
return util.UniqueStrings(addrs)
|
||||
}
|
||||
|
||||
func (s *Service) ExternalAddresses() []string {
|
||||
func (s *service) ExternalAddresses() []string {
|
||||
s.listenersMut.RLock()
|
||||
var addrs []string
|
||||
for _, listener := range s.listeners {
|
||||
@@ -616,7 +633,7 @@ func (s *Service) ExternalAddresses() []string {
|
||||
return util.UniqueStrings(addrs)
|
||||
}
|
||||
|
||||
func (s *Service) Status() map[string]interface{} {
|
||||
func (s *service) Status() map[string]interface{} {
|
||||
s.listenersMut.RLock()
|
||||
result := make(map[string]interface{})
|
||||
for addr, listener := range s.listeners {
|
||||
@@ -636,7 +653,7 @@ func (s *Service) Status() map[string]interface{} {
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Service) NATType() string {
|
||||
func (s *service) NATType() string {
|
||||
s.listenersMut.RLock()
|
||||
defer s.listenersMut.RUnlock()
|
||||
for _, listener := range s.listeners {
|
||||
|
||||
@@ -27,6 +27,7 @@ type Connection interface {
|
||||
RemoteAddr() net.Addr
|
||||
Priority() int
|
||||
String() string
|
||||
Crypto() string
|
||||
}
|
||||
|
||||
// completeConn is the aggregation of an internalConn and the
|
||||
@@ -101,6 +102,11 @@ func (c internalConn) Priority() int {
|
||||
return c.priority
|
||||
}
|
||||
|
||||
func (c internalConn) Crypto() string {
|
||||
cs := c.ConnectionState()
|
||||
return fmt.Sprintf("%s-%s", tlsVersionNames[cs.Version], tlsCipherSuiteNames[cs.CipherSuite])
|
||||
}
|
||||
|
||||
func (c internalConn) Transport() string {
|
||||
transport := c.connType.Transport()
|
||||
host, _, err := net.SplitHostPort(c.LocalAddr().String())
|
||||
@@ -118,11 +124,11 @@ func (c internalConn) Transport() string {
|
||||
}
|
||||
|
||||
func (c internalConn) String() string {
|
||||
return fmt.Sprintf("%s-%s/%s", c.LocalAddr(), c.RemoteAddr(), c.Type())
|
||||
return fmt.Sprintf("%s-%s/%s/%s", c.LocalAddr(), c.RemoteAddr(), c.Type(), c.Crypto())
|
||||
}
|
||||
|
||||
type dialerFactory interface {
|
||||
New(*config.Wrapper, *tls.Config) genericDialer
|
||||
New(config.Wrapper, *tls.Config) genericDialer
|
||||
Priority() int
|
||||
AlwaysWAN() bool
|
||||
Valid(config.Configuration) error
|
||||
@@ -135,7 +141,7 @@ type genericDialer interface {
|
||||
}
|
||||
|
||||
type listenerFactory interface {
|
||||
New(*url.URL, *config.Wrapper, *tls.Config, chan internalConn, *nat.Service) genericListener
|
||||
New(*url.URL, config.Wrapper, *tls.Config, chan internalConn, *nat.Service) genericListener
|
||||
Valid(config.Configuration) error
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func init() {
|
||||
}
|
||||
|
||||
type tcpDialer struct {
|
||||
cfg *config.Wrapper
|
||||
cfg config.Wrapper
|
||||
tlsCfg *tls.Config
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (d *tcpDialer) RedialFrequency() time.Duration {
|
||||
|
||||
type tcpDialerFactory struct{}
|
||||
|
||||
func (tcpDialerFactory) New(cfg *config.Wrapper, tlsCfg *tls.Config) genericDialer {
|
||||
func (tcpDialerFactory) New(cfg config.Wrapper, tlsCfg *tls.Config) genericDialer {
|
||||
return &tcpDialer{
|
||||
cfg: cfg,
|
||||
tlsCfg: tlsCfg,
|
||||
|
||||
@@ -29,7 +29,7 @@ type tcpListener struct {
|
||||
onAddressesChangedNotifier
|
||||
|
||||
uri *url.URL
|
||||
cfg *config.Wrapper
|
||||
cfg config.Wrapper
|
||||
tlsCfg *tls.Config
|
||||
stop chan struct{}
|
||||
conns chan internalConn
|
||||
@@ -195,7 +195,7 @@ func (t *tcpListener) NATType() string {
|
||||
|
||||
type tcpListenerFactory struct{}
|
||||
|
||||
func (f *tcpListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
|
||||
func (f *tcpListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
|
||||
return &tcpListener{
|
||||
uri: fixupPort(uri, config.DefaultTCPPort),
|
||||
cfg: cfg,
|
||||
|
||||
@@ -73,7 +73,7 @@ func addToBlockMap(db *instance, folder []byte, fs []protocol.FileInfo) {
|
||||
name := []byte(f.Name)
|
||||
for i, block := range f.Blocks {
|
||||
binary.BigEndian.PutUint32(blockBuf, uint32(i))
|
||||
keyBuf = t.db.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
|
||||
keyBuf = t.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
|
||||
t.Put(keyBuf, blockBuf)
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func discardFromBlockMap(db *instance, folder []byte, fs []protocol.FileInfo) {
|
||||
if !ef.IsDirectory() && !ef.IsDeleted() && !ef.IsInvalid() {
|
||||
name := []byte(ef.Name)
|
||||
for _, block := range ef.Blocks {
|
||||
keyBuf = t.db.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
|
||||
keyBuf = t.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
|
||||
t.Delete(keyBuf)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -80,7 +80,7 @@ func (db *instance) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
|
||||
if ok {
|
||||
if !ef.IsDirectory() && !ef.IsDeleted() && !ef.IsInvalid() {
|
||||
for _, block := range ef.Blocks {
|
||||
keyBuf = t.db.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
|
||||
keyBuf = db.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
|
||||
t.Delete(keyBuf)
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ func (db *instance) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
|
||||
l.Debugf("insert (local); folder=%q %v", folder, f)
|
||||
t.Put(dk, mustMarshal(&f))
|
||||
|
||||
gk = t.db.keyer.GenerateGlobalVersionKey(gk, folder, []byte(f.Name))
|
||||
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, []byte(f.Name))
|
||||
keyBuf, _ = t.updateGlobal(gk, keyBuf, folder, protocol.LocalDeviceID[:], f, meta)
|
||||
|
||||
keyBuf = db.keyer.GenerateSequenceKey(keyBuf, folder, f.Sequence)
|
||||
@@ -110,7 +110,7 @@ func (db *instance) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
|
||||
if !f.IsDirectory() && !f.IsDeleted() && !f.IsInvalid() {
|
||||
for i, block := range f.Blocks {
|
||||
binary.BigEndian.PutUint32(blockBuf, uint32(i))
|
||||
keyBuf = t.db.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
|
||||
keyBuf = db.keyer.GenerateBlockMapKey(keyBuf, folder, block.Hash, name)
|
||||
t.Put(keyBuf, blockBuf)
|
||||
}
|
||||
}
|
||||
|
||||
+28
-1
@@ -19,7 +19,8 @@ import (
|
||||
|
||||
const (
|
||||
dbMaxOpenFiles = 100
|
||||
dbWriteBuffer = 4 << 20
|
||||
dbWriteBuffer = 16 << 20
|
||||
dbFlushBatch = dbWriteBuffer / 4 // Some leeway for any leveldb in-memory optimizations
|
||||
)
|
||||
|
||||
// Lowlevel is the lowest level database interface. It has a very simple
|
||||
@@ -127,3 +128,29 @@ func leveldbIsCorrupted(err error) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type batch struct {
|
||||
*leveldb.Batch
|
||||
db *Lowlevel
|
||||
}
|
||||
|
||||
func (db *Lowlevel) newBatch() *batch {
|
||||
return &batch{
|
||||
Batch: new(leveldb.Batch),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// checkFlush flushes and resets the batch if its size exceeds dbFlushBatch.
|
||||
func (b *batch) checkFlush() {
|
||||
if len(b.Dump()) > dbFlushBatch {
|
||||
b.flush()
|
||||
b.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *batch) flush() {
|
||||
if err := b.db.Write(b.Batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-13
@@ -10,7 +10,6 @@ import (
|
||||
"encoding/binary"
|
||||
"time"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
@@ -39,21 +38,12 @@ func NewNamespacedKV(db *Lowlevel, prefix string) *NamespacedKV {
|
||||
func (n *NamespacedKV) Reset() {
|
||||
it := n.db.NewIterator(util.BytesPrefix(n.prefix), nil)
|
||||
defer it.Release()
|
||||
batch := new(leveldb.Batch)
|
||||
batch := n.db.newBatch()
|
||||
for it.Next() {
|
||||
batch.Delete(it.Key())
|
||||
if batch.Len() > batchFlushSize {
|
||||
if err := n.db.Write(batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
batch.Reset()
|
||||
}
|
||||
}
|
||||
if batch.Len() > 0 {
|
||||
if err := n.db.Write(batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
batch.checkFlush()
|
||||
}
|
||||
batch.flush()
|
||||
}
|
||||
|
||||
// PutInt64 stores a new int64. Any existing value (even if of another type)
|
||||
|
||||
@@ -104,18 +104,18 @@ func (db *schemaUpdater) updateSchema0to1() {
|
||||
var gk, buf []byte
|
||||
|
||||
for dbi.Next() {
|
||||
t.checkFlush()
|
||||
|
||||
folder, ok := db.keyer.FolderFromDeviceFileKey(dbi.Key())
|
||||
if !ok {
|
||||
// not having the folder in the index is bad; delete and continue
|
||||
t.Delete(dbi.Key())
|
||||
t.checkFlush()
|
||||
continue
|
||||
}
|
||||
device, ok := db.keyer.DeviceFromDeviceFileKey(dbi.Key())
|
||||
if !ok {
|
||||
// not having the device in the index is bad; delete and continue
|
||||
t.Delete(dbi.Key())
|
||||
t.checkFlush()
|
||||
continue
|
||||
}
|
||||
name := db.keyer.NameFromDeviceFileKey(dbi.Key())
|
||||
@@ -128,7 +128,6 @@ func (db *schemaUpdater) updateSchema0to1() {
|
||||
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
|
||||
buf = t.removeFromGlobal(gk, buf, folder, device, nil, nil)
|
||||
t.Delete(dbi.Key())
|
||||
t.checkFlush()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -149,7 +148,6 @@ func (db *schemaUpdater) updateSchema0to1() {
|
||||
panic("can't happen: " + err.Error())
|
||||
}
|
||||
t.Put(dbi.Key(), bs)
|
||||
t.checkFlush()
|
||||
symlinkConv++
|
||||
}
|
||||
|
||||
@@ -210,7 +208,7 @@ func (db *schemaUpdater) updateSchema2to3() {
|
||||
if !need(f, ok, v) {
|
||||
return true
|
||||
}
|
||||
nk = t.db.keyer.GenerateNeedFileKey(nk, folder, []byte(f.FileName()))
|
||||
nk = t.keyer.GenerateNeedFileKey(nk, folder, []byte(f.FileName()))
|
||||
t.Put(nk, nil)
|
||||
t.checkFlush()
|
||||
return true
|
||||
@@ -282,7 +280,7 @@ func (db *schemaUpdater) updateSchema6to7() {
|
||||
svl, err := t.Get(gk, nil)
|
||||
if err != nil {
|
||||
// If there is no global list, we hardly need it.
|
||||
t.Delete(t.db.keyer.GenerateNeedFileKey(nk, folder, name))
|
||||
t.Delete(t.keyer.GenerateNeedFileKey(nk, folder, name))
|
||||
return true
|
||||
}
|
||||
var fl VersionList
|
||||
@@ -293,7 +291,7 @@ func (db *schemaUpdater) updateSchema6to7() {
|
||||
return true
|
||||
}
|
||||
if localFV, haveLocalFV := fl.Get(protocol.LocalDeviceID[:]); !need(global, haveLocalFV, localFV.Version) {
|
||||
t.Delete(t.db.keyer.GenerateNeedFileKey(nk, folder, name))
|
||||
t.Delete(t.keyer.GenerateNeedFileKey(nk, folder, name))
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
+14
-31
@@ -12,13 +12,10 @@ import (
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
// Flush batches to disk when they contain this many records.
|
||||
const batchFlushSize = 64
|
||||
|
||||
// A readOnlyTransaction represents a database snapshot.
|
||||
type readOnlyTransaction struct {
|
||||
*leveldb.Snapshot
|
||||
db *instance
|
||||
keyer keyer
|
||||
}
|
||||
|
||||
func (db *instance) newReadOnlyTransaction() readOnlyTransaction {
|
||||
@@ -28,7 +25,7 @@ func (db *instance) newReadOnlyTransaction() readOnlyTransaction {
|
||||
}
|
||||
return readOnlyTransaction{
|
||||
Snapshot: snap,
|
||||
db: db,
|
||||
keyer: db.keyer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +34,7 @@ func (t readOnlyTransaction) close() {
|
||||
}
|
||||
|
||||
func (t readOnlyTransaction) getFile(folder, device, file []byte) (protocol.FileInfo, bool) {
|
||||
return t.getFileByKey(t.db.keyer.GenerateDeviceFileKey(nil, folder, device, file))
|
||||
return t.getFileByKey(t.keyer.GenerateDeviceFileKey(nil, folder, device, file))
|
||||
}
|
||||
|
||||
func (t readOnlyTransaction) getFileByKey(key []byte) (protocol.FileInfo, bool) {
|
||||
@@ -65,7 +62,7 @@ func (t readOnlyTransaction) getFileTrunc(key []byte, trunc bool) (FileIntf, boo
|
||||
}
|
||||
|
||||
func (t readOnlyTransaction) getGlobal(keyBuf, folder, file []byte, truncate bool) ([]byte, FileIntf, bool) {
|
||||
keyBuf = t.db.keyer.GenerateGlobalVersionKey(keyBuf, folder, file)
|
||||
keyBuf = t.keyer.GenerateGlobalVersionKey(keyBuf, folder, file)
|
||||
|
||||
bs, err := t.Get(keyBuf, nil)
|
||||
if err != nil {
|
||||
@@ -77,7 +74,7 @@ func (t readOnlyTransaction) getGlobal(keyBuf, folder, file []byte, truncate boo
|
||||
return keyBuf, nil, false
|
||||
}
|
||||
|
||||
keyBuf = t.db.keyer.GenerateDeviceFileKey(keyBuf, folder, vl.Versions[0].Device, file)
|
||||
keyBuf = t.keyer.GenerateDeviceFileKey(keyBuf, folder, vl.Versions[0].Device, file)
|
||||
if fi, ok := t.getFileTrunc(keyBuf, truncate); ok {
|
||||
return keyBuf, fi, true
|
||||
}
|
||||
@@ -90,14 +87,13 @@ func (t readOnlyTransaction) getGlobal(keyBuf, folder, file []byte, truncate boo
|
||||
// batch size.
|
||||
type readWriteTransaction struct {
|
||||
readOnlyTransaction
|
||||
*leveldb.Batch
|
||||
*batch
|
||||
}
|
||||
|
||||
func (db *instance) newReadWriteTransaction() readWriteTransaction {
|
||||
t := db.newReadOnlyTransaction()
|
||||
return readWriteTransaction{
|
||||
readOnlyTransaction: t,
|
||||
Batch: new(leveldb.Batch),
|
||||
readOnlyTransaction: db.newReadOnlyTransaction(),
|
||||
batch: db.newBatch(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,19 +102,6 @@ func (t readWriteTransaction) close() {
|
||||
t.readOnlyTransaction.close()
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) checkFlush() {
|
||||
if t.Batch.Len() > batchFlushSize {
|
||||
t.flush()
|
||||
t.Batch.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) flush() {
|
||||
if err := t.db.Write(t.Batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// updateGlobal adds this device+version to the version list for the given
|
||||
// file. If the device is already present in the list, the version is updated.
|
||||
// If the file does not have an entry in the global list, it is created.
|
||||
@@ -142,7 +125,7 @@ func (t readWriteTransaction) updateGlobal(gk, keyBuf, folder, device []byte, fi
|
||||
// Inserted a new newest version
|
||||
global = file
|
||||
} else {
|
||||
keyBuf = t.db.keyer.GenerateDeviceFileKey(keyBuf, folder, fl.Versions[0].Device, name)
|
||||
keyBuf = t.keyer.GenerateDeviceFileKey(keyBuf, folder, fl.Versions[0].Device, name)
|
||||
if new, ok := t.getFileByKey(keyBuf); ok {
|
||||
global = new
|
||||
} else {
|
||||
@@ -167,7 +150,7 @@ func (t readWriteTransaction) updateGlobal(gk, keyBuf, folder, device []byte, fi
|
||||
// The previous newest version is now at index 1
|
||||
oldGlobalFV = fl.Versions[1]
|
||||
}
|
||||
keyBuf = t.db.keyer.GenerateDeviceFileKey(keyBuf, folder, oldGlobalFV.Device, name)
|
||||
keyBuf = t.keyer.GenerateDeviceFileKey(keyBuf, folder, oldGlobalFV.Device, name)
|
||||
if oldFile, ok := t.getFileByKey(keyBuf); ok {
|
||||
// A failure to get the file here is surprising and our
|
||||
// global size data will be incorrect until a restart...
|
||||
@@ -187,7 +170,7 @@ func (t readWriteTransaction) updateGlobal(gk, keyBuf, folder, device []byte, fi
|
||||
// device according to the version list and global FileInfo given and updates
|
||||
// the db accordingly.
|
||||
func (t readWriteTransaction) updateLocalNeed(keyBuf, folder, name []byte, fl VersionList, global protocol.FileInfo) []byte {
|
||||
keyBuf = t.db.keyer.GenerateNeedFileKey(keyBuf, folder, name)
|
||||
keyBuf = t.keyer.GenerateNeedFileKey(keyBuf, folder, name)
|
||||
hasNeeded, _ := t.Has(keyBuf, nil)
|
||||
if localFV, haveLocalFV := fl.Get(protocol.LocalDeviceID[:]); need(global, haveLocalFV, localFV.Version) {
|
||||
if !hasNeeded {
|
||||
@@ -246,21 +229,21 @@ func (t readWriteTransaction) removeFromGlobal(gk, keyBuf, folder, device []byte
|
||||
if removedAt == 0 {
|
||||
// A failure to get the file here is surprising and our
|
||||
// global size data will be incorrect until a restart...
|
||||
keyBuf = t.db.keyer.GenerateDeviceFileKey(keyBuf, folder, device, file)
|
||||
keyBuf = t.keyer.GenerateDeviceFileKey(keyBuf, folder, device, file)
|
||||
if f, ok := t.getFileByKey(keyBuf); ok {
|
||||
meta.removeFile(protocol.GlobalDeviceID, f)
|
||||
}
|
||||
}
|
||||
|
||||
if len(fl.Versions) == 0 {
|
||||
keyBuf = t.db.keyer.GenerateNeedFileKey(keyBuf, folder, file)
|
||||
keyBuf = t.keyer.GenerateNeedFileKey(keyBuf, folder, file)
|
||||
t.Delete(keyBuf)
|
||||
t.Delete(gk)
|
||||
return keyBuf
|
||||
}
|
||||
|
||||
if removedAt == 0 {
|
||||
keyBuf = t.db.keyer.GenerateDeviceFileKey(keyBuf, folder, fl.Versions[0].Device, file)
|
||||
keyBuf = t.keyer.GenerateDeviceFileKey(keyBuf, folder, fl.Versions[0].Device, file)
|
||||
global, ok := t.getFileByKey(keyBuf)
|
||||
if !ok {
|
||||
panic("This file must exist in the db")
|
||||
|
||||
+18
-18
@@ -144,7 +144,7 @@ func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFileInfo{fi}, err
|
||||
return basicFileInfo{fi}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Remove(name string) error {
|
||||
@@ -184,7 +184,7 @@ func (f *BasicFilesystem) Stat(name string) (FileInfo, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFileInfo{fi}, err
|
||||
return basicFileInfo{fi}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
|
||||
@@ -215,7 +215,7 @@ func (f *BasicFilesystem) Open(name string) (File, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFile{fd, name}, err
|
||||
return basicFile{fd, name}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
||||
@@ -227,7 +227,7 @@ func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFile{fd, name}, err
|
||||
return basicFile{fd, name}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Create(name string) (File, error) {
|
||||
@@ -239,7 +239,7 @@ func (f *BasicFilesystem) Create(name string) (File, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFile{fd, name}, err
|
||||
return basicFile{fd, name}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Walk(root string, walkFn WalkFunc) error {
|
||||
@@ -283,8 +283,8 @@ func (f *BasicFilesystem) URI() string {
|
||||
func (f *BasicFilesystem) SameFile(fi1, fi2 FileInfo) bool {
|
||||
// Like os.SameFile, we always return false unless fi1 and fi2 were created
|
||||
// by this package's Stat/Lstat method.
|
||||
f1, ok1 := fi1.(fsFileInfo)
|
||||
f2, ok2 := fi2.(fsFileInfo)
|
||||
f1, ok1 := fi1.(basicFileInfo)
|
||||
f2, ok2 := fi2.(basicFileInfo)
|
||||
if !ok1 || !ok2 {
|
||||
return false
|
||||
}
|
||||
@@ -292,36 +292,36 @@ func (f *BasicFilesystem) SameFile(fi1, fi2 FileInfo) bool {
|
||||
return os.SameFile(f1.FileInfo, f2.FileInfo)
|
||||
}
|
||||
|
||||
// fsFile implements the fs.File interface on top of an os.File
|
||||
type fsFile struct {
|
||||
// basicFile implements the fs.File interface on top of an os.File
|
||||
type basicFile struct {
|
||||
*os.File
|
||||
name string
|
||||
}
|
||||
|
||||
func (f fsFile) Name() string {
|
||||
func (f basicFile) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f fsFile) Stat() (FileInfo, error) {
|
||||
func (f basicFile) Stat() (FileInfo, error) {
|
||||
info, err := f.File.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFileInfo{info}, nil
|
||||
return basicFileInfo{info}, nil
|
||||
}
|
||||
|
||||
// fsFileInfo implements the fs.FileInfo interface on top of an os.FileInfo.
|
||||
type fsFileInfo struct {
|
||||
// basicFileInfo implements the fs.FileInfo interface on top of an os.FileInfo.
|
||||
type basicFileInfo struct {
|
||||
os.FileInfo
|
||||
}
|
||||
|
||||
func (e fsFileInfo) IsSymlink() bool {
|
||||
// Must use fsFileInfo.Mode() because it may apply magic.
|
||||
func (e basicFileInfo) IsSymlink() bool {
|
||||
// Must use basicFileInfo.Mode() because it may apply magic.
|
||||
return e.Mode()&ModeSymlink != 0
|
||||
}
|
||||
|
||||
func (e fsFileInfo) IsRegular() bool {
|
||||
// Must use fsFileInfo.Mode() because it may apply magic.
|
||||
func (e basicFileInfo) IsRegular() bool {
|
||||
// Must use basicFileInfo.Mode() because it may apply magic.
|
||||
return e.Mode()&ModeType == 0
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (C) 2019 The Syncthing Authors.
|
||||
// Copyright (C) 2017 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,
|
||||
@@ -10,14 +10,18 @@ package fs
|
||||
|
||||
import "syscall"
|
||||
|
||||
func (e fsFileInfo) Owner() int {
|
||||
func (e basicFileInfo) Mode() FileMode {
|
||||
return FileMode(e.FileInfo.Mode())
|
||||
}
|
||||
|
||||
func (e basicFileInfo) Owner() int {
|
||||
if st, ok := e.Sys().(*syscall.Stat_t); ok {
|
||||
return int(st.Uid)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (e fsFileInfo) Group() int {
|
||||
func (e basicFileInfo) Group() int {
|
||||
if st, ok := e.Sys().(*syscall.Stat_t); ok {
|
||||
return int(st.Gid)
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func isWindowsExecutable(path string) bool {
|
||||
return execExts[strings.ToLower(filepath.Ext(path))]
|
||||
}
|
||||
|
||||
func (e fsFileInfo) Mode() FileMode {
|
||||
func (e basicFileInfo) Mode() FileMode {
|
||||
m := e.FileInfo.Mode()
|
||||
if m&os.ModeSymlink != 0 && e.Size() > 0 {
|
||||
// "Symlinks" with nonzero size are in fact "hard" links, such as
|
||||
@@ -48,3 +48,11 @@ func (e fsFileInfo) Mode() FileMode {
|
||||
m &^= 0022
|
||||
return FileMode(m)
|
||||
}
|
||||
|
||||
func (e basicFileInfo) Owner() int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (e basicFileInfo) Group() int {
|
||||
return -1
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright (C) 2019 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 fs
|
||||
|
||||
func (e fsFileInfo) Owner() int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (e fsFileInfo) Group() int {
|
||||
return -1
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (C) 2017 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/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
func (e fsFileInfo) Mode() FileMode {
|
||||
return FileMode(e.FileInfo.Mode())
|
||||
}
|
||||
@@ -25,7 +25,6 @@ const (
|
||||
LevelVerbose
|
||||
LevelInfo
|
||||
LevelWarn
|
||||
LevelFatal
|
||||
NumLevels
|
||||
)
|
||||
|
||||
@@ -49,8 +48,6 @@ type Logger interface {
|
||||
Infof(format string, vals ...interface{})
|
||||
Warnln(vals ...interface{})
|
||||
Warnf(format string, vals ...interface{})
|
||||
Fatalln(vals ...interface{})
|
||||
Fatalf(format string, vals ...interface{})
|
||||
ShouldDebug(facility string) bool
|
||||
SetDebug(facility string, enabled bool)
|
||||
Facilities() map[string]string
|
||||
@@ -190,28 +187,6 @@ func (l *logger) Warnf(format string, vals ...interface{}) {
|
||||
l.callHandlers(LevelWarn, s)
|
||||
}
|
||||
|
||||
// Fatalln logs a line with a FATAL prefix and exits the process with exit
|
||||
// code 1.
|
||||
func (l *logger) Fatalln(vals ...interface{}) {
|
||||
s := fmt.Sprintln(vals...)
|
||||
l.mut.Lock()
|
||||
defer l.mut.Unlock()
|
||||
l.logger.Output(2, "FATAL: "+s)
|
||||
l.callHandlers(LevelFatal, s)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Fatalf logs a formatted line with a FATAL prefix and exits the process with
|
||||
// exit code 1.
|
||||
func (l *logger) Fatalf(format string, vals ...interface{}) {
|
||||
s := fmt.Sprintf(format, vals...)
|
||||
l.mut.Lock()
|
||||
defer l.mut.Unlock()
|
||||
l.logger.Output(2, "FATAL: "+s)
|
||||
l.callHandlers(LevelFatal, s)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// ShouldDebug returns true if the given facility has debugging enabled.
|
||||
func (l *logger) ShouldDebug(facility string) bool {
|
||||
l.mut.Lock()
|
||||
|
||||
+33
-32
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/scanner"
|
||||
"github.com/syncthing/syncthing/lib/stats"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/watchaggregator"
|
||||
)
|
||||
@@ -37,10 +38,14 @@ var errWatchNotStarted = errors.New("not started")
|
||||
type folder struct {
|
||||
stateTracker
|
||||
config.FolderConfiguration
|
||||
*stats.FolderStatisticsReference
|
||||
|
||||
localFlags uint32
|
||||
|
||||
model *Model
|
||||
model *model
|
||||
shortID protocol.ShortID
|
||||
fset *db.FileSet
|
||||
ignores *ignore.Matcher
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
@@ -73,15 +78,18 @@ type puller interface {
|
||||
pull() bool // true when successfull and should not be retried
|
||||
}
|
||||
|
||||
func newFolder(model *Model, cfg config.FolderConfiguration) folder {
|
||||
func newFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration) folder {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return folder{
|
||||
stateTracker: newStateTracker(cfg.ID),
|
||||
FolderConfiguration: cfg,
|
||||
stateTracker: newStateTracker(cfg.ID),
|
||||
FolderConfiguration: cfg,
|
||||
FolderStatisticsReference: stats.NewFolderStatisticsReference(model.db, cfg.ID),
|
||||
|
||||
model: model,
|
||||
shortID: model.shortID,
|
||||
fset: fset,
|
||||
ignores: ignores,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
|
||||
@@ -285,11 +293,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
f.model.fmut.RLock()
|
||||
fset := f.model.folderFiles[f.ID]
|
||||
ignores := f.model.folderIgnores[f.ID]
|
||||
f.model.fmut.RUnlock()
|
||||
mtimefs := fset.MtimeFS()
|
||||
mtimefs := f.fset.MtimeFS()
|
||||
|
||||
f.setState(FolderScanWaiting)
|
||||
scanLimiter.take(1)
|
||||
@@ -311,16 +315,16 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
// Check if the ignore patterns changed as part of scanning this folder.
|
||||
// If they did we should schedule a pull of the folder so that we
|
||||
// request things we might have suddenly become unignored and so on.
|
||||
oldHash := ignores.Hash()
|
||||
oldHash := f.ignores.Hash()
|
||||
defer func() {
|
||||
if ignores.Hash() != oldHash {
|
||||
if f.ignores.Hash() != oldHash {
|
||||
l.Debugln("Folder", f.Description(), "ignore patterns change detected while scanning; triggering puller")
|
||||
f.ignoresUpdated()
|
||||
f.SchedulePull()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
|
||||
if err := f.ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
|
||||
err = fmt.Errorf("loading ignores: %v", err)
|
||||
f.setError(err)
|
||||
return err
|
||||
@@ -329,8 +333,8 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
// Clean the list of subitems to ensure that we start at a known
|
||||
// directory, and don't scan subdirectories of things we've already
|
||||
// scanned.
|
||||
subDirs = unifySubs(subDirs, func(f string) bool {
|
||||
_, ok := fset.Get(protocol.LocalDeviceID, f)
|
||||
subDirs = unifySubs(subDirs, func(file string) bool {
|
||||
_, ok := f.fset.Get(protocol.LocalDeviceID, file)
|
||||
return ok
|
||||
})
|
||||
|
||||
@@ -339,14 +343,14 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
fchan := scanner.Walk(f.ctx, scanner.Config{
|
||||
Folder: f.ID,
|
||||
Subs: subDirs,
|
||||
Matcher: ignores,
|
||||
Matcher: f.ignores,
|
||||
TempLifetime: time.Duration(f.model.cfg.Options().KeepTemporariesH) * time.Hour,
|
||||
CurrentFiler: cFiler{f.model, f.ID},
|
||||
Filesystem: mtimefs,
|
||||
IgnorePerms: f.IgnorePerms,
|
||||
AutoNormalize: f.AutoNormalize,
|
||||
Hashers: f.model.numHashers(f.ID),
|
||||
ShortID: f.model.shortID,
|
||||
ShortID: f.shortID,
|
||||
ProgressTickIntervalS: f.ScanProgressIntervalS,
|
||||
UseLargeBlocks: f.UseLargeBlocks,
|
||||
LocalFlags: f.localFlags,
|
||||
@@ -365,7 +369,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
oldBatchFn := batchFn // can't reference batchFn directly (recursion)
|
||||
batchFn = func(fs []protocol.FileInfo) error {
|
||||
for i := range fs {
|
||||
switch gf, ok := fset.GetGlobal(fs[i].Name); {
|
||||
switch gf, ok := f.fset.GetGlobal(fs[i].Name); {
|
||||
case !ok:
|
||||
continue
|
||||
case gf.IsEquivalentOptional(fs[i], false, false, protocol.FlagLocalReceiveOnly):
|
||||
@@ -425,7 +429,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
for _, sub := range subDirs {
|
||||
var iterError error
|
||||
|
||||
fset.WithPrefixedHaveTruncated(protocol.LocalDeviceID, sub, func(fi db.FileIntf) bool {
|
||||
f.fset.WithPrefixedHaveTruncated(protocol.LocalDeviceID, sub, func(fi db.FileIntf) bool {
|
||||
file := fi.(db.FileInfoTruncated)
|
||||
|
||||
if err := batch.flushIfFull(); err != nil {
|
||||
@@ -436,7 +440,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
if ignoredParent != "" && !fs.IsParent(file.Name, ignoredParent) {
|
||||
for _, file := range toIgnore {
|
||||
l.Debugln("marking file as ignored", file)
|
||||
nf := file.ConvertToIgnoredFileInfo(f.model.id.Short())
|
||||
nf := file.ConvertToIgnoredFileInfo(f.shortID)
|
||||
batch.append(nf)
|
||||
changes++
|
||||
if err := batch.flushIfFull(); err != nil {
|
||||
@@ -448,7 +452,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
ignoredParent = ""
|
||||
}
|
||||
|
||||
switch ignored := ignores.Match(file.Name).IsIgnored(); {
|
||||
switch ignored := f.ignores.Match(file.Name).IsIgnored(); {
|
||||
case !file.IsIgnored() && ignored:
|
||||
// File was not ignored at last pass but has been ignored.
|
||||
if file.IsDirectory() {
|
||||
@@ -463,7 +467,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
}
|
||||
|
||||
l.Debugln("marking file as ignored", f)
|
||||
nf := file.ConvertToIgnoredFileInfo(f.model.id.Short())
|
||||
nf := file.ConvertToIgnoredFileInfo(f.shortID)
|
||||
batch.append(nf)
|
||||
changes++
|
||||
|
||||
@@ -490,9 +494,9 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
Size: 0,
|
||||
ModifiedS: file.ModifiedS,
|
||||
ModifiedNs: file.ModifiedNs,
|
||||
ModifiedBy: f.model.id.Short(),
|
||||
ModifiedBy: f.shortID,
|
||||
Deleted: true,
|
||||
Version: file.Version.Update(f.model.shortID),
|
||||
Version: file.Version.Update(f.shortID),
|
||||
LocalFlags: f.localFlags,
|
||||
}
|
||||
// We do not want to override the global version
|
||||
@@ -501,7 +505,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
// other existing versions, which will be resolved
|
||||
// by the normal pulling mechanisms.
|
||||
if file.ShouldConflict() {
|
||||
nf.Version = nf.Version.DropOthers(f.model.shortID)
|
||||
nf.Version = nf.Version.DropOthers(f.shortID)
|
||||
}
|
||||
|
||||
batch.append(nf)
|
||||
@@ -513,7 +517,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
if iterError == nil && len(toIgnore) > 0 {
|
||||
for _, file := range toIgnore {
|
||||
l.Debugln("marking file as ignored", f)
|
||||
nf := file.ConvertToIgnoredFileInfo(f.model.id.Short())
|
||||
nf := file.ConvertToIgnoredFileInfo(f.shortID)
|
||||
batch.append(nf)
|
||||
changes++
|
||||
if iterError = batch.flushIfFull(); iterError != nil {
|
||||
@@ -532,7 +536,7 @@ func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
f.model.folderStatRef(f.ID).ScanCompleted()
|
||||
f.ScanCompleted()
|
||||
f.setState(FolderIdle)
|
||||
return nil
|
||||
}
|
||||
@@ -603,24 +607,21 @@ func (f *folder) restartWatch() {
|
||||
// this asynchronously, you should probably use scheduleWatchRestart instead.
|
||||
func (f *folder) startWatch() {
|
||||
ctx, cancel := context.WithCancel(f.ctx)
|
||||
f.model.fmut.RLock()
|
||||
ignores := f.model.folderIgnores[f.folderID]
|
||||
f.model.fmut.RUnlock()
|
||||
f.watchMut.Lock()
|
||||
f.watchChan = make(chan []string)
|
||||
f.watchCancel = cancel
|
||||
f.watchMut.Unlock()
|
||||
go f.startWatchAsync(ctx, ignores)
|
||||
go f.startWatchAsync(ctx)
|
||||
}
|
||||
|
||||
// startWatchAsync tries to start the filesystem watching and retries every minute on failure.
|
||||
// It is a convenience function that should not be used except in startWatch.
|
||||
func (f *folder) startWatchAsync(ctx context.Context, ignores *ignore.Matcher) {
|
||||
func (f *folder) startWatchAsync(ctx context.Context) {
|
||||
timer := time.NewTimer(0)
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
eventChan, err := f.Filesystem().Watch(".", ignores, ctx, f.IgnorePerms)
|
||||
eventChan, err := f.Filesystem().Watch(".", f.ignores, ctx, f.IgnorePerms)
|
||||
f.watchMut.Lock()
|
||||
prevErr := f.watchErr
|
||||
f.watchErr = err
|
||||
|
||||
@@ -56,8 +56,8 @@ type receiveOnlyFolder struct {
|
||||
*sendReceiveFolder
|
||||
}
|
||||
|
||||
func newReceiveOnlyFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem) service {
|
||||
sr := newSendReceiveFolder(model, cfg, ver, fs).(*sendReceiveFolder)
|
||||
func newReceiveOnlyFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem) service {
|
||||
sr := newSendReceiveFolder(model, fset, ignores, cfg, ver, fs).(*sendReceiveFolder)
|
||||
sr.localFlags = protocol.FlagLocalReceiveOnly // gets propagated to the scanner, and set on locally changed files
|
||||
return &receiveOnlyFolder{sr}
|
||||
}
|
||||
@@ -66,18 +66,13 @@ func (f *receiveOnlyFolder) Revert(fs *db.FileSet, updateFn func([]protocol.File
|
||||
f.setState(FolderScanning)
|
||||
defer f.setState(FolderIdle)
|
||||
|
||||
// XXX: This *really* should be given to us in the constructor...
|
||||
f.model.fmut.RLock()
|
||||
ignores := f.model.folderIgnores[f.folderID]
|
||||
f.model.fmut.RUnlock()
|
||||
|
||||
scanChan := make(chan string)
|
||||
go f.pullScannerRoutine(scanChan)
|
||||
defer close(scanChan)
|
||||
|
||||
delQueue := &deleteQueue{
|
||||
handler: f, // for the deleteFile and deleteDir methods
|
||||
ignores: ignores,
|
||||
handler: f, // for the deleteItemOnDisk and deleteDirOnDisk methods
|
||||
ignores: f.ignores,
|
||||
scanChan: scanChan,
|
||||
}
|
||||
|
||||
@@ -171,8 +166,8 @@ func (f *receiveOnlyFolder) Revert(fs *db.FileSet, updateFn func([]protocol.File
|
||||
// directories for last.
|
||||
type deleteQueue struct {
|
||||
handler interface {
|
||||
deleteFile(file protocol.FileInfo, scanChan chan<- string) (dbUpdateJob, error)
|
||||
deleteDir(dir string, ignores *ignore.Matcher, scanChan chan<- string) error
|
||||
deleteItemOnDisk(item protocol.FileInfo, scanChan chan<- string) error
|
||||
deleteDirOnDisk(dir string, scanChan chan<- string) error
|
||||
}
|
||||
ignores *ignore.Matcher
|
||||
dirs []string
|
||||
@@ -193,7 +188,7 @@ func (q *deleteQueue) handle(fi protocol.FileInfo) (bool, error) {
|
||||
}
|
||||
|
||||
// Kill it.
|
||||
_, err := q.handler.deleteFile(fi, q.scanChan)
|
||||
err := q.handler.deleteItemOnDisk(fi, q.scanChan)
|
||||
return true, err
|
||||
}
|
||||
|
||||
@@ -205,7 +200,7 @@ func (q *deleteQueue) flush() ([]string, error) {
|
||||
var deleted []string
|
||||
|
||||
for _, dir := range q.dirs {
|
||||
if err := q.handler.deleteDir(dir, q.ignores, q.scanChan); err == nil {
|
||||
if err := q.handler.deleteDirOnDisk(dir, q.scanChan); err == nil {
|
||||
deleted = append(deleted, dir)
|
||||
} else if err != nil && firstError == nil {
|
||||
firstError = err
|
||||
|
||||
@@ -35,15 +35,9 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
|
||||
testOs.MkdirAll("_recvonly/.stfolder", 0755)
|
||||
testOs.MkdirAll("_recvonly/ignDir", 0755)
|
||||
testOs.MkdirAll("_recvonly/unknownDir", 0755)
|
||||
if err := ioutil.WriteFile("_recvonly/ignDir/ignFile", []byte("hello\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile("_recvonly/unknownDir/unknownFile", []byte("hello\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile("_recvonly/.stignore", []byte("ignDir\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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))
|
||||
|
||||
knownFiles := setupKnownFiles(t, []byte("hello\n"))
|
||||
|
||||
@@ -166,15 +160,11 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
|
||||
// Update the file.
|
||||
|
||||
newData := []byte("totally different data\n")
|
||||
if err := ioutil.WriteFile("_recvonly/knownDir/knownFile", newData, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, ioutil.WriteFile("_recvonly/knownDir/knownFile", newData, 0644))
|
||||
|
||||
// Rescan.
|
||||
|
||||
if err := m.ScanFolder("ro"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, m.ScanFolder("ro"))
|
||||
|
||||
// We now have a newer file than the rest of the cluster. Global state should reflect this.
|
||||
|
||||
@@ -268,13 +258,9 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
|
||||
// Create a file and modify another
|
||||
|
||||
file := "_recvonly/foo"
|
||||
if err := ioutil.WriteFile(file, []byte("hello\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, ioutil.WriteFile(file, []byte("hello\n"), 0644))
|
||||
|
||||
if err := ioutil.WriteFile("_recvonly/knownDir/knownFile", []byte("bye\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, ioutil.WriteFile("_recvonly/knownDir/knownFile", []byte("bye\n"), 0644))
|
||||
|
||||
m.ScanFolder("ro")
|
||||
|
||||
@@ -286,9 +272,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
|
||||
// Remove the file again and undo the modification
|
||||
|
||||
testOs.Remove(file)
|
||||
if err := ioutil.WriteFile("_recvonly/knownDir/knownFile", oldData, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, ioutil.WriteFile("_recvonly/knownDir/knownFile", oldData, 0644))
|
||||
folderFs.Chtimes("knownDir/knownFile", knownFiles[1].ModTime(), knownFiles[1].ModTime())
|
||||
|
||||
m.ScanFolder("ro")
|
||||
@@ -303,9 +287,7 @@ func setupKnownFiles(t *testing.T, data []byte) []protocol.FileInfo {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
testOs.MkdirAll("_recvonly/knownDir", 0755)
|
||||
if err := ioutil.WriteFile("_recvonly/knownDir/knownFile", data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, ioutil.WriteFile("_recvonly/knownDir/knownFile", data, 0644))
|
||||
|
||||
t0 := time.Now().Add(-1 * time.Minute)
|
||||
testOs.Chtimes("_recvonly/knownDir/knownFile", t0, t0)
|
||||
@@ -336,7 +318,7 @@ func setupKnownFiles(t *testing.T, data []byte) []protocol.FileInfo {
|
||||
return knownFiles
|
||||
}
|
||||
|
||||
func setupROFolder() *Model {
|
||||
func setupROFolder() *model {
|
||||
fcfg := config.NewFolderConfiguration(myID, "ro", "receive only test", fs.FilesystemTypeBasic, "_recvonly")
|
||||
fcfg.Type = config.FolderTypeReceiveOnly
|
||||
fcfg.Devices = []config.FolderDeviceConfiguration{{DeviceID: device1}}
|
||||
@@ -349,7 +331,7 @@ func setupROFolder() *Model {
|
||||
wrp := createTmpWrapper(cfg)
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(wrp, myID, "syncthing", "dev", db, nil)
|
||||
m := newModel(wrp, myID, "syncthing", "dev", db, nil)
|
||||
|
||||
m.ServeBackground()
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/ignore"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/versioner"
|
||||
)
|
||||
@@ -22,9 +23,9 @@ type sendOnlyFolder struct {
|
||||
folder
|
||||
}
|
||||
|
||||
func newSendOnlyFolder(model *Model, cfg config.FolderConfiguration, _ versioner.Versioner, _ fs.Filesystem) service {
|
||||
func newSendOnlyFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, _ versioner.Versioner, _ fs.Filesystem) service {
|
||||
f := &sendOnlyFolder{
|
||||
folder: newFolder(model, cfg),
|
||||
folder: newFolder(model, fset, ignores, cfg),
|
||||
}
|
||||
f.folder.puller = f
|
||||
return f
|
||||
@@ -43,22 +44,17 @@ func (f *sendOnlyFolder) pull() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
f.model.fmut.RLock()
|
||||
folderFiles := f.model.folderFiles[f.folderID]
|
||||
ignores := f.model.folderIgnores[f.folderID]
|
||||
f.model.fmut.RUnlock()
|
||||
|
||||
batch := make([]protocol.FileInfo, 0, maxBatchSizeFiles)
|
||||
batchSizeBytes := 0
|
||||
|
||||
folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
f.fset.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
if len(batch) == maxBatchSizeFiles || batchSizeBytes > maxBatchSizeBytes {
|
||||
f.model.updateLocalsFromPulling(f.folderID, batch)
|
||||
batch = batch[:0]
|
||||
batchSizeBytes = 0
|
||||
}
|
||||
|
||||
if ignores.ShouldIgnore(intf.FileName()) {
|
||||
if f.ignores.ShouldIgnore(intf.FileName()) {
|
||||
file := intf.(protocol.FileInfo)
|
||||
file.SetIgnored(f.shortID)
|
||||
batch = append(batch, file)
|
||||
@@ -67,7 +63,7 @@ func (f *sendOnlyFolder) pull() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
curFile, ok := f.model.CurrentFolderFile(f.folderID, intf.FileName())
|
||||
curFile, ok := f.fset.Get(protocol.LocalDeviceID, intf.FileName())
|
||||
if !ok {
|
||||
if intf.IsDeleted() {
|
||||
panic("Should never get a deleted file as needed when we don't have it")
|
||||
|
||||
+289
-239
@@ -60,13 +60,16 @@ type copyBlocksState struct {
|
||||
const retainBits = fs.ModeSetgid | fs.ModeSetuid | fs.ModeSticky
|
||||
|
||||
var (
|
||||
activity = newDeviceActivity()
|
||||
errNoDevice = errors.New("peers who had this file went away, or the file has changed while syncing. will retry later")
|
||||
errDirHasToBeScanned = errors.New("directory contains unexpected files, scheduling scan")
|
||||
errDirHasIgnored = errors.New("directory contains ignored files (see ignore documentation for (?d) prefix)")
|
||||
errDirNotEmpty = errors.New("directory is not empty; files within are probably ignored on connected devices only")
|
||||
errNotAvailable = errors.New("no connected device has the required version of this file")
|
||||
errModified = errors.New("file modified but not rescanned; will try again later")
|
||||
activity = newDeviceActivity()
|
||||
errNoDevice = errors.New("peers who had this file went away, or the file has changed while syncing. will retry later")
|
||||
errDirHasToBeScanned = errors.New("directory contains unexpected files, scheduling scan")
|
||||
errDirHasIgnored = errors.New("directory contains ignored files (see ignore documentation for (?d) prefix)")
|
||||
errDirNotEmpty = errors.New("directory is not empty; files within are probably ignored on connected devices only")
|
||||
errNotAvailable = errors.New("no connected device has the required version of this file")
|
||||
errModified = errors.New("file modified but not rescanned; will try again later")
|
||||
errUnexpectedDirOnFileDel = errors.New("encountered directory when trying to remove file/symlink")
|
||||
errIncompatibleSymlink = errors.New("incompatible symlink entry; rescan with newer Syncthing on source")
|
||||
contextRemovingOldItem = "removing item to be replaced"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -104,9 +107,9 @@ type sendReceiveFolder struct {
|
||||
pullErrorsMut sync.Mutex
|
||||
}
|
||||
|
||||
func newSendReceiveFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem) service {
|
||||
func newSendReceiveFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem) service {
|
||||
f := &sendReceiveFolder{
|
||||
folder: newFolder(model, cfg),
|
||||
folder: newFolder(model, fset, ignores, cfg),
|
||||
fs: fs,
|
||||
versioner: ver,
|
||||
queue: newJobQueue(),
|
||||
@@ -141,14 +144,9 @@ func (f *sendReceiveFolder) pull() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
f.model.fmut.RLock()
|
||||
ignores := f.model.folderIgnores[f.folderID]
|
||||
folderFiles := f.model.folderFiles[f.folderID]
|
||||
f.model.fmut.RUnlock()
|
||||
|
||||
// If there is nothing to do, don't even enter pulling state.
|
||||
abort := true
|
||||
folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
f.fset.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
abort = false
|
||||
return false
|
||||
})
|
||||
@@ -162,13 +160,13 @@ func (f *sendReceiveFolder) pull() bool {
|
||||
}
|
||||
|
||||
// Check if the ignore patterns changed.
|
||||
oldHash := ignores.Hash()
|
||||
oldHash := f.ignores.Hash()
|
||||
defer func() {
|
||||
if ignores.Hash() != oldHash {
|
||||
if f.ignores.Hash() != oldHash {
|
||||
f.ignoresUpdated()
|
||||
}
|
||||
}()
|
||||
if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
|
||||
if err := f.ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
|
||||
err = fmt.Errorf("loading ignores: %v", err)
|
||||
f.setError(err)
|
||||
return false
|
||||
@@ -194,9 +192,9 @@ func (f *sendReceiveFolder) pull() bool {
|
||||
default:
|
||||
}
|
||||
|
||||
changed := f.pullerIteration(ignores, folderFiles, scanChan)
|
||||
changed := f.pullerIteration(scanChan)
|
||||
|
||||
l.Debugln(f, "changed", changed, "on try", tries)
|
||||
l.Debugln(f, "changed", changed, "on try", tries+1)
|
||||
|
||||
if changed == 0 {
|
||||
// No files were changed by the puller, so we are in
|
||||
@@ -224,7 +222,7 @@ func (f *sendReceiveFolder) pull() bool {
|
||||
// returns the number items that should have been synced (even those that
|
||||
// might have failed). One puller iteration handles all files currently
|
||||
// flagged as needed in the folder.
|
||||
func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher, folderFiles *db.FileSet, scanChan chan<- string) int {
|
||||
func (f *sendReceiveFolder) pullerIteration(scanChan chan<- string) int {
|
||||
pullChan := make(chan pullBlockState)
|
||||
copyChan := make(chan copyBlocksState)
|
||||
finisherChan := make(chan *sharedPullerState)
|
||||
@@ -263,11 +261,11 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher, folderFiles
|
||||
doneWg.Add(1)
|
||||
// finisherRoutine finishes when finisherChan is closed
|
||||
go func() {
|
||||
f.finisherRoutine(ignores, finisherChan, dbUpdateChan, scanChan)
|
||||
f.finisherRoutine(finisherChan, dbUpdateChan, scanChan)
|
||||
doneWg.Done()
|
||||
}()
|
||||
|
||||
changed, fileDeletions, dirDeletions, err := f.processNeeded(ignores, folderFiles, dbUpdateChan, copyChan, finisherChan, scanChan)
|
||||
changed, fileDeletions, dirDeletions, err := f.processNeeded(dbUpdateChan, copyChan, finisherChan, scanChan)
|
||||
|
||||
// Signal copy and puller routines that we are done with the in data for
|
||||
// this iteration. Wait for them to finish.
|
||||
@@ -282,7 +280,7 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher, folderFiles
|
||||
doneWg.Wait()
|
||||
|
||||
if err == nil {
|
||||
f.processDeletions(ignores, fileDeletions, dirDeletions, dbUpdateChan, scanChan)
|
||||
f.processDeletions(fileDeletions, dirDeletions, dbUpdateChan, scanChan)
|
||||
}
|
||||
|
||||
// Wait for db updates and scan scheduling to complete
|
||||
@@ -292,7 +290,7 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher, folderFiles
|
||||
return changed
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) processNeeded(ignores *ignore.Matcher, folderFiles *db.FileSet, 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, finisherChan chan<- *sharedPullerState, scanChan chan<- string) (int, map[string]protocol.FileInfo, []protocol.FileInfo, error) {
|
||||
changed := 0
|
||||
var processDirectly []protocol.FileInfo
|
||||
var dirDeletions []protocol.FileInfo
|
||||
@@ -303,7 +301,7 @@ func (f *sendReceiveFolder) processNeeded(ignores *ignore.Matcher, folderFiles *
|
||||
// Regular files to pull goes into the file queue, everything else
|
||||
// (directories, symlinks and deletes) goes into the "process directly"
|
||||
// pile.
|
||||
folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
f.fset.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
select {
|
||||
case <-f.ctx.Done():
|
||||
return false
|
||||
@@ -318,14 +316,14 @@ func (f *sendReceiveFolder) processNeeded(ignores *ignore.Matcher, folderFiles *
|
||||
file := intf.(protocol.FileInfo)
|
||||
|
||||
switch {
|
||||
case ignores.ShouldIgnore(file.Name):
|
||||
case f.ignores.ShouldIgnore(file.Name):
|
||||
file.SetIgnored(f.shortID)
|
||||
l.Debugln(f, "Handling ignored file", file)
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateInvalidate}
|
||||
changed++
|
||||
|
||||
case runtime.GOOS == "windows" && fs.WindowsInvalidFilename(file.Name):
|
||||
f.newPullError("pull", file.Name, fs.ErrInvalidFilename)
|
||||
f.newPullError(file.Name, fs.ErrInvalidFilename)
|
||||
|
||||
case file.IsDeleted():
|
||||
if file.IsDirectory() {
|
||||
@@ -334,7 +332,7 @@ func (f *sendReceiveFolder) processNeeded(ignores *ignore.Matcher, folderFiles *
|
||||
dirDeletions = append(dirDeletions, file)
|
||||
} else {
|
||||
fileDeletions[file.Name] = file
|
||||
df, ok := f.model.CurrentFolderFile(f.folderID, file.Name)
|
||||
df, ok := f.fset.Get(protocol.LocalDeviceID, file.Name)
|
||||
// Local file can be already deleted, but with a lower version
|
||||
// number, hence the deletion coming in again as part of
|
||||
// WithNeed, furthermore, the file can simply be of the wrong
|
||||
@@ -394,12 +392,11 @@ func (f *sendReceiveFolder) processNeeded(ignores *ignore.Matcher, folderFiles *
|
||||
switch {
|
||||
case fi.IsDirectory() && !fi.IsSymlink():
|
||||
l.Debugln(f, "Handling directory", fi.Name)
|
||||
f.handleDir(fi, dbUpdateChan)
|
||||
f.handleDir(fi, dbUpdateChan, scanChan)
|
||||
|
||||
case fi.IsSymlink():
|
||||
l.Debugln("Handling symlink", fi.Name)
|
||||
l.Debugln(f, "Handling symlink", fi.Name)
|
||||
f.handleSymlink(fi, dbUpdateChan)
|
||||
f.handleSymlink(fi, dbUpdateChan, scanChan)
|
||||
|
||||
default:
|
||||
l.Warnln(fi)
|
||||
@@ -439,7 +436,7 @@ nextFile:
|
||||
break
|
||||
}
|
||||
|
||||
fi, ok := f.model.CurrentGlobalFile(f.folderID, fileName)
|
||||
fi, ok := f.fset.GetGlobal(fileName)
|
||||
if !ok {
|
||||
// File is no longer in the index. Mark it as done and drop it.
|
||||
f.queue.Done(fileName)
|
||||
@@ -472,7 +469,7 @@ nextFile:
|
||||
// desired state with the delete bit set is in the deletion
|
||||
// map.
|
||||
desired := fileDeletions[candidate.Name]
|
||||
if err := f.renameFile(candidate, desired, fi, ignores, dbUpdateChan, scanChan); err != nil {
|
||||
if err := f.renameFile(candidate, desired, fi, dbUpdateChan, scanChan); err != nil {
|
||||
// Failed to rename, try to handle files as separate
|
||||
// deletions and updates.
|
||||
break
|
||||
@@ -486,7 +483,7 @@ nextFile:
|
||||
}
|
||||
}
|
||||
|
||||
devices := folderFiles.Availability(fileName)
|
||||
devices := f.fset.Availability(fileName)
|
||||
for _, dev := range devices {
|
||||
if _, ok := f.model.Connection(dev); ok {
|
||||
changed++
|
||||
@@ -495,13 +492,13 @@ nextFile:
|
||||
continue nextFile
|
||||
}
|
||||
}
|
||||
f.newPullError("pull", fileName, errNotAvailable)
|
||||
f.newPullError(fileName, errNotAvailable)
|
||||
}
|
||||
|
||||
return changed, fileDeletions, dirDeletions, nil
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) processDeletions(ignores *ignore.Matcher, fileDeletions map[string]protocol.FileInfo, dirDeletions []protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
|
||||
func (f *sendReceiveFolder) processDeletions(fileDeletions map[string]protocol.FileInfo, dirDeletions []protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
|
||||
for _, file := range fileDeletions {
|
||||
select {
|
||||
case <-f.ctx.Done():
|
||||
@@ -511,7 +508,7 @@ func (f *sendReceiveFolder) processDeletions(ignores *ignore.Matcher, fileDeleti
|
||||
|
||||
l.Debugln(f, "Deleting file", file.Name)
|
||||
if update, err := f.deleteFile(file, scanChan); err != nil {
|
||||
f.newPullError("delete file", file.Name, err)
|
||||
f.newPullError(file.Name, errors.Wrap(err, "delete file"))
|
||||
} else {
|
||||
dbUpdateChan <- update
|
||||
}
|
||||
@@ -526,12 +523,12 @@ func (f *sendReceiveFolder) processDeletions(ignores *ignore.Matcher, fileDeleti
|
||||
|
||||
dir := dirDeletions[len(dirDeletions)-i-1]
|
||||
l.Debugln(f, "Deleting dir", dir.Name)
|
||||
f.handleDeleteDir(dir, ignores, dbUpdateChan, scanChan)
|
||||
f.deleteDir(dir, dbUpdateChan, scanChan)
|
||||
}
|
||||
}
|
||||
|
||||
// handleDir creates or updates the given directory
|
||||
func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob) {
|
||||
func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
@@ -559,19 +556,45 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<
|
||||
}
|
||||
|
||||
if shouldDebug() {
|
||||
curFile, _ := f.model.CurrentFolderFile(f.folderID, file.Name)
|
||||
curFile, _ := f.fset.Get(protocol.LocalDeviceID, file.Name)
|
||||
l.Debugf("need dir\n\t%v\n\t%v", file, curFile)
|
||||
}
|
||||
|
||||
info, err := f.fs.Lstat(file.Name)
|
||||
switch {
|
||||
// There is already something under that name, but it's a file/link.
|
||||
// Most likely a file/link is getting replaced with a directory.
|
||||
// Remove the file/link and fall through to directory creation.
|
||||
case err == nil && (!info.IsDir() || info.IsSymlink()):
|
||||
err = osutil.InWritableDir(f.fs.Remove, f.fs, file.Name)
|
||||
// There is already something under that name, we need to handle that.
|
||||
// Unless it already is a directory, as we only track permissions,
|
||||
// that don't result in a conflict.
|
||||
case err == nil && !info.IsDir():
|
||||
// Check that it is what we have in the database.
|
||||
curFile, hasCurFile := f.model.CurrentFolderFile(f.folderID, file.Name)
|
||||
if changed, err := f.itemChanged(info, curFile, hasCurFile, scanChan); err != nil {
|
||||
f.newPullError(file.Name, err)
|
||||
return
|
||||
} else if changed {
|
||||
l.Debugln("item changed on disk compared to db; not replacing with dir:", file.Name)
|
||||
scanChan <- curFile.Name
|
||||
f.newPullError(file.Name, errModified)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove it to replace with the dir.
|
||||
if !curFile.IsSymlink() && f.inConflict(curFile.Version, file.Version) {
|
||||
// The new file has been changed in conflict with the existing one. We
|
||||
// should file it away as a conflict instead of just removing or
|
||||
// archiving. Also merge with the version vector we had, to indicate
|
||||
// we have resolved the conflict.
|
||||
// Symlinks aren't checked for conflicts.
|
||||
|
||||
file.Version = file.Version.Merge(curFile.Version)
|
||||
err = osutil.InWritableDir(func(name string) error {
|
||||
return f.moveForConflict(name, file.ModifiedBy.String(), scanChan)
|
||||
}, f.fs, curFile.Name)
|
||||
} else {
|
||||
err = f.deleteItemOnDisk(file, scanChan)
|
||||
}
|
||||
if err != nil {
|
||||
f.newPullError("dir replace", file.Name, err)
|
||||
f.newPullError(file.Name, err)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
@@ -606,26 +629,26 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<
|
||||
if err = osutil.InWritableDir(mkdir, f.fs, file.Name); err == nil {
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateHandleDir}
|
||||
} else {
|
||||
f.newPullError("dir mkdir", file.Name, err)
|
||||
f.newPullError(file.Name, errors.Wrap(err, "creating directory"))
|
||||
}
|
||||
return
|
||||
// Weird error when stat()'ing the dir. Probably won't work to do
|
||||
// anything else with it if we can't even stat() it.
|
||||
case err != nil:
|
||||
f.newPullError("dir stat", file.Name, err)
|
||||
f.newPullError(file.Name, errors.Wrap(err, "checking file to be replaced"))
|
||||
return
|
||||
}
|
||||
|
||||
// The directory already exists, so we just correct the mode bits. (We
|
||||
// don't handle modification times on directories, because that sucks...)
|
||||
// It's OK to change mode bits on stuff within non-writable directories.
|
||||
if f.IgnorePerms || file.NoPermissions {
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateHandleDir}
|
||||
} else if err := f.fs.Chmod(file.Name, mode|(fs.FileMode(info.Mode())&retainBits)); err == nil {
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateHandleDir}
|
||||
} else {
|
||||
f.newPullError("dir chmod", file.Name, err)
|
||||
if !f.IgnorePerms && !file.NoPermissions {
|
||||
if err := f.fs.Chmod(file.Name, mode|(fs.FileMode(info.Mode())&retainBits)); err != nil {
|
||||
f.newPullError(file.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateHandleDir}
|
||||
}
|
||||
|
||||
// checkParent verifies that the thing we are handling lives inside a directory,
|
||||
@@ -634,7 +657,7 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
|
||||
parent := filepath.Dir(file)
|
||||
|
||||
if err := osutil.TraversesSymlink(f.fs, parent); err != nil {
|
||||
f.newPullError("traverses q", file, err)
|
||||
f.newPullError(file, errors.Wrap(err, "checking parent dirs"))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -655,7 +678,7 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
|
||||
}
|
||||
l.Debugf("%v resurrecting parent directory of %v", f, file)
|
||||
if err := f.fs.MkdirAll(parent, 0755); err != nil {
|
||||
f.newPullError("resurrecting parent dir", file, err)
|
||||
f.newPullError(file, errors.Wrap(err, "resurrecting parent dir"))
|
||||
return false
|
||||
}
|
||||
scanChan <- parent
|
||||
@@ -663,7 +686,7 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
|
||||
}
|
||||
|
||||
// handleSymlink creates or updates the given symlink
|
||||
func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob) {
|
||||
func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
@@ -686,25 +709,48 @@ func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, dbUpdateChan c
|
||||
}()
|
||||
|
||||
if shouldDebug() {
|
||||
curFile, _ := f.model.CurrentFolderFile(f.folderID, file.Name)
|
||||
curFile, _ := f.fset.Get(protocol.LocalDeviceID, file.Name)
|
||||
l.Debugf("need symlink\n\t%v\n\t%v", file, curFile)
|
||||
}
|
||||
|
||||
if len(file.SymlinkTarget) == 0 {
|
||||
// Index entry from a Syncthing predating the support for including
|
||||
// the link target in the index entry. We log this as an error.
|
||||
err = errors.New("incompatible symlink entry; rescan with newer Syncthing on source")
|
||||
f.newPullError("symlink", file.Name, err)
|
||||
f.newPullError(file.Name, errIncompatibleSymlink)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = f.fs.Lstat(file.Name); err == nil {
|
||||
// There is already something under that name. Remove it to replace
|
||||
// with the symlink. This also handles the "change symlink type"
|
||||
// path.
|
||||
err = osutil.InWritableDir(f.fs.Remove, f.fs, file.Name)
|
||||
// There is already something under that name, we need to handle that.
|
||||
if info, err := f.fs.Lstat(file.Name); err == nil {
|
||||
// Check that it is what we have in the database.
|
||||
curFile, hasCurFile := f.model.CurrentFolderFile(f.folderID, file.Name)
|
||||
if changed, err := f.itemChanged(info, curFile, hasCurFile, scanChan); err != nil {
|
||||
f.newPullError(file.Name, err)
|
||||
return
|
||||
} else if changed {
|
||||
l.Debugln("item changed on disk compared to db; not replacing with symlink:", file.Name)
|
||||
scanChan <- curFile.Name
|
||||
f.newPullError(file.Name, errModified)
|
||||
return
|
||||
}
|
||||
// Remove it to replace with the symlink. This also handles the
|
||||
// "change symlink type" path.
|
||||
if !curFile.IsDirectory() && !curFile.IsSymlink() && f.inConflict(curFile.Version, file.Version) {
|
||||
// The new file has been changed in conflict with the existing one. We
|
||||
// should file it away as a conflict instead of just removing or
|
||||
// archiving. Also merge with the version vector we had, to indicate
|
||||
// we have resolved the conflict.
|
||||
// Directories and symlinks aren't checked for conflicts.
|
||||
|
||||
file.Version = file.Version.Merge(curFile.Version)
|
||||
err = osutil.InWritableDir(func(name string) error {
|
||||
return f.moveForConflict(name, file.ModifiedBy.String(), scanChan)
|
||||
}, f.fs, curFile.Name)
|
||||
} else {
|
||||
err = f.deleteItemOnDisk(file, scanChan)
|
||||
}
|
||||
if err != nil {
|
||||
f.newPullError("symlink remove", file.Name, err)
|
||||
f.newPullError(file.Name, errors.Wrap(err, "symlink remove"))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -721,12 +767,12 @@ func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, dbUpdateChan c
|
||||
if err = osutil.InWritableDir(createLink, f.fs, file.Name); err == nil {
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateHandleSymlink}
|
||||
} else {
|
||||
f.newPullError("symlink create", file.Name, err)
|
||||
f.newPullError(file.Name, errors.Wrap(err, "symlink create"))
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteDir attempts to remove a directory that was deleted on a remote
|
||||
func (f *sendReceiveFolder) handleDeleteDir(file protocol.FileInfo, ignores *ignore.Matcher, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
|
||||
// deleteDir attempts to remove a directory that was deleted on a remote
|
||||
func (f *sendReceiveFolder) deleteDir(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
@@ -748,8 +794,8 @@ func (f *sendReceiveFolder) handleDeleteDir(file protocol.FileInfo, ignores *ign
|
||||
})
|
||||
}()
|
||||
|
||||
if err = f.deleteDir(file.Name, ignores, scanChan); err != nil {
|
||||
f.newPullError("delete dir", file.Name, err)
|
||||
if err = f.deleteDirOnDisk(file.Name, scanChan); err != nil {
|
||||
f.newPullError(file.Name, errors.Wrap(err, "delete dir"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -779,7 +825,7 @@ func (f *sendReceiveFolder) deleteFile(file protocol.FileInfo, scanChan chan<- s
|
||||
})
|
||||
}()
|
||||
|
||||
cur, ok := f.model.CurrentFolderFile(f.folderID, file.Name)
|
||||
cur, ok := f.fset.Get(protocol.LocalDeviceID, file.Name)
|
||||
if !ok {
|
||||
// We should never try to pull a deletion for a file we don't have in the DB.
|
||||
l.Debugln(f, "not deleting file we don't have", file.Name)
|
||||
@@ -789,15 +835,21 @@ func (f *sendReceiveFolder) deleteFile(file protocol.FileInfo, scanChan chan<- s
|
||||
return dbUpdateJob{}, err
|
||||
}
|
||||
|
||||
// We are asked to delete a file, but what we have on disk and in db
|
||||
// is a directory. Something is wrong here, should probably not happen.
|
||||
if cur.IsDirectory() {
|
||||
return dbUpdateJob{}, errUnexpectedDirOnFileDel
|
||||
}
|
||||
|
||||
if f.inConflict(cur.Version, file.Version) {
|
||||
// There is a conflict here. Move the file to a conflict copy instead
|
||||
// of deleting. Also merge with the version vector we had, to indicate
|
||||
// we have resolved the conflict.
|
||||
file.Version = file.Version.Merge(cur.Version)
|
||||
err = osutil.InWritableDir(func(name string) error {
|
||||
return f.moveForConflict(name, file.ModifiedBy.String())
|
||||
}, f.fs, file.Name)
|
||||
} else if f.versioner != nil && !cur.IsSymlink() {
|
||||
// There is a conflict here, which shouldn't happen as deletions
|
||||
// always lose. Merge the version vector of the file we have
|
||||
// locally and commit it to db to resolve the conflict.
|
||||
cur.Version = cur.Version.Merge(file.Version)
|
||||
return dbUpdateJob{cur, dbUpdateHandleFile}, nil
|
||||
}
|
||||
|
||||
if f.versioner != nil && !cur.IsSymlink() {
|
||||
err = osutil.InWritableDir(f.versioner.Archive, f.fs, file.Name)
|
||||
} else {
|
||||
err = osutil.InWritableDir(f.fs.Remove, f.fs, file.Name)
|
||||
@@ -822,7 +874,7 @@ func (f *sendReceiveFolder) deleteFile(file protocol.FileInfo, scanChan chan<- s
|
||||
|
||||
// renameFile attempts to rename an existing file to a destination
|
||||
// and set the right attributes on it.
|
||||
func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, ignores *ignore.Matcher, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
|
||||
func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
@@ -864,7 +916,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, ig
|
||||
return err
|
||||
}
|
||||
// Check that the target corresponds to what we have in the DB
|
||||
curTarget, ok := f.model.CurrentFolderFile(f.folderID, target.Name)
|
||||
curTarget, ok := f.fset.Get(protocol.LocalDeviceID, target.Name)
|
||||
switch stat, serr := f.fs.Lstat(target.Name); {
|
||||
case serr != nil && fs.IsNotExist(serr):
|
||||
if !ok || curTarget.IsDeleted() {
|
||||
@@ -920,7 +972,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, ig
|
||||
// of the source and the creation of the target temp file. Fix-up the metadata,
|
||||
// update the local index of the target file and rename from temp to real name.
|
||||
|
||||
if err = f.performFinish(ignores, target, curTarget, true, tempName, dbUpdateChan, scanChan); err != nil {
|
||||
if err = f.performFinish(target, curTarget, true, tempName, dbUpdateChan, scanChan); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -966,7 +1018,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, ig
|
||||
// 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) {
|
||||
curFile, hasCurFile := f.model.CurrentFolderFile(f.folderID, file.Name)
|
||||
curFile, hasCurFile := f.fset.Get(protocol.LocalDeviceID, file.Name)
|
||||
|
||||
have, need := blockDiff(curFile.Blocks, file.Blocks)
|
||||
|
||||
@@ -1025,7 +1077,7 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c
|
||||
}
|
||||
|
||||
if err := f.CheckAvailableSpace(blocksSize); err != nil {
|
||||
f.newPullError("pulling file", file.Name, err)
|
||||
f.newPullError(file.Name, err)
|
||||
f.queue.Done(file.Name)
|
||||
return
|
||||
}
|
||||
@@ -1137,7 +1189,7 @@ func (f *sendReceiveFolder) shortcutFile(file, curFile protocol.FileInfo, dbUpda
|
||||
|
||||
if !f.IgnorePerms && !file.NoPermissions {
|
||||
if err = f.fs.Chmod(file.Name, fs.FileMode(file.Permissions&0777)); err != nil {
|
||||
f.newPullError("shortcut", file.Name, err)
|
||||
f.newPullError(file.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1173,12 +1225,10 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
|
||||
|
||||
folderFilesystems := make(map[string]fs.Filesystem)
|
||||
var folders []string
|
||||
f.model.fmut.RLock()
|
||||
for folder, cfg := range f.model.folderCfgs {
|
||||
for folder, cfg := range f.model.cfg.Folders() {
|
||||
folderFilesystems[folder] = cfg.Filesystem()
|
||||
folders = append(folders, folder)
|
||||
}
|
||||
f.model.fmut.RUnlock()
|
||||
|
||||
var file fs.File
|
||||
var weakHashFinder *weakhash.Finder
|
||||
@@ -1215,7 +1265,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
|
||||
for _, block := range state.blocks {
|
||||
select {
|
||||
case <-f.ctx.Done():
|
||||
state.fail("folder stopped", f.ctx.Err())
|
||||
state.fail(errors.Wrap(f.ctx.Err(), "folder stopped"))
|
||||
break blocks
|
||||
default:
|
||||
}
|
||||
@@ -1242,7 +1292,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
|
||||
|
||||
_, err = dstFd.WriteAt(buf, block.Offset)
|
||||
if err != nil {
|
||||
state.fail("dst write", err)
|
||||
state.fail(errors.Wrap(err, "dst write"))
|
||||
|
||||
}
|
||||
if offset == block.Offset {
|
||||
@@ -1278,7 +1328,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
|
||||
|
||||
_, err = dstFd.WriteAt(buf, block.Offset)
|
||||
if err != nil {
|
||||
state.fail("dst write", err)
|
||||
state.fail(errors.Wrap(err, "dst write"))
|
||||
}
|
||||
if path == state.file.Name {
|
||||
state.copiedFromOrigin()
|
||||
@@ -1383,7 +1433,7 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
|
||||
for {
|
||||
select {
|
||||
case <-f.ctx.Done():
|
||||
state.fail("folder stopped", f.ctx.Err())
|
||||
state.fail(errors.Wrap(f.ctx.Err(), "folder stopped"))
|
||||
return
|
||||
default:
|
||||
}
|
||||
@@ -1394,9 +1444,9 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
|
||||
selected, found := activity.leastBusy(candidates)
|
||||
if !found {
|
||||
if lastError != nil {
|
||||
state.fail("pull", lastError)
|
||||
state.fail(errors.Wrap(lastError, "pull"))
|
||||
} else {
|
||||
state.fail("pull", errNoDevice)
|
||||
state.fail(errors.Wrap(errNoDevice, "pull"))
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -1425,7 +1475,7 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
|
||||
// Save the block data we got from the cluster
|
||||
_, err = fd.WriteAt(buf, state.block.Offset)
|
||||
if err != nil {
|
||||
state.fail("save", err)
|
||||
state.fail(errors.Wrap(err, "save"))
|
||||
} else {
|
||||
state.pullDone(state.block)
|
||||
}
|
||||
@@ -1434,7 +1484,7 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
|
||||
out <- state.sharedPullerState
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) performFinish(ignores *ignore.Matcher, file, curFile protocol.FileInfo, hasCurFile bool, tempName string, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
|
||||
func (f *sendReceiveFolder) performFinish(file, curFile protocol.FileInfo, hasCurFile bool, tempName string, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
|
||||
// Set the correct permission bits on the new file
|
||||
if !f.IgnorePerms && !file.NoPermissions {
|
||||
if err := f.fs.Chmod(tempName, fs.FileMode(file.Permissions&0777)); err != nil {
|
||||
@@ -1451,81 +1501,30 @@ func (f *sendReceiveFolder) performFinish(ignores *ignore.Matcher, file, curFile
|
||||
// There is an old file or directory already in place. We need to
|
||||
// handle that.
|
||||
|
||||
curMode := uint32(stat.Mode())
|
||||
|
||||
// Check that the file on disk is what we expect it to be according
|
||||
// to the database. If there's a mismatch here, there might be local
|
||||
// changes that we don't know about yet and we should scan before
|
||||
// touching the file. There is also a case where we think the file
|
||||
// should be there, but it was removed, which is a conflict, yet
|
||||
// creations always wins when competing with a deletion, so no need
|
||||
// to handle that specially.
|
||||
changed := false
|
||||
switch {
|
||||
case !hasCurFile || curFile.Deleted:
|
||||
// The file appeared from nowhere
|
||||
l.Debugln("file exists on disk but not in db; not finishing:", file.Name)
|
||||
changed = true
|
||||
|
||||
case stat.IsDir() != curFile.IsDirectory() || stat.IsSymlink() != curFile.IsSymlink():
|
||||
// The file changed type. IsRegular is implicitly tested in the condition above
|
||||
l.Debugln("file type changed but not rescanned; not finishing:", curFile.Name)
|
||||
changed = true
|
||||
|
||||
case stat.IsRegular():
|
||||
if !stat.ModTime().Equal(curFile.ModTime()) || stat.Size() != curFile.Size {
|
||||
l.Debugln("file modified but not rescanned; not finishing:", curFile.Name)
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
// check permissions
|
||||
fallthrough
|
||||
|
||||
case stat.IsDir():
|
||||
// Dirs only have perm, no modetime/size
|
||||
if !f.IgnorePerms && !curFile.NoPermissions && curFile.HasPermissionBits() && !protocol.PermsEqual(curFile.Permissions, curMode) {
|
||||
l.Debugln("file permission modified but not rescanned; not finishing:", curFile.Name)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
if changed, err := f.itemChanged(stat, curFile, hasCurFile, scanChan); err != nil {
|
||||
return err
|
||||
} else if changed {
|
||||
l.Debugln("file changed on disk compared to db; not finishing:", file.Name)
|
||||
scanChan <- curFile.Name
|
||||
return errModified
|
||||
}
|
||||
|
||||
switch {
|
||||
case stat.IsDir() || stat.IsSymlink():
|
||||
// It's a directory or a symlink. These are not versioned or
|
||||
// archived for conflicts, only removed (which of course fails for
|
||||
// non-empty directories).
|
||||
|
||||
if err = f.deleteDir(file.Name, ignores, scanChan); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case f.inConflict(curFile.Version, file.Version):
|
||||
if !curFile.IsDirectory() && !curFile.IsSymlink() && f.inConflict(curFile.Version, file.Version) {
|
||||
// The new file has been changed in conflict with the existing one. We
|
||||
// should file it away as a conflict instead of just removing or
|
||||
// archiving. Also merge with the version vector we had, to indicate
|
||||
// we have resolved the conflict.
|
||||
// Directories and symlinks aren't checked for conflicts.
|
||||
|
||||
file.Version = file.Version.Merge(curFile.Version)
|
||||
err = osutil.InWritableDir(func(name string) error {
|
||||
return f.moveForConflict(name, file.ModifiedBy.String())
|
||||
}, f.fs, file.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case f.versioner != nil && !file.IsSymlink():
|
||||
// If we should use versioning, let the versioner archive the old
|
||||
// file before we replace it. Archiving a non-existent file is not
|
||||
// an error.
|
||||
|
||||
if err = osutil.InWritableDir(f.versioner.Archive, f.fs, file.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.moveForConflict(name, file.ModifiedBy.String(), scanChan)
|
||||
}, f.fs, curFile.Name)
|
||||
} else {
|
||||
err = f.deleteItemOnDisk(curFile, scanChan)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1543,7 +1542,7 @@ func (f *sendReceiveFolder) performFinish(ignores *ignore.Matcher, file, curFile
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) finisherRoutine(ignores *ignore.Matcher, in <-chan *sharedPullerState, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
|
||||
func (f *sendReceiveFolder) finisherRoutine(in <-chan *sharedPullerState, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
|
||||
for state := range in {
|
||||
if closed, err := state.finalClose(); closed {
|
||||
l.Debugln(f, "closing", state.file.Name)
|
||||
@@ -1551,11 +1550,11 @@ func (f *sendReceiveFolder) finisherRoutine(ignores *ignore.Matcher, in <-chan *
|
||||
f.queue.Done(state.file.Name)
|
||||
|
||||
if err == nil {
|
||||
err = f.performFinish(ignores, state.file, state.curFile, state.hasCurFile, state.tempName, dbUpdateChan, scanChan)
|
||||
err = f.performFinish(state.file, state.curFile, state.hasCurFile, state.tempName, dbUpdateChan, scanChan)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
f.newPullError("finisher", state.file.Name, err)
|
||||
f.newPullError(state.file.Name, err)
|
||||
} else {
|
||||
blockStatsMut.Lock()
|
||||
blockStats["total"] += state.reused + state.copyTotal + state.pullTotal
|
||||
@@ -1598,42 +1597,15 @@ func (f *sendReceiveFolder) Jobs() ([]string, []string) {
|
||||
func (f *sendReceiveFolder) dbUpdaterRoutine(dbUpdateChan <-chan dbUpdateJob) {
|
||||
const maxBatchTime = 2 * time.Second
|
||||
|
||||
batch := make([]dbUpdateJob, 0, maxBatchSizeFiles)
|
||||
files := make([]protocol.FileInfo, 0, maxBatchSizeFiles)
|
||||
batch := newFileInfoBatch(nil)
|
||||
tick := time.NewTicker(maxBatchTime)
|
||||
defer tick.Stop()
|
||||
|
||||
changedDirs := make(map[string]struct{})
|
||||
found := false
|
||||
var lastFile protocol.FileInfo
|
||||
|
||||
handleBatch := func() {
|
||||
found := false
|
||||
var lastFile protocol.FileInfo
|
||||
|
||||
for _, job := range batch {
|
||||
files = append(files, job.file)
|
||||
|
||||
switch job.jobType {
|
||||
case dbUpdateHandleFile, dbUpdateShortcutFile:
|
||||
changedDirs[filepath.Dir(job.file.Name)] = struct{}{}
|
||||
case dbUpdateHandleDir:
|
||||
changedDirs[job.file.Name] = struct{}{}
|
||||
case dbUpdateHandleSymlink, dbUpdateInvalidate:
|
||||
// fsyncing symlinks is only supported by MacOS
|
||||
// and invalidated files are db only changes -> no sync
|
||||
}
|
||||
|
||||
if job.file.IsInvalid() || (job.file.IsDirectory() && !job.file.IsSymlink()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if job.jobType&(dbUpdateHandleFile|dbUpdateDeleteFile) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
found = true
|
||||
lastFile = job.file
|
||||
}
|
||||
|
||||
batch.flushFn = func(files []protocol.FileInfo) error {
|
||||
// sync directories
|
||||
for dir := range changedDirs {
|
||||
delete(changedDirs, dir)
|
||||
@@ -1653,14 +1625,13 @@ func (f *sendReceiveFolder) dbUpdaterRoutine(dbUpdateChan <-chan dbUpdateJob) {
|
||||
f.model.updateLocalsFromPulling(f.folderID, files)
|
||||
|
||||
if found {
|
||||
f.model.receivedFile(f.folderID, lastFile)
|
||||
f.ReceivedFile(lastFile.Name, lastFile.IsDeleted())
|
||||
found = false
|
||||
}
|
||||
|
||||
batch = batch[:0]
|
||||
files = files[:0]
|
||||
return nil
|
||||
}
|
||||
|
||||
batchSizeBytes := 0
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
@@ -1669,26 +1640,35 @@ loop:
|
||||
break loop
|
||||
}
|
||||
|
||||
job.file.Sequence = 0
|
||||
batch = append(batch, job)
|
||||
|
||||
batchSizeBytes += job.file.ProtoSize()
|
||||
if len(batch) == maxBatchSizeFiles || batchSizeBytes > maxBatchSizeBytes {
|
||||
handleBatch()
|
||||
batchSizeBytes = 0
|
||||
switch job.jobType {
|
||||
case dbUpdateHandleFile, dbUpdateShortcutFile:
|
||||
changedDirs[filepath.Dir(job.file.Name)] = struct{}{}
|
||||
case dbUpdateHandleDir:
|
||||
changedDirs[job.file.Name] = struct{}{}
|
||||
case dbUpdateHandleSymlink, dbUpdateInvalidate:
|
||||
// fsyncing symlinks is only supported by MacOS
|
||||
// and invalidated files are db only changes -> no sync
|
||||
}
|
||||
|
||||
// For some reason we seem to care about file deletions and
|
||||
// content modification, but not about metadata and dirs/symlinks.
|
||||
if !job.file.IsInvalid() && job.jobType&(dbUpdateHandleFile|dbUpdateDeleteFile) != 0 {
|
||||
found = true
|
||||
lastFile = job.file
|
||||
}
|
||||
|
||||
job.file.Sequence = 0
|
||||
|
||||
batch.append(job.file)
|
||||
|
||||
batch.flushIfFull()
|
||||
|
||||
case <-tick.C:
|
||||
if len(batch) > 0 {
|
||||
handleBatch()
|
||||
batchSizeBytes = 0
|
||||
}
|
||||
batch.flush()
|
||||
}
|
||||
}
|
||||
|
||||
if len(batch) > 0 {
|
||||
handleBatch()
|
||||
}
|
||||
batch.flush()
|
||||
}
|
||||
|
||||
// pullScannerRoutine aggregates paths to be scanned after pulling. The scan is
|
||||
@@ -1736,25 +1716,23 @@ func removeAvailability(availabilities []Availability, availability Availability
|
||||
return availabilities
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) moveForConflict(name string, lastModBy string) error {
|
||||
if strings.Contains(filepath.Base(name), ".sync-conflict-") {
|
||||
func (f *sendReceiveFolder) moveForConflict(name, lastModBy string, scanChan chan<- string) error {
|
||||
if isConflict(name) {
|
||||
l.Infoln("Conflict for", name, "which is already a conflict copy; not copying again.")
|
||||
if err := f.fs.Remove(name); err != nil && !fs.IsNotExist(err) {
|
||||
return err
|
||||
return errors.Wrap(err, contextRemovingOldItem)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if f.MaxConflicts == 0 {
|
||||
if err := f.fs.Remove(name); err != nil && !fs.IsNotExist(err) {
|
||||
return err
|
||||
return errors.Wrap(err, contextRemovingOldItem)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := filepath.Ext(name)
|
||||
withoutExt := name[:len(name)-len(ext)]
|
||||
newName := withoutExt + time.Now().Format(".sync-conflict-20060102-150405-") + lastModBy + ext
|
||||
newName := conflictName(name, lastModBy)
|
||||
err := f.fs.Rename(name, newName)
|
||||
if fs.IsNotExist(err) {
|
||||
// We were supposed to move a file away but it does not exist. Either
|
||||
@@ -1764,23 +1742,23 @@ func (f *sendReceiveFolder) moveForConflict(name string, lastModBy string) error
|
||||
err = nil
|
||||
}
|
||||
if f.MaxConflicts > -1 {
|
||||
matches, gerr := f.fs.Glob(withoutExt + ".sync-conflict-????????-??????*" + ext)
|
||||
if gerr == nil && len(matches) > f.MaxConflicts {
|
||||
matches := existingConflicts(name, f.fs)
|
||||
if len(matches) > f.MaxConflicts {
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
|
||||
for _, match := range matches[f.MaxConflicts:] {
|
||||
gerr = f.fs.Remove(match)
|
||||
if gerr != nil {
|
||||
if gerr := f.fs.Remove(match); gerr != nil {
|
||||
l.Debugln(f, "removing extra conflict", gerr)
|
||||
}
|
||||
}
|
||||
} else if gerr != nil {
|
||||
l.Debugln(f, "globbing for conflicts", gerr)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
scanChan <- newName
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) newPullError(context, path string, err error) {
|
||||
func (f *sendReceiveFolder) newPullError(path string, err error) {
|
||||
f.pullErrorsMut.Lock()
|
||||
defer f.pullErrorsMut.Unlock()
|
||||
|
||||
@@ -1790,8 +1768,13 @@ func (f *sendReceiveFolder) newPullError(context, path string, err error) {
|
||||
if _, ok := f.pullErrors[path]; ok {
|
||||
return
|
||||
}
|
||||
l.Infof("Puller (folder %s, file %q): %s: %v", f.Description(), path, context, err)
|
||||
f.pullErrors[path] = fmt.Sprintf("%s: %s", context, err.Error())
|
||||
|
||||
l.Infof("Puller (folder %s, file %q): %v", f.Description(), path, err)
|
||||
|
||||
// Establish context to differentiate from errors while scanning.
|
||||
// Use "syncing" as opposed to "pulling" as the latter might be used
|
||||
// for errors occurring specificly in the puller routine.
|
||||
f.pullErrors[path] = fmt.Sprintln("syncing:", err)
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) clearPullErrors() {
|
||||
@@ -1813,9 +1796,33 @@ func (f *sendReceiveFolder) Errors() []FileError {
|
||||
return errors
|
||||
}
|
||||
|
||||
// deleteDir attempts to delete a directory. It checks for files/dirs inside
|
||||
// deleteItemOnDisk deletes the file represented by old that is about to be replaced by new.
|
||||
func (f *sendReceiveFolder) deleteItemOnDisk(item protocol.FileInfo, scanChan chan<- string) (err error) {
|
||||
defer func() {
|
||||
err = errors.Wrap(err, contextRemovingOldItem)
|
||||
}()
|
||||
|
||||
switch {
|
||||
case item.IsDirectory():
|
||||
// Directories aren't archived and need special treatment due
|
||||
// to potential children.
|
||||
return f.deleteDirOnDisk(item.Name, scanChan)
|
||||
|
||||
case !item.IsSymlink() && f.versioner != nil:
|
||||
// If we should use versioning, let the versioner archive the
|
||||
// file before we replace it. Archiving a non-existent file is not
|
||||
// an error.
|
||||
// Symlinks aren't archived.
|
||||
|
||||
return osutil.InWritableDir(f.versioner.Archive, f.fs, item.Name)
|
||||
}
|
||||
|
||||
return osutil.InWritableDir(f.fs.Remove, f.fs, item.Name)
|
||||
}
|
||||
|
||||
// deleteDirOnDisk attempts to delete a directory. It checks for files/dirs inside
|
||||
// the directory and removes them if possible or returns an error if it fails
|
||||
func (f *sendReceiveFolder) deleteDir(dir string, ignores *ignore.Matcher, scanChan chan<- string) error {
|
||||
func (f *sendReceiveFolder) deleteDirOnDisk(dir string, scanChan chan<- string) error {
|
||||
files, _ := f.fs.DirNames(dir)
|
||||
|
||||
toBeDeleted := make([]string, 0, len(files))
|
||||
@@ -1826,11 +1833,11 @@ func (f *sendReceiveFolder) deleteDir(dir string, ignores *ignore.Matcher, scanC
|
||||
|
||||
for _, dirFile := range files {
|
||||
fullDirFile := filepath.Join(dir, dirFile)
|
||||
if fs.IsTemporary(dirFile) || ignores.Match(fullDirFile).IsDeletable() {
|
||||
if fs.IsTemporary(dirFile) || f.ignores.Match(fullDirFile).IsDeletable() {
|
||||
toBeDeleted = append(toBeDeleted, fullDirFile)
|
||||
} else if ignores != nil && ignores.Match(fullDirFile).IsIgnored() {
|
||||
} else if f.ignores != nil && f.ignores.Match(fullDirFile).IsIgnored() {
|
||||
hasIgnored = true
|
||||
} else if cf, ok := f.model.CurrentFolderFile(f.ID, fullDirFile); !ok || cf.IsDeleted() || cf.IsInvalid() {
|
||||
} else if cf, ok := f.fset.Get(protocol.LocalDeviceID, fullDirFile); !ok || cf.IsDeleted() || cf.IsInvalid() {
|
||||
// Something appeared in the dir that we either are not aware of
|
||||
// at all, that we think should be deleted or that is invalid,
|
||||
// but not currently ignored -> schedule scan. The scanChan
|
||||
@@ -1877,8 +1884,35 @@ func (f *sendReceiveFolder) deleteDir(dir string, ignores *ignore.Matcher, scanC
|
||||
return err
|
||||
}
|
||||
|
||||
// itemChanged returns true if the given disk file differs from the information
|
||||
// in the database and schedules that file for scanning
|
||||
func (f *sendReceiveFolder) itemChanged(stat fs.FileInfo, item protocol.FileInfo, hasItem bool, scanChan chan<- string) (changed bool, err error) {
|
||||
defer func() {
|
||||
if changed {
|
||||
scanChan <- item.Name
|
||||
}
|
||||
}()
|
||||
|
||||
if !hasItem || item.Deleted {
|
||||
// The item appeared from nowhere
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check that the item on disk is what we expect it to be according
|
||||
// to the database. If there's a mismatch here, there might be local
|
||||
// changes that we don't know about yet and we should scan before
|
||||
// touching the item.
|
||||
statItem, err := scanner.CreateFileInfo(stat, item.Name, f.fs)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "comparing item on disk to db")
|
||||
}
|
||||
|
||||
return !statItem.IsEquivalentOptional(item, f.IgnorePerms, true, protocol.LocalAllFlags), nil
|
||||
}
|
||||
|
||||
// checkToBeDeleted makes sure the file on disk is compatible with what there is
|
||||
// in the DB before the caller proceeds with actually deleting it.
|
||||
// I.e. non-nil error status means "Do not delete!".
|
||||
func (f *sendReceiveFolder) checkToBeDeleted(cur protocol.FileInfo, scanChan chan<- string) error {
|
||||
stat, err := f.fs.Lstat(cur.Name)
|
||||
if err != nil {
|
||||
@@ -1890,13 +1924,11 @@ func (f *sendReceiveFolder) checkToBeDeleted(cur protocol.FileInfo, scanChan cha
|
||||
// do not delete.
|
||||
return err
|
||||
}
|
||||
fi, err := scanner.CreateFileInfo(stat, cur.Name, f.fs)
|
||||
changed, err := f.itemChanged(stat, cur, true, scanChan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsEquivalentOptional(cur, f.IgnorePerms, true, protocol.LocalAllFlags) {
|
||||
// File changed
|
||||
scanChan <- cur.Name
|
||||
if changed {
|
||||
return errModified
|
||||
}
|
||||
return nil
|
||||
@@ -1967,3 +1999,21 @@ func componentCount(name string) int {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func isConflict(name string) bool {
|
||||
return strings.Contains(filepath.Base(name), ".sync-conflict-")
|
||||
}
|
||||
|
||||
func existingConflicts(name string, fs fs.Filesystem) []string {
|
||||
ext := filepath.Ext(name)
|
||||
matches, err := fs.Glob(name[:len(name)-len(ext)] + ".sync-conflict-????????-??????*" + ext)
|
||||
if err != nil {
|
||||
l.Debugln("globbing for conflicts", err)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
+198
-125
@@ -62,7 +62,7 @@ var diffTestData = []struct {
|
||||
{"cont", "contents", 3, []protocol.BlockInfo{{Offset: 3, Size: 3}, {Offset: 6, Size: 2}}},
|
||||
}
|
||||
|
||||
func setUpFile(filename string, blockNumbers []int) protocol.FileInfo {
|
||||
func setupFile(filename string, blockNumbers []int) protocol.FileInfo {
|
||||
// Create existing file
|
||||
existingBlocks := make([]protocol.BlockInfo, len(blockNumbers))
|
||||
for i := range blockNumbers {
|
||||
@@ -75,27 +75,38 @@ func setUpFile(filename string, blockNumbers []int) protocol.FileInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func setUpModel(files ...protocol.FileInfo) *Model {
|
||||
db := db.OpenMemory()
|
||||
model := NewModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
|
||||
model.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
model.updateLocalsFromScanning("default", files)
|
||||
return model
|
||||
func createFile(t *testing.T, name string, fs fs.Filesystem) protocol.FileInfo {
|
||||
t.Helper()
|
||||
|
||||
f, err := fs.Create(name)
|
||||
must(t, err)
|
||||
f.Close()
|
||||
fi, err := fs.Stat(name)
|
||||
must(t, err)
|
||||
file, err := scanner.CreateFileInfo(fi, name, fs)
|
||||
must(t, err)
|
||||
return file
|
||||
}
|
||||
|
||||
func setUpSendReceiveFolder(model *Model) *sendReceiveFolder {
|
||||
func setupSendReceiveFolder(files ...protocol.FileInfo) (*model, *sendReceiveFolder) {
|
||||
w := createTmpWrapper(defaultCfg)
|
||||
model := newModel(w, myID, "syncthing", "dev", db.OpenMemory(), nil)
|
||||
fcfg := testFolderConfigTmp()
|
||||
model.AddFolder(fcfg)
|
||||
|
||||
// Update index
|
||||
if files != nil {
|
||||
model.updateLocalsFromScanning("default", files)
|
||||
}
|
||||
|
||||
f := &sendReceiveFolder{
|
||||
folder: folder{
|
||||
stateTracker: newStateTracker("default"),
|
||||
model: model,
|
||||
fset: model.folderFiles[fcfg.ID],
|
||||
initialScanFinished: make(chan struct{}),
|
||||
ctx: context.TODO(),
|
||||
FolderConfiguration: config.FolderConfiguration{
|
||||
FilesystemType: fs.FilesystemTypeBasic,
|
||||
Path: "testdata",
|
||||
PullerMaxPendingKiB: defaultPullerPendingKiB,
|
||||
},
|
||||
FolderConfiguration: fcfg,
|
||||
},
|
||||
|
||||
queue: newJobQueue(),
|
||||
@@ -107,7 +118,7 @@ func setUpSendReceiveFolder(model *Model) *sendReceiveFolder {
|
||||
// Folders are never actually started, so no initial scan will be done
|
||||
close(f.initialScanFinished)
|
||||
|
||||
return f
|
||||
return model, f
|
||||
}
|
||||
|
||||
// Layout of the files: (indexes from the above array)
|
||||
@@ -121,12 +132,16 @@ func TestHandleFile(t *testing.T) {
|
||||
// Pull: 1, 3, 4, 6, 7
|
||||
|
||||
existingBlocks := []int{0, 2, 0, 0, 5, 0, 0, 8}
|
||||
existingFile := setUpFile("filex", existingBlocks)
|
||||
existingFile := setupFile("filex", existingBlocks)
|
||||
requiredFile := existingFile
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
|
||||
m := setUpModel(existingFile)
|
||||
f := setUpSendReceiveFolder(m)
|
||||
m, f := setupSendReceiveFolder(existingFile)
|
||||
defer func() {
|
||||
os.Remove(m.cfg.ConfigPath())
|
||||
os.Remove(f.Filesystem().URI())
|
||||
}()
|
||||
|
||||
copyChan := make(chan copyBlocksState, 1)
|
||||
dbUpdateChan := make(chan dbUpdateJob, 1)
|
||||
|
||||
@@ -163,12 +178,20 @@ func TestHandleFileWithTemp(t *testing.T) {
|
||||
// Pull: 1, 6
|
||||
|
||||
existingBlocks := []int{0, 2, 0, 0, 5, 0, 0, 8}
|
||||
existingFile := setUpFile("file", existingBlocks)
|
||||
existingFile := setupFile("file", existingBlocks)
|
||||
requiredFile := existingFile
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
|
||||
m := setUpModel(existingFile)
|
||||
f := setUpSendReceiveFolder(m)
|
||||
m, f := setupSendReceiveFolder(existingFile)
|
||||
defer func() {
|
||||
os.Remove(m.cfg.ConfigPath())
|
||||
os.Remove(f.Filesystem().URI())
|
||||
}()
|
||||
|
||||
if _, err := prepareTmpFile(f.Filesystem()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
copyChan := make(chan copyBlocksState, 1)
|
||||
dbUpdateChan := make(chan dbUpdateJob, 1)
|
||||
|
||||
@@ -197,8 +220,6 @@ func TestHandleFileWithTemp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCopierFinder(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
// After diff between required and existing we should:
|
||||
// Copy: 1, 2, 3, 4, 6, 7, 8
|
||||
// Since there is no existing file, nor a temp file
|
||||
@@ -206,18 +227,24 @@ func TestCopierFinder(t *testing.T) {
|
||||
// After dropping out blocks found locally:
|
||||
// Pull: 1, 5, 6, 8
|
||||
|
||||
tempFile := filepath.Join("testdata", fs.TempName("file2"))
|
||||
testOs.Remove(tempFile)
|
||||
defer testOs.Remove(tempFile)
|
||||
tempFile := fs.TempName("file2")
|
||||
|
||||
existingBlocks := []int{0, 2, 3, 4, 0, 0, 7, 0}
|
||||
existingFile := setUpFile(fs.TempName("file"), existingBlocks)
|
||||
existingFile := setupFile(fs.TempName("file"), existingBlocks)
|
||||
requiredFile := existingFile
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
requiredFile.Name = "file2"
|
||||
|
||||
m := setUpModel(existingFile)
|
||||
f := setUpSendReceiveFolder(m)
|
||||
m, f := setupSendReceiveFolder(existingFile)
|
||||
defer func() {
|
||||
os.Remove(m.cfg.ConfigPath())
|
||||
os.Remove(f.Filesystem().URI())
|
||||
}()
|
||||
|
||||
if _, err := prepareTmpFile(f.Filesystem()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
copyChan := make(chan copyBlocksState)
|
||||
pullChan := make(chan pullBlockState, 4)
|
||||
finisherChan := make(chan *sharedPullerState, 1)
|
||||
@@ -259,7 +286,7 @@ func TestCopierFinder(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify that the fetched blocks have actually been written to the temp file
|
||||
blks, err := scanner.HashFile(context.TODO(), fs.NewFilesystem(fs.FilesystemTypeBasic, "."), tempFile, protocol.MinBlockSize, nil, false)
|
||||
blks, err := scanner.HashFile(context.TODO(), f.Filesystem(), tempFile, protocol.MinBlockSize, nil, false)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
@@ -273,9 +300,15 @@ func TestCopierFinder(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWeakHash(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
// Setup the model/pull environment
|
||||
model, fo := setupSendReceiveFolder()
|
||||
ffs := fo.Filesystem()
|
||||
defer func() {
|
||||
os.Remove(model.cfg.ConfigPath())
|
||||
os.Remove(ffs.URI())
|
||||
}()
|
||||
|
||||
tempFile := filepath.Join("testdata", fs.TempName("weakhash"))
|
||||
tempFile := fs.TempName("weakhash")
|
||||
var shift int64 = 10
|
||||
var size int64 = 1 << 20
|
||||
expectBlocks := int(size / protocol.MinBlockSize)
|
||||
@@ -284,18 +317,10 @@ func TestWeakHash(t *testing.T) {
|
||||
expectPulls++
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
for _, path := range []string{tempFile, "testdata/weakhash"} {
|
||||
testOs.Remove(path)
|
||||
}
|
||||
}
|
||||
|
||||
cleanup()
|
||||
defer cleanup()
|
||||
|
||||
f := testOs.Create("testdata/weakhash")
|
||||
f, err := ffs.Create("weakhash")
|
||||
must(t, err)
|
||||
defer f.Close()
|
||||
_, err := io.CopyN(f, rand.Reader, size)
|
||||
_, err = io.CopyN(f, rand.Reader, size)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -337,9 +362,8 @@ func TestWeakHash(t *testing.T) {
|
||||
ModifiedS: info.ModTime().Unix() + 1,
|
||||
}
|
||||
|
||||
// Setup the model/pull environment
|
||||
m := setUpModel(existingFile)
|
||||
fo := setUpSendReceiveFolder(m)
|
||||
model.updateLocalsFromScanning("default", []protocol.FileInfo{existingFile})
|
||||
|
||||
copyChan := make(chan copyBlocksState)
|
||||
pullChan := make(chan pullBlockState, expectBlocks)
|
||||
finisherChan := make(chan *sharedPullerState, 1)
|
||||
@@ -372,7 +396,9 @@ func TestWeakHash(t *testing.T) {
|
||||
}
|
||||
|
||||
finish.fd.Close()
|
||||
testOs.Remove(tempFile)
|
||||
if err := ffs.Remove(tempFile); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Test 2 - using weak hash, expectPulls blocks pulled.
|
||||
fo.WeakHashThresholdPct = -1
|
||||
@@ -404,8 +430,12 @@ func TestCopierCleanup(t *testing.T) {
|
||||
}
|
||||
|
||||
// Create a file
|
||||
file := setUpFile("test", []int{0})
|
||||
m := setUpModel(file)
|
||||
file := setupFile("test", []int{0})
|
||||
m, f := setupSendReceiveFolder(file)
|
||||
defer func() {
|
||||
os.Remove(m.cfg.ConfigPath())
|
||||
os.Remove(f.Filesystem().URI())
|
||||
}()
|
||||
|
||||
file.Blocks = []protocol.BlockInfo{blocks[1]}
|
||||
file.Version = file.Version.Update(myID.Short())
|
||||
@@ -435,21 +465,17 @@ func TestCopierCleanup(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
file := setupFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
|
||||
|
||||
file := setUpFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
|
||||
defer testOs.Remove("testdata/" + fs.TempName("filex"))
|
||||
|
||||
db := db.OpenMemory()
|
||||
|
||||
m := NewModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m, f := setupSendReceiveFolder()
|
||||
defer func() {
|
||||
os.Remove(m.cfg.ConfigPath())
|
||||
os.Remove(f.Filesystem().URI())
|
||||
}()
|
||||
|
||||
// Set up our evet subscription early
|
||||
s := events.Default.Subscribe(events.ItemFinished)
|
||||
|
||||
f := setUpSendReceiveFolder(m)
|
||||
|
||||
// queue.Done should be called by the finisher routine
|
||||
f.queue.Push("filex", 0, time.Time{})
|
||||
f.queue.Pop()
|
||||
@@ -465,7 +491,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
dbUpdateChan := make(chan dbUpdateJob, 1)
|
||||
|
||||
go f.copierRoutine(copyChan, pullChan, finisherBufferChan)
|
||||
go f.finisherRoutine(ignore.New(defaultFs), finisherChan, dbUpdateChan, make(chan string))
|
||||
go f.finisherRoutine(finisherChan, dbUpdateChan, make(chan string))
|
||||
|
||||
f.handleFile(file, copyChan, finisherChan, dbUpdateChan)
|
||||
|
||||
@@ -474,7 +500,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
toPull := <-pullChan
|
||||
|
||||
// Close the file, causing errors on further access
|
||||
toPull.sharedPullerState.fail("test", os.ErrNotExist)
|
||||
toPull.sharedPullerState.fail(os.ErrNotExist)
|
||||
|
||||
// Unblock copier
|
||||
go func() {
|
||||
@@ -529,20 +555,17 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
file := setupFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
|
||||
|
||||
file := setUpFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
|
||||
defer testOs.Remove("testdata/" + fs.TempName("filex"))
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m, f := setupSendReceiveFolder()
|
||||
defer func() {
|
||||
os.Remove(m.cfg.ConfigPath())
|
||||
os.Remove(f.Filesystem().URI())
|
||||
}()
|
||||
|
||||
// Set up our evet subscription early
|
||||
s := events.Default.Subscribe(events.ItemFinished)
|
||||
|
||||
f := setUpSendReceiveFolder(m)
|
||||
|
||||
// queue.Done should be called by the finisher routine
|
||||
f.queue.Push("filex", 0, time.Time{})
|
||||
f.queue.Pop()
|
||||
@@ -559,7 +582,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
|
||||
go f.copierRoutine(copyChan, pullChan, finisherBufferChan)
|
||||
go f.pullerRoutine(pullChan, finisherBufferChan)
|
||||
go f.finisherRoutine(ignore.New(defaultFs), finisherChan, dbUpdateChan, make(chan string))
|
||||
go f.finisherRoutine(finisherChan, dbUpdateChan, make(chan string))
|
||||
|
||||
f.handleFile(file, copyChan, finisherChan, dbUpdateChan)
|
||||
|
||||
@@ -612,35 +635,32 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIssue3164(t *testing.T) {
|
||||
m := setUpModel(protocol.FileInfo{})
|
||||
f := setUpSendReceiveFolder(m)
|
||||
m, f := setupSendReceiveFolder()
|
||||
ffs := f.Filesystem()
|
||||
tmpDir := ffs.URI()
|
||||
defer func() {
|
||||
os.Remove(m.cfg.ConfigPath())
|
||||
os.Remove(tmpDir)
|
||||
}()
|
||||
|
||||
defaultFs.RemoveAll("issue3164")
|
||||
defer defaultFs.RemoveAll("issue3164")
|
||||
|
||||
if err := defaultFs.MkdirAll("issue3164/oktodelete/foobar", 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile("testdata/issue3164/oktodelete/foobar/file", []byte("Hello"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile("testdata/issue3164/oktodelete/file", []byte("Hello"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ignDir := filepath.Join("issue3164", "oktodelete")
|
||||
subDir := filepath.Join(ignDir, "foobar")
|
||||
must(t, ffs.MkdirAll(subDir, 0777))
|
||||
must(t, ioutil.WriteFile(filepath.Join(tmpDir, subDir, "file"), []byte("Hello"), 0644))
|
||||
must(t, ioutil.WriteFile(filepath.Join(tmpDir, ignDir, "file"), []byte("Hello"), 0644))
|
||||
file := protocol.FileInfo{
|
||||
Name: "issue3164",
|
||||
}
|
||||
|
||||
matcher := ignore.New(defaultFs)
|
||||
if err := matcher.Parse(bytes.NewBufferString("(?d)oktodelete"), ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
matcher := ignore.New(ffs)
|
||||
must(t, matcher.Parse(bytes.NewBufferString("(?d)oktodelete"), ""))
|
||||
f.ignores = matcher
|
||||
|
||||
dbUpdateChan := make(chan dbUpdateJob, 1)
|
||||
|
||||
f.handleDeleteDir(file, matcher, dbUpdateChan, make(chan string))
|
||||
f.deleteDir(file, dbUpdateChan, make(chan string))
|
||||
|
||||
if _, err := defaultFs.Stat("testdata/issue3164"); !fs.IsNotExist(err) {
|
||||
if _, err := ffs.Stat("issue3164"); !fs.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -707,27 +727,25 @@ func TestDiffEmpty(t *testing.T) {
|
||||
// option is true and the permissions do not match between the file on disk and
|
||||
// in the db.
|
||||
func TestDeleteIgnorePerms(t *testing.T) {
|
||||
m := setUpModel()
|
||||
f := setUpSendReceiveFolder(m)
|
||||
m, f := setupSendReceiveFolder()
|
||||
ffs := f.Filesystem()
|
||||
defer func() {
|
||||
os.Remove(m.cfg.ConfigPath())
|
||||
os.Remove(ffs.URI())
|
||||
}()
|
||||
f.IgnorePerms = true
|
||||
|
||||
ffs := f.Filesystem()
|
||||
name := "deleteIgnorePerms"
|
||||
file, err := ffs.Create(name)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer ffs.Remove(name)
|
||||
defer file.Close()
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, err)
|
||||
fi, err := scanner.CreateFileInfo(stat, name, ffs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, err)
|
||||
ffs.Chmod(name, 0600)
|
||||
scanChan := make(chan string)
|
||||
finished := make(chan struct{})
|
||||
@@ -740,9 +758,7 @@ func TestDeleteIgnorePerms(t *testing.T) {
|
||||
<-finished
|
||||
case <-finished:
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, err)
|
||||
}
|
||||
|
||||
func TestCopyOwner(t *testing.T) {
|
||||
@@ -761,24 +777,10 @@ func TestCopyOwner(t *testing.T) {
|
||||
// Set up a folder with the CopyParentOwner bit and backed by a fake
|
||||
// filesystem.
|
||||
|
||||
m := setUpModel()
|
||||
f := &sendReceiveFolder{
|
||||
folder: folder{
|
||||
stateTracker: newStateTracker("default"),
|
||||
model: m,
|
||||
initialScanFinished: make(chan struct{}),
|
||||
ctx: context.TODO(),
|
||||
FolderConfiguration: config.FolderConfiguration{
|
||||
FilesystemType: fs.FilesystemTypeFake,
|
||||
Path: "/TestCopyOwner",
|
||||
CopyOwnershipFromParent: true,
|
||||
},
|
||||
},
|
||||
|
||||
queue: newJobQueue(),
|
||||
pullErrors: make(map[string]string),
|
||||
pullErrorsMut: sync.NewMutex(),
|
||||
}
|
||||
m, f := setupSendReceiveFolder()
|
||||
defer os.Remove(m.cfg.ConfigPath())
|
||||
f.folder.FolderConfiguration = config.NewFolderConfiguration(m.id, f.ID, f.Label, fs.FilesystemTypeFake, "/TestCopyOwner")
|
||||
f.folder.FolderConfiguration.CopyOwnershipFromParent = true
|
||||
|
||||
f.fs = f.Filesystem()
|
||||
|
||||
@@ -798,7 +800,7 @@ func TestCopyOwner(t *testing.T) {
|
||||
|
||||
dbUpdateChan := make(chan dbUpdateJob, 1)
|
||||
defer close(dbUpdateChan)
|
||||
f.handleDir(dir, dbUpdateChan)
|
||||
f.handleDir(dir, dbUpdateChan, nil)
|
||||
<-dbUpdateChan // empty the channel for later
|
||||
|
||||
info, err := f.fs.Lstat("foo/bar")
|
||||
@@ -829,7 +831,7 @@ func TestCopyOwner(t *testing.T) {
|
||||
copierChan := make(chan copyBlocksState)
|
||||
defer close(copierChan)
|
||||
go f.copierRoutine(copierChan, nil, finisherChan)
|
||||
go f.finisherRoutine(nil, finisherChan, dbUpdateChan, nil)
|
||||
go f.finisherRoutine(finisherChan, dbUpdateChan, nil)
|
||||
f.handleFile(file, copierChan, nil, nil)
|
||||
<-dbUpdateChan
|
||||
|
||||
@@ -849,7 +851,7 @@ func TestCopyOwner(t *testing.T) {
|
||||
SymlinkTarget: "over the rainbow",
|
||||
}
|
||||
|
||||
f.handleSymlink(symlink, dbUpdateChan)
|
||||
f.handleSymlink(symlink, dbUpdateChan, nil)
|
||||
<-dbUpdateChan
|
||||
|
||||
info, err = f.fs.Lstat("foo/bar/sym")
|
||||
@@ -860,3 +862,74 @@ func TestCopyOwner(t *testing.T) {
|
||||
t.Fatalf("Expected symlink owner/group to be %d/%d, not %d/%d", expOwner, expGroup, info.Owner(), info.Group())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSRConflictReplaceFileByDir checks that a conflict is created when an existing file
|
||||
// is replaced with a directory and versions are conflicting
|
||||
func TestSRConflictReplaceFileByDir(t *testing.T) {
|
||||
m, f := setupSendReceiveFolder()
|
||||
ffs := f.Filesystem()
|
||||
defer func() {
|
||||
os.Remove(m.cfg.ConfigPath())
|
||||
os.Remove(ffs.URI())
|
||||
}()
|
||||
|
||||
name := "foo"
|
||||
|
||||
// create local file
|
||||
file := createFile(t, name, ffs)
|
||||
file.Version = protocol.Vector{}.Update(myID.Short())
|
||||
m.updateLocalsFromScanning(f.ID, []protocol.FileInfo{file})
|
||||
|
||||
// Simulate remote creating a dir with the same name
|
||||
file.Type = protocol.FileInfoTypeDirectory
|
||||
rem := device1.Short()
|
||||
file.Version = protocol.Vector{}.Update(rem)
|
||||
file.ModifiedBy = rem
|
||||
|
||||
dbUpdateChan := make(chan dbUpdateJob, 1)
|
||||
scanChan := make(chan string, 1)
|
||||
|
||||
f.handleDir(file, dbUpdateChan, scanChan)
|
||||
|
||||
if confls := existingConflicts(name, ffs); len(confls) != 1 {
|
||||
t.Fatal("Expected one conflict, got", len(confls))
|
||||
} else if scan := <-scanChan; confls[0] != scan {
|
||||
t.Fatal("Expected request to scan", confls[0], "got", scan)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSRConflictReplaceFileByLink checks that a conflict is created when an existing file
|
||||
// is replaced with a link and versions are conflicting
|
||||
func TestSRConflictReplaceFileByLink(t *testing.T) {
|
||||
m, f := setupSendReceiveFolder()
|
||||
ffs := f.Filesystem()
|
||||
defer func() {
|
||||
os.Remove(m.cfg.ConfigPath())
|
||||
os.Remove(ffs.URI())
|
||||
}()
|
||||
|
||||
name := "foo"
|
||||
|
||||
// create local file
|
||||
file := createFile(t, name, ffs)
|
||||
file.Version = protocol.Vector{}.Update(myID.Short())
|
||||
m.updateLocalsFromScanning(f.ID, []protocol.FileInfo{file})
|
||||
|
||||
// Simulate remote creating a symlink with the same name
|
||||
file.Type = protocol.FileInfoTypeSymlink
|
||||
file.SymlinkTarget = "bar"
|
||||
rem := device1.Short()
|
||||
file.Version = protocol.Vector{}.Update(rem)
|
||||
file.ModifiedBy = rem
|
||||
|
||||
dbUpdateChan := make(chan dbUpdateJob, 1)
|
||||
scanChan := make(chan string, 1)
|
||||
|
||||
f.handleSymlink(file, dbUpdateChan, scanChan)
|
||||
|
||||
if confls := existingConflicts(name, ffs); len(confls) != 1 {
|
||||
t.Fatal("Expected one conflict, got", len(confls))
|
||||
} else if scan := <-scanChan; confls[0] != scan {
|
||||
t.Fatal("Expected request to scan", confls[0], "got", scan)
|
||||
}
|
||||
}
|
||||
|
||||
+152
-115
@@ -65,6 +65,7 @@ type service interface {
|
||||
CheckHealth() error
|
||||
Errors() []FileError
|
||||
WatchError() error
|
||||
GetStatistics() stats.FolderStatistics
|
||||
|
||||
getState() (folderState, time.Time, error)
|
||||
setState(state folderState)
|
||||
@@ -76,10 +77,60 @@ type Availability struct {
|
||||
FromTemporary bool `json:"fromTemporary"`
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
type Model interface {
|
||||
suture.Service
|
||||
|
||||
connections.Model
|
||||
|
||||
AddFolder(cfg config.FolderConfiguration)
|
||||
RestartFolder(from, to config.FolderConfiguration)
|
||||
StartFolder(folder string)
|
||||
ResetFolder(folder string)
|
||||
DelayScan(folder string, next time.Duration)
|
||||
ScanFolder(folder string) error
|
||||
ScanFolders() map[string]error
|
||||
ScanFolderSubdirs(folder string, subs []string) error
|
||||
State(folder string) (string, time.Time, error)
|
||||
FolderErrors(folder string) ([]FileError, error)
|
||||
WatchError(folder string) error
|
||||
Override(folder string)
|
||||
Revert(folder string)
|
||||
BringToFront(folder, file string)
|
||||
GetIgnores(folder string) ([]string, []string, error)
|
||||
SetIgnores(folder string, content []string) error
|
||||
|
||||
GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error)
|
||||
RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error)
|
||||
|
||||
LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated
|
||||
NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated)
|
||||
RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error)
|
||||
CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool)
|
||||
CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool)
|
||||
Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability
|
||||
|
||||
GlobalSize(folder string) db.Counts
|
||||
LocalSize(folder string) db.Counts
|
||||
NeedSize(folder string) db.Counts
|
||||
ReceiveOnlyChangedSize(folder string) db.Counts
|
||||
|
||||
CurrentSequence(folder string) (int64, bool)
|
||||
RemoteSequence(folder string) (int64, bool)
|
||||
|
||||
Completion(device protocol.DeviceID, folder string) FolderCompletion
|
||||
ConnectionStats() map[string]interface{}
|
||||
DeviceStatistics() map[string]stats.DeviceStatistics
|
||||
FolderStatistics() map[string]stats.FolderStatistics
|
||||
UsageReportingStats(version int, preview bool) map[string]interface{}
|
||||
|
||||
StartDeadlockDetector(timeout time.Duration)
|
||||
GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
|
||||
}
|
||||
|
||||
type model struct {
|
||||
*suture.Supervisor
|
||||
|
||||
cfg *config.Wrapper
|
||||
cfg config.Wrapper
|
||||
db *db.Lowlevel
|
||||
finder *db.BlockFinder
|
||||
progressEmitter *ProgressEmitter
|
||||
@@ -98,7 +149,6 @@ type Model struct {
|
||||
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
|
||||
folderRunners map[string]service // folder -> puller or scanner
|
||||
folderRunnerTokens map[string][]suture.ServiceToken // folder -> tokens for puller or scanner
|
||||
folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef
|
||||
folderRestartMuts syncMutexMap // folder -> restart mutex
|
||||
|
||||
pmut sync.RWMutex // protects the below
|
||||
@@ -112,7 +162,7 @@ type Model struct {
|
||||
foldersRunning int32 // for testing only
|
||||
}
|
||||
|
||||
type folderFactory func(*Model, config.FolderConfiguration, versioner.Versioner, fs.Filesystem) service
|
||||
type folderFactory func(*model, *db.FileSet, *ignore.Matcher, config.FolderConfiguration, versioner.Versioner, fs.Filesystem) service
|
||||
|
||||
var (
|
||||
folderFactories = make(map[config.FolderType]folderFactory)
|
||||
@@ -134,8 +184,8 @@ var (
|
||||
// NewModel creates and starts a new model. The model starts in read-only mode,
|
||||
// where it sends index information to connected peers and responds to requests
|
||||
// for file data without altering the local folder in any way.
|
||||
func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string) *Model {
|
||||
m := &Model{
|
||||
func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string) Model {
|
||||
m := &model{
|
||||
Supervisor: suture.New("model", suture.Spec{
|
||||
Log: func(line string) {
|
||||
l.Debugln(line)
|
||||
@@ -158,7 +208,6 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersi
|
||||
folderIgnores: make(map[string]*ignore.Matcher),
|
||||
folderRunners: make(map[string]service),
|
||||
folderRunnerTokens: make(map[string][]suture.ServiceToken),
|
||||
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
||||
conn: make(map[protocol.DeviceID]connections.Connection),
|
||||
connRequestLimiters: make(map[protocol.DeviceID]*byteSemaphore),
|
||||
closed: make(map[protocol.DeviceID]chan struct{}),
|
||||
@@ -180,7 +229,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersi
|
||||
// StartDeadlockDetector starts a deadlock detector on the models locks which
|
||||
// causes panics in case the locks cannot be acquired in the given timeout
|
||||
// period.
|
||||
func (m *Model) StartDeadlockDetector(timeout time.Duration) {
|
||||
func (m *model) StartDeadlockDetector(timeout time.Duration) {
|
||||
l.Infof("Starting deadlock detector with %v timeout", timeout)
|
||||
detector := newDeadlockDetector(timeout)
|
||||
detector.Watch("fmut", m.fmut)
|
||||
@@ -188,7 +237,7 @@ func (m *Model) StartDeadlockDetector(timeout time.Duration) {
|
||||
}
|
||||
|
||||
// StartFolder constructs the folder service and starts it.
|
||||
func (m *Model) StartFolder(folder string) {
|
||||
func (m *model) StartFolder(folder string) {
|
||||
m.fmut.Lock()
|
||||
m.pmut.Lock()
|
||||
folderType := m.startFolderLocked(folder)
|
||||
@@ -199,7 +248,7 @@ func (m *Model) StartFolder(folder string) {
|
||||
l.Infof("Ready to synchronize %s (%s)", folderCfg.Description(), folderType)
|
||||
}
|
||||
|
||||
func (m *Model) startFolderLocked(folder string) config.FolderType {
|
||||
func (m *model) startFolderLocked(folder string) config.FolderType {
|
||||
if err := m.checkFolderRunningLocked(folder); err == errFolderMissing {
|
||||
panic("cannot start nonexistent folder " + folder)
|
||||
} else if err == nil {
|
||||
@@ -213,15 +262,15 @@ func (m *Model) startFolderLocked(folder string) config.FolderType {
|
||||
panic(fmt.Sprintf("unknown folder type 0x%x", cfg.Type))
|
||||
}
|
||||
|
||||
fs := m.folderFiles[folder]
|
||||
fset := m.folderFiles[folder]
|
||||
|
||||
// Find any devices for which we hold the index in the db, but the folder
|
||||
// is not shared, and drop it.
|
||||
expected := mapDevices(cfg.DeviceIDs())
|
||||
for _, available := range fs.ListDevices() {
|
||||
for _, available := range fset.ListDevices() {
|
||||
if _, ok := expected[available]; !ok {
|
||||
l.Debugln("dropping", folder, "state for", available)
|
||||
fs.Drop(available)
|
||||
fset.Drop(available)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +279,7 @@ func (m *Model) startFolderLocked(folder string) config.FolderType {
|
||||
m.closeLocked(id, fmt.Errorf("started folder %v", cfg.Description()))
|
||||
}
|
||||
|
||||
v, ok := fs.Sequence(protocol.LocalDeviceID), true
|
||||
v, ok := fset.Sequence(protocol.LocalDeviceID), true
|
||||
indexHasFiles := ok && v > 0
|
||||
if !indexHasFiles {
|
||||
// It's a blank folder, so this may the first time we're looking at
|
||||
@@ -255,14 +304,14 @@ func (m *Model) startFolderLocked(folder string) config.FolderType {
|
||||
m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
|
||||
}
|
||||
|
||||
ffs := fs.MtimeFS()
|
||||
ffs := fset.MtimeFS()
|
||||
|
||||
// These are our metadata files, and they should always be hidden.
|
||||
ffs.Hide(config.DefaultMarkerName)
|
||||
ffs.Hide(".stversions")
|
||||
ffs.Hide(".stignore")
|
||||
|
||||
p := folderFactory(m, cfg, ver, ffs)
|
||||
p := folderFactory(m, fset, m.folderIgnores[folder], cfg, ver, ffs)
|
||||
|
||||
m.folderRunners[folder] = p
|
||||
|
||||
@@ -274,7 +323,7 @@ func (m *Model) startFolderLocked(folder string) config.FolderType {
|
||||
return cfg.Type
|
||||
}
|
||||
|
||||
func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
|
||||
func (m *model) warnAboutOverwritingProtectedFiles(folder string) {
|
||||
if m.folderCfgs[folder].Type == config.FolderTypeSendOnly {
|
||||
return
|
||||
}
|
||||
@@ -308,7 +357,7 @@ func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) AddFolder(cfg config.FolderConfiguration) {
|
||||
func (m *model) AddFolder(cfg config.FolderConfiguration) {
|
||||
if len(cfg.ID) == 0 {
|
||||
panic("cannot add empty folder id")
|
||||
}
|
||||
@@ -322,7 +371,7 @@ func (m *Model) AddFolder(cfg config.FolderConfiguration) {
|
||||
m.fmut.Unlock()
|
||||
}
|
||||
|
||||
func (m *Model) addFolderLocked(cfg config.FolderConfiguration) {
|
||||
func (m *model) addFolderLocked(cfg config.FolderConfiguration) {
|
||||
m.folderCfgs[cfg.ID] = cfg
|
||||
folderFs := cfg.Filesystem()
|
||||
m.folderFiles[cfg.ID] = db.NewFileSet(cfg.ID, folderFs, m.db)
|
||||
@@ -334,7 +383,7 @@ func (m *Model) addFolderLocked(cfg config.FolderConfiguration) {
|
||||
m.folderIgnores[cfg.ID] = ignores
|
||||
}
|
||||
|
||||
func (m *Model) RemoveFolder(cfg config.FolderConfiguration) {
|
||||
func (m *model) RemoveFolder(cfg config.FolderConfiguration) {
|
||||
m.fmut.Lock()
|
||||
m.pmut.Lock()
|
||||
// Delete syncthing specific files
|
||||
@@ -348,7 +397,7 @@ func (m *Model) RemoveFolder(cfg config.FolderConfiguration) {
|
||||
m.fmut.Unlock()
|
||||
}
|
||||
|
||||
func (m *Model) tearDownFolderLocked(cfg config.FolderConfiguration, err error) {
|
||||
func (m *model) tearDownFolderLocked(cfg config.FolderConfiguration, err error) {
|
||||
// Close connections to affected devices
|
||||
// Must happen before stopping the folder service to abort ongoing
|
||||
// transmissions and thus allow timely service termination.
|
||||
@@ -373,10 +422,9 @@ func (m *Model) tearDownFolderLocked(cfg config.FolderConfiguration, err error)
|
||||
delete(m.folderIgnores, cfg.ID)
|
||||
delete(m.folderRunners, cfg.ID)
|
||||
delete(m.folderRunnerTokens, cfg.ID)
|
||||
delete(m.folderStatRefs, cfg.ID)
|
||||
}
|
||||
|
||||
func (m *Model) RestartFolder(from, to config.FolderConfiguration) {
|
||||
func (m *model) RestartFolder(from, to config.FolderConfiguration) {
|
||||
if len(to.ID) == 0 {
|
||||
panic("bug: cannot restart empty folder ID")
|
||||
}
|
||||
@@ -421,7 +469,7 @@ func (m *Model) RestartFolder(from, to config.FolderConfiguration) {
|
||||
l.Infof("%v folder %v (%v)", infoMsg, to.Description(), to.Type)
|
||||
}
|
||||
|
||||
func (m *Model) UsageReportingStats(version int, preview bool) map[string]interface{} {
|
||||
func (m *model) UsageReportingStats(version int, preview bool) map[string]interface{} {
|
||||
stats := make(map[string]interface{})
|
||||
if version >= 3 {
|
||||
// Block stats
|
||||
@@ -524,6 +572,7 @@ type ConnectionInfo struct {
|
||||
Address string
|
||||
ClientVersion string
|
||||
Type string
|
||||
Crypto string
|
||||
}
|
||||
|
||||
func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
|
||||
@@ -536,11 +585,12 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
|
||||
"address": info.Address,
|
||||
"clientVersion": info.ClientVersion,
|
||||
"type": info.Type,
|
||||
"crypto": info.Crypto,
|
||||
})
|
||||
}
|
||||
|
||||
// ConnectionStats returns a map with connection statistics for each device.
|
||||
func (m *Model) ConnectionStats() map[string]interface{} {
|
||||
func (m *model) ConnectionStats() map[string]interface{} {
|
||||
m.fmut.RLock()
|
||||
m.pmut.RLock()
|
||||
|
||||
@@ -559,6 +609,7 @@ func (m *Model) ConnectionStats() map[string]interface{} {
|
||||
}
|
||||
if conn, ok := m.conn[device]; ok {
|
||||
ci.Type = conn.Type()
|
||||
ci.Crypto = conn.Crypto()
|
||||
ci.Connected = ok
|
||||
ci.Statistics = conn.Statistics()
|
||||
if addr := conn.RemoteAddr(); addr != nil {
|
||||
@@ -587,7 +638,7 @@ func (m *Model) ConnectionStats() map[string]interface{} {
|
||||
}
|
||||
|
||||
// DeviceStatistics returns statistics about each device
|
||||
func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
|
||||
func (m *model) DeviceStatistics() map[string]stats.DeviceStatistics {
|
||||
res := make(map[string]stats.DeviceStatistics)
|
||||
for id := range m.cfg.Devices() {
|
||||
res[id.String()] = m.deviceStatRef(id).GetStatistics()
|
||||
@@ -596,10 +647,12 @@ func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
|
||||
}
|
||||
|
||||
// FolderStatistics returns statistics about each folder
|
||||
func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
|
||||
func (m *model) FolderStatistics() map[string]stats.FolderStatistics {
|
||||
res := make(map[string]stats.FolderStatistics)
|
||||
for id := range m.cfg.Folders() {
|
||||
res[id] = m.folderStatRef(id).GetStatistics()
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
for id, runner := range m.folderRunners {
|
||||
res[id] = runner.GetStatistics()
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -614,7 +667,7 @@ type FolderCompletion struct {
|
||||
|
||||
// Completion returns the completion status, in percent, for the given device
|
||||
// and folder.
|
||||
func (m *Model) Completion(device protocol.DeviceID, folder string) FolderCompletion {
|
||||
func (m *model) Completion(device protocol.DeviceID, folder string) FolderCompletion {
|
||||
m.fmut.RLock()
|
||||
rf, ok := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
@@ -696,7 +749,7 @@ func addSizeOfFile(s *db.Counts, f db.FileIntf) {
|
||||
|
||||
// GlobalSize returns the number of files, deleted files and total bytes for all
|
||||
// files in the global model.
|
||||
func (m *Model) GlobalSize(folder string) db.Counts {
|
||||
func (m *model) GlobalSize(folder string) db.Counts {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
@@ -707,7 +760,7 @@ func (m *Model) GlobalSize(folder string) db.Counts {
|
||||
|
||||
// LocalSize returns the number of files, deleted files and total bytes for all
|
||||
// files in the local folder.
|
||||
func (m *Model) LocalSize(folder string) db.Counts {
|
||||
func (m *model) LocalSize(folder string) db.Counts {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
@@ -719,7 +772,7 @@ func (m *Model) LocalSize(folder string) db.Counts {
|
||||
// ReceiveOnlyChangedSize returns the number of files, deleted files and
|
||||
// total bytes for all files that have changed locally in a receieve only
|
||||
// folder.
|
||||
func (m *Model) ReceiveOnlyChangedSize(folder string) db.Counts {
|
||||
func (m *model) ReceiveOnlyChangedSize(folder string) db.Counts {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
@@ -729,7 +782,7 @@ func (m *Model) ReceiveOnlyChangedSize(folder string) db.Counts {
|
||||
}
|
||||
|
||||
// NeedSize returns the number and total size of currently needed files.
|
||||
func (m *Model) NeedSize(folder string) db.Counts {
|
||||
func (m *model) NeedSize(folder string) db.Counts {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
|
||||
@@ -753,7 +806,7 @@ func (m *Model) NeedSize(folder string) db.Counts {
|
||||
// NeedFolderFiles returns paginated list of currently needed files in
|
||||
// progress, queued, and to be queued on next puller iteration, as well as the
|
||||
// total number of files currently needed.
|
||||
func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) {
|
||||
func (m *model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
|
||||
@@ -820,7 +873,7 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
|
||||
// LocalChangedFiles returns a paginated list of currently needed files in
|
||||
// progress, queued, and to be queued on next puller iteration, as well as the
|
||||
// total number of files currently needed.
|
||||
func (m *Model) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
|
||||
func (m *model) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
|
||||
@@ -861,7 +914,7 @@ func (m *Model) LocalChangedFiles(folder string, page, perpage int) []db.FileInf
|
||||
// RemoteNeedFolderFiles returns paginated list of currently needed files in
|
||||
// progress, queued, and to be queued on next puller iteration, as well as the
|
||||
// total number of files currently needed.
|
||||
func (m *Model) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error) {
|
||||
func (m *model) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error) {
|
||||
m.fmut.RLock()
|
||||
m.pmut.RLock()
|
||||
if err := m.checkDeviceFolderConnectedLocked(device, folder); err != nil {
|
||||
@@ -891,17 +944,17 @@ func (m *Model) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, p
|
||||
|
||||
// Index is called when a new device is connected and we receive their full index.
|
||||
// Implements the protocol.Model interface.
|
||||
func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo) {
|
||||
func (m *model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo) {
|
||||
m.handleIndex(deviceID, folder, fs, false)
|
||||
}
|
||||
|
||||
// IndexUpdate is called for incremental updates to connected devices' indexes.
|
||||
// Implements the protocol.Model interface.
|
||||
func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo) {
|
||||
func (m *model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo) {
|
||||
m.handleIndex(deviceID, folder, fs, true)
|
||||
}
|
||||
|
||||
func (m *Model) handleIndex(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo, update bool) {
|
||||
func (m *model) handleIndex(deviceID protocol.DeviceID, folder string, fs []protocol.FileInfo, update bool) {
|
||||
op := "Index"
|
||||
if update {
|
||||
op += " update"
|
||||
@@ -923,7 +976,7 @@ func (m *Model) handleIndex(deviceID protocol.DeviceID, folder string, fs []prot
|
||||
m.fmut.RUnlock()
|
||||
|
||||
if !existing {
|
||||
l.Fatalf("%v for nonexistent folder %q", op, folder)
|
||||
panic(fmt.Sprintf("%v for nonexistent folder %q", op, folder))
|
||||
}
|
||||
|
||||
if running {
|
||||
@@ -931,7 +984,7 @@ func (m *Model) handleIndex(deviceID protocol.DeviceID, folder string, fs []prot
|
||||
} else if update {
|
||||
// Runner may legitimately not be set if this is the "cleanup" Index
|
||||
// message at startup.
|
||||
l.Fatalf("%v for not running folder %q", op, folder)
|
||||
panic(fmt.Sprintf("%v for not running folder %q", op, folder))
|
||||
}
|
||||
|
||||
m.pmut.RLock()
|
||||
@@ -956,7 +1009,7 @@ func (m *Model) handleIndex(deviceID protocol.DeviceID, folder string, fs []prot
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterConfig) {
|
||||
func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterConfig) {
|
||||
// Check the peer device's announced folders against our own. Emits events
|
||||
// for folders that we don't expect (unknown or not shared).
|
||||
// Also, collect a list of folders we do share, and if he's interested in
|
||||
@@ -1096,7 +1149,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
}
|
||||
}
|
||||
|
||||
go sendIndexes(conn, folder.ID, fs, m.folderIgnores[folder.ID], startSequence, dropSymlinks)
|
||||
go sendIndexes(conn, folder.ID, fs, startSequence, dropSymlinks)
|
||||
}
|
||||
|
||||
m.pmut.Lock()
|
||||
@@ -1136,7 +1189,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
}
|
||||
|
||||
// handleIntroductions handles adding devices/shares that are shared by an introducer device
|
||||
func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig) (folderDeviceSet, bool) {
|
||||
func (m *model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig) (folderDeviceSet, bool) {
|
||||
// This device is an introducer. Go through the announced lists of folders
|
||||
// and devices and add what we are missing, remove what we have extra that
|
||||
// has been introducer by the introducer.
|
||||
@@ -1192,7 +1245,7 @@ func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
|
||||
}
|
||||
|
||||
// handleDeintroductions handles removals of devices/shares that are removed by an introducer device
|
||||
func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig, foldersDevices folderDeviceSet) bool {
|
||||
func (m *model) handleDeintroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig, foldersDevices folderDeviceSet) bool {
|
||||
changed := false
|
||||
devicesNotIntroduced := make(map[protocol.DeviceID]struct{})
|
||||
|
||||
@@ -1249,7 +1302,7 @@ func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
|
||||
|
||||
// handleAutoAccepts handles adding and sharing folders for devices that have
|
||||
// AutoAcceptFolders set to true.
|
||||
func (m *Model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder protocol.Folder) bool {
|
||||
func (m *model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder protocol.Folder) bool {
|
||||
if cfg, ok := m.cfg.Folder(folder.ID); !ok {
|
||||
defaultPath := m.cfg.Options().DefaultFolderPath
|
||||
defaultPathFs := fs.NewFilesystem(fs.FilesystemTypeBasic, defaultPath)
|
||||
@@ -1295,7 +1348,7 @@ func (m *Model) handleAutoAccepts(deviceCfg config.DeviceConfiguration, folder p
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) {
|
||||
func (m *model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) {
|
||||
addresses := []string{"dynamic"}
|
||||
for _, addr := range device.Addresses {
|
||||
if addr != "dynamic" {
|
||||
@@ -1324,7 +1377,7 @@ func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.Dev
|
||||
}
|
||||
|
||||
// Closed is called when a connection has been closed
|
||||
func (m *Model) Closed(conn protocol.Connection, err error) {
|
||||
func (m *model) Closed(conn protocol.Connection, err error) {
|
||||
device := conn.ID()
|
||||
|
||||
m.pmut.Lock()
|
||||
@@ -1350,14 +1403,14 @@ func (m *Model) Closed(conn protocol.Connection, err error) {
|
||||
}
|
||||
|
||||
// close will close the underlying connection for a given device
|
||||
func (m *Model) close(device protocol.DeviceID, err error) {
|
||||
func (m *model) close(device protocol.DeviceID, err error) {
|
||||
m.pmut.Lock()
|
||||
m.closeLocked(device, err)
|
||||
m.pmut.Unlock()
|
||||
}
|
||||
|
||||
// closeLocked will close the underlying connection for a given device
|
||||
func (m *Model) closeLocked(device protocol.DeviceID, err error) {
|
||||
func (m *model) closeLocked(device protocol.DeviceID, err error) {
|
||||
conn, ok := m.conn[device]
|
||||
if !ok {
|
||||
// There is no connection to close
|
||||
@@ -1398,7 +1451,7 @@ func (r *requestResponse) Wait() {
|
||||
|
||||
// Request returns the specified data segment by reading it from local disk.
|
||||
// Implements the protocol.Model interface.
|
||||
func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (out protocol.RequestResponse, err error) {
|
||||
func (m *model) Request(deviceID protocol.DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (out protocol.RequestResponse, err error) {
|
||||
if size < 0 || offset < 0 {
|
||||
return nil, protocol.ErrInvalid
|
||||
}
|
||||
@@ -1519,7 +1572,7 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, size in
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *Model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem, folder, name string, blockIndex int, hash []byte) {
|
||||
func (m *model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem, folder, name string, blockIndex int, hash []byte) {
|
||||
cf, ok := m.CurrentFolderFile(folder, name)
|
||||
if !ok {
|
||||
l.Debugf("%v recheckFile: %s: %q / %q: no current file", m, deviceID, folder, name)
|
||||
@@ -1560,7 +1613,7 @@ func (m *Model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
func (m *model) CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
m.fmut.RLock()
|
||||
fs, ok := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
@@ -1570,7 +1623,7 @@ func (m *Model) CurrentFolderFile(folder string, file string) (protocol.FileInfo
|
||||
return fs.Get(protocol.LocalDeviceID, file)
|
||||
}
|
||||
|
||||
func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
func (m *model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
m.fmut.RLock()
|
||||
fs, ok := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
@@ -1581,7 +1634,7 @@ func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo
|
||||
}
|
||||
|
||||
type cFiler struct {
|
||||
m *Model
|
||||
m Model
|
||||
r string
|
||||
}
|
||||
|
||||
@@ -1591,7 +1644,7 @@ func (cf cFiler) CurrentFile(file string) (protocol.FileInfo, bool) {
|
||||
}
|
||||
|
||||
// Connection returns the current connection for device, and a boolean whether a connection was found.
|
||||
func (m *Model) Connection(deviceID protocol.DeviceID) (connections.Connection, bool) {
|
||||
func (m *model) Connection(deviceID protocol.DeviceID) (connections.Connection, bool) {
|
||||
m.pmut.RLock()
|
||||
cn, ok := m.conn[deviceID]
|
||||
m.pmut.RUnlock()
|
||||
@@ -1601,7 +1654,7 @@ func (m *Model) Connection(deviceID protocol.DeviceID) (connections.Connection,
|
||||
return cn, ok
|
||||
}
|
||||
|
||||
func (m *Model) GetIgnores(folder string) ([]string, []string, error) {
|
||||
func (m *model) GetIgnores(folder string) ([]string, []string, error) {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
|
||||
@@ -1630,7 +1683,7 @@ func (m *Model) GetIgnores(folder string) ([]string, []string, error) {
|
||||
return ignores.Lines(), ignores.Patterns(), nil
|
||||
}
|
||||
|
||||
func (m *Model) SetIgnores(folder string, content []string) error {
|
||||
func (m *model) SetIgnores(folder string, content []string) error {
|
||||
cfg, ok := m.cfg.Folders()[folder]
|
||||
if !ok {
|
||||
return fmt.Errorf("folder %s does not exist", cfg.Description())
|
||||
@@ -1664,7 +1717,7 @@ func (m *Model) SetIgnores(folder string, content []string) error {
|
||||
// OnHello is called when an device connects to us.
|
||||
// This allows us to extract some information from the Hello message
|
||||
// and add it to a list of known devices ahead of any checks.
|
||||
func (m *Model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protocol.HelloResult) error {
|
||||
func (m *model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protocol.HelloResult) error {
|
||||
if m.cfg.IgnoredDevice(remoteID) {
|
||||
return errDeviceIgnored
|
||||
}
|
||||
@@ -1694,7 +1747,7 @@ func (m *Model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protoco
|
||||
}
|
||||
|
||||
// GetHello is called when we are about to connect to some remote device.
|
||||
func (m *Model) GetHello(id protocol.DeviceID) protocol.HelloIntf {
|
||||
func (m *model) GetHello(id protocol.DeviceID) protocol.HelloIntf {
|
||||
name := ""
|
||||
if _, ok := m.cfg.Device(id); ok {
|
||||
name = m.cfg.MyName()
|
||||
@@ -1709,7 +1762,7 @@ func (m *Model) GetHello(id protocol.DeviceID) protocol.HelloIntf {
|
||||
// AddConnection adds a new peer connection to the model. An initial index will
|
||||
// be sent to the connected peer, thereafter index updates whenever the local
|
||||
// folder changes.
|
||||
func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloResult) {
|
||||
func (m *model) AddConnection(conn connections.Connection, hello protocol.HelloResult) {
|
||||
deviceID := conn.ID()
|
||||
device, ok := m.cfg.Device(deviceID)
|
||||
if !ok {
|
||||
@@ -1778,7 +1831,7 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
|
||||
m.deviceWasSeen(deviceID)
|
||||
}
|
||||
|
||||
func (m *Model) DownloadProgress(device protocol.DeviceID, folder string, updates []protocol.FileDownloadProgressUpdate) {
|
||||
func (m *model) DownloadProgress(device protocol.DeviceID, folder string, updates []protocol.FileDownloadProgressUpdate) {
|
||||
m.fmut.RLock()
|
||||
cfg, ok := m.folderCfgs[folder]
|
||||
m.fmut.RUnlock()
|
||||
@@ -1799,7 +1852,7 @@ func (m *Model) DownloadProgress(device protocol.DeviceID, folder string, update
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatisticsReference {
|
||||
func (m *model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatisticsReference {
|
||||
m.fmut.Lock()
|
||||
defer m.fmut.Unlock()
|
||||
|
||||
@@ -1812,27 +1865,11 @@ func (m *Model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatistic
|
||||
return sr
|
||||
}
|
||||
|
||||
func (m *Model) deviceWasSeen(deviceID protocol.DeviceID) {
|
||||
func (m *model) deviceWasSeen(deviceID protocol.DeviceID) {
|
||||
m.deviceStatRef(deviceID).WasSeen()
|
||||
}
|
||||
|
||||
func (m *Model) folderStatRef(folder string) *stats.FolderStatisticsReference {
|
||||
m.fmut.Lock()
|
||||
defer m.fmut.Unlock()
|
||||
|
||||
sr, ok := m.folderStatRefs[folder]
|
||||
if !ok {
|
||||
sr = stats.NewFolderStatisticsReference(m.db, folder)
|
||||
m.folderStatRefs[folder] = sr
|
||||
}
|
||||
return sr
|
||||
}
|
||||
|
||||
func (m *Model) receivedFile(folder string, file protocol.FileInfo) {
|
||||
m.folderStatRef(folder).ReceivedFile(file.Name, file.IsDeleted())
|
||||
}
|
||||
|
||||
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, prevSequence int64, dropSymlinks bool) {
|
||||
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, prevSequence int64, dropSymlinks bool) {
|
||||
deviceID := conn.ID()
|
||||
var err error
|
||||
|
||||
@@ -1840,7 +1877,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
|
||||
defer l.Debugf("Exiting sendIndexes for %s to %s at %s: %v", folder, deviceID, conn, err)
|
||||
|
||||
// We need to send one index, regardless of whether there is something to send or not
|
||||
prevSequence, err = sendIndexTo(prevSequence, conn, folder, fs, ignores, dropSymlinks)
|
||||
prevSequence, err = sendIndexTo(prevSequence, conn, folder, fs, dropSymlinks)
|
||||
|
||||
// Subscribe to LocalIndexUpdated (we have new information to send) and
|
||||
// DeviceDisconnected (it might be us who disconnected, so we should
|
||||
@@ -1863,7 +1900,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
|
||||
continue
|
||||
}
|
||||
|
||||
prevSequence, err = sendIndexTo(prevSequence, conn, folder, fs, ignores, dropSymlinks)
|
||||
prevSequence, err = sendIndexTo(prevSequence, conn, folder, fs, dropSymlinks)
|
||||
|
||||
// Wait a short amount of time before entering the next loop. If there
|
||||
// are continuous changes happening to the local index, this gives us
|
||||
@@ -1874,7 +1911,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
|
||||
|
||||
// sendIndexTo sends file infos with a sequence number higher than prevSequence and
|
||||
// returns the highest sent sequence number.
|
||||
func sendIndexTo(prevSequence int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, dropSymlinks bool) (int64, error) {
|
||||
func sendIndexTo(prevSequence int64, conn protocol.Connection, folder string, fs *db.FileSet, dropSymlinks bool) (int64, error) {
|
||||
deviceID := conn.ID()
|
||||
initial := prevSequence == 0
|
||||
batch := newFileInfoBatch(nil)
|
||||
@@ -1949,7 +1986,7 @@ func sendIndexTo(prevSequence int64, conn protocol.Connection, folder string, fs
|
||||
return f.Sequence, err
|
||||
}
|
||||
|
||||
func (m *Model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo) {
|
||||
func (m *model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo) {
|
||||
m.updateLocals(folder, fs)
|
||||
|
||||
m.fmut.RLock()
|
||||
@@ -1959,7 +1996,7 @@ func (m *Model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo)
|
||||
m.diskChangeDetected(folderCfg, fs, events.LocalChangeDetected)
|
||||
}
|
||||
|
||||
func (m *Model) updateLocalsFromPulling(folder string, fs []protocol.FileInfo) {
|
||||
func (m *model) updateLocalsFromPulling(folder string, fs []protocol.FileInfo) {
|
||||
m.updateLocals(folder, fs)
|
||||
|
||||
m.fmut.RLock()
|
||||
@@ -1969,7 +2006,7 @@ func (m *Model) updateLocalsFromPulling(folder string, fs []protocol.FileInfo) {
|
||||
m.diskChangeDetected(folderCfg, fs, events.RemoteChangeDetected)
|
||||
}
|
||||
|
||||
func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
|
||||
func (m *model) updateLocals(folder string, fs []protocol.FileInfo) {
|
||||
m.fmut.RLock()
|
||||
files := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
@@ -1992,7 +2029,7 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) diskChangeDetected(folderCfg config.FolderConfiguration, files []protocol.FileInfo, typeOfEvent events.EventType) {
|
||||
func (m *model) diskChangeDetected(folderCfg config.FolderConfiguration, files []protocol.FileInfo, typeOfEvent events.EventType) {
|
||||
for _, file := range files {
|
||||
if file.IsInvalid() {
|
||||
continue
|
||||
@@ -2035,7 +2072,7 @@ func (m *Model) diskChangeDetected(folderCfg config.FolderConfiguration, files [
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
func (m *model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
|
||||
m.pmut.RLock()
|
||||
nc, ok := m.conn[deviceID]
|
||||
m.pmut.RUnlock()
|
||||
@@ -2049,7 +2086,7 @@ func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, o
|
||||
return nc.Request(folder, name, offset, size, hash, weakHash, fromTemporary)
|
||||
}
|
||||
|
||||
func (m *Model) ScanFolders() map[string]error {
|
||||
func (m *model) ScanFolders() map[string]error {
|
||||
m.fmut.RLock()
|
||||
folders := make([]string, 0, len(m.folderCfgs))
|
||||
for folder := range m.folderCfgs {
|
||||
@@ -2087,11 +2124,11 @@ func (m *Model) ScanFolders() map[string]error {
|
||||
return errors
|
||||
}
|
||||
|
||||
func (m *Model) ScanFolder(folder string) error {
|
||||
func (m *model) ScanFolder(folder string) error {
|
||||
return m.ScanFolderSubdirs(folder, nil)
|
||||
}
|
||||
|
||||
func (m *Model) ScanFolderSubdirs(folder string, subs []string) error {
|
||||
func (m *model) ScanFolderSubdirs(folder string, subs []string) error {
|
||||
m.fmut.RLock()
|
||||
if err := m.checkFolderRunningLocked(folder); err != nil {
|
||||
m.fmut.RUnlock()
|
||||
@@ -2103,7 +2140,7 @@ func (m *Model) ScanFolderSubdirs(folder string, subs []string) error {
|
||||
return runner.Scan(subs)
|
||||
}
|
||||
|
||||
func (m *Model) DelayScan(folder string, next time.Duration) {
|
||||
func (m *model) DelayScan(folder string, next time.Duration) {
|
||||
m.fmut.Lock()
|
||||
runner, ok := m.folderRunners[folder]
|
||||
m.fmut.Unlock()
|
||||
@@ -2115,7 +2152,7 @@ func (m *Model) DelayScan(folder string, next time.Duration) {
|
||||
|
||||
// numHashers returns the number of hasher routines to use for a given folder,
|
||||
// taking into account configuration and available CPU cores.
|
||||
func (m *Model) numHashers(folder string) int {
|
||||
func (m *model) numHashers(folder string) int {
|
||||
m.fmut.Lock()
|
||||
folderCfg := m.folderCfgs[folder]
|
||||
numFolders := len(m.folderCfgs)
|
||||
@@ -2144,7 +2181,7 @@ func (m *Model) numHashers(folder string) int {
|
||||
|
||||
// generateClusterConfig returns a ClusterConfigMessage that is correct for
|
||||
// the given peer device
|
||||
func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.ClusterConfig {
|
||||
func (m *model) generateClusterConfig(device protocol.DeviceID) protocol.ClusterConfig {
|
||||
var message protocol.ClusterConfig
|
||||
|
||||
m.fmut.RLock()
|
||||
@@ -2201,7 +2238,7 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
|
||||
return message
|
||||
}
|
||||
|
||||
func (m *Model) State(folder string) (string, time.Time, error) {
|
||||
func (m *model) State(folder string) (string, time.Time, error) {
|
||||
m.fmut.RLock()
|
||||
runner, ok := m.folderRunners[folder]
|
||||
m.fmut.RUnlock()
|
||||
@@ -2215,7 +2252,7 @@ func (m *Model) State(folder string) (string, time.Time, error) {
|
||||
return state.String(), changed, err
|
||||
}
|
||||
|
||||
func (m *Model) FolderErrors(folder string) ([]FileError, error) {
|
||||
func (m *model) FolderErrors(folder string) ([]FileError, error) {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if err := m.checkFolderRunningLocked(folder); err != nil {
|
||||
@@ -2224,7 +2261,7 @@ func (m *Model) FolderErrors(folder string) ([]FileError, error) {
|
||||
return m.folderRunners[folder].Errors(), nil
|
||||
}
|
||||
|
||||
func (m *Model) WatchError(folder string) error {
|
||||
func (m *model) WatchError(folder string) error {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if err := m.checkFolderRunningLocked(folder); err != nil {
|
||||
@@ -2233,7 +2270,7 @@ func (m *Model) WatchError(folder string) error {
|
||||
return m.folderRunners[folder].WatchError()
|
||||
}
|
||||
|
||||
func (m *Model) Override(folder string) {
|
||||
func (m *model) Override(folder string) {
|
||||
// Grab the runner and the file set.
|
||||
|
||||
m.fmut.RLock()
|
||||
@@ -2251,7 +2288,7 @@ func (m *Model) Override(folder string) {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) Revert(folder string) {
|
||||
func (m *model) Revert(folder string) {
|
||||
// Grab the runner and the file set.
|
||||
|
||||
m.fmut.RLock()
|
||||
@@ -2272,7 +2309,7 @@ func (m *Model) Revert(folder string) {
|
||||
// CurrentSequence returns the change version for the given folder.
|
||||
// This is guaranteed to increment if the contents of the local folder has
|
||||
// changed.
|
||||
func (m *Model) CurrentSequence(folder string) (int64, bool) {
|
||||
func (m *model) CurrentSequence(folder string) (int64, bool) {
|
||||
m.fmut.RLock()
|
||||
fs, ok := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
@@ -2288,7 +2325,7 @@ func (m *Model) CurrentSequence(folder string) (int64, bool) {
|
||||
// RemoteSequence returns the change version for the given folder, as
|
||||
// sent by remote peers. This is guaranteed to increment if the contents of
|
||||
// the remote or global folder has changed.
|
||||
func (m *Model) RemoteSequence(folder string) (int64, bool) {
|
||||
func (m *model) RemoteSequence(folder string) (int64, bool) {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
|
||||
@@ -2308,7 +2345,7 @@ func (m *Model) RemoteSequence(folder string) (int64, bool) {
|
||||
return ver, true
|
||||
}
|
||||
|
||||
func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{} {
|
||||
func (m *model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{} {
|
||||
m.fmut.RLock()
|
||||
files, ok := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
@@ -2372,7 +2409,7 @@ func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly
|
||||
return output
|
||||
}
|
||||
|
||||
func (m *Model) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) {
|
||||
func (m *model) GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error) {
|
||||
fcfg, ok := m.cfg.Folder(folder)
|
||||
if !ok {
|
||||
return nil, errFolderMissing
|
||||
@@ -2432,7 +2469,7 @@ func (m *Model) GetFolderVersions(folder string) (map[string][]versioner.FileVer
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (m *Model) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
|
||||
func (m *model) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
|
||||
fcfg, ok := m.cfg.Folder(folder)
|
||||
if !ok {
|
||||
return nil, errFolderMissing
|
||||
@@ -2503,7 +2540,7 @@ func (m *Model) RestoreFolderVersions(folder string, versions map[string]time.Ti
|
||||
return errors, nil
|
||||
}
|
||||
|
||||
func (m *Model) Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
|
||||
func (m *model) Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
|
||||
// The slightly unusual locking sequence here is because we need to hold
|
||||
// pmut for the duration (as the value returned from foldersFiles can
|
||||
// get heavily modified on Close()), but also must acquire fmut before
|
||||
@@ -2544,7 +2581,7 @@ next:
|
||||
}
|
||||
|
||||
// BringToFront bumps the given files priority in the job queue.
|
||||
func (m *Model) BringToFront(folder, file string) {
|
||||
func (m *model) BringToFront(folder, file string) {
|
||||
m.pmut.RLock()
|
||||
defer m.pmut.RUnlock()
|
||||
|
||||
@@ -2554,20 +2591,20 @@ func (m *Model) BringToFront(folder, file string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) ResetFolder(folder string) {
|
||||
func (m *model) ResetFolder(folder string) {
|
||||
l.Infof("Cleaning data for folder %q", folder)
|
||||
db.DropFolder(m.db, folder)
|
||||
}
|
||||
|
||||
func (m *Model) String() string {
|
||||
func (m *model) String() string {
|
||||
return fmt.Sprintf("model@%p", m)
|
||||
}
|
||||
|
||||
func (m *Model) VerifyConfiguration(from, to config.Configuration) error {
|
||||
func (m *model) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
||||
func (m *model) CommitConfiguration(from, to config.Configuration) bool {
|
||||
// TODO: This should not use reflect, and should take more care to try to handle stuff without restart.
|
||||
|
||||
// Go through the folder configs and figure out if we need to restart or not.
|
||||
@@ -2661,7 +2698,7 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
||||
// checkFolderRunningLocked returns nil if the folder is up and running and a
|
||||
// descriptive error if not.
|
||||
// Need to hold (read) lock on m.fmut when calling this.
|
||||
func (m *Model) checkFolderRunningLocked(folder string) error {
|
||||
func (m *model) checkFolderRunningLocked(folder string) error {
|
||||
_, ok := m.folderRunners[folder]
|
||||
if ok {
|
||||
return nil
|
||||
@@ -2679,7 +2716,7 @@ func (m *Model) checkFolderRunningLocked(folder string) error {
|
||||
// checkFolderDeviceStatusLocked first checks the folder and then whether the
|
||||
// given device is connected and shares this folder.
|
||||
// Need to hold (read) lock on both m.fmut and m.pmut when calling this.
|
||||
func (m *Model) checkDeviceFolderConnectedLocked(device protocol.DeviceID, folder string) error {
|
||||
func (m *model) checkDeviceFolderConnectedLocked(device protocol.DeviceID, folder string) error {
|
||||
if err := m.checkFolderRunningLocked(folder); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2814,7 +2851,7 @@ func (b *fileInfoBatch) append(f protocol.FileInfo) {
|
||||
}
|
||||
|
||||
func (b *fileInfoBatch) flushIfFull() error {
|
||||
if len(b.infos) == maxBatchSizeFiles || b.size > maxBatchSizeBytes {
|
||||
if len(b.infos) >= maxBatchSizeFiles || b.size >= maxBatchSizeBytes {
|
||||
return b.flush()
|
||||
}
|
||||
return nil
|
||||
|
||||
+206
-352
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@ type ProgressEmitter struct {
|
||||
|
||||
// NewProgressEmitter creates a new progress emitter which emits
|
||||
// DownloadProgress events every interval.
|
||||
func NewProgressEmitter(cfg *config.Wrapper) *ProgressEmitter {
|
||||
func NewProgressEmitter(cfg config.Wrapper) *ProgressEmitter {
|
||||
t := &ProgressEmitter{
|
||||
stop: make(chan struct{}),
|
||||
registry: make(map[string]*sharedPullerState),
|
||||
|
||||
@@ -8,6 +8,7 @@ package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
@@ -51,12 +52,10 @@ func expectTimeout(w *events.Subscription, t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProgressEmitter(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
w := events.Default.Subscribe(events.DownloadProgress)
|
||||
|
||||
c := createTmpWrapper(config.Configuration{})
|
||||
defer testOs.Remove(c.ConfigPath())
|
||||
defer os.Remove(c.ConfigPath())
|
||||
c.SetOptions(config.OptionsConfiguration{
|
||||
ProgressUpdateIntervalS: 0,
|
||||
})
|
||||
@@ -104,10 +103,8 @@ func TestProgressEmitter(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSendDownloadProgressMessages(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
c := createTmpWrapper(config.Configuration{})
|
||||
defer testOs.Remove(c.ConfigPath())
|
||||
defer os.Remove(c.ConfigPath())
|
||||
c.SetOptions(config.OptionsConfiguration{
|
||||
ProgressUpdateIntervalS: 0,
|
||||
TempIndexMinBlocks: 10,
|
||||
|
||||
+130
-130
@@ -18,7 +18,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/ignore"
|
||||
@@ -26,16 +25,15 @@ import (
|
||||
)
|
||||
|
||||
func TestRequestSimple(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
// Verify that the model performs a request and creates a file based on
|
||||
// an incoming index update.
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
tfs := fcfg.Filesystem()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(tfs.URI())
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
|
||||
// We listen for incoming index updates and trigger when we see one for
|
||||
@@ -64,14 +62,12 @@ func TestRequestSimple(t *testing.T) {
|
||||
<-done
|
||||
|
||||
// Verify the contents
|
||||
if err := equalContents(filepath.Join(tmpDir, "testfile"), contents); err != nil {
|
||||
if err := equalContents(filepath.Join(tfs.URI(), "testfile"), contents); err != nil {
|
||||
t.Error("File did not sync correctly:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSymlinkTraversalRead(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
// Verify that a symlink can not be traversed for reading.
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -79,11 +75,11 @@ func TestSymlinkTraversalRead(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(fcfg.Filesystem().URI())
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
|
||||
// We listen for incoming index updates and trigger when we see one for
|
||||
@@ -119,8 +115,6 @@ func TestSymlinkTraversalRead(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSymlinkTraversalWrite(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
// Verify that a symlink can not be traversed for writing.
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
@@ -128,11 +122,11 @@ func TestSymlinkTraversalWrite(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(fcfg.Filesystem().URI())
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
|
||||
// We listen for incoming index updates and trigger when we see one for
|
||||
@@ -189,15 +183,13 @@ func TestSymlinkTraversalWrite(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRequestCreateTmpSymlink(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
// Test that an update for a temporary file is invalidated
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(fcfg.Filesystem().URI())
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
|
||||
// We listen for incoming index updates and trigger when we see one for
|
||||
@@ -231,8 +223,6 @@ func TestRequestCreateTmpSymlink(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRequestVersioningSymlinkAttack(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("no symlink support on Windows")
|
||||
}
|
||||
@@ -240,39 +230,25 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
|
||||
// Sets up a folder with trashcan versioning and tries to use a
|
||||
// deleted symlink to escape
|
||||
|
||||
tmpDir := createTmpDir()
|
||||
defer testOs.RemoveAll(tmpDir)
|
||||
w, fcfg := tmpDefaultWrapper()
|
||||
defer func() {
|
||||
os.RemoveAll(fcfg.Filesystem().URI())
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
|
||||
cfg := defaultCfgWrapper.RawCopy()
|
||||
cfg.Folders[0] = config.NewFolderConfiguration(myID, "default", "default", fs.FilesystemTypeBasic, tmpDir)
|
||||
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
|
||||
{DeviceID: myID},
|
||||
{DeviceID: device1},
|
||||
}
|
||||
cfg.Folders[0].Versioning = config.VersioningConfiguration{
|
||||
Type: "trashcan",
|
||||
}
|
||||
w := createTmpWrapper(cfg)
|
||||
defer testOs.Remove(w.ConfigPath())
|
||||
fcfg.Versioning = config.VersioningConfiguration{Type: "trashcan"}
|
||||
w.SetFolder(fcfg)
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(w, myID, "syncthing", "dev", db, nil)
|
||||
m.AddFolder(cfg.Folders[0])
|
||||
m.ServeBackground()
|
||||
m.StartFolder("default")
|
||||
m, fc := setupModelWithConnectionFromWrapper(w)
|
||||
defer m.Stop()
|
||||
|
||||
defer testOs.RemoveAll(tmpDir)
|
||||
|
||||
fc := addFakeConn(m, device1)
|
||||
fc.folder = "default"
|
||||
|
||||
// Create a temporary directory that we will use as target to see if
|
||||
// we can escape to it
|
||||
tmpdir, err := ioutil.TempDir("", "syncthing-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
// We listen for incoming index updates and trigger when we see one for
|
||||
// the expected test file.
|
||||
@@ -328,17 +304,16 @@ func TestPullInvalidIgnoredSR(t *testing.T) {
|
||||
func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
|
||||
t.Helper()
|
||||
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
w := createTmpWrapper(defaultCfgWrapper.RawCopy())
|
||||
fcfg, tmpDir := testFolderConfigTmp()
|
||||
fcfg := testFolderConfigTmp()
|
||||
fss := fcfg.Filesystem()
|
||||
fcfg.Type = ft
|
||||
w.SetFolder(fcfg)
|
||||
m, fc := setupModelWithConnectionFromWrapper(w)
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(fss.URI())
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
|
||||
// Reach in and update the ignore matcher to one that always does
|
||||
@@ -346,7 +321,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
|
||||
// because we might be changing the files on disk often enough that the
|
||||
// mtimes will be unreliable to determine change status.
|
||||
m.fmut.Lock()
|
||||
m.folderIgnores["default"] = ignore.New(fcfg.Filesystem(), ignore.WithChangeDetector(newAlwaysChanged()))
|
||||
m.folderIgnores["default"] = ignore.New(fss, ignore.WithChangeDetector(newAlwaysChanged()))
|
||||
m.fmut.Unlock()
|
||||
|
||||
if err := m.SetIgnores("default", []string{"*ignored*"}); err != nil {
|
||||
@@ -366,7 +341,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
|
||||
fc.deleteFile(invDel)
|
||||
fc.addFile(ign, 0644, protocol.FileInfoTypeFile, contents)
|
||||
fc.addFile(ignExisting, 0644, protocol.FileInfoTypeFile, contents)
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpDir, ignExisting), otherContents, 0644); err != nil {
|
||||
if err := ioutil.WriteFile(filepath.Join(fss.URI(), ignExisting), otherContents, 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -456,13 +431,11 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
|
||||
}
|
||||
|
||||
func TestIssue4841(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(fcfg.Filesystem().URI())
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
|
||||
received := make(chan protocol.FileInfo)
|
||||
@@ -499,20 +472,17 @@ func TestIssue4841(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRescanIfHaveInvalidContent(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
tmpDir := fcfg.Filesystem().URI()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(tmpDir)
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
|
||||
payload := []byte("hello")
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpDir, "foo"), payload, 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, ioutil.WriteFile(filepath.Join(tmpDir, "foo"), payload, 0777))
|
||||
|
||||
received := make(chan protocol.FileInfo)
|
||||
fc.mut.Lock()
|
||||
@@ -549,9 +519,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
|
||||
payload = []byte("bye")
|
||||
buf = make([]byte, len(payload))
|
||||
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpDir, "foo"), payload, 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, ioutil.WriteFile(filepath.Join(tmpDir, "foo"), payload, 0777))
|
||||
|
||||
_, err = m.Request(device1, "default", "foo", int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
|
||||
if err == nil {
|
||||
@@ -569,18 +537,16 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParentDeletion(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
testFs := fcfg.Filesystem()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(testFs.URI())
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
|
||||
parent := "foo"
|
||||
child := filepath.Join(parent, "bar")
|
||||
testFs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir)
|
||||
|
||||
received := make(chan []protocol.FileInfo)
|
||||
fc.addFile(parent, 0777, protocol.FileInfoTypeDirectory, nil)
|
||||
@@ -603,9 +569,7 @@ func TestParentDeletion(t *testing.T) {
|
||||
}
|
||||
|
||||
// Delete parent dir
|
||||
if err := testFs.RemoveAll(parent); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, testFs.RemoveAll(parent))
|
||||
|
||||
// Scan only the child dir (not the parent)
|
||||
if err := m.ScanFolderSubdirs("default", []string{child}); err != nil {
|
||||
@@ -656,13 +620,11 @@ func TestRequestSymlinkWindows(t *testing.T) {
|
||||
t.Skip("windows specific test")
|
||||
}
|
||||
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(fcfg.Filesystem().URI())
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
|
||||
done := make(chan struct{})
|
||||
@@ -719,9 +681,16 @@ func TestRequestSymlinkWindows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testFolderConfigTmp() (config.FolderConfiguration, string) {
|
||||
func tmpDefaultWrapper() (config.Wrapper, config.FolderConfiguration) {
|
||||
w := createTmpWrapper(defaultCfgWrapper.RawCopy())
|
||||
fcfg := testFolderConfigTmp()
|
||||
w.SetFolder(fcfg)
|
||||
return w, fcfg
|
||||
}
|
||||
|
||||
func testFolderConfigTmp() config.FolderConfiguration {
|
||||
tmpDir := createTmpDir()
|
||||
return testFolderConfig(tmpDir), tmpDir
|
||||
return testFolderConfig(tmpDir)
|
||||
}
|
||||
|
||||
func testFolderConfig(path string) config.FolderConfiguration {
|
||||
@@ -731,15 +700,13 @@ func testFolderConfig(path string) config.FolderConfiguration {
|
||||
return cfg
|
||||
}
|
||||
|
||||
func setupModelWithConnection() (*Model, *fakeConnection, string, *config.Wrapper) {
|
||||
w := createTmpWrapper(defaultCfgWrapper.RawCopy())
|
||||
fcfg, tmpDir := testFolderConfigTmp()
|
||||
w.SetFolder(fcfg)
|
||||
func setupModelWithConnection() (*model, *fakeConnection, config.FolderConfiguration, config.Wrapper) {
|
||||
w, fcfg := tmpDefaultWrapper()
|
||||
m, fc := setupModelWithConnectionFromWrapper(w)
|
||||
return m, fc, tmpDir, w
|
||||
return m, fc, fcfg, w
|
||||
}
|
||||
|
||||
func setupModelWithConnectionFromWrapper(w *config.Wrapper) (*Model, *fakeConnection) {
|
||||
func setupModelWithConnectionFromWrapper(w config.Wrapper) (*model, *fakeConnection) {
|
||||
m := setupModel(w)
|
||||
|
||||
fc := addFakeConn(m, device1)
|
||||
@@ -751,7 +718,7 @@ func setupModelWithConnectionFromWrapper(w *config.Wrapper) (*Model, *fakeConnec
|
||||
}
|
||||
|
||||
func createTmpDir() string {
|
||||
tmpDir, err := ioutil.TempDir("", "_request-")
|
||||
tmpDir, err := ioutil.TempDir("", "syncthing_testFolder-")
|
||||
if err != nil {
|
||||
panic("Failed to create temporary testing dir")
|
||||
}
|
||||
@@ -768,15 +735,14 @@ func equalContents(path string, contents []byte) error {
|
||||
}
|
||||
|
||||
func TestRequestRemoteRenameChanged(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
tfs := fcfg.Filesystem()
|
||||
tmpDir := tfs.URI()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(tmpDir)
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
tfs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir)
|
||||
|
||||
done := make(chan struct{})
|
||||
fc.mut.Lock()
|
||||
@@ -806,26 +772,49 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
|
||||
fc.sendIndexUpdate()
|
||||
select {
|
||||
case <-done:
|
||||
done = make(chan struct{})
|
||||
fc.mut.Lock()
|
||||
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
|
||||
select {
|
||||
case <-done:
|
||||
t.Fatalf("More than one index update sent")
|
||||
default:
|
||||
}
|
||||
close(done)
|
||||
}
|
||||
fc.mut.Unlock()
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("timed out")
|
||||
}
|
||||
|
||||
for _, n := range [2]string{a, b} {
|
||||
if err := equalContents(filepath.Join(tmpDir, n), data[n]); err != nil {
|
||||
t.Fatal(err)
|
||||
must(t, equalContents(filepath.Join(tmpDir, n), data[n]))
|
||||
}
|
||||
|
||||
var gotA, gotB, gotConfl bool
|
||||
done = make(chan struct{})
|
||||
fc.mut.Lock()
|
||||
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
|
||||
select {
|
||||
case <-done:
|
||||
t.Fatalf("Received more index updates than expected")
|
||||
default:
|
||||
}
|
||||
for _, f := range fs {
|
||||
switch {
|
||||
case f.Name == a:
|
||||
if gotA {
|
||||
t.Error("Got more than one index update for", f.Name)
|
||||
}
|
||||
gotA = true
|
||||
case f.Name == b:
|
||||
if gotB {
|
||||
t.Error("Got more than one index update for", f.Name)
|
||||
}
|
||||
gotB = true
|
||||
case strings.HasPrefix(f.Name, "b.sync-conflict-"):
|
||||
if gotConfl {
|
||||
t.Error("Got more than one index update for conflicts of", f.Name)
|
||||
}
|
||||
gotConfl = true
|
||||
default:
|
||||
t.Error("Got unexpected file in index update", f.Name)
|
||||
}
|
||||
}
|
||||
if gotA && gotB && gotConfl {
|
||||
close(done)
|
||||
}
|
||||
}
|
||||
fc.mut.Unlock()
|
||||
|
||||
fd, err := tfs.OpenFile(b, fs.OptReadWrite, 0644)
|
||||
if err != nil {
|
||||
@@ -840,11 +829,20 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
|
||||
// rename
|
||||
fc.deleteFile(a)
|
||||
fc.updateFile(b, 0644, protocol.FileInfoTypeFile, data[a])
|
||||
// Make sure the remote file for b is newer and thus stays global -> local conflict
|
||||
fc.mut.Lock()
|
||||
for i := range fc.files {
|
||||
if fc.files[i].Name == b {
|
||||
fc.files[i].ModifiedS += 100
|
||||
break
|
||||
}
|
||||
}
|
||||
fc.mut.Unlock()
|
||||
fc.sendIndexUpdate()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("timed out")
|
||||
t.Errorf("timed out without receiving all expected index updates")
|
||||
}
|
||||
|
||||
// Check outcome
|
||||
@@ -853,24 +851,30 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
|
||||
case path == a:
|
||||
t.Errorf(`File "a" was not removed`)
|
||||
case path == b:
|
||||
if err := equalContents(filepath.Join(tmpDir, b), otherData); err != nil {
|
||||
t.Errorf(`Modified file "b" was overwritten`)
|
||||
if err := equalContents(filepath.Join(tmpDir, b), data[a]); err != nil {
|
||||
t.Error(`File "b" has unexpected content (renamed from a on remote)`)
|
||||
}
|
||||
case strings.HasPrefix(path, b+".sync-conflict-"):
|
||||
if err := equalContents(filepath.Join(tmpDir, path), otherData); err != nil {
|
||||
t.Error(`Sync conflict of "b" has unexptected content`)
|
||||
}
|
||||
case path == "." || strings.HasPrefix(path, ".stfolder"):
|
||||
default:
|
||||
t.Error("Found unexpected file", path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestRequestRemoteRenameConflict(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
tfs := fcfg.Filesystem()
|
||||
tmpDir := tfs.URI()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(tmpDir)
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
tfs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir)
|
||||
|
||||
recv := make(chan int)
|
||||
fc.mut.Lock()
|
||||
@@ -900,9 +904,7 @@ func TestRequestRemoteRenameConflict(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, n := range [2]string{a, b} {
|
||||
if err := equalContents(filepath.Join(tmpDir, n), data[n]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
must(t, equalContents(filepath.Join(tmpDir, n), data[n]))
|
||||
}
|
||||
|
||||
fd, err := tfs.OpenFile(b, fs.OptReadWrite, 0644)
|
||||
@@ -960,15 +962,13 @@ func TestRequestRemoteRenameConflict(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRequestDeleteChanged(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
m, fc, tmpDir, w := setupModelWithConnection()
|
||||
m, fc, fcfg, w := setupModelWithConnection()
|
||||
tfs := fcfg.Filesystem()
|
||||
defer func() {
|
||||
m.Stop()
|
||||
testOs.RemoveAll(tmpDir)
|
||||
testOs.Remove(w.ConfigPath())
|
||||
os.RemoveAll(tfs.URI())
|
||||
os.Remove(w.ConfigPath())
|
||||
}()
|
||||
tfs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir)
|
||||
|
||||
done := make(chan struct{})
|
||||
fc.mut.Lock()
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
@@ -96,7 +97,7 @@ func (s *sharedPullerState) tempFile() (io.WriterAt, error) {
|
||||
// here.
|
||||
dir := filepath.Dir(s.tempName)
|
||||
if info, err := s.fs.Stat(dir); err != nil {
|
||||
s.failLocked("dst stat dir", err)
|
||||
s.failLocked(errors.Wrap(err, "ensuring parent dir is writeable"))
|
||||
return nil, err
|
||||
} else if info.Mode()&0200 == 0 {
|
||||
err := s.fs.Chmod(dir, 0755)
|
||||
@@ -139,13 +140,13 @@ func (s *sharedPullerState) tempFile() (io.WriterAt, error) {
|
||||
// what the umask dictates.
|
||||
|
||||
if err := s.fs.Chmod(s.tempName, mode); err != nil {
|
||||
s.failLocked("dst create chmod", err)
|
||||
s.failLocked(errors.Wrap(err, "setting perms on temp file"))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fd, err := s.fs.OpenFile(s.tempName, flags, mode)
|
||||
if err != nil {
|
||||
s.failLocked("dst create", err)
|
||||
s.failLocked(errors.Wrap(err, "opening temp file"))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -176,7 +177,7 @@ func (s *sharedPullerState) tempFile() (io.WriterAt, error) {
|
||||
l.Debugln("failed to remove temporary file:", remErr)
|
||||
}
|
||||
|
||||
s.failLocked("dst truncate", err)
|
||||
s.failLocked(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -190,19 +191,19 @@ func (s *sharedPullerState) tempFile() (io.WriterAt, error) {
|
||||
|
||||
// fail sets the error on the puller state compose of error, and marks the
|
||||
// sharedPullerState as failed. Is a no-op when called on an already failed state.
|
||||
func (s *sharedPullerState) fail(context string, err error) {
|
||||
func (s *sharedPullerState) fail(err error) {
|
||||
s.mut.Lock()
|
||||
defer s.mut.Unlock()
|
||||
|
||||
s.failLocked(context, err)
|
||||
s.failLocked(err)
|
||||
}
|
||||
|
||||
func (s *sharedPullerState) failLocked(context string, err error) {
|
||||
func (s *sharedPullerState) failLocked(err error) {
|
||||
if s.err != nil || err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.err = fmt.Errorf("%s: %s", context, err.Error())
|
||||
s.err = err
|
||||
}
|
||||
|
||||
func (s *sharedPullerState) failed() error {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
@@ -15,18 +16,17 @@ import (
|
||||
|
||||
// Test creating temporary file inside read-only directory
|
||||
func TestReadOnlyDir(t *testing.T) {
|
||||
testOs := &fatalOs{t}
|
||||
|
||||
// Create a read only directory, clean it up afterwards.
|
||||
testOs.Mkdir("testdata/read_only_dir", 0555)
|
||||
defer func() {
|
||||
testOs.Chmod("testdata/read_only_dir", 0755)
|
||||
testOs.RemoveAll("testdata/read_only_dir")
|
||||
}()
|
||||
tmpDir := createTmpDir()
|
||||
defer os.RemoveAll(tmpDir)
|
||||
if err := os.Chmod(tmpDir, 0555); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chmod(tmpDir, 0755)
|
||||
|
||||
s := sharedPullerState{
|
||||
fs: fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata"),
|
||||
tempName: "read_only_dir/.temp_name",
|
||||
fs: fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir),
|
||||
tempName: ".temp_name",
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
|
||||
@@ -38,6 +38,6 @@ func TestReadOnlyDir(t *testing.T) {
|
||||
t.Fatal("Unexpected nil fd")
|
||||
}
|
||||
|
||||
s.fail("Test done", nil)
|
||||
s.fail(nil)
|
||||
s.finalClose()
|
||||
}
|
||||
|
||||
@@ -21,40 +21,38 @@ type fatalOs struct {
|
||||
fatal
|
||||
}
|
||||
|
||||
func (f *fatalOs) must(fn func() error) {
|
||||
func must(f fatal, err error) {
|
||||
f.Helper()
|
||||
if err := fn(); err != nil {
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fatalOs) Chmod(name string, mode os.FileMode) {
|
||||
f.Helper()
|
||||
f.must(func() error { return os.Chmod(name, mode) })
|
||||
must(f, os.Chmod(name, mode))
|
||||
}
|
||||
|
||||
func (f *fatalOs) Chtimes(name string, atime time.Time, mtime time.Time) {
|
||||
f.Helper()
|
||||
f.must(func() error { return os.Chtimes(name, atime, mtime) })
|
||||
must(f, os.Chtimes(name, atime, mtime))
|
||||
}
|
||||
|
||||
func (f *fatalOs) Create(name string) *os.File {
|
||||
f.Helper()
|
||||
file, err := os.Create(name)
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
must(f, err)
|
||||
return file
|
||||
}
|
||||
|
||||
func (f *fatalOs) Mkdir(name string, perm os.FileMode) {
|
||||
f.Helper()
|
||||
f.must(func() error { return os.Mkdir(name, perm) })
|
||||
must(f, os.Mkdir(name, perm))
|
||||
}
|
||||
|
||||
func (f *fatalOs) MkdirAll(name string, perm os.FileMode) {
|
||||
f.Helper()
|
||||
f.must(func() error { return os.MkdirAll(name, perm) })
|
||||
must(f, os.MkdirAll(name, perm))
|
||||
}
|
||||
|
||||
func (f *fatalOs) Remove(name string) {
|
||||
@@ -73,14 +71,12 @@ func (f *fatalOs) RemoveAll(name string) {
|
||||
|
||||
func (f *fatalOs) Rename(oldname, newname string) {
|
||||
f.Helper()
|
||||
f.must(func() error { return os.Rename(oldname, newname) })
|
||||
must(f, os.Rename(oldname, newname))
|
||||
}
|
||||
|
||||
func (f *fatalOs) Stat(name string) os.FileInfo {
|
||||
f.Helper()
|
||||
info, err := os.Stat(name)
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
must(f, err)
|
||||
return info
|
||||
}
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ import (
|
||||
// setup/renewal of a port mapping.
|
||||
type Service struct {
|
||||
id protocol.DeviceID
|
||||
cfg *config.Wrapper
|
||||
cfg config.Wrapper
|
||||
stop chan struct{}
|
||||
|
||||
mappings []*Mapping
|
||||
@@ -31,7 +31,7 @@ type Service struct {
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
func NewService(id protocol.DeviceID, cfg *config.Wrapper) *Service {
|
||||
func NewService(id protocol.DeviceID, cfg config.Wrapper) *Service {
|
||||
return &Service{
|
||||
id: id,
|
||||
cfg: cfg,
|
||||
|
||||
+4
-14
@@ -304,7 +304,7 @@ func (w *walker) handleItem(ctx context.Context, path string, toHashChan chan<-
|
||||
|
||||
switch {
|
||||
case info.IsSymlink():
|
||||
if err := w.walkSymlink(ctx, path, finishedChan); err != nil {
|
||||
if err := w.walkSymlink(ctx, path, info, finishedChan); err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
@@ -411,19 +411,14 @@ func (w *walker) walkDir(ctx context.Context, relPath string, info fs.FileInfo,
|
||||
|
||||
// walkSymlink returns nil or an error, if the error is of the nature that
|
||||
// it should stop the entire walk.
|
||||
func (w *walker) walkSymlink(ctx context.Context, relPath string, finishedChan chan<- ScanResult) error {
|
||||
func (w *walker) walkSymlink(ctx context.Context, relPath string, info fs.FileInfo, finishedChan chan<- ScanResult) error {
|
||||
// Symlinks are not supported on Windows. We ignore instead of returning
|
||||
// an error.
|
||||
if runtime.GOOS == "windows" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We always rehash symlinks as they have no modtime or
|
||||
// permissions. We check if they point to the old target by
|
||||
// checking that their existing blocks match with the blocks in
|
||||
// the index.
|
||||
|
||||
target, err := w.Filesystem.ReadSymlink(relPath)
|
||||
f, err := CreateFileInfo(info, relPath, w.Filesystem)
|
||||
if err != nil {
|
||||
w.handleError(ctx, "reading link:", relPath, err, finishedChan)
|
||||
return nil
|
||||
@@ -431,12 +426,6 @@ func (w *walker) walkSymlink(ctx context.Context, relPath string, finishedChan c
|
||||
|
||||
curFile, hasCurFile := w.CurrentFiler.CurrentFile(relPath)
|
||||
|
||||
f := protocol.FileInfo{
|
||||
Name: relPath,
|
||||
Type: protocol.FileInfoTypeSymlink,
|
||||
NoPermissions: true, // Symlinks don't have permissions of their own
|
||||
SymlinkTarget: target,
|
||||
}
|
||||
f = w.updateFileInfo(f, curFile)
|
||||
|
||||
if hasCurFile {
|
||||
@@ -613,6 +602,7 @@ func CreateFileInfo(fi fs.FileInfo, name string, filesystem fs.Filesystem) (prot
|
||||
return protocol.FileInfo{}, err
|
||||
}
|
||||
f.SymlinkTarget = target
|
||||
f.NoPermissions = true // Symlinks don't have permissions of their own
|
||||
return f, nil
|
||||
}
|
||||
f.Permissions = uint32(fi.Mode() & fs.ModePerm)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/build"
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
)
|
||||
|
||||
@@ -69,6 +70,11 @@ var (
|
||||
func init() {
|
||||
// Creates the list of ciper suites that SecureDefault uses.
|
||||
cipherSuites = buildCipherSuites()
|
||||
if build.IsBeta {
|
||||
// Append "tls13=1" to GODEBUG before starting TLS, to enable TLS
|
||||
// 1.3 in Go 1.12.
|
||||
os.Setenv("GODEBUG", os.Getenv("GODEBUG")+",tls13=1")
|
||||
}
|
||||
}
|
||||
|
||||
// SecureDefault returns a tls.Config with reasonable, secure defaults set.
|
||||
@@ -84,7 +90,7 @@ func SecureDefault() *tls.Config {
|
||||
// secure (so the web tells me, don't ask me to explain the
|
||||
// details).
|
||||
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
||||
// The cipher suite lists built above.
|
||||
// The cipher suite lists built above. These are ignored in TLS 1.3.
|
||||
CipherSuites: cs,
|
||||
// We've put some thought into this choice and would like it to
|
||||
// matter.
|
||||
|
||||
@@ -125,14 +125,14 @@ func newAggregator(folderCfg config.FolderConfiguration, ctx context.Context) *a
|
||||
return a
|
||||
}
|
||||
|
||||
func Aggregate(in <-chan fs.Event, out chan<- []string, folderCfg config.FolderConfiguration, cfg *config.Wrapper, ctx context.Context) {
|
||||
func Aggregate(in <-chan fs.Event, out chan<- []string, folderCfg config.FolderConfiguration, cfg config.Wrapper, ctx context.Context) {
|
||||
a := newAggregator(folderCfg, ctx)
|
||||
|
||||
// Necessary for unit tests where the backend is mocked
|
||||
go a.mainLoop(in, out, cfg)
|
||||
}
|
||||
|
||||
func (a *aggregator) mainLoop(in <-chan fs.Event, out chan<- []string, cfg *config.Wrapper) {
|
||||
func (a *aggregator) mainLoop(in <-chan fs.Event, out chan<- []string, cfg config.Wrapper) {
|
||||
a.notifyTimer = time.NewTimer(a.notifyDelay)
|
||||
defer a.notifyTimer.Stop()
|
||||
|
||||
|
||||
+19
-19
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "STDISCOSRV" "1" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "STDISCOSRV" "1" "Mar 05, 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’s web GUI. Go to settings,
|
||||
Global Discovery Server and add stdiscosrv’s host address to the comma\-separated
|
||||
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
|
||||
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’t yet, head over to getting\-started first.
|
||||
haven\(aqt 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’t necessary with the \fBhttp\fP flag:
|
||||
the certificate and key at startup. This isn\(aqt 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’s
|
||||
The "X\-Forwarded\-For" http header must be passed through with the client\(aqs
|
||||
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,13 +371,13 @@ server {
|
||||
.UNINDENT
|
||||
.sp
|
||||
An example of automating the SSL certificates and reverse\-proxying the Discovery
|
||||
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>\&.
|
||||
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>\&.
|
||||
.SH SEE ALSO
|
||||
.sp
|
||||
\fBsyncthing\-networking(7)\fP, \fBsyncthing\-faq(7)\fP
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
+9
-9
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "STRELAYSRV" "1" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "STRELAYSRV" "1" "Mar 05, 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. ‘tcp’ for IPv4 and IPv6, ‘tcp4’ for IPv4, ‘tcp6’ for IPv6 (default “tcp”).
|
||||
Protocol used for listening. \(aqtcp\(aq for IPv4 and IPv6, \(aqtcp4\(aq for IPv4, \(aqtcp6\(aq 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’s device ID is output on start\-up.
|
||||
The relay\(aqs 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’re using \fBufw\fP, add the following to \fB/etc/ufw/before.rules\fP:
|
||||
Or, if you\(aqre using \fBufw\fP, add the following to \fB/etc/ufw/before.rules\fP:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
@@ -271,6 +271,6 @@ Please consult Linux distribution documentation to persist firewall rules.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
+18
-18
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-BEP" "7" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "Mar 05, 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’s place or
|
||||
way to represent the folder, perhaps by using the ID in it\(aqs 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’s
|
||||
character ("/") as path separator, regardless of the implementation\(aqs
|
||||
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’s folder.
|
||||
corresponding to a part of a certain file in the peer\(aqs 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’s temporary form)
|
||||
performed from the temporary file (converting name to it\(aqs 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’s intended that the limit may be further
|
||||
containing a large block list. It\(aqs 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}
|
||||
@@ -1161,6 +1161,6 @@ _
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
+16
-16
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "Mar 05, 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’s RSA public and private key. These form the basis for the
|
||||
The device\(aqs 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’ve actually
|
||||
advanced users only; do not change unless requested to or you\(aqve 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’t
|
||||
change these unless you know what you’re doing.
|
||||
Tweak for rate limiting the puller when it retries pulling files. Don\(aqt
|
||||
change these unless you know what you\(aqre 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’t change these unless you know
|
||||
what you’re doing.
|
||||
Tweak for rate limiting the puller. Don\(aqt change these unless you know
|
||||
what you\(aqre 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,13 +967,13 @@ 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’d like to sync your home folder in non\-send only mode, you may add the
|
||||
If you\(aqd 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’d also like to backup your configuration files, add another folder in
|
||||
If you\(aqd also like to backup your configuration files, add another folder in
|
||||
send only mode for just the configuration folder.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
+24
-24
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "Mar 05, 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’t actually add any security or
|
||||
(\fBcert.pem\fP). The self signing part doesn\(aqt 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’s how they are used in Syncthing. When
|
||||
Now we know what device IDs are, here\(aqs 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’ve seen one of these broadcasts for a given
|
||||
device ID that’s where we try to connect.
|
||||
port number). If we\(aqve seen one of these broadcasts for a given
|
||||
device ID that\(aqs 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’t have a static address and haven’t seen
|
||||
global server). If we don\(aqt have a static address and haven\(aqt 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’s an infinite
|
||||
You can argue that of course there are collisions \- there\(aqs 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’m going to quote \fI\%stack
|
||||
I\(aqm 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’s 45 orders of magnitude
|
||||
in the next second to about 10^\-15. That\(aqs 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’s also worth noting that the property of SHA\-256 that we are using is not
|
||||
It\(aqs 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’t help you
|
||||
you can find two messages that result in a hash collision that doesn\(aqt 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’t get in.
|
||||
to exactly the hash that my certificate already has or you won\(aqt get in.
|
||||
.sp
|
||||
Note also that it’s not good enough to find a random blob of bits that happen to
|
||||
Note also that it\(aqs 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’t protected by crypto. This
|
||||
Currently, the local discovery mechanism isn\(aqt 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’s a mouthful to read over the phone, annoying to type into an SMS or even
|
||||
It\(aqs 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’t a vulnerability as such, but a user experience problem. There are
|
||||
This isn\(aqt 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
|
||||
@@ -261,6 +261,6 @@ accepting the connection.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-EVENT-API" "7" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "Mar 05, 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
|
||||
@@ -876,6 +876,6 @@ seconds and is now in state \fBidle\fP\&.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
+46
-49
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-FAQ" "7" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "Mar 05, 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’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
|
||||
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
|
||||
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
|
||||
@@ -150,8 +150,9 @@ compressed) decompressed.
|
||||
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 60 seconds to detect
|
||||
file changes. This means checking every file’s modification time and
|
||||
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
|
||||
comparing it to the database. This can cause spikes of CPU usage for large
|
||||
folders.
|
||||
.UNINDENT
|
||||
@@ -168,20 +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’s CPU power.
|
||||
.sp
|
||||
To reduce CPU spikes from scanning activity, use a filesystem notifications
|
||||
plugin. This is delivered by default via Synctrayzor, Syncthing\-GTK and on
|
||||
Android. For other setups, consider using \fI\%syncthing\-inotify\fP <\fBhttps://github.com/syncthing/syncthing-inotify\fP>\&.
|
||||
system\(aqs CPU power.
|
||||
.SH SHOULD I KEEP MY DEVICE IDS SECRET?
|
||||
.sp
|
||||
No. The IDs are not sensitive. Given a device ID it’s possible to find the IP
|
||||
No. The IDs are not sensitive. Given a device ID it\(aqs possible to find the IP
|
||||
address for that device, if global discovery is enabled on it. Knowing the device
|
||||
ID doesn’t help you actually establish a connection to that device or get a list
|
||||
ID doesn\(aqt 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’s
|
||||
device ID. It’s 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\(aqs
|
||||
device ID. It\(aqs 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!)
|
||||
@@ -196,7 +193,7 @@ device\-ids
|
||||
.sp
|
||||
Syncthing does recognize conflicts. When a file has been modified on two devices
|
||||
simultaneously and the content actually differs, one of the files will be
|
||||
renamed to \fB<filename>.sync\-conflict\-<date>\-<time>.<ext>\fP\&. The file with the
|
||||
renamed to \fB<filename>.sync\-conflict\-<date>\-<time>\-<modifiedBy>.<ext>\fP\&. The file with the
|
||||
older modification time will be marked as the conflicting file and thus be
|
||||
renamed. If the modification times are equal, the file originating from the
|
||||
device which has the larger value of the first 63 bits for his device ID will be
|
||||
@@ -205,19 +202,17 @@ If the conflict is between a modification and a deletion of the file, the
|
||||
modified file always wins and is resurrected without renaming on the
|
||||
device where it was deleted.
|
||||
.sp
|
||||
Beware that the \fB<filename>.sync\-conflict\-<date>\-<time>.<ext>\fP files are
|
||||
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’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. Moreover, if there’s something that automatically
|
||||
causes a conflict on change you’ll end up with \fBsync\-conflict\-...sync\-conflict
|
||||
\-...\-sync\-conflict\fP files.
|
||||
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"
|
||||
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’t be
|
||||
created by Syncthing when the folder is added. If this folder can\(aqt 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.
|
||||
@@ -226,13 +221,15 @@ you know will always exist in the folder.
|
||||
See the previous question.
|
||||
.SH AM I ABLE TO NEST SHARED FOLDERS IN SYNCTHING?
|
||||
.sp
|
||||
Do not share a folder which is inside another shared folder. This behaviour
|
||||
is in no way supported, recommended or coded for in any way, and comes with
|
||||
many pitfalls.
|
||||
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.
|
||||
.SH HOW DO I RENAME/MOVE A SYNCED FOLDER?
|
||||
.sp
|
||||
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
|
||||
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
|
||||
something goes wrong during the move and is synchronized to your other
|
||||
devices.
|
||||
.sp
|
||||
@@ -240,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’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
|
||||
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
|
||||
move. Changes made on other devices may be overwritten, or changes made
|
||||
locally may be overwritten by those on other devices.
|
||||
.sp
|
||||
@@ -255,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’s method would be wasteful. There are better
|
||||
doing so using Syncthing\(aqs 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
|
||||
@@ -292,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
|
||||
@@ -301,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’t do much with a stolen device ID, because
|
||||
connect two devices. An attacker can\(aqt 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
|
||||
@@ -357,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’t want the terminal
|
||||
If you only want to access the remote gui and don\(aqt want the terminal
|
||||
session, use this example,
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
@@ -382,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.
|
||||
@@ -407,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’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?
|
||||
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?
|
||||
.sp
|
||||
You can change the theme in the settings. Syncthing ships with other themes
|
||||
than the default.
|
||||
@@ -419,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.
|
||||
@@ -436,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’s home directory (run \fBsyncthing
|
||||
creates \fBsyncthing.log\fP in Syncthing\(aqs 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?
|
||||
@@ -463,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’s apt\-get, you should upgrade
|
||||
If you use a package manager such as Debian\(aqs 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
|
||||
@@ -491,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’re using systemd, runit, or upstart, we already ship examples, check
|
||||
If you\(aqre 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’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
|
||||
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
|
||||
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’t have
|
||||
will almost certainly cover 100% of what you want to do. If you don\(aqt 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.
|
||||
@@ -558,6 +555,6 @@ sudo sh \-c \(aqecho 204800 > /proc/sys/fs/inotify/max_user_watches\(aq
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
+11
-11
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "Mar 05, 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’s OK for the “addresses” field to be either the empty list (\fB[]\fP),
|
||||
It\(aqs 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’t conform to the expected format, \fB400\fP (Bad Request) is
|
||||
posted data doesn\(aqt 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’s 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\(aqs 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
|
||||
@@ -121,6 +121,6 @@ The discovery server prints its certificate ID in this manner on startup.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v4
|
||||
.
|
||||
@@ -118,6 +118,6 @@ between two announces and conclude that the announcing device has restarted.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-NETWORKING" "7" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "Mar 05, 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’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:
|
||||
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:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
@@ -142,6 +142,6 @@ Syncthing can use a SOCKS5 proxy for outbound connections. Please see proxying\&
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-RELAY" "7" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-RELAY" "7" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-relay \- Relay Protocol v1
|
||||
.
|
||||
@@ -37,7 +37,7 @@ connect to each other directly otherwise. This is usually due to both devices
|
||||
being behind a NAT and neither side being able to open a port which would
|
||||
be directly accessible from the internet.
|
||||
.sp
|
||||
A relay was designed to relay BEP protocol, hence the reliance on device ID’s
|
||||
A relay was designed to relay BEP protocol, hence the reliance on device ID\(aqs
|
||||
in the protocol spec, but at the same time it is general enough that could be
|
||||
reused by other protocols or applications, as the data transferred between two
|
||||
devices which use a relay is completely obscure and does not affect the
|
||||
@@ -92,14 +92,14 @@ exists.
|
||||
After the client has joined, no more messages are exchanged apart from
|
||||
Ping/Pong messages for general connection keep alive checking.
|
||||
.sp
|
||||
From this point onwards, the client stand\-by’s and waits for SessionInvitation
|
||||
From this point onwards, the client stand\-by\(aqs and waits for SessionInvitation
|
||||
messages from the relay, which implies that some other device is trying to
|
||||
connect with you. SessionInvitation message contains the unique session key
|
||||
which then can be used to establish a connection in session mode.
|
||||
.sp
|
||||
If the client fails to send a JoinRelayRequest message within the first ping
|
||||
interval, the connection is terminated.
|
||||
If the client fails to send a message (even if it’s a ping message) every minute
|
||||
If the client fails to send a message (even if it\(aqs a ping message) every minute
|
||||
(by default), the connection is terminated.
|
||||
.SS Temporary protocol submode
|
||||
.sp
|
||||
@@ -574,7 +574,7 @@ identify which session you are trying to connect to.
|
||||
.B : Address
|
||||
An optional IP address on which the relay server is expecting you to
|
||||
connect, in order to start a connection in session mode.
|
||||
Empty/all zero IP should be replaced with the relay’s public IP address that
|
||||
Empty/all zero IP should be replaced with the relay\(aqs public IP address that
|
||||
was used when establishing the protocol mode connection.
|
||||
.TP
|
||||
.B : Port
|
||||
@@ -585,14 +585,14 @@ in order to start a connection in session mode.
|
||||
Because both sides connecting to the relay use the client side of the socket,
|
||||
and some protocols behave differently depending if the connection starts on
|
||||
the server side or the client side, this boolean indicates which side of the
|
||||
connection this client should assume it’s getting. The value is inverted in
|
||||
connection this client should assume it\(aqs getting. The value is inverted in
|
||||
the invitation which is sent to the other device, so that there is always
|
||||
one client socket, and one server socket.
|
||||
.UNINDENT
|
||||
.SH HOW SYNCTHING USES RELAYS, AND GENERAL SECURITY
|
||||
.sp
|
||||
In the case of Syncthing and BEP, when two devices connect via relay, they
|
||||
start their standard TLS connection encapsulated within the relay’s plain\-text
|
||||
start their standard TLS connection encapsulated within the relay\(aqs plain\-text
|
||||
session connection, effectively upgrading the plain\-text connection to a TLS
|
||||
connection.
|
||||
.sp
|
||||
@@ -694,6 +694,6 @@ _
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-REST-API" "7" "Jan 26, 2019" "v1" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "Mar 05, 2019" "v1" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.
|
||||
@@ -47,7 +47,7 @@ with \fBcurl\fP\&.
|
||||
.SS GET /rest/system/browse
|
||||
.sp
|
||||
Returns a list of directories matching the path given by the optional parameter
|
||||
\fBcurrent\fP\&. The path can use \fI\%patterns as described in Go’s filepath package\fP <\fBhttps://golang.org/pkg/path/filepath/#Match\fP>\&. A ‘*’ will always be appended
|
||||
\fBcurrent\fP\&. The path can use \fI\%patterns as described in Go\(aqs filepath package\fP <\fBhttps://golang.org/pkg/path/filepath/#Match\fP>\&. A \(aq*\(aq will always be appended
|
||||
to the given path (e.g. \fB/tmp/\fP matches all its subdirectories). If the option
|
||||
\fBcurrent\fP is not given, filesystem root paths are returned.
|
||||
.INDENT 0.0
|
||||
@@ -904,8 +904,8 @@ curl \-X POST \-H X\-API\-Key:... http://127.0.0.1:8384/rest/db/revert?folder=de
|
||||
Request immediate scan. Takes the optional parameters \fBfolder\fP (folder ID),
|
||||
\fBsub\fP (path relative to the folder root) and \fBnext\fP (time in seconds). If
|
||||
\fBfolder\fP is omitted or empty all folders are scanned. If \fBsub\fP is given,
|
||||
only this path (and children, in case it’s a directory) is scanned. The \fBnext\fP
|
||||
argument delays Syncthing’s automated rescan interval for a given amount of
|
||||
only this path (and children, in case it\(aqs a directory) is scanned. The \fBnext\fP
|
||||
argument delays Syncthing\(aqs automated rescan interval for a given amount of
|
||||
seconds.
|
||||
.sp
|
||||
Requesting scan of a path that no longer exists, but previously did, is
|
||||
@@ -980,7 +980,7 @@ Data in the cluster latest version.
|
||||
Data that is locally the same as the cluster latest version.
|
||||
.TP
|
||||
.B local*:
|
||||
Data that is locally present, regardless of whether it’s the same or different version as the cluster latest version.
|
||||
Data that is locally present, regardless of whether it\(aqs the same or different version as the cluster latest version.
|
||||
.TP
|
||||
.B need*:
|
||||
Data that is needed to become up to date with the cluster latest version (i.e., data that is out of sync).
|
||||
@@ -1023,7 +1023,7 @@ only the desired event types, add a parameter
|
||||
\fBevents=EventTypeA,EventTypeB,...\fP where the event types are any of the event\-types\&.
|
||||
.sp
|
||||
The optional parameter \fBsince=<lastSeenID>\fP sets the ID of the last event
|
||||
you’ve already seen. Syncthing returns a JSON encoded array of event objects,
|
||||
you\(aqve already seen. Syncthing returns a JSON encoded array of event objects,
|
||||
starting at the event just after the one with this last seen ID. The default
|
||||
value is 0, which returns all events. There is a limit to the number of events
|
||||
buffered, so if the rate of events is high or the time between polling calls is
|
||||
@@ -1202,6 +1202,6 @@ Returns the data sent in the anonymous usage report.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2014-2018, The Syncthing Authors
|
||||
2014-2019, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user