Compare commits

...
21 Commits
Author SHA1 Message Date
Simon FreiandGitHub 1a22689328 lib/db: Add failure reports to failures iterating over hashes (#7755) 2021-06-07 23:10:35 +02:00
Jakob BorgandGitHub ce65aea0ab lib/db: Use a more concurrent GC (fixes #7722) (#7750)
This changes the GC mechanism so that the first pass (which reads all
FileInfos to populate bloom filters with block & version hashes) can
happen concurrently with normal database operations.

The big gcMut still exists, and we grab it temporarily to block all
other modifications while we set up the bloom filters. We then release
the lock and let other things happen, with those other things also
updating the bloom filters as required. Once the first phase is done we
again grab the gcMut, knowing that we are the sole modifier of the
database, and do the cleanup.

I also removed the final compaction step.
2021-06-07 10:52:06 +02:00
André ColombandGitHub 45edad867c all: Allow dismissing pending devices / folders without ignoring (fixes #7700) (#7712) 2021-06-07 10:29:24 +02:00
André ColombandGitHub ea0a408849 gui: Modal dialog for listeners and discovery status (#7539) 2021-06-07 09:08:44 +02:00
Simon FreiandGitHub 18592af993 lib/model: Fix wrongly hardcoded arguments in test helper (#7749) 2021-06-05 17:01:23 +02:00
Simon FreiandGitHub b1e0e7b923 lib/model: Fix indexhandling for new folders paused on remote (#7747) 2021-06-05 16:27:15 +02:00
Jakob Borg 1e212a8dc6 cmd/syncthing: Wording on error return in "cli debug file" 2021-06-05 11:45:29 +02:00
Jakob Borg 0e9d2a13af cmd/syncthing: Improve "cli debug file" handling
Proper URL encoding, and return a sensible error when it's not found.
2021-06-05 11:19:02 +02:00
Simon FreiandGitHub 6494a9332d lib/model: Fix test introduced in #7714 failing due to #7689 (#7745) 2021-06-04 15:32:47 +02:00
Simon FreiandGitHub 41baccb85d lib/model: Fix passwords on receive-enc needing token (ref #7518) (#7739) 2021-06-03 15:39:49 +02:00
Simon FreiandGitHub 52eb7392c4 lib/api, lib/config: Apply defaults before deserializing json (#7690) 2021-06-03 15:09:35 +02:00
Simon FreiandGitHub 855c53ad02 lib/model: Fix reverting when version has only our own ID (fixes #7708) (#7714) 2021-06-03 15:08:56 +02:00
Simon FreiandGitHub 004eded398 lib/model: Don't share with introduced device if encrypted (fixes #7724) (#7734) 2021-06-03 15:02:57 +02:00
Simon FreiandGitHub df48276300 lib/model: Ensure indexes are only received after checking IDs (ref #7649) (#7689) 2021-06-03 14:58:50 +02:00
Jakob Borg accaf23aa3 gui, man, authors: Update docs, translations, and contributors 2021-06-02 07:45:38 +02:00
André ColombandGitHub de6fe4dc6f gui: Show ID under each device section (local / remote), clickable for QR code (#7728) 2021-06-01 18:04:11 +02:00
Ikko AshimineandGitHub 08f6a91441 contributing: Fix GitHub format (#7713) 2021-05-31 10:27:35 +02:00
greatroarandGitHub 95c9561e97 lib/db: Clean up Timer and wait for logging before return in GC (#7720) 2021-05-31 09:50:21 +02:00
Simon FreiandGitHub fcb19518c7 build, lib/model: Add flag to run tests without -short and fix failure (#7716) 2021-05-28 22:20:18 +02:00
Jakob BorgandGitHub fbaf696821 readme: Remove drama (#7711) 2021-05-26 14:59:49 +02:00
Jakob Borg abe3483a8f gui, man, authors: Update docs, translations, and contributors 2021-05-26 07:45:29 +02:00
79 changed files with 1595 additions and 873 deletions
+1
View File
@@ -114,6 +114,7 @@ Heiko Zuerker (Smiley73) <heiko@zuerker.org>
Hugo Locurcio <hugo.locurcio@hugo.pro>
Iain Barnett <iainspeed@gmail.com>
Ian Johnson (anonymouse64) <ian.johnson@canonical.com> <person.uwsome@gmail.com>
Ikko Ashimine <eltociear@gmail.com>
Ilya Brin <464157+ilyabrin@users.noreply.github.com>
Iskander Sharipov (Alex) <quasilyte@gmail.com>
Jaakko Hannikainen (jgke) <jgke@jgke.fi>
+1 -1
View File
@@ -1,6 +1,6 @@
## Reporting Bugs
Please file bugs in the [Github Issue
Please file bugs in the [GitHub Issue
Tracker](https://github.com/syncthing/syncthing/issues). Include at
least the following:
+2 -5
View File
@@ -73,9 +73,8 @@ This helps the team understand what are the biggest pain points for our users, a
## Getting in Touch
The first and best point of contact is the [Forum][8]. There is also an IRC
channel, `#syncthing` on [freenode][4] (with a [web client][9]), for talking
directly to developers and users. If you've found something that is clearly a
The first and best point of contact is the [Forum][8].
If you've found something that is clearly a
bug, feel free to report it in the [GitHub issue tracker][10].
## Building
@@ -102,12 +101,10 @@ All code is licensed under the [MPLv2 License][7].
[1]: https://docs.syncthing.net/specs/bep-v1.html
[2]: https://docs.syncthing.net/intro/getting-started.html
[3]: https://github.com/syncthing/syncthing/blob/main/etc
[4]: https://www.freenode.net/
[5]: https://docs.syncthing.net/dev/building.html
[6]: https://docs.syncthing.net/
[7]: https://github.com/syncthing/syncthing/blob/main/LICENSE
[8]: https://forum.syncthing.net/
[9]: https://kiwiirc.com/client/irc.freenode.net/#syncthing
[10]: https://github.com/syncthing/syncthing/issues
[11]: https://docs.syncthing.net/users/contrib.html#gui-wrappers
[12]: https://www.bountysource.com/teams/syncthing/issues
+10 -1
View File
@@ -50,7 +50,9 @@ var (
benchRun string
debugBinary bool
coverage bool
long bool
timeout = "120s"
longTimeout = "600s"
numVersions = 5
withNextGenGUI = os.Getenv("BUILD_NEXT_GEN_GUI") != ""
)
@@ -363,6 +365,7 @@ func parseFlags() {
flag.StringVar(&cc, "cc", os.Getenv("CC"), "Set CC environment variable for `go build`")
flag.BoolVar(&debugBinary, "debug-binary", debugBinary, "Create unoptimized binary to use with delve, set -gcflags='-N -l' and omit -ldflags")
flag.BoolVar(&coverage, "coverage", coverage, "Write coverage profile of tests to coverage.txt")
flag.BoolVar(&long, "long", long, "Run tests without the -short flag")
flag.IntVar(&numVersions, "num-versions", numVersions, "Number of versions for changelog command")
flag.StringVar(&run, "run", "", "Specify which tests to run")
flag.StringVar(&benchRun, "bench", "", "Specify which benchmarks to run")
@@ -374,7 +377,13 @@ func test(tags []string, pkgs ...string) {
lazyRebuildAssets()
tags = append(tags, "purego")
args := []string{"test", "-short", "-timeout", timeout, "-tags", strings.Join(tags, " ")}
args := []string{"test", "-tags", strings.Join(tags, " ")}
if long {
timeout = longTimeout
} else {
args = append(args, "-short")
}
args = append(args, "-timeout", timeout)
if runtime.GOARCH == "amd64" {
switch runtime.GOOS {
+3 -1
View File
@@ -134,9 +134,11 @@ func (c *apiClient) Post(url, body string) (*http.Response, error) {
return c.Do(request)
}
var errNotFound = errors.New("invalid endpoint or API call")
func checkResponse(response *http.Response) error {
if response.StatusCode == http.StatusNotFound {
return errors.New("invalid endpoint or API call")
return errNotFound
} else if response.StatusCode == http.StatusUnauthorized {
return errors.New("invalid API key")
} else if response.StatusCode != http.StatusOK {
+5 -1
View File
@@ -8,6 +8,7 @@ package cli
import (
"fmt"
"net/url"
"github.com/urfave/cli"
)
@@ -35,7 +36,10 @@ var debugCommand = cli.Command{
func debugFile() cli.ActionFunc {
return func(c *cli.Context) error {
return indexDumpOutput(fmt.Sprintf("debug/file?folder=%v&file=%v", c.Args()[0], normalizePath(c.Args()[1])))(c)
query := make(url.Values)
query.Set("folder", c.Args()[0])
query.Set("file", normalizePath(c.Args()[1]))
return indexDumpOutput("debug/file?" + query.Encode())(c)
}
}
+3
View File
@@ -48,6 +48,9 @@ func indexDumpOutput(url string) cli.ActionFunc {
return err
}
response, err := client.Get(url)
if errors.Is(err, errNotFound) {
return errors.New("not found (folder/file not in database)")
}
if err != nil {
return err
}
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Натиснете, за да видите грешки при откриването",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Затвори",
"Command": "Команда",
"Comment, when used at the start of a line": "Коментар, използван в началото на реда",
@@ -153,6 +154,7 @@
"Help": "Помощ",
"Home page": "Начална страница",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"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.",
"Ignore": "Игнорирай",
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Clica per a vore els fallos en el descobriment",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Tancar",
"Command": "Comando",
"Comment, when used at the start of a line": "Comentar, quant s'utilitza al principi d'una línia",
@@ -153,6 +154,7 @@
"Help": "Ajuda",
"Home page": "Pàgina inicial",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"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.",
"Ignore": "Ignorar",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Mazání verzí",
"Cleanup Interval": "Interval mazání",
"Click to see discovery failures": "Kliknutím zobrazíte nezdary při objevování",
"Click to see full identification string and QR code.": "Kliknutím zobrazíte úplnou identifikaci a QR kód.",
"Close": "Zavřít",
"Command": "Příkaz",
"Comment, when used at the start of a line": "Pokud použito na jeho začátku, je řádek považován za komentář",
@@ -153,6 +154,7 @@
"Help": "Nápověda",
"Home page": "Domovská stránka",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Nicméně Vaše současná nastavení značí, že si nepřejete funkci povolit. Automatické hlášení pádů tedy bylo vypnuto.",
"Identification": "Identifikace",
"If untrusted, enter encryption password": "V případě nedůvěryhodnosti zadat šifrovací heslo",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Pokud chcete ostatním uživatelům tohoto počítače zabránit v přístupu k Syncthing (a skrze něj tedy ke svým souborům), zvažte nastavení ověřování se.",
"Ignore": "Ignorovat",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Rydder op i versioner",
"Cleanup Interval": "Ryd op interval",
"Click to see discovery failures": "Klik for at se opdagelsesfejl",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Luk",
"Command": "Kommando",
"Comment, when used at the start of a line": "Kommentar, når den bruges i starten af en linje",
@@ -153,6 +154,7 @@
"Help": "Hjælp",
"Home page": "Hjem",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Hvis ikke troværdig, indtast krypteringsadgangskode",
"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.",
"Ignore": "Ignorér",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Versionen bereinigen",
"Cleanup Interval": "Bereinigungsintervall",
"Click to see discovery failures": "Klick um Gerätesuchfehler anzuzeigen",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Schließen",
"Command": "Befehl",
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
@@ -153,6 +154,7 @@
"Help": "Hilfe",
"Home page": "Homepage",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Ihre aktuellen Einstellungen weisen jedoch daraufhin, dass Sie die Aktivierung möglicherweise nicht wünschen. Wir haben die automatischen Absturzberichte für Sie deaktiviert.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Wenn nicht vertraut, geben Sie das Verschlüsselungspasswort ein",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Falls Sie andere Benutzer dieses Computers daran hindern möchten auf Syncthing und somit ihren Dateien Zugriff zu haben, setzen Sie bitte einen Benutzernamen und Passwort.",
"Ignore": "Ignorieren",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Πατήστε για να δείτε τις αποτυχίες ανεύρεσης συσκευών",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Τέλος",
"Command": "Εντολή",
"Comment, when used at the start of a line": "Σχόλιο, όταν χρησιμοποιείται στην αρχή μιας γραμμής",
@@ -153,6 +154,7 @@
"Help": "Βοήθεια",
"Home page": "Αρχική σελίδα",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Ωστόσο, σύμφωνα με τις τρέχουσες ρυθμίσεις σας, μάλλον δεν επιθυμείτε αυτή τη λειτουργία. Οι αναφορές σφαλμάτων απενεργοποιήθηκαν.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"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.",
"Ignore": "Αγνόησε",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Click to see discovery failures",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Close",
"Command": "Command",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
@@ -153,6 +154,7 @@
"Help": "Help",
"Home page": "Home page",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"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.",
"Ignore": "Ignore",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Click to see discovery failures",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Close",
"Command": "Command",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
@@ -153,6 +154,7 @@
"Help": "Help",
"Home page": "Home page",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"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.",
"Ignore": "Ignore",
+13
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Click to see discovery failures",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Close",
"Command": "Command",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
@@ -93,6 +94,8 @@
"Discovered": "Discovered",
"Discovery": "Discovery",
"Discovery Failures": "Discovery Failures",
"Dismiss": "Dismiss",
"Do not add it to the ignore list, so this notification may recurr.": "Do not add it to the ignore list, so this notification may recurr.",
"Do not restore": "Do not restore",
"Do not restore all": "Do not restore all",
"Do you want to enable watching for changes for all your folders?": "Do you want to enable watching for changes for all your folders?",
@@ -153,6 +156,7 @@
"Help": "Help",
"Home page": "Home page",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"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.",
"Ignore": "Ignore",
@@ -225,6 +229,7 @@
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:",
"Permanently add it to the ignore list, suppressing further notifications.": "Permanently add it to the ignore list, suppressing further notifications.",
"Permissions": "Permissions",
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
@@ -284,6 +289,8 @@
"Sharing": "Sharing",
"Show ID": "Show ID",
"Show QR": "Show QR",
"Show detailed discovery status": "Show detailed discovery status",
"Show detailed listener status": "Show detailed listener status",
"Show diff with previous version": "Show diff with previous version",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.",
@@ -293,7 +300,9 @@
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Size": "Size",
"Smallest First": "Smallest First",
"Some discovery methods could not be established for finding other devices or announcing this device:": "Some discovery methods could not be established for finding other devices or announcing this device:",
"Some items could not be restored:": "Some items could not be restored:",
"Some listening addresses could not be enabled to accept connections:": "Some listening addresses could not be enabled to accept connections:",
"Source Code": "Source Code",
"Stable releases and release candidates": "Stable releases and release candidates",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.",
@@ -310,6 +319,8 @@
"Syncthing has been shut down.": "Syncthing has been shut down.",
"Syncthing includes the following software or portions thereof:": "Syncthing includes the following software or portions thereof:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is listening on the following network addresses for connection attempts from other devices:": "Syncthing is listening on the following network addresses for connection attempts from other devices:",
"Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.": "Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.",
"Syncthing is restarting.": "Syncthing is restarting.",
"Syncthing is upgrading.": "Syncthing is upgrading.",
"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.",
@@ -334,6 +345,7 @@
"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.": "The following items were changed locally.",
"The following methods are used to discover other devices on the network and announce this device to be found by others:": "The following methods are used to discover other devices on the network and announce this device to be found by others:",
"The following unexpected items were found.": "The following unexpected items were found.",
"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.",
@@ -351,6 +363,7 @@
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This Device": "This Device",
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
"This device cannot automatically discover other devices or announce its own address to be found by others. Only devices with statically configured addresses can connect.": "This device cannot automatically discover other devices or announce its own address to be found by others. Only devices with statically configured addresses can connect.",
"This is a major version upgrade.": "This is a major version upgrade.",
"This setting controls the free space required on the home (i.e., index database) disk.": "This setting controls the free space required on the home (i.e., index database) disk.",
"Time": "Time",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Alklaku por vidi malsukcesajn malkovrojn",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Fermi",
"Command": "Komando",
"Comment, when used at the start of a line": "Komento, kiam uzita ĉe la komenco de lineo",
@@ -153,6 +154,7 @@
"Help": "Helpo",
"Home page": "Hejma paĝo",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"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.",
"Ignore": "Ignoru",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Limpiando Versiones",
"Cleanup Interval": "Intervalo de Limpieza",
"Click to see discovery failures": "Clica para ver fallos de descubrimiento.",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Cerrar",
"Command": "Acción",
"Comment, when used at the start of a line": "Comentar, cuando se usa al comienzo de una línea",
@@ -153,6 +154,7 @@
"Help": "Ayuda",
"Home page": "Página de inicio",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Sin embargo, su configuración actual indica que puede no querer habilitarlo. Hemos deshabilitado el informe automático de errores por usted.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Si no es de confianza, ingrese la contraseña de cifrado.",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Si quiere impedirle a otros usuarios de esta computadora acceder a Syncthing y, a través de este, a sus archivos, considere establecer la autenticación.",
"Ignore": "Ignorar",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Limpiando Versiones",
"Cleanup Interval": "Intervalo de Limpieza",
"Click to see discovery failures": "Clica para ver fallos de descubrimiento.",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Cerrar",
"Command": "Acción",
"Comment, when used at the start of a line": "Comentar, cuando se usa al comienzo de una línea",
@@ -153,6 +154,7 @@
"Help": "Ayuda",
"Home page": "Página de inicio",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Sin embargo, su configuración actual indica que puede no quererla activa. Hemos desactivado los informes automáticos de fallos por usted.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Si no es de confianza, introduzca la contraseña de cifrado",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Si desea evitar que otros usuarios de esta computadora accedan a Syncthing y, a través de él, a sus archivos, considere establecer la autenticación.",
"Ignore": "Ignorar",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Bertsio garbiketa",
"Cleanup Interval": "Garbiketa-tartea",
"Click to see discovery failures": "Klik egin hutsegiteak ikusteko",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Hetsi",
"Command": "Kontrolagailua",
"Comment, when used at the start of a line": "Komentarioa, lerro baten hastean delarik",
@@ -153,6 +154,7 @@
"Help": "Laguntza",
"Home page": "Harrera",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Hala ere, zure ezarpenen arabera baliteke aktibatu nahi ez izatea. Txostenen bidalketa automatikoa desaktibatu dugu zuretzat.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Fidagarria ez bada, idatzi enkriptatzeko pasahitza",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Ordenagailu honetako beste erabiltzaile batzuk Syncthing-era eta haren bidez zure fitxategietara sartzea eragotzi nahi baduzu, kontuan izan autentifikazioa konfiguratu behar duzula.",
"Ignore": "Kontuan ez hartu",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Klikkaa nähdäksesi etsinnässä tapahtuneet virheet",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Sulje",
"Command": "Komento",
"Comment, when used at the start of a line": "Kommentti, käytettäessä rivin alussa",
@@ -153,6 +154,7 @@
"Help": "Apua",
"Home page": "Kotisivu",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"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.",
"Ignore": "Ohita",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Purge des versions",
"Cleanup Interval": "Intervalle de purge",
"Click to see discovery failures": "Voir les échecs de découverte",
"Click to see full identification string and QR code.": "Voir la chaîne d'identification complète et le code QR.",
"Close": "Fermer",
"Command": "Commande",
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
@@ -153,6 +154,7 @@
"Help": "Aide (en anglais)",
"Home page": "Page d'accueil",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Cependant, vos réglages indiquent que vous pourriez souhaiter ne pas l'activer. Nous avons désactivé pour vous l'envoi automatique des rapports.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Entrez un mot de passe pour chiffrer",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Si vous voulez empêcher d'autres utilisateurs de cet appareil à accéder à Syncthing, et via Syncthing à vos fichiers, prenez quelques secondes pour régler l'authentification. ",
"Ignore": "Refuser",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Klik om ûntdekkingsflaters te besjen",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Slute",
"Command": "Kommando",
"Comment, when used at the start of a line": "Kommentaar, wannear as brûkt by it begjin fan in rige",
@@ -153,6 +154,7 @@
"Help": "Help",
"Home page": "Hiemstee",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Lykwols, jo aktuele ynstellings litte sjen dat jo it miskien net oan sette wol. Wy hawwe automatysk rapportearjen fan fêstrinnen foar jo útsetten.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"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.",
"Ignore": "Negearje",
+2
View File
@@ -45,6 +45,7 @@
"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",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Bezárás",
"Command": "Parancs",
"Comment, when used at the start of a line": "Megjegyzés, a sor elején használva",
@@ -153,6 +154,7 @@
"Help": "Súgó",
"Home page": "Főoldal",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "A jelenlegi beállítások azonban azt jelzik, hogy nem kívánja engedélyezni. Az automatikus összeomlás-jelentés ezért letiltásra került.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Ha nem megbízható, adjon hozzá titkosítási jelszót",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Ha kívánatos, hogy a számítógép többi felhasználója ne tudja elérni a Syncthinget, sem annak fájljait, érdemes az azonosítást beállítani.",
"Ignore": "Mellőzés",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Pulizia Versioni",
"Cleanup Interval": "Intervallo di Pulizia",
"Click to see discovery failures": "Clicca per vedere gli errori di rilevamento",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Chiudi",
"Command": "Comando",
"Comment, when used at the start of a line": "Per commentare, va inserito all'inizio di una riga",
@@ -153,6 +154,7 @@
"Help": "Aiuto",
"Home page": "Pagina home",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Tuttavia, le impostazioni correnti indicano che potresti non volerla attiva. Abbiamo disattivato la segnalazione automatica degli arresti anomali per te.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Se non attendibile, immettere la password di crittografia",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Se vuoi impedire ad altri utenti di questo computer di accedere a Syncthing e attraverso di esso ai tuoi file, prendi in considerazione la configurazione dell'autenticazione.",
"Ignore": "Ignora",
+9 -7
View File
@@ -25,11 +25,11 @@
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"Anonymous Usage Reporting": "匿名での使用状況レポート",
"Anonymous usage report format has changed. Would you like to move to the new format?": "匿名での使用状況レポートのフォーマットが変わりました。新形式でのレポートに移行しますか?",
"Are you sure you want to continue?": "Are you sure you want to continue?",
"Are you sure you want to permanently delete all these files?": "Are you sure you want to permanently delete all these files?",
"Are you sure you want to continue?": "続行してもよろしいですか?",
"Are you sure you want to permanently delete all these files?": "これらのファイルをすべて完全に削除してもよろしいですか?",
"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?": "Are you sure you want to restore {{count}} files?",
"Are you sure you want to restore {%count%} files?": "{{count}} ファイルを復元してもよろしいですか?",
"Are you sure you want to upgrade?": "アップグレードしてよろしいですか?",
"Auto Accept": "自動承諾",
"Automatic Crash Reporting": "自動クラッシュレポート",
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "接続に失敗した探索サーバーを確認するにはクリックしてください",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "閉じる",
"Command": "コマンド",
"Comment, when used at the start of a line": "行頭で使用するとコメント行になります",
@@ -63,11 +64,11 @@
"Danger!": "危険!",
"Debugging Facilities": "デバッグ機能",
"Default Configuration": "デフォルトの設定",
"Default Device": "Default Device",
"Default Folder": "Default Folder",
"Default Device": "デフォルトのデバイス",
"Default Folder": "デフォルトのフォルダー",
"Default Folder Path": "デフォルトのフォルダーパス",
"Defaults": "Defaults",
"Delete Unexpected Items": "Delete Unexpected Items",
"Defaults": "デフォルト",
"Delete Unexpected Items": "予期しないアイテムを削除",
"Deleted": "削除",
"Deselect All": "すべて選択解除",
"Deselect devices to stop sharing this folder with.": "このフォルダの共有を停止したいデバイスがある場合は、当該デバイスの選択を解除してください。",
@@ -153,6 +154,7 @@
"Help": "ヘルプ",
"Home page": "ホームページ",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "信頼しない場合、暗号化パスワードを入力",
"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.",
"Ignore": "無視",
+39 -37
View File
@@ -22,14 +22,14 @@
"Allow Anonymous Usage Reporting?": "익명 사용 보고서를 보내시겠습니까?",
"Allowed Networks": "허가된 네트워크",
"Alphabetic": "알파벳순",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "외부 커맨드가 파일 버전을 관리합니다. 공유 폴더에서 파일을 삭제해야 합니다. 응용 프로그램의 경로에 공백이 있으면 따옴표로 묶어야합니다.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "외부 명령이 파일 버전을 관리합니다. 공유 폴더에서 파일을 삭제해야 합니다. 응용 프로그램의 경로에 공백이 있으면 따옴표로 묶어야 합니다.",
"Anonymous Usage Reporting": "익명 사용 보고서",
"Anonymous usage report format has changed. Would you like to move to the new format?": "익명 사용 리포트의 형식이 변경되었습니다. 새 형식으로 이동 하시겠습니까?",
"Are you sure you want to continue?": "Are you sure you want to continue?",
"Are you sure you want to permanently delete all these files?": "Are you sure you want to permanently delete all these files?",
"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 continue?": "계속하시겠습니까?",
"Are you sure you want to permanently delete all these files?": "모든 파일을 영구 삭제하시겠습니까?",
"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?": "업그레이드를 하시겠습니까?",
"Auto Accept": "자동 수락",
"Automatic Crash Reporting": "자동 충돌 보고",
@@ -45,6 +45,7 @@
"Cleaning Versions": "버전 정리 중",
"Cleanup Interval": "정리 간격",
"Click to see discovery failures": "탐색 실패 보기",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "닫기",
"Command": "커맨드",
"Comment, when used at the start of a line": "명령행에서 시작을 할수 있어요.",
@@ -58,16 +59,16 @@
"Copied from elsewhere": "다른 곳에서 복사됨",
"Copied from original": "원본에서 복사됨",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "무시 패턴 만들기, {{path}}에 존재하는 파일을 덮어쓰기 합니다",
"Currently Shared With Devices": "현재 공유된 기기",
"Danger!": "경고!",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "무시 패턴 생성 중, {{path}}에 존재하는 파일을 덮어니다.",
"Currently Shared With Devices": "현재 공유된 기기",
"Danger!": "위험!",
"Debugging Facilities": "디버깅 기능",
"Default Configuration": "기본 설정",
"Default Device": "기본 기기",
"Default Folder": "기본 폴더",
"Default Folder Path": "기본 폴더 경로",
"Defaults": "기본 설정",
"Delete Unexpected Items": "Delete Unexpected Items",
"Delete Unexpected Items": "예기치 못한 항목 삭제",
"Deleted": "삭제됨",
"Deselect All": "모두 선택 해제",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
@@ -77,8 +78,8 @@
"Device ID": "기기 ID",
"Device Identification": "기기 식별자",
"Device Name": "기기 이름",
"Device is untrusted, enter encryption password": "Device is untrusted, enter encryption password",
"Device rate limits": "Device rate limits",
"Device is untrusted, enter encryption password": "신뢰하지 않는 기기, 암호화 비밀번호를 입력하세요",
"Device rate limits": "기기 속도 제한",
"Device that last modified the item": "항목을 마지막으로 수정 한 기기",
"Devices": "기기",
"Disable Crash Reporting": "충돌 보고 해제",
@@ -87,7 +88,7 @@
"Disabled periodic scanning and enabled watching for changes": "주기적 탐색을 사용 중지하고 변경 사항 감시 하기",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
"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",
"Discard": "무시",
"Disconnected": "연결 끊김",
"Disconnected (Unused)": "연결 해제됨 (미사용)",
"Discovered": "탐색됨",
@@ -118,7 +119,7 @@
"Error": "오류",
"External File Versioning": "외부 파일 버전 관리",
"Failed Items": "실패한 항목",
"Failed to setup, retrying": "Failed to setup, retrying",
"Failed to setup, retrying": "설정 적용 실패, 재시도 중",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "IPv6 네트워크에 연결되지 않은 경우 IPv6 서버에 연결 할 수 없습니다.",
"File Pull Order": "파일 동기화 순서",
"File Versioning": "파일 버전 관리",
@@ -153,10 +154,11 @@
"Help": "도움말",
"Home page": "홈페이지",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"Identification": "Identification",
"If untrusted, enter encryption password": "신뢰하지 않는 경우 암호화 비밀번호를 입력하세요",
"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.",
"Ignore": "무시",
"Ignore Patterns": "패턴 무시",
"Ignore Patterns": "무시 패턴",
"Ignore Permissions": "권한 무시",
"Ignored Devices": "무시된 기기",
"Ignored Folders": "무시된 폴더",
@@ -173,15 +175,15 @@
"Last seen": "마지막 접속",
"Latest Change": "최신 변경",
"Learn more": "더 알아보기",
"Limit": "Limit",
"Limit": "제한",
"Listeners": "수신자",
"Loading data...": "데이터 불러오는중...",
"Loading...": "불러오는 중...",
"Local Additions": "로컬 변경사항",
"Local Additions": "로컬 변경 항목",
"Local Discovery": "로컬 노드 검색",
"Local State": "로컬 상태",
"Local State (Total)": "로컬 상태 (합계)",
"Locally Changed Items": "Locally Changed Items",
"Locally Changed Items": "로컬 변경 항목",
"Log": "기록",
"Log tailing paused. Scroll to the bottom to continue.": "Log tailing paused. Scroll to the bottom to continue.",
"Logs": "기록",
@@ -238,7 +240,7 @@
"Random": "무작위",
"Receive Encrypted": "암호화 수신",
"Receive Only": "수신 전용",
"Received data is already encrypted": "Received data is already encrypted",
"Received data is already encrypted": "수신된 데이터는 이미 암호화되어 있습니다",
"Recent Changes": "최근 변경",
"Reduced by ignore patterns": "무시 패턴으로 축소",
"Release Notes": "릴리즈 노트",
@@ -260,7 +262,7 @@
"Resume": "재개",
"Resume All": "모두 재개",
"Reused": "재개",
"Revert Local Changes": "로컬 변경사항",
"Revert Local Changes": "로컬 변경 항목",
"Save": "저장",
"Scan Time Remaining": "탐색 남은 시간",
"Scanning": "탐색 중",
@@ -268,7 +270,7 @@
"Select All": "모두 선택",
"Select a version": "버전 선택",
"Select additional devices to share this folder with.": "이 폴더를 추가로 공유할 기기를 선택하세요.",
"Select additional folders to share with this device.": "Select additional folders to share with this device.",
"Select additional folders to share with this device.": "이 기기와 공유할 추가 폴더를 선택하세요.",
"Select latest version": "가장 최신 버전 선택",
"Select oldest version": "가장 오래된 버전 선택",
"Select the folders to share with this device.": "이 기기와 공유할 폴더를 선택합니다.",
@@ -333,8 +335,8 @@
"The folder path cannot be blank.": "폴더 경로는 비워 둘 수 없습니다.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "다음과 같은 간격이 사용됩니다: 첫 한 시간 동안은 버전이 매 30초마다 유지되며, 첫 하루 동안은 매 시간, 첫 한 달 동안은 매 일마다 유지됩니다. 그리고 최대 날짜까지는 버전이 매 주마다 유지됩니다.",
"The following items could not be synchronized.": "이 항목들은 동기화 할 수 없습니다.",
"The following items were changed locally.": "The following items were changed locally.",
"The following unexpected items were found.": "The following unexpected items were found.",
"The following items were changed locally.": "다음 로컬 변경 항목이 발견되었습니다.",
"The following unexpected items were found.": "예기치 못한 항목이 발견되었습니다.",
"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.": "최대 보존 기간은 숫자여야 하며 비워 둘 수 없습니다.",
@@ -347,7 +349,7 @@
"The rate limit must be a non-negative number (0: no limit)": "대역폭 제한 설정은 반드시 양수로 입력해야 합니다 (0: 무제한)",
"The rescan interval must be a non-negative number of seconds.": "재탐색 간격은 초단위이며 양수로 입력해야 합니다.",
"There are no devices to share this folder with.": "이 폴더를 공유할 기기가 없습니다.",
"There are no folders to share with this device.": "There are no folders to share with this device.",
"There are no folders to share with this device.": "이 기기와 공유할 폴더가 없습니다.",
"They are retried automatically and will be synced when the error is resolved.": "오류가 해결되면 자동적으로 동기화 됩니다.",
"This Device": "현재 기기",
"This can easily give hackers access to read and change any files on your computer.": "이 설정은 해커가 손쉽게 사용자 컴퓨터의 모든 파일을 읽고 변경할 수 있도록 할 수 있습니다.",
@@ -361,14 +363,14 @@
"Unavailable": "불가능",
"Unavailable/Disabled by administrator or maintainer": "운영자 또는 관리자에 의해 불가능/비활성화 됨",
"Undecided (will prompt)": "Undecided (will prompt)",
"Unexpected Items": "Unexpected Items",
"Unexpected items have been found in this folder.": "Unexpected items have been found in this folder.",
"Unignore": "Unignore",
"Unexpected Items": "예기치 못한 항목",
"Unexpected items have been found in this folder.": "이 폴더에 예기치 못한 항목이 발견되었습니다.",
"Unignore": "무시 취소",
"Unknown": "알 수 없음",
"Unshared": "공유되지 않음",
"Unshared Devices": "공유되지 않은 기기",
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Unshared Folders": "공유되지 않은 폴더",
"Untrusted": "신뢰하지 않음",
"Up to Date": "최신 데이터",
"Updated": "업데이트 완료",
"Upgrade": "업데이트",
@@ -386,12 +388,12 @@
"Waiting to Clean": "정리 대기 중",
"Waiting to Scan": "탐색 대기 중",
"Waiting to Sync": "동기화 대기 중",
"Warning": "Warning",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "경고, 이 경로는 현재 존재하는 폴더 \"{{otherFolder}}\" 의 상위 폴더 입니다.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "경고, 이 경로는 현재 존재하는 폴더 \"{{otherFolderLabel}}\" ({{otherFolder}}) 의 상위 폴더 입니다.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "경고, 이 경로는 현재 존재하는 폴더 \"{{otherFolder}}\" 의 하위 폴더 입니다.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "경고, 이 경로는 현재 존재하는 폴더 \"{{otherFolderLabel}}\" ({{otherFolder}}) 의 하위 폴더 입니다.",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "경고 : {{syncthingInotify}} 와 같은 외부 감시 도구를 사용하는 경우 비활성화되어 있는지 확인해야 합니다.",
"Warning": "경고",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "경고 : 이 경로는 현재 존재하는 폴더 \"{{otherFolder}}\"의 상위 폴더입니다.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "경고 : 이 경로는 현재 존재하는 폴더 \"{{otherFolderLabel}}\" ({{otherFolder}})의 상위 폴더입니다.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "경고 : 이 경로는 현재 존재하는 폴더 \"{{otherFolder}}\"의 하위 폴더입니다.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "경고 : 이 경로는 현재 존재하는 폴더 \"{{otherFolderLabel}}\" ({{otherFolder}})의 하위 폴더입니다.",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "경고 : {{syncthingInotify}}와 같은 외부 감시 도구를 사용하는 경우 비활성화되어 있는지 확인해야 합니다.",
"Watch for Changes": "변경 사항 감시",
"Watching for Changes": "변경 사항 감시",
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
@@ -402,7 +404,7 @@
"You can change your choice at any time in the Settings dialog.": "설정창 에서 언제든지 원하시는 때에 설정을 변경하는 것이 가능합니다.",
"You can read more about the two release channels at the link below.": "이 두 개의 출시 채널에 대해 아래 링크에서 자세하게 읽어 보실 수 있습니다.",
"You have no ignored devices.": "무시된 기기가 없습니다.",
"You have no ignored folders.": "You have no ignored folders.",
"You have no ignored folders.": "무시된 폴더가 없습니다.",
"You have unsaved changes. Do you really want to discard them?": "저장되지 않은 변경이 있습니다. 변경사항을 무시하시겠습니까?",
"You must keep at least one version.": "최소 한 개의 버전은 유지해야 합니다.",
"You should never add or change anything locally in a \"{%receiveEncrypted%}\" folder.": "You should never add or change anything locally in a \"{{receiveEncrypted}}\" folder.",
+4 -2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Spustelėkite, norėdami pamatyti matomumo nesėkmes",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Užverti",
"Command": "Komanda",
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
@@ -153,6 +154,7 @@
"Help": "Pagalba",
"Home page": "Pagrindinis puslapis",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Vis dėlto, jūsų esami nustatymai nurodo, kad jūs, greičiausiai, nenorite turėti jas įjungtas. Mes jums išjungėme automatines ataskaitas apie strigtis.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Jei norite neleisti kitiems šio kompiuterio vartotojams gauti prieigą prie Syncthing, o per ją, prieigą prie jūsų failų, tuomet apsvarstykite galimybę nusistatyti tapatybės nustatymą.",
"Ignore": "Nepaisyti",
@@ -361,8 +363,8 @@
"Unavailable": "Neprieinama",
"Unavailable/Disabled by administrator or maintainer": "Neprieinama/Išjungta administratoriaus ar prižiūrėtojo",
"Undecided (will prompt)": "Nenuspręsta (bus klausiama)",
"Unexpected Items": "Unexpected Items",
"Unexpected items have been found in this folder.": "Unexpected items have been found in this folder.",
"Unexpected Items": "Netikėti elementai",
"Unexpected items have been found in this folder.": "Šiame aplanke rasta netikėtų elementų.",
"Unignore": "Nustoti nepaisyti",
"Unknown": "Nežinoma",
"Unshared": "Nesidalinama",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Cleanup Interval",
"Click to see discovery failures": "Klikk for å se oppslagsfeil",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Lukk",
"Command": "Kommando",
"Comment, when used at the start of a line": "Kommentar, når det blir brukt i starten av en linje.",
@@ -153,6 +154,7 @@
"Help": "Hjelp",
"Home page": "Hjemmeside",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"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.",
"Ignore": "Ignorer",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Versies opruimen",
"Cleanup Interval": "Opruiminterval",
"Click to see discovery failures": "Klikken om detectieproblemen weer te geven",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Sluiten",
"Command": "Opdracht",
"Comment, when used at the start of a line": "Opmerking, wanneer gebruikt aan het begin van een regel",
@@ -153,6 +154,7 @@
"Help": "Help",
"Home page": "Startpagina",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Uw huidige instellingen geven echter aan dat u het misschien niet wilt inschakelen. We hebben de automatische crashrapportage voor u uitgeschakeld.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Indien niet vertrouwd, versleutelingswachtwoord opgeven",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Als u wilt voorkomen dat andere gebruikers op deze computer toegang krijgen tot Syncthing en daardoor uw bestanden, kunt u overwegen om authenticatie in te stellen.",
"Ignore": "Negeren",
+4 -2
View File
@@ -18,7 +18,7 @@
"Advanced": "Zaawansowane",
"Advanced Configuration": "Zaawansowane ustawienia",
"All Data": "Wszystkie dane",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Wszystkie foldery współdzielone z tym urządzeniem muszą być zabezpieczone hasłem, tak aby żadne przesyłane dane nie były możliwe do odczytu bez podania tego hasła.",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "Wszystkie foldery współdzielone z tym urządzeniem muszą być zabezpieczone hasłem, tak aby wszystkie przesyłane dane były nie do odczytu bez podania danego hasła.",
"Allow Anonymous Usage Reporting?": "Czy chcesz zezwolić na anonimowe statystyki użycia?",
"Allowed Networks": "Dozwolone sieci",
"Alphabetic": "Alfabetycznie",
@@ -45,6 +45,7 @@
"Cleaning Versions": "Czyszczenie wersji",
"Cleanup Interval": "Przedział czasowy czyszczenia",
"Click to see discovery failures": "Kliknij tutaj, aby zobaczyć błędy odnajdywania",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Zamknij",
"Command": "Polecenie",
"Comment, when used at the start of a line": "Komentarz, jeżeli znajduje się na początku linii",
@@ -58,7 +59,7 @@
"Copied from elsewhere": "Skopiowane z innego miejsca ",
"Copied from original": "Skopiowane z oryginału",
"Copyright © 2014-2019 the following Contributors:": "Wszelkie prawa zastrzeżone © 2014-2019 dla współtwórców:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Ustawianie wzorów ignorowania, nadpisze istniejący plik w {{path}}.",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Tworzenie wzorów ignorowania, nadpisze istniejący plik w {{path}}.",
"Currently Shared With Devices": "Obecnie współdzielony z urządzeniami",
"Danger!": "Niebezpieczeństwo!",
"Debugging Facilities": "Narzędzia do debugowania",
@@ -153,6 +154,7 @@
"Help": "Pomoc",
"Home page": "Strona domowa",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Niemniej jednak, obecne ustawienia wskazują, że możesz nie chcieć włączać tej funkcji. Automatyczne zgłaszanie awarii zostało wyłączone na tym urządzeniu.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Jeżeli folder jest niezaufany, wprowadź szyfrujące hasło",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Jeżeli chcesz zakazać innym użytkownikom tego komputera dostępu do Syncthing, a przez niego do swoich plików, zastanów się nad włączeniem uwierzytelniania.",
"Ignore": "Ignoruj",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Limpando Versões",
"Cleanup Interval": "Intervalo de Limpeza",
"Click to see discovery failures": "Clique para ver as falhas na descoberta de dispositivos",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Fechar",
"Command": "Comando",
"Comment, when used at the start of a line": "Comentário, se usado no início de uma linha",
@@ -153,6 +154,7 @@
"Help": "Ajuda",
"Home page": "Página inicial",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Se o dispositivo não é confiável, digite a senha de criptografia",
"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.",
"Ignore": "Ignorar",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Limpando versões",
"Cleanup Interval": "Intervalo entre limpezas",
"Click to see discovery failures": "Clique para ver as falhas da pesquisa",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Fechar",
"Command": "Comando",
"Comment, when used at the start of a line": "Comentário, quando usado no início de uma linha",
@@ -153,6 +154,7 @@
"Help": "Ajuda",
"Home page": "Página do projecto",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Contudo, a sua configuração actual indica que pode não a querer activada. Nós desactivámos automaticamente o relatório de estouro para si.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Se não for fiável, insira uma senha de encriptação",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Se quiser evitar que outros utilizadores neste computador acedam ao Syncthing e, através dele, aos seus ficheiros, considere configurar a autenticação.",
"Ignore": "Ignorar",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Очистка Версий",
"Cleanup Interval": "Интервал очистки",
"Click to see discovery failures": "Щёлкните, чтобы посмотреть ошибки",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Закрыть",
"Command": "Команда",
"Comment, when used at the start of a line": "Комментарий, если используется в начале строки",
@@ -153,6 +154,7 @@
"Help": "Помощь",
"Home page": "Сайт",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Ваши настройки указывают что вы не хотите, чтобы эта функция была включена. Мы отключили отправку отчетов о сбоях.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Если ненадёжное, укажите пароль шифрования",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Если вы хотите запретить другим пользователям на этом компьютере доступ к Syncthing и через него к вашим файлам, подумайте о настройке аутентификации.",
"Ignore": "Игнорировать",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Cleaning Versions",
"Cleanup Interval": "Interval čistenia",
"Click to see discovery failures": "Kliknite pre zobrazenie zlyhaní zisťovania.",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Zatvoriť",
"Command": "Príkaz",
"Comment, when used at the start of a line": "Komentár, keď použité na začiatku riadku",
@@ -153,6 +154,7 @@
"Help": "Pomoc",
"Home page": "Domovská stránka",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Ak nie je dôveryhodný, uveďte heslo na dešifrovanie",
"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.",
"Ignore": "Ignorovať",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Rensningsversioner",
"Cleanup Interval": "Rensningsintervall",
"Click to see discovery failures": "Klicka för att se upptäcktsmisslyckanden",
"Click to see full identification string and QR code.": "Klicka för att se fullständig identifieringssträng och QR-kod.",
"Close": "Stäng",
"Command": "Kommando",
"Comment, when used at the start of a line": "Kommentara, vid användning i början av en rad.",
@@ -153,6 +154,7 @@
"Help": "Hjälp",
"Home page": "Webbplats",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Dina aktuella inställningar visar dock att du kanske inte vill att den ska aktiveras. Vi har inaktiverat automatisk krasch rapportering för dig.",
"Identification": "Identifiering",
"If untrusted, enter encryption password": "Om opålitlig, ange krypteringslösenord",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Om du vill förhindra att andra användare på denna dator får åtkomst till Syncthing och genom det dina filer, överväg att ställa in autentisering.",
"Ignore": "Ignorera",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Sürümleri Temizleme",
"Cleanup Interval": "Aralığı Temizle",
"Click to see discovery failures": "Keşif hatalarını görmek için tıklayın",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Kapat",
"Command": "Komut",
"Comment, when used at the start of a line": "Açıklama, bir satırın başında kullanıldığında",
@@ -153,6 +154,7 @@
"Help": "Yardım",
"Home page": "Ana Sayfa",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Ancak, şu anki ayarlarınız etkinleştirilmesini istemediğinizi gösterir. Sizin için otomatik çökme bildirmeyi etkisizleştirdik.",
"Identification": "Identification",
"If untrusted, enter encryption password": "Eğer güvenilmezse, şifreleme parolasını girin",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Eğer bu bilgisayardaki diğer kullanıcıların Syncthing'e ve onun aracılığıyla dosyalarınıza erişmesini önlemek istiyorsanız, kimlik doğrulamasını ayarlamayı düşünün.",
"Ignore": "Yoksay",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "Очищення версій",
"Cleanup Interval": "Інтервал очищення",
"Click to see discovery failures": "Клікніть, щоб переглянути помилки виявлення",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "Закрити",
"Command": "Команда",
"Comment, when used at the start of a line": "Коментар, якщо використовується на початку рядка",
@@ -153,6 +154,7 @@
"Help": "Допомога",
"Home page": "Домашня сторінка",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "Однак ваші поточні налаштування вказують, що ви, можливо, не хочете, щоб це було ввімкнено. Ми відключили автоматичне повідомлення про аварійне завершення роботи.",
"Identification": "Identification",
"If untrusted, enter encryption password": "If untrusted, enter encryption password",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "Якщо ви хочете заборонити іншим користувачам цього комп’ютера отримувати доступ до Syncthing і через нього до своїх файлів, подумайте про налаштування автентифікації.",
"Ignore": "Ігнорувати",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "清除版本",
"Cleanup Interval": "清除间隔",
"Click to see discovery failures": "点击查看设备发现错误",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "关闭",
"Command": "命令",
"Comment, when used at the start of a line": "注释,在行首使用",
@@ -153,6 +154,7 @@
"Help": "帮助",
"Home page": "主页",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "我们已经为您关闭了自动崩溃报告发送功能,因为您当前的设置显示您可能并不想启用该功能。",
"Identification": "Identification",
"If untrusted, enter encryption password": "如想更安全,请输入加密密码",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "如果要阻止此计算机上的其他用户访问Syncthing并通过它访问文件,请考虑设置身份验证。",
"Ignore": "忽略",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "清除版本",
"Cleanup Interval": "清除間隔",
"Click to see discovery failures": "點擊查看設備發現錯誤",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "關閉",
"Command": "命令",
"Comment, when used at the start of a line": "註釋,在行首使用",
@@ -153,6 +154,7 @@
"Help": "幫助",
"Home page": "主頁",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "我們已經為您關閉了自動崩潰報告發送功能,因為您當前的設置顯示您可能並不想啟用該功能。",
"Identification": "Identification",
"If untrusted, enter encryption password": "如果不受信任,請輸入加密密碼",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "如果要阻止此計算機上的其他用戶訪問Syncthing並通過它訪問文件,請考慮設置身份驗證。",
"Ignore": "忽略",
+2
View File
@@ -45,6 +45,7 @@
"Cleaning Versions": "正在清除歷史版本",
"Cleanup Interval": "清除間隔",
"Click to see discovery failures": "點擊以查閱失敗的探索",
"Click to see full identification string and QR code.": "Click to see full identification string and QR code.",
"Close": "關閉",
"Command": "指令",
"Comment, when used at the start of a line": "註解,當輸入在一行的開頭時",
@@ -153,6 +154,7 @@
"Help": "說明",
"Home page": "首頁",
"However, your current settings indicate you might not want it enabled. We have disabled automatic crash reporting for you.": "但是,當前設定表明您可能不希望啟用它。我們為您停用了當機自動回報。",
"Identification": "Identification",
"If untrusted, enter encryption password": "如未受信任,請輸入加密密碼",
"If you want to prevent other users on this computer from accessing Syncthing and through it your files, consider setting up authentication.": "如果您想防止在此電腦上的其他使用者存取 Syncthing 及其文件,請考慮設定身份驗證。",
"Ignore": "忽略",
+34 -18
View File
@@ -102,7 +102,7 @@
</a>
<ul class="dropdown-menu">
<li><a href="" ng-click="showSettings()"><span class="fas fa-fw fa-cog"></span>&nbsp;<span translate>Settings</span></a></li>
<li><a href="" data-toggle="modal" data-target="#idqr" ng-click="currentDevice=thisDevice()"><span class="fas fa-fw fa-qrcode"></span>&nbsp;<span translate>Show ID</span></a></li>
<li><a href="" ng-click="showDeviceIdentification(thisDevice())"><span class="fas fa-fw fa-qrcode"></span>&nbsp;<span translate>Show ID</span></a></li>
<li class="divider" aria-hidden="true"></li>
<li><a href="" ng-click="shutdown()"><span class="fas fa-fw fa-power-off"></span>&nbsp;<span translate>Shutdown</span></a></li>
<li><a href="" ng-click="restart()"><span class="fas fa-fw fa-refresh"></span>&nbsp;<span translate>Restart</span></a></li>
@@ -209,10 +209,13 @@
</div>
<div class="panel-footer clearfix">
<div class="pull-right">
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingDevice(deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification may recurr.' | translate}}">
<span class="far fa-clock"></span>&nbsp;<span translate>Dismiss</span>
</button>
<button type="button" class="btn btn-sm btn-success" ng-click="addDevice(deviceID, pendingDevice.name)">
<span class="fas fa-plus"></span>&nbsp;<span translate>Add Device</span>
</button>
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreDevice(deviceID, pendingDevice)">
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreDevice(deviceID, pendingDevice)" tooltip data-original-title="{{'Permanently add it to the ignore list, suppressing further notifications.' | translate}}">
<span class="fas fa-times"></span>&nbsp;<span translate>Ignore</span>
</button>
</div>
@@ -250,13 +253,16 @@
</div>
<div class="panel-footer clearfix">
<div class="pull-right">
<button type="button" class="btn btn-sm btn-default" ng-click="dismissPendingFolder(folderID, deviceID)" tooltip data-original-title="{{'Do not add it to the ignore list, so this notification may recurr.' | translate}}">
<span class="far fa-clock"></span>&nbsp;<span translate>Dismiss</span>
</button>
<button type="button" class="btn btn-sm btn-success" ng-click="addFolderAndShare(folderID, pendingFolder, deviceID)" ng-if="!folders[folderID]">
<span class="fas fa-check"></span>&nbsp;<span translate>Add</span>
</button>
<button type="button" class="btn btn-sm btn-success" ng-click="shareFolderWithDevice(folderID, deviceID)" ng-if="folders[folderID]">
<span class="fas fa-check"></span>&nbsp;<span translate>Share</span>
</button>
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreFolder(deviceID, folderID, offeringDevice)">
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreFolder(deviceID, folderID, offeringDevice)" tooltip data-original-title="{{'Permanently add it to the ignore list, suppressing further notifications.' | translate}}">
<span class="fas fa-times"></span>&nbsp;<span translate>Ignore</span>
</button>
</div>
@@ -655,26 +661,20 @@
<tr>
<th><span class="fas fa-fw fa-sitemap"></span>&nbsp;<span translate>Listeners</span></th>
<td class="text-right">
<span ng-if="listenersFailed.length == 0" class="data text-success">
<span>{{listenersTotal}}/{{listenersTotal}}</span>
</span>
<span ng-if="listenersFailed.length != 0" class="data" ng-class="{'text-danger': listenersFailed.length == listenersTotal}">
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{listenersFailed.join('<br>\n')}}">
{{listenersTotal-listenersFailed.length}}/{{listenersTotal}}
</span>
<span class="data" tooltip data-original-title="{{'Show detailed listener status' | translate}}.">
<a href="" style="color:inherit" ng-class="{'text-success': listenersFailed.length == 0, 'text-danger': listenersFailed.length == listenersTotal}" ng-click="showListenerStatus()">
{{listenersTotal-listenersFailed.length}}/{{listenersTotal}}
</a>
</span>
</td>
</tr>
<tr ng-if="system.discoveryEnabled">
<th><span class="fas fa-fw fa-map-signs"></span>&nbsp;<span translate>Discovery</span></th>
<td class="text-right">
<span ng-if="discoveryFailed.length == 0" class="data text-success">
<span>{{discoveryTotal}}/{{discoveryTotal}}</span>
</span>
<span ng-if="discoveryFailed.length != 0" class="data" ng-class="{'text-danger': discoveryFailed.length == discoveryTotal}">
<span popover data-trigger="hover" data-placement="bottom" data-content="{{'Click to see discovery failures' | translate}}.">
<a href="" style="color:inherit" ng-click="showDiscoveryFailures()">{{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}</a>
</span>
<span class="data" tooltip data-original-title="{{'Show detailed discovery status' | translate}}.">
<a href="" style="color:inherit" ng-class="{'text-success': discoveryFailed.length == 0, 'text-danger': discoveryFailed.length == discoveryTotal}" ng-click="showDiscoveryStatus()">
{{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}
</a>
</span>
</td>
</tr>
@@ -682,6 +682,14 @@
<th><span class="far fa-fw fa-clock"></span>&nbsp;<span translate>Uptime</span></th>
<td class="text-right">{{system.uptime | duration:"m"}}</td>
</tr>
<tr>
<th><span class="fas fa-fw fa-qrcode"></span>&nbsp;<span translate>Identification</span></th>
<td class="text-right">
<span tooltip data-original-title="{{'Click to see full identification string and QR code.' | translate}}">
<a href="" ng-click="showDeviceIdentification(thisDevice())">{{deviceShortID(deviceCfg.deviceID)}}</a>
</span>
</td>
</tr>
<tr>
<th><span class="fas fa-fw fa-tag"></span>&nbsp;<span translate>Version</span></th>
<td class="text-right">
@@ -806,6 +814,14 @@
<th><span class="far fa-fw fa-handshake-o"></span>&nbsp;<span translate>Introduced By</span></th>
<td class="text-right">{{ deviceName(devices[deviceCfg.introducedBy]) || deviceCfg.introducedBy.substring(0, 5) }}</td>
</tr>
<tr>
<th><span class="fas fa-fw fa-qrcode"></span>&nbsp;<span translate>Identification</span></th>
<td class="text-right">
<span tooltip data-original-title="{{'Click to see full identification string and QR code.' | translate}}">
<a href="" ng-click="showDeviceIdentification(deviceCfg)">{{deviceShortID(deviceCfg.deviceID)}}</a>
</span>
</td>
</tr>
<tr ng-if="connections[deviceCfg.deviceID].clientVersion">
<th><span class="fas fa-fw fa-tag"></span>&nbsp;<span translate>Version</span></th>
<td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
@@ -905,7 +921,7 @@
<ng-include src="'syncthing/core/upgradeModalView.html'"></ng-include>
<ng-include src="'syncthing/core/majorUpgradeModalView.html'"></ng-include>
<ng-include src="'syncthing/core/aboutModalView.html'"></ng-include>
<ng-include src="'syncthing/core/discoveryFailuresModalView.html'"></ng-include>
<ng-include src="'syncthing/core/connectivityStatusModalView.html'"></ng-include>
<ng-include src="'syncthing/folder/removeFolderDialogView.html'"></ng-include>
<ng-include src="'syncthing/folder/revertOverrideView.html'"></ng-include>
<ng-include src="'syncthing/device/removeDeviceDialogView.html'"></ng-include>
@@ -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, Jesse Lucas, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, 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, Tomasz Wilczyński, 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, 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, Choongkyu, Chris Howie, Chris Joel, Chris Tonkinson, Christian Prescott, 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, Eric Lesiuta, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, 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 Lichtblau, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jonathan Cross, Jonta, 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, Lars Lehtonen, 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, MichaIng, 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, Phani Rithvij, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Quentin Hibon, Rahmi Pruitt, Richard Hartmann, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, Ross Smith II, Ruslan Yevdokymov, Sacheendra Talluri, Scott Klupfel, Shaarad Dalvi, Simon Mwepu, Sly_tom_cat, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, 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, bt90, chenrui, chucic, deepsource-autofix[bot], dependabot[bot], derekriemer, desbma, georgespatton, ghjklw, janost, jaseg, jelle van der Waa, klemens, marco-m, mclang, mv1005, otbutz, overkill, perewa, rubenbe, wangguoliang, wouter bolsterlee, xarx00, xjtdy888, 佛跳墙
Jakob Borg, Audrius Butkevicius, Jesse Lucas, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, André Colomb, 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, Tomasz Wilczyński, 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, 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, Choongkyu, Chris Howie, Chris Joel, Chris Tonkinson, Christian Prescott, 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, Eric Lesiuta, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Lampe, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gahl Saraf, Gilli Sigurdsson, Gleb Sinyavskiy, Graham Miln, Han Boetes, HansK-p, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Ikko Ashimine, Ilya Brin, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jack Croft, Jacob, Jake Peterson, James Patterson, Jaroslav Lichtblau, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan, Jonathan Cross, Jonta, 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, Lars Lehtonen, 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, MichaIng, 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, Phani Rithvij, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Quentin Hibon, Rahmi Pruitt, Richard Hartmann, Robert Carosi, Roberto Santalla, Robin Schoonover, Roman Zaynetdinov, Ross Smith II, Ruslan Yevdokymov, Sacheendra Talluri, Scott Klupfel, Shaarad Dalvi, Simon Mwepu, Sly_tom_cat, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Klauser, Tobias Nygren, Tobias Tom, Tom Jakubowski, 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, bt90, chenrui, chucic, deepsource-autofix[bot], dependabot[bot], derekriemer, desbma, georgespatton, ghjklw, janost, jaseg, jelle van der Waa, klemens, marco-m, mclang, mv1005, otbutz, overkill, perewa, rubenbe, wangguoliang, wouter bolsterlee, xarx00, xjtdy888, 佛跳墙
</div>
</div>
<hr />
@@ -0,0 +1,60 @@
<modal id="connectivity-status" status="{{connectivityStatusParams.status}}" icon="fas fa-fw {{connectivityStatusParams.type == 'listeners' ? 'fa-sitemap' : 'fa-map-signs'}}" heading="{{connectivityStatusParams.heading}}" large="no" closeable="yes">
<div class="modal-body" ng-switch="connectivityStatusParams.type">
<div ng-switch-when="listeners">
<p translate ng-if="listenersRunning.length == 0">
Syncthing is not listening for connection attempts from other devices on any address. Only outgoing connections from this device may work.
</p>
<div ng-if="listenersRunning.length > 0">
<p translate>
Syncthing is listening on the following network addresses for connection attempts from other devices:
</p>
<ul>
<li ng-repeat="listener in listenersRunning">{{listener}}</li>
</ul>
</div>
<div ng-if="listenersFailed.length > 0">
<p translate>
Some listening addresses could not be enabled to accept connections:
</p>
<ul>
<li ng-repeat="listener in listenersFailed">{{listener}}</li>
</ul>
</div>
</div>
<div ng-switch-default><!-- discovery methods -->
<p translate ng-if="discoveryRunning.length == 0">
This device cannot automatically discover other devices or announce its own address to be found by others. Only devices with statically configured addresses can connect.
</p>
<div ng-if="discoveryRunning.length > 0">
<p translate>
The following methods are used to discover other devices on the network and announce this device to be found by others:
</p>
<ul>
<li ng-repeat="discovery in discoveryRunning">{{discovery}}</li>
</ul>
</div>
<div ng-if="discoveryFailed.length > 0">
<p translate>
Some discovery methods could not be established for finding other devices or announcing this device:
</p>
<ul>
<li ng-repeat="discovery in discoveryFailed">{{discovery}}</li>
</ul>
</div>
<div class="row">
<div class="col-md-offset-2 col-md-8">
<div class="panel panel-default">
<div translate class="panel-body">
Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
</button>
</div>
</modal>
@@ -1,21 +0,0 @@
<modal id="discovery-failures" status="danger" icon="fas fa-exclamation-circle" heading="{{'Discovery Failures' | translate}}" large="yes" closeable="yes">
<div class="modal-body">
<ul>
<li ng-repeat="failure in discoveryFailed">{{failure}}</li>
</ul>
</div>
<div class="row">
<div class="col-md-offset-2 col-md-8">
<div class="panel panel-default">
<div translate class="panel-body">
Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
</button>
</div>
</modal>
@@ -471,22 +471,30 @@ angular.module('syncthing.core')
}
var listenersFailed = [];
var listenersRunning = [];
for (var address in data.connectionServiceStatus) {
if (data.connectionServiceStatus[address].error) {
listenersFailed.push(address + ": " + data.connectionServiceStatus[address].error);
} else {
listenersRunning.push(address);
}
}
$scope.listenersFailed = listenersFailed;
$scope.listenersRunning = listenersRunning;
$scope.listenersTotal = $scope.sizeOf(data.connectionServiceStatus);
$scope.discoveryTotal = data.discoveryMethods;
var discoveryFailed = [];
for (var disco in data.discoveryErrors) {
if (data.discoveryErrors[disco]) {
discoveryFailed.push(disco + ": " + data.discoveryErrors[disco]);
var discoveryRunning = [];
for (var disco in data.discoveryStatus) {
if (data.discoveryStatus[disco] && data.discoveryStatus[disco].error) {
discoveryFailed.push(disco + ": " + data.discoveryStatus[disco].error);
} else {
discoveryRunning.push(disco);
}
}
$scope.discoveryFailed = discoveryFailed;
$scope.discoveryRunning = discoveryRunning;
$scope.discoveryTotal = $scope.sizeOf(data.discoveryStatus);
refreshNoAuthWarning();
@@ -1202,13 +1210,20 @@ angular.module('syncthing.core')
};
$scope.deviceName = function (deviceCfg) {
if (typeof deviceCfg === 'undefined' || typeof deviceCfg.deviceID === 'undefined') {
if (typeof deviceCfg === 'undefined') {
return "";
}
if (deviceCfg.name) {
return deviceCfg.name;
}
return deviceCfg.deviceID.substr(0, 6);
return $scope.deviceShortID(deviceCfg.deviceID);
};
$scope.deviceShortID = function (deviceID) {
if (typeof deviceID === 'undefined') {
return "";
}
return deviceID.substr(0, 6);
};
$scope.thisDeviceName = function () {
@@ -1222,6 +1237,11 @@ angular.module('syncthing.core')
return device.deviceID.substr(0, 6);
};
$scope.showDeviceIdentification = function (deviceCfg) {
$scope.currentDevice = deviceCfg;
$('#idqr').modal();
};
$scope.setDevicePause = function (device, pause) {
$scope.devices[device].paused = pause;
$scope.config.devices = $scope.deviceList();
@@ -1237,8 +1257,34 @@ angular.module('syncthing.core')
}
};
$scope.showDiscoveryFailures = function () {
$('#discovery-failures').modal();
$scope.showListenerStatus = function () {
var params = {
type: 'listeners',
};
if ($scope.listenersFailed.length > 0) {
params.status = 'danger';
params.heading = $translate.instant("Listener Failures");
} else {
params.status = 'default';
params.heading = $translate.instant("Listener Status");
}
$scope.connectivityStatusParams = params;
$('#connectivity-status').modal();
};
$scope.showDiscoveryStatus = function () {
var params = {
type: 'discovery',
};
if ($scope.discoveryFailed.length > 0) {
params.status = 'danger';
params.heading = $translate.instant("Discovery Failures");
} else {
params.status = 'default';
params.heading = $translate.instant("Discovery Status");
}
$scope.connectivityStatusParams = params;
$('#connectivity-status').modal();
};
$scope.logging = {
@@ -1703,6 +1749,10 @@ angular.module('syncthing.core')
$scope.saveConfig();
};
$scope.dismissPendingDevice = function (deviceID) {
$http.delete(urlbase + '/cluster/pending/devices?device=' + encodeURIComponent(deviceID));
};
$scope.unignoreDeviceFromTemporaryConfig = function (ignoredDevice) {
$scope.tmpRemoteIgnoredDevices = $scope.tmpRemoteIgnoredDevices.filter(function (existingIgnoredDevice) {
return ignoredDevice.deviceID !== existingIgnoredDevice.deviceID;
@@ -2117,7 +2167,7 @@ angular.module('syncthing.core')
folderCfg.versioning.params.command = '' + folderCfg._guiVersioning.externalCommand;
break;
default:
delete folderCfg.versioning;
folderCfg.versioning = {type: ''};
}
delete folderCfg._guiVersioning;
@@ -2174,6 +2224,11 @@ angular.module('syncthing.core')
}
};
$scope.dismissPendingFolder = function (folderID, deviceID) {
$http.delete(urlbase + '/cluster/pending/folders?folder=' + encodeURIComponent(folderID)
+ '&device=' + encodeURIComponent(deviceID));
};
$scope.sharesFolder = function (folderCfg) {
var names = [];
folderCfg.devices.forEach(function (device) {
+57 -8
View File
@@ -291,6 +291,10 @@ func (s *service) Serve(ctx context.Context) error {
restMux.HandlerFunc(http.MethodPost, "/rest/system/resume", s.makeDevicePauseHandler(false)) // [device]
restMux.HandlerFunc(http.MethodPost, "/rest/system/debug", s.postSystemDebug) // [enable] [disable]
// The DELETE handlers
restMux.HandlerFunc(http.MethodDelete, "/rest/cluster/pending/devices", s.deletePendingDevices) // device
restMux.HandlerFunc(http.MethodDelete, "/rest/cluster/pending/folders", s.deletePendingFolders) // folder [device]
// Config endpoints
configBuilder := &configMuxBuilder{
@@ -632,6 +636,21 @@ func (s *service) getPendingDevices(w http.ResponseWriter, r *http.Request) {
sendJSON(w, devices)
}
func (s *service) deletePendingDevices(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
device := qs.Get("device")
deviceID, err := protocol.DeviceIDFromString(device)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := s.model.DismissPendingDevice(deviceID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (s *service) getPendingFolders(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
@@ -650,6 +669,22 @@ func (s *service) getPendingFolders(w http.ResponseWriter, r *http.Request) {
sendJSON(w, folders)
}
func (s *service) deletePendingFolders(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
device := qs.Get("device")
deviceID, err := protocol.DeviceIDFromString(device)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
folderID := qs.Get("folder")
if err := s.model.DismissPendingFolder(deviceID, folderID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (s *service) restPing(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{"ping": "pong"})
}
@@ -1023,16 +1058,16 @@ func (s *service) getSystemStatus(w http.ResponseWriter, r *http.Request) {
res["tilde"] = tilde
if s.cfg.Options().LocalAnnEnabled || s.cfg.Options().GlobalAnnEnabled {
res["discoveryEnabled"] = true
discoErrors := make(map[string]string)
discoMethods := 0
for disco, err := range s.discoverer.ChildErrors() {
discoMethods++
if err != nil {
discoErrors[disco] = err.Error()
discoStatus := s.discoverer.ChildErrors()
res["discoveryStatus"] = discoveryStatusMap(discoStatus)
res["discoveryMethods"] = len(discoStatus) // DEPRECATED: Redundant, only for backwards compatibility, should be removed.
discoErrors := make(map[string]*string, len(discoStatus))
for s, e := range discoStatus {
if e != nil {
discoErrors[s] = errorString(e)
}
}
res["discoveryMethods"] = discoMethods
res["discoveryErrors"] = discoErrors
res["discoveryErrors"] = discoErrors // DEPRECATED: Redundant, only for backwards compatibility, should be removed.
}
res["connectionServiceStatus"] = s.connectionsService.ListenerStatus()
@@ -1905,6 +1940,20 @@ func errorString(err error) *string {
return nil
}
type discoveryStatusEntry struct {
Error *string `json:"error"`
}
func discoveryStatusMap(errs map[string]error) map[string]discoveryStatusEntry {
out := make(map[string]discoveryStatusEntry, len(errs))
for s, e := range errs {
out[s] = discoveryStatusEntry{
Error: errorString(e),
}
}
return out
}
// sanitizedHostname returns the given name in a suitable form for use as
// the common name in a certificate, or an error.
func sanitizedHostname(name string) (string, error) {
+44 -29
View File
@@ -17,6 +17,7 @@ import (
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/util"
)
type configMuxBuilder struct {
@@ -63,10 +64,15 @@ func (c *configMuxBuilder) registerFolders(path string) {
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
var folders []config.FolderConfiguration
if err := unmarshalTo(r.Body, &folders); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
data, err := unmarshalToRawMessages(r.Body)
folders := make([]config.FolderConfiguration, len(data))
defaultFolder := c.cfg.DefaultFolder()
for i, bs := range data {
folders[i] = defaultFolder.Copy()
if err := json.Unmarshal(bs, &folders[i]); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetFolders(folders)
@@ -79,7 +85,7 @@ func (c *configMuxBuilder) registerFolders(path string) {
})
c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustFolder(w, r, config.FolderConfiguration{}, false)
c.adjustFolder(w, r, c.cfg.DefaultFolder(), false)
})
}
@@ -89,10 +95,15 @@ func (c *configMuxBuilder) registerDevices(path string) {
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
var devices []config.DeviceConfiguration
if err := unmarshalTo(r.Body, &devices); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
data, err := unmarshalToRawMessages(r.Body)
devices := make([]config.DeviceConfiguration, len(data))
defaultDevice := c.cfg.DefaultDevice()
for i, bs := range data {
devices[i] = defaultDevice.Copy()
if err := json.Unmarshal(bs, &devices[i]); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetDevices(devices)
@@ -105,19 +116,7 @@ func (c *configMuxBuilder) registerDevices(path string) {
})
c.HandlerFunc(http.MethodPost, path, func(w http.ResponseWriter, r *http.Request) {
var device config.DeviceConfiguration
if err := unmarshalTo(r.Body, &device); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
waiter, err := c.cfg.Modify(func(cfg *config.Configuration) {
cfg.SetDevice(device)
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
c.finish(w, waiter)
c.adjustDevice(w, r, c.cfg.DefaultDevice(), false)
})
}
@@ -132,7 +131,7 @@ func (c *configMuxBuilder) registerFolder(path string) {
})
c.Handle(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
c.adjustFolder(w, r, config.FolderConfiguration{}, false)
c.adjustFolder(w, r, c.cfg.DefaultFolder(), false)
})
c.Handle(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
@@ -176,7 +175,7 @@ func (c *configMuxBuilder) registerDevice(path string) {
})
c.Handle(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
c.adjustDevice(w, r, config.DeviceConfiguration{}, false)
c.adjustDevice(w, r, c.cfg.DefaultDevice(), false)
})
c.Handle(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
@@ -206,7 +205,9 @@ func (c *configMuxBuilder) registerDefaultFolder(path string) {
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustFolder(w, r, config.FolderConfiguration{}, true)
var cfg config.FolderConfiguration
util.SetDefaults(&cfg)
c.adjustFolder(w, r, cfg, true)
})
c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
@@ -220,7 +221,9 @@ func (c *configMuxBuilder) registerDefaultDevice(path string) {
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustDevice(w, r, config.DeviceConfiguration{}, true)
var cfg config.DeviceConfiguration
util.SetDefaults(&cfg)
c.adjustDevice(w, r, cfg, true)
})
c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
@@ -234,7 +237,9 @@ func (c *configMuxBuilder) registerOptions(path string) {
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustOptions(w, r, config.OptionsConfiguration{})
var cfg config.OptionsConfiguration
util.SetDefaults(&cfg)
c.adjustOptions(w, r, cfg)
})
c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
@@ -248,7 +253,9 @@ func (c *configMuxBuilder) registerLDAP(path string) {
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustLDAP(w, r, config.LDAPConfiguration{})
var cfg config.LDAPConfiguration
util.SetDefaults(&cfg)
c.adjustLDAP(w, r, cfg)
})
c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
@@ -262,7 +269,9 @@ func (c *configMuxBuilder) registerGUI(path string) {
})
c.HandlerFunc(http.MethodPut, path, func(w http.ResponseWriter, r *http.Request) {
c.adjustGUI(w, r, config.GUIConfiguration{})
var cfg config.GUIConfiguration
util.SetDefaults(&cfg)
c.adjustGUI(w, r, cfg)
})
c.HandlerFunc(http.MethodPatch, path, func(w http.ResponseWriter, r *http.Request) {
@@ -403,6 +412,12 @@ func unmarshalTo(body io.ReadCloser, to interface{}) error {
return json.Unmarshal(bs, to)
}
func unmarshalToRawMessages(body io.ReadCloser) ([]json.RawMessage, error) {
var data []json.RawMessage
err := unmarshalTo(body, &data)
return data, err
}
func checkGUIPassword(oldPassword, newPassword string) (string, error) {
if newPassword == oldPassword {
return newPassword, nil
+29 -4
View File
@@ -163,19 +163,44 @@ func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, int, error) {
}
func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
var cfg Configuration
util.SetDefaults(&cfg)
bs, err := ioutil.ReadAll(r)
if err != nil {
return Configuration{}, err
}
var cfg Configuration
util.SetDefaults(&cfg)
if err := json.Unmarshal(bs, &cfg); err != nil {
return Configuration{}, err
}
// Unmarshal list of devices and folders separately to set defaults
var rawFoldersDevices struct {
Folders []json.RawMessage
Devices []json.RawMessage
}
if err := json.Unmarshal(bs, &rawFoldersDevices); err != nil {
return Configuration{}, err
}
cfg.Folders = make([]FolderConfiguration, len(rawFoldersDevices.Folders))
for i, bs := range rawFoldersDevices.Folders {
cfg.Folders[i] = cfg.Defaults.Folder.Copy()
if err := json.Unmarshal(bs, &cfg.Folders[i]); err != nil {
return Configuration{}, err
}
}
cfg.Devices = make([]DeviceConfiguration, len(rawFoldersDevices.Devices))
for i, bs := range rawFoldersDevices.Devices {
cfg.Devices[i] = cfg.Defaults.Device.Copy()
if err := json.Unmarshal(bs, &cfg.Devices[i]); err != nil {
return Configuration{}, err
}
}
if err := cfg.prepare(myID); err != nil {
return Configuration{}, err
}
+72 -40
View File
@@ -70,6 +70,9 @@ type Lowlevel struct {
recheckInterval time.Duration
oneFileSetCreated chan struct{}
evLogger events.Logger
blockFilter *bloomFilter
versionFilter *bloomFilter
}
func NewLowlevel(backend backend.Backend, evLogger events.Logger, opts ...Option) (*Lowlevel, error) {
@@ -686,29 +689,30 @@ func (db *Lowlevel) gcIndirect(ctx context.Context) (err error) {
// Indirection GC needs to run when there are no modifications to the
// FileInfos or indirected items.
db.gcMut.Lock()
defer db.gcMut.Unlock()
l.Debugln("Starting database GC")
l.Debugln("Started database GC")
// Create a new set of bloom filters, while holding the gcMut which
// guarantees that no other modifications are happening concurrently.
db.gcMut.Lock()
capacity := indirectGCBloomCapacity
if db.gcKeyCount > capacity {
capacity = db.gcKeyCount
}
db.blockFilter = newBloomFilter(capacity)
db.versionFilter = newBloomFilter(capacity)
db.gcMut.Unlock()
defer func() {
// Forget the bloom filters on the way out.
db.gcMut.Lock()
db.blockFilter = nil
db.versionFilter = nil
db.gcMut.Unlock()
}()
var discardedBlocks, matchedBlocks, discardedVersions, matchedVersions int
internalCtx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
// Only print something if the process takes more than "a moment".
select {
case <-internalCtx.Done():
case <-time.After(10 * time.Second):
l.Infoln("Database GC started - many Syncthing operations will be unresponsive until it's finished")
<-internalCtx.Done()
if err != nil || ctx.Err() != nil {
return
}
l.Infof("Database GC done (discarded/remaining: %v/%v blocks, %v/%v versions)", discardedBlocks, matchedBlocks, discardedVersions, matchedVersions)
}
}()
t, err := db.newReadWriteTransaction()
if err != nil {
return err
@@ -720,16 +724,13 @@ func (db *Lowlevel) gcIndirect(ctx context.Context) (err error) {
// items. For simplicity's sake we track just one count, which is the
// highest of the various indirected items.
capacity := indirectGCBloomCapacity
if db.gcKeyCount > capacity {
capacity = db.gcKeyCount
}
blockFilter := newBloomFilter(capacity)
versionFilter := newBloomFilter(capacity)
// Iterate the FileInfos, unmarshal the block and version hashes and
// add them to the filter.
// This happens concurrently with normal database modifications, though
// those modifications will now also add their blocks and versions to
// the bloom filters.
it, err := t.NewPrefixIterator([]byte{KeyTypeDevice})
if err != nil {
return err
@@ -746,18 +747,35 @@ func (db *Lowlevel) gcIndirect(ctx context.Context) (err error) {
if err := hashes.Unmarshal(it.Value()); err != nil {
return err
}
if len(hashes.BlocksHash) > 0 {
blockFilter.add(hashes.BlocksHash)
}
if len(hashes.VersionHash) > 0 {
versionFilter.add(hashes.VersionHash)
}
db.recordIndirectionHashes(hashes)
}
it.Release()
if err := it.Error(); err != nil {
return err
}
// For the next phase we grab the GC lock again and hold it for the rest
// of the method call. Now there can't be any further modifications to
// the database or the bloom filters.
db.gcMut.Lock()
defer db.gcMut.Unlock()
// Only print something if the process takes more than "a moment".
logWait := make(chan struct{})
logTimer := time.AfterFunc(10*time.Second, func() {
l.Infoln("Database GC in progress - many Syncthing operations will be unresponsive until it's finished")
close(logWait)
})
defer func() {
if logTimer.Stop() {
return
}
<-logWait // Make sure messages are sent in order.
l.Infof("Database GC complete (discarded/remaining: %v/%v blocks, %v/%v versions)",
discardedBlocks, matchedBlocks, discardedVersions, matchedVersions)
}()
// Iterate over block lists, removing keys with hashes that don't match
// the filter.
@@ -774,7 +792,7 @@ func (db *Lowlevel) gcIndirect(ctx context.Context) (err error) {
}
key := blockListKey(it.Key())
if blockFilter.has(key.Hash()) {
if db.blockFilter.has(key.Hash()) {
matchedBlocks++
continue
}
@@ -803,7 +821,7 @@ func (db *Lowlevel) gcIndirect(ctx context.Context) (err error) {
}
key := versionKey(it.Key())
if versionFilter.has(key.Hash()) {
if db.versionFilter.has(key.Hash()) {
matchedVersions++
continue
}
@@ -827,17 +845,31 @@ func (db *Lowlevel) gcIndirect(ctx context.Context) (err error) {
return err
}
l.Debugf("Finished GC, starting compaction (discarded/remaining: %v/%v blocks, %v/%v versions)", discardedBlocks, matchedBlocks, discardedVersions, matchedVersions)
l.Debugf("Finished GC (discarded/remaining: %v/%v blocks, %v/%v versions)", discardedBlocks, matchedBlocks, discardedVersions, matchedVersions)
return db.Compact()
return nil
}
func newBloomFilter(capacity int) bloomFilter {
func (db *Lowlevel) recordIndirectionHashesForFile(f *protocol.FileInfo) {
db.recordIndirectionHashes(IndirectionHashesOnly{BlocksHash: f.BlocksHash, VersionHash: f.VersionHash})
}
func (db *Lowlevel) recordIndirectionHashes(hs IndirectionHashesOnly) {
// must be called with gcMut held (at least read-held)
if db.blockFilter != nil && len(hs.BlocksHash) > 0 {
db.blockFilter.add(hs.BlocksHash)
}
if db.versionFilter != nil && len(hs.VersionHash) > 0 {
db.versionFilter.add(hs.VersionHash)
}
}
func newBloomFilter(capacity int) *bloomFilter {
var buf [16]byte
io.ReadFull(rand.Reader, buf[:])
return bloomFilter{
f: blobloom.NewOptimized(blobloom.Config{
return &bloomFilter{
f: blobloom.NewSyncOptimized(blobloom.Config{
Capacity: uint64(capacity),
FPRate: indirectGCBloomFalsePositiveRate,
MaxBits: 8 * indirectGCBloomMaxBytes,
@@ -849,7 +881,7 @@ func newBloomFilter(capacity int) bloomFilter {
}
type bloomFilter struct {
f *blobloom.Filter
f *blobloom.SyncFilter
k0, k1 uint64 // Random key for SipHash.
}
+11 -6
View File
@@ -26,11 +26,13 @@ func (db *Lowlevel) AddOrUpdatePendingDevice(device protocol.DeviceID, name, add
return db.Put(key, bs)
}
func (db *Lowlevel) RemovePendingDevice(device protocol.DeviceID) {
func (db *Lowlevel) RemovePendingDevice(device protocol.DeviceID) error {
key := db.keyer.GeneratePendingDeviceKey(nil, device[:])
if err := db.Delete(key); err != nil {
l.Warnf("Failed to remove pending device entry: %v", err)
return err
}
return nil
}
// PendingDevices enumerates all entries. Invalid ones are dropped from the database
@@ -79,32 +81,35 @@ func (db *Lowlevel) AddOrUpdatePendingFolder(id string, of ObservedFolder, devic
}
// RemovePendingFolderForDevice removes entries for specific folder / device combinations.
func (db *Lowlevel) RemovePendingFolderForDevice(id string, device protocol.DeviceID) {
func (db *Lowlevel) RemovePendingFolderForDevice(id string, device protocol.DeviceID) error {
key, err := db.keyer.GeneratePendingFolderKey(nil, device[:], []byte(id))
if err != nil {
return
return err
}
if err := db.Delete(key); err != nil {
l.Warnf("Failed to remove pending folder entry: %v", err)
return err
}
return nil
}
// RemovePendingFolder removes all entries matching a specific folder ID.
func (db *Lowlevel) RemovePendingFolder(id string) {
func (db *Lowlevel) RemovePendingFolder(id string) error {
iter, err := db.NewPrefixIterator([]byte{KeyTypePendingFolder})
if err != nil {
l.Infof("Could not iterate through pending folder entries: %v", err)
return
return err
}
defer iter.Release()
for iter.Next() {
if id != string(db.keyer.FolderFromPendingFolderKey(iter.Key())) {
continue
}
if err := db.Delete(iter.Key()); err != nil {
if err = db.Delete(iter.Key()); err != nil {
l.Warnf("Failed to remove pending folder entry: %v", err)
}
}
return err
}
// Consolidated information about a pending folder
+14 -2
View File
@@ -398,12 +398,16 @@ func (t *readOnlyTransaction) withBlocksHash(folder, hash []byte, iterator Itera
f.Name = osutil.NativeFilename(f.Name)
if !bytes.Equal(f.BlocksHash, hash) {
l.Warnf("Mismatching block map list hashes: got %x expected %x", f.BlocksHash, hash)
msg := "Mismatching block map list hashes"
t.evLogger.Log(events.Failure, fmt.Sprintln(msg, "in withBlocksHash"))
l.Warnf("%v: got %x expected %x", msg, f.BlocksHash, hash)
continue
}
if f.IsDeleted() || f.IsInvalid() || f.IsDirectory() || f.IsSymlink() {
l.Warnf("Found something of unexpected type in block list map: %s", f)
msg := "Found something of unexpected type in block list map"
t.evLogger.Log(events.Failure, fmt.Sprintln(msg, "in withBlocksHash"))
l.Warnf("%v: %s", msg, f)
continue
}
@@ -534,6 +538,11 @@ func (t *readOnlyTransaction) withNeedLocal(folder []byte, truncate bool, fn Ite
type readWriteTransaction struct {
backend.WriteTransaction
readOnlyTransaction
indirectionTracker
}
type indirectionTracker interface {
recordIndirectionHashesForFile(f *protocol.FileInfo)
}
func (db *Lowlevel) newReadWriteTransaction(hooks ...backend.CommitHook) (readWriteTransaction, error) {
@@ -547,6 +556,7 @@ func (db *Lowlevel) newReadWriteTransaction(hooks ...backend.CommitHook) (readWr
ReadTransaction: tran,
keyer: db.keyer,
},
indirectionTracker: db,
}, nil
}
@@ -606,6 +616,8 @@ func (t readWriteTransaction) putFile(fkey []byte, fi protocol.FileInfo) error {
fi.VersionHash = nil
}
t.indirectionTracker.recordIndirectionHashesForFile(&fi)
fiBs := mustMarshal(&fi)
return t.Put(fkey, fiBs)
}
+7 -1
View File
@@ -885,7 +885,13 @@ func (f *fakeFile) Truncate(size int64) error {
defer f.mut.Unlock()
if f.content != nil {
f.content = f.content[:int(size)]
if int64(cap(f.content)) < size {
c := make([]byte, size)
copy(c[:len(f.content)], f.content)
f.content = c
} else {
f.content = f.content[:int(size)]
}
}
f.rng = nil
f.size = size
+13 -5
View File
@@ -100,9 +100,15 @@ func (f *receiveOnlyFolder) revert() error {
}
fi.LocalFlags &^= protocol.FlagLocalReceiveOnly
if len(fi.Version.Counters) == 1 && fi.Version.Counters[0].ID == f.shortID {
// We are the only device mentioned in the version vector so the
// file must originate here. A revert then means to delete it.
switch gf, ok := snap.GetGlobal(fi.Name); {
case !ok:
msg := "Unexpected global file that we have locally"
l.Debugf("%v revert: %v: %v", f, msg, fi.Name)
f.evLogger.Log(events.Failure, msg)
return true
case gf.IsReceiveOnlyChanged():
// The global file is our own. A revert then means to delete it.
// We'll delete files directly, directories get queued and
// handled below.
@@ -114,10 +120,12 @@ func (f *receiveOnlyFolder) revert() error {
if !handled {
return true // continue
}
fi.SetDeleted(f.shortID)
fi.Version = protocol.Vector{} // if this file ever resurfaces anywhere we want our delete to be strictly older
} else {
case gf.IsEquivalentOptional(fi, f.modTimeWindow, false, false, protocol.FlagLocalReceiveOnly):
// What we have locally is equivalent to the global file.
fi = gf
default:
// Revert means to throw away our local changes. We reset the
// version to the empty vector, which is strictly older than any
// other existing version. It is not in conflict with anything,
+76 -6
View File
@@ -14,6 +14,7 @@ import (
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/scanner"
@@ -29,6 +30,7 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
// Create some test data
@@ -110,6 +112,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
// Create some test data
@@ -136,7 +139,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
if size.Files != 1 || size.Directories != 1 {
t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
}
size = needSize(t, m, "ro")
size = needSizeLocal(t, m, "ro")
if size.Files+size.Directories > 0 {
t.Fatalf("Need: expected nothing: %+v", size)
}
@@ -165,7 +168,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
t.Fatalf("Local: expected the new file to be reflected: %+v", size)
}
size = needSize(t, m, "ro")
size = needSizeLocal(t, m, "ro")
if size.Files+size.Directories > 0 {
t.Fatalf("Need: expected nothing: %+v", size)
}
@@ -186,7 +189,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
t.Fatalf("Local: expected the local size to remain: %+v", size)
}
size = needSize(t, m, "ro")
size = needSizeLocal(t, m, "ro")
if size.Files != 1 || size.Bytes != int64(len(oldData)) {
t.Fatalf("Local: expected to need the old file data: %+v", size)
}
@@ -199,6 +202,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
// Create some test data
@@ -225,7 +229,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
if size.Files != 1 || size.Directories != 1 {
t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
}
size = needSize(t, m, "ro")
size = needSizeLocal(t, m, "ro")
if size.Files+size.Directories > 0 {
t.Fatalf("Need: expected nothing: %+v", size)
}
@@ -268,6 +272,7 @@ func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
// Create some test data
@@ -294,7 +299,7 @@ func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
if size.Files != 1 || size.Directories != 1 {
t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
}
size = needSize(t, m, "ro")
size = needSizeLocal(t, m, "ro")
if size.Files+size.Directories > 0 {
t.Fatalf("Need: expected nothing: %+v", size)
}
@@ -332,6 +337,7 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
// Create some test data
@@ -358,7 +364,7 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
if size.Files != 1 || size.Directories != 1 {
t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
}
size = needSize(t, m, "ro")
size = needSizeLocal(t, m, "ro")
if size.Files+size.Directories > 0 {
t.Fatalf("Need: expected nothing: %+v", size)
}
@@ -410,6 +416,70 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
}
}
func TestRecvOnlyRevertOwnID(t *testing.T) {
// If the folder was receive-only in the past, the global item might have
// only our id in the version vector and be valid. There was a bug based on
// the incorrect assumption that this can never happen.
// Get us a model up and running
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
// Create some test data
must(t, ffs.MkdirAll(".stfolder", 0755))
data := []byte("hello\n")
name := "foo"
must(t, writeFile(ffs, name, data, 0644))
// Make sure the file is scanned and locally changed
must(t, m.ScanFolder("ro"))
fi, ok := m.testCurrentFolderFile(f.ID, name)
if !ok {
t.Fatal("File missing")
} else if !fi.IsReceiveOnlyChanged() {
t.Fatal("File should be receiveonly changed")
}
fi.LocalFlags = 0
v := fi.Version.Counters[0].Value
fi.Version.Counters[0].Value = uint64(time.Unix(int64(v), 0).Add(-10 * time.Second).Unix())
// Monitor the outcome
sub := f.evLogger.Subscribe(events.LocalIndexUpdated)
defer sub.Unsubscribe()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
for {
select {
case <-ctx.Done():
return
case <-sub.C():
if file, _ := m.testCurrentFolderFile(f.ID, name); file.Deleted {
t.Error("local file was deleted")
cancel()
} else if file.IsEquivalent(fi, f.modTimeWindow) {
cancel() // That's what we are waiting for
}
}
}
}()
// Receive an index update with an older version, but valid and then revert
must(t, m.Index(device1, f.ID, []protocol.FileInfo{fi}))
f.Revert()
select {
case <-ctx.Done():
case <-time.After(10 * time.Second):
t.Fatal("timed out")
}
}
func setupKnownFiles(t *testing.T, ffs fs.Filesystem, data []byte) []protocol.FileInfo {
t.Helper()
+1
View File
@@ -1280,6 +1280,7 @@ func TestPullSymlinkOverExistingWindows(t *testing.T) {
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
addFakeConn(m, device1, f.ID)
name := "foo"
if fd, err := f.mtimefs.Create(name); err != nil {
+549
View File
@@ -0,0 +1,549 @@
// 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 model
import (
"context"
"fmt"
"sync"
"time"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/svcutil"
)
type indexHandler struct {
conn protocol.Connection
downloads *deviceDownloadState
folder string
folderIsReceiveEncrypted bool
prevSequence int64
evLogger events.Logger
token suture.ServiceToken
cond *sync.Cond
paused bool
fset *db.FileSet
runner service
}
func newIndexHandler(conn protocol.Connection, downloads *deviceDownloadState, folder config.FolderConfiguration, fset *db.FileSet, runner service, startInfo *clusterConfigDeviceInfo, evLogger events.Logger) *indexHandler {
myIndexID := fset.IndexID(protocol.LocalDeviceID)
mySequence := fset.Sequence(protocol.LocalDeviceID)
var startSequence int64
// This is the other side's description of what it knows
// about us. Lets check to see if we can start sending index
// updates directly or need to send the index from start...
if startInfo.local.IndexID == myIndexID {
// They say they've seen our index ID before, so we can
// send a delta update only.
if startInfo.local.MaxSequence > mySequence {
// Safety check. They claim to have more or newer
// index data than we have - either we have lost
// index data, or reset the index without resetting
// the IndexID, or something else weird has
// happened. We send a full index to reset the
// situation.
l.Infof("Device %v folder %s is delta index compatible, but seems out of sync with reality", conn.ID().Short(), folder.Description())
startSequence = 0
} else {
l.Debugf("Device %v folder %s is delta index compatible (mlv=%d)", conn.ID().Short(), folder.Description(), startInfo.local.MaxSequence)
startSequence = startInfo.local.MaxSequence
}
} else if startInfo.local.IndexID != 0 {
// They say they've seen an index ID from us, but it's
// not the right one. Either they are confused or we
// must have reset our database since last talking to
// them. We'll start with a full index transfer.
l.Infof("Device %v folder %s has mismatching index ID for us (%v != %v)", conn.ID().Short(), folder.Description(), startInfo.local.IndexID, myIndexID)
startSequence = 0
} else {
l.Debugf("Device %v folder %s has no index ID for us", conn.ID().Short(), folder.Description())
}
// This is the other side's description of themselves. We
// check to see that it matches the IndexID we have on file,
// otherwise we drop our old index data and expect to get a
// completely new set.
theirIndexID := fset.IndexID(conn.ID())
if startInfo.remote.IndexID == 0 {
// They're not announcing an index ID. This means they
// do not support delta indexes and we should clear any
// information we have from them before accepting their
// index, which will presumably be a full index.
l.Debugf("Device %v folder %s does not announce an index ID", conn.ID().Short(), folder.Description())
fset.Drop(conn.ID())
} else if startInfo.remote.IndexID != theirIndexID {
// The index ID we have on file is not what they're
// announcing. They must have reset their database and
// will probably send us a full index. We drop any
// information we have and remember this new index ID
// instead.
l.Infof("Device %v folder %s has a new index ID (%v)", conn.ID().Short(), folder.Description(), startInfo.remote.IndexID)
fset.Drop(conn.ID())
fset.SetIndexID(conn.ID(), startInfo.remote.IndexID)
}
return &indexHandler{
conn: conn,
downloads: downloads,
folder: folder.ID,
folderIsReceiveEncrypted: folder.Type == config.FolderTypeReceiveEncrypted,
prevSequence: startSequence,
evLogger: evLogger,
fset: fset,
runner: runner,
cond: sync.NewCond(new(sync.Mutex)),
}
}
func (s *indexHandler) Serve(ctx context.Context) (err error) {
l.Debugf("Starting index handler for %s to %s at %s (slv=%d)", s.folder, s.conn.ID(), s.conn, s.prevSequence)
defer func() {
err = svcutil.NoRestartErr(err)
l.Debugf("Exiting index handler for %s to %s at %s: %v", s.folder, s.conn.ID(), s.conn, err)
}()
// We need to send one index, regardless of whether there is something to send or not
s.cond.L.Lock()
for s.paused {
s.cond.Wait()
}
fset := s.fset
s.cond.L.Unlock()
err = s.sendIndexTo(ctx, fset)
// Subscribe to LocalIndexUpdated (we have new information to send) and
// DeviceDisconnected (it might be us who disconnected, so we should
// exit).
sub := s.evLogger.Subscribe(events.LocalIndexUpdated | events.DeviceDisconnected)
defer sub.Unsubscribe()
evChan := sub.C()
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for err == nil {
s.cond.L.Lock()
for s.paused {
s.cond.Wait()
}
fset := s.fset
s.cond.L.Unlock()
// While we have sent a sequence at least equal to the one
// currently in the database, wait for the local index to update. The
// local index may update for other folders than the one we are
// sending for.
if fset.Sequence(protocol.LocalDeviceID) <= s.prevSequence {
select {
case <-ctx.Done():
return ctx.Err()
case <-evChan:
case <-ticker.C:
}
continue
}
err = s.sendIndexTo(ctx, fset)
// Wait a short amount of time before entering the next loop. If there
// are continuous changes happening to the local index, this gives us
// time to batch them up a little.
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(250 * time.Millisecond):
}
}
return err
}
func (s *indexHandler) resume(fset *db.FileSet, runner service) {
s.cond.L.Lock()
if !s.paused {
s.evLogger.Log(events.Failure, "index handler got resumed while not paused")
}
s.paused = false
s.fset = fset
s.runner = runner
s.cond.L.Unlock()
}
func (s *indexHandler) pause() {
s.cond.L.Lock()
if s.paused {
s.evLogger.Log(events.Failure, "index handler got paused while already paused")
}
s.paused = true
s.fset = nil
s.runner = nil
s.cond.L.Unlock()
}
// sendIndexTo sends file infos with a sequence number higher than prevSequence and
// returns the highest sent sequence number.
func (s *indexHandler) sendIndexTo(ctx context.Context, fset *db.FileSet) error {
initial := s.prevSequence == 0
batch := db.NewFileInfoBatch(nil)
batch.SetFlushFunc(func(fs []protocol.FileInfo) error {
l.Debugf("%v: Sending %d files (<%d bytes)", s, len(fs), batch.Size())
if initial {
initial = false
return s.conn.Index(ctx, s.folder, fs)
}
return s.conn.IndexUpdate(ctx, s.folder, fs)
})
var err error
var f protocol.FileInfo
snap, err := fset.Snapshot()
if err != nil {
return svcutil.AsFatalErr(err, svcutil.ExitError)
}
defer snap.Release()
previousWasDelete := false
snap.WithHaveSequence(s.prevSequence+1, func(fi protocol.FileIntf) bool {
// This is to make sure that renames (which is an add followed by a delete) land in the same batch.
// Even if the batch is full, we allow a last delete to slip in, we do this by making sure that
// the batch ends with a non-delete, or that the last item in the batch is already a delete
if batch.Full() && (!fi.IsDeleted() || previousWasDelete) {
if err = batch.Flush(); err != nil {
return false
}
}
if shouldDebug() {
if fi.SequenceNo() < s.prevSequence+1 {
panic(fmt.Sprintln("sequence lower than requested, got:", fi.SequenceNo(), ", asked to start at:", s.prevSequence+1))
}
}
if f.Sequence > 0 && fi.SequenceNo() <= f.Sequence {
l.Warnln("Non-increasing sequence detected: Checking and repairing the db...")
// Abort this round of index sending - the next one will pick
// up from the last successful one with the repeaired db.
defer func() {
if fixed, dbErr := fset.RepairSequence(); dbErr != nil {
l.Warnln("Failed repairing sequence entries:", dbErr)
panic("Failed repairing sequence entries")
} else {
s.evLogger.Log(events.Failure, "detected and repaired non-increasing sequence")
l.Infof("Repaired %v sequence entries in database", fixed)
}
}()
return false
}
f = fi.(protocol.FileInfo)
// If this is a folder receiving encrypted files only, we
// mustn't ever send locally changed file infos. Those aren't
// encrypted and thus would be a protocol error at the remote.
if s.folderIsReceiveEncrypted && fi.IsReceiveOnlyChanged() {
return true
}
f = prepareFileInfoForIndex(f)
previousWasDelete = f.IsDeleted()
batch.Append(f)
return true
})
if err != nil {
return err
}
err = batch.Flush()
// True if there was nothing to be sent
if f.Sequence == 0 {
return err
}
s.prevSequence = f.Sequence
return err
}
func (s *indexHandler) receive(fs []protocol.FileInfo, update bool, op string) error {
deviceID := s.conn.ID()
s.cond.L.Lock()
paused := s.paused
fset := s.fset
runner := s.runner
s.cond.L.Unlock()
if paused {
l.Infof("%v for paused folder %q", op, s.folder)
return fmt.Errorf("%v: %w", s.folder, ErrFolderPaused)
}
defer runner.SchedulePull()
s.downloads.Update(s.folder, makeForgetUpdate(fs))
if !update {
fset.Drop(deviceID)
}
for i := range fs {
// The local attributes should never be transmitted over the wire.
// Make sure they look like they weren't.
fs[i].LocalFlags = 0
fs[i].VersionHash = nil
}
fset.Update(deviceID, fs)
seq := fset.Sequence(deviceID)
s.evLogger.Log(events.RemoteIndexUpdated, map[string]interface{}{
"device": deviceID.String(),
"folder": s.folder,
"items": len(fs),
"sequence": seq,
"version": seq, // legacy for sequence
})
return nil
}
func prepareFileInfoForIndex(f protocol.FileInfo) protocol.FileInfo {
// Mark the file as invalid if any of the local bad stuff flags are set.
f.RawInvalid = f.IsInvalid()
// If the file is marked LocalReceive (i.e., changed locally on a
// receive only folder) we do not want it to ever become the
// globally best version, invalid or not.
if f.IsReceiveOnlyChanged() {
f.Version = protocol.Vector{}
}
// never sent externally
f.LocalFlags = 0
f.VersionHash = nil
return f
}
func (s *indexHandler) String() string {
return fmt.Sprintf("indexHandler@%p for %s to %s at %s", s, s.folder, s.conn.ID().Short(), s.conn)
}
type indexHandlerRegistry struct {
sup *suture.Supervisor
evLogger events.Logger
conn protocol.Connection
downloads *deviceDownloadState
indexHandlers map[string]*indexHandler
startInfos map[string]*clusterConfigDeviceInfo
folderStates map[string]*indexHandlerFolderState
mut sync.Mutex
}
type indexHandlerFolderState struct {
cfg config.FolderConfiguration
fset *db.FileSet
runner service
}
func newIndexHandlerRegistry(conn protocol.Connection, downloads *deviceDownloadState, closed chan struct{}, parentSup *suture.Supervisor, evLogger events.Logger) *indexHandlerRegistry {
r := &indexHandlerRegistry{
conn: conn,
downloads: downloads,
evLogger: evLogger,
indexHandlers: make(map[string]*indexHandler),
startInfos: make(map[string]*clusterConfigDeviceInfo),
folderStates: make(map[string]*indexHandlerFolderState),
mut: sync.Mutex{},
}
r.sup = suture.New(r.String(), svcutil.SpecWithDebugLogger(l))
ourToken := parentSup.Add(r.sup)
r.sup.Add(svcutil.AsService(func(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-closed:
parentSup.Remove(ourToken)
}
return nil
}, fmt.Sprintf("%v/waitForClosed", r)))
return r
}
func (r *indexHandlerRegistry) String() string {
return fmt.Sprintf("indexHandlerRegistry/%v", r.conn.ID().Short())
}
func (r *indexHandlerRegistry) GetSupervisor() *suture.Supervisor {
return r.sup
}
func (r *indexHandlerRegistry) startLocked(folder config.FolderConfiguration, fset *db.FileSet, runner service, startInfo *clusterConfigDeviceInfo) {
if is, ok := r.indexHandlers[folder.ID]; ok {
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexHandlers, folder.ID)
}
delete(r.startInfos, folder.ID)
is := newIndexHandler(r.conn, r.downloads, folder, fset, runner, startInfo, r.evLogger)
is.token = r.sup.Add(is)
r.indexHandlers[folder.ID] = is
}
// AddIndexInfo starts an index handler for given folder, unless it is paused.
// If it is paused, the given startInfo is stored to start the sender once the
// folder is resumed.
// If an index handler is already running, it will be stopped first.
func (r *indexHandlerRegistry) AddIndexInfo(folder string, startInfo *clusterConfigDeviceInfo) {
r.mut.Lock()
defer r.mut.Unlock()
if is, ok := r.indexHandlers[folder]; ok {
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexHandlers, folder)
l.Debugf("Removed index sender for device %v and folder %v due to added pending", r.conn.ID().Short(), folder)
}
folderState, ok := r.folderStates[folder]
if !ok {
l.Debugf("Pending index handler for device %v and folder %v", r.conn.ID().Short(), folder)
r.startInfos[folder] = startInfo
return
}
r.startLocked(folderState.cfg, folderState.fset, folderState.runner, startInfo)
}
// Remove stops a running index handler or removes one pending to be started.
// It is a noop if the folder isn't known.
func (r *indexHandlerRegistry) Remove(folder string) {
r.mut.Lock()
defer r.mut.Unlock()
l.Debugf("Removing index handler for device %v and folder %v", r.conn.ID().Short(), folder)
if is, ok := r.indexHandlers[folder]; ok {
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexHandlers, folder)
}
delete(r.startInfos, folder)
l.Debugf("Removed index handler for device %v and folder %v", r.conn.ID().Short(), folder)
}
// RemoveAllExcept stops all running index handlers and removes those pending to be started,
// except mentioned ones.
// It is a noop if the folder isn't known.
func (r *indexHandlerRegistry) RemoveAllExcept(except map[string]struct{}) {
r.mut.Lock()
defer r.mut.Unlock()
for folder, is := range r.indexHandlers {
if _, ok := except[folder]; !ok {
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexHandlers, folder)
l.Debugf("Removed index handler for device %v and folder %v (removeAllExcept)", r.conn.ID().Short(), folder)
}
}
for folder := range r.startInfos {
if _, ok := except[folder]; !ok {
delete(r.startInfos, folder)
l.Debugf("Removed pending index handler for device %v and folder %v (removeAllExcept)", r.conn.ID().Short(), folder)
}
}
}
// RegisterFolderState must be called whenever something about the folder
// changes. The exception being if the folder is removed entirely, then call
// Remove. The fset and runner arguments may be nil, if given folder is paused.
func (r *indexHandlerRegistry) RegisterFolderState(folder config.FolderConfiguration, fset *db.FileSet, runner service) {
if !folder.SharedWith(r.conn.ID()) {
r.Remove(folder.ID)
return
}
r.mut.Lock()
if folder.Paused {
r.folderPausedLocked(folder.ID)
} else {
r.folderStartedLocked(folder, fset, runner)
}
r.mut.Unlock()
}
// folderPausedLocked stops a running index handler.
// It is a noop if the folder isn't known or has not been started yet.
func (r *indexHandlerRegistry) folderPausedLocked(folder string) {
l.Debugf("Pausing index handler for device %v and folder %v", r.conn.ID().Short(), folder)
delete(r.folderStates, folder)
if is, ok := r.indexHandlers[folder]; ok {
is.pause()
l.Debugf("Paused index handler for device %v and folder %v", r.conn.ID().Short(), folder)
} else {
l.Debugf("No index handler for device %v and folder %v to pause", r.conn.ID().Short(), folder)
}
}
// folderStartedLocked resumes an already running index handler or starts it, if it
// was added while paused.
// It is a noop if the folder isn't known.
func (r *indexHandlerRegistry) folderStartedLocked(folder config.FolderConfiguration, fset *db.FileSet, runner service) {
r.folderStates[folder.ID] = &indexHandlerFolderState{
cfg: folder,
fset: fset,
runner: runner,
}
is, isOk := r.indexHandlers[folder.ID]
if info, ok := r.startInfos[folder.ID]; ok {
if isOk {
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexHandlers, folder.ID)
l.Debugf("Removed index handler for device %v and folder %v in resume", r.conn.ID().Short(), folder.ID)
}
r.startLocked(folder, fset, runner, info)
delete(r.startInfos, folder.ID)
l.Debugf("Started index handler for device %v and folder %v in resume", r.conn.ID().Short(), folder.ID)
} else if isOk {
l.Debugf("Resuming index handler for device %v and folder %v", r.conn.ID().Short(), folder)
is.resume(fset, runner)
} else {
l.Debugf("Not resuming index handler for device %v and folder %v as none is paused and there is no start info", r.conn.ID().Short(), folder.ID)
}
}
func (r *indexHandlerRegistry) ReceiveIndex(folder string, fs []protocol.FileInfo, update bool, op string) error {
r.mut.Lock()
defer r.mut.Unlock()
is, isOk := r.indexHandlers[folder]
if !isOk {
l.Infof("%v for nonexistent or paused folder %q", op, folder)
return ErrFolderMissing
}
return is.receive(fs, update, op)
}
// makeForgetUpdate takes an index update and constructs a download progress update
// causing to forget any progress for files which we've just been sent.
func makeForgetUpdate(files []protocol.FileInfo) []protocol.FileDownloadProgressUpdate {
updates := make([]protocol.FileDownloadProgressUpdate, 0, len(files))
for _, file := range files {
if file.IsSymlink() || file.IsDirectory() || file.IsDeleted() {
continue
}
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: file.Name,
Version: file.Version,
UpdateType: protocol.FileDownloadProgressUpdateTypeForget,
})
}
return updates
}
-430
View File
@@ -1,430 +0,0 @@
// 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 model
import (
"context"
"fmt"
"sync"
"time"
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/svcutil"
)
type indexSender struct {
conn protocol.Connection
folder string
folderIsReceiveEncrypted bool
fset *db.FileSet
prevSequence int64
evLogger events.Logger
connClosed chan struct{}
done chan struct{}
token suture.ServiceToken
pauseChan chan struct{}
resumeChan chan *db.FileSet
}
func (s *indexSender) Serve(ctx context.Context) (err error) {
l.Debugf("Starting indexSender for %s to %s at %s (slv=%d)", s.folder, s.conn.ID(), s.conn, s.prevSequence)
defer func() {
close(s.done)
err = svcutil.NoRestartErr(err)
l.Debugf("Exiting indexSender for %s to %s at %s: %v", s.folder, s.conn.ID(), s.conn, err)
}()
// We need to send one index, regardless of whether there is something to send or not
err = s.sendIndexTo(ctx)
// Subscribe to LocalIndexUpdated (we have new information to send) and
// DeviceDisconnected (it might be us who disconnected, so we should
// exit).
sub := s.evLogger.Subscribe(events.LocalIndexUpdated | events.DeviceDisconnected)
defer sub.Unsubscribe()
paused := false
evChan := sub.C()
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for err == nil {
select {
case <-ctx.Done():
return ctx.Err()
case <-s.connClosed:
return nil
default:
}
// While we have sent a sequence at least equal to the one
// currently in the database, wait for the local index to update. The
// local index may update for other folders than the one we are
// sending for.
if s.fset.Sequence(protocol.LocalDeviceID) <= s.prevSequence {
select {
case <-ctx.Done():
return ctx.Err()
case <-s.connClosed:
return nil
case <-evChan:
case <-ticker.C:
case <-s.pauseChan:
paused = true
case s.fset = <-s.resumeChan:
paused = false
}
continue
}
if !paused {
err = s.sendIndexTo(ctx)
}
// Wait a short amount of time before entering the next loop. If there
// are continuous changes happening to the local index, this gives us
// time to batch them up a little.
time.Sleep(250 * time.Millisecond)
}
return err
}
func (s *indexSender) resume(fset *db.FileSet) {
select {
case <-s.done:
case s.resumeChan <- fset:
}
}
func (s *indexSender) pause() {
select {
case <-s.done:
case s.pauseChan <- struct{}{}:
}
}
// sendIndexTo sends file infos with a sequence number higher than prevSequence and
// returns the highest sent sequence number.
func (s *indexSender) sendIndexTo(ctx context.Context) error {
initial := s.prevSequence == 0
batch := db.NewFileInfoBatch(nil)
batch.SetFlushFunc(func(fs []protocol.FileInfo) error {
l.Debugf("%v: Sending %d files (<%d bytes)", s, len(fs), batch.Size())
if initial {
initial = false
return s.conn.Index(ctx, s.folder, fs)
}
return s.conn.IndexUpdate(ctx, s.folder, fs)
})
var err error
var f protocol.FileInfo
snap, err := s.fset.Snapshot()
if err != nil {
return svcutil.AsFatalErr(err, svcutil.ExitError)
}
defer snap.Release()
previousWasDelete := false
snap.WithHaveSequence(s.prevSequence+1, func(fi protocol.FileIntf) bool {
// This is to make sure that renames (which is an add followed by a delete) land in the same batch.
// Even if the batch is full, we allow a last delete to slip in, we do this by making sure that
// the batch ends with a non-delete, or that the last item in the batch is already a delete
if batch.Full() && (!fi.IsDeleted() || previousWasDelete) {
if err = batch.Flush(); err != nil {
return false
}
}
if shouldDebug() {
if fi.SequenceNo() < s.prevSequence+1 {
panic(fmt.Sprintln("sequence lower than requested, got:", fi.SequenceNo(), ", asked to start at:", s.prevSequence+1))
}
}
if f.Sequence > 0 && fi.SequenceNo() <= f.Sequence {
l.Warnln("Non-increasing sequence detected: Checking and repairing the db...")
// Abort this round of index sending - the next one will pick
// up from the last successful one with the repeaired db.
defer func() {
if fixed, dbErr := s.fset.RepairSequence(); dbErr != nil {
l.Warnln("Failed repairing sequence entries:", dbErr)
panic("Failed repairing sequence entries")
} else {
s.evLogger.Log(events.Failure, "detected and repaired non-increasing sequence")
l.Infof("Repaired %v sequence entries in database", fixed)
}
}()
return false
}
f = fi.(protocol.FileInfo)
// If this is a folder receiving encrypted files only, we
// mustn't ever send locally changed file infos. Those aren't
// encrypted and thus would be a protocol error at the remote.
if s.folderIsReceiveEncrypted && fi.IsReceiveOnlyChanged() {
return true
}
f = prepareFileInfoForIndex(f)
previousWasDelete = f.IsDeleted()
batch.Append(f)
return true
})
if err != nil {
return err
}
err = batch.Flush()
// True if there was nothing to be sent
if f.Sequence == 0 {
return err
}
s.prevSequence = f.Sequence
return err
}
func prepareFileInfoForIndex(f protocol.FileInfo) protocol.FileInfo {
// Mark the file as invalid if any of the local bad stuff flags are set.
f.RawInvalid = f.IsInvalid()
// If the file is marked LocalReceive (i.e., changed locally on a
// receive only folder) we do not want it to ever become the
// globally best version, invalid or not.
if f.IsReceiveOnlyChanged() {
f.Version = protocol.Vector{}
}
// never sent externally
f.LocalFlags = 0
f.VersionHash = nil
return f
}
func (s *indexSender) String() string {
return fmt.Sprintf("indexSender@%p for %s to %s at %s", s, s.folder, s.conn.ID(), s.conn)
}
type indexSenderRegistry struct {
deviceID protocol.DeviceID
sup *suture.Supervisor
evLogger events.Logger
conn protocol.Connection
closed chan struct{}
indexSenders map[string]*indexSender
startInfos map[string]*indexSenderStartInfo
mut sync.Mutex
}
func newIndexSenderRegistry(conn protocol.Connection, closed chan struct{}, sup *suture.Supervisor, evLogger events.Logger) *indexSenderRegistry {
return &indexSenderRegistry{
deviceID: conn.ID(),
conn: conn,
closed: closed,
sup: sup,
evLogger: evLogger,
indexSenders: make(map[string]*indexSender),
startInfos: make(map[string]*indexSenderStartInfo),
mut: sync.Mutex{},
}
}
// add starts an index sender for given folder.
// If an index sender is already running, it will be stopped first.
func (r *indexSenderRegistry) add(folder config.FolderConfiguration, fset *db.FileSet, startInfo *indexSenderStartInfo) {
r.mut.Lock()
r.addLocked(folder, fset, startInfo)
l.Debugf("Started index sender for device %v and folder %v", r.deviceID.Short(), folder.ID)
r.mut.Unlock()
}
func (r *indexSenderRegistry) addLocked(folder config.FolderConfiguration, fset *db.FileSet, startInfo *indexSenderStartInfo) {
myIndexID := fset.IndexID(protocol.LocalDeviceID)
mySequence := fset.Sequence(protocol.LocalDeviceID)
var startSequence int64
// This is the other side's description of what it knows
// about us. Lets check to see if we can start sending index
// updates directly or need to send the index from start...
if startInfo.local.IndexID == myIndexID {
// They say they've seen our index ID before, so we can
// send a delta update only.
if startInfo.local.MaxSequence > mySequence {
// Safety check. They claim to have more or newer
// index data than we have - either we have lost
// index data, or reset the index without resetting
// the IndexID, or something else weird has
// happened. We send a full index to reset the
// situation.
l.Infof("Device %v folder %s is delta index compatible, but seems out of sync with reality", r.deviceID, folder.Description())
startSequence = 0
} else {
l.Debugf("Device %v folder %s is delta index compatible (mlv=%d)", r.deviceID, folder.Description(), startInfo.local.MaxSequence)
startSequence = startInfo.local.MaxSequence
}
} else if startInfo.local.IndexID != 0 {
// They say they've seen an index ID from us, but it's
// not the right one. Either they are confused or we
// must have reset our database since last talking to
// them. We'll start with a full index transfer.
l.Infof("Device %v folder %s has mismatching index ID for us (%v != %v)", r.deviceID, folder.Description(), startInfo.local.IndexID, myIndexID)
startSequence = 0
} else {
l.Debugf("Device %v folder %s has no index ID for us", r.deviceID, folder.Description())
}
// This is the other side's description of themselves. We
// check to see that it matches the IndexID we have on file,
// otherwise we drop our old index data and expect to get a
// completely new set.
theirIndexID := fset.IndexID(r.deviceID)
if startInfo.remote.IndexID == 0 {
// They're not announcing an index ID. This means they
// do not support delta indexes and we should clear any
// information we have from them before accepting their
// index, which will presumably be a full index.
l.Debugf("Device %v folder %s does not announce an index ID", r.deviceID, folder.Description())
fset.Drop(r.deviceID)
} else if startInfo.remote.IndexID != theirIndexID {
// The index ID we have on file is not what they're
// announcing. They must have reset their database and
// will probably send us a full index. We drop any
// information we have and remember this new index ID
// instead.
l.Infof("Device %v folder %s has a new index ID (%v)", r.deviceID, folder.Description(), startInfo.remote.IndexID)
fset.Drop(r.deviceID)
fset.SetIndexID(r.deviceID, startInfo.remote.IndexID)
}
if is, ok := r.indexSenders[folder.ID]; ok {
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexSenders, folder.ID)
}
delete(r.startInfos, folder.ID)
is := &indexSender{
conn: r.conn,
connClosed: r.closed,
done: make(chan struct{}),
folder: folder.ID,
folderIsReceiveEncrypted: folder.Type == config.FolderTypeReceiveEncrypted,
fset: fset,
prevSequence: startSequence,
evLogger: r.evLogger,
pauseChan: make(chan struct{}),
resumeChan: make(chan *db.FileSet),
}
is.token = r.sup.Add(is)
r.indexSenders[folder.ID] = is
}
// addPending stores the given info to start an index sender once resume is called
// for this folder.
// If an index sender is already running, it will be stopped.
func (r *indexSenderRegistry) addPending(folder string, startInfo *indexSenderStartInfo) {
r.mut.Lock()
defer r.mut.Unlock()
if is, ok := r.indexSenders[folder]; ok {
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexSenders, folder)
l.Debugf("Removed index sender for device %v and folder %v due to added pending", r.deviceID.Short(), folder)
}
r.startInfos[folder] = startInfo
l.Debugf("Pending index sender for device %v and folder %v", r.deviceID.Short(), folder)
}
// remove stops a running index sender or removes one pending to be started.
// It is a noop if the folder isn't known.
func (r *indexSenderRegistry) remove(folder string) {
r.mut.Lock()
defer r.mut.Unlock()
if is, ok := r.indexSenders[folder]; ok {
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexSenders, folder)
}
delete(r.startInfos, folder)
l.Debugf("Removed index sender for device %v and folder %v", r.deviceID.Short(), folder)
}
// removeAllExcept stops all running index senders and removes those pending to be started,
// except mentioned ones.
// It is a noop if the folder isn't known.
func (r *indexSenderRegistry) removeAllExcept(except map[string]struct{}) {
r.mut.Lock()
defer r.mut.Unlock()
for folder, is := range r.indexSenders {
if _, ok := except[folder]; !ok {
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexSenders, folder)
l.Debugf("Removed index sender for device %v and folder %v (removeAllExcept)", r.deviceID.Short(), folder)
}
}
for folder := range r.startInfos {
if _, ok := except[folder]; !ok {
delete(r.startInfos, folder)
l.Debugf("Removed pending index sender for device %v and folder %v (removeAllExcept)", r.deviceID.Short(), folder)
}
}
}
// pause stops a running index sender.
// It is a noop if the folder isn't known or has not been started yet.
func (r *indexSenderRegistry) pause(folder string) {
r.mut.Lock()
defer r.mut.Unlock()
if is, ok := r.indexSenders[folder]; ok {
is.pause()
l.Debugf("Paused index sender for device %v and folder %v", r.deviceID.Short(), folder)
} else {
l.Debugf("No index sender for device %v and folder %v to pause", r.deviceID.Short(), folder)
}
}
// resume unpauses an already running index sender or starts it, if it was added
// while paused.
// It is a noop if the folder isn't known.
func (r *indexSenderRegistry) resume(folder config.FolderConfiguration, fset *db.FileSet) {
r.mut.Lock()
defer r.mut.Unlock()
is, isOk := r.indexSenders[folder.ID]
if info, ok := r.startInfos[folder.ID]; ok {
if isOk {
r.sup.RemoveAndWait(is.token, 0)
delete(r.indexSenders, folder.ID)
l.Debugf("Removed index sender for device %v and folder %v in resume", r.deviceID.Short(), folder.ID)
}
r.addLocked(folder, fset, info)
delete(r.startInfos, folder.ID)
l.Debugf("Started index sender for device %v and folder %v in resume", r.deviceID.Short(), folder.ID)
} else if isOk {
is.resume(fset)
l.Debugf("Resume index sender for device %v and folder %v", r.deviceID.Short(), folder.ID)
} else {
l.Debugf("Not resuming index sender for device %v and folder %v as none is paused and there is no start info", r.deviceID.Short(), folder.ID)
}
}
type indexSenderStartInfo struct {
local, remote protocol.Device
}
+150
View File
@@ -177,6 +177,29 @@ type Model struct {
result1 map[protocol.DeviceID]stats.DeviceStatistics
result2 error
}
DismissPendingDeviceStub func(protocol.DeviceID) error
dismissPendingDeviceMutex sync.RWMutex
dismissPendingDeviceArgsForCall []struct {
arg1 protocol.DeviceID
}
dismissPendingDeviceReturns struct {
result1 error
}
dismissPendingDeviceReturnsOnCall map[int]struct {
result1 error
}
DismissPendingFolderStub func(protocol.DeviceID, string) error
dismissPendingFolderMutex sync.RWMutex
dismissPendingFolderArgsForCall []struct {
arg1 protocol.DeviceID
arg2 string
}
dismissPendingFolderReturns struct {
result1 error
}
dismissPendingFolderReturnsOnCall map[int]struct {
result1 error
}
DownloadProgressStub func(protocol.DeviceID, string, []protocol.FileDownloadProgressUpdate) error
downloadProgressMutex sync.RWMutex
downloadProgressArgsForCall []struct {
@@ -1332,6 +1355,129 @@ func (fake *Model) DeviceStatisticsReturnsOnCall(i int, result1 map[protocol.Dev
}{result1, result2}
}
func (fake *Model) DismissPendingDevice(arg1 protocol.DeviceID) error {
fake.dismissPendingDeviceMutex.Lock()
ret, specificReturn := fake.dismissPendingDeviceReturnsOnCall[len(fake.dismissPendingDeviceArgsForCall)]
fake.dismissPendingDeviceArgsForCall = append(fake.dismissPendingDeviceArgsForCall, struct {
arg1 protocol.DeviceID
}{arg1})
stub := fake.DismissPendingDeviceStub
fakeReturns := fake.dismissPendingDeviceReturns
fake.recordInvocation("DismissPendingDevice", []interface{}{arg1})
fake.dismissPendingDeviceMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *Model) DismissPendingDeviceCallCount() int {
fake.dismissPendingDeviceMutex.RLock()
defer fake.dismissPendingDeviceMutex.RUnlock()
return len(fake.dismissPendingDeviceArgsForCall)
}
func (fake *Model) DismissPendingDeviceCalls(stub func(protocol.DeviceID) error) {
fake.dismissPendingDeviceMutex.Lock()
defer fake.dismissPendingDeviceMutex.Unlock()
fake.DismissPendingDeviceStub = stub
}
func (fake *Model) DismissPendingDeviceArgsForCall(i int) protocol.DeviceID {
fake.dismissPendingDeviceMutex.RLock()
defer fake.dismissPendingDeviceMutex.RUnlock()
argsForCall := fake.dismissPendingDeviceArgsForCall[i]
return argsForCall.arg1
}
func (fake *Model) DismissPendingDeviceReturns(result1 error) {
fake.dismissPendingDeviceMutex.Lock()
defer fake.dismissPendingDeviceMutex.Unlock()
fake.DismissPendingDeviceStub = nil
fake.dismissPendingDeviceReturns = struct {
result1 error
}{result1}
}
func (fake *Model) DismissPendingDeviceReturnsOnCall(i int, result1 error) {
fake.dismissPendingDeviceMutex.Lock()
defer fake.dismissPendingDeviceMutex.Unlock()
fake.DismissPendingDeviceStub = nil
if fake.dismissPendingDeviceReturnsOnCall == nil {
fake.dismissPendingDeviceReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.dismissPendingDeviceReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *Model) DismissPendingFolder(arg1 protocol.DeviceID, arg2 string) error {
fake.dismissPendingFolderMutex.Lock()
ret, specificReturn := fake.dismissPendingFolderReturnsOnCall[len(fake.dismissPendingFolderArgsForCall)]
fake.dismissPendingFolderArgsForCall = append(fake.dismissPendingFolderArgsForCall, struct {
arg1 protocol.DeviceID
arg2 string
}{arg1, arg2})
stub := fake.DismissPendingFolderStub
fakeReturns := fake.dismissPendingFolderReturns
fake.recordInvocation("DismissPendingFolder", []interface{}{arg1, arg2})
fake.dismissPendingFolderMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *Model) DismissPendingFolderCallCount() int {
fake.dismissPendingFolderMutex.RLock()
defer fake.dismissPendingFolderMutex.RUnlock()
return len(fake.dismissPendingFolderArgsForCall)
}
func (fake *Model) DismissPendingFolderCalls(stub func(protocol.DeviceID, string) error) {
fake.dismissPendingFolderMutex.Lock()
defer fake.dismissPendingFolderMutex.Unlock()
fake.DismissPendingFolderStub = stub
}
func (fake *Model) DismissPendingFolderArgsForCall(i int) (protocol.DeviceID, string) {
fake.dismissPendingFolderMutex.RLock()
defer fake.dismissPendingFolderMutex.RUnlock()
argsForCall := fake.dismissPendingFolderArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *Model) DismissPendingFolderReturns(result1 error) {
fake.dismissPendingFolderMutex.Lock()
defer fake.dismissPendingFolderMutex.Unlock()
fake.DismissPendingFolderStub = nil
fake.dismissPendingFolderReturns = struct {
result1 error
}{result1}
}
func (fake *Model) DismissPendingFolderReturnsOnCall(i int, result1 error) {
fake.dismissPendingFolderMutex.Lock()
defer fake.dismissPendingFolderMutex.Unlock()
fake.DismissPendingFolderStub = nil
if fake.dismissPendingFolderReturnsOnCall == nil {
fake.dismissPendingFolderReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.dismissPendingFolderReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *Model) DownloadProgress(arg1 protocol.DeviceID, arg2 string, arg3 []protocol.FileDownloadProgressUpdate) error {
var arg3Copy []protocol.FileDownloadProgressUpdate
if arg3 != nil {
@@ -3254,6 +3400,10 @@ func (fake *Model) Invocations() map[string][][]interface{} {
defer fake.delayScanMutex.RUnlock()
fake.deviceStatisticsMutex.RLock()
defer fake.deviceStatisticsMutex.RUnlock()
fake.dismissPendingDeviceMutex.RLock()
defer fake.dismissPendingDeviceMutex.RUnlock()
fake.dismissPendingFolderMutex.RLock()
defer fake.dismissPendingFolderMutex.RUnlock()
fake.downloadProgressMutex.RLock()
defer fake.downloadProgressMutex.RUnlock()
fake.folderErrorsMutex.RLock()
+175 -164
View File
@@ -107,6 +107,8 @@ type Model interface {
PendingDevices() (map[protocol.DeviceID]db.ObservedDevice, error)
PendingFolders(device protocol.DeviceID) (map[string]db.PendingFolder, error)
DismissPendingDevice(device protocol.DeviceID) error
DismissPendingFolder(device protocol.DeviceID, folder string) error
StartDeadlockDetector(timeout time.Duration)
GlobalDirectoryTree(folder, prefix string, levels int, dirsOnly bool) ([]*TreeEntry, error)
@@ -158,7 +160,7 @@ type model struct {
helloMessages map[protocol.DeviceID]protocol.Hello
deviceDownloads map[protocol.DeviceID]*deviceDownloadState
remotePausedFolders map[protocol.DeviceID]map[string]struct{} // deviceID -> folders
indexSenders map[protocol.DeviceID]*indexSenderRegistry
indexHandlers map[protocol.DeviceID]*indexHandlerRegistry
// for testing only
foldersRunning int32
@@ -192,12 +194,9 @@ var (
errEncryptionPassword = errors.New("different encryption passwords used")
errEncryptionTokenRead = errors.New("failed to read encryption token")
errEncryptionTokenWrite = errors.New("failed to write encryption token")
errEncryptionNeedToken = errors.New("require password token for receive-encrypted token")
errMissingRemoteInClusterConfig = errors.New("remote device missing in cluster config")
errMissingLocalInClusterConfig = errors.New("local device missing in cluster config")
errConnLimitReached = errors.New("connection limit reached")
// messages for failure reports
failureUnexpectedGenerateCCError = "unexpected error occurred in generateClusterConfig"
)
// NewModel creates and starts a new model. The model starts in read-only mode,
@@ -246,7 +245,7 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
helloMessages: make(map[protocol.DeviceID]protocol.Hello),
deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState),
remotePausedFolders: make(map[protocol.DeviceID]map[string]struct{}),
indexSenders: make(map[protocol.DeviceID]*indexSenderRegistry),
indexHandlers: make(map[protocol.DeviceID]*indexHandlerRegistry),
}
for devID := range cfg.Devices() {
m.deviceStatRefs[devID] = stats.NewDeviceStatisticsReference(m.db, devID)
@@ -485,8 +484,8 @@ func (m *model) removeFolder(cfg config.FolderConfiguration) {
}
m.cleanupFolderLocked(cfg)
for _, r := range m.indexSenders {
r.remove(cfg.ID)
for _, r := range m.indexHandlers {
r.Remove(cfg.ID)
}
m.fmut.Unlock()
@@ -558,21 +557,9 @@ func (m *model) restartFolder(from, to config.FolderConfiguration, cacheIgnoredF
// Care needs to be taken because we already hold fmut and the lock order
// must be the same everywhere. As fmut is acquired first, this is fine.
// toDeviceIDs := to.DeviceIDs()
m.pmut.RLock()
for _, id := range to.DeviceIDs() {
indexSenders, ok := m.indexSenders[id]
if !ok {
continue
}
// In case the folder was newly shared with us we already got a
// cluster config and wont necessarily get another soon - start
// sending indexes if connected.
if to.Paused {
indexSenders.pause(to.ID)
} else if !from.SharedWith(indexSenders.deviceID) || fsetNil || from.Paused {
indexSenders.resume(to, fset)
}
for _, indexRegistry := range m.indexHandlers {
indexRegistry.RegisterFolderState(to, fset, m.folderRunners[to.ID])
}
m.pmut.RUnlock()
@@ -601,18 +588,19 @@ func (m *model) newFolder(cfg config.FolderConfiguration, cacheIgnoredFiles bool
m.fmut.Lock()
defer m.fmut.Unlock()
m.addAndStartFolderLocked(cfg, fset, cacheIgnoredFiles)
// Cluster configs might be received and processed before reaching this
// point, i.e. before the folder is started. If that's the case, start
// index senders here.
// Care needs to be taken because we already hold fmut and the lock order
// must be the same everywhere. As fmut is acquired first, this is fine.
m.pmut.RLock()
for _, id := range cfg.DeviceIDs() {
if is, ok := m.indexSenders[id]; ok {
is.resume(cfg, fset)
}
for _, indexRegistry := range m.indexHandlers {
indexRegistry.RegisterFolderState(cfg, fset, m.folderRunners[cfg.ID])
}
m.pmut.RUnlock()
m.addAndStartFolderLocked(cfg, fset, cacheIgnoredFiles)
return nil
}
@@ -1151,46 +1139,23 @@ func (m *model) handleIndex(deviceID protocol.DeviceID, folder string, fs []prot
return errors.Wrap(ErrFolderPaused, folder)
}
m.fmut.RLock()
files, existing := m.folderFiles[folder]
runner, running := m.folderRunners[folder]
m.fmut.RUnlock()
if !existing {
l.Infof("%v for nonexistent folder %q", op, folder)
return errors.Wrap(ErrFolderMissing, folder)
}
if running {
defer runner.SchedulePull()
}
m.pmut.RLock()
downloads := m.deviceDownloads[deviceID]
indexHandler, ok := m.indexHandlers[deviceID]
m.pmut.RUnlock()
downloads.Update(folder, makeForgetUpdate(fs))
if !update {
files.Drop(deviceID)
if !ok {
// This should be impossible, as an index handler always exists for an
// open connection, and this method can't be called on a closed
// connection
m.evLogger.Log(events.Failure, "index sender does not exist for connection on which indexes were received")
l.Debugf("%v for folder (ID %q) sent from device %q: missing index handler", op, folder, deviceID)
return errors.Wrap(errors.New("index handler missing"), folder)
}
for i := range fs {
// The local attributes should never be transmitted over the wire.
// Make sure they look like they weren't.
fs[i].LocalFlags = 0
fs[i].VersionHash = nil
}
files.Update(deviceID, fs)
seq := files.Sequence(deviceID)
m.evLogger.Log(events.RemoteIndexUpdated, map[string]interface{}{
"device": deviceID.String(),
"folder": folder,
"items": len(fs),
"sequence": seq,
"version": seq, // legacy for sequence
})
return indexHandler.ReceiveIndex(folder, fs, update, op)
}
return nil
type clusterConfigDeviceInfo struct {
local, remote protocol.Device
}
func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterConfig) error {
@@ -1199,8 +1164,10 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
// Also, collect a list of folders we do share, and if he's interested in
// temporary indexes, subscribe the connection.
l.Debugf("Handling ClusterConfig from %v", deviceID.Short())
m.pmut.RLock()
indexSenderRegistry, ok := m.indexSenders[deviceID]
indexHandlerRegistry, ok := m.indexHandlers[deviceID]
m.pmut.RUnlock()
if !ok {
panic("bug: ClusterConfig called on closed or nonexistent connection")
@@ -1214,9 +1181,9 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
// Assemble the device information from the connected device about
// themselves and us for all folders.
ccDeviceInfos := make(map[string]*indexSenderStartInfo, len(cm.Folders))
ccDeviceInfos := make(map[string]*clusterConfigDeviceInfo, len(cm.Folders))
for _, folder := range cm.Folders {
info := &indexSenderStartInfo{}
info := &clusterConfigDeviceInfo{}
for _, dev := range folder.Devices {
if dev.ID == m.id {
info.local = dev
@@ -1269,7 +1236,7 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
w.Wait()
}
tempIndexFolders, paused, err := m.ccHandleFolders(cm.Folders, deviceCfg, ccDeviceInfos, indexSenderRegistry)
tempIndexFolders, paused, err := m.ccHandleFolders(cm.Folders, deviceCfg, ccDeviceInfos, indexHandlerRegistry)
if err != nil {
return err
}
@@ -1310,7 +1277,7 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
return nil
}
func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.DeviceConfiguration, ccDeviceInfos map[string]*indexSenderStartInfo, indexSenders *indexSenderRegistry) ([]string, map[string]struct{}, error) {
func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.DeviceConfiguration, ccDeviceInfos map[string]*clusterConfigDeviceInfo, indexHandlers *indexHandlerRegistry) ([]string, map[string]struct{}, error) {
var folderDevice config.FolderDeviceConfiguration
tempIndexFolders := make([]string, 0, len(folders))
paused := make(map[string]struct{}, len(folders))
@@ -1330,7 +1297,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
folderDevice, ok = cfg.Device(deviceID)
}
if !ok {
indexSenders.remove(folder.ID)
indexHandlers.Remove(folder.ID)
if deviceCfg.IgnoredFolder(folder.ID) {
l.Infof("Ignoring folder %s from device %s since we are configured to", folder.Description(), deviceID)
continue
@@ -1341,7 +1308,9 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
if err := m.db.AddOrUpdatePendingFolder(folder.ID, of, deviceID); err != nil {
l.Warnf("Failed to persist pending folder entry to database: %v", err)
}
indexSenders.addPending(folder.ID, ccDeviceInfos[folder.ID])
if !folder.Paused {
indexHandlers.AddIndexInfo(folder.ID, ccDeviceInfos[folder.ID])
}
updatedPending = append(updatedPending, updatedPendingFolder{
FolderID: folder.ID,
FolderLabel: folder.Label,
@@ -1359,13 +1328,13 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
}
if folder.Paused {
indexSenders.remove(folder.ID)
indexHandlers.Remove(folder.ID)
paused[cfg.ID] = struct{}{}
continue
}
if cfg.Paused {
indexSenders.addPending(folder.ID, ccDeviceInfos[folder.ID])
indexHandlers.AddIndexInfo(folder.ID, ccDeviceInfos[folder.ID])
continue
}
@@ -1403,40 +1372,22 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
tempIndexFolders = append(tempIndexFolders, folder.ID)
}
m.fmut.RLock()
fs, ok := m.folderFiles[folder.ID]
m.fmut.RUnlock()
if !ok {
// Shouldn't happen because !cfg.Paused, but might happen
// if the folder is about to be unpaused, but not yet.
l.Debugln("ccH: no fset", folder.ID)
indexSenders.addPending(folder.ID, ccDeviceInfos[folder.ID])
indexHandlers.AddIndexInfo(folder.ID, ccDeviceInfos[folder.ID])
}
indexHandlers.RemoveAllExcept(seenFolders)
expiredPendingList := make([]map[string]string, 0, len(expiredPending))
for folder := range expiredPending {
if err = m.db.RemovePendingFolderForDevice(folder, deviceID); err != nil {
// Nothing we can fix; logged from DB already
continue
}
indexSenders.add(cfg, fs, ccDeviceInfos[folder.ID])
// We might already have files that we need to pull so let the
// folder runner know that it should recheck the index data.
m.fmut.RLock()
if runner := m.folderRunners[folder.ID]; runner != nil {
defer runner.SchedulePull()
}
m.fmut.RUnlock()
expiredPendingList = append(expiredPendingList, map[string]string{
"folderID": folder,
"deviceID": deviceID.String(),
})
}
indexSenders.removeAllExcept(seenFolders)
for folder := range expiredPending {
m.db.RemovePendingFolderForDevice(folder, deviceID)
}
if len(updatedPending) > 0 || len(expiredPending) > 0 {
expiredPendingList := make([]map[string]string, 0, len(expiredPending))
for folderID := range expiredPending {
expiredPendingList = append(expiredPendingList, map[string]string{
"folderID": folderID,
"deviceID": deviceID.String(),
})
}
if len(updatedPending) > 0 || len(expiredPendingList) > 0 {
m.evLogger.Log(events.PendingFoldersChanged, map[string]interface{}{
"added": updatedPending,
"removed": expiredPendingList,
@@ -1446,7 +1397,7 @@ func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.Devi
return tempIndexFolders, paused, nil
}
func (m *model) ccCheckEncryption(fcfg config.FolderConfiguration, folderDevice config.FolderDeviceConfiguration, ccDeviceInfos *indexSenderStartInfo, deviceUntrusted bool) error {
func (m *model) ccCheckEncryption(fcfg config.FolderConfiguration, folderDevice config.FolderDeviceConfiguration, ccDeviceInfos *clusterConfigDeviceInfo, deviceUntrusted bool) error {
hasTokenRemote := len(ccDeviceInfos.remote.EncryptionPasswordToken) > 0
hasTokenLocal := len(ccDeviceInfos.local.EncryptionPasswordToken) > 0
isEncryptedRemote := folderDevice.EncryptionPassword != ""
@@ -1472,9 +1423,9 @@ func (m *model) ccCheckEncryption(fcfg config.FolderConfiguration, folderDevice
if !(hasTokenRemote || hasTokenLocal) {
if isEncryptedRemote {
return errEncryptionPlainForReceiveEncrypted
} else {
return errEncryptionPlainForRemoteEncrypted
} else {
return errEncryptionPlainForReceiveEncrypted
}
}
@@ -1565,15 +1516,7 @@ func (m *model) sendClusterConfig(ids []protocol.DeviceID) {
m.pmut.RUnlock()
// Generating cluster-configs acquires fmut -> must happen outside of pmut.
for _, conn := range ccConns {
cm, passwords, err := m.generateClusterConfig(conn.ID())
if err != nil {
if err != errEncryptionNeedToken {
m.evLogger.Log(events.Failure, failureUnexpectedGenerateCCError)
continue
}
go conn.Close(err)
continue
}
cm, passwords := m.generateClusterConfig(conn.ID())
conn.SetFolderPasswords(passwords)
go conn.ClusterConfig(cm)
}
@@ -1616,6 +1559,11 @@ func (m *model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm
continue
}
if fcfg.Type != config.FolderTypeReceiveEncrypted && device.EncryptionPasswordToken != nil {
l.Infof("Cannot share folder %s with %v because the introducer %v encrypts data, which requires a password", folder.Description(), device.ID, introducerCfg.DeviceID)
continue
}
// We don't yet share this folder with this device. Add the device
// to sharing list of the folder.
l.Infof("Sharing folder %s with %v (vouched for by introducer %v)", folder.Description(), device.ID, introducerCfg.DeviceID)
@@ -1688,7 +1636,7 @@ func (m *model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
// handleAutoAccepts handles adding and sharing folders for devices that have
// AutoAcceptFolders set to true.
func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Folder, ccDeviceInfos *indexSenderStartInfo, cfg config.FolderConfiguration, haveCfg bool, defaultPath string) (config.FolderConfiguration, bool) {
func (m *model) handleAutoAccepts(deviceID protocol.DeviceID, folder protocol.Folder, ccDeviceInfos *clusterConfigDeviceInfo, cfg config.FolderConfiguration, haveCfg bool, defaultPath string) (config.FolderConfiguration, bool) {
if !haveCfg {
defaultPathFs := fs.NewFilesystem(fs.FilesystemTypeBasic, defaultPath)
pathAlternatives := []string{
@@ -1783,7 +1731,7 @@ func (m *model) Closed(device protocol.DeviceID, err error) {
delete(m.remotePausedFolders, device)
closed := m.closed[device]
delete(m.closed, device)
delete(m.indexSenders, device)
delete(m.indexHandlers, device)
m.pmut.Unlock()
m.progressEmitter.temporaryIndexUnsubscribe(conn)
@@ -2244,6 +2192,9 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
return
}
// The slightly unusual locking sequence here is because we must acquire
// fmut before pmut. (The locks can be *released* in any order.)
m.fmut.RLock()
m.pmut.Lock()
if oldConn, ok := m.conn[deviceID]; ok {
l.Infoln("Replacing old connection", oldConn, "with", conn, "for", deviceID)
@@ -2253,9 +2204,12 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
// actual close without holding pmut as the connection will call
// back into Closed() for the cleanup.
closed := m.closed[deviceID]
m.fmut.RUnlock()
m.pmut.Unlock()
oldConn.Close(errReplacingConnection)
<-closed
// Again, lock fmut before pmut.
m.fmut.RLock()
m.pmut.Lock()
}
@@ -2263,7 +2217,12 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
closed := make(chan struct{})
m.closed[deviceID] = closed
m.deviceDownloads[deviceID] = newDeviceDownloadState()
m.indexSenders[deviceID] = newIndexSenderRegistry(conn, closed, m.Supervisor, m.evLogger)
indexRegistry := newIndexHandlerRegistry(conn, m.deviceDownloads[deviceID], closed, m.Supervisor, m.evLogger)
for id, fcfg := range m.folderCfgs {
indexRegistry.RegisterFolderState(fcfg, m.folderFiles[id], m.folderRunners[id])
}
m.indexHandlers[deviceID] = indexRegistry
m.fmut.RUnlock()
// 0: default, <0: no limiting
switch {
case device.MaxRequestKiB > 0:
@@ -2295,12 +2254,7 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
m.pmut.Unlock()
// Acquires fmut, so has to be done outside of pmut.
cm, passwords, err := m.generateClusterConfig(deviceID)
// We ignore errEncryptionNeedToken on a new connection, as the missing
// token should be delivered in the cluster-config about to be received.
if err != nil && err != errEncryptionNeedToken {
m.evLogger.Log(events.Failure, failureUnexpectedGenerateCCError)
}
cm, passwords := m.generateClusterConfig(deviceID)
conn.SetFolderPasswords(passwords)
conn.ClusterConfig(cm)
@@ -2463,7 +2417,7 @@ func (m *model) numHashers(folder string) int {
// generateClusterConfig returns a ClusterConfigMessage that is correct and the
// set of folder passwords for the given peer device
func (m *model) generateClusterConfig(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string, error) {
func (m *model) generateClusterConfig(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string) {
var message protocol.ClusterConfig
m.fmut.RLock()
@@ -2476,15 +2430,11 @@ func (m *model) generateClusterConfig(device protocol.DeviceID) (protocol.Cluste
continue
}
var encryptionToken []byte
var hasEncryptionToken bool
if folderCfg.Type == config.FolderTypeReceiveEncrypted {
if encryptionToken, hasEncryptionToken = m.folderEncryptionPasswordTokens[folderCfg.ID]; !hasEncryptionToken {
// We haven't gotten a token yet and without one the other side
// can't validate us - reset the connection to trigger a new
// cluster-config and get the token.
return message, nil, errEncryptionNeedToken
}
encryptionToken, hasEncryptionToken := m.folderEncryptionPasswordTokens[folderCfg.ID]
if folderCfg.Type == config.FolderTypeReceiveEncrypted && !hasEncryptionToken {
// We haven't gotten a token for us yet and without one the other
// side can't validate us - pretend we don't have the folder yet.
continue
}
protocolFolder := protocol.Folder{
@@ -2541,7 +2491,7 @@ func (m *model) generateClusterConfig(device protocol.DeviceID) (protocol.Cluste
message.Folders = append(message.Folders, protocolFolder)
}
return message, passwords, nil
return message, passwords
}
func (m *model) State(folder string) (string, time.Time, error) {
@@ -2833,6 +2783,7 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
// Tracks devices affected by any configuration change to resend ClusterConfig.
clusterConfigDevices := make(deviceIDSet, len(from.Devices)+len(to.Devices))
closeDevices := make([]protocol.DeviceID, 0, len(to.Devices))
fromFolders := mapFolders(from.Folders)
toFolders := mapFolders(to.Folders)
@@ -2875,7 +2826,23 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
return true
}
clusterConfigDevices.add(fromCfg.DeviceIDs())
clusterConfigDevices.add(toCfg.DeviceIDs())
if toCfg.Type != config.FolderTypeReceiveEncrypted {
clusterConfigDevices.add(toCfg.DeviceIDs())
} else {
// If we don't have the encryption token yet, we need to drop
// the connection to make the remote re-send the cluster-config
// and with it the token.
m.fmut.RLock()
_, ok := m.folderEncryptionPasswordTokens[toCfg.ID]
m.fmut.RUnlock()
if !ok {
for _, id := range toCfg.DeviceIDs() {
closeDevices = append(closeDevices, id)
}
} else {
clusterConfigDevices.add(toCfg.DeviceIDs())
}
}
}
// Emit the folder pause/resume event
@@ -2898,7 +2865,6 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
// Pausing a device, unpausing is handled by the connection service.
fromDevices := from.DeviceMap()
toDevices := to.DeviceMap()
closeDevices := make([]protocol.DeviceID, 0, len(to.Devices))
for deviceID, toCfg := range toDevices {
fromCfg, ok := fromDevices[deviceID]
if !ok {
@@ -2913,17 +2879,17 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
continue
}
// Ignored folder was removed, reconnect to retrigger the prompt.
if !toCfg.Paused && len(fromCfg.IgnoredFolders) > len(toCfg.IgnoredFolders) {
closeDevices = append(closeDevices, deviceID)
}
if toCfg.Paused {
l.Infoln("Pausing", deviceID)
closeDevices = append(closeDevices, deviceID)
delete(clusterConfigDevices, deviceID)
m.evLogger.Log(events.DevicePaused, map[string]string{"device": deviceID.String()})
} else {
// Ignored folder was removed, reconnect to retrigger the prompt.
if len(fromCfg.IgnoredFolders) > len(toCfg.IgnoredFolders) {
closeDevices = append(closeDevices, deviceID)
}
l.Infoln("Resuming", deviceID)
m.evLogger.Log(events.DeviceResumed, map[string]string{"device": deviceID.String()})
}
}
@@ -2939,11 +2905,13 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
m.pmut.RLock()
for _, id := range closeDevices {
delete(clusterConfigDevices, id)
if conn, ok := m.conn[id]; ok {
go conn.Close(errDevicePaused)
}
}
for _, id := range removedDevices {
delete(clusterConfigDevices, id)
if conn, ok := m.conn[id]; ok {
go conn.Close(errDeviceRemoved)
}
@@ -2982,10 +2950,13 @@ func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.Device
// folders as well, assuming the folder is no longer of interest
// at all (but might become pending again).
l.Debugf("Discarding pending removed folder %v from all devices", folderID)
m.db.RemovePendingFolder(folderID)
removedPendingFolders = append(removedPendingFolders, map[string]string{
"folderID": folderID,
})
if err := m.db.RemovePendingFolder(folderID); err != nil {
// Nothing we can fix; logged from DB already
} else {
removedPendingFolders = append(removedPendingFolders, map[string]string{
"folderID": folderID,
})
}
continue
}
for deviceID := range pf.OfferedBy {
@@ -3004,7 +2975,10 @@ func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.Device
}
continue
removeFolderForDevice:
m.db.RemovePendingFolderForDevice(folderID, deviceID)
if err := m.db.RemovePendingFolderForDevice(folderID, deviceID); err != nil {
// Nothing we can fix; logged from DB already
continue
}
removedPendingFolders = append(removedPendingFolders, map[string]string{
"folderID": folderID,
"deviceID": deviceID.String(),
@@ -3034,7 +3008,10 @@ func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.Device
}
continue
removeDevice:
m.db.RemovePendingDevice(deviceID)
if err := m.db.RemovePendingDevice(deviceID); err != nil {
// Nothing we can fix; logged from DB already
continue
}
removedPendingDevices = append(removedPendingDevices, map[string]string{
"deviceID": deviceID.String(),
})
@@ -3076,6 +3053,57 @@ func (m *model) PendingFolders(device protocol.DeviceID) (map[string]db.PendingF
return m.db.PendingFoldersForDevice(device)
}
// DismissPendingDevices removes the record of a specific pending device.
func (m *model) DismissPendingDevice(device protocol.DeviceID) error {
l.Debugf("Discarding pending device %v", device)
err := m.db.RemovePendingDevice(device)
if err != nil {
return err
}
removedPendingDevices := []map[string]string{
{"deviceID": device.String()},
}
m.evLogger.Log(events.PendingDevicesChanged, map[string]interface{}{
"removed": removedPendingDevices,
})
return nil
}
// DismissPendingFolders removes records of pending folders. Either a specific folder /
// device combination, or all matching a specific folder ID if the device argument is
// specified as EmptyDeviceID.
func (m *model) DismissPendingFolder(device protocol.DeviceID, folder string) error {
var removedPendingFolders []map[string]string
if device == protocol.EmptyDeviceID {
l.Debugf("Discarding pending removed folder %s from all devices", folder)
err := m.db.RemovePendingFolder(folder)
if err != nil {
return err
}
removedPendingFolders = []map[string]string{
{"folderID": folder},
}
} else {
l.Debugf("Discarding pending folder %s from device %v", folder, device)
err := m.db.RemovePendingFolderForDevice(folder, device)
if err != nil {
return err
}
removedPendingFolders = []map[string]string{
{
"folderID": folder,
"deviceID": device.String(),
},
}
}
if len(removedPendingFolders) > 0 {
m.evLogger.Log(events.PendingFoldersChanged, map[string]interface{}{
"removed": removedPendingFolders,
})
}
return nil
}
// mapFolders returns a map of folder ID to folder configuration for the given
// slice of folder configurations.
func mapFolders(folders []config.FolderConfiguration) map[string]config.FolderConfiguration {
@@ -3119,23 +3147,6 @@ func readOffsetIntoBuf(fs fs.Filesystem, file string, offset int64, buf []byte)
return n, err
}
// makeForgetUpdate takes an index update and constructs a download progress update
// causing to forget any progress for files which we've just been sent.
func makeForgetUpdate(files []protocol.FileInfo) []protocol.FileDownloadProgressUpdate {
updates := make([]protocol.FileDownloadProgressUpdate, 0, len(files))
for _, file := range files {
if file.IsSymlink() || file.IsDirectory() || file.IsDeleted() {
continue
}
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: file.Name,
Version: file.Version,
UpdateType: protocol.FileDownloadProgressUpdateTypeForget,
})
}
return updates
}
// folderDeviceSet is a set of (folder, deviceID) pairs
type folderDeviceSet map[string]map[protocol.DeviceID]struct{}
+60 -47
View File
@@ -220,15 +220,16 @@ func BenchmarkIndex_100(b *testing.B) {
}
func benchmarkIndex(b *testing.B, nfiles int) {
m := setupModel(b, defaultCfgWrapper)
defer cleanupModel(m)
m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
files := genFiles(nfiles)
must(b, m.Index(device1, "default", files))
must(b, m.Index(device1, fcfg.ID, files))
b.ResetTimer()
for i := 0; i < b.N; i++ {
must(b, m.Index(device1, "default", files))
must(b, m.Index(device1, fcfg.ID, files))
}
b.ReportAllocs()
}
@@ -246,17 +247,18 @@ func BenchmarkIndexUpdate_10000_1(b *testing.B) {
}
func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
m := setupModel(b, defaultCfgWrapper)
defer cleanupModel(m)
m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
files := genFiles(nfiles)
ufiles := genFiles(nufiles)
must(b, m.Index(device1, "default", files))
must(b, m.Index(device1, fcfg.ID, files))
b.ResetTimer()
for i := 0; i < b.N; i++ {
must(b, m.IndexUpdate(device1, "default", ufiles))
must(b, m.IndexUpdate(device1, fcfg.ID, ufiles))
}
b.ReportAllocs()
}
@@ -428,8 +430,7 @@ func TestClusterConfig(t *testing.T) {
m.ServeBackground()
defer cleanupModel(m)
cm, _, err := m.generateClusterConfig(device2)
must(t, err)
cm, _ := m.generateClusterConfig(device2)
if l := len(cm.Folders); l != 2 {
t.Fatalf("Incorrect number of folders %d != 2", l)
@@ -516,12 +517,18 @@ func TestIntroducer(t *testing.T) {
},
},
})
cc := basicClusterConfig(myID, device1, "folder1")
cc := basicClusterConfig(myID, device1, "folder1", "folder2")
cc.Folders[0].Devices = append(cc.Folders[0].Devices, protocol.Device{
ID: device2,
Introducer: true,
SkipIntroductionRemovals: true,
})
cc.Folders[1].Devices = append(cc.Folders[1].Devices, protocol.Device{
ID: device2,
Introducer: true,
SkipIntroductionRemovals: true,
EncryptionPasswordToken: []byte("faketoken"),
})
m.ClusterConfig(device1, cc)
if newDev, ok := m.cfg.Device(device2); !ok || !newDev.Introducer || !newDev.SkipIntroductionRemovals {
@@ -532,6 +539,12 @@ func TestIntroducer(t *testing.T) {
t.Error("expected folder 1 to have device2 introduced by device 1")
}
for _, devCfg := range m.cfg.Folders()["folder2"].Devices {
if devCfg.DeviceID == device2 {
t.Error("Device was added even though it's untrusted")
}
}
cleanupModel(m)
cancel()
m, cancel = newState(t, config.Configuration{
@@ -854,8 +867,7 @@ func TestIssue4897(t *testing.T) {
defer cleanupModel(m)
cancel()
cm, _, err := m.generateClusterConfig(device1)
must(t, err)
cm, _ := m.generateClusterConfig(device1)
if l := len(cm.Folders); l != 1 {
t.Errorf("Cluster config contains %v folders, expected 1", l)
}
@@ -1695,9 +1707,8 @@ func TestRWScanRecovery(t *testing.T) {
}
func TestGlobalDirectoryTree(t *testing.T) {
w, fcfg, wCancel := tmpDefaultWrapper()
m, _, fcfg, wCancel := setupModelWithConnection(t)
defer wCancel()
m := setupModel(t, w)
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
b := func(isfile bool, path ...string) protocol.FileInfo {
@@ -1999,18 +2010,18 @@ func BenchmarkTree_100_10(b *testing.B) {
}
func benchmarkTree(b *testing.B, n1, n2 int) {
m := newModel(b, defaultCfgWrapper, myID, "syncthing", "dev", nil)
m.ServeBackground()
defer cleanupModel(m)
m, _, fcfg, wcfgCancel := setupModelWithConnection(b)
defer wcfgCancel()
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
m.ScanFolder("default")
m.ScanFolder(fcfg.ID)
files := genDeepFiles(n1, n2)
must(b, m.Index(device1, "default", files))
must(b, m.Index(device1, fcfg.ID, files))
b.ResetTimer()
for i := 0; i < b.N; i++ {
m.GlobalDirectoryTree("default", "", -1, false)
m.GlobalDirectoryTree(fcfg.ID, "", -1, false)
}
b.ReportAllocs()
}
@@ -2347,7 +2358,7 @@ func TestIssue3496(t *testing.T) {
t.Log(comp)
// Check that NeedSize does the correct thing
need := needSize(t, m, "default")
need := needSizeLocal(t, m, "default")
if need.Files != 1 || need.Bytes != 1234 {
// The one we added synthetically above
t.Errorf("Incorrect need size; %d, %d != 1, 1234", need.Files, need.Bytes)
@@ -2627,43 +2638,43 @@ func TestCustomMarkerName(t *testing.T) {
}
func TestRemoveDirWithContent(t *testing.T) {
defer func() {
defaultFs.RemoveAll("dirwith")
}()
m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI())
defaultFs.MkdirAll("dirwith", 0755)
tfs.MkdirAll("dirwith", 0755)
content := filepath.Join("dirwith", "content")
fd, err := defaultFs.Create(content)
fd, err := tfs.Create(content)
must(t, err)
fd.Close()
m := setupModel(t, defaultCfgWrapper)
defer cleanupModel(m)
must(t, m.ScanFolder(fcfg.ID))
dir, ok := m.testCurrentFolderFile("default", "dirwith")
dir, ok := m.testCurrentFolderFile(fcfg.ID, "dirwith")
if !ok {
t.Fatalf("Can't get dir \"dirwith\" after initial scan")
}
dir.Deleted = true
dir.Version = dir.Version.Update(device1.Short()).Update(device1.Short())
file, ok := m.testCurrentFolderFile("default", content)
file, ok := m.testCurrentFolderFile(fcfg.ID, content)
if !ok {
t.Fatalf("Can't get file \"%v\" after initial scan", content)
}
file.Deleted = true
file.Version = file.Version.Update(device1.Short()).Update(device1.Short())
must(t, m.IndexUpdate(device1, "default", []protocol.FileInfo{dir, file}))
must(t, m.IndexUpdate(device1, fcfg.ID, []protocol.FileInfo{dir, file}))
// Is there something we could trigger on instead of just waiting?
timeout := time.NewTimer(5 * time.Second)
for {
dir, ok := m.testCurrentFolderFile("default", "dirwith")
dir, ok := m.testCurrentFolderFile(fcfg.ID, "dirwith")
if !ok {
t.Fatalf("Can't get dir \"dirwith\" after index update")
}
file, ok := m.testCurrentFolderFile("default", content)
file, ok := m.testCurrentFolderFile(fcfg.ID, content)
if !ok {
t.Fatalf("Can't get file \"%v\" after index update", content)
}
@@ -3766,14 +3777,14 @@ func TestAddFolderCompletion(t *testing.T) {
}
func TestScanDeletedROChangedOnSR(t *testing.T) {
w, fcfg, wCancel := tmpDefaultWrapper()
defer wCancel()
fcfg.Type = config.FolderTypeReceiveOnly
setFolder(t, w, fcfg)
m := setupModel(t, w)
defer cleanupModel(m)
name := "foo"
m, _, fcfg, wCancel := setupModelWithConnection(t)
ffs := fcfg.Filesystem()
defer wCancel()
defer cleanupModelAndRemoveDir(m, ffs.URI())
fcfg.Type = config.FolderTypeReceiveOnly
setFolder(t, m.cfg, fcfg)
name := "foo"
must(t, writeFile(ffs, name, []byte(name), 0644))
m.ScanFolders()
@@ -3794,7 +3805,7 @@ func TestScanDeletedROChangedOnSR(t *testing.T) {
}
fcfg.Type = config.FolderTypeSendReceive
setFolder(t, w, fcfg)
setFolder(t, m.cfg, fcfg)
m.ScanFolders()
if receiveOnlyChangedSize(t, m, fcfg.ID).Deleted != 0 {
@@ -3889,6 +3900,8 @@ func TestIssue6961(t *testing.T) {
}
m.ServeBackground()
defer cleanupModelAndRemoveDir(m, tfs.URI())
addFakeConn(m, device1, fcfg.ID)
addFakeConn(m, device2, fcfg.ID)
m.ScanFolders()
name := "foo"
@@ -3937,9 +3950,8 @@ func TestIssue6961(t *testing.T) {
}
func TestCompletionEmptyGlobal(t *testing.T) {
wcfg, fcfg, wcfgCancel := tmpDefaultWrapper()
m, _, fcfg, wcfgCancel := setupModelWithConnection(t)
defer wcfgCancel()
m := setupModel(t, wcfg)
defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
files := []protocol.FileInfo{{Name: "foo", Version: protocol.Vector{}.Update(myID.Short()), Sequence: 1}}
m.fmut.Lock()
@@ -3960,6 +3972,8 @@ func TestNeedMetaAfterIndexReset(t *testing.T) {
addDevice2(t, w, fcfg)
m := setupModel(t, w)
defer cleanupModelAndRemoveDir(m, fcfg.Path)
addFakeConn(m, device1, fcfg.ID)
addFakeConn(m, device2, fcfg.ID)
var seq int64 = 1
files := []protocol.FileInfo{{Name: "foo", Size: 10, Version: protocol.Vector{}.Update(device1.Short()), Sequence: seq}}
@@ -4097,7 +4111,7 @@ func TestCcCheckEncryption(t *testing.T) {
dcfg.EncryptionPassword = pw
}
deviceInfos := &indexSenderStartInfo{
deviceInfos := &clusterConfigDeviceInfo{
remote: protocol.Device{ID: device1, EncryptionPasswordToken: tc.tokenRemote},
local: protocol.Device{ID: myID, EncryptionPasswordToken: tc.tokenLocal},
}
@@ -4144,8 +4158,7 @@ func TestCCFolderNotRunning(t *testing.T) {
defer cleanupModelAndRemoveDir(m, tfs.URI())
// A connection can happen before all the folders are started.
cc, _, err := m.generateClusterConfig(device1)
must(t, err)
cc, _ := m.generateClusterConfig(device1)
if l := len(cc.Folders); l != 1 {
t.Fatalf("Expected 1 folder in CC, got %v", l)
}
+1
View File
@@ -1446,6 +1446,7 @@ func TestRequestGlobalInvalidToValid(t *testing.T) {
})
must(t, err)
waiter.Wait()
addFakeConn(m, device2, fcfg.ID)
tfs := fcfg.Filesystem()
defer cleanupModelAndRemoveDir(m, tfs.URI())
+2 -2
View File
@@ -194,7 +194,7 @@ func (m *testModel) testCurrentFolderFile(folder string, file string) (protocol.
}
func (m *testModel) testCompletion(device protocol.DeviceID, folder string) FolderCompletion {
comp, err := m.Completion(protocol.LocalDeviceID, "default")
comp, err := m.Completion(device, folder)
must(m.t, err)
return comp
}
@@ -276,7 +276,7 @@ func receiveOnlyChangedSize(t *testing.T, m Model, folder string) db.Counts {
return snap.ReceiveOnlyChangedSize()
}
func needSize(t *testing.T, m Model, folder string) db.Counts {
func needSizeLocal(t *testing.T, m Model, folder string) db.Counts {
t.Helper()
snap := dbSnapshot(t, m, folder)
defer snap.Release()
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "STDISCOSRV" "1" "May 18, 2021" "v1" "Syncthing"
.TH "STDISCOSRV" "1" "May 28, 2021" "v1" "Syncthing"
.SH NAME
stdiscosrv \- Syncthing Discovery Server
.SH SYNOPSIS
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "STRELAYSRV" "1" "May 18, 2021" "v1" "Syncthing"
.TH "STRELAYSRV" "1" "May 28, 2021" "v1" "Syncthing"
.SH NAME
strelaysrv \- Syncthing Relay Server
.SH SYNOPSIS
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-BEP" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-BEP" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-bep \- Block Exchange Protocol v1
.SH INTRODUCTION AND DEFINITIONS
+4 -4
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-CONFIG" "5" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-CONFIG" "5" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-config \- Syncthing Configuration
.SH SYNOPSIS
@@ -54,7 +54,7 @@ Syncthing also has a database, which is often stored in this directory too.
The config location defaults to \fB$HOME/.config/syncthing\fP
(Unix\-like), \fB$HOME/Library/Application Support/Syncthing\fP (Mac),
or \fB%LOCALAPPDATA%\eSyncthing\fP (Windows). It can be changed at runtime
using the \fB\-config\fP flag. In this directory the following files are
using the \fB\-\-config\fP flag. In this directory the following files are
located:
.INDENT 0.0
.TP
@@ -89,8 +89,8 @@ If \fB~/.local/share/syncthing\fP exists, use that location.
Use the old default location (same as config).
.UNINDENT
.sp
The location of the database can be changed using the \fB\-data\fP flag. The
\fB\-home\fP flag sets both config and database locations at the same time.
The location of the database can be changed using the \fB\-\-data\fP flag. The
\fB\-\-home\fP flag sets both config and database locations at the same time.
The database contains the following files:
.INDENT 0.0
.TP
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-DEVICE-IDS" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-DEVICE-IDS" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-device-ids \- Understanding Device IDs
.sp
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-EVENT-API" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-EVENT-API" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-event-api \- Event API
.SH DESCRIPTION
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-FAQ" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-FAQ" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-faq \- Frequently Asked Questions
.INDENT 0.0
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-GLOBALDISCO" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-GLOBALDISCO" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-globaldisco \- Global Discovery Protocol v3
.SH ANNOUNCEMENTS
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-LOCALDISCO" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-LOCALDISCO" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-localdisco \- Local Discovery Protocol v4
.SH MODE OF OPERATION
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-NETWORKING" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-NETWORKING" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-networking \- Firewall Setup
.SH ROUTER SETUP
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-RELAY" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-RELAY" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-relay \- Relay Protocol v1
.SH WHAT IS A RELAY?
+5 -5
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-REST-API" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-REST-API" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-rest-api \- REST API
.sp
@@ -691,7 +691,7 @@ New in version 1.12.0.
These endpoints facilitate access and modification of the configuration in a granular way. Config sent to the endpoints must be in the same
format as returned by the corresponding GET request. When posting the
configuration succeeds, the posted configuration is immediately applied, except
for changes that require a restart. Query \fI\%/rest/system/config/insync\fP to check if
for changes that require a restart. Query \fI\%/rest/config/restart\-required\fP to check if
a restart is required.
.sp
For all endpoints supporting \fBPATCH\fP, it takes the existing config and
@@ -703,10 +703,10 @@ addresses.
.SS /rest/config
.sp
\fBGET\fP returns the entire config and \fBPUT\fP replaces it.
.SS /rest/system/config/insync
.SS /rest/config/restart\-required
.sp
\fBGET\fP returns whether the config is in sync, i.e. whether the running configuration is
the same as that on disk or if a restart is required.
\fBGET\fP returns whether a restart of Syncthing is required for the current
config to take effect.
.SS /rest/config/folders, /rest/config/devices
.sp
\fBGET\fP returns all folders respectively devices as an array. \fBPUT\fP takes an array and
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-SECURITY" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-SECURITY" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-security \- Security Principles
.sp
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-STIGNORE" "5" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-STIGNORE" "5" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-stignore \- Prevent files from being synchronized to other nodes
.SH SYNOPSIS
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING-VERSIONING" "7" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING-VERSIONING" "7" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing-versioning \- Keep automatic backups of deleted files by other nodes
.sp
+1 -1
View File
@@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.TH "SYNCTHING" "1" "May 18, 2021" "v1" "Syncthing"
.TH "SYNCTHING" "1" "May 28, 2021" "v1" "Syncthing"
.SH NAME
syncthing \- Syncthing
.SH SYNOPSIS