Compare commits

...

48 Commits

Author SHA1 Message Date
Jakob Borg
3943873626 lib/db, lib/model: Avoid accounting mishap on folder restart (fixes #6938) (#6939)
We created a new fileset before stopping the folder during restart. When
we create that fileset it loads the current metadata and sequence
numbers from the database. But the folder may have time to update those
before stopping, leaving the new fileset with bad data.

This would cause wrong accounting (forgotten files) and potentially
sequence reuse causing files not sent to other devices.

This change reuses the fileset on restart, skipping the issue entirely.
It also moves the creation of the fileset back under the lock so there
should be no chance of concurrency issues here.
2020-08-28 07:48:25 +02:00
Jakob Borg
15ce96f704 lib/model: Save config after auto accepting folders (fixes #6922) (#6937) 2020-08-28 07:48:25 +02:00
Simon Frei
675b535ead build: Bump goleveldb to latest commit (#6895) 2020-08-21 12:35:05 +02:00
Simon Frei
a65b46debd lib/model: Don't mark globally deleted items as recv-only (fixes #6910) (#6911) 2020-08-21 12:34:22 +02:00
Simon Frei
b628460930 lib/model: Prevent localflag flipflopping on del. item (ref #6865) (#6919) 2020-08-21 10:09:48 +02:00
Simon Frei
cc1f6e4d4a lib/db, lib/model: Cover exec-paths with debug logging (#6918) 2020-08-20 16:11:20 +02:00
Jakob Borg
f79e980fdf build: Update QUIC and gopsutil (fixes #6889) (#6915) 2020-08-20 11:57:20 +02:00
Simon Frei
88599bc154 lib/model: Handle del. dir with locally changed items on pull (fixes #6873) (#6914) 2020-08-20 10:56:29 +02:00
Simon Frei
fc2c46e82f lib/config, lib/fs: Make junction behaviour configurable (ref #6606) (#6907) 2020-08-19 19:58:51 +02:00
Simon Frei
ce4d149bf5 lib/nat: Don't hang on draining timer chan (fixes #6908) (#6912) 2020-08-19 15:58:44 +01:00
Audrius Butkevicius
cbbc262161 lib/db: Dont recurse on flush (fixes #6905) (#6906)
Or else we crash.

Co-authored-by: Jakob Borg <jakob@kastelo.net>
Co-authored-by: Simon Frei <freisim93@gmail.com>
2020-08-19 13:13:44 +02:00
Jakob Borg
96e35aa7f5 build: Add -trimpath compiler option (#6904)
Quoting the manual:

    -trimpath
      remove all file system paths from the resulting executable.
      Instead of absolute file system paths, the recorded file names
      will begin with either "go" (for the standard library),
      or a module path@version (when using modules),
      or a plain import path (when using GOPATH).

That is, when we panic, instead of:

    goroutine 1 [running]:
    main.main()
        /Users/jb/dev/syncthing/syncthing/cmd/syncthing/main.go:272 +0x116

we get:

    goroutine 1 [running]:
    main.main()
        github.com/syncthing/syncthing@/cmd/syncthing/main.go:272 +0x116

(Module path and file path within module.)
2020-08-19 09:40:21 +02:00
Jakob Borg
086d1f8f6a build: We now target Go 1.14 2020-08-19 08:49:42 +02:00
Audrius Butkevicius
758a1a6a37 lib/fs: Disable ioctl on ppc (fixes #6898) (#6901) 2020-08-19 07:56:35 +02:00
Jakob Borg
b1bfa9aece gui, man, authors: Update docs, translations, and contributors 2020-08-19 07:45:23 +02:00
Audrius Butkevicius
d0c6c18b4f lib/dialer: Try dialing without reuse in parallel (fixes #6892) (#6893) 2020-08-18 13:11:12 +02:00
Jakob Borg
2286a6ebef cmd/stcrashreceiver: Don't crash on nil err 2020-08-18 12:54:20 +02:00
Audrius Butkevicius
bf9ff17267 all: Remove need to restart syncthing (#6883) 2020-08-18 09:26:33 +02:00
Simon Frei
8f5215878b lib/db: Don't put truncated files (ref #6855, ref #6501) (#6888) 2020-08-18 09:20:12 +02:00
Simon Frei
9c2117f08e lib/osutil: Check returned error instead of info (ref #6885) (#6887) 2020-08-12 11:33:56 +02:00
Jakob Borg
fa9bc08afb gui, man, authors: Update docs, translations, and contributors 2020-08-12 07:45:23 +02:00
Simon Frei
fd8bea6179 lib/osutil: Preserve perms in AtomicWriter (fixes #tbd) (#6885) 2020-08-10 18:48:37 +02:00
Jakob Borg
f2f1a28206 Merge branch 'release' into main
* release:
  lib/fs: Fix WatchRename test for FreeBSD (fixes #6613)
2020-08-07 09:10:50 +02:00
Jakob Borg
872fbf6e32 Merge branch 'release' into main
* release:
  lib/fs: Unwrap mtimeFile, get fd the "correct" way (ref #6875) (#6877)
  lib/model: Don't close file early (fixes #6875) (#6876)
2020-08-07 07:50:38 +02:00
Audrius Butkevicius
e9bb17307d lib/fs: Unwrap mtimeFile, get fd the "correct" way (ref #6875) (#6877) 2020-08-07 07:47:48 +02:00
Jakob Borg
ff84f075d5 gui, man, authors: Update docs, translations, and contributors 2020-08-05 07:45:24 +02:00
Jakob Borg
5e1f39b6f6 lib/fs: Fix WatchRename test for FreeBSD (fixes #6613) 2020-08-03 23:24:01 +02:00
Audrius Butkevicius
96e197e502 lib/model: Don't close file early (fixes #6875) (#6876) 2020-08-03 21:54:42 +02:00
Simon Frei
bbf25e2d02 lib/db: Log context on panic (#6872) 2020-08-01 17:32:36 +02:00
Simon Frei
5c9df60699 gui: Don't show pull order on SO folders (ref #6807) (#6871) 2020-08-01 11:19:54 +02:00
Simon Frei
a85bc1c3a6 lib/model: Check folder error before sync-waiting (fixes #6793) (#6847) 2020-07-31 19:26:09 +02:00
Simon Frei
850dd4cd25 lib/db: Don't count invalid items to global state (ref #6850) (#6863) 2020-07-30 13:49:14 +02:00
Simon Frei
e46c8ab9ee lib/model: Detect deleted RO items scanning on non-RO (fixes #6864) (#6865) 2020-07-30 13:41:45 +02:00
Jakob Borg
2dc2aa5d21 lib/connections, lib/tlsutil: Handle certName in Go 1.15 (fixes #6867) (#6868)
Our authentication is based on device ID (certificate fingerprint) but
we also check the certificate name for ... historical extra security
reasons. (I don't think this adds anything but it is what it is.) Since
that check breaks in Go 1.15 this change does two things:

- Adds a manual check for the peer certificate CommonName, and if they
  are equal we are happy and don't call the more advanced
  VerifyHostname() function. This allows our old style certificates to
  still pass the check.

- Adds the cert name "syncthing" as a DNS SAN when generating the
  certificate. This is the correct way nowadays and makes VerifyHostname()
  happy in Go 1.15 as well, even without the above patch.
2020-07-30 13:36:11 +02:00
Jakob Borg
d53a2567a4 build: Actually set build tags (#6866)
Apparently our Tags field depended on having specific files react to
tags and add themselves there. This, instead, works for all tags.

Also, pass tags to the test command line.
2020-07-30 10:58:43 +02:00
Jakob Borg
6f4671ed27 lib/connections: Add noquic tag
The QUIC package is notorious for being incompatible with either too
old or too new Go releases. Currently it doesn't build with Go 1.15 RC
and I want to test the rest with Go 1.15. With this I can do `go run
build.go --tags noquic` to do that.
2020-07-30 09:42:02 +02:00
Jakob Borg
b70dbfa0f7 gui, man, authors: Update docs, translations, and contributors 2020-07-29 07:45:25 +02:00
Simon Frei
424d1b1608 lib/db: Commit meta when dropping device (#6862) 2020-07-28 16:46:42 +02:00
Simon Frei
1b9e5c0937 lib/db: Include blocks in db check (ref #6855) (#6861) 2020-07-28 16:25:07 +02:00
Jakob Borg
26b188dc0e Merge branch 'release' into main
* release:
  lib/config: Repair versioning XML serialization (fixes #6859, ref #6693)
2020-07-28 12:09:48 +02:00
Jakob Borg
62cff26edf lib/config: Repair versioning XML serialization (fixes #6859, ref #6693) 2020-07-28 12:08:37 +02:00
Simon Frei
932d8c69de lib/fs: Properly handle case insensitive systems (fixes #1787, fixes #2739, fixes #5708)
With this change we emulate a case sensitive filesystem on top of
insensitive filesystems. This means we correctly pick up case-only renames
and throw a case conflict error when there would be multiple files differing
only in case.

This safety check has a small performance hit (about 20% more filesystem
operations when scanning for changes). The new advanced folder option
`caseSensitiveFS` can be used to disable the safety checks, retaining the
previous behavior on systems known to be fully case sensitive.

Co-authored-by: Jakob Borg <jakob@kastelo.net>
2020-07-28 11:15:11 +02:00
Simon Frei
21dd9d6b43 build: Replace hypen in -dirty for version (ref #6758) (#6858) 2020-07-28 10:58:01 +02:00
Simon Frei
08e0f938a9 lib/db: Update global meta even if unchanged (fixes #6850) (#6852) 2020-07-24 12:36:16 +02:00
Simon Frei
ebead944b5 lib/fs: Pass infinite recursion error on instead of warning (#6846)
Prompted by https://forum.syncthing.net/t/infinite-filesystem-recursion-detected/15285. In my opinion the filesystem shouldn't throw warnings but pass on errors for the caller to decide what's to be happening with it. Right now in this PR an infinite recursion is a normal scan error, i.e. folder is in failed state and displays failed items, but no warning. I think that's appropriate but if deemed appropriate an additional warning can be thrown in the scanner.
2020-07-22 22:10:24 +02:00
Jakob Borg
d91d77a2b2 gui, man, authors: Update docs, translations, and contributors 2020-07-22 07:45:19 +02:00
Audrius Butkevicius
55147f5901 lib/db: Rework flush hooks (#6838) 2020-07-19 08:55:27 +02:00
Alex Lindeman
851ee51c1b gui: Sharpen device icons (fixes #5579) (#6837)
Add [shape-rendering: crispEdges](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/shape-rendering) to `.identicon` class to remove blurry grid lines within device icons
2020-07-16 19:49:48 +02:00
128 changed files with 2816 additions and 935 deletions

View File

@@ -19,6 +19,7 @@ Adel Qalieh (adelq) <aqalieh95@gmail.com> <adelq@users.noreply.github.com>
Alan Pope <alan@popey.com>
Alberto Donato <albertodonato@users.noreply.github.com>
Alessandro G. (alessandro.g89) <alessandro.g89@gmail.com>
Alex Lindeman <139387+aelindeman@users.noreply.github.com>
Alex Xu <alex.hello71@gmail.com>
Alexander Graf (alex2108) <register-github@alex-graf.de>
Alexandre Viau (aviau) <alexandre@alexandreviau.net> <aviau@debian.org>

View File

@@ -297,10 +297,10 @@ func runCommand(cmd string, target target) {
build(target, tags)
case "test":
test("github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
test(strings.Fields(extraTags), "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
case "bench":
bench("github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
bench(strings.Fields(extraTags), "github.com/syncthing/syncthing/lib/...", "github.com/syncthing/syncthing/cmd/...")
case "integration":
integration(false)
@@ -379,10 +379,11 @@ func parseFlags() {
flag.Parse()
}
func test(pkgs ...string) {
func test(tags []string, pkgs ...string) {
lazyRebuildAssets()
args := []string{"test", "-short", "-timeout", timeout, "-tags", "purego"}
tags = append(tags, "purego")
args := []string{"test", "-short", "-timeout", timeout, "-tags", strings.Join(tags, " ")}
if runtime.GOARCH == "amd64" {
switch runtime.GOOS {
@@ -400,9 +401,9 @@ func test(pkgs ...string) {
runPrint(goCmd, append(args, pkgs...)...)
}
func bench(pkgs ...string) {
func bench(tags []string, pkgs ...string) {
lazyRebuildAssets()
args := append([]string{"test", "-run", "NONE"}, benchArgs()...)
args := append([]string{"test", "-run", "NONE", "-tags", strings.Join(tags, " ")}, benchArgs()...)
runPrint(goCmd, append(args, pkgs...)...)
}
@@ -464,7 +465,7 @@ func install(target target, tags []string) {
defer shouldCleanupSyso(sysoPath)
}
args := []string{"install", "-v"}
args := []string{"install", "-v", "-trimpath"}
args = appendParameters(args, tags, target.buildPkgs...)
runPrint(goCmd, args...)
}
@@ -492,7 +493,7 @@ func build(target target, tags []string) {
defer shouldCleanupSyso(sysoPath)
}
args := []string{"build", "-v"}
args := []string{"build", "-v", "-trimpath"}
args = appendParameters(args, tags, target.buildPkgs...)
runPrint(goCmd, args...)
}
@@ -526,7 +527,7 @@ func appendParameters(args []string, tags []string, pkgs ...string) []string {
if !debugBinary {
// Regular binaries get version tagged and skip some debug symbols
args = append(args, "-ldflags", ldflags())
args = append(args, "-ldflags", ldflags(tags))
} else {
// -gcflags to disable optimizations and inlining. Skip -ldflags
// because `Could not launch program: decoding dwarf section info at
@@ -829,13 +830,14 @@ func transifex() {
runPrint(goCmd, "run", "../../../../script/transifexdl.go")
}
func ldflags() string {
func ldflags(tags []string) string {
b := new(strings.Builder)
b.WriteString("-w")
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Version=%s", version)
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Stamp=%d", buildStamp())
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.User=%s", buildUser())
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Host=%s", buildHost())
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Tags=%s", strings.Join(tags, ","))
if v := os.Getenv("EXTRA_LDFLAGS"); v != "" {
fmt.Fprintf(b, " %s", v)
}
@@ -874,7 +876,10 @@ func getGitVersion() (string, error) {
}
v0 := string(bs)
versionRe := regexp.MustCompile(`-([0-9]{1,3}-g[0-9a-f]{5,10})`)
// To be more semantic-versionish and ensure proper ordering in our
// upgrade process, we make sure there's only one hypen in the version.
versionRe := regexp.MustCompile(`-([0-9]{1,3}-g[0-9a-f]{5,10}(-dirty)?)`)
if m := versionRe.FindStringSubmatch(vcur); len(m) > 0 {
suffix := strings.ReplaceAll(m[1], "-", ".")

View File

@@ -72,8 +72,12 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
url := urlPrefix + l.version + filename[idx:]
resp, err := l.client.Get(url)
if err != nil || resp.StatusCode != http.StatusOK {
fmt.Println("Loading source:", err.Error())
if err != nil {
fmt.Println("Loading source:", err)
return nil, 0
}
if resp.StatusCode != http.StatusOK {
fmt.Println("Loading source:", resp.Status)
return nil, 0
}
data, err := ioutil.ReadAll(resp.Body)

View File

@@ -634,12 +634,12 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
os.Exit(1)
}
// Check if auto-upgrades should be done and if yes, do an initial
// Check if auto-upgrades is possible, and if yes, and it's enabled do an initial
// upgrade immedately. The auto-upgrade routine can only be started
// later after App is initialised.
shouldAutoUpgrade := shouldUpgrade(cfg, runtimeOptions)
if shouldAutoUpgrade {
autoUpgradePossible := autoUpgradePossible(runtimeOptions)
if autoUpgradePossible && cfg.Options().AutoUpgradeEnabled() {
// try to do upgrade directly and log the error if relevant.
release, err := initialAutoUpgradeCheck(db.NewMiscDataNamespace(ldb))
if err == nil {
@@ -680,7 +680,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
app := syncthing.New(cfg, ldb, evLogger, cert, appOpts)
if shouldAutoUpgrade {
if autoUpgradePossible {
go autoUpgrade(cfg, app, evLogger)
}
@@ -702,9 +702,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
}
}
if opts := cfg.Options(); opts.RestartOnWakeup {
go standbyMonitor(app)
}
go standbyMonitor(app, cfg)
if err := app.Start(); err != nil {
os.Exit(syncthing.ExitError.AsInt())
@@ -818,12 +816,12 @@ func ensureDir(dir string, mode fs.FileMode) error {
return nil
}
func standbyMonitor(app *syncthing.App) {
func standbyMonitor(app *syncthing.App, cfg config.Wrapper) {
restartDelay := 60 * time.Second
now := time.Now()
for {
time.Sleep(10 * time.Second)
if time.Since(now) > 2*time.Minute {
if time.Since(now) > 2*time.Minute && cfg.Options().RestartOnWakeup {
l.Infof("Paused state detected, possibly woke up from standby. Restarting in %v.", restartDelay)
// We most likely just woke from standby. If we restart
@@ -838,13 +836,10 @@ func standbyMonitor(app *syncthing.App) {
}
}
func shouldUpgrade(cfg config.Wrapper, runtimeOptions RuntimeOptions) bool {
func autoUpgradePossible(runtimeOptions RuntimeOptions) bool {
if upgrade.DisabledByCompilation {
return false
}
if !cfg.Options().ShouldAutoUpgrade() {
return false
}
if runtimeOptions.NoUpgrade {
l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.")
return false
@@ -862,18 +857,19 @@ func autoUpgrade(cfg config.Wrapper, app *syncthing.App, evLogger events.Logger)
if !ok || data["clientName"] != "syncthing" || upgrade.CompareVersions(data["clientVersion"], build.Version) != upgrade.Newer {
continue
}
l.Infof("Connected to device %s with a newer version (current %q < remote %q). Checking for upgrades.", data["id"], build.Version, data["clientVersion"])
if cfg.Options().AutoUpgradeEnabled() {
l.Infof("Connected to device %s with a newer version (current %q < remote %q). Checking for upgrades.", data["id"], build.Version, data["clientVersion"])
}
case <-timer.C:
}
opts := cfg.Options()
checkInterval := time.Duration(opts.AutoUpgradeIntervalH) * time.Hour
if checkInterval < time.Hour {
// We shouldn't be here if AutoUpgradeIntervalH < 1, but for
// safety's sake.
checkInterval = time.Hour
if !opts.AutoUpgradeEnabled() {
timer.Reset(upgradeCheckInterval)
continue
}
checkInterval := time.Duration(opts.AutoUpgradeIntervalH) * time.Hour
rel, err := upgrade.LatestRelease(opts.ReleasesURL, build.Version, opts.UpgradeToPreReleases)
if err == upgrade.ErrUpgradeUnsupported {
sub.Unsubscribe()

View File

@@ -743,6 +743,7 @@ func getReport(db *sql.DB) map[string]interface{} {
inc(features["Folder"]["v3"], "Weak hash, always", rep.FolderUsesV3.AlwaysWeakHash)
inc(features["Folder"]["v3"], "Weak hash, custom threshold", rep.FolderUsesV3.CustomWeakHashThreshold)
inc(features["Folder"]["v3"], "Filesystem watcher", rep.FolderUsesV3.FsWatcherEnabled)
inc(features["Folder"]["v3"], "Case sensitive FS", rep.FolderUsesV3.CaseSensitiveFS)
add(featureGroups["Folder"]["v3"], "Conflicts", "Disabled", rep.FolderUsesV3.ConflictsDisabled)
add(featureGroups["Folder"]["v3"], "Conflicts", "Unlimited", rep.FolderUsesV3.ConflictsUnlimited)

17
go.mod
View File

@@ -19,14 +19,14 @@ require (
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/gobwas/glob v0.2.3
github.com/gogo/protobuf v1.3.1
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9
github.com/greatroar/blobloom v0.3.0
github.com/jackpal/gateway v1.0.6
github.com/jackpal/go-nat-pmp v1.0.2
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kr/pretty v0.2.0 // indirect
github.com/lib/pq v1.2.0
github.com/lucas-clemente/quic-go v0.17.3
github.com/lucas-clemente/quic-go v0.18.0
github.com/maruel/panicparse v1.3.0
github.com/mattn/go-isatty v0.0.11
github.com/minio/sha256-simd v0.1.1
@@ -36,17 +36,18 @@ require (
github.com/prometheus/client_golang v1.2.1
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563
github.com/sasha-s/go-deadlock v0.2.0
github.com/shirou/gopsutil v0.0.0-20190714054239-47ef3260b6bf
github.com/shirou/gopsutil v2.20.7+incompatible
github.com/syncthing/notify v0.0.0-20190709140112-69c7a957d3e2
github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d
github.com/syndtr/goleveldb v1.0.1-0.20200815071216-d9e9293bd0f7
github.com/thejerf/suture v3.0.2+incompatible
github.com/urfave/cli v1.22.2
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc
golang.org/x/sys v0.0.0-20200819171115-d785dc25833f
golang.org/x/text v0.3.3
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4
google.golang.org/protobuf v1.25.0 // indirect
)
go 1.13
go 1.14

128
go.sum
View File

@@ -16,13 +16,10 @@ github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzS
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75 h1:3ILjVyslFbc4jl1w5TWuvvslFD/nDfR2H8tVaMVLrEY=
github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -46,6 +43,7 @@ github.com/calmh/xdr v1.1.0 h1:U/Dd4CXNLoo8EiQ4ulJUXkgO1/EyQLgDKLgpY1SOoJE=
github.com/calmh/xdr v1.1.0/go.mod h1:E8sz2ByAdXC8MbANf1LCRYzedSnnc+/sXXJs/PVqoeg=
github.com/ccding/go-stun v0.0.0-20180726100737-be486d185f3d h1:As4937T5NVbJ/DmZT9z33pyLEprMd6CUSfhbmMY57Io=
github.com/ccding/go-stun v0.0.0-20180726100737-be486d185f3d/go.mod h1:3FK1bMar37f7jqVY7q/63k3OMX1c47pGCufzt3X0sYE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20190905060710-a5e0173ced67 h1:8k9FLYBLKT+9v2HQJ/a95ZemmTx+/ltJcAiRhVushG8=
github.com/certifi/gocertifi v0.0.0-20190905060710-a5e0173ced67/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
@@ -72,7 +70,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/siphash v1.2.1 h1:4cLinnzVJDKxTCl9B01807Yiy+W7ZzVHj/KIroQRvT4=
github.com/dchest/siphash v1.2.1/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
github.com/dgraph-io/badger v1.6.1 h1:w9pSFNSdq/JPM1N12Fz/F/bzo993Is1W+Q7HjPzi7yg=
github.com/dgraph-io/badger/v2 v2.0.3 h1:inzdf6VF/NZ+tJ8RwwYMjJMvsOALTHYdozn0qSl6XJI=
github.com/dgraph-io/badger/v2 v2.0.3/go.mod h1:3KY8+bsP8wI0OEnQJAKpd4wIJW/Mm32yw2j/9FUVnIM=
github.com/dgraph-io/ristretto v0.0.2-0.20200115201040-8f368f2f2ab3 h1:MQLRM35Pp0yAyBYksjbj1nZI/w6eyRY/mWoM1sFf4kU=
@@ -81,6 +78,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczC
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
@@ -88,27 +87,21 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-asn1-ber/asn1-ber v1.3.1 h1:gvPdv/Hr++TRFCl0UbPFHC54P9N9jgsRPnmnr419Uck=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-asn1-ber/asn1-ber v1.5.0 h1:/S4hO/AO6tLMlPX0oftGSOcdGJJN/MuYzfgWRMn199E=
github.com/go-asn1-ber/asn1-ber v1.5.0/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-ldap/ldap/v3 v3.1.7 h1:aHjuWTgZsnxjMgqzx0JHwNqz4jBYZTcNarbPFkW1Oww=
github.com/go-ldap/ldap/v3 v3.1.7/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-ldap/ldap/v3 v3.1.10 h1:7WsKqasmPThNvdl0Q5GPpbTDD/ZD98CfuawrMIuh7qQ=
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
github.com/go-ldap/ldap/v3 v3.2.0 h1:fkS0nXg43MZvU0UNTOGyQv60WdwHRXa1eX0CSzuKLvY=
github.com/go-ldap/ldap/v3 v3.2.0/go.mod h1:dtLsnBXnSLIsMRbCBuRpHflCGaYzZ5jn+x1q7XqMTKU=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -120,12 +113,16 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
@@ -138,6 +135,9 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -147,6 +147,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -155,8 +157,6 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/greatroar/blobloom v0.2.1 h1:Ie7+kTQFhcvfFHhzOYSJA2XE5sp8c9iB5iapu2tIig4=
github.com/greatroar/blobloom v0.2.1/go.mod h1:we9vO6GNYMmsNvCWINtZnQbcGEHUT6hGBAznNHd6RlE=
github.com/greatroar/blobloom v0.3.0 h1:TSf9vu9lZ840bnMXNFpFKe61AISBZL5a9uRL62KixCY=
github.com/greatroar/blobloom v0.3.0/go.mod h1:we9vO6GNYMmsNvCWINtZnQbcGEHUT6hGBAznNHd6RlE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
@@ -189,26 +189,16 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lucas-clemente/quic-go v0.15.6 h1:2l7g4fjjjwqmM3NirLiBtszMaR7aDTSsiiXY9FrGroo=
github.com/lucas-clemente/quic-go v0.15.6/go.mod h1:Myi1OyS0FOjL3not4BxT7KN29bRkcMUV5JVVFLKtDp8=
github.com/lucas-clemente/quic-go v0.15.7 h1:Pu7To5/G9JoP1mwlrcIvfV8ByPBlCzif3MCl8+1W83I=
github.com/lucas-clemente/quic-go v0.15.7/go.mod h1:Myi1OyS0FOjL3not4BxT7KN29bRkcMUV5JVVFLKtDp8=
github.com/lucas-clemente/quic-go v0.16.0 h1:jJw36wfzGJhmOhAOaOC2lS36WgeqXQszH47A7spo1LI=
github.com/lucas-clemente/quic-go v0.16.0/go.mod h1:I0+fcNTdb9eS1ZcjQZbDVPGchJ86chcIxPALn9lEJqE=
github.com/lucas-clemente/quic-go v0.16.1 h1:hleW5QcJCNjGS10ggod6tQQy7XjVtagS6UIdzBMAWpg=
github.com/lucas-clemente/quic-go v0.16.1/go.mod h1:I0+fcNTdb9eS1ZcjQZbDVPGchJ86chcIxPALn9lEJqE=
github.com/lucas-clemente/quic-go v0.17.1 h1:ezsH76xpn6hKugfsXUy6voIJBFmAOwnM/Oy9F4b/n+M=
github.com/lucas-clemente/quic-go v0.17.1/go.mod h1:I0+fcNTdb9eS1ZcjQZbDVPGchJ86chcIxPALn9lEJqE=
github.com/lucas-clemente/quic-go v0.17.2 h1:4iQInIuNQkPNZmsy9rCnwuOzpH0qGnDo4jn0QfI/qE4=
github.com/lucas-clemente/quic-go v0.17.2/go.mod h1:I0+fcNTdb9eS1ZcjQZbDVPGchJ86chcIxPALn9lEJqE=
github.com/lucas-clemente/quic-go v0.17.3 h1:jMX/MmDNCljfisgMmPGUcBJ+zUh9w3d3ia4YJjYS3TM=
github.com/lucas-clemente/quic-go v0.17.3/go.mod h1:I0+fcNTdb9eS1ZcjQZbDVPGchJ86chcIxPALn9lEJqE=
github.com/lucas-clemente/quic-go v0.18.0 h1:JhQDdqxdwdmGdKsKgXi1+coHRoGhvU6z0rNzOJqZ/4o=
github.com/lucas-clemente/quic-go v0.18.0/go.mod h1:yXttHsSNxQi8AWijC/vLP+OJczXqzHSOcJrM5ITUlCg=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI=
github.com/marten-seemann/qtls v0.9.1 h1:O0YKQxNVPaiFgMng0suWEOY2Sb4LT2sRn9Qimq3Z1IQ=
github.com/marten-seemann/qtls v0.9.1/go.mod h1:T1MmAdDPyISzxlK6kjRr0pcZFBVd1OZbBb/j3cvzHhk=
github.com/marten-seemann/qpack v0.2.0/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls v0.10.0 h1:ECsuYUKalRL240rRD4Ri33ISb7kAQ3qGDlrrl55b2pc=
github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs=
github.com/marten-seemann/qtls-go1-15 v0.1.0 h1:i/YPXVxz8q9umso/5y474CNcHmTpA+5DH+mFPjx6PZg=
github.com/marten-seemann/qtls-go1-15 v0.1.0/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/maruel/panicparse v1.3.0 h1:1Ep/RaYoSL1r5rTILHQQbyzHG8T4UP5ZbQTYTo4bdDc=
github.com/maruel/panicparse v1.3.0/go.mod h1:vszMjr5QQ4F5FSRfraldcIA/BCw5xrdLL+zEcU2nRBs=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
@@ -234,15 +224,15 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw=
github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
github.com/oschwald/geoip2-golang v1.4.0 h1:5RlrjCgRyIGDz/mBmPfnAF4h8k0IAcRv9PvrpOfz+Ug=
github.com/oschwald/geoip2-golang v1.4.0/go.mod h1:8QwxJvRImBH+Zl6Aa6MaIcs5YdlZSTKtzmPGzQqi9ng=
@@ -289,9 +279,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/sasha-s/go-deadlock v0.2.0 h1:lMqc+fUb7RrFS3gQLtoQsJ7/6TV/pAIFvBsqX73DK8Y=
github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZqiDbRupzT10=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil v0.0.0-20190714054239-47ef3260b6bf h1:c9SV5NzG4KOk448TUE7iqCmb4E4y79CZF4zDdc1Jx3Q=
github.com/shirou/gopsutil v0.0.0-20190714054239-47ef3260b6bf/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
github.com/shirou/gopsutil v2.20.7+incompatible h1:Ymv4OD12d6zm+2yONe39VSmp2XooJe8za7ngOLW/o/w=
github.com/shirou/gopsutil v2.20.7+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY=
github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM=
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
@@ -322,6 +311,7 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@@ -341,8 +331,8 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/syncthing/notify v0.0.0-20190709140112-69c7a957d3e2 h1:6tuEEEpg+mxM82E0YingzoXzXXISYR/o/7I9n573LWI=
github.com/syncthing/notify v0.0.0-20190709140112-69c7a957d3e2/go.mod h1:Sn4ChoS7e4FxjCN1XHPVBT43AgnRLbuaB8pEc1Zcdjg=
github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs=
github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA=
github.com/syndtr/goleveldb v1.0.1-0.20200815071216-d9e9293bd0f7 h1:udtnv1cokhJYqnUfCMCppJ71bFN9VKfG1BQ6UsYZnx8=
github.com/syndtr/goleveldb v1.0.1-0.20200815071216-d9e9293bd0f7/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/thejerf/suture v3.0.2+incompatible h1:GtMydYcnK4zBJ0KL6Lx9vLzl6Oozb65wh252FTBxrvM=
github.com/thejerf/suture v3.0.2+incompatible/go.mod h1:ibKwrVj+Uzf3XZdAiNWUouPaAbSoemxOHLmJmwheEMc=
@@ -357,6 +347,7 @@ github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0 h1:okhMind4q9H1OxF44gN
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0/go.mod h1:TTbGUfE+cXXceWtbTHq6lqcTvYPBKLNejBEbnUsQJtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -367,12 +358,15 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 h1:Q7tZBpemrlsc2I7IyODzhtallWRSm4Q0d09pL6XbQtU=
golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -381,14 +375,16 @@ golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -400,6 +396,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -414,20 +411,29 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 h1:Dho5nD6R3PcW2SH1or8vS0dszDaXRxIw55lBX7XiE5g=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200819171115-d785dc25833f h1:KJuwZVtZBVzDmEDtB2zro9CXkD9O0dpCv4o2LHbQIAw=
golang.org/x/sys v0.0.0-20200819171115-d785dc25833f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -441,10 +447,13 @@ golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
@@ -457,23 +466,33 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
@@ -486,10 +505,13 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck=

View File

@@ -241,6 +241,7 @@ identicon {
.identicon {
width: 1em;
height: 1em;
shape-rendering: crispEdges;
}
a.toggler {

View File

@@ -39,6 +39,8 @@
"Bugs": "Бъгове",
"Changelog": "Списък с промени",
"Clean out after": "Изчисти след",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Натиснете, за да видите грешки при откриването",
"Close": "Затвори",
"Command": "Команда",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"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 cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурацията е запазена, но не е активирана. Syncthing трябва да рестартира, за да се активира новата конфигурация.",
"The device ID cannot be blank.": "Полето идентификатор на устройство не може да бъде празно.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор на устройство за въвеждане тук, може да бъде намерен в \"Промени > Покажи идентификатора\" на другото устройство. Интервалите и тиретата са пожелание (биват прескачани).",
@@ -305,6 +308,8 @@
"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 interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"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 number of days must be a number and cannot be blank.": "Броят дни трябва да бъде число и не може да бъде празно.",
@@ -346,6 +351,7 @@
"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 Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Предупреждение, този път е по-горна директория на съществуващата папка \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "файла",
"full documentation": "пълна документация",
"items": "елемента",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} желае да сподели папката \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} желае да сподели папката \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Errors (Bugs)",
"Changelog": "Registre de canvis",
"Clean out after": "Netejar després de",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Clica per a vore els fallos en el descobriment",
"Close": "Tancar",
"Command": "Comando",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfície d'administració de Syncthing està configurat per a permetre l'accés remot sense una contrasenya.",
"The aggregated statistics are publicly available at the URL below.": "Les estadístiques agregades estàn disponibles en la URL que figura a continuació.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració ha sigut gravada però no activada. Syncthing deu reiniciar per tal d'activar la nova configuració.",
"The device ID cannot be blank.": "L'ID del dispositiu no pot estar buida.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID del dispositiu que hi ha que introduïr ací es pot trobar en el menú \"Accions > Mostrar ID\" en l'altre dispositiu. Els espais i les barres son opcionals (ignorats).",
@@ -305,6 +308,8 @@
"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.": "S'utilitzen els següents intervals: per a la primera hora es guarda una versió cada 30 segons, per al primer dia es guarda una versió cada hora, per als primers 30 dies es guarda una versió diaria, fins l'edat màxima es guarda una versió cada setmana.",
"The following items could not be synchronized.": "Els següents objectes no s'han pogut sincronitzar.",
"The following items were changed locally.": "Els següents ítems es canviaren localment.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "L'edat màxima deu ser un nombre i no pot estar buida.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El temps màxim per a guardar una versió (en dies, ficar 0 per a guardar les versions per a sempre).",
"The number of days must be a number and cannot be blank.": "El nombre de dies deu ser un nombre i no pot estar en blanc.",
@@ -346,6 +351,7 @@
"Versions": "Versions",
"Versions Path": "Ruta de les versions",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions s'esborren automàticament si són més antigues que l'edat màxima o excedixen el nombre de fitxer permesos en un interval.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Perill! Esta ruta és un directori pare d'una carpeta ja existent \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "arxius",
"full documentation": "Documentació completa",
"items": "Elements",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartit la carpeta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vol compartir la carpeta \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Chyby",
"Changelog": "Seznam změn",
"Clean out after": "Vyčistit po",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Kliknutím zobrazíte nezdary při objevování",
"Close": "Zavřít",
"Command": "Příkaz",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "V nastavení aplikace Syncthing je povoleno vzdálené připojení k administrátorskému rozhraní bez zadání hesla.",
"The aggregated statistics are publicly available at the URL below.": "Souhrnné statistiky jsou veřejně dostupné na níže uvedené URL.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Nastavení byla uložena, ale nejsou aktivována. Pro aktivaci nového nastavení je třeba Syncthing restartovat.",
"The device ID cannot be blank.": "Identifikátor zařízení nemůže zůstat nevyplněný.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Identifikátor zařízení, který je třeba vložit, lze nalézt v dialogu „Akce > Zobrazit identifikátor“ na druhém zařízení. Mezery a pomlčky nejsou nutné (budou ignorovány).",
@@ -305,6 +308,8 @@
"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.": "Jsou použity následující intervaly: za první hodinu jsou ponechány verze pro každých 30 sekund, za první den jsou ponechány verze pro každou hodinu, za prvních 30 dní jsou ponechány verze pro každý den a do nejvyššího nastaveného stáří jsou ponechány verze pro každý týden.",
"The following items could not be synchronized.": "Následující položky nemohly být synchronizovány.",
"The following items were changed locally.": "Tyto položky byly změněny lokálně",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "Nejvyšší stáří je třeba zadat v podobě čísla a nemůže být prázdné.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maximální doba pro zachování verze (dny, zapsáním hodnoty 0 bude ponecháno navždy).",
"The number of days must be a number and cannot be blank.": "Je třeba, aby počet dní bylo číslo a nemůže zůstat nevyplněné.",
@@ -346,6 +351,7 @@
"Versions": "Verze",
"Versions Path": "Popis umístění verzí",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Verze jsou automaticky smazány, pokud jsou starší než maximální časový limit nebo překročí počet souborů povolených pro interval.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Čekání na skenování",
"Waiting to Sync": "Čekání na synchronizaci",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varování, tento popis umístění je nadřazenou složkou existující „{{otherFolder}}“.",
@@ -370,6 +376,7 @@
"files": "souborů",
"full documentation": "úplná dokumentace",
"items": "položky",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce sdílet složku „{{folder}}“.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} chce sdílet složku „{{folderlabel}}“ ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Fejl",
"Changelog": "Udgivelsesnoter",
"Clean out after": "Rens efter",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Klik for at se opdagelsesfejl",
"Close": "Luk",
"Command": "Kommando",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing-administationsfladen er sat op til at kunne fjernstyres uden adgangskode.",
"The aggregated statistics are publicly available at the URL below.": "Den indsamlede statistik er offentligt tilgængelig på den nedenstående URL.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen er gemt, men ikke aktiveret. Syncthing skal genstarte for at aktivere den nye konfiguration.",
"The device ID cannot be blank.": "Enhedens ID må ikke være tom.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Det enheds-ID, som skal indtastes her, kan findes under menuen “Handlinger > Vis ID” på den anden enhed. Mellemrum og bindestreger er valgfri (ignoreres).",
@@ -305,6 +308,8 @@
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De følgende intervaller er brugt: Inden for den første time bliver en version gemt hvert 30. sekund, inden for den første dag bliver en version gemt hver time, inden for de første 30 dage bliver en version gemt hver dag, og indtil den maksimale alder bliver en version gemt hver uge.",
"The following items could not be synchronized.": "Følgende filer kunne ikke synkroniseres.",
"The following items were changed locally.": "De følgende filer er ændret lokalt.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "Maksimal alder skal være et tal og feltet må ikke være tomt.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den maksimale tid, en version skal gemmes (i dage; sæt lig med 0 for at beholde gamle versioner for altid).",
"The number of days must be a number and cannot be blank.": "Antallet af dage skal være et tal og feltet må ikke være tomt.",
@@ -346,6 +351,7 @@
"Versions": "Versioner",
"Versions Path": "Versionssti",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner slettes automatisk, hvis de er ældre end den givne maksimum alder eller overstiger det tilladte antal filer i et interval.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Advarsel: Denne sti er en forældermappe til den eksisterende mappe “{{otherFolder}}”.",
@@ -370,6 +376,7 @@
"files": "filer",
"full documentation": "fuld dokumentation",
"items": "filer",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker at dele mappen “{{folder}}”.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} ønsker at dele mappen “{{folderlabel}}” ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Fehler",
"Changelog": "Änderungsprotokoll",
"Clean out after": "Löschen nach",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Bereinigungsintervall",
"Click to see discovery failures": "Klick um Gerätesuchfehler anzuzeigen",
"Close": "Schließen",
"Command": "Befehl",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "Die Syncthing Autoren",
"The Syncthing admin interface is configured to allow remote access without a password.": "Die Syncthing-Oberfläche erlaubt mit den jetzigen Einstellungen einen Zugriff ohne Passwort.",
"The aggregated statistics are publicly available at the URL below.": "Die gesammelten Statistiken sind öffentlich unter der nachfolgenden URL verfügbar.",
"The cleanup interval cannot be blank.": "Das Bereinigungsintervall darf nicht leer sein.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber noch nicht aktiviert. Syncthing muss neugestartet werden, um die neue Konfiguration zu übernehmen.",
"The device ID cannot be blank.": "Die Gerätekennung darf nicht leer sein.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Die hier einzutragende Gerätekennung kann im Dialog \"Aktionen > Kennung anzeigen\" auf dem anderen Gerät gefunden werden. Leerzeichen und Bindestriche sind optional (werden ignoriert).",
@@ -305,6 +308,8 @@
"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.": "Es wird in folgenden Abständen versioniert: In der ersten Stunde wird alle 30 Sekunden eine Version behalten, am ersten Tag eine jede Stunde, in den ersten 30 Tagen eine jeden Tag. Danach wird bis zum angegebenen Höchstalter eine Version pro Woche behalten.",
"The following items could not be synchronized.": "Die folgenden Elemente konnten nicht synchronisiert werden.",
"The following items were changed locally.": "Die folgenden Elemente wurden lokal geändert.",
"The interval must be a positive number of seconds.": "Das Intervall muss eine positive Zahl von Sekunden sein.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "Das Höchstalter muss angegeben werden und eine Zahl sein.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Die längste Zeit, die alte Versionen vorgehalten werden (in Tagen) (0 um alte Versionen für immer zu behalten).",
"The number of days must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Ganzzahl und darf nicht leer sein.",
@@ -346,6 +351,7 @@
"Versions": "Versionen",
"Versions Path": "Versionierungspfad",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Alte Dateiversionen werden automatisch gelöscht, wenn sie älter als das angegebene Höchstalter sind oder die angegebene Höchstzahl an Dateien erreicht ist.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Warte auf Scan",
"Waiting to Sync": "Warten auf die Synchronisation",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warnung, dieser Pfad ist ein übergeordneter Ordner eines existierenden Ordners \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "Dateien",
"full documentation": "Komplette Dokumentation",
"items": "Elemente",
"seconds": "Sekunden",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} möchte den Ordner \"{{folder}}\" teilen.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} möchte den Ordner \"{{folderlabel}}\" ({{folder}}) teilen."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Bugs",
"Changelog": "Πληροφορίες εκδόσεων",
"Clean out after": "Εκκαθάριση μετά από",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Πατήστε για να δείτε τις αποτυχίες ανεύρεσης συσκευών",
"Close": "Τέλος",
"Command": "Εντολή",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "Η διεπαφή διαχείρισης του Syncthing είναι ρυθμισμένη να επιτρέπει την πρόσβαση χωρίς κωδικό.",
"The aggregated statistics are publicly available at the URL below.": "Τα στατιστικά που έχουν συλλεγεί είναι δημόσια διαθέσιμα στη παρακάτω διεύθυνση.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Οι ρυθμίσεις έχουν αποθηκευτεί αλλά δεν έχουν ενεργοποιηθεί. Πρέπει να επανεκκινήσεις το Syncthing για να ισχύσουν οι νέες ρυθμίσεις.",
"The device ID cannot be blank.": "Η ταυτότητα της συσκευής δεν μπορεί να είναι κενή",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Η ταυτότητα της συσκευής που θα μπει εδώ βρίσκεται στο μενού «Ενέργειες > Εμφάνιση ταυτότητας» στην άλλη συσκευή. Κενοί χαρακτήρες και παύλες είναι προαιρετικοί (θα αγνοηθούν).",
@@ -305,6 +308,8 @@
"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 interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"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 number of days must be a number and cannot be blank.": "Ο αριθμός ημερών πρέπει να είναι αριθμός και σίγουρα όχι κενό.",
@@ -346,6 +351,7 @@
"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 Clean": "Waiting to Clean",
"Waiting to Scan": "Αναμονή σάρωσης",
"Waiting to Sync": "Αναμονή συγχρονισμού",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Προσοχή, αυτό το μονοπάτι είναι γονικός φάκελος ενός υπάρχοντος φακέλου \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "αρχεία",
"full documentation": "πλήρης τεκμηρίωση",
"items": "εγγραφές",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "Η συσκευή {{device}} θέλει να μοιράσει τον φάκελο «{{folder}}».",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "Η συσκευή {{device}} επιθυμεί να διαμοιράσει τον φάκελο \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Bugs",
"Changelog": "Changelog",
"Clean out after": "Clean out after",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Click to see discovery failures",
"Close": "Close",
"Command": "Command",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
"The device ID cannot be blank.": "The device ID cannot be blank.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).",
@@ -305,6 +308,8 @@
"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.": "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.",
"The following items could not be synchronized.": "The following items could not be synchronised.",
"The following items were changed locally.": "The following items were changed locally.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "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).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
@@ -346,6 +351,7 @@
"Versions": "Versions",
"Versions Path": "Versions Path",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a parent directory of an existing folder \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "files",
"full documentation": "full documentation",
"items": "items",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Cimoj",
"Changelog": "Ŝanĝoprotokolo",
"Clean out after": "Purigi poste",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Alklaku por vidi malsukcesajn malkovrojn",
"Close": "Fermi",
"Command": "Komando",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "La administra interfaco de Syncthing estas agordita por permesi foran atingon sen pasvorto.",
"The aggregated statistics are publicly available at the URL below.": "La agregita statistikoj estas publike disponebla ĉe la URL malsupre.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La agordo estis registrita sed ne aktivigita. Syncthing devas restarti por aktivigi la novan agordon.",
"The device ID cannot be blank.": "La aparato ID ne povas esti malplena.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "La aparato ID por eniri ĉi tie estas trovebla per \"Agoj > Montru ID\" dialogo en la alia aparato. Interspacoj kaj streketoj estas opcio (ignorigita).",
@@ -305,6 +308,8 @@
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "La jenaj intervaloj estas uzataj: dum la unua horo version restas dum ĉiuj 30 sekundoj, dum la unua tago versio restas konservita dum ĉiu horo, dum la unuaj 30 tagoj versio estas konservita dum ĉiu tago, ĝis la maksimume aĝa versio restas konservita dum ĉiu semajno.",
"The following items could not be synchronized.": "La sekvantaj eroj ne povas esti sinkronigitaj.",
"The following items were changed locally.": "La sekvantaj eroj estis ŝanĝitaj loke.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "La maksimuma aĝo devas esti nombro kaj ne povas esti malplena.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La maksimuma tempo por konservi version (en tagoj, agordi je 0 por konservi versiojn eterne).",
"The number of days must be a number and cannot be blank.": "La nombro da tagoj devas esti nombro kaj ne povas esti malplena.",
@@ -346,6 +351,7 @@
"Versions": "Versioj",
"Versions Path": "Vojo de Versioj",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioj estas aŭtomate forigita se ili estas pli malnovaj ol la maksimuma aĝo aŭ superas la nombron da dosieroj permesita en intervalo.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Atendante skanadon",
"Waiting to Sync": "Atendante sinkronigadon",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Averto, ĉi tiu vojo estas parenta dosierujo de ekzistanta dosierujo \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "dosieroj",
"full documentation": "tuta dokumentado",
"items": "eroj",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} volas komunigi dosierujon \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} volas komunigi dosierujon \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Errores",
"Changelog": "Registro de cambios",
"Clean out after": "Limpiar tras",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Clica para ver fallos de descubrimiento.",
"Close": "Cerrar",
"Command": "Acción",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "El panel de administración de Syncthing está configurado para permitir el acceso remoto sin contraseña.",
"The aggregated statistics are publicly available at the URL below.": "Las estadísticas agragadas están disponibles públicamente en la URL de abajo.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido grabada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
"The device ID cannot be blank.": "La ID del dispositivo no puede estar vacía.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "El ID del dispositivo que hay que introducir aquí se puede encontrar en el diálogo \"Acciones > Mostrar ID\" en el otro dispositivo. Los espacios y las barras son opcionales (ignorados).",
@@ -305,6 +308,8 @@
"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.": "Se utilizan los siguientes intervalos: para la primera hora se mantiene una versión cada 30 segundos, para el primer día se mantiene una versión cada hora, para los primeros 30 días se mantiene una versión diaria hasta la edad máxima de una semana.",
"The following items could not be synchronized.": "Los siguientes elementos no pueden ser sincronizados.",
"The following items were changed locally.": "Los siguientes ítems fueron cambiados localmente.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar vacía.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El tiempo máximo para mantener una versión en días (introducir 0 para mantener las versiones indefinidamente).",
"The number of days must be a number and cannot be blank.": "El número de días debe ser un número y no puede estar en blanco.",
@@ -346,6 +351,7 @@
"Versions": "Versiones",
"Versions Path": "Ruta de las versiones",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Las versiones se borran automáticamente si son más antiguas que la edad máxima o exceden el número de ficheros permitidos en un intervalo.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "¡Peligro! Esta ruta es un directorio principal de la carpeta ya existente \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "archivos",
"full documentation": "Documentación completa",
"items": "Elementos",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Errores",
"Changelog": "Registro de cambios",
"Clean out after": "Limpiar tras",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Clica para ver fallos de descubrimiento.",
"Close": "Cerrar",
"Command": "Acción",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "El panel de administración de Syncthing está configurado para permitir el acceso remoto sin contraseña.",
"The aggregated statistics are publicly available at the URL below.": "Las estadísticas agragadas están disponibles públicamente en la URL de abajo.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido grabada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
"The device ID cannot be blank.": "La ID del dispositivo no puede estar vacía.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "El ID del dispositivo que hay que introducir aquí se puede encontrar en el diálogo \"Acciones > Mostrar ID\" en el otro dispositivo. Los espacios y las barras son opcionales (ignorados).",
@@ -305,6 +308,8 @@
"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.": "Se utilizan los siguientes intervalos: para la primera hora se mantiene una versión cada 30 segundos, para el primer día se mantiene una versión cada hora, para los primeros 30 días se mantiene una versión diaria hasta la edad máxima de una semana.",
"The following items could not be synchronized.": "Los siguientes elementos no pueden ser sincronizados.",
"The following items were changed locally.": "The following items were changed locally.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar vacía.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El tiempo máximo para mantener una versión en días (introducir 0 para mantener las versiones indefinidamente).",
"The number of days must be a number and cannot be blank.": "El número de días debe ser un número y no puede estar en blanco.",
@@ -346,6 +351,7 @@
"Versions": "Versiones",
"Versions Path": "Ruta de las versiones",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Las versiones se borran automáticamente si son más antiguas que la edad máxima o exceden el número de ficheros permitidos en un intervalo.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "¡Peligro! Esta ruta es un directorio principal de la carpeta ya existente \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "archivos",
"full documentation": "Documentación completa",
"items": "Elementos",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Akatsak",
"Changelog": "Bertsioen historia",
"Clean out after": "Garbi …. epearen ondotik",
"Cleaning Versions": "Bertsio garbiketa",
"Cleanup Interval": "Garbiketa-tartea",
"Click to see discovery failures": "Klik egin hutsegiteak ikusteko",
"Close": "Hetsi",
"Command": "Kontrolagailua",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "Syncthing autoreak",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing-en administrazio interfazea pentsatua da urrundikako helbideak pasahitzik gabe onartzeko !",
"The aggregated statistics are publicly available at the URL below.": "Estadistikak zuzen bide honetan publikoki ikusgarriak dira",
"The cleanup interval cannot be blank.": "Garbiketa tartea ez da hutsa izaiten ahal",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurazioa grabatua izan da bainan ez aktibatua. Syncthing berriz piztu behar da konfigurazio berria berriz aktibatzeko.",
"The device ID cannot be blank.": "Tresnaren ID-a ez da hutsa izaiten ahal.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Sartu behar den tresnaren ID-a atxemaiten ahal da menuan \"Ekintzak > ID-a erakuts\" (tresna urrunduarena). Espazio eta gioiak ez dira beharrezkoak.",
@@ -305,6 +308,8 @@
"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.": "Hunako tarteak erabiliak dira: lehen orduan bertsio bat kontserbatua da 30 segundu guziz. Lehen egunean, bertsio bat ordu bakoitz, lehen 30 egunetan bertsio bat egunero. Handik harat, adinaren mugetan egonez, bertsio bat astero.",
"The following items could not be synchronized.": "Ondoko fitxero hauk ez dira sinkronizatuak ahal izan",
"The following items were changed locally.": "Elementu hauek tokiz aldatu dira.",
"The interval must be a positive number of seconds.": "Tarteak segundo kopuru positiboa izan behar du.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Bertsioen direktorioko garbiketa exekutatzeko tartea, segundotan. Zero aldizkako garbiketa desgaitzeko.",
"The maximum age must be a number and cannot be blank.": "Gehieneko adinak zenbaki bat behar du izan eta ez da hutsa izaiten ahal.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Bertsio baten kontserbatzeko epe haundiena (egunez behar du izan. Jar ezazu zerotan bertsioak betirako atxikitzeko)",
"The number of days must be a number and cannot be blank.": "Egunen kopuruak numerikoa izan behar du eta ez da hutsa izaiten ahal",
@@ -346,6 +351,7 @@
"Versions": "Bertsioak",
"Versions Path": "Bertsioen kokalekua",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Bertsioak automatikoki ezeztatuak izanen dira, kontserbatzeko iraupen denbora pasatua badute edo bitartean onartua den kopurua (fitxategi bakoitzean) gainditua balin bada",
"Waiting to Clean": "Garbitzeko zain",
"Waiting to Scan": "Eskaneatzeko zain",
"Waiting to Sync": "Sinkronizatzeko zain",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Kasu, bide hau dagoen partekatze baten karpeta ahaidea da (adibidez, \"{{otherFolder}}\"). Segitzen baduzu, azpi-karpeta berri bat sortu behar duzu, bestenaz arazoak sortzen ahal dira, fitxategi kentzeak edo doblatzeak.",
@@ -370,6 +376,7 @@
"files": "Fitxategiak",
"full documentation": "Dokumentazio osoa",
"items": "Elementuak",
"seconds": "segunduak",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}}k \"{{folder}}\" partekatze hontan gomitatzen zaitu.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}}k \"{{folderlabel}}\" ({{folder}}) hontan gomitatzen zaitu."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Bugit",
"Changelog": "Muutoshistoria",
"Clean out after": "Puhdista seuraavan ajan kuluttua",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Klikkaa nähdäksesi etsinnässä tapahtuneet virheet",
"Close": "Sulje",
"Command": "Komento",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthingin hallintakäyttöliittymä on asetettu sallimaan ulkoiset yhteydet ilman salasanaa.",
"The aggregated statistics are publicly available at the URL below.": "Koostetut tilastot ovat julkisesti saatavilla alla olevassa osoitteessa.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Asetukset on tallennettu, mutta niitä ei ole otettu käyttöön. Syncthingin täytyy käynnistyä uudelleen, jotta uudet asetukset saadaan käyttöön.",
"The device ID cannot be blank.": "Laitteen ID ei voi olla tyhjä.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Tähän kohtaan syötettävän ID:n löytää \"Muokkaa > Näytä ID\" -valikosta toiselta laitteelta. Välit ja viivat ovat valinnaisia (jätetään huomiotta).",
@@ -305,6 +308,8 @@
"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.": "Seuraavat aikavälit ovat käytössä: ensimmäisen tunnin ajalta uusi versio säilytetään joka 30 sekunti, ensimmäisen päivän ajalta uusi versio säilytetään tunneittain ja ensimmäisen 30 päivän aikana uusi versio säilytetään päivittäin. Lopulta uusi versio säilytetään viikoittain, kunnes maksimi-ikä saavutetaan.",
"The following items could not be synchronized.": "Seuraavia nimikkeitä ei voitu synkronoida.",
"The following items were changed locally.": "The following items were changed locally.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "Maksimi-iän tulee olla numero, eikä se voi olla tyhjä.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimiaika versioiden säilytykseen (päivissä, aseta 0 säilyttääksesi versiot ikuisesti).",
"The number of days must be a number and cannot be blank.": "Päivien määrän tulee olla numero, eikä se voi olla tyhjä.",
@@ -346,6 +351,7 @@
"Versions": "Versiot",
"Versions Path": "Versioiden polku",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versiot poistetaan automaattisesti mikäli ne ovat vanhempia kuin maksimi-ikä tai niiden määrä ylittää sallitun määrän tietyllä aikavälillä.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varoitus: tämä polku on olemassa olevan kansion \"{{otherFolder}}\" yläkansio.",
@@ -370,6 +376,7 @@
"files": "tiedostot",
"full documentation": "täysi dokumentaatio",
"items": "kohteet",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} haluaa jakaa kansion \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} haluaa jakaa kansion \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Bugs",
"Changelog": "Historique des versions",
"Clean out after": "Conserver pendant :",
"Cleaning Versions": "Versions de nettoyage",
"Cleanup Interval": "Intervalle de nettoyage",
"Click to see discovery failures": "Voir les échecs de découverte",
"Close": "Fermer",
"Command": "Commande",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "Les concepteurs de Syncthing",
"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.",
"The cleanup interval cannot be blank.": "L'intervalle de nettoyage ne peut pas être vide.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
"The device ID cannot be blank.": "L'ID de l'appareil ne peut être vide.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID d'appareil à saisir ici se trouve dans le menu \"Actions > Afficher mon ID\" de l'appareil distant. Espaces et tirets sont optionnels (ignorés).",
@@ -305,6 +308,8 @@
"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.": "Les seuils de durée suivants définissent le nombre maximum de versions pour chaque fichier : pendant la première heure une version est conservée toutes les 30 secondes. Le premier jour, une version par heure - des versions de la première heure sont alors progressivement effacées pour finir par n'en garder que la dernière. Pour les 30 jours passés, une version par jour - des versions horaires du premier jour sont alors progressivement effacées pour n'en garder qu'une par jour. Au-delà et jusqu'à la limite d'âge, une version est conservée par semaine - des versions journalières du premier mois sont alors progressivement effacées pour n'en garder qu'une.",
"The following items could not be synchronized.": "Les fichiers suivants n'ont pas pu être synchronisés.",
"The following items were changed locally.": "Les éléments suivants ont été modifiés localement.",
"The interval must be a positive number of seconds.": "L'intervalle doit être un nombre positif exprimé en secondes",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "L'intervalle, en secondes, de l'exécution du nettoyage dans le répertoire des versions. Définir à zéro pour désactiver le nettoyage périodique.",
"The maximum age must be a number and cannot be blank.": "L'âge maximum doit être un nombre et ne peut être vide.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Durée maximale de conservation d'une version (en jours, 0 pour conservation éternelle)",
"The number of days must be a number and cannot be blank.": "Le nombre de jours doit être numérique et ne peut pas être vide.",
@@ -346,6 +351,7 @@
"Versions": "Restauration...",
"Versions Path": "Emplacement des versions",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions seront supprimées automatiquement quand elles dépassent la durée maximum de conservation ou si leur nombre (par fichier) est supérieur à la limite prédéfinie pour l'intervalle.",
"Waiting to Clean": "En attente de nettoyage",
"Waiting to Scan": "En attente d'analyse",
"Waiting to Sync": "En attente de synchronisation",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Attention, ce chemin est un répertoire parent d'au moins un partage existant (par exemple \"{{otherFolder}}\"). Si c'est bien ce que vous souhaitez, vous devriez créer un nouveau sous-répertoire, sinon ceci peut causer des problèmes tels que duplications et/ou suppressions intempestives de fichiers.",
@@ -370,6 +376,7 @@
"files": "Fichiers",
"full documentation": "Documentation complète ici",
"items": "éléments",
"seconds": "secondes",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Brekkings",
"Changelog": "Feroaringslochboek",
"Clean out after": "Opromje nei",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Klik om ûntdekkingsflaters te besjen",
"Close": "Slute",
"Command": "Kommando",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "De Syncthing haadbrûker-ynterfaasje is sa ynstelt dat tagong fan ôfstân sûnder wachtwurd tastean is.",
"The aggregated statistics are publicly available at the URL below.": "De fersammele statistiken binnen yn it publyk beskikber fia ûndersteande keppeling.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De konfiguraasje is bewarre mar noch net aktivearre. Syncthing moat werstarte om de nije konfiguraasje te aktivearren.",
"The device ID cannot be blank.": "It apparaat-ID kin net leech wêze.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "It apparaat-ID dat hjir ynfierd wurde kin, kin fûn wurde yn in it \"Askjes > ID sjen litte\" dialooch op de oare apparaten. Spaasjes en streepkes binne mooglik (wurde negeard).",
@@ -305,6 +308,8 @@
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De folgende yntervals wurd brûkt: foar it earste oere wurd eltse 30 sekonden in ferzje bewarre, foar de earste dei wurd eltse oere in ferzje bewarre, foar de earste 30 dagen wurd eltse dei in ferzje bewarre, oant ta de maksimale âldens wurd eltse wike in ferzje bewarre.",
"The following items could not be synchronized.": "De folgende items koene net syngronisearre wurde.",
"The following items were changed locally.": "De neikommende items binne lokaal feroare.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "De maksimale âldens moat in nûmer en net leech wêze.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "De maksimale tiid dat in ferzje bewarre wurde moat (yn dagen, stel yn op 0 om ferzjes foar immer te bewarjen).",
"The number of days must be a number and cannot be blank.": "It tal fan dagen moat in nûmer wêze en mei net leech wêze.",
@@ -346,6 +351,7 @@
"Versions": "Ferzjes",
"Versions Path": "Ferzjes-paad",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Ferzjes wurde automatysk fuortsmiten wannear't se âlder binne dan de maksimale âldens of wannear it tal fan triemen yn in ynterval grutter is dan tastean.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Wachtet om te Skennen",
"Waiting to Sync": "Wachten om te Synchronisearren",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warskôging, dit paad is in boppelizzende triemtafel fan in besteande map \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "triemmen",
"full documentation": "komplete dokumintaasje",
"items": "items",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wol map \"{{folder}}\" diele.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wol map \"{{folderlabel}}\" ({{folder}}) diele."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Hibák",
"Changelog": "Változások",
"Clean out after": "Takarítás",
"Cleaning Versions": "Tisztító verziók",
"Cleanup Interval": "Tisztítási intervallum",
"Click to see discovery failures": "Kattints ide a felfedezési hibák megtekintéséhez",
"Close": "Bezárás",
"Command": "Parancs",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "A Syncthing szerzői",
"The Syncthing admin interface is configured to allow remote access without a password.": "A Syncthing adminisztrációs felületének távoli elérése be van kapcsolva jelszó nélkül.",
"The aggregated statistics are publicly available at the URL below.": "Az összesített statisztikák elérhetők az alábbi címen.",
"The cleanup interval cannot be blank.": "A tisztítási intervallum nem lehet üres.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A beállítások elmentésre kerültek, de nem lettek aktiválva. Újra kell indítani a Syncthing-et az aktiválásukhoz.",
"The device ID cannot be blank.": "Az eszközazonosító nem lehet üres.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Az itt megadandó eszközazonosító a másik eszköz Műveletek > Azonosító mutatása című ablakában látható. Szóközök és kötőjelek használhatók (nem számítanak).",
@@ -305,6 +308,8 @@
"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.": "A következő intervallumokat használjuk: egy régi verziót őrzünk meg az első órában minden 30 másodpercben, az első nap minden órában, az első 30 napban minden nap, egészen addig amíg el nem érjük a maximálisan megtartható verziók számát minden héten.",
"The following items could not be synchronized.": "A következő elemek nem szinkronizálhatóak.",
"The following items were changed locally.": "A következő elemek változtak helyileg.",
"The interval must be a positive number of seconds.": "A tisztítási intervallum egy pozitív számmal kifejezett másodperc kell legyen.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "A verziók mappáin futó tisztítási folyamat intervalluma másodpercekben kifejezve. A nullával letiltható az időszakos tisztítás.",
"The maximum age must be a number and cannot be blank.": "A maximális kornak számnak kell lenni és nem lehet üres.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "A verziók megtartásának maximális ideje (napokban, 0-t megadva örökre megmaradnak).",
"The number of days must be a number and cannot be blank.": "A napok száma szám kell legyen és nem lehet üres.",
@@ -346,6 +351,7 @@
"Versions": "Verziók",
"Versions Path": "Verziók útvonala",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "A régi verziók automatikusan törlődnek, amennyiben öregebbek, mint a maximum kor, vagy már több van belőlük, mint az adott időszakban megtartható maximum.",
"Waiting to Clean": "Várakozás tisztításra",
"Waiting to Scan": "Várakozás átnézésre",
"Waiting to Sync": "Várakozás a szinkronizálásra",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Figyelem, ez az útvonal a meglévő „{{otherFolder}}” mappa szülőmappája.",
@@ -370,6 +376,7 @@
"files": "fájl",
"full documentation": "teljes dokumentáció",
"items": "elem",
"seconds": "másodperc",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} szeretné megosztani a mappát: „{{folder}}”.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} szeretné megosztani a mappát: „{{folderlabel}}” ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Bug",
"Changelog": "Changelog",
"Clean out after": "Svuota dopo",
"Cleaning Versions": "Pulizia Versioni",
"Cleanup Interval": "Intervallo di Pulizia",
"Click to see discovery failures": "Clicca per vedere gli errori di rilevamento",
"Close": "Chiudi",
"Command": "Comando",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "Gli Autori di Syncthing",
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfaccia di amministrazione di Syncthing è configurata in modo da permettere l'accesso senza password.",
"The aggregated statistics are publicly available at the URL below.": "Le statistiche aggregate sono disponibili pubblicamente all'URL seguente.",
"The cleanup interval cannot be blank.": "L'intervallo di pulizia non può essere vuoto.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Syncthing deve essere riavviato per attivare la nuova configurazione.",
"The device ID cannot be blank.": "L'ID del dispositivo non può essere vuoto.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID del dispositivo da inserire qui può essere trovato nella finestra \"Azioni> Mostra ID\" sull'altro dispositivo. Gli spazi e i trattini sono opzionali (ignorati).",
@@ -305,6 +308,8 @@
"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.": "Vengono utilizzati i seguenti intervalli temporali: per la prima ora viene mantenuta una versione ogni 30 secondi, per il primo giorno viene mantenuta una versione ogni ora, per i primi 30 giorni viene mantenuta una versione al giorno, successivamente viene mantenuta una versione ogni settimana fino al periodo massimo impostato.",
"The following items could not be synchronized.": "Non è stato possibile sincronizzare i seguenti elementi.",
"The following items were changed locally.": "I seguenti elementi sono stati modificati localmente.",
"The interval must be a positive number of seconds.": "L'intervallo deve essere un numero positivo di secondi.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "L'intervallo, in secondi, per l'esecuzione della pulizia nella directory delle versioni. Zero per disabilitare la pulizia periodica.",
"The maximum age must be a number and cannot be blank.": "La durata massima dev'essere un numero e non può essere vuoto.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La durata massima di una versione (in giorni, imposta a 0 per mantenere le versioni per sempre).",
"The number of days must be a number and cannot be blank.": "Il numero di giorni deve essere un numero e non può essere vuoto.",
@@ -346,6 +351,7 @@
"Versions": "Versioni",
"Versions Path": "Percorso Cartella Versioni",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Le versioni vengono eliminate automaticamente se superano la durata massima o il numero di file permessi in un determinato intervallo temporale.",
"Waiting to Clean": "In attesa di Pulizia",
"Waiting to Scan": "In attesa di Scansione",
"Waiting to Sync": "In attesa di Sincronizzazione",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Attenzione, questo percorso è una cartella superiore di una cartella esistente \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "file",
"full documentation": "documentazione completa",
"items": "elementi",
"seconds": "secondi",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vuole condividere la cartella \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "バグ",
"Changelog": "更新履歴",
"Clean out after": "以下の期間後に完全に削除する",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "接続に失敗した探索サーバーを確認するにはクリックしてください",
"Close": "閉じる",
"Command": "コマンド",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"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 cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "設定が保存されましたが、まだ有効になっていません。新しい設定を有効にするにはSyncthingを再起動してください。",
"The device ID cannot be blank.": "デバイスIDを入力してください。",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ここに入力するデバイスIDは、接続したい相手側デバイスの [メニュー]→[IDを表示] で確認できます。スペースとハイフンは入力しなくてもかまいません。",
@@ -305,6 +308,8 @@
"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.": "保存間隔は次の通りです。初めの1時間は30秒ごとに古いバージョンを保存します。同様に、初めの1日間は1時間ごと、初めの30日間は1日ごと、その後最大保存日数までは1週間ごとに、古いバージョンを保存します。",
"The following items could not be synchronized.": "以下の項目は同期できませんでした。",
"The following items were changed locally.": "The following items were changed locally.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"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 number of days must be a number and cannot be blank.": "日数は数値を指定してください。空欄にはできません。",
@@ -346,6 +351,7 @@
"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 Clean": "Waiting to Clean",
"Waiting to Scan": "スキャンの待機中",
"Waiting to Sync": "同期の待機中",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "警告: 入力されたパスは、設定済みのフォルダー「{{otherFolder}}」の親ディレクトリです。",
@@ -370,6 +376,7 @@
"files": "個のファイル",
"full documentation": "詳細なマニュアル",
"items": "項目",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} がフォルダー \"{{folder}}\" を共有するよう求めています。",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} がフォルダー「{{folderlabel}}」 ({{folder}}) を共有するよう求めています。"
}

View File

@@ -27,25 +27,27 @@
"Are you sure you want to remove device {%name%}?": "{{name}} 장치를 제거 하시겠습니까?",
"Are you sure you want to remove folder {%label%}?": "{{label}} 폴더를 제거 하시겠습니까?",
"Are you sure you want to restore {%count%} files?": "{{count}} 개의 파일을 복원 하시겠습니까?",
"Are you sure you want to upgrade?": "Are you sure you want to upgrade?",
"Are you sure you want to upgrade?": "업그레이드를 하시겠습니까?",
"Auto Accept": "자동 수락",
"Automatic Crash Reporting": "Automatic Crash Reporting",
"Automatic Crash Reporting": "자동 충돌 보고",
"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.",
"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": "버그",
"Changelog": "바뀐 점",
"Clean out after": "삭제 후",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "탐색 실패 보기",
"Close": "닫기",
"Command": "커맨드",
"Comment, when used at the start of a line": "명령행에서 시작을 할수 있어요.",
"Compression": "압축",
"Configured": "설정됨",
"Connected (Unused)": "Connected (Unused)",
"Connected (Unused)": "접속됨 (미사용)",
"Connection Error": "연결 에러",
"Connection Type": "연결 종류",
"Connections": "연결",
@@ -77,7 +79,7 @@
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).",
"Discard": "Discard",
"Disconnected": "연결 끊김",
"Disconnected (Unused)": "Disconnected (Unused)",
"Disconnected (Unused)": "연결 해제됨 (미사용)",
"Discovered": "탐색됨",
"Discovery": "탐색",
"Discovery Failures": "탐색 실패",
@@ -90,7 +92,7 @@
"Downloading": "다운로드 중",
"Edit": "편집",
"Editing {%path%}.": "{{path}} 수정하기.",
"Enable Crash Reporting": "Enable Crash Reporting",
"Enable Crash Reporting": "충돌 보고 활성화",
"Enable NAT traversal": "NAT traversal 활성화",
"Enable Relaying": "Relaying 활성화",
"Enabled": "활성화됨",
@@ -110,7 +112,7 @@
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "파일이 Syncthing에 의해서 교체되거나 삭제되면 .stversions 폴더에 있는 날짜가 바뀐 버전으로 이동됩니다.",
"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.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
"Filesystem Watcher Errors": "파일 시스템 감시 오류",
"Filter by date": "날짜별 정렬",
"Filter by name": "이름별 정렬",
"Folder": "폴더",
@@ -124,7 +126,7 @@
"GUI": "GUI",
"GUI Authentication Password": "GUI 인증 비밀번호",
"GUI Authentication User": "GUI 인증 사용자",
"GUI Authentication: Set User and Password": "GUI Authentication: Set User and Password",
"GUI Authentication: Set User and Password": "GUI 인증: 사용자와 비밀번호를 설정하세요",
"GUI Listen Address": "GUI 주소",
"GUI Theme": "GUI 테마",
"General": "일반",
@@ -139,8 +141,8 @@
"Ignore": "무시",
"Ignore Patterns": "패턴 무시",
"Ignore Permissions": "권한 무시",
"Ignored Devices": "Ignored Devices",
"Ignored Folders": "Ignored Folders",
"Ignored Devices": "무시된 기기",
"Ignored Folders": "무시된 폴더",
"Ignored at": "Ignored at",
"Incoming Rate Limit (KiB/s)": "다운로드 속도 제한 (KiB/S)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "잘못된 설정은 폴더의 컨텐츠를 훼손하거나 Syncthing의 오작동을 일으킬 수 있습니다.",
@@ -200,7 +202,7 @@
"Pause": "일시 중지",
"Pause All": "모두 일시 중지",
"Paused": "일시 중지됨",
"Paused (Unused)": "Paused (Unused)",
"Paused (Unused)": "정지됨 (미사용)",
"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",
@@ -242,7 +244,7 @@
"Scan Time Remaining": "탐색 남은 시간",
"Scanning": "탐색중",
"See external versioning help for supported templated command line parameters.": "지원되는 템플릿 명령 행 매개 변수에 대해서는 외부 버전 도움말을 참조하십시오.",
"Select All": "Select All",
"Select All": "모두 선택",
"Select a version": "버전 선택",
"Select additional devices to share this folder with.": "Select additional devices to share this folder with.",
"Select latest version": "가장 최신 버전 선택",
@@ -283,10 +285,10 @@
"Syncing": "동기화 중",
"Syncthing has been shut down.": "Syncthing이 종료되었습니다.",
"Syncthing includes the following software or portions thereof:": "Syncthing은 다음과 같은 소프트웨어나 그 일부를 포함합니다:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing는 MPL v2.0 으로 라이센스 된 무료 및 오픈 소스 소프트웨어입니다.",
"Syncthing is restarting.": "Syncthing이 재시작 중입니다.",
"Syncthing is upgrading.": "Syncthing이 업데이트 중입니다.",
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.",
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "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",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"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 cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "설정이 저장되었지만 활성화되지 않았습니다. 설정을 활성화 하려면 Syncthing을 다시 시작하세요.",
"The device ID cannot be blank.": "기기 ID는 비워 둘 수 없습니다.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "여기에 입력한 기기 ID가 다른 장치의 \"동작 - ID 보기\"에 표시됩니다. 공백과 하이픈은 세지 않습니다. 즉 무시됩니다.",
@@ -305,6 +308,8 @@
"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초마다 유지되며, 첫 하루 동안은 매 시간, 첫 한 달 동안은 매 일마다 유지됩니다. 그리고 최대 날짜까지는 버전이 매 주마다 유지됩니다.",
"The following items could not be synchronized.": "이 항목들은 동기화 할 수 없습니다.",
"The following items were changed locally.": "The following items were changed locally.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"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 number of days must be a number and cannot be blank.": "날짜는 숫자여야 하며 비워 둘 수 없습니다.",
@@ -324,7 +329,7 @@
"Time the item was last modified": "항목이 마지막으로 수정 된 시간",
"Trash Can File Versioning": "휴지통을 통한 파일 버전 관리",
"Type": "종류",
"UNIX Permissions": "UNIX Permissions",
"UNIX Permissions": "UNIX 권한",
"Unavailable": "불가능",
"Unavailable/Disabled by administrator or maintainer": "운영자 또는 관리자에 의해 불가능/비활성화 됨",
"Undecided (will prompt)": "Undecided (will prompt)",
@@ -346,6 +351,7 @@
"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 Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "경고, 이 경로는 현재 존재하는 폴더 \"{{otherFolder}}\" 의 상위 폴더 입니다.",
@@ -364,12 +370,13 @@
"You can read more about the two release channels at the link below.": "이 두 개의 출시 채널에 대해 아래 링크에서 자세하게 읽어 보실 수 있습니다.",
"You have no ignored devices.": "You have no ignored devices.",
"You have no ignored folders.": "You have no ignored folders.",
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
"You have unsaved changes. Do you really want to discard them?": "저장되지 않은 변경이 있습니다. 변경사항을 무시하시겠습니까?",
"You must keep at least one version.": "최소 한 개의 버전은 유지해야 합니다.",
"days": "일",
"files": "파일",
"full documentation": "전체 문서",
"items": "항목",
"seconds": "초",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 에서 폴더 \\\"{{folder}}\\\" 를 공유하길 원합니다.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 에서 폴더 \"{{folderLabel}}\" ({{folder}})를 공유하길 원합니다."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Klaidos",
"Changelog": "Pasikeitimai",
"Clean out after": "Išvalyti po",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Spustelėkite, norėdami pamatyti matomumo nesėkmes",
"Close": "Užverti",
"Command": "Komanda",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "Syncthing autoriai",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratoriaus sąsaja yra sukonfigūruota taip, kad be slaptažodžio leistų nuotolinę prieigą.",
"The aggregated statistics are publicly available at the URL below.": "Naudojimosi ataskaitą galite peržiūrėti žemiau nurodytu URL adresu.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Nauji nustatymai išsaugoti, bet neaktyvuoti. Perleiskite Syncthing programą iš naujo norėdami įgalinti naujus nustatymus.",
"The device ID cannot be blank.": "Įrenginio ID negali būti tuščias.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Įrenginio ID, kurį čia reikia įvesti, galite rasti „Veiksmai > Rodyti ID“ dialoge kitame įrenginyje. Tarpai ir brūkšneliai nebūtini (ignoruojami).",
@@ -305,6 +308,8 @@
"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.": "Šie pertraukų nustatymai naudojami: pirmą valandą versijos laikomos 30 sekundžių, pirmą dieną versijos laikomos valandą, pirmas 30 dienų versijos laikomos parą, kol nebus viršytas nustatytas maksimalus amžius.",
"The following items could not be synchronized.": "Nepavyko parsiųsti šių failų.",
"The following items were changed locally.": "Šie elementai buvo pakeisti vietoje.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "Maksimalus amžius turi būti skaitmuo ir negali būti tuščias laukelis.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimalus laikas kurį bus saugojama versija (dienomis, nustatykite 0 norėdami saugoti amžinai).",
"The number of days must be a number and cannot be blank.": "Dienų skaičius turi būti teigiamas skaičius.",
@@ -346,6 +351,7 @@
"Versions": "Versijos",
"Versions Path": "Kelias iki versijos",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versijos ištrinamos jei senesnės už nustatyta maksimalų amžių arba jei viršytas maksimalus failų skaičius per nustatytą laiko tarpą.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Laukiama nuskaityti",
"Waiting to Sync": "Laukiama sinchronizuoti",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Įspėjimas, šis kelias yra esamo aplanko \"{{otherFolder}}\" virškatalogis.",
@@ -370,6 +376,7 @@
"files": "failai",
"full documentation": "pilna dokumentacija",
"items": "įrašai",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} nori dalintis aplanku \"{{folder}}\"",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} nori dalintis aplanku \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Programfeil",
"Changelog": "Endringslogg",
"Clean out after": "Tøm etter",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Klikk for å se oppslagsfeil",
"Close": "Lukk",
"Command": "Kommando",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "Grensesnittet for administrering av Syncthing er satt til å tillate ekstern tilgang uten et passord.",
"The aggregated statistics are publicly available at the URL below.": "Innsamlet statistikk er åpent tilgjengelig via nettadressen angitt nedenfor.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Innstillingene har blitt lagret men ikke aktivert. Syncthing må starte på ny for å aktivere de nye innstillingene.",
"The device ID cannot be blank.": "Enhets-ID kan ikke være tom.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets-ID som skal skrives her kan du finne i menyen \"Handlinger\" > \"Vis ID\" på den andre enheten. Mellomrom og strek er valgfritt (ignoreres)",
@@ -305,6 +308,8 @@
"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.": "Følgende intervall blir brukt: Den første timen blir en versjon lagret hvert 30. sekund, den første dagen blir en versjon lagret hver time, de første 30 dagene blir en versjon lagret hver dag, og inntil maksimal levetid blir en versjon lagret hver uke.",
"The following items could not be synchronized.": "Følgende filer kunne ikke synkroniseres.",
"The following items were changed locally.": "Følgende elementer ble endret lokalt.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "Maksimal levetid må være et tall og kan ikke være tomt.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimal tid å beholde en versjon (i dager, sett til 0 for å beholde versjoner på ubegrenset tid).",
"The number of days must be a number and cannot be blank.": "Antall dager må være et tall og kan ikke være tomt.",
@@ -346,6 +351,7 @@
"Versions": "Versjoner",
"Versions Path": "Plassering av versjoner",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versjoner blir automatisk slettet når maksimal levetid er nådd eller når antall filer er oversteget.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Venter på å starte gjennomsøkning",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Advarsel, denne stien er en foreldremappe for en eksisterende mappe \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "filer",
"full documentation": "all dokumentasjon",
"items": "elementer",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappa \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} ønsker å dele mappa \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Bugs",
"Changelog": "Wijzigingenlogboek",
"Clean out after": "Opruimen na",
"Cleaning Versions": "Versies opruimen",
"Cleanup Interval": "Opruiminterval",
"Click to see discovery failures": "Klikken om detectieproblemen weer te geven",
"Close": "Sluiten",
"Command": "Opdracht",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "De Syncthing-auteurs",
"The Syncthing admin interface is configured to allow remote access without a password.": "De beheerdersinterface van Syncthing is ingesteld om externe toegang zonder wachtwoord toe te staan.",
"The aggregated statistics are publicly available at the URL below.": "De verzamelde statistieken zijn publiek beschikbaar op de onderstaande URL.",
"The cleanup interval cannot be blank.": "Het opruiminterval mag niet leeg zijn.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De configuratie is opslagen maar nog niet ingeschakeld. Syncthing moet opnieuw gestart worden om de nieuwe configuratie in te schakelen.",
"The device ID cannot be blank.": "De apparaat-ID mag niet leeg zijn.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "De hier in te vullen apparaat-ID kan gevonden worden in het venster \"Acties > ID weergeven\" op het andere apparaat. Spaties en streepjes zijn optioneel (en worden genegeerd).",
@@ -305,6 +308,8 @@
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De volgende intervallen worden gebruikt: het eerste uur worden versies iedere 30 seconden bewaard, de eerste dag worden versies ieder uur bewaard, de eerste 30 dagen worden versies iedere dag bewaard, tot de maximale leeftijd worden versies iedere week bewaard.",
"The following items could not be synchronized.": "De volgende items konden niet gesynchroniseerd worden.",
"The following items were changed locally.": "De volgende items werden lokaal gewijzigd.",
"The interval must be a positive number of seconds.": "Het interval moet een positief aantal seconden zijn.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Het interval, in seconden, voor het uitvoeren van opruiming in de versie-map. Nul om de regelmatige schoonmaak uit te schakelen.",
"The maximum age must be a number and cannot be blank.": "De maximumleeftijd moet een getal zijn en mag niet leeg zijn.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "De maximale tijd om een versie te bewaren (in dagen, instellen op 0 om versies voor altijd te bewaren).",
"The number of days must be a number and cannot be blank.": "Het aantal dagen moet een getal zijn en mag niet leeg zijn.",
@@ -312,8 +317,8 @@
"The number of old versions to keep, per file.": "Het aantal te bewaren oude versies, per bestand.",
"The number of versions must be a number and cannot be blank.": "Het aantal versies moet een getal zijn en mag niet leeg ziijn.",
"The path cannot be blank.": "Het pad mag niet leeg zijn.",
"The rate limit must be a non-negative number (0: no limit)": "De snelheidsbegrenzing moet een positief nummer zijn (0: geen begrenzing)",
"The rescan interval must be a non-negative number of seconds.": "Het interval voor opnieuw scannen moet een positief getal in seconden zijn.",
"The rate limit must be a non-negative number (0: no limit)": "De snelheidsbegrenzing moet een positief getal zijn (0: geen begrenzing)",
"The rescan interval must be a non-negative number of seconds.": "Het interval voor opnieuw scannen moet een positief aantal seconden zijn.",
"There are no devices to share this folder with.": "Er zijn geen apparaten om deze map mee te delen.",
"They are retried automatically and will be synced when the error is resolved.": "Ze worden automatisch opnieuw geprobeerd en zullen gesynchroniseerd worden wanneer de fout opgelost is.",
"This Device": "Dit apparaat",
@@ -346,6 +351,7 @@
"Versions": "Versies",
"Versions Path": "Pad voor versies",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versies worden automatisch verwijderd als deze ouder zijn dan de maximale leeftijd of als ze het maximaal aantal toegestane bestanden in een interval overschrijden.",
"Waiting to Clean": "Wachten om op te ruimen",
"Waiting to Scan": "Wachten om te scannen",
"Waiting to Sync": "Wachten om te synchroniseren",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Waarschuwing, dit pad is een bovenliggende map van een bestaande map \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "bestanden",
"full documentation": "volledige documentatie",
"items": "items",
"seconds": "seconden",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wil map \"{{folder}}\" delen.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wil map \"{{folderlabel}}\" ({{folder}}) delen."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Błędy",
"Changelog": "Historia zmian",
"Clean out after": "Uporządkuj",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Kliknij aby zobaczyć błędy odnajdywania",
"Close": "Zamknij",
"Command": "Polecenie",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "Interfejs administracyjny Syncthing jest skonfigurowany w sposób pozwalający na zdalny dostęp bez hasła.",
"The aggregated statistics are publicly available at the URL below.": "Zebrane statystyki są publicznie dostępne pod poniższym linkiem.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfiguracja została zapisana lecz nie jest aktywna. Syncthing musi zostać zrestartowany aby aktywować nową konfiguracje.",
"The device ID cannot be blank.": "ID urządzenia nie może być puste.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ID urządzenia może być znalezione w \"Akcja > Pokaż ID\" na innym urządzeniu. Spacje i myślniki są opcjonalne (są one ignorowane).",
@@ -305,6 +308,8 @@
"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.": "Następujący interwał jest używany: dla pierwszej godziny wersja jest zachowywana co 30 sekund, dla pierwszego dnia wersja jest zachowywana co godzinę, dla pierwszego miesiąca wersja jest zachowywana codziennie, aż do maksymalnego odstępu zapisywania wersji co tydzień.",
"The following items could not be synchronized.": "Następujące elementy nie mogły zostać zsynchronizowane.",
"The following items were changed locally.": "Następujące elementy zostały zmienione lokalnie.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "Maksymalny wiek musi być liczbą i nie może być pusty.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksymalny czas zachowania wersji (w dniach, ustaw 0 aby zachować na zawsze)",
"The number of days must be a number and cannot be blank.": "Ilość dni musi być dodatnia.",
@@ -346,6 +351,7 @@
"Versions": "Wersje",
"Versions Path": "Ścieżka wersji",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Wersje zostają automatycznie usunięte jeżeli są starsze niż maksymalny wiek lub przekraczają liczbę dopuszczalnych wersji.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Uwaga, ta ścieżka jest nadrzędnym folderem istniejącego folderu \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "pliki",
"full documentation": "pełna dokumentacja",
"items": "pozycji",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{folder}}\"",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} chce udostępnić folder \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Erros",
"Changelog": "Registro de alterações",
"Clean out after": "Limpar depois de",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Clique para ver as falhas na descoberta de dispositivos",
"Close": "Fechar",
"Command": "Comando",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "A interface de administração do Syncthing está configurada para permitir acesso remoto sem uma senha.",
"The aggregated statistics are publicly available at the URL below.": "As estatísticas agregadas estão disponíveis no endereço abaixo.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi salva mas ainda não foi ativada. O Syncthing precisa ser reiniciado para a ativação da nova configuração.",
"The device ID cannot be blank.": "O ID de dispositivo não pode ficar vazio.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "O ID do dispositivo a ser inserido aqui pode ser obtido no menu \"Ações > Mostrar ID\" do outro dispositivo. Espaços e hífens são opcionais (ignorados).",
@@ -305,6 +308,8 @@
"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.": "São utilizados os seguintes intervalos: na primeira hora é guardada uma versão a cada 30 segundos, no primeiro dia é guardada uma versão a cada hora, nos primeiros 30 dias é guardada uma versão por dia e, até que atinja a idade máxima, é guardada uma versão por semana.",
"The following items could not be synchronized.": "Os itens a seguir não puderam ser sincronizados.",
"The following items were changed locally.": "The following items were changed locally.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "A idade máxima deve ser um valor numérico. O campo não pode ficar vazio.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "O número máximo de dias em que uma versão é guardada. (Use 0 para manter para sempre).",
"The number of days must be a number and cannot be blank.": "O número de dias deve ser um número válido e não pode ficar em branco.",
@@ -346,6 +351,7 @@
"Versions": "Versões",
"Versions Path": "Caminho do versionamento",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são automaticamente apagadas se elas são mais antigas do que a idade máxima ou excederem o número de arquivos permitido em um intervalo.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Aviso: este caminho é o diretório pai da pasta \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "arquivos",
"full documentation": "documentação completa",
"items": "itens",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer compartilhar a pasta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quer compartilhar a pasta \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,13 +39,15 @@
"Bugs": "Erros",
"Changelog": "Registo de alterações",
"Clean out after": "Esvaziar ao fim de",
"Cleaning Versions": "Limpar versões",
"Cleanup Interval": "Intervalo entre limpezas",
"Click to see discovery failures": "Clique para ver as falhas da pesquisa",
"Close": "Fechar",
"Command": "Comando",
"Comment, when used at the start of a line": "Comentário, quando usado no início de uma linha",
"Compression": "Compressão",
"Configured": "Configurado",
"Connected (Unused)": "Ligado (não usado)",
"Connected (Unused)": "Conectado (não usado)",
"Connection Error": "Erro de ligação",
"Connection Type": "Tipo de ligação",
"Connections": "Ligações",
@@ -77,7 +79,7 @@
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Desactiva a comparação e a sincronização das permissões dos ficheiros. É útil em sistemas onde as permissões não existem ou são personalizadas (ex.: FAT, exFAT, Synology, Android).",
"Discard": "Descartar",
"Disconnected": "Desconectado",
"Disconnected (Unused)": "Desligado (não usado)",
"Disconnected (Unused)": "Desconectado (não usado)",
"Discovered": "Descoberto",
"Discovery": "Pesquisa",
"Discovery Failures": "Falhas da pesquisa",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "Os autores do Syncthing",
"The Syncthing admin interface is configured to allow remote access without a password.": "A interface de administração do Syncthing está configurada para permitir o acesso remoto sem pedir senha.",
"The aggregated statistics are publicly available at the URL below.": "As estatísticas agregadas estão publicamente disponíveis no URL abaixo.",
"The cleanup interval cannot be blank.": "O intervalo entre limpezas não pode estar vazio.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi gravada mas não activada. O Syncthing tem que reiniciar para activar a nova configuração.",
"The device ID cannot be blank.": "O ID do dispositivo não pode estar vazio.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "O ID do dispositivo a colocar aqui pode ser obtido no menu \"Acções > Mostrar ID\" do outro dispositivo. Espaços e hífenes são opcionais (ignorados).",
@@ -305,6 +308,8 @@
"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.": "São utilizados os seguintes intervalos: na primeira hora é guardada uma versão a cada 30 segundos, no primeiro dia é guardada uma versão a cada hora, nos primeiros 30 dias é guardada uma versão por dia e, até que atinja a idade máxima, é guardada uma versão por semana.",
"The following items could not be synchronized.": "Não foi possível sincronizar os elementos seguintes.",
"The following items were changed locally.": "Os itens seguintes foram alterados localmente.",
"The interval must be a positive number of seconds.": "O intervalo tem que ser um número positivo de segundos.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "O intervalo, em segundos, para executar limpezas na pasta das versões. Coloque zero para desactivar a limpeza periódica.",
"The maximum age must be a number and cannot be blank.": "A idade máxima tem que ser um número e não pode estar vazia.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Tempo máximo, em dias, para manter uma versão (use 0 para manter a versão para sempre).",
"The number of days must be a number and cannot be blank.": "O número de dias tem que ser um número e não pode estar em branco.",
@@ -346,6 +351,7 @@
"Versions": "Versões",
"Versions Path": "Caminho das versões",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são eliminadas automaticamente se forem mais antigas do que a idade máxima ou excederem o número máximo de ficheiros permitido num intervalo.",
"Waiting to Clean": "Aguardando a limpeza",
"Waiting to Scan": "Aguardando a verificação",
"Waiting to Sync": "Aguardando a sincronização",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Aviso: Este caminho é uma pasta mãe duma pasta \"{{otherFolder}}\" já existente.",
@@ -370,6 +376,7 @@
"files": "ficheiros",
"full documentation": "documentação completa",
"items": "itens",
"seconds": "segundos",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer partilhar a pasta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quer partilhar a pasta \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Ошибки",
"Changelog": "Журнал изменений",
"Clean out after": "Очистить после",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Щёлкните, чтобы посмотреть ошибки",
"Close": "Закрыть",
"Command": "Команда",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "Авторы 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.": "Агрегированные статистические данные общедоступны по ссылке ниже.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурация была сохранена, но не активирована. Syncthing должен быть перезапущен для применения новой конфигурации.",
"The device ID cannot be blank.": "ID устройства не может быть пустым.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор устройства можно найти в диалоге «Действия → Показать ID» на другом устройстве. Пробелы и дефисы вводить не обязательно (игнорируются).",
@@ -305,6 +308,8 @@
"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 interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"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 number of days must be a number and cannot be blank.": "Количество дней должно быть числом и не может быть пустым.",
@@ -346,6 +351,7 @@
"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 Clean": "Waiting to Clean",
"Waiting to Scan": "Ожидание сканирования",
"Waiting to Sync": "Ожидание синхронизации",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Внимание! Этот путь — родительская директория уже существующей папки «{{otherFolder}}».",
@@ -370,6 +376,7 @@
"files": "файлов",
"full documentation": "полная документация",
"items": "элементы",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой «{{folder}}».",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderlabel}}» ({{folder}})."
}

View File

@@ -39,13 +39,15 @@
"Bugs": "Chyby",
"Changelog": "Záznam zmien",
"Clean out after": "Vyčistenie po",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Kliknite pre zobrazenie zlyhaní zisťovania.",
"Close": "Zatvoriť",
"Command": "Príkaz",
"Comment, when used at the start of a line": "Komentár, keď použité na začiatku riadku",
"Compression": "Kompresia",
"Configured": "Nakonfigurované",
"Connected (Unused)": "Connected (Unused)",
"Connected (Unused)": "Pripojené (Nepoužité)",
"Connection Error": "Chyba pripojenia",
"Connection Type": "Typ pripojenia",
"Connections": "Spojenia",
@@ -77,7 +79,7 @@
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).",
"Discard": "Zahodiť",
"Disconnected": "Odpojené",
"Disconnected (Unused)": "Disconnected (Unused)",
"Disconnected (Unused)": "Odpojené (Nepoužité)",
"Discovered": "Zistené",
"Discovery": "Zisťovanie",
"Discovery Failures": "Zlyhania zisťovania",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The aggregated statistics are publicly available at the URL below.": "Súhrnné štatistiky sú verejne dostupné na uvedenej URL.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
"The device ID cannot be blank.": "ID zariadenia nemôže byť prázdne.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ID zariadenie pre vloženie môžete nájsť na druhom zariadení v dialógu \"Akcia > Zobraziť ID\". Medzery a pomlčky sú voliteľné (ignorované).",
@@ -305,6 +308,8 @@
"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.": "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.",
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The following items were changed locally.": "Tieto položky boli zmenené lokálne",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "Maximálny vek musí byť číslo a nemôže byť prázdne.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
"The number of days must be a number and cannot be blank.": "Počet dní musí byť číslo a nemôže byť prázdny.",
@@ -346,6 +351,7 @@
"Versions": "Verzie",
"Versions Path": "Cesta k verziám",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Verzie sú automaticky zmazané ak sú staršie ako je maximálny časový limit alebo presiahnu počet súborov povolených v danom intervale.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a parent directory of an existing folder \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "súbory",
"full documentation": "úplná dokumntácia",
"items": "položiek",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce zdieľať adresár \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} chce zdieľať adresár \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Felrapporter",
"Changelog": "Ändringslogg",
"Clean out after": "Rensa efteråt",
"Cleaning Versions": "Rensningsversioner",
"Cleanup Interval": "Rensningsintervall",
"Click to see discovery failures": "Klicka för att se upptäcktsmisslyckanden",
"Close": "Stäng",
"Command": "Kommando",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "Syncthing-upphovsmän",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratör gränssnittet är konfigurerat för att tillåta fjärrtillträde utan ett lösenord.",
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgänglig på webbadressen nedan.",
"The cleanup interval cannot be blank.": "Rensningsintervallet kan inte vara tomt.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
"The device ID cannot be blank.": "Enhets-ID kan inte vara tomt.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets-ID som behövs här kan du hitta i \"Åtgärder > Visa ID\"-dialogrutan på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
@@ -305,6 +308,8 @@
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De följande intervallen används: varje 30 sekunder under den första timmen; varje timme under den första dagen; varje dag för de första 30 dagarna; varje vecka tills den maximala åldersgränsen uppnås.",
"The following items could not be synchronized.": "Följande objekt kunde inte synkroniseras.",
"The following items were changed locally.": "Följande objekt ändrades lokalt.",
"The interval must be a positive number of seconds.": "Intervallet måste vara ett positivt antal sekunder.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Intervallet, i sekunder, för att rensa i versionskatalogen. Noll för att inaktivera periodisk rensning.",
"The maximum age must be a number and cannot be blank.": "Åldersgränsen måste vara ett tal och kan inte lämnas tomt.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tiden att behålla en version (i dagar, ställ in på 0 för att behålla versioner för alltid).",
"The number of days must be a number and cannot be blank.": "Antalet dagar måste vara en siffra och får inte vara tomt.",
@@ -346,6 +351,7 @@
"Versions": "Versioner",
"Versions Path": "Sökväg för versioner",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i intervallet.",
"Waiting to Clean": "Väntar på att rensa",
"Waiting to Scan": "Väntar på att skanna",
"Waiting to Sync": "Väntar på att synkronisera",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en överordnad mapp av en befintlig mapp \"{{otherFolder}}\".",
@@ -370,6 +376,7 @@
"files": "filer",
"full documentation": "fullständig dokumentation",
"items": "objekt",
"seconds": "sekunder",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela mapp \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela mapp \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Yazılım Hataları",
"Changelog": "Değişim Günlüğü",
"Clean out after": "Şundan sonra temizle",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Keşfetme hatalarını görmek için tıklayınız",
"Close": "Kapat",
"Command": "Komut",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing yönetici arayüzü parolasız olarak uzaktan erişime izin verilecek şekilde yapılandırıldı.",
"The aggregated statistics are publicly available at the URL below.": "Toplanan istatistikler halka açık biçimde aşağıdaki adrestedir.",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Yapılandırma kaydedildi ancak etkinleştirilmedi. Etkinleştirmek için Syncthing yeniden başlatılmalı.",
"The device ID cannot be blank.": "Aygıt ID değeri boş bırakılamaz.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Buraya girilecek olan aygıt ID'si diğer aygıtlarda \"Eylemler > ID Göster\" penceresinde bulunabilir. Boşluklar ve çizgiler isteğe bağlıdır (gözardı edilmiş).",
@@ -305,6 +308,8 @@
"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.": "Kullanılan zaman aralıkları: ilk bir saat zarfında her 30 saniyede bir, ilk gün zarfında saatte bir, ilk 30 gün zarfında her gün, azami süreye kadar geçen zamanda ise her hafta yeni bir sürüm değeri oluşturulur/tutulur.",
"The following items could not be synchronized.": "Aşağıdaki ögelerin eşzamanlama işlemi gerçekleştirilemedi.",
"The following items were changed locally.": "Aşağıdaki öğeler yerel olarak değiştirildi.",
"The interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"The maximum age must be a number and cannot be blank.": "Azami süre tanımı boş bırakılmamalı ve bir sayı olarak tanımlanmalıdır.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Bir sürümün tutulması için belirlenen azami süre (sürümleri sürekli olarak tutabilmek için 0 değeri atayın)",
"The number of days must be a number and cannot be blank.": "Gün sayısı boş bırakılmamalı ve bir sayı olarak tanımlanmalıdır.",
@@ -346,6 +351,7 @@
"Versions": "Sürümler",
"Versions Path": "Sürümler için Klasör Yolu",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Sürümler, tanımlı azami süre veya belirlenen zaman aralığı için izin verilen dosya sayısıılmışsa kendiliğinden silinir.",
"Waiting to Clean": "Waiting to Clean",
"Waiting to Scan": "Tarama için Bekliyor",
"Waiting to Sync": "Senkronizasyon için Bekliyor",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Uyarı ! Bu yol varolan \"{{otherFolder}}\" klasörünün üst dizinidir.",
@@ -370,6 +376,7 @@
"files": "dosyalar",
"full documentation": "belgelendirme içeriğinin tümü",
"items": "öğeler",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} \"{{folder}}\" klasörünü paylaşmak istiyor.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} \"{{folderlabel}}\" ({{folder}}) klasörünü paylaşmak istiyor."
}

View File

@@ -39,6 +39,8 @@
"Bugs": "Помилки",
"Changelog": "Перелік змін",
"Clean out after": "Очистити після",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Інтервал очищення",
"Click to see discovery failures": "Клікніть, щоб переглянути помилки виявлення",
"Close": "Закрити",
"Command": "Команда",
@@ -291,9 +293,10 @@
"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": "Поверніть мене назад",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Адреса графічного інтерфейсу користувача перевизначена стартовими налаштуваннями. Зміни зроблені тут не матимуть ефекту доки існує перевизначення.",
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing Authors": "Автори 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.": "Зібрана статистика публічно доступна за наступним посиланням.",
"The cleanup interval cannot be blank.": "Інтервал очищення не може бути порожнім.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфігурацію збережено, але не активовано. Необхідно перезапустити Syncthing для того, щоби активувати нову конфігурацію.",
"The device ID cannot be blank.": "ID пристрою не може бути порожнім.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ID пристрою, який необхідно додати. Може бути знайдений у вікні \"Дії > Показати ID\" в меню іншого пристрою. Пробіли та тире необов'язкові (будуть проігноровні).",
@@ -305,6 +308,8 @@
"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 interval must be a positive number of seconds.": "Інтервал повинен бути додатною кількістю секунд.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "Інтервал, в секундах, для запуску очищення в директорії версій. Нуль для вимкнення періодичної чистки.",
"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 number of days must be a number and cannot be blank.": "Кількість днів має бути числом і не може бути порожнім.",
@@ -346,6 +351,7 @@
"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 Clean": "Очікування очищення",
"Waiting to Scan": "Очікування сканування",
"Waiting to Sync": "Очікування синхронізації",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Увага, цей шлях є батьківським каталогом директорії \"{{otherFolder}}\", що й так синхронізується .",
@@ -370,6 +376,7 @@
"files": "файли",
"full documentation": "повна документація",
"items": "елементи",
"seconds": "секунд",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хоче поділитися директорією \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} хоче поділитися директорією \"{{folderLabel}}\" ({{folder}})."
}

View File

@@ -39,13 +39,15 @@
"Bugs": "问题回报",
"Changelog": "更新日志",
"Clean out after": "在该时间后清除",
"Cleaning Versions": "清除版本",
"Cleanup Interval": "清除间隔",
"Click to see discovery failures": "点击查看设备发现错误",
"Close": "关闭",
"Command": "命令",
"Comment, when used at the start of a line": "注释,在行首使用",
"Compression": "压缩",
"Configured": "已配置",
"Connected (Unused)": "Connected (Unused)",
"Connected (Unused)": "已连接(未使用)",
"Connection Error": "连接出错",
"Connection Type": "连接类型",
"Connections": "连接",
@@ -74,10 +76,10 @@
"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:": "已禁用定期扫描但设置更改监视失败,正在以每 1m 一次重试:",
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).",
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "禁用比较和同步文件权限。 适用于不存在或自定义权限的系统(例如FATexFATSynologyAndroid)。",
"Discard": "丢弃",
"Disconnected": "连接已断开",
"Disconnected (Unused)": "Disconnected (Unused)",
"Disconnected (Unused)": "断开连接(未使用)",
"Discovered": "已发现",
"Discovery": "设备发现",
"Discovery Failures": "设备发现错误",
@@ -124,7 +126,7 @@
"GUI": "图形用户界面",
"GUI Authentication Password": "图形管理界面密码",
"GUI Authentication User": "图形管理界面用户名",
"GUI Authentication: Set User and Password": "GUI Authentication: Set User and Password",
"GUI Authentication: Set User and Password": "GUI身份验证:设置用户和密码",
"GUI Listen Address": "GUI 监听地址",
"GUI Theme": "GUI 主题",
"General": "常规",
@@ -135,7 +137,7 @@
"Help": "帮助",
"Home page": "主页",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "我们已经为您关闭了自动崩溃报告发送功能,因为您当前的设置显示您可能并不想启用该功能。",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "如果要阻止此计算机上的其他用户访问Syncthing并通过它访问文件请考虑设置身份验证。",
"Ignore": "忽略",
"Ignore Patterns": "忽略模式",
"Ignore Permissions": "忽略文件权限",
@@ -200,7 +202,7 @@
"Pause": "暂停",
"Pause All": "全部暂停",
"Paused": "已暂停",
"Paused (Unused)": "Paused (Unused)",
"Paused (Unused)": "已暂停(未使用)",
"Pending changes": "待定的更改",
"Periodic scanning at given interval and disabled watching for changes": "正以给定的间隔定期扫描并已禁用更改监视",
"Periodic scanning at given interval and enabled watching for changes": "正以给定的间隔定期扫描并已启用更改监视",
@@ -291,9 +293,10 @@
"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": "带我回去",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "GUI 地址已被启动选项覆盖。当覆盖存在时,此处的更改就不会生效。",
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing Authors": "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 cleanup interval cannot be blank.": "清理间隔不能为空。",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "设置已经保存但是还未生效。Syncthing 需要重启以启用新的设置。",
"The device ID cannot be blank.": "设备 ID 不能为空。",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "在这里所需要输入的设备 ID可以在目标设备的“操作->显示 ID”中看到。空格和横线可选将会被忽略。",
@@ -305,6 +308,8 @@
"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.": "保留的历史版本会遵循以下条件:最近一小时内的历史版本,更新间隔小于三十秒的仅保留一份。最近一天内的历史版本,更新间隔小于一小时的仅保留一份。最近一个月内的历史版本,更新间隔小于一天的仅保留一份。距离现在超过一个月且小于最长保留时间的,更新间隔小于一周的仅保留一份。",
"The following items could not be synchronized.": "下列项目无法被同步。",
"The following items were changed locally.": "下列项目存在本地更改。",
"The interval must be a positive number of seconds.": "间隔必须为正数秒。",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "在版本目录中运行清理的间隔。0表示禁用定期清除。",
"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 number of days must be a number and cannot be blank.": "天数必须为数字,且不能为空。",
@@ -341,11 +346,12 @@
"Uptime": "已启动",
"Usage reporting is always enabled for candidate releases.": "发布候选版总是会启用使用报告。",
"Use HTTPS for GUI": "使用加密连接到图形管理页面",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Username/Password has not been set for the GUI authentication. Please consider setting it up.",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "尚未为GUI身份验证设置用户名/密码。 请考虑进行设置。",
"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 Clean": "等待清除",
"Waiting to Scan": "等待扫描",
"Waiting to Sync": "等待同步",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "警告,该路径是已有文件夹\"{{otherFolder}}\"的上级目录。",
@@ -370,6 +376,7 @@
"files": "文件",
"full documentation": "完整文档",
"items": "条目",
"seconds": "秒",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想将 “{{folder}}” 文件夹共享给您。",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 想要共享 \"{{folderlabel}}\" ({{folder}}) 文件夹给您。"
}

View File

@@ -39,6 +39,8 @@
"Bugs": "程式錯誤",
"Changelog": "更新日誌",
"Clean out after": "於之後清空",
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "點擊以查閱失敗的探索",
"Close": "關閉",
"Command": "指令",
@@ -294,6 +296,7 @@
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing 管理介面被設定允許無密碼的遠端存取。",
"The aggregated statistics are publicly available at the URL below.": "匯總統計資訊可於下方網址取得。",
"The cleanup interval cannot be blank.": "The cleanup interval cannot be blank.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "組態已經儲存但尚未啟用。Syncthing 必須重新啟動以便啟用新的組態。",
"The device ID cannot be blank.": "裝置識別碼不能為空白。",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "其它裝置的裝置識別碼可在它們的 \"操作 > 顯示識別碼\" 對話框找到。空白及連接符號可省略。",
@@ -305,6 +308,8 @@
"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 interval must be a positive number of seconds.": "The interval must be a positive number of seconds.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.",
"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 number of days must be a number and cannot be blank.": "天數必須必須為一個數字且不得為空。",
@@ -346,6 +351,7 @@
"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 Clean": "Waiting to Clean",
"Waiting to Scan": "Waiting to Scan",
"Waiting to Sync": "Waiting to Sync",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "警告,此路徑是現存資料夾 \"{{otherFolder}}\" 的上級目錄。",
@@ -370,6 +376,7 @@
"files": "個檔案",
"full documentation": "完整說明文件",
"items": "個項目",
"seconds": "seconds",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想要分享資料夾 \"{{folder}}\"。",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 想要分享資料夾 \"{{folderlabel}}\" ({{folder}})。"
}

View File

@@ -456,7 +456,7 @@
</div>
</td>
</tr>
<tr ng-if="folder.order != 'random'">
<tr ng-if="folder.order != 'random' && folder.type != 'sendonly'">
<th><span class="fas fa-fw fa-sort"></span>&nbsp;<span translate>File Pull Order</span></th>
<td class="text-right" ng-switch="folder.order">
<span ng-switch-when="random" translate>Random</span>

View File

@@ -19,7 +19,7 @@
<h4 class="text-center" translate>The Syncthing Authors</h4>
<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, Evgeny Kuznetsov, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, dependabot-preview[bot], greatroar, Aaron Bieber, Adam Piggott, Adel Qalieh, Alan Pope, Alberto Donato, Alessandro G., Alex Xu, Aman Gupta, Andrew Dunham, Andrew Rabert, Andrey D, André Colomb, Anjan Momi, Antoine Lamielle, Aranjedeath, Arkadiusz Tymiński, Arthur Axel fREW Schmidt, Artur Zubilewicz, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benno Fünfstück, Benny Ng, Boqin Qin, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Chris Tonkinson, Colin Kennedy, Cromefire_, Cyprien Devillez, Dale Visser, Dan, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Han Boetes, HansK-p, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Ilya Brin, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jack Croft, Jacob, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jonathan Cross, Jose Manuel Delicado, Jörg Thalheim, Jędrzej Kula, Kalle Laine, Karol Różycki, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., Kurt Fitzner, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Lukas Lihotzki, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Mateusz Naściszewski, Mateusz Ż, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max Schulze, MaximAL, Maxime Thirouin, Michael Jephcote, Michael Rienstra, Michael Tilli, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, Nicholas Rishel, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, Otiel, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, 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, Robin Schoonover, Roman Zaynetdinov, Ross Smith II, Ruslan Yevdokymov, Sacheendra Talluri, Scott Klupfel, Shaarad Dalvi, Simon Mwepu, Sly_tom_cat, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tomasz Wilczyński, Tommy Thorn, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, Vladimir Rusinov, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, boomsquared, chenrui, chucic, dependabot[bot], derekriemer, desbma, georgespatton, ghjklw, janost, jaseg, jelle van der Waa, klemens, marco-m, mv1005, otbutz, perewa, rubenbe, wangguoliang, xarx00, xjtdy888, 佛跳墙
Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Evgeny Kuznetsov, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, dependabot-preview[bot], greatroar, Aaron Bieber, Adam Piggott, Adel Qalieh, Alan Pope, Alberto Donato, Alessandro G., Alex Lindeman, Alex Xu, Aman Gupta, Andrew Dunham, Andrew Rabert, Andrey D, André Colomb, Anjan Momi, Antoine Lamielle, Aranjedeath, Arkadiusz Tymiński, Arthur Axel fREW Schmidt, Artur Zubilewicz, Aurélien Rainone, BAHADIR YILMAZ, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benno Fünfstück, Benny Ng, Boqin Qin, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Chris Tonkinson, Colin Kennedy, Cromefire_, Cyprien Devillez, Dale Visser, Dan, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Han Boetes, HansK-p, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Ilya Brin, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jack Croft, Jacob, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jonathan Cross, Jose Manuel Delicado, Jörg Thalheim, Jędrzej Kula, Kalle Laine, Karol Różycki, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin Bushiri, Kevin White, Jr., Kurt Fitzner, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Lukas Lihotzki, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Marcus Legendre, Mario Majila, Mark Pulford, Mateusz Naściszewski, Mateusz Ż, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max Schulze, MaximAL, Maxime Thirouin, Michael Jephcote, Michael Rienstra, Michael Tilli, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, Nicholas Rishel, Nico Stapelbroek, Nicolas Braud-Santoni, Nicolas Perraut, Niels Peter Roest, Nils Jakobi, NinoM4ster, Nitroretro, NoLooseEnds, Oliver Freyermuth, Otiel, Oyebanji Jacob Mayowa, Pablo, Pascal Jungblut, Paul Brit, 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, Robin Schoonover, Roman Zaynetdinov, Ross Smith II, Ruslan Yevdokymov, Sacheendra Talluri, Scott Klupfel, Shaarad Dalvi, Simon Mwepu, Sly_tom_cat, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tom Jakubowski, Tomasz Wilczyński, Tommy Thorn, Tully Robinson, Tyler Brazier, Tyler Kropp, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, Vladimir Rusinov, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, boomsquared, chenrui, chucic, dependabot[bot], derekriemer, desbma, georgespatton, ghjklw, janost, jaseg, jelle van der Waa, klemens, marco-m, mv1005, otbutz, perewa, rubenbe, wangguoliang, xarx00, xjtdy888, 佛跳墙
</div>
</div>
<hr />

View File

@@ -77,7 +77,7 @@ type service struct {
eventSubs map[events.EventType]events.BufferedSubscription
eventSubsMut sync.Mutex
evLogger events.Logger
discoverer discover.CachingMux
discoverer discover.Manager
connectionsService connections.Service
fss model.FolderSummaryService
urService *ur.Service
@@ -107,7 +107,7 @@ type Service interface {
WaitForStart() error
}
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.CachingMux, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, contr Controller, noUpgrade bool) Service {
func New(id protocol.DeviceID, cfg config.Wrapper, assetDir, tlsDefaultCommonName string, m model.Model, defaultSub, diskSub events.BufferedSubscription, evLogger events.Logger, discoverer discover.Manager, connectionsService connections.Service, urService *ur.Service, fss model.FolderSummaryService, errors, systemLog logger.Recorder, contr Controller, noUpgrade bool) Service {
s := &service{
id: id,
cfg: cfg,
@@ -149,7 +149,7 @@ func (s *service) getListener(guiCfg config.GUIConfiguration) (net.Listener, err
// If the certificate has expired or will expire in the next month, fail
// it and generate a new one.
if err == nil {
err = checkExpiry(cert)
err = shouldRegenerateCertificate(cert)
}
if err != nil {
l.Infoln("Loading HTTPS certificate:", err)
@@ -1736,7 +1736,11 @@ func addressIsLocalhost(addr string) bool {
}
}
func checkExpiry(cert tls.Certificate) error {
// shouldRegenerateCertificate checks for certificate expiry or other known
// issues with our API/GUI certificate and returns either nil (leave the
// certificate alone) or an error describing the reason the certificate
// should be regenerated.
func shouldRegenerateCertificate(cert tls.Certificate) error {
leaf := cert.Leaf
if leaf == nil {
// Leaf can be nil or not, depending on how parsed the certificate
@@ -1752,12 +1756,21 @@ func checkExpiry(cert tls.Certificate) error {
}
}
if leaf.Subject.String() != leaf.Issuer.String() ||
len(leaf.DNSNames) != 0 || len(leaf.IPAddresses) != 0 {
// The certificate is not self signed, or has DNS/IP attributes we don't
if leaf.Subject.String() != leaf.Issuer.String() || len(leaf.IPAddresses) != 0 {
// The certificate is not self signed, or has IP attributes we don't
// add, so we leave it alone.
return nil
}
if len(leaf.DNSNames) > 1 {
// The certificate has more DNS SANs attributes than we ever add, so
// we leave it alone.
return nil
}
if len(leaf.DNSNames) == 1 && leaf.DNSNames[0] != leaf.Issuer.CommonName {
// The one SAN is different from the issuer, so it's not one of our
// newer self signed certificates.
return nil
}
if leaf.NotAfter.Before(time.Now()) {
return errors.New("certificate has expired")

View File

@@ -1136,7 +1136,7 @@ func TestPrefixMatch(t *testing.T) {
}
}
func TestCheckExpiry(t *testing.T) {
func TestShouldRegenerateCertificate(t *testing.T) {
dir, err := ioutil.TempDir("", "syncthing-test")
if err != nil {
t.Fatal(err)
@@ -1149,7 +1149,7 @@ func TestCheckExpiry(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := checkExpiry(crt); err == nil {
if err := shouldRegenerateCertificate(crt); err == nil {
t.Error("expected expiry error")
}
@@ -1158,7 +1158,7 @@ func TestCheckExpiry(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := checkExpiry(crt); err != nil {
if err := shouldRegenerateCertificate(crt); err != nil {
t.Error("expected no error:", err)
}
@@ -1168,7 +1168,7 @@ func TestCheckExpiry(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := checkExpiry(crt); err == nil {
if err := shouldRegenerateCertificate(crt); err == nil {
t.Error("expected expiry error")
}
}

View File

@@ -8,7 +8,6 @@ package api
import (
"context"
"time"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/protocol"
@@ -43,10 +42,7 @@ func (m *mockedCachingMux) Cache() map[protocol.DeviceID]discover.CacheEntry {
return nil
}
// from events.CachingMux
func (m *mockedCachingMux) Add(finder discover.Finder, cacheTime, negCacheTime time.Duration) {
}
// from events.Manager
func (m *mockedCachingMux) ChildErrors() map[string]error {
return nil

View File

@@ -12,6 +12,7 @@ import (
"os"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"time"
@@ -23,6 +24,7 @@ var (
Host = "unknown"
User = "unknown"
Stamp = "0"
Tags = ""
// Static
Codename = "Fermium Flea"
@@ -34,9 +36,6 @@ var (
IsBeta bool
LongVersion string
// Set by Go build tags
Tags []string
allowedVersionExp = regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z0-9]+)*(\.\d+)*(\+\d+-g[0-9a-f]+)?(-[^\s]+)?$`)
envTags = []string{
@@ -88,13 +87,19 @@ func LongVersionFor(program string) string {
// This string and date format is essentially part of our external API. Never change it.
date := Date.UTC().Format("2006-01-02 15:04:05 MST")
v := fmt.Sprintf(`%s %s "%s" (%s %s-%s) %s@%s %s`, program, Version, Codename, runtime.Version(), runtime.GOOS, runtime.GOARCH, User, Host, date)
tags := strings.Split(Tags, ",")
if len(tags) == 1 && tags[0] == "" {
tags = tags[:0]
}
for _, envVar := range envTags {
if os.Getenv(envVar) != "" {
Tags = append(Tags, strings.ToLower(envVar))
tags = append(tags, strings.ToLower(envVar))
}
}
if len(Tags) > 0 {
v = fmt.Sprintf("%s [%s]", v, strings.Join(Tags, ", "))
if len(tags) > 0 {
sort.Strings(tags)
v = fmt.Sprintf("%s [%s]", v, strings.Join(tags, ", "))
}
return v
}

View File

@@ -9,5 +9,9 @@
package build
func init() {
Tags = append(Tags, "race")
if Tags == "" {
Tags = "race"
} else {
Tags = Tags + ",race"
}
}

View File

@@ -31,7 +31,7 @@ import (
const (
OldestHandledVersion = 10
CurrentVersion = 31
CurrentVersion = 32
MaxRescanIntervalS = 365 * 24 * 60 * 60
)

View File

@@ -23,7 +23,6 @@ import (
"github.com/d4l3k/messagediff"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -127,16 +126,10 @@ func TestDeviceConfig(t *testing.T) {
},
WeakHashThresholdPct: 25,
MarkerName: DefaultMarkerName,
JunctionsAsDirs: true,
},
}
// The cachedFilesystem will have been resolved to an absolute path,
// depending on where the tests are running. Zero it out so we don't
// fail based on that.
for i := range cfg.Folders {
cfg.Folders[i].cachedFilesystem = nil
}
expectedDevices := []DeviceConfiguration{
{
DeviceID: device1,
@@ -466,6 +459,7 @@ func TestFolderCheckPath(t *testing.T) {
if err != nil {
t.Fatal(err)
}
testFs := fs.NewFilesystem(fs.FilesystemTypeBasic, n)
err = os.MkdirAll(filepath.Join(n, "dir", ".stfolder"), os.FileMode(0777))
if err != nil {
@@ -490,7 +484,7 @@ func TestFolderCheckPath(t *testing.T) {
},
}
err = osutil.DebugSymlinkForTestsOnly(filepath.Join(n, "dir"), filepath.Join(n, "link"))
err = fs.DebugSymlinkForTestsOnly(testFs, testFs, "dir", "link")
if err == nil {
t.Log("running with symlink check")
testcases = append(testcases, struct {

View File

@@ -61,8 +61,9 @@ type FolderConfiguration struct {
DisableFsync bool `xml:"disableFsync" json:"disableFsync"`
BlockPullOrder BlockPullOrder `xml:"blockPullOrder" json:"blockPullOrder"`
CopyRangeMethod fs.CopyRangeMethod `xml:"copyRangeMethod" json:"copyRangeMethod" default:"standard"`
CaseSensitiveFS bool `xml:"caseSensitiveFS" json:"caseSensitiveFS"`
JunctionsAsDirs bool `xml:"junctionsAsDirs" json:"junctionsAsDirs"`
cachedFilesystem fs.Filesystem
cachedModTimeWindow time.Duration
DeprecatedReadOnly bool `xml:"ro,attr,omitempty" json:"-"`
@@ -101,11 +102,15 @@ func (f FolderConfiguration) Copy() FolderConfiguration {
func (f FolderConfiguration) Filesystem() fs.Filesystem {
// This is intentionally not a pointer method, because things like
// cfg.Folders["default"].Filesystem() should be valid.
if f.cachedFilesystem == nil {
l.Infoln("bug: uncached filesystem call (should only happen in tests)")
return fs.NewFilesystem(f.FilesystemType, f.Path)
var opts []fs.Option
if f.FilesystemType == fs.FilesystemTypeBasic && f.JunctionsAsDirs {
opts = append(opts, fs.WithJunctionsAsDirs())
}
return f.cachedFilesystem
filesystem := fs.NewFilesystem(f.FilesystemType, f.Path, opts...)
if !f.CaseSensitiveFS {
filesystem = fs.NewCaseFilesystem(filesystem)
}
return filesystem
}
func (f FolderConfiguration) ModTimeWindow() time.Duration {
@@ -210,8 +215,6 @@ func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
}
func (f *FolderConfiguration) prepare() {
f.cachedFilesystem = fs.NewFilesystem(f.FilesystemType, f.Path)
if f.RescanIntervalS > MaxRescanIntervalS {
f.RescanIntervalS = MaxRescanIntervalS
} else if f.RescanIntervalS < 0 {
@@ -263,7 +266,6 @@ func (f FolderConfiguration) RequiresRestartOnly() FolderConfiguration {
// Manual handling for things that are not taken care of by the tag
// copier, yet should not cause a restart.
copy.cachedFilesystem = nil
blank := FolderConfiguration{}
util.CopyMatchingTag(&blank, &copy, "restart", func(v string) bool {

View File

@@ -25,6 +25,7 @@ import (
// update the config version. The order of migrations doesn't matter here,
// put the newest on top for readability.
var migrations = migrationSet{
{32, migrateToConfigV32},
{31, migrateToConfigV31},
{30, migrateToConfigV30},
{29, migrateToConfigV29},
@@ -91,6 +92,12 @@ func migrateToConfigV31(cfg *Configuration) {
cfg.Options.UnackedNotificationIDs = append(cfg.Options.UnackedNotificationIDs, "authenticationUserAndPassword")
}
func migrateToConfigV32(cfg *Configuration) {
for i := range cfg.Folders {
cfg.Folders[i].JunctionsAsDirs = true
}
}
func migrateToConfigV30(cfg *Configuration) {
// The "max concurrent scans" option is now spelled "max folder concurrency"
// to be more general.
@@ -215,7 +222,7 @@ func migrateToConfigV18(cfg *Configuration) {
// Do channel selection for existing users. Those who have auto upgrades
// and usage reporting on default to the candidate channel. Others get
// stable.
if cfg.Options.URAccepted > 0 && cfg.Options.AutoUpgradeIntervalH > 0 {
if cfg.Options.URAccepted > 0 && cfg.Options.AutoUpgradeEnabled() {
cfg.Options.UpgradeToPreReleases = true
}

View File

@@ -17,11 +17,11 @@ import (
type OptionsConfiguration struct {
RawListenAddresses []string `xml:"listenAddress" json:"listenAddresses" default:"default"`
RawGlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" default:"default" restart:"true"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true" restart:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true" restart:"true"`
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027" restart:"true"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027" restart:"true"`
RawGlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" default:"default"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027"`
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
@@ -38,15 +38,15 @@ type OptionsConfiguration struct {
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"` // usage reporting URL
URPostInsecurely bool `xml:"urPostInsecurely" json:"urPostInsecurely" default:"false"` // For testing
URInitialDelayS int `xml:"urInitialDelayS" json:"urInitialDelayS" default:"1800"`
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true" restart:"true"`
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12" restart:"true"` // 0 for off
UpgradeToPreReleases bool `xml:"upgradeToPreReleases" json:"upgradeToPreReleases" restart:"true"` // when auto upgrades are enabled
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"false" restart:"true"`
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12"` // 0 for off
UpgradeToPreReleases bool `xml:"upgradeToPreReleases" json:"upgradeToPreReleases"` // when auto upgrades are enabled
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"false"`
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
MinHomeDiskFree Size `xml:"minHomeDiskFree" json:"minHomeDiskFree" default:"1 %"`
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://upgrades.syncthing.net/meta.json" restart:"true"`
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://upgrades.syncthing.net/meta.json"`
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
OverwriteRemoteDevNames bool `xml:"overwriteRemoteDeviceNamesOnConnect" json:"overwriteRemoteDeviceNamesOnConnect" default:"false"`
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
@@ -56,11 +56,11 @@ type OptionsConfiguration struct {
SetLowPriority bool `xml:"setLowPriority" json:"setLowPriority" default:"true"`
RawMaxFolderConcurrency int `xml:"maxFolderConcurrency" json:"maxFolderConcurrency"`
CRURL string `xml:"crashReportingURL" json:"crURL" default:"https://crash.syncthing.net/newcrash"` // crash reporting URL
CREnabled bool `xml:"crashReportingEnabled" json:"crashReportingEnabled" default:"true" restart:"true"`
StunKeepaliveStartS int `xml:"stunKeepaliveStartS" json:"stunKeepaliveStartS" default:"180"` // 0 for off
StunKeepaliveMinS int `xml:"stunKeepaliveMinS" json:"stunKeepaliveMinS" default:"20"` // 0 for off
CREnabled bool `xml:"crashReportingEnabled" json:"crashReportingEnabled" default:"true"` // Read in the monitor, but it's read before every attempt to report stuff, so does not require a restart.
StunKeepaliveStartS int `xml:"stunKeepaliveStartS" json:"stunKeepaliveStartS" default:"180"` // 0 for off
StunKeepaliveMinS int `xml:"stunKeepaliveMinS" json:"stunKeepaliveMinS" default:"20"` // 0 for off
RawStunServers []string `xml:"stunServer" json:"stunServers" default:"default"`
DatabaseTuning Tuning `xml:"databaseTuning" json:"databaseTuning" restart:"true"`
DatabaseTuning Tuning `xml:"databaseTuning" json:"databaseTuning" restart:"true"` // Can't be adjusted once the database has been opened
RawMaxCIRequestKiB int `xml:"maxConcurrentIncomingRequestKiB" json:"maxConcurrentIncomingRequestKiB"`
DeprecatedUPnPEnabled bool `xml:"upnpEnabled,omitempty" json:"-"`
@@ -201,6 +201,6 @@ func (opts OptionsConfiguration) MaxConcurrentIncomingRequestKiB() int {
return opts.RawMaxCIRequestKiB
}
func (opts OptionsConfiguration) ShouldAutoUpgrade() bool {
func (opts OptionsConfiguration) AutoUpgradeEnabled() bool {
return opts.AutoUpgradeIntervalH > 0
}

View File

@@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
// +build go1.12
// +build go1.12,!noquic
package connections

View File

@@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build go1.12
// +build go1.12,!noquic
package connections

View File

@@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build go1.12
// +build go1.12,!noquic
package connections

View File

@@ -305,7 +305,11 @@ func (s *service) handle(ctx context.Context) {
if certName == "" {
certName = s.tlsDefaultCommonName
}
if err := remoteCert.VerifyHostname(certName); err != nil {
if remoteCert.Subject.CommonName == certName {
// All good. We do this check because our old style certificates
// have "syncthing" in the CommonName field and no SANs, which
// is not accepted by VerifyHostname() any more as of Go 1.15.
} else if err := remoteCert.VerifyHostname(certName); err != nil {
// Incorrect certificate name is something the user most
// likely wants to know about, since it's an advanced
// config. Warn instead of Info.

View File

@@ -7,6 +7,7 @@
package backend
import (
"errors"
"os"
"strings"
"sync"
@@ -15,6 +16,11 @@ import (
"github.com/syncthing/syncthing/lib/locations"
)
// CommitHook is a function that is executed before a WriteTransaction is
// committed or before it is flushed to disk, e.g. on calling CheckPoint. The
// transaction can be accessed via a closure.
type CommitHook func(WriteTransaction) error
// The Reader interface specifies the read-only operations available on the
// main database and on read-only transactions (snapshots). Note that when
// called directly on the database handle these operations may take implicit
@@ -52,16 +58,15 @@ type ReadTransaction interface {
// A Checkpoint is a potential partial commit of the transaction so far, for
// purposes of saving memory when transactions are in-RAM. Note that
// transactions may be checkpointed *anyway* even if this is not called, due to
// resource constraints, but this gives you a chance to decide when.
//
// Functions can be passed to Checkpoint. These are run if and only if the
// checkpoint will result in a flush, and will run before the flush. The
// transaction can be accessed via a closure. If an error is returned from
// these functions the flush will be aborted and the error bubbled.
// resource constraints, but this gives you a chance to decide when. If, and
// only if, calling Checkpoint will result in a partial commit/flush, the
// CommitHooks passed to Backend.NewWriteTransaction are called before
// committing. If any of those returns an error, committing is aborted and the
// error bubbled.
type WriteTransaction interface {
ReadTransaction
Writer
Checkpoint(...func() error) error
Checkpoint() error
Commit() error
}
@@ -108,7 +113,7 @@ type Backend interface {
Reader
Writer
NewReadTransaction() (ReadTransaction, error)
NewWriteTransaction() (WriteTransaction, error)
NewWriteTransaction(hooks ...CommitHook) (WriteTransaction, error)
Close() error
Compact() error
}
@@ -153,13 +158,13 @@ type errNotFound struct{}
func (*errNotFound) Error() string { return "key not found" }
func IsClosed(err error) bool {
_, ok := err.(*errClosed)
return ok
var e *errClosed
return errors.As(err, &e)
}
func IsNotFound(err error) bool {
_, ok := err.(*errNotFound)
return ok
var e *errNotFound
return errors.As(err, &e)
}
// releaser manages counting on top of a waitgroup

View File

@@ -69,7 +69,7 @@ func (b *badgerBackend) NewReadTransaction() (ReadTransaction, error) {
}, nil
}
func (b *badgerBackend) NewWriteTransaction() (WriteTransaction, error) {
func (b *badgerBackend) NewWriteTransaction(hooks ...CommitHook) (WriteTransaction, error) {
rel1, err := newReleaser(b.closeWG)
if err != nil {
return nil, err
@@ -90,9 +90,10 @@ func (b *badgerBackend) NewWriteTransaction() (WriteTransaction, error) {
txn: rtxn,
rel: rel1,
},
txn: wtxn,
bdb: b.bdb,
rel: rel2,
txn: wtxn,
bdb: b.bdb,
rel: rel2,
commitHooks: hooks,
}, nil
}
@@ -249,10 +250,11 @@ func (l badgerSnapshot) Release() {
type badgerTransaction struct {
badgerSnapshot
txn *badger.Txn
bdb *badger.DB
rel *releaser
size int
txn *badger.Txn
bdb *badger.DB
rel *releaser
size int
commitHooks []CommitHook
}
func (t *badgerTransaction) Delete(key []byte) error {
@@ -295,15 +297,20 @@ func (t *badgerTransaction) transactionRetried(fn func(*badger.Txn) error) error
func (t *badgerTransaction) Commit() error {
defer t.rel.Release()
defer t.badgerSnapshot.Release()
for _, hook := range t.commitHooks {
if err := hook(t); err != nil {
return err
}
}
return wrapBadgerErr(t.txn.Commit())
}
func (t *badgerTransaction) Checkpoint(preFlush ...func() error) error {
func (t *badgerTransaction) Checkpoint() error {
if t.size < checkpointFlushMinSize {
return nil
}
for _, hook := range preFlush {
if err := hook(); err != nil {
for _, hook := range t.commitHooks {
if err := hook(t); err != nil {
return err
}
}

View File

@@ -59,7 +59,7 @@ func (b *leveldbBackend) newSnapshot() (leveldbSnapshot, error) {
}, nil
}
func (b *leveldbBackend) NewWriteTransaction() (WriteTransaction, error) {
func (b *leveldbBackend) NewWriteTransaction(hooks ...CommitHook) (WriteTransaction, error) {
rel, err := newReleaser(b.closeWG)
if err != nil {
return nil, err
@@ -74,6 +74,8 @@ func (b *leveldbBackend) NewWriteTransaction() (WriteTransaction, error) {
ldb: b.ldb,
batch: new(leveldb.Batch),
rel: rel,
commitHooks: hooks,
inFlush: false,
}, nil
}
@@ -142,9 +144,11 @@ func (l leveldbSnapshot) Release() {
// an actual leveldb transaction)
type leveldbTransaction struct {
leveldbSnapshot
ldb *leveldb.DB
batch *leveldb.Batch
rel *releaser
ldb *leveldb.DB
batch *leveldb.Batch
rel *releaser
commitHooks []CommitHook
inFlush bool
}
func (t *leveldbTransaction) Delete(key []byte) error {
@@ -157,8 +161,8 @@ func (t *leveldbTransaction) Put(key, val []byte) error {
return t.checkFlush(dbFlushBatchMax)
}
func (t *leveldbTransaction) Checkpoint(preFlush ...func() error) error {
return t.checkFlush(dbFlushBatchMin, preFlush...)
func (t *leveldbTransaction) Checkpoint() error {
return t.checkFlush(dbFlushBatchMin)
}
func (t *leveldbTransaction) Commit() error {
@@ -174,19 +178,25 @@ func (t *leveldbTransaction) Release() {
}
// checkFlush flushes and resets the batch if its size exceeds the given size.
func (t *leveldbTransaction) checkFlush(size int, preFlush ...func() error) error {
if len(t.batch.Dump()) < size {
func (t *leveldbTransaction) checkFlush(size int) error {
// Hooks might put values in the database, which triggers a checkFlush which might trigger a flush,
// which might trigger the hooks.
// Don't recurse...
if t.inFlush || len(t.batch.Dump()) < size {
return nil
}
for _, hook := range preFlush {
if err := hook(); err != nil {
return err
}
}
return t.flush()
}
func (t *leveldbTransaction) flush() error {
t.inFlush = true
defer func() { t.inFlush = false }()
for _, hook := range t.commitHooks {
if err := hook(t); err != nil {
return err
}
}
if t.batch.Len() == 0 {
return nil
}

View File

@@ -9,6 +9,8 @@ package db
import (
"bytes"
"context"
"fmt"
"os"
"testing"
"github.com/syncthing/syncthing/lib/db/backend"
@@ -306,6 +308,7 @@ func TestRepairSequence(t *testing.T) {
{Name: "missing", Blocks: genBlocks(3)},
{Name: "overwriting", Blocks: genBlocks(4)},
{Name: "inconsistent", Blocks: genBlocks(5)},
{Name: "inconsistentNotIndirected", Blocks: genBlocks(2)},
}
for i, f := range files {
files[i].Version = f.Version.Update(short)
@@ -322,7 +325,7 @@ func TestRepairSequence(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := trans.putFile(dk, f, false); err != nil {
if err := trans.putFile(dk, f); err != nil {
t.Fatal(err)
}
sk, err := trans.keyer.GenerateSequenceKey(nil, folder, seq)
@@ -367,10 +370,13 @@ func TestRepairSequence(t *testing.T) {
files[3].Sequence = seq
addFile(files[3], seq)
// Inconistent file
// Inconistent files
seq++
files[4].Sequence = 101
addFile(files[4], seq)
seq++
files[5].Sequence = 102
addFile(files[5], seq)
// And a sequence entry pointing at nothing because why not
sk, err = trans.keyer.GenerateSequenceKey(nil, folder, 100001)
@@ -640,7 +646,7 @@ func TestGCIndirect(t *testing.T) {
db := NewLowlevel(backend.OpenMemory())
defer db.Close()
meta := newMetadataTracker()
meta := newMetadataTracker(db.keyer)
// Add three files with different block lists
@@ -725,6 +731,118 @@ func TestGCIndirect(t *testing.T) {
}
}
func TestUpdateTo14(t *testing.T) {
db := NewLowlevel(backend.OpenMemory())
defer db.Close()
folderStr := "default"
folder := []byte(folderStr)
name := []byte("foo")
file := protocol.FileInfo{Name: string(name), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(blocksIndirectionCutoff - 1)}
file.BlocksHash = protocol.BlocksHash(file.Blocks)
fileWOBlocks := file
fileWOBlocks.Blocks = nil
meta := db.loadMetadataTracker(folderStr)
// Initally add the correct file the usual way, all good here.
if err := db.updateLocalFiles(folder, []protocol.FileInfo{file}, meta); err != nil {
t.Fatal(err)
}
// Simulate the previous bug, where .putFile could write a file info without
// blocks, even though the file has them (and thus a non-nil BlocksHash).
trans, err := db.newReadWriteTransaction()
if err != nil {
t.Fatal(err)
}
defer trans.close()
key, err := db.keyer.GenerateDeviceFileKey(nil, folder, protocol.LocalDeviceID[:], name)
if err != nil {
t.Fatal(err)
}
fiBs := mustMarshal(&fileWOBlocks)
if err := trans.Put(key, fiBs); err != nil {
t.Fatal(err)
}
if err := trans.Commit(); err != nil {
t.Fatal(err)
}
trans.close()
// Run migration, pretending were still on schema 13.
if err := (&schemaUpdater{db}).updateSchemaTo14(13); err != nil {
t.Fatal(err)
}
// checks
ro, err := db.newReadOnlyTransaction()
if err != nil {
t.Fatal(err)
}
defer ro.close()
if f, ok, err := ro.getFileByKey(key); err != nil {
t.Fatal(err)
} else if !ok {
t.Error("file missing")
} else if !f.MustRescan() {
t.Error("file not marked as MustRescan")
}
if vl, err := ro.getGlobalVersions(nil, folder, name); err != nil {
t.Fatal(err)
} else if fv, ok := vl.GetGlobal(); !ok {
t.Error("missing global")
} else if !fv.IsInvalid() {
t.Error("global not marked as invalid")
}
}
func TestFlushRecursion(t *testing.T) {
// Verify that a commit hook can write to the transaction without
// causing another flush and thus recursion.
// Badger doesn't work like this.
if os.Getenv("USE_BADGER") != "" {
t.Skip("Not supported on Badger")
}
db := NewLowlevel(backend.OpenMemory())
defer db.Close()
// A commit hook that writes a small piece of data to the transaction.
hookFired := 0
hook := func(tx backend.WriteTransaction) error {
err := tx.Put([]byte(fmt.Sprintf("hook-key-%d", hookFired)), []byte(fmt.Sprintf("hook-value-%d", hookFired)))
if err != nil {
t.Fatal(err)
}
hookFired++
return nil
}
// A transaction.
tx, err := db.NewWriteTransaction(hook)
if err != nil {
t.Fatal(err)
}
defer tx.Release()
// Write stuff until the transaction flushes, thus firing the hook.
i := 0
for hookFired == 0 {
err := tx.Put([]byte(fmt.Sprintf("key-%d", i)), []byte(fmt.Sprintf("value-%d", i)))
if err != nil {
t.Fatal(err)
}
i++
}
// The hook should have fired precisely once.
if hookFired != 1 {
t.Error("expect one hook fire, not", hookFired)
}
}
func numBlockLists(db *Lowlevel) (int, error) {
it, err := db.Backend.NewPrefixIterator([]byte{KeyTypeBlockList})
if err != nil {

View File

@@ -117,7 +117,7 @@ func (db *Lowlevel) updateRemoteFiles(folder, device []byte, fs []protocol.FileI
db.gcMut.RLock()
defer db.gcMut.RUnlock()
t, err := db.newReadWriteTransaction()
t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
if err != nil {
return err
}
@@ -140,6 +140,7 @@ func (db *Lowlevel) updateRemoteFiles(folder, device []byte, fs []protocol.FileI
return err
}
if ok && unchanged(f, ef) {
l.Debugf("not inserting unchanged (remote); folder=%q device=%v %v", folder, devID, f)
continue
}
@@ -148,8 +149,8 @@ func (db *Lowlevel) updateRemoteFiles(folder, device []byte, fs []protocol.FileI
}
meta.addFile(devID, f)
l.Debugf("insert; folder=%q device=%v %v", folder, devID, f)
if err := t.putFile(dk, f, false); err != nil {
l.Debugf("insert (remote); folder=%q device=%v %v", folder, devID, f)
if err := t.putFile(dk, f); err != nil {
return err
}
@@ -162,17 +163,11 @@ func (db *Lowlevel) updateRemoteFiles(folder, device []byte, fs []protocol.FileI
return err
}
if err := t.Checkpoint(func() error {
return meta.toDB(t, folder)
}); err != nil {
if err := t.Checkpoint(); err != nil {
return err
}
}
if err := meta.toDB(t, folder); err != nil {
return err
}
return t.Commit()
}
@@ -182,7 +177,7 @@ func (db *Lowlevel) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
db.gcMut.RLock()
defer db.gcMut.RUnlock()
t, err := db.newReadWriteTransaction()
t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
if err != nil {
return err
}
@@ -202,6 +197,7 @@ func (db *Lowlevel) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
return err
}
if ok && unchanged(f, ef) {
l.Debugf("not inserting unchanged (local); folder=%q %v", folder, f)
continue
}
blocksHashSame := ok && bytes.Equal(ef.BlocksHash, f.BlocksHash)
@@ -246,7 +242,7 @@ func (db *Lowlevel) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
meta.addFile(protocol.LocalDeviceID, f)
l.Debugf("insert (local); folder=%q %v", folder, f)
if err := t.putFile(dk, f, false); err != nil {
if err := t.putFile(dk, f); err != nil {
return err
}
@@ -290,17 +286,11 @@ func (db *Lowlevel) updateLocalFiles(folder []byte, fs []protocol.FileInfo, meta
}
}
if err := t.Checkpoint(func() error {
return meta.toDB(t, folder)
}); err != nil {
if err := t.Checkpoint(); err != nil {
return err
}
}
if err := meta.toDB(t, folder); err != nil {
return err
}
return t.Commit()
}
@@ -374,7 +364,7 @@ func (db *Lowlevel) dropDeviceFolder(device, folder []byte, meta *metadataTracke
db.gcMut.RLock()
defer db.gcMut.RUnlock()
t, err := db.newReadWriteTransaction()
t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
if err != nil {
return err
}
@@ -509,7 +499,7 @@ func checkGlobalsFilterDevices(dk, folder, name []byte, devices [][]byte, vl *Ve
if err != nil {
return false, err
}
f, ok, err := t.getFileTrunc(dk, true)
f, ok, err := t.getFileTrunc(dk, false)
if err != nil {
return false, err
}
@@ -830,7 +820,7 @@ func (db *Lowlevel) getMetaAndCheck(folder string) *metadataTracker {
}
func (db *Lowlevel) loadMetadataTracker(folder string) *metadataTracker {
meta := newMetadataTracker()
meta := newMetadataTracker(db.keyer)
if err := meta.fromDB(db, []byte(folder)); err != nil {
if err == errMetaInconsistent {
l.Infof("Stored folder metadata for %q is inconsistent; recalculating", folder)
@@ -855,20 +845,22 @@ func (db *Lowlevel) loadMetadataTracker(folder string) *metadataTracker {
return meta
}
func (db *Lowlevel) recalcMeta(folder string) (*metadataTracker, error) {
meta := newMetadataTracker()
if err := db.checkGlobals([]byte(folder)); err != nil {
func (db *Lowlevel) recalcMeta(folderStr string) (*metadataTracker, error) {
folder := []byte(folderStr)
meta := newMetadataTracker(db.keyer)
if err := db.checkGlobals(folder); err != nil {
return nil, err
}
t, err := db.newReadWriteTransaction()
t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
if err != nil {
return nil, err
}
defer t.close()
var deviceID protocol.DeviceID
err = t.withAllFolderTruncated([]byte(folder), func(device []byte, f FileInfoTruncated) bool {
err = t.withAllFolderTruncated(folder, func(device []byte, f FileInfoTruncated) bool {
copy(deviceID[:], device)
meta.addFile(deviceID, f)
return true
@@ -877,13 +869,13 @@ func (db *Lowlevel) recalcMeta(folder string) (*metadataTracker, error) {
return nil, err
}
err = t.withGlobal([]byte(folder), nil, true, func(f protocol.FileIntf) bool {
err = t.withGlobal(folder, nil, true, func(f protocol.FileIntf) bool {
meta.addFile(protocol.GlobalDeviceID, f)
return true
})
meta.emptyNeeded(protocol.LocalDeviceID)
err = t.withNeed([]byte(folder), protocol.LocalDeviceID[:], true, func(f protocol.FileIntf) bool {
err = t.withNeed(folder, protocol.LocalDeviceID[:], true, func(f protocol.FileIntf) bool {
meta.addNeeded(protocol.LocalDeviceID, f)
return true
})
@@ -892,7 +884,7 @@ func (db *Lowlevel) recalcMeta(folder string) (*metadataTracker, error) {
}
for _, device := range meta.devices() {
meta.emptyNeeded(device)
err = t.withNeed([]byte(folder), device[:], true, func(f protocol.FileIntf) bool {
err = t.withNeed(folder, device[:], true, func(f protocol.FileIntf) bool {
meta.addNeeded(device, f)
return true
})
@@ -902,9 +894,6 @@ func (db *Lowlevel) recalcMeta(folder string) (*metadataTracker, error) {
}
meta.SetCreated()
if err := meta.toDB(t, []byte(folder)); err != nil {
return nil, err
}
if err := t.Commit(); err != nil {
return nil, err
}
@@ -944,7 +933,7 @@ func (db *Lowlevel) verifyLocalSequence(curSeq int64, folder string) bool {
// match those in the corresponding file entries. It returns the amount of fixed
// entries.
func (db *Lowlevel) repairSequenceGCLocked(folderStr string, meta *metadataTracker) (int, error) {
t, err := db.newReadWriteTransaction()
t, err := db.newReadWriteTransaction(meta.CommitHook([]byte(folderStr)))
if err != nil {
return 0, err
}
@@ -969,11 +958,11 @@ func (db *Lowlevel) repairSequenceGCLocked(folderStr string, meta *metadataTrack
var sk sequenceKey
for it.Next() {
intf, err := t.unmarshalTrunc(it.Value(), true)
intf, err := t.unmarshalTrunc(it.Value(), false)
if err != nil {
return 0, err
}
fi := intf.(FileInfoTruncated)
fi := intf.(protocol.FileInfo)
if sk, err = t.keyer.GenerateSequenceKey(sk, folder, fi.Sequence); err != nil {
return 0, err
}
@@ -992,13 +981,11 @@ func (db *Lowlevel) repairSequenceGCLocked(folderStr string, meta *metadataTrack
if err := t.Put(sk, it.Key()); err != nil {
return 0, err
}
if err := t.putFile(it.Key(), fi.copyToFileInfo(), true); err != nil {
if err := t.putFile(it.Key(), fi); err != nil {
return 0, err
}
}
if err := t.Checkpoint(func() error {
return meta.toDB(t, folder)
}); err != nil {
if err := t.Checkpoint(); err != nil {
return 0, err
}
}
@@ -1045,10 +1032,6 @@ func (db *Lowlevel) repairSequenceGCLocked(folderStr string, meta *metadataTrack
it.Release()
if err := meta.toDB(t, folder); err != nil {
return 0, err
}
return fixed, t.Commit()
}

View File

@@ -12,6 +12,7 @@ import (
"math/bits"
"time"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
)
@@ -25,6 +26,7 @@ type countsMap struct {
// metadataTracker keeps metadata on a per device, per local flag basis.
type metadataTracker struct {
keyer keyer
countsMap
mut sync.RWMutex
dirty bool
@@ -37,9 +39,10 @@ type metaKey struct {
const needFlag uint32 = 1 << 31 // Last bit, as early ones are local flags
func newMetadataTracker() *metadataTracker {
func newMetadataTracker(keyer keyer) *metadataTracker {
return &metadataTracker{
mut: sync.NewRWMutex(),
keyer: keyer,
mut: sync.NewRWMutex(),
countsMap: countsMap{
indexes: make(map[metaKey]int),
},
@@ -69,10 +72,16 @@ func (m *metadataTracker) Marshal() ([]byte, error) {
return m.counts.Marshal()
}
func (m *metadataTracker) CommitHook(folder []byte) backend.CommitHook {
return func(t backend.WriteTransaction) error {
return m.toDB(t, folder)
}
}
// toDB saves the marshalled metadataTracker to the given db, under the key
// corresponding to the given folder
func (m *metadataTracker) toDB(t readWriteTransaction, folder []byte) error {
key, err := t.keyer.GenerateFolderMetaKey(nil, folder)
func (m *metadataTracker) toDB(t backend.WriteTransaction, folder []byte) error {
key, err := m.keyer.GenerateFolderMetaKey(nil, folder)
if err != nil {
return err
}
@@ -164,22 +173,27 @@ func (m *metadataTracker) addFile(dev protocol.DeviceID, f protocol.FileIntf) {
m.mut.Lock()
defer m.mut.Unlock()
m.dirty = true
m.updateSeqLocked(dev, f)
if f.IsInvalid() && f.FileLocalFlags() == 0 {
// This is a remote invalid file; it does not count.
m.updateFileLocked(dev, f, m.addFileLocked)
}
func (m *metadataTracker) updateFileLocked(dev protocol.DeviceID, f protocol.FileIntf, fn func(protocol.DeviceID, uint32, protocol.FileIntf)) {
m.dirty = true
if f.IsInvalid() && (f.FileLocalFlags() == 0 || dev == protocol.GlobalDeviceID) {
// This is a remote invalid file or concern the global state.
// In either case invalid files are not accounted.
return
}
if flags := f.FileLocalFlags(); flags == 0 {
// Account regular files in the zero-flags bucket.
m.addFileLocked(dev, 0, f)
fn(dev, 0, f)
} else {
// Account in flag specific buckets.
eachFlagBit(flags, func(flag uint32) {
m.addFileLocked(dev, flag, f)
fn(dev, flag, f)
})
}
}
@@ -241,25 +255,10 @@ func (m *metadataTracker) addFileLocked(dev protocol.DeviceID, flag uint32, f pr
// removeFile removes a file from the counts
func (m *metadataTracker) removeFile(dev protocol.DeviceID, f protocol.FileIntf) {
if f.IsInvalid() && f.FileLocalFlags() == 0 {
// This is a remote invalid file; it does not count.
return
}
m.mut.Lock()
defer m.mut.Unlock()
m.dirty = true
if flags := f.FileLocalFlags(); flags == 0 {
// Remove regular files from the zero-flags bucket
m.removeFileLocked(dev, 0, f)
} else {
// Remove from flag specific buckets.
eachFlagBit(flags, func(flag uint32) {
m.removeFileLocked(dev, flag, f)
})
}
m.updateFileLocked(dev, f, m.removeFileLocked)
}
// removeNeeded removes a file from the needed counts

View File

@@ -52,7 +52,7 @@ func TestEachFlagBit(t *testing.T) {
func TestMetaDevices(t *testing.T) {
d1 := protocol.DeviceID{1}
d2 := protocol.DeviceID{2}
meta := newMetadataTracker()
meta := newMetadataTracker(nil)
meta.addFile(d1, protocol.FileInfo{Sequence: 1})
meta.addFile(d1, protocol.FileInfo{Sequence: 2, LocalFlags: 1})
@@ -85,7 +85,7 @@ func TestMetaDevices(t *testing.T) {
func TestMetaSequences(t *testing.T) {
d1 := protocol.DeviceID{1}
meta := newMetadataTracker()
meta := newMetadataTracker(nil)
meta.addFile(d1, protocol.FileInfo{Sequence: 1})
meta.addFile(d1, protocol.FileInfo{Sequence: 2, RawInvalid: true})

View File

@@ -27,9 +27,10 @@ import (
// 8-9: v1.4.0
// 10-11: v1.6.0
// 12-13: v1.7.0
// 14: v1.9.0
const (
dbVersion = 13
dbMinSyncthingVersion = "v1.7.0"
dbVersion = 14
dbMinSyncthingVersion = "v1.9.0"
)
var errFolderMissing = errors.New("folder present in global list but missing in keyer index")
@@ -95,6 +96,7 @@ func (db *schemaUpdater) updateSchema() error {
{10, db.updateSchemaTo10},
{11, db.updateSchemaTo11},
{13, db.updateSchemaTo13},
{14, db.updateSchemaTo14},
}
for _, m := range migrations {
@@ -537,7 +539,7 @@ func (db *schemaUpdater) rewriteFiles(t readWriteTransaction) error {
if fi.Blocks == nil {
continue
}
if err := t.putFile(it.Key(), fi, false); err != nil {
if err := t.putFile(it.Key(), fi); err != nil {
return err
}
if err := t.Checkpoint(); err != nil {
@@ -683,6 +685,70 @@ func (db *schemaUpdater) updateSchemaTo13(prev int) error {
return t.Commit()
}
func (db *schemaUpdater) updateSchemaTo14(_ int) error {
// Checks for missing blocks and marks those entries as requiring a
// rehash/being invalid. The db is checked/repaired afterwards, i.e.
// no care is taken to get metadata and sequences right.
// If the corresponding files changed on disk compared to the global
// version, this will cause a conflict.
var key, gk []byte
for _, folderStr := range db.ListFolders() {
folder := []byte(folderStr)
meta := newMetadataTracker(db.keyer)
meta.counts.Created = 0 // Recalculate metadata afterwards
t, err := db.newReadWriteTransaction(meta.CommitHook(folder))
if err != nil {
return err
}
defer t.close()
key, err = t.keyer.GenerateDeviceFileKey(key, folder, protocol.LocalDeviceID[:], nil)
it, err := t.NewPrefixIterator(key)
if err != nil {
return err
}
defer it.Release()
for it.Next() {
var fi protocol.FileInfo
if err := fi.Unmarshal(it.Value()); err != nil {
return err
}
if len(fi.Blocks) > 0 || len(fi.BlocksHash) == 0 {
continue
}
key = t.keyer.GenerateBlockListKey(key, fi.BlocksHash)
_, err := t.Get(key)
if err == nil {
continue
}
fi.SetMustRescan(protocol.LocalDeviceID.Short())
if err = t.putFile(it.Key(), fi); err != nil {
return err
}
gk, err = t.keyer.GenerateGlobalVersionKey(gk, folder, []byte(fi.Name))
if err != nil {
return err
}
key, _, err = t.updateGlobal(gk, key, folder, protocol.LocalDeviceID[:], fi, meta)
if err != nil {
return err
}
}
it.Release()
if err = t.Commit(); err != nil {
return err
}
t.close()
}
return nil
}
func (db *schemaUpdater) rewriteGlobals(t readWriteTransaction) error {
it, err := t.NewPrefixIterator([]byte{KeyTypeGlobal})
if err != nil {

View File

@@ -13,6 +13,8 @@
package db
import (
"fmt"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
@@ -45,7 +47,8 @@ func NewFileSet(folder string, fs fs.Filesystem, db *Lowlevel) *FileSet {
}
func (s *FileSet) Drop(device protocol.DeviceID) {
l.Debugf("%s Drop(%v)", s.folder, device)
opStr := fmt.Sprintf("%s Drop(%v)", s.folder, device)
l.Debugf(opStr)
s.updateMutex.Lock()
defer s.updateMutex.Unlock()
@@ -53,7 +56,7 @@ func (s *FileSet) Drop(device protocol.DeviceID) {
if err := s.db.dropDeviceFolder(device[:], []byte(s.folder), s.meta); backend.IsClosed(err) {
return
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
if device == protocol.LocalDeviceID {
@@ -75,24 +78,25 @@ func (s *FileSet) Drop(device protocol.DeviceID) {
if backend.IsClosed(err) {
return
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
defer t.close()
if err := s.meta.toDB(t, []byte(s.folder)); backend.IsClosed(err) {
return
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
if err := t.Commit(); backend.IsClosed(err) {
return
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
}
func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
l.Debugf("%s Update(%v, [%d])", s.folder, device, len(fs))
opStr := fmt.Sprintf("%s Update(%v, [%d])", s.folder, device, len(fs))
l.Debugf(opStr)
// do not modify fs in place, it is still used in outer scope
fs = append([]protocol.FileInfo(nil), fs...)
@@ -111,13 +115,13 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
if device == protocol.LocalDeviceID {
// For the local device we have a bunch of metadata to track.
if err := s.db.updateLocalFiles([]byte(s.folder), fs, s.meta); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
return
}
// Easy case, just update the files and we're done.
if err := s.db.updateRemoteFiles([]byte(s.folder), device[:], fs, s.meta); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
@@ -128,9 +132,11 @@ type Snapshot struct {
}
func (s *FileSet) Snapshot() *Snapshot {
opStr := fmt.Sprintf("%s Snapshot()", s.folder)
l.Debugf(opStr)
t, err := s.db.newReadOnlyTransaction()
if err != nil {
panic(err)
fatalError(err, opStr)
}
return &Snapshot{
folder: s.folder,
@@ -144,89 +150,102 @@ func (s *Snapshot) Release() {
}
func (s *Snapshot) WithNeed(device protocol.DeviceID, fn Iterator) {
l.Debugf("%s WithNeed(%v)", s.folder, device)
opStr := fmt.Sprintf("%s WithNeed(%v)", s.folder, device)
l.Debugf(opStr)
if err := s.t.withNeed([]byte(s.folder), device[:], false, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
func (s *Snapshot) WithNeedTruncated(device protocol.DeviceID, fn Iterator) {
l.Debugf("%s WithNeedTruncated(%v)", s.folder, device)
opStr := fmt.Sprintf("%s WithNeedTruncated(%v)", s.folder, device)
l.Debugf(opStr)
if err := s.t.withNeed([]byte(s.folder), device[:], true, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
func (s *Snapshot) WithHave(device protocol.DeviceID, fn Iterator) {
l.Debugf("%s WithHave(%v)", s.folder, device)
opStr := fmt.Sprintf("%s WithHave(%v)", s.folder, device)
l.Debugf(opStr)
if err := s.t.withHave([]byte(s.folder), device[:], nil, false, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
func (s *Snapshot) WithHaveTruncated(device protocol.DeviceID, fn Iterator) {
l.Debugf("%s WithHaveTruncated(%v)", s.folder, device)
opStr := fmt.Sprintf("%s WithHaveTruncated(%v)", s.folder, device)
l.Debugf(opStr)
if err := s.t.withHave([]byte(s.folder), device[:], nil, true, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
func (s *Snapshot) WithHaveSequence(startSeq int64, fn Iterator) {
l.Debugf("%s WithHaveSequence(%v)", s.folder, startSeq)
opStr := fmt.Sprintf("%s WithHaveSequence(%v)", s.folder, startSeq)
l.Debugf(opStr)
if err := s.t.withHaveSequence([]byte(s.folder), startSeq, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
// Except for an item with a path equal to prefix, only children of prefix are iterated.
// E.g. for prefix "dir", "dir/file" is iterated, but "dir.file" is not.
func (s *Snapshot) WithPrefixedHaveTruncated(device protocol.DeviceID, prefix string, fn Iterator) {
l.Debugf(`%s WithPrefixedHaveTruncated(%v, "%v")`, s.folder, device, prefix)
opStr := fmt.Sprintf(`%s WithPrefixedHaveTruncated(%v, "%v")`, s.folder, device, prefix)
l.Debugf(opStr)
if err := s.t.withHave([]byte(s.folder), device[:], []byte(osutil.NormalizedFilename(prefix)), true, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
func (s *Snapshot) WithGlobal(fn Iterator) {
l.Debugf("%s WithGlobal()", s.folder)
opStr := fmt.Sprintf("%s WithGlobal()", s.folder)
l.Debugf(opStr)
if err := s.t.withGlobal([]byte(s.folder), nil, false, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
func (s *Snapshot) WithGlobalTruncated(fn Iterator) {
l.Debugf("%s WithGlobalTruncated()", s.folder)
opStr := fmt.Sprintf("%s WithGlobalTruncated()", s.folder)
l.Debugf(opStr)
if err := s.t.withGlobal([]byte(s.folder), nil, true, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
// Except for an item with a path equal to prefix, only children of prefix are iterated.
// E.g. for prefix "dir", "dir/file" is iterated, but "dir.file" is not.
func (s *Snapshot) WithPrefixedGlobalTruncated(prefix string, fn Iterator) {
l.Debugf(`%s WithPrefixedGlobalTruncated("%v")`, s.folder, prefix)
opStr := fmt.Sprintf(`%s WithPrefixedGlobalTruncated("%v")`, s.folder, prefix)
l.Debugf(opStr)
if err := s.t.withGlobal([]byte(s.folder), []byte(osutil.NormalizedFilename(prefix)), true, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
func (s *Snapshot) Get(device protocol.DeviceID, file string) (protocol.FileInfo, bool) {
opStr := fmt.Sprintf("%s Get(%v)", s.folder, file)
l.Debugf(opStr)
f, ok, err := s.t.getFile([]byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file)))
if backend.IsClosed(err) {
return protocol.FileInfo{}, false
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
f.Name = osutil.NativeFilename(f.Name)
return f, ok
}
func (s *Snapshot) GetGlobal(file string) (protocol.FileInfo, bool) {
opStr := fmt.Sprintf("%s GetGlobal(%v)", s.folder, file)
l.Debugf(opStr)
_, fi, ok, err := s.t.getGlobal(nil, []byte(s.folder), []byte(osutil.NormalizedFilename(file)), false)
if backend.IsClosed(err) {
return protocol.FileInfo{}, false
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
if !ok {
return protocol.FileInfo{}, false
@@ -237,11 +256,13 @@ func (s *Snapshot) GetGlobal(file string) (protocol.FileInfo, bool) {
}
func (s *Snapshot) GetGlobalTruncated(file string) (FileInfoTruncated, bool) {
opStr := fmt.Sprintf("%s GetGlobalTruncated(%v)", s.folder, file)
l.Debugf(opStr)
_, fi, ok, err := s.t.getGlobal(nil, []byte(s.folder), []byte(osutil.NormalizedFilename(file)), true)
if backend.IsClosed(err) {
return FileInfoTruncated{}, false
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
if !ok {
return FileInfoTruncated{}, false
@@ -252,11 +273,13 @@ func (s *Snapshot) GetGlobalTruncated(file string) (FileInfoTruncated, bool) {
}
func (s *Snapshot) Availability(file string) []protocol.DeviceID {
opStr := fmt.Sprintf("%s Availability(%v)", s.folder, file)
l.Debugf(opStr)
av, err := s.t.availability([]byte(s.folder), []byte(osutil.NormalizedFilename(file)))
if backend.IsClosed(err) {
return nil
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
return av
}
@@ -288,9 +311,7 @@ func (s *Snapshot) ReceiveOnlyChangedSize() Counts {
}
func (s *Snapshot) GlobalSize() Counts {
global := s.meta.Counts(protocol.GlobalDeviceID, 0)
recvOnlyChanged := s.meta.Counts(protocol.GlobalDeviceID, protocol.FlagLocalReceiveOnly)
return global.Add(recvOnlyChanged)
return s.meta.Counts(protocol.GlobalDeviceID, 0)
}
func (s *Snapshot) NeedSize(device protocol.DeviceID) Counts {
@@ -345,9 +366,10 @@ func (s *Snapshot) RemoteNeedFolderFiles(device protocol.DeviceID, page, perpage
}
func (s *Snapshot) WithBlocksHash(hash []byte, fn Iterator) {
l.Debugf(`%s WithBlocksHash("%x")`, s.folder, hash)
opStr := fmt.Sprintf(`%s WithBlocksHash("%x")`, s.folder, hash)
l.Debugf(opStr)
if err := s.t.withBlocksHash([]byte(s.folder), hash, nativeFileIterator(fn)); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
@@ -356,11 +378,13 @@ func (s *FileSet) Sequence(device protocol.DeviceID) int64 {
}
func (s *FileSet) IndexID(device protocol.DeviceID) protocol.IndexID {
opStr := fmt.Sprintf("%s IndexID(%v)", s.folder, device)
l.Debugf(opStr)
id, err := s.db.getIndexID(device[:], []byte(s.folder))
if backend.IsClosed(err) {
return 0
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
if id == 0 && device == protocol.LocalDeviceID {
// No index ID set yet. We create one now.
@@ -369,7 +393,7 @@ func (s *FileSet) IndexID(device protocol.DeviceID) protocol.IndexID {
if backend.IsClosed(err) {
return 0
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
}
return id
@@ -379,17 +403,21 @@ func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
if device == protocol.LocalDeviceID {
panic("do not explicitly set index ID for local device")
}
opStr := fmt.Sprintf("%s SetIndexID(%v, %v)", s.folder, device, id)
l.Debugf(opStr)
if err := s.db.setIndexID(device[:], []byte(s.folder), id); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
func (s *FileSet) MtimeFS() *fs.MtimeFS {
opStr := fmt.Sprintf("%s MtimeFS()", s.folder)
l.Debugf(opStr)
prefix, err := s.db.keyer.GenerateMtimesKey(nil, []byte(s.folder))
if backend.IsClosed(err) {
return nil
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
kv := NewNamespacedKV(s.db, string(prefix))
return fs.NewMtimeFS(s.fs, kv)
@@ -414,6 +442,8 @@ func (s *FileSet) updateAndGCMutexLock() {
// DropFolder clears out all information related to the given folder from the
// database.
func DropFolder(db *Lowlevel, folder string) {
opStr := fmt.Sprintf("DropFolder(%v)", folder)
l.Debugf(opStr)
droppers := []func([]byte) error{
db.dropFolder,
db.dropMtimes,
@@ -424,7 +454,7 @@ func DropFolder(db *Lowlevel, folder string) {
if err := drop([]byte(folder)); backend.IsClosed(err) {
return
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
}
}
@@ -432,20 +462,22 @@ func DropFolder(db *Lowlevel, folder string) {
// DropDeltaIndexIDs removes all delta index IDs from the database.
// This will cause a full index transmission on the next connection.
func DropDeltaIndexIDs(db *Lowlevel) {
opStr := "DropDeltaIndexIDs"
l.Debugf(opStr)
dbi, err := db.NewPrefixIterator([]byte{KeyTypeIndexID})
if backend.IsClosed(err) {
return
} else if err != nil {
panic(err)
fatalError(err, opStr)
}
defer dbi.Release()
for dbi.Next() {
if err := db.Delete(dbi.Key()); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
if err := dbi.Error(); err != nil && !backend.IsClosed(err) {
panic(err)
fatalError(err, opStr)
}
}
@@ -483,3 +515,8 @@ func nativeFileIterator(fn Iterator) Iterator {
}
}
}
func fatalError(err error, opStr string) {
l.Warnf("Fatal error: %v: %v", opStr, err)
panic(err)
}

View File

@@ -1707,6 +1707,38 @@ func TestNeedRemoteAfterReset(t *testing.T) {
}
}
// https://github.com/syncthing/syncthing/issues/6850
func TestIgnoreLocalChanged(t *testing.T) {
ldb := db.NewLowlevel(backend.OpenMemory())
defer ldb.Close()
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
// Add locally changed file
files := fileList{
protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2), LocalFlags: protocol.FlagLocalReceiveOnly},
}
s.Update(protocol.LocalDeviceID, files)
if c := globalSize(s).Files; c != 0 {
t.Error("Expected no global file, got", c)
}
if c := localSize(s).Files; c != 1 {
t.Error("Expected one local file, got", c)
}
// Change file to ignored
files[0].LocalFlags = protocol.FlagLocalIgnored
s.Update(protocol.LocalDeviceID, files)
if c := globalSize(s).Files; c != 0 {
t.Error("Expected no global file, got", c)
}
if c := localSize(s).Files; c != 0 {
t.Error("Expected no local file, got", c)
}
}
func replace(fs *db.FileSet, device protocol.DeviceID, files []protocol.FileInfo) {
fs.Drop(device)
fs.Update(device, files)

View File

@@ -9,9 +9,10 @@ package db
import (
"bytes"
"errors"
"github.com/syncthing/syncthing/lib/osutil"
"fmt"
"github.com/syncthing/syncthing/lib/db/backend"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -109,7 +110,7 @@ func (t readOnlyTransaction) fillFileInfo(fi *protocol.FileInfo) error {
key = t.keyer.GenerateBlockListKey(key, fi.BlocksHash)
bs, err := t.Get(key)
if err != nil {
return err
return fmt.Errorf("filling Blocks: %w", err)
}
var bl BlockList
if err := bl.Unmarshal(bs); err != nil {
@@ -122,7 +123,7 @@ func (t readOnlyTransaction) fillFileInfo(fi *protocol.FileInfo) error {
key = t.keyer.GenerateVersionKey(key, fi.VersionHash)
bs, err := t.Get(key)
if err != nil {
return err
return fmt.Errorf("filling Version: %w", err)
}
var v protocol.Vector
if err := v.Unmarshal(bs); err != nil {
@@ -516,8 +517,8 @@ type readWriteTransaction struct {
readOnlyTransaction
}
func (db *Lowlevel) newReadWriteTransaction() (readWriteTransaction, error) {
tran, err := db.NewWriteTransaction()
func (db *Lowlevel) newReadWriteTransaction(hooks ...backend.CommitHook) (readWriteTransaction, error) {
tran, err := db.NewWriteTransaction(hooks...)
if err != nil {
return readWriteTransaction{}, err
}
@@ -543,18 +544,13 @@ func (t readWriteTransaction) close() {
}
// putFile stores a file in the database, taking care of indirected fields.
// Set the truncated flag when putting a file that deliberatly can have an
// empty block list but a non-empty block list hash. This should normally be
// false.
func (t readWriteTransaction) putFile(fkey []byte, fi protocol.FileInfo, truncated bool) error {
func (t readWriteTransaction) putFile(fkey []byte, fi protocol.FileInfo) error {
var bkey []byte
// Always set the blocks hash when there are blocks. Leave the blocks
// hash alone when there are no blocks and we might be putting a
// "truncated" FileInfo (no blocks, but the hash reference is live).
// Always set the blocks hash when there are blocks.
if len(fi.Blocks) > 0 {
fi.BlocksHash = protocol.BlocksHash(fi.Blocks)
} else if !truncated {
} else {
fi.BlocksHash = nil
}
@@ -838,16 +834,11 @@ func (t readWriteTransaction) removeFromGlobal(gk, keyBuf, folder, device, file
return keyBuf, nil
}
keyBuf, err = t.keyer.GenerateDeviceFileKey(keyBuf, folder, device, file)
var f protocol.FileIntf
keyBuf, f, err = t.getGlobalFromFileVersion(keyBuf, folder, file, true, oldGlobalFV)
if err != nil {
return nil, err
}
f, ok, err := t.getFileTrunc(keyBuf, true)
if err != nil {
return nil, err
} else if !ok {
return nil, errEntryFromGlobalMissing
}
meta.removeFile(protocol.GlobalDeviceID, f)
// Remove potential device needs

View File

@@ -83,37 +83,15 @@ func dialContextWithFallback(ctx context.Context, fallback proxy.ContextDialer,
return dialerConn{conn, newDialerAddr(network, addr)}, nil
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var proxyConn, fallbackConn net.Conn
var proxyErr, fallbackErr error
proxyDone := make(chan struct{})
fallbackDone := make(chan struct{})
go func() {
proxyConn, proxyErr = dialer.DialContext(ctx, network, addr)
l.Debugf("Dialing proxy result %s %s: %v %v", network, addr, proxyConn, proxyErr)
if proxyErr == nil {
proxyConn = dialerConn{proxyConn, newDialerAddr(network, addr)}
proxyDialFudgeAddress := func(ctx context.Context, network, address string) (net.Conn, error) {
conn, err := dialer.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
close(proxyDone)
}()
go func() {
fallbackConn, fallbackErr = fallback.DialContext(ctx, network, addr)
l.Debugf("Dialing fallback result %s %s: %v %v", network, addr, fallbackConn, fallbackErr)
close(fallbackDone)
}()
<-proxyDone
if proxyErr == nil {
go func() {
<-fallbackDone
if fallbackErr == nil {
_ = fallbackConn.Close()
}
}()
return proxyConn, nil
return dialerConn{conn, newDialerAddr(network, addr)}, err
}
<-fallbackDone
return fallbackConn, fallbackErr
return dialTwicePreferFirst(ctx, proxyDialFudgeAddress, fallback.DialContext, "proxy", "fallback", network, addr)
}
// DialContext dials via context and/or directly, depending on how it is configured.
@@ -125,19 +103,86 @@ func DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
// DialContextReusePort tries dialing via proxy if a proxy is configured, and falls back to
// a direct connection reusing the port from the connections registry, if no proxy is defined, or connecting via proxy
// fails. If the context has a timeout, the timeout might be applied twice.
// fails. It also in parallel dials without reusing the port, just in case reusing the port affects routing decisions badly.
func DialContextReusePort(ctx context.Context, network, addr string) (net.Conn, error) {
dialer := &net.Dialer{
Control: ReusePortControl,
// If proxy is configured, there is no point trying to reuse listen addresses.
if proxy.FromEnvironment() != proxy.Direct {
return DialContext(ctx, network, addr)
}
localAddrInterface := registry.Get(network, tcpAddrLess)
if localAddrInterface != nil {
if addr, ok := localAddrInterface.(*net.TCPAddr); !ok {
return nil, errUnexpectedInterfaceType
} else {
dialer.LocalAddr = addr
if localAddrInterface == nil {
// Nothing listening, nothing to reuse.
return DialContext(ctx, network, addr)
}
laddr, ok := localAddrInterface.(*net.TCPAddr)
if !ok {
return nil, errUnexpectedInterfaceType
}
// Dial twice, once reusing the listen address, another time not reusing it, just in case reusing the address
// influences routing and we fail to reach our destination.
dialer := net.Dialer{
Control: ReusePortControl,
LocalAddr: laddr,
}
return dialTwicePreferFirst(ctx, dialer.DialContext, (&net.Dialer{}).DialContext, "reuse", "non-reuse", network, addr)
}
type dialFunc func(ctx context.Context, network, address string) (net.Conn, error)
func dialTwicePreferFirst(ctx context.Context, first, second dialFunc, firstName, secondName, network, address string) (net.Conn, error) {
// Delay second dial by some time.
sleep := time.Second
if deadline, ok := ctx.Deadline(); ok {
timeout := time.Until(deadline)
if timeout > 0 {
sleep = timeout / 3
}
}
return dialContextWithFallback(ctx, dialer, network, addr)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var firstConn, secondConn net.Conn
var firstErr, secondErr error
firstDone := make(chan struct{})
secondDone := make(chan struct{})
go func() {
firstConn, firstErr = first(ctx, network, address)
l.Debugf("Dialing %s result %s %s: %v %v", firstName, network, address, firstConn, firstErr)
close(firstDone)
}()
go func() {
select {
case <-firstDone:
if firstErr == nil {
// First succeeded, no point doing anything in second
secondErr = errors.New("didn't dial")
close(secondDone)
return
}
case <-ctx.Done():
secondErr = ctx.Err()
close(secondDone)
return
case <-time.After(sleep):
}
secondConn, secondErr = second(ctx, network, address)
l.Debugf("Dialing %s result %s %s: %v %v", secondName, network, address, secondConn, secondErr)
close(secondDone)
}()
<-firstDone
if firstErr == nil {
go func() {
<-secondDone
if secondConn != nil {
_ = secondConn.Close()
}
}()
return firstConn, firstErr
}
<-secondDone
return secondConn, secondErr
}

View File

@@ -7,41 +7,21 @@
package discover
import (
"context"
"sort"
stdsync "sync"
"time"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/util"
"github.com/thejerf/suture"
"github.com/syncthing/syncthing/lib/protocol"
)
// The CachingMux aggregates results from multiple Finders. Each Finder has
// an associated cache time and negative cache time. The cache time sets how
// long we cache and return successful lookup results, the negative cache
// time sets how long we refrain from asking about the same device ID after
// receiving a negative answer. The value of zero disables caching (positive
// or negative).
type CachingMux interface {
FinderService
Add(finder Finder, cacheTime, negCacheTime time.Duration)
ChildErrors() map[string]error
}
type cachingMux struct {
*suture.Supervisor
finders []cachedFinder
caches []*cache
mut sync.RWMutex
}
// A cachedFinder is a Finder with associated cache timeouts.
type cachedFinder struct {
Finder
cacheTime time.Duration
negCacheTime time.Duration
cache *cache
token *suture.ServiceToken
}
// An error may implement cachedError, in which case it will be interrogated
@@ -51,150 +31,6 @@ type cachedError interface {
CacheFor() time.Duration
}
func NewCachingMux() CachingMux {
return &cachingMux{
Supervisor: suture.New("discover.cachingMux", suture.Spec{
PassThroughPanics: true,
}),
mut: sync.NewRWMutex(),
}
}
// Add registers a new Finder, with associated cache timeouts.
func (m *cachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration) {
m.mut.Lock()
m.finders = append(m.finders, cachedFinder{finder, cacheTime, negCacheTime})
m.caches = append(m.caches, newCache())
m.mut.Unlock()
if service, ok := finder.(suture.Service); ok {
m.Supervisor.Add(service)
}
}
// Lookup attempts to resolve the device ID using any of the added Finders,
// while obeying the cache settings.
func (m *cachingMux) Lookup(ctx context.Context, deviceID protocol.DeviceID) (addresses []string, err error) {
m.mut.RLock()
for i, finder := range m.finders {
if cacheEntry, ok := m.caches[i].Get(deviceID); ok {
// We have a cache entry. Lets see what it says.
if cacheEntry.found && time.Since(cacheEntry.when) < finder.cacheTime {
// It's a positive, valid entry. Use it.
l.Debugln("cached discovery entry for", deviceID, "at", finder)
l.Debugln(" cache:", cacheEntry)
addresses = append(addresses, cacheEntry.Addresses...)
continue
}
valid := time.Now().Before(cacheEntry.validUntil) || time.Since(cacheEntry.when) < finder.negCacheTime
if !cacheEntry.found && valid {
// It's a negative, valid entry. We should not make another
// attempt right now.
l.Debugln("negative cache entry for", deviceID, "at", finder, "valid until", cacheEntry.when.Add(finder.negCacheTime), "or", cacheEntry.validUntil)
continue
}
// It's expired. Ignore and continue.
}
// Perform the actual lookup and cache the result.
if addrs, err := finder.Lookup(ctx, deviceID); err == nil {
l.Debugln("lookup for", deviceID, "at", finder)
l.Debugln(" addresses:", addrs)
addresses = append(addresses, addrs...)
m.caches[i].Set(deviceID, CacheEntry{
Addresses: addrs,
when: time.Now(),
found: len(addrs) > 0,
})
} else {
// Lookup returned error, add a negative cache entry.
entry := CacheEntry{
when: time.Now(),
found: false,
}
if err, ok := err.(cachedError); ok {
entry.validUntil = time.Now().Add(err.CacheFor())
}
m.caches[i].Set(deviceID, entry)
}
}
m.mut.RUnlock()
addresses = util.UniqueTrimmedStrings(addresses)
sort.Strings(addresses)
l.Debugln("lookup results for", deviceID)
l.Debugln(" addresses: ", addresses)
return addresses, nil
}
func (m *cachingMux) String() string {
return "discovery cache"
}
func (m *cachingMux) Error() error {
return nil
}
func (m *cachingMux) ChildErrors() map[string]error {
children := make(map[string]error, len(m.finders))
m.mut.RLock()
for _, f := range m.finders {
children[f.String()] = f.Error()
}
m.mut.RUnlock()
return children
}
func (m *cachingMux) Cache() map[protocol.DeviceID]CacheEntry {
// Res will be the "total" cache, i.e. the union of our cache and all our
// children's caches.
res := make(map[protocol.DeviceID]CacheEntry)
m.mut.RLock()
for i := range m.finders {
// Each finder[i] has a corresponding cache at cache[i]. Go through
// it and populate the total, appending any addresses and keeping
// the newest "when" time. We skip any negative cache entries.
for k, v := range m.caches[i].Cache() {
if v.found {
cur := res[k]
if v.when.After(cur.when) {
cur.when = v.when
}
cur.Addresses = append(cur.Addresses, v.Addresses...)
res[k] = cur
}
}
// Then ask the finder itself for its cache and do the same. If this
// finder is a global discovery client, it will have no cache. If it's
// a local discovery client, this will be its current state.
for k, v := range m.finders[i].Cache() {
if v.found {
cur := res[k]
if v.when.After(cur.when) {
cur.when = v.when
}
cur.Addresses = append(cur.Addresses, v.Addresses...)
res[k] = cur
}
}
}
m.mut.RUnlock()
for k, v := range res {
v.Addresses = util.UniqueTrimmedStrings(v.Addresses)
res[k] = v
}
return res
}
// A cache can be embedded wherever useful
type cache struct {

View File

@@ -8,10 +8,13 @@ package discover
import (
"context"
"crypto/tls"
"reflect"
"testing"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -30,15 +33,19 @@ func TestCacheUnique(t *testing.T) {
"tcp://192.0.2.44:22000",
}
c := NewCachingMux()
c.(*cachingMux).ServeBackground()
cfg := config.New(protocol.LocalDeviceID)
cfg.Options.LocalAnnEnabled = false
cfg.Options.GlobalAnnEnabled = false
c := NewManager(protocol.LocalDeviceID, config.Wrap("", cfg, events.NoopLogger), tls.Certificate{}, events.NoopLogger, nil).(*manager)
c.ServeBackground()
defer c.Stop()
// Add a fake discovery service and verify we get its answers through the
// cache.
f1 := &fakeDiscovery{addresses0}
c.Add(f1, time.Minute, 0)
c.addLocked("f1", f1, time.Minute, 0)
ctx := context.Background()
@@ -54,7 +61,7 @@ func TestCacheUnique(t *testing.T) {
// duplicate or otherwise mess up the responses now.
f2 := &fakeDiscovery{addresses1}
c.Add(f2, time.Minute, 0)
c.addLocked("f2", f2, time.Minute, 0)
addr, err = c.Lookup(ctx, protocol.LocalDeviceID)
if err != nil {
@@ -86,15 +93,19 @@ func (f *fakeDiscovery) Cache() map[protocol.DeviceID]CacheEntry {
}
func TestCacheSlowLookup(t *testing.T) {
c := NewCachingMux()
c.(*cachingMux).ServeBackground()
cfg := config.New(protocol.LocalDeviceID)
cfg.Options.LocalAnnEnabled = false
cfg.Options.GlobalAnnEnabled = false
c := NewManager(protocol.LocalDeviceID, config.Wrap("", cfg, events.NoopLogger), tls.Certificate{}, events.NoopLogger, nil).(*manager)
c.ServeBackground()
defer c.Stop()
// Add a slow discovery service.
started := make(chan struct{})
f1 := &slowDiscovery{time.Second, started}
c.Add(f1, time.Minute, 0)
c.addLocked("f1", f1, time.Minute, 0)
// Start a lookup, which will take at least a second

View File

@@ -37,11 +37,6 @@ type FinderService interface {
suture.Service
}
type FinderMux interface {
Finder
ChildStatus() map[string]error
}
// The AddressLister answers questions about what addresses we are listening
// on.
type AddressLister interface {

View File

@@ -12,6 +12,7 @@ import (
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
@@ -448,3 +449,15 @@ func (c *contextClient) Post(ctx context.Context, url, ctype string, data io.Rea
req.Header.Set("Content-Type", ctype)
return c.Client.Do(req)
}
func globalDiscoveryIdentity(addr string) string {
return "global discovery server " + addr
}
func ipv4Identity(port int) string {
return fmt.Sprintf("IPv4 local broadcast discovery on port %d", port)
}
func ipv6Identity(addr string) string {
return fmt.Sprintf("IPv6 local multicast discovery on address %s", addr)
}

297
lib/discover/manager.go Normal file
View File

@@ -0,0 +1,297 @@
// Copyright (C) 2020 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 discover
import (
"context"
"crypto/tls"
"fmt"
"sort"
"time"
"github.com/thejerf/suture"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/util"
)
// The Manager aggregates results from multiple Finders. Each Finder has
// an associated cache time and negative cache time. The cache time sets how
// long we cache and return successful lookup results, the negative cache
// time sets how long we refrain from asking about the same device ID after
// receiving a negative answer. The value of zero disables caching (positive
// or negative).
type Manager interface {
FinderService
ChildErrors() map[string]error
}
type manager struct {
*suture.Supervisor
myID protocol.DeviceID
cfg config.Wrapper
cert tls.Certificate
evLogger events.Logger
addressLister AddressLister
finders map[string]cachedFinder
mut sync.RWMutex
}
func NewManager(myID protocol.DeviceID, cfg config.Wrapper, cert tls.Certificate, evLogger events.Logger, lister AddressLister) Manager {
return &manager{
Supervisor: suture.New("discover.Manager", suture.Spec{
PassThroughPanics: true,
}),
myID: myID,
cfg: cfg,
cert: cert,
evLogger: evLogger,
addressLister: lister,
finders: make(map[string]cachedFinder),
mut: sync.NewRWMutex(),
}
}
func (m *manager) Serve() {
m.cfg.Subscribe(m)
defer m.cfg.Unsubscribe(m)
m.CommitConfiguration(config.Configuration{}, m.cfg.RawCopy())
m.Supervisor.Serve()
}
func (m *manager) addLocked(identity string, finder Finder, cacheTime, negCacheTime time.Duration) {
entry := cachedFinder{
Finder: finder,
cacheTime: cacheTime,
negCacheTime: negCacheTime,
cache: newCache(),
token: nil,
}
if service, ok := finder.(suture.Service); ok {
token := m.Supervisor.Add(service)
entry.token = &token
}
m.finders[identity] = entry
l.Infoln("Using discovery mechanism:", identity)
}
func (m *manager) removeLocked(identity string) {
entry, ok := m.finders[identity]
if !ok {
return
}
if entry.token != nil {
err := m.Supervisor.Remove(*entry.token)
if err != nil {
l.Warnf("removing discovery %s: %s", identity, err)
}
}
delete(m.finders, identity)
l.Infoln("Stopped using discovery mechanism: ", identity)
}
// Lookup attempts to resolve the device ID using any of the added Finders,
// while obeying the cache settings.
func (m *manager) Lookup(ctx context.Context, deviceID protocol.DeviceID) (addresses []string, err error) {
m.mut.RLock()
for _, finder := range m.finders {
if cacheEntry, ok := finder.cache.Get(deviceID); ok {
// We have a cache entry. Lets see what it says.
if cacheEntry.found && time.Since(cacheEntry.when) < finder.cacheTime {
// It's a positive, valid entry. Use it.
l.Debugln("cached discovery entry for", deviceID, "at", finder)
l.Debugln(" cache:", cacheEntry)
addresses = append(addresses, cacheEntry.Addresses...)
continue
}
valid := time.Now().Before(cacheEntry.validUntil) || time.Since(cacheEntry.when) < finder.negCacheTime
if !cacheEntry.found && valid {
// It's a negative, valid entry. We should not make another
// attempt right now.
l.Debugln("negative cache entry for", deviceID, "at", finder, "valid until", cacheEntry.when.Add(finder.negCacheTime), "or", cacheEntry.validUntil)
continue
}
// It's expired. Ignore and continue.
}
// Perform the actual lookup and cache the result.
if addrs, err := finder.Lookup(ctx, deviceID); err == nil {
l.Debugln("lookup for", deviceID, "at", finder)
l.Debugln(" addresses:", addrs)
addresses = append(addresses, addrs...)
finder.cache.Set(deviceID, CacheEntry{
Addresses: addrs,
when: time.Now(),
found: len(addrs) > 0,
})
} else {
// Lookup returned error, add a negative cache entry.
entry := CacheEntry{
when: time.Now(),
found: false,
}
if err, ok := err.(cachedError); ok {
entry.validUntil = time.Now().Add(err.CacheFor())
}
finder.cache.Set(deviceID, entry)
}
}
m.mut.RUnlock()
addresses = util.UniqueTrimmedStrings(addresses)
sort.Strings(addresses)
l.Debugln("lookup results for", deviceID)
l.Debugln(" addresses: ", addresses)
return addresses, nil
}
func (m *manager) String() string {
return "discovery cache"
}
func (m *manager) Error() error {
return nil
}
func (m *manager) ChildErrors() map[string]error {
children := make(map[string]error, len(m.finders))
m.mut.RLock()
for _, f := range m.finders {
children[f.String()] = f.Error()
}
m.mut.RUnlock()
return children
}
func (m *manager) Cache() map[protocol.DeviceID]CacheEntry {
// Res will be the "total" cache, i.e. the union of our cache and all our
// children's caches.
res := make(map[protocol.DeviceID]CacheEntry)
m.mut.RLock()
for _, finder := range m.finders {
// Each finder[i] has a corresponding cache. Go through
// it and populate the total, appending any addresses and keeping
// the newest "when" time. We skip any negative cache finders.
for k, v := range finder.cache.Cache() {
if v.found {
cur := res[k]
if v.when.After(cur.when) {
cur.when = v.when
}
cur.Addresses = append(cur.Addresses, v.Addresses...)
res[k] = cur
}
}
// Then ask the finder itself for its cache and do the same. If this
// finder is a global discovery client, it will have no cache. If it's
// a local discovery client, this will be its current state.
for k, v := range finder.Cache() {
if v.found {
cur := res[k]
if v.when.After(cur.when) {
cur.when = v.when
}
cur.Addresses = append(cur.Addresses, v.Addresses...)
res[k] = cur
}
}
}
m.mut.RUnlock()
for k, v := range res {
v.Addresses = util.UniqueTrimmedStrings(v.Addresses)
res[k] = v
}
return res
}
func (m *manager) VerifyConfiguration(_, _ config.Configuration) error {
return nil
}
func (m *manager) CommitConfiguration(_, to config.Configuration) (handled bool) {
m.mut.Lock()
defer m.mut.Unlock()
toIdentities := make(map[string]struct{})
if to.Options.GlobalAnnEnabled {
for _, srv := range to.Options.GlobalDiscoveryServers() {
toIdentities[globalDiscoveryIdentity(srv)] = struct{}{}
}
}
if to.Options.LocalAnnEnabled {
toIdentities[ipv4Identity(to.Options.LocalAnnPort)] = struct{}{}
toIdentities[ipv6Identity(to.Options.LocalAnnMCAddr)] = struct{}{}
}
// Remove things that we're not expected to have.
for identity := range m.finders {
if _, ok := toIdentities[identity]; !ok {
m.removeLocked(identity)
}
}
// Add things we don't have.
if to.Options.GlobalAnnEnabled {
for _, srv := range to.Options.GlobalDiscoveryServers() {
identity := globalDiscoveryIdentity(srv)
// Skip, if it's already running.
if _, ok := m.finders[identity]; ok {
continue
}
gd, err := NewGlobal(srv, m.cert, m.addressLister, m.evLogger)
if err != nil {
l.Warnln("Global discovery:", err)
continue
}
// Each global discovery server gets its results cached for five
// minutes, and is not asked again for a minute when it's returned
// unsuccessfully.
m.addLocked(identity, gd, 5*time.Minute, time.Minute)
}
}
if to.Options.LocalAnnEnabled {
// v4 broadcasts
v4Identity := ipv4Identity(to.Options.LocalAnnPort)
if _, ok := m.finders[v4Identity]; !ok {
bcd, err := NewLocal(m.myID, fmt.Sprintf(":%d", to.Options.LocalAnnPort), m.addressLister, m.evLogger)
if err != nil {
l.Warnln("IPv4 local discovery:", err)
} else {
m.addLocked(v4Identity, bcd, 0, 0)
}
}
// v6 multicasts
v6Identity := ipv6Identity(to.Options.LocalAnnMCAddr)
if _, ok := m.finders[v6Identity]; !ok {
mcd, err := NewLocal(m.myID, to.Options.LocalAnnMCAddr, m.addressLister, m.evLogger)
if err != nil {
l.Warnln("IPv6 local discovery:", err)
} else {
m.addLocked(v6Identity, mcd, 0, 0)
}
}
}
return true
}

View File

@@ -23,13 +23,24 @@ var (
ErrNotRelative = errors.New("not a relative path")
)
func WithJunctionsAsDirs() Option {
return func(fs Filesystem) {
if basic, ok := fs.(*BasicFilesystem); !ok {
l.Warnln("WithJunctionsAsDirs must only be used with FilesystemTypeBasic")
} else {
basic.junctionsAsDirs = true
}
}
}
// The BasicFilesystem implements all aspects by delegating to package os.
// All paths are relative to the root and cannot (should not) escape the root directory.
type BasicFilesystem struct {
root string
root string
junctionsAsDirs bool
}
func newBasicFilesystem(root string) *BasicFilesystem {
func newBasicFilesystem(root string, opts ...Option) *BasicFilesystem {
if root == "" {
root = "." // Otherwise "" becomes "/" below
}
@@ -64,7 +75,13 @@ func newBasicFilesystem(root string) *BasicFilesystem {
root = longFilenameSupport(root)
}
return &BasicFilesystem{root}
fs := &BasicFilesystem{
root: root,
}
for _, opt := range opts {
opt(fs)
}
return fs
}
// rooted expands the relative path to the full path that is then used with os
@@ -145,7 +162,7 @@ func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
if err != nil {
return nil, err
}
fi, err := underlyingLstat(name)
fi, err := f.underlyingLstat(name)
if err != nil {
return nil, err
}

View File

@@ -4,7 +4,7 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
// +build linux
// +build linux,!ppc,!ppc64,!ppc64le
package fs

View File

@@ -16,7 +16,7 @@ import (
// Lstat is like os.Lstat, except lobotomized for Android. See
// https://forum.syncthing.net/t/2395
func underlyingLstat(name string) (fi os.FileInfo, err error) {
func (*BasicFilesystem) underlyingLstat(name string) (fi os.FileInfo, err error) {
for i := 0; i < 10; i++ { // We have to draw the line somewhere
fi, err = os.Lstat(name)
if err, ok := err.(*os.PathError); ok && err.Err == syscall.EINTR {

View File

@@ -10,6 +10,6 @@ package fs
import "os"
func underlyingLstat(name string) (fi os.FileInfo, err error) {
func (*BasicFilesystem) underlyingLstat(name string) (fi os.FileInfo, err error) {
return os.Lstat(name)
}

View File

@@ -66,12 +66,12 @@ func (fi *dirJunctFileInfo) IsDir() bool {
return true
}
func underlyingLstat(name string) (os.FileInfo, error) {
func (f *BasicFilesystem) underlyingLstat(name string) (os.FileInfo, error) {
var fi, err = os.Lstat(name)
// NTFS directory junctions are treated as ordinary directories,
// NTFS directory junctions can be treated as ordinary directories,
// see https://forum.syncthing.net/t/option-to-follow-directory-junctions-symbolic-links/14750
if err == nil && fi.Mode()&os.ModeSymlink != 0 {
if err == nil && f.junctionsAsDirs && fi.Mode()&os.ModeSymlink != 0 {
var isJunct bool
isJunct, err = isDirectoryJunction(name)
if err == nil && isJunct {

View File

@@ -1,13 +1,13 @@
// Copyright (C) 2017 The Syncthing Authors.
// Copyright (C) 2020 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 noupgrade
// +build !windows
package build
package fs
func init() {
Tags = append(Tags, "noupgrade")
func newBasicRealCaser(fs Filesystem) realCaser {
return newDefaultRealCaser(fs)
}

View File

@@ -0,0 +1,58 @@
// Copyright (C) 2020 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
import (
"path/filepath"
"strings"
"syscall"
)
type basicRealCaserWindows struct {
uri string
}
func newBasicRealCaser(fs Filesystem) realCaser {
return &basicRealCaserWindows{fs.URI()}
}
// RealCase returns the correct case for the given name, which is a relative
// path below root, as it exists on disk.
func (r *basicRealCaserWindows) realCase(name string) (string, error) {
if name == "." {
return ".", nil
}
path := r.uri
comps := strings.Split(name, string(PathSeparator))
var err error
for i, comp := range comps {
path = filepath.Join(path, comp)
comps[i], err = r.realCaseBase(path)
if err != nil {
return "", err
}
}
return filepath.Join(comps...), nil
}
func (*basicRealCaserWindows) realCaseBase(path string) (string, error) {
p, err := syscall.UTF16PtrFromString(fixLongPath(path))
if err != nil {
return "", err
}
var fd syscall.Win32finddata
h, err := syscall.FindFirstFile(p, &fd)
if err != nil {
return "", err
}
syscall.FindClose(h)
return syscall.UTF16ToString(fd.FileName[:]), nil
}
func (r *basicRealCaserWindows) dropCache() {}

448
lib/fs/casefs.go Normal file
View File

@@ -0,0 +1,448 @@
// Copyright (C) 2020 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
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"sync"
"time"
)
// Both values were chosen by magic.
const (
caseCacheTimeout = time.Second
// When the number of names (all lengths of []string from DirNames)
// exceeds this, we drop the cache.
caseMaxCachedNames = 1 << 20
)
type ErrCaseConflict struct {
given, real string
}
func (e *ErrCaseConflict) Error() string {
return fmt.Sprintf(`given name "%v" differs from name in filesystem "%v"`, e.given, e.real)
}
func IsErrCaseConflict(err error) bool {
e := &ErrCaseConflict{}
return errors.As(err, &e)
}
type realCaser interface {
realCase(name string) (string, error)
dropCache()
}
type fskey struct {
fstype FilesystemType
uri string
}
var (
caseFilesystems = make(map[fskey]Filesystem)
caseFilesystemsMut sync.Mutex
)
// caseFilesystem is a BasicFilesystem with additional checks to make a
// potentially case insensitive underlying FS behave like it's case-sensitive.
type caseFilesystem struct {
Filesystem
realCaser
}
// NewCaseFilesystem ensures that the given, potentially case-insensitive filesystem
// behaves like a case-sensitive filesystem. Meaning that it takes into account
// the real casing of a path and returns ErrCaseConflict if the given path differs
// from the real path. It is safe to use with any filesystem, i.e. also a
// case-sensitive one. However it will add some overhead and thus shouldn't be
// used if the filesystem is known to already behave case-sensitively.
func NewCaseFilesystem(fs Filesystem) Filesystem {
caseFilesystemsMut.Lock()
defer caseFilesystemsMut.Unlock()
k := fskey{fs.Type(), fs.URI()}
if caseFs, ok := caseFilesystems[k]; ok {
return caseFs
}
caseFs := &caseFilesystem{
Filesystem: fs,
}
switch k.fstype {
case FilesystemTypeBasic:
caseFs.realCaser = newBasicRealCaser(fs)
default:
caseFs.realCaser = newDefaultRealCaser(fs)
}
caseFilesystems[k] = caseFs
return caseFs
}
func (f *caseFilesystem) Chmod(name string, mode FileMode) error {
if err := f.checkCase(name); err != nil {
return err
}
return f.Filesystem.Chmod(name, mode)
}
func (f *caseFilesystem) Lchown(name string, uid, gid int) error {
if err := f.checkCase(name); err != nil {
return err
}
return f.Filesystem.Lchown(name, uid, gid)
}
func (f *caseFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
if err := f.checkCase(name); err != nil {
return err
}
return f.Filesystem.Chtimes(name, atime, mtime)
}
func (f *caseFilesystem) Mkdir(name string, perm FileMode) error {
if err := f.checkCase(name); err != nil {
return err
}
if err := f.Filesystem.Mkdir(name, perm); err != nil {
return err
}
f.dropCache()
return nil
}
func (f *caseFilesystem) MkdirAll(path string, perm FileMode) error {
if err := f.checkCase(path); err != nil {
return err
}
if err := f.Filesystem.MkdirAll(path, perm); err != nil {
return err
}
f.dropCache()
return nil
}
func (f *caseFilesystem) Lstat(name string) (FileInfo, error) {
var err error
if name, err = Canonicalize(name); err != nil {
return nil, err
}
stat, err := f.Filesystem.Lstat(name)
if err != nil {
return nil, err
}
if err = f.checkCaseExisting(name); err != nil {
return nil, err
}
return stat, nil
}
func (f *caseFilesystem) Remove(name string) error {
if err := f.checkCase(name); err != nil {
return err
}
if err := f.Filesystem.Remove(name); err != nil {
return err
}
f.dropCache()
return nil
}
func (f *caseFilesystem) RemoveAll(name string) error {
if err := f.checkCase(name); err != nil {
return err
}
if err := f.Filesystem.RemoveAll(name); err != nil {
return err
}
f.dropCache()
return nil
}
func (f *caseFilesystem) Rename(oldpath, newpath string) error {
if err := f.checkCase(oldpath); err != nil {
return err
}
if err := f.Filesystem.Rename(oldpath, newpath); err != nil {
return err
}
f.dropCache()
return nil
}
func (f *caseFilesystem) Stat(name string) (FileInfo, error) {
var err error
if name, err = Canonicalize(name); err != nil {
return nil, err
}
stat, err := f.Filesystem.Stat(name)
if err != nil {
return nil, err
}
if err = f.checkCaseExisting(name); err != nil {
return nil, err
}
return stat, nil
}
func (f *caseFilesystem) DirNames(name string) ([]string, error) {
if err := f.checkCase(name); err != nil {
return nil, err
}
return f.Filesystem.DirNames(name)
}
func (f *caseFilesystem) Open(name string) (File, error) {
if err := f.checkCase(name); err != nil {
return nil, err
}
return f.Filesystem.Open(name)
}
func (f *caseFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
if err := f.checkCase(name); err != nil {
return nil, err
}
file, err := f.Filesystem.OpenFile(name, flags, mode)
if err != nil {
return nil, err
}
f.dropCache()
return file, nil
}
func (f *caseFilesystem) ReadSymlink(name string) (string, error) {
if err := f.checkCase(name); err != nil {
return "", err
}
return f.Filesystem.ReadSymlink(name)
}
func (f *caseFilesystem) Create(name string) (File, error) {
if err := f.checkCase(name); err != nil {
return nil, err
}
file, err := f.Filesystem.Create(name)
if err != nil {
return nil, err
}
f.dropCache()
return file, nil
}
func (f *caseFilesystem) CreateSymlink(target, name string) error {
if err := f.checkCase(name); err != nil {
return err
}
if err := f.Filesystem.CreateSymlink(target, name); err != nil {
return err
}
f.dropCache()
return nil
}
func (f *caseFilesystem) Walk(root string, walkFn WalkFunc) error {
// Walking the filesystem is likely (in Syncthing's case certainly) done
// to pick up external changes, for which caching is undesirable.
f.dropCache()
if err := f.checkCase(root); err != nil {
return err
}
return f.Filesystem.Walk(root, walkFn)
}
func (f *caseFilesystem) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, <-chan error, error) {
if err := f.checkCase(path); err != nil {
return nil, nil, err
}
return f.Filesystem.Watch(path, ignore, ctx, ignorePerms)
}
func (f *caseFilesystem) Hide(name string) error {
if err := f.checkCase(name); err != nil {
return err
}
return f.Filesystem.Hide(name)
}
func (f *caseFilesystem) Unhide(name string) error {
if err := f.checkCase(name); err != nil {
return err
}
return f.Filesystem.Unhide(name)
}
func (f *caseFilesystem) checkCase(name string) error {
var err error
if name, err = Canonicalize(name); err != nil {
return err
}
// Stat is necessary for case sensitive FS, as it's then not a conflict
// if name is e.g. "foo" and on dir there is "Foo".
if _, err := f.Filesystem.Lstat(name); err != nil {
if IsNotExist(err) {
return nil
}
return err
}
return f.checkCaseExisting(name)
}
// checkCaseExisting must only be called after successfully canonicalizing and
// stating the file.
func (f *caseFilesystem) checkCaseExisting(name string) error {
realName, err := f.realCase(name)
if IsNotExist(err) {
// It did exist just before -> cache is outdated, try again
f.dropCache()
realName, err = f.realCase(name)
}
if err != nil {
return err
}
if realName != name {
return &ErrCaseConflict{name, realName}
}
return nil
}
type defaultRealCaser struct {
fs Filesystem
root *caseNode
count int
timer *time.Timer
timerStop chan struct{}
mut sync.RWMutex
}
func newDefaultRealCaser(fs Filesystem) *defaultRealCaser {
caser := &defaultRealCaser{
fs: fs,
root: &caseNode{name: "."},
timer: time.NewTimer(0),
}
<-caser.timer.C
return caser
}
func (r *defaultRealCaser) realCase(name string) (string, error) {
out := "."
if name == out {
return out, nil
}
r.mut.Lock()
defer func() {
if r.count > caseMaxCachedNames {
select {
case r.timerStop <- struct{}{}:
default:
}
r.dropCacheLocked()
}
r.mut.Unlock()
}()
node := r.root
for _, comp := range strings.Split(name, string(PathSeparator)) {
if node.dirNames == nil {
// Haven't called DirNames yet
var err error
node.dirNames, err = r.fs.DirNames(out)
if err != nil {
return "", err
}
node.dirNamesLower = make([]string, len(node.dirNames))
for i, n := range node.dirNames {
node.dirNamesLower[i] = UnicodeLowercase(n)
}
node.children = make(map[string]*caseNode)
node.results = make(map[string]*caseNode)
r.count += len(node.dirNames)
} else if child, ok := node.results[comp]; ok {
// Check if this exact name has been queried before to shortcut
node = child
out = filepath.Join(out, child.name)
continue
}
// Actually loop dirNames to search for a match
n, err := findCaseInsensitiveMatch(comp, node.dirNames, node.dirNamesLower)
if err != nil {
return "", err
}
child, ok := node.children[n]
if !ok {
child = &caseNode{name: n}
}
node.results[comp] = child
node.children[n] = child
node = child
out = filepath.Join(out, n)
}
return out, nil
}
func (r *defaultRealCaser) startCaseResetTimerLocked() {
r.timerStop = make(chan struct{})
r.timer.Reset(caseCacheTimeout)
go func() {
select {
case <-r.timer.C:
r.dropCache()
case <-r.timerStop:
if !r.timer.Stop() {
<-r.timer.C
}
r.mut.Lock()
r.timerStop = nil
r.mut.Unlock()
}
}()
}
func (r *defaultRealCaser) dropCache() {
r.mut.Lock()
r.dropCacheLocked()
r.mut.Unlock()
}
func (r *defaultRealCaser) dropCacheLocked() {
r.root = &caseNode{name: "."}
r.count = 0
}
// Both name and the key to children are "Real", case resolved names of the path
// component this node represents (i.e. containing no path separator).
// The key to results is also a path component, but as given to RealCase, not
// case resolved.
type caseNode struct {
name string
dirNames []string
dirNamesLower []string
children map[string]*caseNode
results map[string]*caseNode
}
func findCaseInsensitiveMatch(name string, names, namesLower []string) (string, error) {
lower := UnicodeLowercase(name)
candidate := ""
for i, n := range names {
if n == name {
return n, nil
}
if candidate == "" && namesLower[i] == lower {
candidate = n
}
}
if candidate == "" {
return "", ErrNotExist
}
return candidate, nil
}

279
lib/fs/casefs_test.go Normal file
View File

@@ -0,0 +1,279 @@
// Copyright (C) 2020 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
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
func TestRealCase(t *testing.T) {
// Verify realCase lookups on various underlying filesystems.
t.Run("fake-sensitive", func(t *testing.T) {
testRealCase(t, newFakeFilesystem(t.Name()))
})
t.Run("fake-insensitive", func(t *testing.T) {
testRealCase(t, newFakeFilesystem(t.Name()+"?insens=true"))
})
t.Run("actual", func(t *testing.T) {
fsys, tmpDir := setup(t)
defer os.RemoveAll(tmpDir)
testRealCase(t, fsys)
})
}
func testRealCase(t *testing.T, fsys Filesystem) {
testFs := NewCaseFilesystem(fsys).(*caseFilesystem)
comps := []string{"Foo", "bar", "BAZ", "bAs"}
path := filepath.Join(comps...)
testFs.MkdirAll(filepath.Join(comps[:len(comps)-1]...), 0777)
fd, err := testFs.Create(path)
if err != nil {
t.Fatal(err)
}
fd.Close()
for i, tc := range []struct {
in string
len int
}{
{path, 4},
{strings.ToLower(path), 4},
{strings.ToUpper(path), 4},
{"foo", 1},
{"FOO", 1},
{"foO", 1},
{filepath.Join("Foo", "bar"), 2},
{filepath.Join("Foo", "bAr"), 2},
{filepath.Join("FoO", "bar"), 2},
{filepath.Join("foo", "bar", "BAZ"), 3},
{filepath.Join("Foo", "bar", "bAz"), 3},
{filepath.Join("foo", "bar", "BAZ"), 3}, // Repeat on purpose
} {
out, err := testFs.realCase(tc.in)
if err != nil {
t.Error(err)
} else if exp := filepath.Join(comps[:tc.len]...); out != exp {
t.Errorf("tc %v: Expected %v, got %v", i, exp, out)
}
}
}
func TestRealCaseSensitive(t *testing.T) {
// Verify that realCase returns the best on-disk case for case sensitive
// systems. Test is skipped if the underlying fs is insensitive.
t.Run("fake-sensitive", func(t *testing.T) {
testRealCaseSensitive(t, newFakeFilesystem(t.Name()))
})
t.Run("actual", func(t *testing.T) {
fsys, tmpDir := setup(t)
defer os.RemoveAll(tmpDir)
testRealCaseSensitive(t, fsys)
})
}
func testRealCaseSensitive(t *testing.T, fsys Filesystem) {
testFs := NewCaseFilesystem(fsys).(*caseFilesystem)
names := make([]string, 2)
names[0] = "foo"
names[1] = strings.ToUpper(names[0])
for _, n := range names {
if err := testFs.MkdirAll(n, 0777); err != nil {
if IsErrCaseConflict(err) {
t.Skip("Filesystem is case-insensitive")
}
t.Fatal(err)
}
}
for _, n := range names {
if rn, err := testFs.realCase(n); err != nil {
t.Error(err)
} else if rn != n {
t.Errorf("Got %v, expected %v", rn, n)
}
}
}
func TestCaseFSStat(t *testing.T) {
// Verify that a Stat() lookup behaves in a case sensitive manner
// regardless of the underlying fs.
t.Run("fake-sensitive", func(t *testing.T) {
testCaseFSStat(t, newFakeFilesystem(t.Name()))
})
t.Run("fake-insensitive", func(t *testing.T) {
testCaseFSStat(t, newFakeFilesystem(t.Name()+"?insens=true"))
})
t.Run("actual", func(t *testing.T) {
fsys, tmpDir := setup(t)
defer os.RemoveAll(tmpDir)
testCaseFSStat(t, fsys)
})
}
func testCaseFSStat(t *testing.T, fsys Filesystem) {
fd, err := fsys.Create("foo")
if err != nil {
t.Fatal(err)
}
fd.Close()
// Check if the underlying fs is sensitive or not
sensitive := true
if _, err = fsys.Stat("FOO"); err == nil {
sensitive = false
}
testFs := NewCaseFilesystem(fsys)
_, err = testFs.Stat("FOO")
if sensitive {
if IsNotExist(err) {
t.Log("pass: case sensitive underlying fs")
} else {
t.Error("expected NotExist, not", err, "for sensitive fs")
}
} else if IsErrCaseConflict(err) {
t.Log("pass: case insensitive underlying fs")
} else {
t.Error("expected ErrCaseConflict, not", err, "for insensitive fs")
}
}
func BenchmarkWalkCaseFakeFS10k(b *testing.B) {
fsys, paths, err := fakefsForBenchmark(10_000, 0)
if err != nil {
b.Fatal(err)
}
slowsys, paths, err := fakefsForBenchmark(10_000, 100*time.Microsecond)
if err != nil {
b.Fatal(err)
}
b.Run("raw-fastfs", func(b *testing.B) {
benchmarkWalkFakeFS(b, fsys, paths)
b.ReportAllocs()
})
b.Run("case-fastfs", func(b *testing.B) {
benchmarkWalkFakeFS(b, NewCaseFilesystem(fsys), paths)
b.ReportAllocs()
})
b.Run("raw-slowfs", func(b *testing.B) {
benchmarkWalkFakeFS(b, slowsys, paths)
b.ReportAllocs()
})
b.Run("case-slowfs", func(b *testing.B) {
benchmarkWalkFakeFS(b, NewCaseFilesystem(slowsys), paths)
b.ReportAllocs()
})
}
func benchmarkWalkFakeFS(b *testing.B, fsys Filesystem, paths []string) {
// Simulate a scanner pass over the filesystem. First walk it to
// discover all names, then stat each name individually to check if it's
// been deleted or not (pretending that they all existed in the
// database).
var ms0 runtime.MemStats
runtime.ReadMemStats(&ms0)
t0 := time.Now()
for i := 0; i < b.N; i++ {
if err := doubleWalkFS(fsys, paths); err != nil {
b.Fatal(err)
}
}
t1 := time.Now()
var ms1 runtime.MemStats
runtime.ReadMemStats(&ms1)
// We add metrics per path entry
b.ReportMetric(float64(t1.Sub(t0))/float64(b.N)/float64(len(paths)), "ns/entry")
b.ReportMetric(float64(ms1.Mallocs-ms0.Mallocs)/float64(b.N)/float64(len(paths)), "allocs/entry")
b.ReportMetric(float64(ms1.TotalAlloc-ms0.TotalAlloc)/float64(b.N)/float64(len(paths)), "B/entry")
}
func TestStressCaseFS(t *testing.T) {
// Exercise a bunch of paralell operations for stressing out race
// conditions in the realnamer cache etc.
const limit = 10 * time.Second
if testing.Short() {
t.Skip("long test")
}
fsys, paths, err := fakefsForBenchmark(10_000, 0)
if err != nil {
t.Fatal(err)
}
for i := 0; i < runtime.NumCPU()/2+1; i++ {
t.Run(fmt.Sprintf("walker-%d", i), func(t *testing.T) {
// Walk the filesystem and stat everything
t.Parallel()
t0 := time.Now()
for time.Since(t0) < limit {
if err := doubleWalkFS(fsys, paths); err != nil {
t.Fatal(err)
}
}
})
t.Run(fmt.Sprintf("toucher-%d", i), func(t *testing.T) {
// Touch all the things
t.Parallel()
t0 := time.Now()
for time.Since(t0) < limit {
for _, p := range paths {
now := time.Now()
if err := fsys.Chtimes(p, now, now); err != nil {
t.Fatal(err)
}
}
}
})
}
}
func doubleWalkFS(fsys Filesystem, paths []string) error {
if err := fsys.Walk("/", func(path string, info FileInfo, err error) error {
return err
}); err != nil {
return err
}
for _, p := range paths {
if _, err := fsys.Lstat(p); err != nil {
return err
}
}
return nil
}
func fakefsForBenchmark(nfiles int, latency time.Duration) (Filesystem, []string, error) {
fsys := NewFilesystem(FilesystemTypeFake, fmt.Sprintf("fakefsForBenchmark?files=%d&insens=true&latency=%s", nfiles, latency))
var paths []string
if err := fsys.Walk("/", func(path string, info FileInfo, err error) error {
paths = append(paths, path)
return err
}); err != nil {
return nil, nil, err
}
if len(paths) < nfiles {
return nil, nil, errors.New("didn't find enough stuff")
}
return fsys, paths, nil
}

View File

@@ -0,0 +1,47 @@
// 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
import (
"os"
"path/filepath"
)
// DebugSymlinkForTestsOnly is not and should not be used in Syncthing code,
// hence the cumbersome name to make it obvious if this ever leaks. Its
// reason for existence is the Windows version, which allows creating
// symlinks when non-elevated.
func DebugSymlinkForTestsOnly(oldFs, newFs Filesystem, oldname, newname string) error {
if caseFs, ok := unwrapFilesystem(newFs).(*caseFilesystem); ok {
if err := caseFs.checkCase(newname); err != nil {
return err
}
caseFs.dropCache()
}
if err := os.Symlink(filepath.Join(oldFs.URI(), oldname), filepath.Join(newFs.URI(), newname)); err != nil {
return err
}
return nil
}
// unwrapFilesystem removes "wrapping" filesystems to expose the underlying filesystem.
func unwrapFilesystem(fs Filesystem) Filesystem {
for {
switch sfs := fs.(type) {
case *logFilesystem:
fs = sfs.Filesystem
case *walkFilesystem:
fs = sfs.Filesystem
case *MtimeFS:
fs = sfs.Filesystem
default:
return sfs
}
}
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package osutil
package fs
import (
"os"
@@ -17,7 +17,10 @@ import (
// This is not and should not be used in Syncthing code, hence the
// cumbersome name to make it obvious if this ever leaks. Nonetheless it's
// useful in tests.
func DebugSymlinkForTestsOnly(oldname, newname string) error {
func DebugSymlinkForTestsOnly(oldFs, newFS Filesystem, oldname, newname string) error {
oldname = filepath.Join(oldFs.URI(), oldname)
newname = filepath.Join(newFS.URI(), newname)
// CreateSymbolicLink is not supported before Windows Vista
if syscall.LoadCreateSymbolicLink() != nil {
return &os.LinkError{"symlink", oldname, newname, syscall.EWINDOWS}

View File

@@ -48,14 +48,17 @@ const randomBlockShift = 14 // 128k
// sizeavg=n to set the average size of random files, in bytes (default 1<<20)
// seed=n to set the initial random seed (default 0)
// insens=b "true" makes filesystem case-insensitive Windows- or OSX-style (default false)
// latency=d to set the amount of time each "disk" operation takes, where d is time.ParseDuration format
//
// - Two fakefs:s pointing at the same root path see the same files.
//
type fakefs struct {
uri string
mut sync.Mutex
root *fakeEntry
insens bool
withContent bool
latency time.Duration
}
var (
@@ -63,23 +66,25 @@ var (
fakefsFs = make(map[string]*fakefs)
)
func newFakeFilesystem(root string) *fakefs {
func newFakeFilesystem(rootURI string, _ ...Option) *fakefs {
fakefsMut.Lock()
defer fakefsMut.Unlock()
root := rootURI
var params url.Values
uri, err := url.Parse(root)
uri, err := url.Parse(rootURI)
if err == nil {
root = uri.Path
params = uri.Query()
}
if fs, ok := fakefsFs[root]; ok {
if fs, ok := fakefsFs[rootURI]; ok {
// Already have an fs at this path
return fs
}
fs := &fakefs{
uri: "fake://" + rootURI,
root: &fakeEntry{
name: "/",
entryType: fakeEntryTypeDir,
@@ -129,6 +134,10 @@ func newFakeFilesystem(root string) *fakefs {
// Also create a default folder marker for good measure
fs.Mkdir(".stfolder", 0700)
// We only set the latency after doing the operations required to create
// the filesystem initially.
fs.latency, _ = time.ParseDuration(params.Get("latency"))
fakefsFs[root] = fs
return fs
}
@@ -185,6 +194,7 @@ func (fs *fakefs) entryForName(name string) *fakeEntry {
func (fs *fakefs) Chmod(name string, mode FileMode) error {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
entry := fs.entryForName(name)
if entry == nil {
return os.ErrNotExist
@@ -196,6 +206,7 @@ func (fs *fakefs) Chmod(name string, mode FileMode) error {
func (fs *fakefs) Lchown(name string, uid, gid int) error {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
entry := fs.entryForName(name)
if entry == nil {
return os.ErrNotExist
@@ -208,6 +219,7 @@ func (fs *fakefs) Lchown(name string, uid, gid int) error {
func (fs *fakefs) Chtimes(name string, atime time.Time, mtime time.Time) error {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
entry := fs.entryForName(name)
if entry == nil {
return os.ErrNotExist
@@ -219,6 +231,7 @@ func (fs *fakefs) Chtimes(name string, atime time.Time, mtime time.Time) error {
func (fs *fakefs) create(name string) (*fakeEntry, error) {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
if entry := fs.entryForName(name); entry != nil {
if entry.entryType == fakeEntryTypeDir {
@@ -284,6 +297,7 @@ func (fs *fakefs) CreateSymlink(target, name string) error {
func (fs *fakefs) DirNames(name string) ([]string, error) {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
entry := fs.entryForName(name)
if entry == nil {
@@ -301,6 +315,7 @@ func (fs *fakefs) DirNames(name string) ([]string, error) {
func (fs *fakefs) Lstat(name string) (FileInfo, error) {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
entry := fs.entryForName(name)
if entry == nil {
@@ -318,6 +333,7 @@ func (fs *fakefs) Lstat(name string) (FileInfo, error) {
func (fs *fakefs) Mkdir(name string, perm FileMode) error {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
dir := filepath.Dir(name)
base := filepath.Base(name)
@@ -348,6 +364,10 @@ func (fs *fakefs) Mkdir(name string, perm FileMode) error {
}
func (fs *fakefs) MkdirAll(name string, perm FileMode) error {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
name = filepath.ToSlash(name)
name = strings.Trim(name, "/")
comps := strings.Split(name, "/")
@@ -382,6 +402,7 @@ func (fs *fakefs) MkdirAll(name string, perm FileMode) error {
func (fs *fakefs) Open(name string) (File, error) {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
entry := fs.entryForName(name)
if entry == nil || entry.entryType != fakeEntryTypeFile {
@@ -401,6 +422,7 @@ func (fs *fakefs) OpenFile(name string, flags int, mode FileMode) (File, error)
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
dir := filepath.Dir(name)
base := filepath.Base(name)
@@ -438,6 +460,7 @@ func (fs *fakefs) OpenFile(name string, flags int, mode FileMode) (File, error)
func (fs *fakefs) ReadSymlink(name string) (string, error) {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
entry := fs.entryForName(name)
if entry == nil {
@@ -451,6 +474,7 @@ func (fs *fakefs) ReadSymlink(name string) (string, error) {
func (fs *fakefs) Remove(name string) error {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
if fs.insens {
name = UnicodeLowercase(name)
@@ -472,6 +496,7 @@ func (fs *fakefs) Remove(name string) error {
func (fs *fakefs) RemoveAll(name string) error {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
if fs.insens {
name = UnicodeLowercase(name)
@@ -491,6 +516,7 @@ func (fs *fakefs) RemoveAll(name string) error {
func (fs *fakefs) Rename(oldname, newname string) error {
fs.mut.Lock()
defer fs.mut.Unlock()
time.Sleep(fs.latency)
oldKey := filepath.Base(oldname)
newKey := filepath.Base(newname)
@@ -578,7 +604,7 @@ func (fs *fakefs) Type() FilesystemType {
}
func (fs *fakefs) URI() string {
return "fake://" + fs.root.name
return fs.uri
}
func (fs *fakefs) SameFile(fi1, fi2 FileInfo) bool {

View File

@@ -178,13 +178,15 @@ var IsPermission = os.IsPermission
// IsPathSeparator is the equivalent of os.IsPathSeparator
var IsPathSeparator = os.IsPathSeparator
func NewFilesystem(fsType FilesystemType, uri string) Filesystem {
type Option func(Filesystem)
func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem {
var fs Filesystem
switch fsType {
case FilesystemTypeBasic:
fs = newBasicFilesystem(uri)
fs = newBasicFilesystem(uri, opts...)
case FilesystemTypeFake:
fs = newFakeFilesystem(uri)
fs = newFakeFilesystem(uri, opts...)
default:
l.Debugln("Unknown filesystem", fsType, uri)
fs = &errorFilesystem{

View File

@@ -11,9 +11,12 @@
package fs
import (
"errors"
"path/filepath"
)
var ErrInfiniteRecursion = errors.New("infinite filesystem recursion detected")
type ancestorDirList struct {
list []FileInfo
fs Filesystem
@@ -90,8 +93,7 @@ func (f *walkFilesystem) walk(path string, info FileInfo, walkFn WalkFunc, ances
ancestors.Push(info)
defer ancestors.Pop()
} else {
l.Warnf("Infinite filesystem recursion detected on path '%s', not walking further down", path)
return nil
return walkFn(path, info, ErrInfiniteRecursion)
}
names, err := f.DirNames(path)

View File

@@ -7,6 +7,7 @@
package fs
import (
"errors"
"fmt"
osexec "os/exec"
"path/filepath"
@@ -56,7 +57,7 @@ func testWalkTraverseDirJunct(t *testing.T, fsType FilesystemType, uri string) {
t.Skip("Directory junctions are available and tested on windows only")
}
fs := NewFilesystem(fsType, uri)
fs := NewFilesystem(fsType, uri, WithJunctionsAsDirs())
if err := fs.MkdirAll("target/foo", 0); err != nil {
t.Fatal(err)
@@ -89,7 +90,7 @@ func testWalkInfiniteRecursion(t *testing.T, fsType FilesystemType, uri string)
t.Skip("Infinite recursion detection is tested on windows only")
}
fs := NewFilesystem(fsType, uri)
fs := NewFilesystem(fsType, uri, WithJunctionsAsDirs())
if err := fs.MkdirAll("target/foo", 0); err != nil {
t.Fatal(err)
@@ -105,8 +106,16 @@ func testWalkInfiniteRecursion(t *testing.T, fsType FilesystemType, uri string)
}
dirjunctCnt := 0
fooCnt := 0
found := false
if err := fs.Walk("towalk", func(path string, info FileInfo, err error) error {
if err != nil {
if errors.Is(err, ErrInfiniteRecursion) {
if found {
t.Fatal("second infinite recursion detected at", path)
}
found = true
return nil
}
t.Fatal(err)
}
if info.Name() == "dirjunct" {
@@ -118,7 +127,7 @@ func testWalkInfiniteRecursion(t *testing.T, fsType FilesystemType, uri string)
}); err != nil {
t.Fatal(err)
}
if dirjunctCnt != 2 || fooCnt != 1 {
if dirjunctCnt != 2 || fooCnt != 1 || !found {
t.Fatal("Infinite recursion not detected correctly")
}
}

View File

@@ -334,16 +334,38 @@ func (f *folder) pull() (success bool) {
return true
}
// Abort early (before acquiring a token) if there's a folder error
err := f.getHealthErrorWithoutIgnores()
f.setError(err)
if err != nil {
l.Debugln("Skipping pull of", f.Description(), "due to folder error:", err)
return false
}
f.setState(FolderSyncWaiting)
defer f.setState(FolderIdle)
if err := f.ioLimiter.takeWithContext(f.ctx, 1); err != nil {
f.setError(err)
return true
}
defer f.ioLimiter.give(1)
startTime := time.Now()
// Check if the ignore patterns changed.
oldHash := f.ignores.Hash()
defer func() {
if f.ignores.Hash() != oldHash {
f.ignoresUpdated()
}
}()
err = f.getHealthErrorAndLoadIgnores()
f.setError(err)
if err != nil {
l.Debugln("Skipping pull of", f.Description(), "due to folder error:", err)
return false
}
success = f.puller.pull()
if success {
@@ -462,10 +484,11 @@ func (f *folder) scanSubdirs(subDirs []string) error {
// What we have locally is equivalent to the global file.
fi.Version = fi.Version.Merge(gf.Version)
fallthrough
case fi.IsDeleted() && gf.IsReceiveOnlyChanged():
case fi.IsDeleted() && (gf.IsReceiveOnlyChanged() || gf.IsDeleted()):
// Our item is deleted and the global item is our own
// receive only file. We can't delete file infos, so
// we just pretend it is a normal deleted file (nobody
// receive only file or deleted too. In the former
// case we can't delete file infos, so we just
// pretend it is a normal deleted file (nobody
// cares about that).
fi.LocalFlags &^= protocol.FlagLocalReceiveOnly
}
@@ -557,6 +580,8 @@ func (f *folder) scanSubdirs(subDirs []string) error {
}
switch ignored := f.ignores.Match(file.Name).IsIgnored(); {
case file.IsIgnored() && ignored:
return true
case !file.IsIgnored() && ignored:
// File was not ignored at last pass but has been ignored.
if file.IsDirectory() {
@@ -600,24 +625,24 @@ func (f *folder) scanSubdirs(subDirs []string) error {
// sure the file gets in sync on the following pull.
nf.Version = protocol.Vector{}
}
l.Debugln("marking file as deleted", nf)
batchAppend(nf, snap)
changes++
case file.IsDeleted() && file.IsReceiveOnlyChanged() && f.localFlags&protocol.FlagLocalReceiveOnly != 0 && len(snap.Availability(file.Name)) == 0:
file.Version = protocol.Vector{}
file.LocalFlags &^= protocol.FlagLocalReceiveOnly
l.Debugln("marking deleted item that doesn't exist anywhere as not receive-only", file)
batchAppend(file.ConvertDeletedToFileInfo(), snap)
changes++
case file.IsDeleted() && file.IsReceiveOnlyChanged() && f.Type != config.FolderTypeReceiveOnly:
// No need to bump the version for a file that was and is
// deleted and just the folder type/local flags changed.
file.LocalFlags &^= protocol.FlagLocalReceiveOnly
l.Debugln("removing receive-only flag on deleted item", file)
batchAppend(file.ConvertDeletedToFileInfo(), snap)
changes++
}
// Check for deleted, locally changed items that noone else has.
if f.localFlags&protocol.FlagLocalReceiveOnly == 0 {
return true
}
if !fi.IsDeleted() || !fi.IsReceiveOnlyChanged() || len(snap.Availability(fi.FileName())) > 0 {
return true
}
nf := fi.(db.FileInfoTruncated).ConvertDeletedToFileInfo()
nf.LocalFlags = 0
nf.Version = protocol.Vector{}
batchAppend(nf, snap)
changes++
return true
})

View File

@@ -46,11 +46,7 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
m.Index(device1, "ro", knownFiles)
f.updateLocalsFromScanning(knownFiles)
m.fmut.RLock()
snap := m.folderFiles["ro"].Snapshot()
m.fmut.RUnlock()
size := snap.GlobalSize()
snap.Release()
size := globalSize(t, m, "ro")
if size.Files != 1 || size.Directories != 1 {
t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
}
@@ -59,10 +55,10 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
must(t, m.ScanFolder("ro"))
// We should now have two files and two directories.
// We should now have two files and two directories, with global state unchanged.
size = globalSize(t, m, "ro")
if size.Files != 2 || size.Directories != 2 {
if size.Files != 1 || size.Directories != 1 {
t.Fatalf("Global: expected 2 files and 2 directories: %+v", size)
}
size = localSize(t, m, "ro")

View File

@@ -61,18 +61,17 @@ 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")
errDirPrefix = "directory has been deleted on a remote device but "
errDirHasToBeScanned = errors.New(errDirPrefix + "contains changed files, scheduling scan")
errDirHasIgnored = errors.New(errDirPrefix + "contains ignored files (see ignore documentation for (?d) prefix)")
errDirHasReceiveOnlyChanged = errors.New(errDirPrefix + "contains locally changed files")
errDirNotEmpty = errors.New(errDirPrefix + "is not empty; the contents are probably ignored on that remote device, but not locally")
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"
activity = newDeviceActivity()
errNoDevice = errors.New("peers who had this file went away, or the file has changed while syncing. will retry later")
errDirPrefix = "directory has been deleted on a remote device but "
errDirHasToBeScanned = errors.New(errDirPrefix + "contains changed files, scheduling scan")
errDirHasIgnored = errors.New(errDirPrefix + "contains ignored files (see ignore documentation for (?d) prefix)")
errDirNotEmpty = errors.New(errDirPrefix + "is not empty; the contents are probably ignored on that remote device, but not locally")
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"
)
type dbUpdateType int
@@ -129,9 +128,9 @@ type sendReceiveFolder struct {
blockPullReorderer blockPullReorderer
writeLimiter *byteSemaphore
pullErrors map[string]string // errors for most recent/current iteration
oldPullErrors map[string]string // errors from previous iterations for log filtering only
pullErrorsMut sync.Mutex
pullErrors map[string]string // actual exposed pull errors
tempPullErrors map[string]string // pull errors that might be just transient
pullErrorsMut sync.Mutex
}
func newSendReceiveFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem, evLogger events.Logger, ioLimiter *byteSemaphore) service {
@@ -166,20 +165,6 @@ func newSendReceiveFolder(model *model, fset *db.FileSet, ignores *ignore.Matche
// pull returns true if it manages to get all needed items from peers, i.e. get
// the device in sync with the global state.
func (f *sendReceiveFolder) pull() bool {
// Check if the ignore patterns changed.
oldHash := f.ignores.Hash()
defer func() {
if f.ignores.Hash() != oldHash {
f.ignoresUpdated()
}
}()
err := f.getHealthErrorAndLoadIgnores()
f.setError(err)
if err != nil {
l.Debugln("Skipping pull of", f.Description(), "due to folder error:", err)
return false
}
l.Debugf("%v pulling", f)
scanChan := make(chan string)
@@ -192,6 +177,10 @@ func (f *sendReceiveFolder) pull() bool {
changed := 0
f.pullErrorsMut.Lock()
f.pullErrors = nil
f.pullErrorsMut.Unlock()
for tries := 0; tries < maxPullerIterations; tries++ {
select {
case <-f.ctx.Done():
@@ -216,8 +205,14 @@ func (f *sendReceiveFolder) pull() bool {
}
f.pullErrorsMut.Lock()
f.pullErrors = f.tempPullErrors
f.tempPullErrors = nil
for path, err := range f.pullErrors {
l.Infof("Puller (folder %s, item %q): %v", f.Description(), path, err)
}
pullErrNum := len(f.pullErrors)
f.pullErrorsMut.Unlock()
if pullErrNum > 0 {
l.Infof("%v: Failed to sync %v items", f.Description(), pullErrNum)
f.evLogger.Log(events.FolderErrors, map[string]interface{}{
@@ -235,8 +230,7 @@ func (f *sendReceiveFolder) pull() bool {
// flagged as needed in the folder.
func (f *sendReceiveFolder) pullerIteration(scanChan chan<- string) int {
f.pullErrorsMut.Lock()
f.oldPullErrors = f.pullErrors
f.pullErrors = make(map[string]string)
f.tempPullErrors = make(map[string]string)
f.pullErrorsMut.Unlock()
snap := f.fset.Snapshot()
@@ -306,10 +300,6 @@ func (f *sendReceiveFolder) pullerIteration(scanChan chan<- string) int {
close(dbUpdateChan)
updateWg.Wait()
f.pullErrorsMut.Lock()
f.oldPullErrors = nil
f.pullErrorsMut.Unlock()
f.queue.Reset()
return changed
@@ -739,7 +729,11 @@ func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, snap *db.Snaps
}
// There is already something under that name, we need to handle that.
if info, err := f.fs.Lstat(file.Name); err == nil {
switch info, err := f.fs.Lstat(file.Name); {
case err != nil && !fs.IsNotExist(err):
f.newPullError(file.Name, errors.Wrap(err, "checking for existing symlink"))
return
case err == nil:
// Check that it is what we have in the database.
curFile, hasCurFile := f.model.CurrentFolderFile(f.folderID, file.Name)
if err := f.scanIfItemChanged(file.Name, info, curFile, hasCurFile, scanChan); err != nil {
@@ -1783,7 +1777,7 @@ func (f *sendReceiveFolder) newPullError(path string, err error) {
// We might get more than one error report for a file (i.e. error on
// Write() followed by Close()); we keep the first error as that is
// probably closer to the root cause.
if _, ok := f.pullErrors[path]; ok {
if _, ok := f.tempPullErrors[path]; ok {
return
}
@@ -1791,15 +1785,9 @@ func (f *sendReceiveFolder) newPullError(path string, err error) {
// Use "syncing" as opposed to "pulling" as the latter might be used
// for errors occurring specificly in the puller routine.
errStr := fmt.Sprintln("syncing:", err)
f.pullErrors[path] = errStr
f.tempPullErrors[path] = errStr
if oldErr, ok := f.oldPullErrors[path]; ok && oldErr == errStr {
l.Debugf("Repeat error on puller (folder %s, item %q): %v", f.Description(), path, err)
delete(f.oldPullErrors, path) // Potential repeats are now caught by f.pullErrors itself
return
}
l.Infof("Puller (folder %s, item %q): %v", f.Description(), path, err)
l.Debugf("%v new error for %v: %v", f, path, err)
}
func (f *sendReceiveFolder) Errors() []FileError {
@@ -1905,7 +1893,10 @@ func (f *sendReceiveFolder) deleteDirOnDisk(dir string, snap *db.Snapshot, scanC
return errDirHasIgnored
}
if hasReceiveOnlyChanged {
return errDirHasReceiveOnlyChanged
// Pretend we deleted the directory. It will be resurrected as a
// receive-only changed item on scan.
scanChan <- dir
return nil
}
if hasKnown {
return errDirNotEmpty

View File

@@ -10,11 +10,13 @@ import (
"bytes"
"context"
"crypto/rand"
"errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@@ -95,6 +97,7 @@ func setupSendReceiveFolder(files ...protocol.FileInfo) (*model, *sendReceiveFol
model.Supervisor.Stop()
f := model.folderRunners[fcfg.ID].(*sendReceiveFolder)
f.pullErrors = make(map[string]string)
f.tempPullErrors = make(map[string]string)
f.ctx = context.Background()
// Update index
@@ -993,7 +996,7 @@ func TestDeleteBehindSymlink(t *testing.T) {
must(t, osutil.RenameOrCopy(fs.CopyRangeMethodStandard, ffs, destFs, file, "file"))
must(t, ffs.RemoveAll(link))
if err := osutil.DebugSymlinkForTestsOnly(destFs.URI(), filepath.Join(ffs.URI(), link)); err != nil {
if err := fs.DebugSymlinkForTestsOnly(destFs, ffs, "", link); err != nil {
if runtime.GOOS == "windows" {
// Probably we require permissions we don't have.
t.Skip("Need admin permissions or developer mode to run symlink test on Windows: " + err.Error())
@@ -1097,6 +1100,122 @@ func TestPullDeleteUnscannedDir(t *testing.T) {
}
}
func TestPullCaseOnlyPerformFinish(t *testing.T) {
m, f := setupSendReceiveFolder()
defer cleanupSRFolder(f, m)
ffs := f.Filesystem()
name := "foo"
contents := []byte("contents")
must(t, writeFile(ffs, name, contents, 0644))
must(t, f.scanSubdirs(nil))
var cur protocol.FileInfo
hasCur := false
snap := dbSnapshot(t, m, f.ID)
defer snap.Release()
snap.WithHave(protocol.LocalDeviceID, func(i protocol.FileIntf) bool {
if hasCur {
t.Fatal("got more than one file")
}
cur = i.(protocol.FileInfo)
hasCur = true
return true
})
if !hasCur {
t.Fatal("file is missing")
}
remote := *(&cur)
remote.Version = protocol.Vector{}.Update(device1.Short())
remote.Name = strings.ToUpper(cur.Name)
temp := fs.TempName(remote.Name)
must(t, writeFile(ffs, temp, contents, 0644))
scanChan := make(chan string, 1)
dbUpdateChan := make(chan dbUpdateJob, 1)
err := f.performFinish(remote, cur, hasCur, temp, snap, dbUpdateChan, scanChan)
select {
case <-dbUpdateChan: // boring case sensitive filesystem
return
case <-scanChan:
t.Error("no need to scan anything here")
default:
}
var caseErr *fs.ErrCaseConflict
if !errors.As(err, &caseErr) {
t.Error("Expected case conflict error, got", err)
}
}
func TestPullCaseOnlyDir(t *testing.T) {
testPullCaseOnlyDirOrSymlink(t, true)
}
func TestPullCaseOnlySymlink(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlinks not supported on windows")
}
testPullCaseOnlyDirOrSymlink(t, false)
}
func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
m, f := setupSendReceiveFolder()
defer cleanupSRFolder(f, m)
ffs := f.Filesystem()
name := "foo"
if dir {
must(t, ffs.Mkdir(name, 0777))
} else {
must(t, ffs.CreateSymlink("target", name))
}
must(t, f.scanSubdirs(nil))
var cur protocol.FileInfo
hasCur := false
snap := dbSnapshot(t, m, f.ID)
defer snap.Release()
snap.WithHave(protocol.LocalDeviceID, func(i protocol.FileIntf) bool {
if hasCur {
t.Fatal("got more than one file")
}
cur = i.(protocol.FileInfo)
hasCur = true
return true
})
if !hasCur {
t.Fatal("file is missing")
}
scanChan := make(chan string, 1)
dbUpdateChan := make(chan dbUpdateJob, 1)
remote := *(&cur)
remote.Version = protocol.Vector{}.Update(device1.Short())
remote.Name = strings.ToUpper(cur.Name)
if dir {
f.handleDir(remote, snap, dbUpdateChan, scanChan)
} else {
f.handleSymlink(remote, snap, dbUpdateChan, scanChan)
}
select {
case <-dbUpdateChan: // boring case sensitive filesystem
return
case <-scanChan:
t.Error("no need to scan anything here")
default:
}
if errStr, ok := f.tempPullErrors[remote.Name]; !ok {
t.Error("missing error for", remote.Name)
} else if !strings.Contains(errStr, "differs from name") {
t.Error("unexpected error", errStr, "for", remote.Name)
}
}
func cleanupSharedPullerState(s *sharedPullerState) {
s.mut.Lock()
defer s.mut.Unlock()

View File

@@ -12,6 +12,7 @@ import (
"testing"
"github.com/d4l3k/messagediff"
"github.com/syncthing/syncthing/lib/config"
)

View File

@@ -121,10 +121,9 @@ type model struct {
evLogger events.Logger
// constant or concurrency safe fields
finder *db.BlockFinder
progressEmitter *ProgressEmitter
shortID protocol.ShortID
cacheIgnoredFiles bool
finder *db.BlockFinder
progressEmitter *ProgressEmitter
shortID protocol.ShortID
// globalRequestLimiter limits the amount of data in concurrent incoming
// requests
globalRequestLimiter *byteSemaphore
@@ -202,7 +201,6 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
finder: db.NewBlockFinder(ldb),
progressEmitter: NewProgressEmitter(cfg, evLogger),
shortID: id.Short(),
cacheIgnoredFiles: cfg.Options().CacheIgnoredFiles,
globalRequestLimiter: newByteSemaphore(1024 * cfg.Options().MaxConcurrentIncomingRequestKiB()),
folderIOLimiter: newByteSemaphore(cfg.Options().MaxFolderConcurrency()),
@@ -245,12 +243,13 @@ func (m *model) ServeBackground() {
func (m *model) onServe() {
// Add and start folders
cacheIgnoredFiles := m.cfg.Options().CacheIgnoredFiles
for _, folderCfg := range m.cfg.Folders() {
if folderCfg.Paused {
folderCfg.CreateRoot()
continue
}
m.newFolder(folderCfg)
m.newFolder(folderCfg, cacheIgnoredFiles)
}
m.cfg.Subscribe(m)
}
@@ -278,8 +277,8 @@ func (m *model) StartDeadlockDetector(timeout time.Duration) {
}
// Need to hold lock on m.fmut when calling this.
func (m *model) addAndStartFolderLocked(cfg config.FolderConfiguration, fset *db.FileSet) {
ignores := ignore.New(cfg.Filesystem(), ignore.WithCache(m.cacheIgnoredFiles))
func (m *model) addAndStartFolderLocked(cfg config.FolderConfiguration, fset *db.FileSet, cacheIgnoredFiles bool) {
ignores := ignore.New(cfg.Filesystem(), ignore.WithCache(cacheIgnoredFiles))
if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
l.Warnln("Loading ignores:", err)
}
@@ -445,7 +444,7 @@ func (m *model) cleanupFolderLocked(cfg config.FolderConfiguration) {
delete(m.folderVersioners, cfg.ID)
}
func (m *model) restartFolder(from, to config.FolderConfiguration) {
func (m *model) restartFolder(from, to config.FolderConfiguration, cacheIgnoredFiles bool) {
if len(to.ID) == 0 {
panic("bug: cannot restart empty folder ID")
}
@@ -453,6 +452,7 @@ func (m *model) restartFolder(from, to config.FolderConfiguration) {
l.Warnf("bug: folder restart cannot change ID %q -> %q", from.ID, to.ID)
panic("bug: folder restart cannot change ID")
}
folder := to.ID
// This mutex protects the entirety of the restart operation, preventing
// there from being more than one folder restart operation in progress
@@ -460,7 +460,7 @@ func (m *model) restartFolder(from, to config.FolderConfiguration) {
// because those locks are released while we are waiting for the folder
// to shut down (and must be so because the folder might need them as
// part of its operations before shutting down).
restartMut := m.folderRestartMuts.Get(to.ID)
restartMut := m.folderRestartMuts.Get(folder)
restartMut.Lock()
defer restartMut.Unlock()
@@ -478,13 +478,6 @@ func (m *model) restartFolder(from, to config.FolderConfiguration) {
errMsg = "restarting"
}
var fset *db.FileSet
if !to.Paused {
// Creating the fileset can take a long time (metadata calculation)
// so we do it outside of the lock.
fset = db.NewFileSet(to.ID, to.Filesystem(), m.db)
}
err := fmt.Errorf("%v folder %v", errMsg, to.Description())
m.stopFolder(from, err)
// Need to send CC change to both from and to devices.
@@ -493,14 +486,23 @@ func (m *model) restartFolder(from, to config.FolderConfiguration) {
m.fmut.Lock()
defer m.fmut.Unlock()
// Cache the (maybe) existing fset before it's removed by cleanupFolderLocked
fset := m.folderFiles[folder]
m.cleanupFolderLocked(from)
if !to.Paused {
m.addAndStartFolderLocked(to, fset)
if fset == nil {
// Create a new fset. Might take a while and we do it under
// locking, but it's unsafe to create fset:s concurrently so
// that's the price we pay.
fset = db.NewFileSet(folder, to.Filesystem(), m.db)
}
m.addAndStartFolderLocked(to, fset, cacheIgnoredFiles)
}
l.Infof("%v folder %v (%v)", infoMsg, to.Description(), to.Type)
}
func (m *model) newFolder(cfg config.FolderConfiguration) {
func (m *model) newFolder(cfg config.FolderConfiguration, cacheIgnoredFiles bool) {
// Creating the fileset can take a long time (metadata calculation) so
// we do it outside of the lock.
fset := db.NewFileSet(cfg.ID, cfg.Filesystem(), m.db)
@@ -510,7 +512,7 @@ func (m *model) newFolder(cfg config.FolderConfiguration) {
m.fmut.Lock()
defer m.fmut.Unlock()
m.addAndStartFolderLocked(cfg, fset)
m.addAndStartFolderLocked(cfg, fset, cacheIgnoredFiles)
}
func (m *model) UsageReportingStats(report *contract.Report, version int, preview bool) {
@@ -1020,6 +1022,7 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
// that might not exist until the config is committed.
w, _ := m.cfg.SetFolders(changedFolders)
w.Wait()
changed = true
}
}
@@ -2471,7 +2474,7 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
l.Infoln("Paused folder", cfg.Description())
} else {
l.Infoln("Adding folder", cfg.Description())
m.newFolder(cfg)
m.newFolder(cfg, to.Options.CacheIgnoredFiles)
}
}
}
@@ -2490,8 +2493,8 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
// This folder exists on both sides. Settings might have changed.
// Check if anything differs that requires a restart.
if !reflect.DeepEqual(fromCfg.RequiresRestartOnly(), toCfg.RequiresRestartOnly()) {
m.restartFolder(fromCfg, toCfg)
if !reflect.DeepEqual(fromCfg.RequiresRestartOnly(), toCfg.RequiresRestartOnly()) || from.Options.CacheIgnoredFiles != to.Options.CacheIgnoredFiles {
m.restartFolder(fromCfg, toCfg, to.Options.CacheIgnoredFiles)
}
// Emit the folder pause/resume event

View File

@@ -270,17 +270,15 @@ func BenchmarkRequestOut(b *testing.B) {
}
func BenchmarkRequestInSingleFile(b *testing.B) {
testOs := &fatalOs{b}
m := setupModel(defaultCfgWrapper)
defer cleanupModel(m)
buf := make([]byte, 128<<10)
rand.Read(buf)
testOs.RemoveAll("testdata/request")
defer testOs.RemoveAll("testdata/request")
testOs.MkdirAll("testdata/request/for/a/file/in/a/couple/of/dirs", 0755)
ioutil.WriteFile("testdata/request/for/a/file/in/a/couple/of/dirs/128k", buf, 0644)
mustRemove(b, defaultFs.RemoveAll("request"))
defer func() { mustRemove(b, defaultFs.RemoveAll("request")) }()
must(b, defaultFs.MkdirAll("request/for/a/file/in/a/couple/of/dirs", 0755))
writeFile(defaultFs, "request/for/a/file/in/a/couple/of/dirs/128k", buf, 0644)
b.ResetTimer()
@@ -294,13 +292,11 @@ func BenchmarkRequestInSingleFile(b *testing.B) {
}
func TestDeviceRename(t *testing.T) {
testOs := &fatalOs{t}
hello := protocol.HelloResult{
ClientName: "syncthing",
ClientVersion: "v0.9.4",
}
defer testOs.Remove("testdata/tmpconfig.xml")
defer func() { mustRemove(t, defaultFs.Remove("tmpconfig.xml")) }()
rawCfg := config.New(device1)
rawCfg.Devices = []config.DeviceConfiguration{
@@ -1447,12 +1443,10 @@ func changeIgnores(t *testing.T, m *model, expected []string) {
}
func TestIgnores(t *testing.T) {
testOs := &fatalOs{t}
// Assure a clean start state
testOs.RemoveAll(filepath.Join("testdata", config.DefaultMarkerName))
testOs.MkdirAll(filepath.Join("testdata", config.DefaultMarkerName), 0644)
ioutil.WriteFile("testdata/.stignore", []byte(".*\nquux\n"), 0644)
mustRemove(t, defaultFs.RemoveAll(config.DefaultMarkerName))
mustRemove(t, defaultFs.MkdirAll(config.DefaultMarkerName, 0644))
writeFile(defaultFs, ".stignore", []byte(".*\nquux\n"), 0644)
m := setupModel(defaultCfgWrapper)
defer cleanupModel(m)
@@ -1481,7 +1475,7 @@ func TestIgnores(t *testing.T) {
// Invalid path, marker should be missing, hence returns an error.
fcfg := config.FolderConfiguration{ID: "fresh", Path: "XXX"}
ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(m.cacheIgnoredFiles))
ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(m.cfg.Options().CacheIgnoredFiles))
m.fmut.Lock()
m.folderCfgs[fcfg.ID] = fcfg
m.folderIgnores[fcfg.ID] = ignores
@@ -1496,7 +1490,7 @@ func TestIgnores(t *testing.T) {
pausedDefaultFolderConfig := defaultFolderConfig
pausedDefaultFolderConfig.Paused = true
m.restartFolder(defaultFolderConfig, pausedDefaultFolderConfig)
m.restartFolder(defaultFolderConfig, pausedDefaultFolderConfig, false)
// Here folder initialization is not an issue as a paused folder isn't
// added to the model and thus there is no initial scan happening.
@@ -1504,18 +1498,16 @@ func TestIgnores(t *testing.T) {
// Make sure no .stignore file is considered valid
defer func() {
testOs.Rename("testdata/.stignore.bak", "testdata/.stignore")
must(t, defaultFs.Rename(".stignore.bak", ".stignore"))
}()
testOs.Rename("testdata/.stignore", "testdata/.stignore.bak")
must(t, defaultFs.Rename(".stignore", ".stignore.bak"))
changeIgnores(t, m, []string{})
}
func TestEmptyIgnores(t *testing.T) {
testOs := &fatalOs{t}
// Assure a clean start state
testOs.RemoveAll(filepath.Join("testdata", config.DefaultMarkerName))
testOs.MkdirAll(filepath.Join("testdata", config.DefaultMarkerName), 0644)
mustRemove(t, defaultFs.RemoveAll(config.DefaultMarkerName))
must(t, defaultFs.MkdirAll(config.DefaultMarkerName, 0644))
m := setupModel(defaultCfgWrapper)
defer cleanupModel(m)
@@ -2117,14 +2109,14 @@ func benchmarkTree(b *testing.B, n1, n2 int) {
}
func TestIssue3028(t *testing.T) {
testOs := &fatalOs{t}
// Create two files that we'll delete, one with a name that is a prefix of the other.
must(t, ioutil.WriteFile("testdata/testrm", []byte("Hello"), 0644))
defer testOs.Remove("testdata/testrm")
must(t, ioutil.WriteFile("testdata/testrm2", []byte("Hello"), 0644))
defer testOs.Remove("testdata/testrm2")
must(t, writeFile(defaultFs, "testrm", []byte("Hello"), 0644))
must(t, writeFile(defaultFs, "testrm2", []byte("Hello"), 0644))
defer func() {
mustRemove(t, defaultFs.Remove("testrm"))
mustRemove(t, defaultFs.Remove("testrm2"))
}()
// Create a model and default folder
@@ -2138,8 +2130,8 @@ func TestIssue3028(t *testing.T) {
// Delete and rescan specifically these two
testOs.Remove("testdata/testrm")
testOs.Remove("testdata/testrm2")
must(t, defaultFs.Remove("testrm"))
must(t, defaultFs.Remove("testrm2"))
m.ScanFolderSubdirs("default", []string{"testrm", "testrm2"})
// Verify that the number of files decreased by two and the number of
@@ -2298,7 +2290,7 @@ func TestIndexesForUnknownDevicesDropped(t *testing.T) {
}
m := newModel(defaultCfgWrapper, myID, "syncthing", "dev", dbi, nil)
m.newFolder(defaultFolderConfig)
m.newFolder(defaultFolderConfig, false)
defer cleanupModel(m)
// Remote sequence is cached, hence need to recreated.
@@ -2601,7 +2593,7 @@ func TestIssue2571(t *testing.T) {
must(t, testFs.RemoveAll("toLink"))
must(t, osutil.DebugSymlinkForTestsOnly(filepath.Join(testFs.URI(), "linkTarget"), filepath.Join(testFs.URI(), "toLink")))
must(t, fs.DebugSymlinkForTestsOnly(testFs, testFs, "linkTarget", "toLink"))
m.ScanFolder("default")
@@ -2718,13 +2710,10 @@ func TestCustomMarkerName(t *testing.T) {
{Name: "dummyfile"},
})
fcfg := config.FolderConfiguration{
ID: "default",
Path: "rwtestfolder",
Type: config.FolderTypeSendReceive,
RescanIntervalS: 1,
MarkerName: "myfile",
}
fcfg := testFolderConfigTmp()
fcfg.ID = "default"
fcfg.RescanIntervalS = 1
fcfg.MarkerName = "myfile"
cfg := createTmpWrapper(config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
Devices: []config.DeviceConfiguration{
@@ -2735,13 +2724,12 @@ func TestCustomMarkerName(t *testing.T) {
})
testOs.RemoveAll(fcfg.Path)
defer testOs.RemoveAll(fcfg.Path)
m := newModel(cfg, myID, "syncthing", "dev", ldb, nil)
sub := m.evLogger.Subscribe(events.StateChanged)
defer sub.Unsubscribe()
m.ServeBackground()
defer cleanupModel(m)
defer cleanupModelAndRemoveDir(m, fcfg.Path)
waitForState(t, sub, "default", "folder path missing")
@@ -3355,7 +3343,7 @@ func TestConnCloseOnRestart(t *testing.T) {
newFcfg.Paused = true
done := make(chan struct{})
go func() {
m.restartFolder(fcfg, newFcfg)
m.restartFolder(fcfg, newFcfg, false)
close(done)
}()
select {
@@ -3806,6 +3794,57 @@ func TestBlockListMap(t *testing.T) {
}
}
func TestScanRenameCaseOnly(t *testing.T) {
wcfg, fcfg := tmpDefaultWrapper()
m := setupModel(wcfg)
defer cleanupModel(m)
ffs := fcfg.Filesystem()
name := "foo"
must(t, writeFile(ffs, name, []byte("contents"), 0644))
m.ScanFolders()
snap := dbSnapshot(t, m, fcfg.ID)
defer snap.Release()
found := false
snap.WithHave(protocol.LocalDeviceID, func(i protocol.FileIntf) bool {
if found {
t.Fatal("got more than one file")
}
if i.FileName() != name {
t.Fatalf("got file %v, expected %v", i.FileName(), name)
}
found = true
return true
})
snap.Release()
upper := strings.ToUpper(name)
must(t, ffs.Rename(name, upper))
m.ScanFolders()
snap = dbSnapshot(t, m, fcfg.ID)
defer snap.Release()
found = false
snap.WithHave(protocol.LocalDeviceID, func(i protocol.FileIntf) bool {
if i.FileName() == name {
if i.IsDeleted() {
return true
}
t.Fatal("renamed file not deleted")
}
if i.FileName() != upper {
t.Fatalf("got file %v, expected %v", i.FileName(), upper)
}
if found {
t.Fatal("got more than the expected files")
}
found = true
return true
})
}
func TestConnectionTerminationOnFolderAdd(t *testing.T) {
testConfigChangeClosesConnections(t, false, true, nil, func(cfg config.Wrapper) {
fcfg := testFolderConfigTmp()
@@ -3904,6 +3943,47 @@ func TestAddFolderCompletion(t *testing.T) {
}
}
func TestScanDeletedROChangedOnSR(t *testing.T) {
w, fcfg := tmpDefaultWrapper()
fcfg.Type = config.FolderTypeReceiveOnly
waiter, _ := w.SetFolder(fcfg)
waiter.Wait()
m := setupModel(w)
defer cleanupModel(m)
name := "foo"
ffs := fcfg.Filesystem()
must(t, writeFile(ffs, name, []byte(name), 0644))
m.ScanFolders()
file, ok := m.CurrentFolderFile(fcfg.ID, name)
if !ok {
t.Fatal("file missing in db")
}
// A remote must have the file, otherwise the deletion below is
// automatically resolved as not a ro-changed item.
m.IndexUpdate(device1, fcfg.ID, []protocol.FileInfo{file})
must(t, ffs.Remove(name))
m.ScanFolders()
if receiveOnlyChangedSize(t, m, fcfg.ID).Deleted != 1 {
t.Fatal("expected one receive only changed deleted item")
}
fcfg.Type = config.FolderTypeSendReceive
waiter, _ = w.SetFolder(fcfg)
waiter.Wait()
m.ScanFolders()
if receiveOnlyChangedSize(t, m, fcfg.ID).Deleted != 0 {
t.Fatal("expected no receive only changed deleted item")
}
if localSize(t, m, fcfg.ID).Deleted != 1 {
t.Fatal("expected one local deleted item")
}
}
func testConfigChangeClosesConnections(t *testing.T, expectFirstClosed, expectSecondClosed bool, pre func(config.Wrapper), fn func(config.Wrapper)) {
t.Helper()
wcfg, _ := tmpDefaultWrapper()

View File

@@ -323,7 +323,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(fss.URI(), ignExisting), otherContents, 0644); err != nil {
if err := writeFile(fss, ignExisting, otherContents, 0644); err != nil {
panic(err)
}
@@ -465,12 +465,12 @@ func TestIssue4841(t *testing.T) {
func TestRescanIfHaveInvalidContent(t *testing.T) {
m, fc, fcfg := setupModelWithConnection()
tmpDir := fcfg.Filesystem().URI()
defer cleanupModelAndRemoveDir(m, tmpDir)
tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI())
payload := []byte("hello")
must(t, ioutil.WriteFile(filepath.Join(tmpDir, "foo"), payload, 0777))
must(t, writeFile(tfs, "foo", payload, 0777))
received := make(chan []protocol.FileInfo)
fc.mut.Lock()
@@ -511,7 +511,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
payload = []byte("bye")
buf = make([]byte, len(payload))
must(t, ioutil.WriteFile(filepath.Join(tmpDir, "foo"), payload, 0777))
must(t, writeFile(tfs, "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 {
@@ -1051,7 +1051,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
}
fc.mut.Unlock()
if err := ioutil.WriteFile(filepath.Join(fss.URI(), file), contents, 0644); err != nil {
if err := writeFile(fss, file, contents, 0644); err != nil {
panic(err)
}
m.ScanFolders()

View File

@@ -9,6 +9,8 @@ package model
import (
"os"
"time"
"github.com/syncthing/syncthing/lib/fs"
)
// fatal is the required common interface between *testing.B and *testing.T
@@ -28,6 +30,13 @@ func must(f fatal, err error) {
}
}
func mustRemove(f fatal, err error) {
f.Helper()
if err != nil && !fs.IsNotExist(err) {
f.Fatal(err)
}
}
func (f *fatalOs) Chmod(name string, mode os.FileMode) {
f.Helper()
must(f, os.Chmod(name, mode))

View File

@@ -36,9 +36,8 @@ func init() {
device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
defaultFs = fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata")
defaultFolderConfig = testFolderConfig("testdata")
defaultFs = defaultFolderConfig.Filesystem()
defaultCfgWrapper = createTmpWrapper(config.New(myID))
_, _ = defaultCfgWrapper.SetDevice(config.NewDeviceConfiguration(device1, "device1"))

View File

@@ -85,7 +85,10 @@ func (s *Service) serve(ctx context.Context) {
case <-timer.C:
case <-s.processScheduled:
if !timer.Stop() {
<-timer.C
select {
case <-timer.C:
default:
}
}
case <-ctx.Done():
timer.Stop()

View File

@@ -93,6 +93,11 @@ func (w *AtomicWriter) Close() error {
return err
}
info, infoErr := w.fs.Lstat(w.path)
if infoErr != nil && !fs.IsNotExist(infoErr) {
w.err = infoErr
return infoErr
}
err := w.fs.Rename(w.next.Name(), w.path)
if runtime.GOOS == "windows" && fs.IsPermission(err) {
// On Windows, we might not be allowed to rename over the file
@@ -105,6 +110,12 @@ func (w *AtomicWriter) Close() error {
w.err = err
return err
}
if infoErr == nil {
if err := w.fs.Chmod(w.path, info.Mode()); err != nil {
w.err = err
return err
}
}
// fsync the directory too
if fd, err := w.fs.Open(filepath.Dir(w.next.Name())); err == nil {

View File

@@ -110,4 +110,10 @@ func testCreateAtomicReplace(t *testing.T, oldPerms os.FileMode) {
if !bytes.Equal(bs, []byte("hello")) {
t.Error("incorrect data")
}
if info, err := os.Stat(testfile); err != nil {
t.Fatal(err)
} else if info.Mode() != oldPerms {
t.Fatalf("Perms changed during atomic write: 0%o", info.Mode())
}
}

Some files were not shown because too many files have changed in this diff Show More