Compare commits

..

34 Commits

Author SHA1 Message Date
Jakob Borg
2145b3701d lib/connections: Correct race on loop variable (fixes #8320) (#8321) 2022-05-04 18:17:03 +02:00
Simon Frei
4031568cdf gui: Bandaid for null http errors (fixes #8261) (#8305) 2022-04-25 19:15:14 +02:00
Syncthing Release Automation
fff9bf98eb gui, man, authors: Update docs, translations, and contributors 2022-04-25 03:54:44 +00:00
Simon Frei
6a7fc49c6b lib/discover: Increase global discovery timeout (#8303) 2022-04-23 16:12:25 +02:00
red_led
89f5d0d400 gui: Always show vertical scroll bar (#8301)
This stops interface from jumping left and right when page content no longer fits into screen.
2022-04-22 21:39:30 +02:00
André Colomb
1eda82b95f lib/model: Improve remoteFolderState reporting (fixes #8266) (#8283) 2022-04-22 08:42:20 +02:00
Jakob Borg
623ec03dad lib/model: Correct type of event data (fixes #8294) (#8295)
These things are fragile, every event should use an ${eventType}Data struct or something instead.
2022-04-21 15:45:31 +02:00
Jakob Borg
c0de42e3df gui: Use neutral color for zero out of zero listeners (#8281) 2022-04-20 14:43:11 +02:00
tomasz1986
4893513800 gui: Improve Latest Change translation string for better multilanguage support (#8290)
Currently, the "Latest Change" translation string is hard-coded to use
the English-like word order of V ("deleted") + N ("file"). As such, it
is incompatible with languages that require to use a different word
order, e.g. Korean or Japanese. In other words, a proper translation of
the string to those languages is currently impossible.

This commit changes the translation string, so that it includes the file
variable, and thanks to this, it can be easily adopted to languages with
different word order than English.

Signed-off-by: Tomasz Wilczyński <twilczynski@naver.com>
2022-04-18 06:41:26 +02:00
Syncthing Release Automation
100142067d gui, man, authors: Update docs, translations, and contributors 2022-04-18 03:54:08 +00:00
Simon Frei
3907cb0693 lib/model: Subscribe to correct event for fs watching (ref #8249) (#8287) 2022-04-17 12:41:25 +04:00
Jakob Borg
61dffabf97 cmd/syncthing, lib/logger: Add date to default log format (fixes #8272) (#8273)
This changes the default log format to include the date.
2022-04-15 07:46:14 +04:00
Eng Zer Jun
bc27aa12cd all: use T.TempDir to create temporary test directory (#8280)
This commit replaces `os.MkdirTemp` with `t.TempDir` in tests. The
directory created by `t.TempDir` is automatically removed when the test
and all its subtests complete.

Prior to this commit, temporary directory created using `os.MkdirTemp`
needs to be removed manually by calling `os.RemoveAll`, which is omitted
in some tests. The error handling boilerplate e.g.
	defer func() {
		if err := os.RemoveAll(dir); err != nil {
			t.Fatal(err)
		}
	}
is also tedious, but `t.TempDir` handles this for us nicely.

Reference: https://pkg.go.dev/testing#T.TempDir
Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2022-04-15 07:44:06 +04:00
André Colomb
0537b9546f lib/model: Switch the remoteFolderState default value to valid (#8275)
Showing all folders from disconnected or paused remote devices as
unaccepted would be a lot of false positives.  As we cannot know
whether the remote has accepted while it doesn't have an active
connection, let's better report false negatives, as in assuming the
folders are accepted.
2022-04-13 18:15:58 +02:00
Simon Frei
0525c755f4 build: Bump quic-go to 0.26.0 for go1.18 update (#8231)
Merging because I want this in the RC, when we do the RC...
2022-04-12 16:27:29 +04:00
Simon Frei
bcd91f536e lib/connections: Create the forgotten channel (ref #8263) (#8267) 2022-04-11 17:32:22 +04:00
Syncthing Release Automation
f9c6c69fa8 gui, man, authors: Update docs, translations, and contributors 2022-04-11 03:52:11 +00:00
André Colomb
0c46e0a9cc gui, lib/model: Mark folders unaccepted by remote device (fixes #8202) (#8201) 2022-04-10 22:47:57 +02:00
Greg
bca6d31b95 Correct comment typo in build.go (#8234) 2022-04-10 22:24:57 +02:00
Simon Frei
db72579f0e lib: Get rid of buggy filesystem wrapping (#8257) 2022-04-10 20:55:05 +02:00
Jakob Borg
9b09bcc5f1 lib/connections: Always run a simple connection test (#7866) 2022-04-10 20:54:42 +02:00
Simon Frei
22e12904c9 lib/connections: Make request tests sequential (#8263) 2022-04-10 20:54:16 +02:00
Simon Frei
b947056e62 lib: Removal global connection registry (#8254) 2022-04-09 16:04:56 +02:00
Jeffery To
e30898ddb3 build: Update go directive to 1.17 (fixes #8258) (#8260)
This should fix compiling from the source tarball with Go 1.18.
2022-04-09 16:01:24 +02:00
Simon Frei
072fa46bfd lib/connections, lib/model: Improve new conn handling (#8253) 2022-04-07 17:35:33 +02:00
Simon Frei
edc3a77b98 lib/fs, lib/model: Add warning about kqueue resource usage (fixes #7855) (#8249) 2022-04-05 21:32:06 +02:00
Syncthing Release Automation
2b80848341 gui, man, authors: Update docs, translations, and contributors 2022-04-04 03:50:52 +00:00
André Colomb
30fa462e33 all: Comments and cosmetics (#8218)
* lib/api: Note ItemStarted and ItemFinished for default filtering.

The reasoning why LocalChangeDetected and RemoteChangeDetected events
are not included in the event stream by default (without explicit
filter mask requested) also holds for the ItemStarted and ItemFinished
events.  They should be excluded as well when we start to break the
API compatibility for some reason.

* gui: Enumerate unused event types in the eventService.

Define constants for the unused event types as well, for completeness'
sake.  They are intentionally not handled in the GUI currently.

* cmd/syncthing: Harmonize uppercase CLI argument placeholders.

Use ALL-UPPERCASE and connecting dashes to distinguish argument
placeholders from literal argument options (e.g. "cpu" or "heap" for
profiling).  The dash makes it clear which words form a single
argument and where a new argument starts.

This style is already used for the "syncthing cli debug file" command.

* lib/model: Simplify event data structure.

Using map[string]interface{} is not necessary when all values are
known to be strings.
2022-04-02 20:36:19 +02:00
Syncthing Release Automation
11ac945b87 gui, man, authors: Update docs, translations, and contributors 2022-03-28 03:50:34 +00:00
Simon Frei
55c513b827 lib/model: Clear folder error after loading ignores (fixes #8232) (#8238) 2022-03-27 21:29:40 +02:00
greatroar
0eca0ac45a lib/model: Chmod to mode|0700, not 755 (#8235) 2022-03-26 12:07:07 +02:00
greatroar
4be867c560 all: Replace errors.Cause with errors.Is (#8236) 2022-03-26 12:05:57 +02:00
Naveen
44b11ec257 Pin actions to a full length commit SHA (#8224) 2022-03-24 16:46:06 +01:00
Greg
53926a1ae6 lib/fs: Do not follow symlinks in watcher on solaris (fixes #8020) (#8223) 2022-03-24 08:36:43 +01:00
139 changed files with 1230 additions and 895 deletions

View File

@@ -10,11 +10,11 @@ jobs:
runs-on: ubuntu-latest
name: Update translations and documentation
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
with:
fetch-depth: 0
token: ${{ secrets.ACTIONS_GITHUB_TOKEN }}
- uses: actions/setup-go@v2
- uses: actions/setup-go@bfdd3570ce990073878bf10f6b2d79082de49492 # v2
with:
go-version: ^1.17.6
- run: |

View File

@@ -96,6 +96,7 @@ Dominik Heidler (asdil12) <dominik@heidler.eu>
Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
Elliot Huffman <thelich2@gmail.com>
Emil Hessman (ceh) <emil@hessman.se>
Eng Zer Jun <engzerjun@gmail.com>
Eric Lesiuta <elesiuta@gmail.com>
Erik Meitner (WSGCSysadmin) <e.meitner@willystreet.coop>
Evgeny Kuznetsov <evgeny@kuznetsov.md>
@@ -112,6 +113,7 @@ Gilli Sigurdsson (gillisig) <gilli@vx.is>
Gleb Sinyavskiy <zhulik.gleb@gmail.com>
Graham Miln (grahammiln) <graham.miln@dssw.co.uk> <graham.miln@miln.eu>
greatroar <61184462+greatroar@users.noreply.github.com>
Greg <gco@jazzhaiku.com>
Han Boetes <han@boetes.org>
HansK-p <42314815+HansK-p@users.noreply.github.com>
Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com>
@@ -135,6 +137,7 @@ Jaroslav Lichtblau <svetlemodry@users.noreply.github.com>
Jaroslav Malec (dzarda) <dzardacz@gmail.com>
jaseg <githubaccount@jaseg.net>
Jaya Chithra (jayachithra) <s.k.jayachithra@gmail.com>
Jeffery To <jeffery.to@gmail.com>
jelle van der Waa <jelle@vdwaa.nl>
Jens Diemer (jedie) <github.com@jensdiemer.de> <git@jensdiemer.de>
Jerry Jacobs (xor-gate) <jerry.jacobs@xor-gate.org> <xor-gate@users.noreply.github.com>
@@ -202,6 +205,7 @@ MikolajTwarog <43782609+MikolajTwarog@users.noreply.github.com>
Mingxuan Lin <gdlmx@users.noreply.github.com>
mv1005 <49659413+mv1005@users.noreply.github.com>
Nate Morrison (nrm21) <natemorrison@gmail.com>
Naveen <172697+naveensrinivasan@users.noreply.github.com>
Nicholas Rishel (PrototypeNM1) <rishel.nick@gmail.com> <PrototypeNM1@users.noreply.github.com>
Nico Stapelbroek <3368018+nstapelbroek@users.noreply.github.com>
Nicolas Braud-Santoni <nicolas@braud-santoni.eu>
@@ -235,6 +239,7 @@ Piotr Bejda (piobpl) <piotrb10@gmail.com>
Pramodh KP (pramodhkp) <pramodh.p@directi.com> <1507241+pramodhkp@users.noreply.github.com>
Quentin Hibon <qh.public@yahoo.com>
Rahmi Pruitt <rjpruitt16@gmail.com>
red_led <red-led@users.noreply.github.com>
Richard Hartmann <RichiH@users.noreply.github.com>
Robert Carosi (nov1n) <robert@carosi.nl>
Roberto Santalla <roobre@users.noreply.github.com>

View File

@@ -883,7 +883,7 @@ func shouldRebuildAssets(target, srcdir string) bool {
func updateDependencies() {
runPrint(goCmd, "get", "-u", "./cmd/...")
runPrint(goCmd, "mod", "tidy", "-go=1.16", "-compat=1.16")
runPrint(goCmd, "mod", "tidy", "-go=1.17", "-compat=1.17")
// We might have updated the protobuf package and should regenerate to match.
proto()
@@ -985,7 +985,7 @@ func getGitVersion() (string, error) {
v0 := string(bs)
// To be more semantic-versionish and ensure proper ordering in our
// upgrade process, we make sure there's only one hypen in the version.
// upgrade process, we make sure there's only one hyphen in the version.
versionRe := regexp.MustCompile(`-([0-9]{1,3}-g[0-9a-f]{5,10}(-dirty)?)`)
if m := versionRe.FindStringSubmatch(vcur); len(m) > 0 {

View File

@@ -84,7 +84,7 @@ func checkServers(deviceID protocol.DeviceID, servers ...string) {
}
func checkServer(deviceID protocol.DeviceID, server string) checkResult {
disco, err := discover.NewGlobal(server, tls.Certificate{}, nil, events.NoopLogger)
disco, err := discover.NewGlobal(server, tls.Certificate{}, nil, events.NoopLogger, nil)
if err != nil {
return checkResult{error: err}
}

View File

@@ -27,7 +27,7 @@ var errorsCommand = cli.Command{
{
Name: "push",
Usage: "Push an error to active clients",
ArgsUsage: "[error message]",
ArgsUsage: "ERROR-MESSAGE",
Action: expects(1, errorsPush),
},
{

View File

@@ -39,13 +39,13 @@ var operationCommand = cli.Command{
{
Name: "folder-override",
Usage: "Override changes on folder (remote for sendonly, local for receiveonly). WARNING: Destructive - deletes/changes your data.",
ArgsUsage: "[folder id]",
ArgsUsage: "FOLDER-ID",
Action: expects(1, foldersOverride),
},
{
Name: "default-ignores",
Usage: "Set the default ignores (config) from a file",
ArgsUsage: "path",
ArgsUsage: "PATH",
Action: expects(1, setDefaultIgnores),
},
},

View File

@@ -68,9 +68,9 @@ The --logflags value is a sum of the following:
8 Long filename
16 Short filename
I.e. to prefix each log line with date and time, set --logflags=3 (1 + 2 from
above). The value 0 is used to disable all of the above. The default is to
show time only (2).
I.e. to prefix each log line with time and filename, set --logflags=18 (2 + 16
from above). The value 0 is used to disable all of the above. The default is
to show date and time (3).
Logging always happens to the command line (stdout) and optionally to the
file at the path specified by --logfile=path. In addition to an path, the special
@@ -187,7 +187,7 @@ type serveOptions struct {
func defaultVars() kong.Vars {
vars := kong.Vars{}
vars["logFlags"] = strconv.Itoa(log.Ltime)
vars["logFlags"] = strconv.Itoa(logger.DefaultFlags)
vars["logMaxSize"] = strconv.Itoa(10 << 20) // 10 MiB
vars["logMaxFiles"] = "3" // plus the current one

View File

@@ -17,14 +17,17 @@ import (
func TestRotatedFile(t *testing.T) {
// Verify that log rotation happens.
dir, err := os.MkdirTemp("", "syncthing")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
dir := t.TempDir()
open := func(name string) (io.WriteCloser, error) {
return os.Create(name)
f, err := os.Create(name)
t.Cleanup(func() {
if f != nil {
_ = f.Close()
}
})
return f, err
}
logName := filepath.Join(dir, "log.txt")

29
go.mod
View File

@@ -28,7 +28,7 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/lib/pq v1.10.3
github.com/lucas-clemente/quic-go v0.25.0
github.com/lucas-clemente/quic-go v0.27.0
github.com/maruel/panicparse v1.6.1
github.com/maxbrunsfeld/counterfeiter/v6 v6.3.0
github.com/minio/sha256-simd v1.0.0
@@ -53,8 +53,31 @@ require (
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c
golang.org/x/text v0.3.7
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
golang.org/x/tools v0.1.6
golang.org/x/tools v0.1.7
google.golang.org/protobuf v1.27.1
)
go 1.16
require (
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/oschwald/maxminddb-golang v1.8.0 // indirect
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)
go 1.17

25
go.sum
View File

@@ -240,21 +240,24 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucas-clemente/quic-go v0.22.0/go.mod h1:vF5M1XqhBAHgbjKcJOXY3JZz3GP0T3FQhz/uyOUS38Q=
github.com/lucas-clemente/quic-go v0.25.0 h1:K+X9Gvd7JXsOHtU0N2icZ2Nw3rx82uBej3mP4CLgibc=
github.com/lucas-clemente/quic-go v0.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T2U9ZlW1AaHOsMOg=
github.com/lucas-clemente/quic-go v0.26.0 h1:ALBQXr9UJ8A1LyzvceX4jd9QFsHvlI0RR6BkV16o00A=
github.com/lucas-clemente/quic-go v0.26.0/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
github.com/lucas-clemente/quic-go v0.27.0 h1:v6WY87q9zD4dKASbG8hy/LpzAVNzEQzw8sEIeloJsc4=
github.com/lucas-clemente/quic-go v0.27.0/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/marten-seemann/qtls-go1-15 v0.1.5/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I=
github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco=
github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
github.com/marten-seemann/qtls-go1-16 v0.1.5 h1:o9JrYPPco/Nukd/HpOHMHZoBDXQqoNtUCmny98/1uqQ=
github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk=
github.com/marten-seemann/qtls-go1-17 v0.1.0-rc.1/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8=
github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk=
github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8=
github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1 h1:EnzzN9fPUkUck/1CuY1FlzBaIYMoiBsdwTNmNGkwUUM=
github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1/go.mod h1:PUhIQk19LoFt2174H4+an8TYvWOGjb/hHwphBeaDHwI=
github.com/marten-seemann/qtls-go1-17 v0.1.1 h1:DQjHPq+aOzUeh9/lixAGunn6rIOQyWChPSI4+hgW7jc=
github.com/marten-seemann/qtls-go1-17 v0.1.1/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s=
github.com/marten-seemann/qtls-go1-18 v0.1.1 h1:qp7p7XXUFL7fpBvSS1sWD+uSqPvzNQK43DH+/qEkj0Y=
github.com/marten-seemann/qtls-go1-18 v0.1.1/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
github.com/maruel/panicparse v1.6.1 h1:803MjBzGcUgE1vYgg3UMNq3G1oyYeKkMu3t6hBS97x0=
github.com/maruel/panicparse v1.6.1/go.mod h1:uoxI4w9gJL6XahaYPMq/z9uadrdr1SyHuQwV2q80Mm0=
github.com/maruel/panicparse/v2 v2.1.1/go.mod h1:AeTWdCE4lcq8OKsLb6cHSj1RWHVSnV9HBCk7sKLF4Jg=
@@ -337,7 +340,6 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -407,7 +409,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
@@ -507,7 +508,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210716203947-853a461950ff/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210924151903-3ad01bbaa167 h1:eDd+TJqbgfXruGQ5sJRU7tEtp/58OAx4+Ayjxg4SM+4=
golang.org/x/net v0.0.0-20210924151903-3ad01bbaa167/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -585,7 +585,6 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -652,8 +651,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.6 h1:SIasE1FVIQOWz2GEAHFOmoW7xchJcqlucjSULTL0Ag4=
golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -10,6 +10,7 @@
body {
padding-bottom: 70px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
overflow-y: scroll;
}
h1, h2, h3, h4, h5 {

View File

@@ -78,10 +78,10 @@
"Defaults": "Подразбирани",
"Delete": "Изтрий",
"Delete Unexpected Items": "Премахване на неочакваните",
"Deleted": "Изтрито",
"Deleted {%file%}": "{{file}} премахнат",
"Deselect All": "Изчистване",
"Deselect devices to stop sharing this folder with.": "Махнете отметката от устройство, за да спрете споделяне с него.",
"Deselect folders to stop sharing with this device.": "Махнете отметката от папка, за да спрете споделяне устройствто.",
"Deselect folders to stop sharing with this device.": "Махнете отметката пред папката, за да спрете споделянето ѝ с устройството.",
"Device": "Устройство",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство \"{{name}}\" ({{device}}) с адрес {{address}} желае да се свърже. Да бъде ли добавено?",
"Device ID": "Идентификатор на устройство",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Броят версии трябва да бъде число и не може да бъде празно.",
"The path cannot be blank.": "Пътят не може да бъде празен.",
"The rate limit must be a non-negative number (0: no limit)": "Ограничението на скоростта трябва да бъде положително число (0: неограничено)",
"The remote device has not accepted sharing this folder.": "Отдалеченото устройство не е приело да споделя папката.",
"The rescan interval must be a non-negative number of seconds.": "Интервалът на обхождане трябва да е положителен брой секунди.",
"There are no devices to share this folder with.": "Няма устройства, с които да споделите папката.",
"There are no file versions to restore.": "Файлът няма версии, които да бъдат възстановени.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Несподелени папки",
"Untrusted": "Недоверено",
"Up to Date": "Синхронизирано",
"Updated": "Обновено",
"Updated {%file%}": "{{file}} обновен",
"Upgrade": "Обновяване",
"Upgrade To {%version%}": "Обновяване до {{version}}",
"Upgrading": "Обновяване",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Esborrar",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Esborrat",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Anul·lar tota la selecció",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "El nombre de versions deu ser un nombre i no pot estar buit.",
"The path cannot be blank.": "La ruta no pot estar buida.",
"The rate limit must be a non-negative number (0: no limit)": "El llímit del ritme deu ser un nombre no negatiu (0: sense llímit)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "L'interval de reescaneig deu ser un nombre positiu de segons.",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Actualitzat",
"Updated": "Actualitzat",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Actualitzar",
"Upgrade To {%version%}": "Actualitzar a {{version}}",
"Upgrading": "Actualitzant",

View File

@@ -78,7 +78,7 @@
"Defaults": "Výchozí hodnoty",
"Delete": "Smazat",
"Delete Unexpected Items": "Smazat neočekávané položky",
"Deleted": "Smazáno",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Zrušit výběr všeho",
"Deselect devices to stop sharing this folder with.": "Zrušte výběr zařízení, se kterými již nemá být tato složka sdílena.",
"Deselect folders to stop sharing with this device.": "Zrušte výběr složek, které se mají přestat sdílet s tímto zařízením.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Je třeba, aby počet verzí bylo číslo a nemůže zůstat nevyplněné.",
"The path cannot be blank.": "Popis umístění nemůže zůstat nevyplněný.",
"The rate limit must be a non-negative number (0: no limit)": "Je třeba, aby limit rychlosti bylo kladné číslo (0: bez limitu)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Je třeba, aby interval opakování skenování bylo kladné číslo.",
"There are no devices to share this folder with.": "Nejsou žádná zařízení, se kterými lze sdílet tuto složku.",
"There are no file versions to restore.": "Žádné verze souboru k obnovení.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Nesdílené složky",
"Untrusted": "Bez důvěry",
"Up to Date": "Aktuální",
"Updated": "Aktualizováno",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Přechod na novější verzi",
"Upgrade To {%version%}": "Aktualizovat na {{version}}",
"Upgrading": "Aktualizuje se",

View File

@@ -28,7 +28,7 @@
"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.": "En ekstern kommando styrer versioneringen. Den skal fjerne filen fra den delte mappe. Hvis stien til programmet indeholder mellemrum, bør den sættes i anførselstegn.",
"Anonymous Usage Reporting": "Anonym brugerstatistik",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Formatet for anonym brugerstatistik er ændret. Vil du flytte til det nye format?",
"Apply": "Apply",
"Apply": "Anvend",
"Are you sure you want to continue?": "Fortsætte?",
"Are you sure you want to override all remote changes?": "Tilsidesæt alle eksterne ændringer?",
"Are you sure you want to permanently delete all these files?": "Slette valgte filer permanent?",
@@ -78,7 +78,7 @@
"Defaults": "Standarder",
"Delete": "Slet",
"Delete Unexpected Items": "Slet ikke forventede elementer ",
"Deleted": "Slettet",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Fravælg alle",
"Deselect devices to stop sharing this folder with.": "Fravælg enheder for at stoppe mappe deling.",
"Deselect folders to stop sharing with this device.": "Fravælg mapper for at stoppe deling med denne enhed.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Antallet af versioner skal være et tal og feltet må ikke være tomt.",
"The path cannot be blank.": "Stien må ikke være tom.",
"The rate limit must be a non-negative number (0: no limit)": "Hastighedsbegrænsningen skal være et ikke-negativt tal (0: ingen begrænsning)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Genskanningsintervallet skal være et ikke-negativt antal sekunder.",
"There are no devices to share this folder with.": "Der er ingen enheder at dele denne mappe med.",
"There are no file versions to restore.": "Der er ingen fil versioner at gendanne.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Ikke delte mapper",
"Untrusted": "Ikke troværdig",
"Up to Date": "Fuldt opdateret",
"Updated": "Opdateret",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Opgradér",
"Upgrade To {%version%}": "Opgradér til {{version}}",
"Upgrading": "Opgraderer",

View File

@@ -78,7 +78,7 @@
"Defaults": "Vorgaben",
"Delete": "Löschen",
"Delete Unexpected Items": "Unerwartete Elemente löschen",
"Deleted": "Gelöscht",
"Deleted {%file%}": "{{file}} gelöscht",
"Deselect All": "Alle abwählen",
"Deselect devices to stop sharing this folder with.": "Geräte abwählen, um diesen Ordner nicht mehr damit zu teilen.",
"Deselect folders to stop sharing with this device.": "Ordner abwählen, um sie nicht mehr für mit diesem Gerät zu teilen.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Ganzzahl und darf nicht leer sein.",
"The path cannot be blank.": "Der Pfad darf nicht leer sein.",
"The rate limit must be a non-negative number (0: no limit)": "Das Datenratelimit muss eine nicht negative Zahl sein (0 = kein Limit).",
"The remote device has not accepted sharing this folder.": "Dieser geteilte Ordner wurde vom Gerät nicht angenommen.",
"The rescan interval must be a non-negative number of seconds.": "Das Scanintervall muss eine nicht negative Anzahl (in Sekunden) sein.",
"There are no devices to share this folder with.": "Es gibt keine Geräte, mit denen dieser Ordner geteilt werden kann.",
"There are no file versions to restore.": "Es gibt keine Dateiversionen zum Wiederherstellen.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Nicht geteilte Ordner",
"Untrusted": "Nicht vertraut",
"Up to Date": "Aktuell",
"Updated": "Aktualisiert",
"Updated {%file%}": "{{file}} aktualisiert",
"Upgrade": "Aktualisierung",
"Upgrade To {%version%}": "Aktualisierung auf {{version}}",
"Upgrading": "Wird aktualisiert",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Διαγραφή",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Διαγραμμένα",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Αποεπιλογή όλων",
"Deselect devices to stop sharing this folder with.": "Αποεπιλέξτε συσκευές για να σταματήσει ο διαμοιρασμός του φακέλου με αυτές.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Ο αριθμός εκδόσεων πρέπει να είναι αριθμός και σίγουρα όχι κενό.",
"The path cannot be blank.": "Το μονοπάτι δεν μπορεί να είναι κενό.",
"The rate limit must be a non-negative number (0: no limit)": "Το όριο ταχύτητας πρέπει να είναι ένας μη-αρνητικός αριθμός (0: χωρίς όριο)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Ο χρόνος επανελέγχου για αλλαγές είναι σε δευτερόλεπτα (δηλ. θετικός αριθμός).",
"There are no devices to share this folder with.": "Δεν υπάρχουν συσκευές με τις οποίες διαμοιράζεται αυτός ο φάκελος.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Ενημερωμένη",
"Updated": "Ενημερωμένο",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Αναβάθμιση",
"Upgrade To {%version%}": "Αναβάθμιση στην έκδοση {{version}}",
"Upgrading": "Αναβάθμιση",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Delete",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Deleted",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Deselect All",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
"The path cannot be blank.": "The path cannot be blank.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Up to Date",
"Updated": "Updated",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Upgrade",
"Upgrade To {%version%}": "Upgrade To {{version}}",
"Upgrading": "Upgrading",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Delete",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Deleted",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Deselect All",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
"The path cannot be blank.": "The path cannot be blank.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Up to Date",
"Updated": "Updated",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Upgrade",
"Upgrade To {%version%}": "Upgrade to {{version}}",
"Upgrading": "Upgrading",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Delete",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Deleted",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Deselect All",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
"The path cannot be blank.": "The path cannot be blank.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Up to Date",
"Updated": "Updated",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Upgrade",
"Upgrade To {%version%}": "Upgrade To {{version}}",
"Upgrading": "Upgrading",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Forigu",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Forigita",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Malelekti Ĉiujn",
"Deselect devices to stop sharing this folder with.": "Malelekti aparatojn por ĉesi komunigi tiun ĉi dosierujon kun ili.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "La nombro da versioj devas esti nombro kaj ne povas esti malplena.",
"The path cannot be blank.": "La vojo ne povas esti malplena.",
"The rate limit must be a non-negative number (0: no limit)": "La rapideca limo devas esti pozitiva nombro (0: senlimo)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "La intervalo de reskano devas esti pozitiva nombro da sekundoj.",
"There are no devices to share this folder with.": "Estas neniu aparato kun kiu komunigi tiun ĉi dosierujon.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Ĝisdata",
"Updated": "Ĝisdatigita",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Altgradigo",
"Upgrade To {%version%}": "Altgradigi Al {{version}}",
"Upgrading": "Altgradigata",

View File

@@ -78,7 +78,7 @@
"Defaults": "Valores por defecto",
"Delete": "Eliminar",
"Delete Unexpected Items": "Borrar elementos inesperados",
"Deleted": "Eliminado",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Deseleccionar Todo",
"Deselect devices to stop sharing this folder with.": "Deseleccione los equipos con los cuales dejar de compartir esta carpeta.",
"Deselect folders to stop sharing with this device.": "Deseleccione las carpetas para dejar de compartir con este equipo.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
"The path cannot be blank.": "La ruta no puede estar vacía.",
"The rate limit must be a non-negative number (0: no limit)": "El límite de velocidad debe ser un número no negativo (0: sin límite)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "El intervalo de actualización debe ser un número positivo de segundos.",
"There are no devices to share this folder with.": "No hay equipos con los cuales compartir esta carpeta.",
"There are no file versions to restore.": "No hay versiones de archivo que restaurar.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Carpetas no compartidas",
"Untrusted": "No confiable",
"Up to Date": "Actualizado",
"Updated": "Actualizado",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Actualizar",
"Upgrade To {%version%}": "Actualizar a {{version}}",
"Upgrading": "Actualizando",

View File

@@ -78,7 +78,7 @@
"Defaults": "Valores Predeterminados",
"Delete": "Eliminar",
"Delete Unexpected Items": "Borrar Elementos Inesperados",
"Deleted": "Eliminado",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Deseleccionar Todo",
"Deselect devices to stop sharing this folder with.": "Deseleccionar dispositivos con los cuales dejar de compartir esta carpeta.",
"Deselect folders to stop sharing with this device.": "Deseleccionar carpetas para dejar de compartir con este dispositivo.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
"The path cannot be blank.": "La ruta no puede estar vacía.",
"The rate limit must be a non-negative number (0: no limit)": "El límite de velocidad debe ser un número no negativo (0: sin límite)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "El intervalo de actualización debe ser un número positivo de segundos.",
"There are no devices to share this folder with.": "No hay dispositivos con los cuales compartir esta carpeta.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Carpetas no Compartidas",
"Untrusted": "No Confiable",
"Up to Date": "Actualizado",
"Updated": "Actualizado",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Actualizar",
"Upgrade To {%version%}": "Actualizar a {{version}}",
"Upgrading": "Actualizando",

View File

@@ -78,7 +78,7 @@
"Defaults": "Lehenetsiak",
"Delete": "Kendu",
"Delete Unexpected Items": "Ezabatu ustekabeko elementuak",
"Deleted": "Kendua",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Hautaketa guztia kendu",
"Deselect devices to stop sharing this folder with.": "Desautatu karpeta honekin partekatu nahi ez dituzun gailuak.",
"Deselect folders to stop sharing with this device.": "Desautatu karpetak gailu honekin partekatzeari uzteko.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Bertsioen kopuruak numerikoa behar du izan eta ez da hutsa izaiten ahal",
"The path cannot be blank.": "Bidea ez da hutsa izaiten ahal",
"The rate limit must be a non-negative number (0: no limit)": "Ixuriaren emaria ez da negatiboa izaiten ahal (0 = mugarik gabekoa)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Ikerketaren tartea ez da segundo kopuru negatiboa izaiten ahal",
"There are no devices to share this folder with.": "Ez dago partekatutako erabilera horri gehitzeko gailurik.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Partekatu gabeko karpetak",
"Untrusted": "Ez da fidagarria",
"Up to Date": "Eguneratua",
"Updated": "Berritua",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Aktualizatu",
"Upgrade To {%version%}": "Egunetaratu {{version}}-ari buruz",
"Upgrading": "Syncthing-en egunetaratzea",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Poista",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Poistettu",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Poista valinnat",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Versioiden määrän rulee olla numero, eikä se voi olla tyhjä.",
"The path cannot be blank.": "Polku ei voi olla tyhjä.",
"The rate limit must be a non-negative number (0: no limit)": "Nopeusrajan tulee olla positiivinen luku tai nolla. (0: ei rajaa)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Uudelleenskannauksen aikavälin tulee olla ei-negatiivinen numero sekunteja.",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Ajan tasalla",
"Updated": "Päivitetty",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Päivitys",
"Upgrade To {%version%}": "Päivitä versioon {{version}}",
"Upgrading": "Päivitetään",

View File

@@ -28,7 +28,7 @@
"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.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers du répertoire partagé. Si le chemin contient des espaces, il doit être spécifié entre guillemets.",
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Le format du rapport anonyme d'utilisation a changé. Voulez-vous passer au nouveau format ?",
"Apply": "Apply",
"Apply": "Appliquer",
"Are you sure you want to continue?": "Confirmez-vous ?",
"Are you sure you want to override all remote changes?": "Voulez-vous vraiment écraser tous les changements distants ?",
"Are you sure you want to permanently delete all these files?": "Êtes-vous sûrs de vouloir définitivement supprimer tous ces fichiers ?",
@@ -78,7 +78,7 @@
"Defaults": "Personnalisation",
"Delete": "Supprimer",
"Delete Unexpected Items": "Supprimer les éléments inattendus",
"Deleted": "Supprimé",
"Deleted {%file%}": "{{file}} supprimé",
"Deselect All": "Tout déselectionner",
"Deselect devices to stop sharing this folder with.": "Désélectionnez les appareils avec lesquels vous ne souhaitez plus partager ces données.",
"Deselect folders to stop sharing with this device.": "Désélectionnez les partages auxquels cet appareil doit plus participer.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Le nombre de versions doit être numérique, et ne peut pas être vide.",
"The path cannot be blank.": "Le chemin ne peut pas être vide.",
"The rate limit must be a non-negative number (0: no limit)": "La limite de débit ne doit pas être négative (0 = pas de limite)",
"The remote device has not accepted sharing this folder.": "L'appareil distant n'a pas (encore ?) accepté de partager ce répertoire.",
"The rescan interval must be a non-negative number of seconds.": "L'intervalle d'analyse ne doit pas être un nombre négatif de secondes.",
"There are no devices to share this folder with.": "Il n'y a aucun appareil à ajouter à ce partage.",
"There are no file versions to restore.": "Aucune version de fichier à restaurer.",
@@ -393,7 +394,7 @@
"Time the item was last modified": "Dernière modification de l'élément",
"Today": "Aujourd'hui",
"Trash Can File Versioning": "Style poubelle",
"Twitter": "Twitter",
"Twitter": "Piaf",
"Type": "Type",
"UNIX Permissions": "Permissions UNIX",
"Unavailable": "Indisponible",
@@ -408,7 +409,7 @@
"Unshared Folders": "Non partagés",
"Untrusted": "Retirer la confiance (chiffrer tous les partages)",
"Up to Date": "À jour",
"Updated": "Mis à jour",
"Updated {%file%}": "{{file}} modifié",
"Upgrade": "Mettre à jour",
"Upgrade To {%version%}": "Mettre à jour vers {{version}}",
"Upgrading": "Mise à jour de Syncthing",

View File

@@ -78,7 +78,7 @@
"Defaults": "Standertwearden",
"Delete": "Fuortsmite",
"Delete Unexpected Items": " Unferwachte items wiskje",
"Deleted": "Fuortsmiten",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Alles Deselektearje",
"Deselect devices to stop sharing this folder with.": "Kies de apparaten om dizze map net langer mei te dielen.",
"Deselect folders to stop sharing with this device.": "Deselektearje mappen om it dielen mei dit apparaat te stopjen.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "It tal fan ferzjes moat in nûmer wêze en mei net leech wêze.",
"The path cannot be blank.": "It paad mei net leech wêze.",
"The rate limit must be a non-negative number (0: no limit)": "It fluggenslimyt moat in posityf nûmer wêze (0: gjin limyt)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "It wersken-ynterfal moat in posityf tal fan sekonden wêze.",
"There are no devices to share this folder with.": "Der binne gjin apparaten om dizze map mei te dielen.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Net-dielde mappen",
"Untrusted": "Net betroud",
"Up to Date": "By de tiid",
"Updated": "Fernijt",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Fernije",
"Upgrade To {%version%}": "Fernije nei {{version}}",
"Upgrading": "Oan it fernijen",

View File

@@ -78,7 +78,7 @@
"Defaults": "Alapértelmezések",
"Delete": "Törlés",
"Delete Unexpected Items": "Váratlan elemek törlése",
"Deleted": "Törölve",
"Deleted {%file%}": "Törölt {{file}}",
"Deselect All": "Kijelölés megszüntetése",
"Deselect devices to stop sharing this folder with.": "Azon eszközök kijelölésének törlése, amelyekkel e mappa megosztása leállítandó.",
"Deselect folders to stop sharing with this device.": "Szüntesse meg a mappák kijelölését a mappák megosztásának leállításához ezzel az eszközzel.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "A megtartott verziók száma nem lehet üres.",
"The path cannot be blank.": "Az elérési útvonal nem lehet üres.",
"The rate limit must be a non-negative number (0: no limit)": "A sebességlimitnek pozitív számnak kell lennie (0: nincs limit)",
"The remote device has not accepted sharing this folder.": "A távoli eszköz nem fogadta el ennek a mappának a megosztását.",
"The rescan interval must be a non-negative number of seconds.": "Az átnézési intervallum nullánál nagyobb másodperc érték kell legyen.",
"There are no devices to share this folder with.": "Nincsenek eszközök, amelyekkel megosztható lenne a mappa.",
"There are no file versions to restore.": "Nincsenek visszaállítható fájlverziók.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Nem megosztott mappák",
"Untrusted": "Nem megbízható",
"Up to Date": "Friss",
"Updated": "Frissítve",
"Updated {%file%}": "Frissített {{file}}",
"Upgrade": "Frissítés",
"Upgrade To {%version%}": "Frissítés a verzióra: {{version}}",
"Upgrading": "Frissítés",

View File

@@ -78,7 +78,7 @@
"Defaults": "Bawaan",
"Delete": "Hapus",
"Delete Unexpected Items": "Hapus Barang Tidak Terduga",
"Deleted": "Terhapus",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Batal Pilih Semua",
"Deselect devices to stop sharing this folder with.": "Batal pilih perangkat untuk memberhentikan pembagian folder ini.",
"Deselect folders to stop sharing with this device.": "Batal pilih folder untuk memberhentikan pembagian dengan perangkat ini.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Jumlah versi harus berupa angka dan tidak dapat kosong.",
"The path cannot be blank.": "Lokasi tidak dapat kosong.",
"The rate limit must be a non-negative number (0: no limit)": "Pembatasan kecepatan harus berupa angka positif (0: tidak terbatas)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Interval pemindaian ulang harus berupa angka positif.",
"There are no devices to share this folder with.": "Tidak ada perangkat untuk membagikan folder ini.",
"There are no file versions to restore.": "Tidak ada versi berkas untuk dipulihkan.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Folder Tidak Terbagi",
"Untrusted": "Tidak Terpercaya",
"Up to Date": "Tersinkron",
"Updated": "Diperbarui",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Tingkatkan",
"Upgrade To {%version%}": "Tingkatkan Ke {{version}}",
"Upgrading": "Meningkatkan",

View File

@@ -78,7 +78,7 @@
"Defaults": "Impostazioni predefinite",
"Delete": "Elimina",
"Delete Unexpected Items": "Elimina elementi imprevisti",
"Deleted": "Cancellato",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Deseleziona tutto",
"Deselect devices to stop sharing this folder with.": "Deseleziona i dispositivi con cui interrompere la condivisione di questa cartella.",
"Deselect folders to stop sharing with this device.": "Deseleziona le cartelle per interromperne la condivisione con questo dispositivo.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Il numero di versioni dev'essere un numero e non può essere vuoto.",
"The path cannot be blank.": "Il percorso non può essere vuoto.",
"The rate limit must be a non-negative number (0: no limit)": "Il limite di banda deve essere un numero non negativo (0: nessun limite)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "L'intervallo di scansione deve essere un numero non negativo secondi.",
"There are no devices to share this folder with.": "Non ci sono dispositivi con cui condividere questa cartella.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Cartelle non condivise",
"Untrusted": "Non attendibile",
"Up to Date": "Sincronizzato",
"Updated": "Aggiornato",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Aggiornamento",
"Upgrade To {%version%}": "Aggiorna alla {{version}}",
"Upgrading": "Aggiornamento",

View File

@@ -78,7 +78,7 @@
"Defaults": "デフォルト",
"Delete": "削除",
"Delete Unexpected Items": "予期しないアイテムを削除",
"Deleted": "削除",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "すべて選択解除",
"Deselect devices to stop sharing this folder with.": "このフォルダの共有を停止したいデバイスがある場合は、当該デバイスの選択を解除してください。",
"Deselect folders to stop sharing with this device.": "このデバイスとの共有を停止するフォルダーを選択解除します。",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "保持するバージョン数は数値を指定してください。空欄にはできません。",
"The path cannot be blank.": "パスを入力してください。",
"The rate limit must be a non-negative number (0: no limit)": "帯域制限値は0以上で指定して下さい。 (0で無制限)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "再スキャン間隔は0秒以上で指定してください。",
"There are no devices to share this folder with.": "どのデバイスともフォルダーを共有していません。",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "非共有のフォルダー",
"Untrusted": "信頼しない",
"Up to Date": "最新",
"Updated": "更新",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "アップグレード",
"Upgrade To {%version%}": "{{version}} にアップグレードする",
"Upgrading": "アップグレード中",

View File

@@ -1,7 +1,7 @@
{
"A device with that ID is already added.": "이 ID 번호를 가진 기기가 이미 추가되어 있습니다.",
"A device with that ID is already added.": "이 식별자를 가진 기기가 이미 추가되어 있습니다.",
"A negative number of days doesn't make sense.": "일수를 음수로 입력하는 것은 말이 되지 않습니다.",
"A new major version may not be compatible with previous versions.": "새로운 주요 버전이 이전 버전과 호환되지 않을 수 있습니다.",
"A new major version may not be compatible with previous versions.": "새로운 주요 버전이 이전 버전과 호환되지 않을 수 있습니다.",
"API Key": "API 키",
"About": "정보",
"Action": "동작",
@@ -20,7 +20,7 @@
"Advanced Configuration": "고급 설정",
"All Data": "전체 데이터",
"All Time": "전체 기간",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "송신하는 데이터 모두를 비밀번호 없이 읽을 수 없도록 이 기기와 공유한 모든 폴더를 비밀번호로 보호해야 합니다.",
"All folders shared with this device must be protected by a password, such that all sent data is unreadable without the given password.": "송신하는 데이터 전체를 비밀번호 없이 읽을 수 없도록 이 기기와 공유한 모든 폴더를 비밀번호로 보호해야 합니다.",
"Allow Anonymous Usage Reporting?": "익명 사용 보고를 허용하시겠습니까?",
"Allowed Networks": "허가된 망",
"Alphabetic": "가나다순",
@@ -78,18 +78,18 @@
"Defaults": "기본 설정",
"Delete": "삭제",
"Delete Unexpected Items": "예기치 못한 항목 삭제",
"Deleted": "삭제됨",
"Deleted {%file%}": "{{file}} 삭제됨",
"Deselect All": "모두 선택 해제",
"Deselect devices to stop sharing this folder with.": "현재 폴더를 공유하지 않을 기기를 선택 해제하십시오.",
"Deselect folders to stop sharing with this device.": "현재 기기와 공유하지 않을 폴더를 선택 해제하십시오.",
"Device": "기기",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "\"{{name}}\" ({{device}} 기기가 {{address}}) 주소에서 접속을 요청했습니다. 새 기기를 추가하시겠습니까?",
"Device ID": "기기 ID 번호",
"Device ID": "기기 식별자",
"Device Identification": "기기 식별자",
"Device Name": "기기명",
"Device is untrusted, enter encryption password": "신뢰하지 않는 기기입니다; 암호화 비밀번호를 입력하십시오",
"Device rate limits": "기기 속도 제한",
"Device that last modified the item": "항목 최근 수정 기기",
"Device that last modified the item": "항목을 가장 최근 수정 기기",
"Devices": "기기",
"Disable Crash Reporting": "충돌 보고 비활성화",
"Disabled": "비활성화됨",
@@ -97,7 +97,7 @@
"Disabled periodic scanning and enabled watching for changes": "주기적 탐색 비활성화됨 및 변경 항목 감시 활성화됨",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "주기적 탐색 비활성화됨 및 변경 항목 감시 설정에 실패함; 1분마다 재시도 중:",
"Disables comparing and syncing file permissions. Useful on systems with nonexistent or custom permissions (e.g. FAT, exFAT, Synology, Android).": "파일 권한의 비교 및 동기화가 비활성화됩니다. FAT, exFAT, Synology, Android 등 파일 권한이 존재하지 않거나 비표준 파일 권한을 사용하는 체제에서 유용합니다.",
"Discard": "일시적 무시",
"Discard": "무시",
"Disconnected": "연결 끊김",
"Disconnected (Unused)": "연결 끊김(미사용)",
"Discovered": "탐지됨",
@@ -134,7 +134,7 @@
"Failed to load file versions.": "파일 버전을 불러오기에 실패했습니다.",
"Failed to load ignore patterns.": "무시 양식을 불러오기에 실패했습니다.",
"Failed to setup, retrying": "설정 적용 실패; 재시도 중",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "IPv6 연결이 없을 때는 IPv6 서버에 접속하지 못하는 것이 정상입니다.",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "IPv6 연결되어 있지 않을 때는 IPv6 서버에 접속하지 못하는 것이 정상입니다.",
"File Pull Order": "파일 수신 순서",
"File Versioning": "파일 버전 관리",
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Syncthing에 의해 교체 또는 삭제된 파일은 .stversions 폴더로 이동됩니다.",
@@ -145,7 +145,7 @@
"Filter by date": "날짜별 검색",
"Filter by name": "이름별 검색",
"Folder": "폴더",
"Folder ID": "폴더 ID 번호",
"Folder ID": "폴더 식별자",
"Folder Label": "폴더명",
"Folder Path": "폴더 경로",
"Folder Type": "폴더 유형",
@@ -162,8 +162,8 @@
"GUI Theme": "GUI 테마",
"General": "일반",
"Generate": "생성",
"Global Discovery": "글로벌 탐지",
"Global Discovery Servers": "글로벌 탐지 서버",
"Global Discovery": "외부 탐지",
"Global Discovery Servers": "외부 탐지 서버",
"Global State": "전체 기기 상태",
"Help": "도움말",
"Home page": "홈페이지",
@@ -190,7 +190,7 @@
"Last 7 Days": "지난 7일",
"Last Month": "지난 달",
"Last Scan": "최근 탐색",
"Last seen": "최근 접속",
"Last seen": "최근 연결",
"Latest Change": "최신 변경 항목",
"Learn more": "더 알아보기",
"Limit": "제한",
@@ -200,7 +200,7 @@
"Loading data...": "데이터를 불러오는 중...",
"Loading...": "불러오는 중...",
"Local Additions": "현재 기기 추가 항목",
"Local Discovery": "로컬 탐지",
"Local Discovery": "내부 탐지",
"Local State": "현재 기기 상태",
"Local State (Total)": "현재 기기 상태(합계)",
"Locally Changed Items": "현재 기기 변경 항목",
@@ -216,12 +216,12 @@
"Mod. Time": "수정 시간",
"Move to top of queue": "대기열 상단으로 이동",
"Multi level wildcard (matches multiple directory levels)": "다중 수준 와일드카드(여러 단계의 디렉토리에서 적용됨)",
"Never": "사용하지 않음",
"Never": "기록 없음",
"New Device": "새 기기",
"New Folder": "새 폴더",
"Newest First": "최신 파일 순",
"No": "아니요",
"No File Versioning": "파일 버전 관리하지 않음",
"No File Versioning": "파일 버전 관리하지 않음",
"No files will be deleted as a result of this operation.": "이 작업의 결과로는 아무 파일도 삭제되지 않습니다.",
"No upgrades": "업데이트하지 않음",
"Not shared": "공유되지 않음",
@@ -308,13 +308,13 @@
"Shared Folders": "공유된 폴더",
"Shared With": "공유된 기기",
"Sharing": "공유",
"Show ID": "기기 ID 번호 보기",
"Show ID": "기기 식별자 보기",
"Show QR": "QR 코드 보기",
"Show detailed discovery status": "탐지 현황 상세 보기",
"Show detailed listener status": "대기자 현황 상세 보기",
"Show diff with previous version": "이전 버전과의 diff 보기",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "기기 ID 번호를 대신해 기기 목록에서 나타납니다. 다른 기기에 선택적 기본값 이름으로 통보됩니다.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "기기 ID 번호를 대신해 기기 목록에서 나타납니다. 비워 둘 경우 다른 기기에서 통보받은 이름으로 갱신됩니다.",
"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.": "기기 식별자를 대신해 기기 목록에서 나타납니다. 비워 둘 경우 다른 기기에서 통보받은 이름으로 갱신됩니다.",
"Shutdown": "종료",
"Shutdown Complete": "종료 완료",
"Simple File Versioning": "간단한 파일 버전 관리",
@@ -351,22 +351,22 @@
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "시작 옵션이 GUI 주소를 덮어쓰고 있습니다. 덮어쓰기가 지속되는 동안에는 설정을 변경할 수 없습니다.",
"The Syncthing Authors": "The Syncthing Authors",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing의 관리자 인터페이스가 비밀번호 없이 원격 접속할 수 있도록 설정되어 있습니다.",
"The aggregated statistics are publicly available at the URL below.": "수집된 통계는 아래의 주소에서 공람되어 있습니다.",
"The aggregated statistics are publicly available at the URL below.": "수집된 통계는 아래의 주소에서 공람할 수 있습니다.",
"The cleanup interval cannot be blank.": "정리 간격은 비워 둘 수 없습니다.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "설정이 저장되었으나 아직 활성화되지 않았습니다. 새 설정을 활성화하려면 Syncthing을 재시작하십시오.",
"The device ID cannot be blank.": "기기 ID 번호는 비워 둘 수 없습니다.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "이 자리에 입력할 기기 ID 번호는 다른 기기의 \"동작 > 기기 ID 번호 보기\"에서 찾을 수 있습니다. 공백과 하이픈은 선택적입니다(무시됩니다).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "암호화된 사용 보고서는 매일 전송됩니다. 사용 중인 운영체제, 폴더 크기와 응용 프로그램 버전을 추적하기 위해서입니다. 만일 보고되는 정보가 변경되면 이 알림창이 다시 표시니다.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "입력한 기기 ID 번호가 올바르지 않습니다. 52 또는 56자의 알파벳과 숫자로 구성되어야 하며, 공백과 하이픈은 선택적입니다.",
"The folder ID cannot be blank.": "폴더 ID 번호는 비워 둘 수 없습니다.",
"The folder ID must be unique.": "폴더 ID 번호는 유일무이해야 합니다.",
"The device ID cannot be blank.": "기기 식별자는 비워 둘 수 없습니다.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "이 자리에 입력할 기기 식별자는 다른 기기의 \"동작 > 기기 식별자 보기\"에서 찾을 수 있습니다. 공백과 하이픈은 선택적입니다(무시됩니다).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "암호화된 사용 보고서는 매일 전송됩니다. 사용 중인 운영체제, 폴더 크기와 응용 프로그램 버전을 추적하기 위해서입니다. 만일 보고되는 정보가 변경되면 이 알림창이 다시 표시될 예정입니다.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "입력한 기기 식별자가 올바르지 않습니다. 52 또는 56자의 알파벳과 숫자로 구성되어야 하며, 공백과 하이픈은 선택적입니다.",
"The folder ID cannot be blank.": "폴더 식별자는 비워 둘 수 없습니다.",
"The folder ID must be unique.": "폴더 식별자는 유일무이해야 합니다.",
"The folder content on other devices will be overwritten to become identical with this device. Files not present here will be deleted on other devices.": "다른 기기의 폴더 내용을 현재 기기와 동일하도록 덮어씁니다. 현재 기기의 폴더에 존재하지 않는 파일은 다른 기기에서 삭제됩니다.",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "현재 기기의 폴더 내용을 다른 기기와 동일하도록 덮어쓰입니다. 현재 기기의 폴더에 새로 추가된 파일은 삭제됩니다.",
"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초마다, 첫 하루 동안은 1시간마다, 첫 30일 동안은 1일마다, 그리고 최대 보관 기간까지는 1주일마다 버전이 보관됩니다.",
"The following items could not be synchronized.": "다음 항목이 동기화되지 못했습니다.",
"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 interval must be a positive number of seconds.": "간격은 초 단위의 양수여야 합니다.",
"The interval, in seconds, for running cleanup in the versions directory. Zero to disable periodic cleaning.": "버전 폴더를 정리하는 초 단위의 간격입니다. 주기적 정리를 비활성화하려면 0을 입력하십시오.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "버전 개수는 숫자여야 하며 비워 둘 수 없습니다.",
"The path cannot be blank.": "경로는 비워 둘 수 없습니다.",
"The rate limit must be a non-negative number (0: no limit)": "속도 제한은 양수여야 합니다(0: 무제한)",
"The remote device has not accepted sharing this folder.": "다른 기기가 이 폴더의 공유를 승인하지 않았습니다.",
"The rescan interval must be a non-negative number of seconds.": "재탐색 간격은 초 단위의 양수여야 합니다.",
"There are no devices to share this folder with.": "이 폴더를 공유할 기기가 없습니다.",
"There are no file versions to restore.": "복구할 파일 버전이 없습니다.",
@@ -385,12 +386,12 @@
"They are retried automatically and will be synced when the error is resolved.": "자동 재시도 중이며 문제가 해결되는 즉시 동기화될 예정입니다.",
"This Device": "현재 기기",
"This Month": "이번 달",
"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 is a major version upgrade.": "주요 버전 업데이트입니다.",
"This setting controls the free space required on the home (i.e., index database) disk.": "이 설정은 홈(즉, 인덕스 데이터베이스) 저장 장치의 여유 공간을 관리합니다.",
"Time": "시간",
"Time the item was last modified": "항목 최근 변경 시간",
"Time the item was last modified": "항목이 가장 최근에 수정된 시간",
"Today": "오늘",
"Trash Can File Versioning": "휴지통을 통한 파일 버전 관리",
"Twitter": "트위터",
@@ -408,7 +409,7 @@
"Unshared Folders": "공유되지 않은 폴더",
"Untrusted": "신뢰하지 않음",
"Up to Date": "최신 상태",
"Updated": "업데이트됨",
"Updated {%file%}": "{{file}} 업데이트됨",
"Upgrade": "업데이트",
"Upgrade To {%version%}": "{{version}}으로 업데이트",
"Upgrading": "업데이트 중",
@@ -435,7 +436,7 @@
"Watching for Changes": "변경 항목 감시",
"Watching for changes discovers most changes without periodic scanning.": "변경 항목 감시는 주기적으로 탐색하지 않아도 대부분의 변경 항목을 탐지합니다.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "새 기기를 추가할 때는 추가한 기기에서도 현재 기기를 추가해야 합니다.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "새 폴더를 추가할 때 폴더 ID 번호는 기기 간에 폴더를 묶어줍니다. 대소문자가 구분되며 모든 기기에서 동일해야 합니다.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "새 폴더를 추가할 때 폴더 식별자는 기기 간에 폴더를 묶어줍니다. 대소문자가 구분되며 모든 기기에서 동일해야 합니다.",
"Yes": "예",
"Yesterday": "어제",
"You can also select one of these nearby devices:": "주변의 기기 중 하나를 선택할 수도 있습니다.",

View File

@@ -78,7 +78,7 @@
"Defaults": "Numatytosios reikšmės",
"Delete": "Ištrinti",
"Delete Unexpected Items": "Ištrinti netikėtus elementus",
"Deleted": "Ištrinta",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Nuimti žymėjimą nuo visų",
"Deselect devices to stop sharing this folder with.": "Panaikinti įrenginių pasirinkimą, kad su jais būtų nustota bendrinti šį aplanką. ",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Versijų skaičius turi būti skaitmuo ir negali būti tuščias laukelis.",
"The path cannot be blank.": "Kelias negali būti tuščias.",
"The rate limit must be a non-negative number (0: no limit)": "Srauto maksimalus greitis privalo būti ne neigiamas skaičius (0: nėra apribojimo)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
"There are no devices to share this folder with.": "Nėra įrenginių su kuriais bendrinti šį aplanką.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Atnaujinta",
"Updated": "Atnaujinta",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Atnaujinimas",
"Upgrade To {%version%}": "Atnaujinti į {{version}}",
"Upgrading": "Atnaujinama",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Slett",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Slettet",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Fjern alle markeringer",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Antall versjoner må være et tall og kan ikke være tomt.",
"The path cannot be blank.": "Plasseringen kan ikke være tom.",
"The rate limit must be a non-negative number (0: no limit)": "Hastighetsbegrensningen kan ikke være et negativt tall (0: ingen begrensing)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Antall sekund for intervallet kan ikke være negativt.",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Oppdatert",
"Updated": "Oppdatert",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Oppgradere",
"Upgrade To {%version%}": "Oppgrader til {{version}}",
"Upgrading": "Oppgraderer",

View File

@@ -28,7 +28,7 @@
"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.": "Een externe opdracht regelt het versiebeheer. Hij moet het bestand verwijderen uit de gedeelde map. Als het pad naar de toepassing spaties bevat, moet dit tussen aanhalingstekens geplaatst worden.",
"Anonymous Usage Reporting": "Anonieme gebruikersstatistieken",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Het formaat voor anonieme gebruikersrapporten is gewijzigd. Wilt u naar het nieuwe formaat overschakelen?",
"Apply": "Apply",
"Apply": "Toepassen",
"Are you sure you want to continue?": "Weet u zeker dat u wilt doorgaan?",
"Are you sure you want to override all remote changes?": "Weet u zeker dat u alle externe wijzigingen wilt overschrijven?",
"Are you sure you want to permanently delete all these files?": "Weet u zeker dat u al deze bestanden permanent wilt verwijderen?",
@@ -78,7 +78,7 @@
"Defaults": "Standaardwaarden",
"Delete": "Verwijderen",
"Delete Unexpected Items": "Onverwachte items verwijderen",
"Deleted": "Verwijderd",
"Deleted {%file%}": "{{file}} verwijderd",
"Deselect All": "Alles deselecteren",
"Deselect devices to stop sharing this folder with.": "Deselecteer apparaten om er deze map niet meer mee te delen.",
"Deselect folders to stop sharing with this device.": "Deselecteer mappen om te stoppen met delen met dit apparaat.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Het aantal versies moet een getal zijn en mag niet leeg ziijn.",
"The path cannot be blank.": "Het pad mag niet leeg zijn.",
"The rate limit must be a non-negative number (0: no limit)": "De snelheidsbegrenzing moet een positief getal zijn (0: geen begrenzing)",
"The remote device has not accepted sharing this folder.": "Het externe apparaat heeft het delen van deze map niet geaccepteerd.",
"The rescan interval must be a non-negative number of seconds.": "Het interval voor opnieuw scannen moet een positief aantal seconden zijn.",
"There are no devices to share this folder with.": "Er zijn geen apparaten om deze map mee te delen.",
"There are no file versions to restore.": "Er zijn geen bestandsversies om te herstellen.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Niet-gedeelde mappen",
"Untrusted": "Niet vertrouwd",
"Up to Date": "Bijgewerkt",
"Updated": "Bijgewerkt",
"Updated {%file%}": "{{file}} bijgewerkt",
"Upgrade": "Bijwerken",
"Upgrade To {%version%}": "Bijwerken naar {{version}}",
"Upgrading": "Bijwerken",

View File

@@ -1,5 +1,5 @@
{
"A device with that ID is already added.": "Urządzenie o tym numerze ID jest już dodane.",
"A device with that ID is already added.": "Urządzenie o tym identyfikatorze jest już dodane.",
"A negative number of days doesn't make sense.": "Ujemna wartość liczbowa dni nie ma sensu.",
"A new major version may not be compatible with previous versions.": "Nowa duża wersja może nie być kompatybilna z poprzednimi wersjami.",
"API Key": "Klucz API",
@@ -78,13 +78,13 @@
"Defaults": "Domyślne",
"Delete": "Usuń",
"Delete Unexpected Items": "Usuń elementy nieoczekiwane",
"Deleted": "Usunięto",
"Deleted {%file%}": "Usunięto {{file}}",
"Deselect All": "Odznacz wszystkie",
"Deselect devices to stop sharing this folder with.": "Odznacz urządzenia, z którymi chcesz przestać współdzielić ten folder.",
"Deselect folders to stop sharing with this device.": "Odznacz foldery, które chcesz przestać współdzielić z tym urządzeniem.",
"Device": "Urządzenie",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Urządzenie o nazwie \"{{name}}\" {{device}} pod adresem ({{address}}) chce się połączyć. Czy dodać nowe urządzenie?",
"Device ID": "Numer ID urządzenia",
"Device ID": "Identyfikator urządzenia",
"Device Identification": "Identyfikator urządzenia",
"Device Name": "Nazwa urządzenia",
"Device is untrusted, enter encryption password": "Urządzenie jest niezaufane; wprowadź szyfrujące hasło",
@@ -145,7 +145,7 @@
"Filter by date": "Filtruj według daty",
"Filter by name": "Filtruj według nazwy",
"Folder": "Folder",
"Folder ID": "Numer ID folderu",
"Folder ID": "Identyfikator folderu",
"Folder Label": "Etykieta folderu",
"Folder Path": "Ścieżka folderu",
"Folder Type": "Rodzaj folderu",
@@ -308,13 +308,13 @@
"Shared Folders": "Współdzielone foldery",
"Shared With": "Współdzielony z",
"Sharing": "Współdzielenie",
"Show ID": "Pokaż numer ID",
"Show ID": "Pokaż identyfikator",
"Show QR": "Pokaż kod QR",
"Show detailed discovery status": "Pokaż szczegółowy stan odnajdywania",
"Show detailed listener status": "Pokaż szczegółowy stan nasłuchujących",
"Show diff with previous version": "Pokaż diff z poprzednią wersją",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Widoczna na liście urządzeń zamiast numeru ID urządzenia. Będzie anonsowana do innych urządzeń jako opcjonalna nazwa domyślna.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Widoczna na liście urządzeń zamiast numeru ID urządzenia. Będzie zaktualizowana do nazwy anonsowanej przez urządzenie, jeżeli pozostanie pusta.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Widoczna na liście urządzeń zamiast identyfikatora urządzenia. Będzie anonsowana do innych urządzeń jako opcjonalna nazwa domyślna.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Widoczna na liście urządzeń zamiast identyfikatora urządzenia. Zostanie zaktualizowana do nazwy anonsowanej przez urządzenie, jeżeli pozostanie pusta.",
"Shutdown": "Wyłącz",
"Shutdown Complete": "Wyłączanie ukończone",
"Simple File Versioning": "Proste wersjonowanie plików",
@@ -354,12 +354,12 @@
"The aggregated statistics are publicly available at the URL below.": "Zebrane statystyki są publicznie dostępne pod poniższym adresem.",
"The cleanup interval cannot be blank.": "Przedział czasowy czyszczenia nie może być pusty.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Ustawienia zostały zapisane, ale nie są jeszcze aktywne. Syncthing musi zostać uruchomiony ponownie, aby aktywować nowe ustawienia.",
"The device ID cannot be blank.": "Numer ID urządzenia nie może być puste.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Numer ID urządzenia do wpisania tutaj można znaleźć w oknie \"Działania > Pokaż numer ID\" na innym urządzeniu. Spacje i myślniki są opcjonalne (ignorowane).",
"The device ID cannot be blank.": "Identyfikator urządzenia nie może być pusty.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Identyfikator urządzenia do wpisania tutaj można znaleźć w oknie \"Działania > Pokaż identyfikator\" na innym urządzeniu. Spacje i myślniki są opcjonalne (ignorowane).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Zaszyfrowane statystyki użycia są wysyłane codziennie. Używane są one do śledzenia popularności systemów, rozmiarów folderów oraz wersji programu. Jeżeli wysyłane statystyki ulegną zmianie, zostaniesz poproszony o ponowne udzielenie zgody w tym oknie.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Wprowadzony numer ID urządzenia wygląda na niepoprawny. Musi ono zawierać 52 lub 56 znaków składających się z liter i cyfr. Spacje i myślniki są opcjonalne.",
"The folder ID cannot be blank.": "Numer ID folderu nie może być pusty.",
"The folder ID must be unique.": "Numer ID folderu musi być niepowtarzalny.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Wprowadzony identyfikator urządzenia wygląda na niepoprawny. Musi on zawierać 52 lub 56 znaków składających się z liter i cyfr. Spacje i myślniki są opcjonalne.",
"The folder ID cannot be blank.": "Identyfikator folderu nie może być pusty.",
"The folder ID must be unique.": "Identyfikator folderu musi być niepowtarzalny.",
"The folder content on other devices will be overwritten to become identical with this device. Files not present here will be deleted on other devices.": "Zawartość folderu na innych urządzeniach zostanie nadpisana tak, aby upodobnić się do tego urządzenia. Pliki nieobecne tutaj zostaną usunięte na innych urządzeniach.",
"The folder content on this device will be overwritten to become identical with other devices. Files newly added here will be deleted.": "Zawartość folderu na tym urządzeniu zostanie nadpisana tak, aby upodobnić się do innych urządzeń. Pliki nowo dodane tutaj zostaną usunięte.",
"The folder path cannot be blank.": "Ścieżka folderu nie może być pusta.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Liczba wersji musi być wartością liczbową oraz nie może być pusta.",
"The path cannot be blank.": "Ścieżka nie może być pusta.",
"The rate limit must be a non-negative number (0: no limit)": "Ograniczenie prędkości musi być nieujemną wartością liczbową (0: brak ograniczeń)",
"The remote device has not accepted sharing this folder.": "Urządzenie zdalne nie wyraziło zgody na udostępnienie tego folderu.",
"The rescan interval must be a non-negative number of seconds.": "Przedział czasowy ponownego skanowania musi być nieujemną liczbą sekund.",
"There are no devices to share this folder with.": "Brak urządzeń, z którymi można współdzielić ten folder.",
"There are no file versions to restore.": "Brak wersji plików, które można przywrócić.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Niewspółdzielone foldery",
"Untrusted": "Niezaufany",
"Up to Date": "Aktualny",
"Updated": "Zaktualizowano",
"Updated {%file%}": "Zaktualizowano {{file}}",
"Upgrade": "Aktualizacja",
"Upgrade To {%version%}": "Aktualizuj do {{version}}",
"Upgrading": "Aktualizowanie",
@@ -435,7 +436,7 @@
"Watching for Changes": "Obserwowanie zmian",
"Watching for changes discovers most changes without periodic scanning.": "Obserwowanie wykrywa większość zmian bez potrzeby okresowego skanowania.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Dodając nowe urządzenie pamiętaj, że musi ono zostać dodane także po drugiej stronie.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Dodając nowy folder pamiętaj, że numer ID jest używany do parowania folderów pomiędzy urządzeniami. Wielkość liter ma znaczenie i musi on być identyczny na wszystkich urządzeniach.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Dodając nowy folder pamiętaj, że identyfikator używany jest do parowania folderów pomiędzy urządzeniami. Wielkość liter ma znaczenie i musi być on identyczny na wszystkich urządzeniach.",
"Yes": "Tak",
"Yesterday": "Wczoraj",
"You can also select one of these nearby devices:": "Możesz również wybrać jedno z pobliskich urządzeń:",

View File

@@ -78,7 +78,7 @@
"Defaults": "Padrões",
"Delete": "Apagar",
"Delete Unexpected Items": "Excluir Itens Inesperados",
"Deleted": "Apagado",
"Deleted {%file%}": "{{file}} excluído",
"Deselect All": "Desmarcar Todos",
"Deselect devices to stop sharing this folder with.": "Desmarque os dispositivos com os quais parar de compartilhar esta pasta.",
"Deselect folders to stop sharing with this device.": "Desmarque as pastas para interromper o compartilhamento com este dispositivo.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "O número de versões deve ser um valor numérico. O campo não pode ficar vazio.",
"The path cannot be blank.": "O caminho não pode ficar vazio.",
"The rate limit must be a non-negative number (0: no limit)": "O limite de velocidade deve ser um número positivo (0: sem limite)",
"The remote device has not accepted sharing this folder.": "O dispositivo remoto não aceitou compartilhar esta pasta.",
"The rescan interval must be a non-negative number of seconds.": "O intervalo entre verificações deve ser um número positivo de segundos.",
"There are no devices to share this folder with.": "Não há dispositivos com os quais compartilhar esta pasta.",
"There are no file versions to restore.": "Não há versões do arquivo para restaurar.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Pastas Não Compartilhadas",
"Untrusted": "Não confiável",
"Up to Date": "Em sincronia",
"Updated": "Atualizado",
"Updated {%file%}": "{{file}} atualizado",
"Upgrade": "Atualização",
"Upgrade To {%version%}": "Atualizar para {{version}}",
"Upgrading": "Atualizando",

View File

@@ -78,7 +78,7 @@
"Defaults": "Predefinições",
"Delete": "Eliminar",
"Delete Unexpected Items": "Eliminar itens inesperados",
"Deleted": "Eliminado",
"Deleted {%file%}": "{{file}} eliminado",
"Deselect All": "Retirar todas as selecções",
"Deselect devices to stop sharing this folder with.": "Retire a selecção para deixar de partilhar a pasta com esses dispositivos.",
"Deselect folders to stop sharing with this device.": "Retire a selecção das pastas para deixar de as partilhar com este dispositivo.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "O número de versões tem que ser um número e não pode estar vazio.",
"The path cannot be blank.": "O caminho não pode estar vazio.",
"The rate limit must be a non-negative number (0: no limit)": "O limite de velocidade tem que ser um número que não seja negativo (0: sem limite)",
"The remote device has not accepted sharing this folder.": "O dispositivo remoto não aceitou a partilha desta pasta.",
"The rescan interval must be a non-negative number of seconds.": "O intervalo entre verificações tem que ser um valor não negativo de segundos.",
"There are no devices to share this folder with.": "Não existem quaisquer dispositivos com os quais se possa partilhar esta pasta.",
"There are no file versions to restore.": "Não existem versões do ficheiro para restaurar.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Pastas não partilhadas",
"Untrusted": "Não fiável",
"Up to Date": "Em sincronia",
"Updated": "Actualizado",
"Updated {%file%}": "{{file}} modificado",
"Upgrade": "Actualizar",
"Upgrade To {%version%}": "Actualizar para {{version}}",
"Upgrading": "Actualizando",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Şterge",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Șters",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Deselect All",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Numărul de versiuni trebuie să fie un număr şi nu poate fi gol.",
"The path cannot be blank.": "Locația nu poate fi goală.",
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Intervalul de rescanare trebuie să nu fie un număr negativ de secunde. ",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "La Zi",
"Updated": "Updated",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Upgrade",
"Upgrade To {%version%}": "Actualizează La Versiunea {{version}}",
"Upgrading": "Se Actualizează",

View File

@@ -78,7 +78,7 @@
"Defaults": "Стандартные параметры",
"Delete": "Удалить",
"Delete Unexpected Items": "Удалить неожиданные элементы",
"Deleted": "Удалено",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Снять выделение",
"Deselect devices to stop sharing this folder with.": "Отмените выбор устройств, чтобы прекратить совместное использование этой папки.",
"Deselect folders to stop sharing with this device.": "Отмените выбор папок, чтобы прекратить совместное использование с этим устройством.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Количество версий должно быть числом и не может быть пустым.",
"The path cannot be blank.": "Путь не может быть пустым.",
"The rate limit must be a non-negative number (0: no limit)": "Скорость должна быть неотрицательным числом (0: нет ограничения)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Интервал пересканирования должен быть неотрицательным количеством секунд.",
"There are no devices to share this folder with.": "Нет устройств, для которых будет доступна эта папка.",
"There are no file versions to restore.": "Нет версий файла для восстановления.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Не общедоступные папки",
"Untrusted": "Ненадёжный",
"Up to Date": "В актуальном состоянии",
"Updated": "Обновлено",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Обновить",
"Upgrade To {%version%}": "Обновить до {{version}}",
"Upgrading": "Обновление",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Delete",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Zmazané",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Odznačiť všetko",
"Deselect devices to stop sharing this folder with.": "Deselect devices to stop sharing this folder with.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Počet verzií musí byť číslo a nemôže byť prázdny.",
"The path cannot be blank.": "Cesta nemôže byť prázdna.",
"The rate limit must be a non-negative number (0: no limit)": "Limit rýchlosti musí byť kladné číslo (0: bez limitu)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"There are no devices to share this folder with.": "There are no devices to share this folder with.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Aktuálne",
"Updated": "Aktualizované",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Aktualizácia",
"Upgrade To {%version%}": "Aktualizovať na {{version}}",
"Upgrading": "Aktualizácia",

View File

@@ -78,7 +78,7 @@
"Defaults": "Privzeti",
"Delete": "Izbriši",
"Delete Unexpected Items": "Izbrišite nepričakovane predmete",
"Deleted": "Izbrisana",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Prekliči vse",
"Deselect devices to stop sharing this folder with.": "Prekliči izbiro naprav, z katerimi ne želiš več deliti mape.",
"Deselect folders to stop sharing with this device.": "Prekliči mape, da se ne delijo več z to napravo.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Število različic mora biti število in ne more biti prazno.",
"The path cannot be blank.": "Pot ne more biti prazna.",
"The rate limit must be a non-negative number (0: no limit)": "Omejitev stopnje odzivnosti mora biti nenegativno število (0: brez omejitve)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Interval skeniranja mora biti pozitivna številka.",
"There are no devices to share this folder with.": "Ni naprav za skupno rabo te mape.",
"There are no file versions to restore.": "Ni različic od datotek za obnoviti.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Mape, ki niso v skupni rabi",
"Untrusted": "Nezaupno",
"Up to Date": "Posodobljeno",
"Updated": "Posodobljeno",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Posodobi",
"Upgrade To {%version%}": "Posodobi na različico {{version}}",
"Upgrading": "Posodabljanje",

View File

@@ -78,9 +78,9 @@
"Defaults": "Standard",
"Delete": "Ta bort",
"Delete Unexpected Items": "Ta bort oväntade objekt",
"Deleted": "Borttagna",
"Deleted {%file%}": "Tog bort {{file}}",
"Deselect All": "Avmarkera alla",
"Deselect devices to stop sharing this folder with.": "Avmarkera enheter för att sluta dela denna mapp med.",
"Deselect devices to stop sharing this folder with.": "Avmarkera enheter som du vill sluta dela denna mapp med.",
"Deselect folders to stop sharing with this device.": "Avmarkera mappar för att sluta dela med denna enhet.",
"Device": "Enhet",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) vill ansluta. Lägg till ny enhet?",
@@ -250,9 +250,9 @@
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning vid givet intervall och misslyckades med att ställa in bevakning av ändringar, försöker igen var 1:e minut:",
"Permanently add it to the ignore list, suppressing further notifications.": "Lägg till det permanent i ignoreringslistan och undertryck ytterligare aviseringar.",
"Permissions": "Behörigheter",
"Please consult the release notes before performing a major upgrade.": "Vänligen läs igenom versionsnyheterna innan du utför en större uppgradering.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Vänligen ställ in en autentiseringsanvändare och ett lösenord för det grafiska användargränssnittet i dialogrutan Inställningar.",
"Please wait": "Vänligen vänta",
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan du utför en större uppgradering.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in en autentiseringsanvändare och ett lösenord för det grafiska användargränssnittet i dialogrutan Inställningar.",
"Please wait": "Vänta",
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix som indikerar att filen kan tas bort om den förhindrar mappborttagning",
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefix som indikerar att mönstret ska matchas utan skiftlägeskänslighet",
"Preparing to Sync": "Förberedelser för synkronisering",
@@ -346,7 +346,7 @@
"Syncthing is upgrading.": "Syncthing uppgraderas.",
"Syncthing now supports automatically reporting crashes to the developers. This feature is enabled by default.": "Syncthing stöder nu automatiskt kraschrapportering till utvecklarna. Denna funktion är aktiverad som standard.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar vara avstängd eller så är det problem med din internetanslutning. Försöker igen...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem med behandlingen av din förfrågan. Vänligen uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem med behandlingen av din förfrågan. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
"Take me back": "Ta mig tillbaka",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Den grafiska gränssnittsadressen åsidosätts av startalternativ. Ändringar här träder inte i kraft så länge åsidosättningen är på plats.",
"The Syncthing Authors": "Syncthing-upphovsmän",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett nummer och kan inte lämnas tomt.",
"The path cannot be blank.": "Sökvägen kan inte vara tom.",
"The rate limit must be a non-negative number (0: no limit)": "Frekvensgränsen måste vara ett icke-negativt tal (0: ingen gräns)",
"The remote device has not accepted sharing this folder.": "Fjärrenheten har inte accepterat delning av den här mappen.",
"The rescan interval must be a non-negative number of seconds.": "Förnyelseintervallet måste vara ett positivt antal sekunder",
"There are no devices to share this folder with.": "Det finns inga enheter att dela denna mapp med.",
"There are no file versions to restore.": "Det finns inga filversioner att återställa.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Odelade mappar",
"Untrusted": "Opålitlig",
"Up to Date": "Uppdaterad",
"Updated": "Uppdaterade",
"Updated {%file%}": "Uppdaterade {{file}}",
"Upgrade": "Uppgradering",
"Upgrade To {%version%}": "Uppgradera till {{version}}",
"Upgrading": "Uppgraderar",
@@ -417,7 +418,7 @@
"Usage reporting is always enabled for candidate releases.": "Användningsrapportering är alltid aktiverad för kandidatutgåvor.",
"Use HTTPS for GUI": "Använd HTTPS för gränssnitt",
"Use notifications from the filesystem to detect changed items.": "Använd aviseringar från filsystemet för att upptäcka ändrade objekt.",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Användarnamn/lösenord har inte ställts in för autentisering av det grafiska gränssnittet. Vänligen överväg att ställa in det.",
"Username/Password has not been set for the GUI authentication. Please consider setting it up.": "Användarnamn/lösenord har inte ställts in för autentisering av det grafiska gränssnittet. Överväg att ställa in det.",
"Version": "Version",
"Versions": "Versioner",
"Versions Path": "Sökväg för versioner",

View File

@@ -78,7 +78,7 @@
"Defaults": "Varsayılanlar",
"Delete": "Sil",
"Delete Unexpected Items": "Beklenmeyen Öğeleri Sil",
"Deleted": "Silinen",
"Deleted {%file%}": "{{file}} silindi",
"Deselect All": "Tüm Seçimi Kaldır",
"Deselect devices to stop sharing this folder with.": "Bu klasörün paylaşımının durdurulacağı cihazların seçimini kaldırın.",
"Deselect folders to stop sharing with this device.": "Bu cihazla paylaşımı durdurulacak klasörlerin seçimini kaldırın.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Sürüm sayısı bir sayı olmak zorundadır ve boş bırakılamaz.",
"The path cannot be blank.": "Yol boş olamaz.",
"The rate limit must be a non-negative number (0: no limit)": "Hız sınırı negatif olmayan bir sayı olmak zorundadır (0: sınır yok)",
"The remote device has not accepted sharing this folder.": "Uzak cihaz bu klasörü paylaşmayı kabul etmedi.",
"The rescan interval must be a non-negative number of seconds.": "Yeniden tarama aralığı negatif olmayan bir saniye sayısı olmak zorundadır.",
"There are no devices to share this folder with.": "Bu klasörün paylaşılacağı cihazlar yok.",
"There are no file versions to restore.": "Geri yüklenecek dosya sürümleri yok.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Paylaşılmayan Klasörler",
"Untrusted": "Güvenilmez",
"Up to Date": "Güncel",
"Updated": "Güncellenen",
"Updated {%file%}": "{{file}} güncellendi",
"Upgrade": "Yükselt",
"Upgrade To {%version%}": "{{version}} Sürümüne Yükselt",
"Upgrading": "Yükseltiliyor",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Видалити",
"Delete Unexpected Items": "Delete Unexpected Items",
"Deleted": "Видалене",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "Зняти вибір з усіх",
"Deselect devices to stop sharing this folder with.": "Зніміть вибір з пристроїв, які не матимуть доступу до цієї директорії.",
"Deselect folders to stop sharing with this device.": "Deselect folders to stop sharing with this device.",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "Кількість версій повинна бути цифрою та не може бути порожньою.",
"The path cannot be blank.": "Шлях не може бути порожнім.",
"The rate limit must be a non-negative number (0: no limit)": "Швидкість має бути додатнім числом.",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "Інтервал повторного сканування повинен бути неід’ємною величиною.",
"There are no devices to share this folder with.": "Відсутні пристрої, які мають доступ до цієї директорії.",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "Unshared Folders",
"Untrusted": "Untrusted",
"Up to Date": "Актуальна версія",
"Updated": "Оновлено",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "Оновлення",
"Upgrade To {%version%}": "Оновити до {{version}}",
"Upgrading": "Оновлення",

View File

@@ -78,7 +78,7 @@
"Defaults": "默认值",
"Delete": "删除",
"Delete Unexpected Items": "删除特殊项目",
"Deleted": "已删除",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "取消全选",
"Deselect devices to stop sharing this folder with.": "反选设备以停止共享此文件夹",
"Deselect folders to stop sharing with this device.": "取消选择文件夹以停止与此设备共享。",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "保留版本数量必须为数字,且不能为空。",
"The path cannot be blank.": "路径不能为空。",
"The rate limit must be a non-negative number (0: no limit)": "传输速度限制为非负整数0 表示不限制)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "扫描间隔单位为秒,且不能为负数。",
"There are no devices to share this folder with.": "没有设备共享此文件夹",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "非共享文件夹",
"Untrusted": "不可信的",
"Up to Date": "同步完成",
"Updated": "已更新",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "更新",
"Upgrade To {%version%}": "升级至版本 {{version}}",
"Upgrading": "升级中",

View File

@@ -78,7 +78,7 @@
"Defaults": "Defaults",
"Delete": "Delete",
"Delete Unexpected Items": "刪除不需要項目",
"Deleted": "已刪除",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "取消全選",
"Deselect devices to stop sharing this folder with.": "反選設備以停止共享此文件夾",
"Deselect folders to stop sharing with this device.": "停止選擇文件夾以停止與此設備共享。",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "保留版本數量必須為數字,且不能為空。",
"The path cannot be blank.": "路徑不能為空。",
"The rate limit must be a non-negative number (0: no limit)": "傳輸速度限制為非負整數0 表示不限制)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "掃瞄間隔單位為秒,且不能為負數。",
"There are no devices to share this folder with.": "沒有設備共享此文件夾",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "未共享的文件夾",
"Untrusted": "不信任",
"Up to Date": "同步完成",
"Updated": "已更新",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "更新",
"Upgrade To {%version%}": "升級至版本 {{version}}",
"Upgrading": "升級中",

View File

@@ -78,7 +78,7 @@
"Defaults": "預設",
"Delete": "刪除",
"Delete Unexpected Items": "刪除不預期的項目",
"Deleted": "已刪除",
"Deleted {%file%}": "Deleted {{file}}",
"Deselect All": "取消選取全部",
"Deselect devices to stop sharing this folder with.": "取消選擇裝置以停用與其共享資料夾。",
"Deselect folders to stop sharing with this device.": "取消選擇資料夾以停用與此裝置共享。",
@@ -378,6 +378,7 @@
"The number of versions must be a number and cannot be blank.": "每個檔案要保留的舊版本數量必須是數字且不能為空白。",
"The path cannot be blank.": "路徑不能空白。",
"The rate limit must be a non-negative number (0: no limit)": "限制速率必須為非負的數字 (0: 不設限制)",
"The remote device has not accepted sharing this folder.": "The remote device has not accepted sharing this folder.",
"The rescan interval must be a non-negative number of seconds.": "重新掃描間隔必須為一個非負數的秒數。",
"There are no devices to share this folder with.": "沒有裝置可以共享此資料夾。",
"There are no file versions to restore.": "There are no file versions to restore.",
@@ -408,7 +409,7 @@
"Unshared Folders": "未共享的資料夾",
"Untrusted": "不信任",
"Up to Date": "最新",
"Updated": "已更新",
"Updated {%file%}": "Updated {{file}}",
"Upgrade": "更新",
"Upgrade To {%version%}": "更新至 {{version}}",
"Upgrading": "正在升級",

View File

@@ -532,7 +532,9 @@
</tr>
<tr>
<th><span class="fas fa-fw fa-share-alt"></span>&nbsp;<span translate>Shared With</span></th>
<td class="text-right" ng-attr-title="{{sharesFolder(folder)}}">{{sharesFolder(folder)}}</td>
<td class="text-right">
<span tooltip data-original-title="{{sharesFolder(folder)}} {{folderHasUnacceptedDevices(folder) ? '<br/>(<sup>1</sup>' + ('The remote device has not accepted sharing this folder.' | translate) + ')' : ''}}" ng-bind-html="sharesFolder(folder)"></span>
</td>
</tr>
<tr ng-if="folderStats[folder.id].lastScan">
<th><span class="far fa-fw fa-clock"></span>&nbsp;<span translate>Last Scan</span></th>
@@ -545,9 +547,8 @@
<th><span class="fas fa-fw fa-exchange-alt"></span>&nbsp;<span translate>Latest Change</span></th>
<td class="text-right">
<span tooltip data-original-title="{{folderStats[folder.id].lastFile.filename}} @ {{folderStats[folder.id].lastFile.at | date:'yyyy-MM-dd HH:mm:ss'}}">
<span translate ng-if="!folderStats[folder.id].lastFile.deleted">Updated</span>
<span translate ng-if="folderStats[folder.id].lastFile.deleted">Deleted</span>
{{folderStats[folder.id].lastFile.filename | basename}}
<span translate translate-value-file="{{folderStats[folder.id].lastFile.filename | basename}}" ng-if="!folderStats[folder.id].lastFile.deleted">Updated {%file%}</span>
<span translate translate-value-file="{{folderStats[folder.id].lastFile.filename | basename}}" ng-if="folderStats[folder.id].lastFile.deleted">Deleted {%file%}</span>
</span>
</td>
</tr>
@@ -667,9 +668,9 @@
<th><span class="fas fa-fw fa-sitemap"></span>&nbsp;<span translate>Listeners</span></th>
<td class="text-right">
<span class="data" tooltip data-original-title="{{'Show detailed listener status' | translate}}.">
<a href="" ng-class="{'text-success': listenersFailed.length == 0, 'text-danger': listenersFailed.length == listenersTotal}" ng-click="showListenerStatus()">
{{listenersTotal-listenersFailed.length}}/{{listenersTotal}}
</a>
<a href="" ng-class="{'text-success': listenersTotal > 0 && listenersFailed.length == 0, 'text-danger': listenersTotal > 0 && listenersFailed.length == listenersTotal}" ng-click="showListenerStatus()">
{{listenersTotal-listenersFailed.length}}/{{listenersTotal}}
</a>
</span>
</td>
</tr>
@@ -678,8 +679,8 @@
<td class="text-right">
<span class="data" tooltip data-original-title="{{'Show detailed discovery status' | translate}}.">
<a href="" ng-class="{'text-success': discoveryFailed.length == 0, 'text-danger': discoveryFailed.length == discoveryTotal}" ng-click="showDiscoveryStatus()">
{{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}
</a>
{{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}
</a>
</span>
</td>
</tr>
@@ -833,7 +834,9 @@
</tr>
<tr ng-if="deviceFolders(deviceCfg).length > 0">
<th><span class="fas fa-fw fa-folder"></span>&nbsp;<span translate>Folders</span></th>
<td class="text-right" ng-attr-title="{{deviceFolders(deviceCfg).map(folderLabel).join(', ')}}">{{deviceFolders(deviceCfg).map(folderLabel).join(", ")}}</td>
<td class="text-right">
<span tooltip data-original-title="{{sharedFolders(deviceCfg)}} {{deviceHasUnacceptedFolders(deviceCfg) ? '<br/>(<sup>1</sup>' + ('The remote device has not accepted sharing this folder.' | translate) + ')' : '' }}" ng-bind-html="sharedFolders(deviceCfg)"></span>
</td>
</tr>
<tr ng-if="deviceCfg.remoteGUIPort > 0">
<th><span class="fas fa-fw fa-desktop"></span>&nbsp;<span translate>Remote GUI</span></th>

View File

@@ -19,7 +19,7 @@
<h4 class="text-center" translate>The Syncthing Authors</h4>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, 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, Syncthing Release Automation, Tomasz Wilczyński, Wulf Weich, dependabot-preview[bot], dependabot[bot], greatroar, Aaron Bieber, Adam Piggott, Adel Qalieh, Alan Pope, Alberto Donato, Alessandro G., Alex Lindeman, Alex Xu, Aman Gupta, Andrew Dunham, Andrew Meyer, Andrew Rabert, Andrey D, Anjan Momi, Antoine Lamielle, Anur, 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, Benjamin Nater, Benno Fünfstück, Benny Ng, Boqin Qin, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Chris Tonkinson, Christian Prescott, Colin Kennedy, Cromefire_, Cyprien Devillez, Dale Visser, Dan, Daniel Barczyk, 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, Kebin Liu, 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, 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, Ryan Qian, Sacheendra Talluri, Scott Klupfel, Shaarad Dalvi, Simon Mwepu, Sly_tom_cat, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Syncthing Automation, 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], derekriemer, desbma, georgespatton, ghjklw, ignacy123, janost, jaseg, jelle van der Waa, jtagcat, klemens, marco-m, mclang, mv1005, otbutz, overkill, perewa, rubenbe, villekalliomaki, 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, Syncthing Release Automation, Tomasz Wilczyński, Wulf Weich, dependabot-preview[bot], dependabot[bot], greatroar, Aaron Bieber, Adam Piggott, Adel Qalieh, Alan Pope, Alberto Donato, Alessandro G., Alex Lindeman, Alex Xu, Aman Gupta, Andrew Dunham, Andrew Meyer, Andrew Rabert, Andrey D, Anjan Momi, Antoine Lamielle, Anur, 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, Benjamin Nater, Benno Fünfstück, Benny Ng, Boqin Qin, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chih-Hsuan Yen, Choongkyu, Chris Howie, Chris Joel, Chris Tonkinson, Christian Prescott, Colin Kennedy, Cromefire_, Cyprien Devillez, Dale Visser, Dan, Daniel Barczyk, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Domenic Horner, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Eng Zer Jun, 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, Greg, 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, Jeffery To, 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, Kebin Liu, 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, Max Schulze, MaximAL, Maxime Thirouin, MichaIng, Michael Jephcote, Michael Rienstra, Michael Tilli, Mike Boone, MikeLund, MikolajTwarog, Mingxuan Lin, Naveen, 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, Ryan Qian, Sacheendra Talluri, Scott Klupfel, Shaarad Dalvi, Simon Mwepu, Sly_tom_cat, Stefan Kuntz, Steven Eckhoff, Suhas Gundimeda, Syncthing Automation, 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], derekriemer, desbma, georgespatton, ghjklw, ignacy123, janost, jaseg, jelle van der Waa, jtagcat, klemens, marco-m, mclang, mv1005, otbutz, overkill, perewa, red_led, rubenbe, villekalliomaki, wangguoliang, wouter bolsterlee, xarx00, xjtdy888, 佛跳墙
</div>
</div>
<hr />

View File

@@ -1,14 +1,14 @@
<div class="col-md-6 checkbox">
<label for="sharedwith-{{id}}">
<input id="sharedwith-{{id}}" ng-model="selected[id]" type="checkbox" />
<span tooltip data-original-title="{{id}}">{{label}}</span>
<span tooltip data-original-title="{{id}}" ng-bind-html="label"></span>
</label>
</div>
<div class="col-md-6">
<div class="input-group">
<span class="input-group-addon" ng-switch="folderType !== 'receiveencrypted' && !encryptionPasswords[id]">
<span ng-switch-when='true' class="fas fa-fw fa-unlock" />
<span ng-switch-default class="fas fa-fw fa-lock" />
<span ng-switch-when='true' class="fas fa-fw fa-unlock"></span>
<span ng-switch-default class="fas fa-fw fa-lock"></span>
</span>
<span ng-switch="folderType === 'receiveencrypted'">
<span ng-switch-when='true'>
@@ -30,10 +30,10 @@
</span>
<span ng-switch="selected[id] && folderType !== 'receiveencrypted'" class="input-group-addon">
<span ng-switch-when='true'>
<span class="button fas fa-fw fa-eye" ng-click="togglePasswordVisibility()" />
<span class="button fas fa-fw fa-eye" ng-click="togglePasswordVisibility()"></span>
</span>
<span ng-switch-default>
<span class="button fas fa-fw fa-eye" disabled />
<span class="button fas fa-fw fa-eye" disabled></span>
</span>
</span>
</div>

View File

@@ -64,19 +64,27 @@ angular.module('syncthing.core')
PENDING_DEVICES_CHANGED: 'PendingDevicesChanged', // Emitted when pending devices were added / updated (connection from unknown ID) or removed (device is ignored or added)
DEVICE_PAUSED: 'DevicePaused', // Emitted when a device has been paused
DEVICE_RESUMED: 'DeviceResumed', // Emitted when a device has been resumed
CLUSTER_CONFIG_RECEIVED: 'ClusterConfigReceived', // Emitted when receiving a remote device's cluster config
DOWNLOAD_PROGRESS: 'DownloadProgress', // Emitted during file downloads for each folder for each file
FAILURE: 'Failure', // Specific errors sent to the usage reporting server for diagnosis
FOLDER_COMPLETION: 'FolderCompletion', //Emitted when the local or remote contents for a folder changes
FOLDER_REJECTED: 'FolderRejected', // DEPRECATED: Emitted when a device sends index information for a folder we do not have, or have but do not share with the device in question
PENDING_FOLDERS_CHANGED: 'PendingFoldersChanged', // Emitted when pending folders were added / updated (offered by some device, but not shared to them) or removed (folder ignored or added or no longer offered from the remote device)
FOLDER_SUMMARY: 'FolderSummary', // Emitted when folder contents have changed locally
ITEM_FINISHED: 'ItemFinished', // Generated when Syncthing ends synchronizing a file to a newer version
ITEM_STARTED: 'ItemStarted', // Generated when Syncthing begins synchronizing a file to a newer version
LISTEN_ADDRESSES_CHANGED: 'ListenAddressesChanged', // Listen address resolution has changed.
LOCAL_CHANGE_DETECTED: 'LocalChangeDetected', // Generated upon scan whenever the local disk has discovered an updated file from the previous scan.
LOCAL_INDEX_UPDATED: 'LocalIndexUpdated', // Generated when the local index information has changed, due to synchronizing one or more items from the cluster or discovering local changes during a scan
LOGIN_ATTEMPT: 'LoginAttempt', // Emitted on every login attempt when authentication is enabled for the GUI.
REMOTE_CHANGE_DETECTED: 'RemoteChangeDetected', // Generated upon scan whenever a file is locally updated due to a remote change.
REMOTE_DOWNLOAD_PROGRESS: 'RemoteDownloadProgress', // DownloadProgress message received from a connected remote device.
REMOTE_INDEX_UPDATED: 'RemoteIndexUpdated', // Generated each time new index information is received from a device
STARTING: 'Starting', // Emitted exactly once, when Syncthing starts, before parsing configuration etc
STARTUP_COMPLETED: 'StartupCompleted', // Emitted exactly once, when initialization is complete and Syncthing is ready to start exchanging data with other devices
STATE_CHANGED: 'StateChanged', // Emitted when a folder changes state
FOLDER_ERRORS: 'FolderErrors', // Emitted when a folder has errors preventing a full sync
FOLDER_WATCH_STATE_CHANGED: 'FolderWatchStateChanged', // Watcher routine encountered a new error, or a previous error disappeared after retrying.
FOLDER_SCAN_PROGRESS: 'FolderScanProgress', // Emitted every ScanProgressIntervalS seconds, indicating how far into the scan it is at.
FOLDER_PAUSED: 'FolderPaused', // Emitted when a folder is paused
FOLDER_RESUMED: 'FolderResumed', // Emitted when a folder is resumed

View File

@@ -178,7 +178,8 @@ angular.module('syncthing.core')
console.log('HTTPError', arg);
online = false;
if (!restarting) {
// We sometimes get arg == null from angularjs - no idea why
if (!restarting && arg) {
if (arg.status === 0) {
// A network error, not an HTTP error
$scope.$emit(Events.OFFLINE);
@@ -2365,17 +2366,37 @@ angular.module('syncthing.core')
+ '&device=' + encodeURIComponent(deviceID));
};
$scope.deviceNameMarkUnaccepted = function (deviceID, folderID) {
var name = $scope.deviceName($scope.devices[deviceID]);
// Add footnote if sharing was not accepted on the remote device
if (deviceID in $scope.completion && folderID in $scope.completion[deviceID] && $scope.completion[deviceID][folderID].remoteState == 'notSharing') {
name += '<sup>1</sup>';
}
return name;
};
$scope.sharesFolder = function (folderCfg) {
var names = [];
folderCfg.devices.forEach(function (device) {
if (device.deviceID !== $scope.myID) {
names.push($scope.deviceName($scope.devices[device.deviceID]));
names.push($scope.deviceNameMarkUnaccepted(device.deviceID, folderCfg.id));
}
});
names.sort();
return names.join(", ");
};
$scope.folderHasUnacceptedDevices = function (folderCfg) {
for (var deviceID in $scope.completion) {
if (deviceID in $scope.devices
&& folderCfg.id in $scope.completion[deviceID]
&& $scope.completion[deviceID][folderCfg.id].remoteState == 'notSharing') {
return true;
}
}
return false;
};
$scope.deviceFolders = function (deviceCfg) {
var folders = [];
$scope.folderList().forEach(function (folder) {
@@ -2397,6 +2418,36 @@ angular.module('syncthing.core')
return label && label.length > 0 ? label : folderID;
};
$scope.folderLabelMarkUnaccepted = function (folderID, deviceID) {
var label = $scope.folderLabel(folderID);
// Add footnote if sharing was not accepted on the remote device
if (deviceID in $scope.completion && folderID in $scope.completion[deviceID] && $scope.completion[deviceID][folderID].remoteState == 'notSharing') {
label += '<sup>1</sup>';
}
return label;
};
$scope.sharedFolders = function (deviceCfg) {
var labels = [];
$scope.deviceFolders(deviceCfg).forEach(function (folderID) {
labels.push($scope.folderLabelMarkUnaccepted(folderID, deviceCfg.deviceID));
});
return labels.join(', ');
};
$scope.deviceHasUnacceptedFolders = function (deviceCfg) {
if (!(deviceCfg.deviceID in $scope.completion)) {
return false;
}
for (var folderID in $scope.completion[deviceCfg.deviceID]) {
if (folderID in $scope.folders
&& $scope.completion[deviceCfg.deviceID][folderID].remoteState == 'notSharing') {
return true;
}
}
return false;
};
$scope.deleteFolder = function (id) {
hideFolderModal();
if ($scope.currentFolder._editing != "existing") {

View File

@@ -83,8 +83,11 @@
<a href="#" ng-click="selectAllSharedFolders(false)" translate>Deselect All</a></small>
</p>
<div class="form-group" ng-repeat="folder in currentSharing.shared">
<share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{folder.id}}" label="{{folderLabel(folder.id)}}" folder-type="{{folder.type}}" untrusted="currentDevice.untrusted" />
<share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{folder.id}}" label="{{folderLabelMarkUnaccepted(folder.id, currentDevice.deviceID)}}" folder-type="{{folder.type}}" untrusted="currentDevice.untrusted" />
</div>
<p class="help-block" ng-if="deviceHasUnacceptedFolders(currentDevice)">
<sup>1</sup> <span translate>The remote device has not accepted sharing this folder.</span>
</p>
</div>
<div class="form-horizontal" ng-if="currentSharing.unrelated.length">
<label translate for="folders">Unshared Folders</label>

View File

@@ -56,8 +56,11 @@
<a href="#" ng-click="selectAllSharedDevices(false)" translate>Deselect All</a></small>
</p>
<div class="form-group" ng-repeat="device in currentSharing.shared">
<share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{device.deviceID}}" label="{{deviceName(device)}}" folder-type="{{currentFolder.type}}" untrusted="device.untrusted || pendingIsRemoteEncrypted(currentFolder.id, device.deviceID)" />
<share-template selected="currentSharing.selected" encryption-passwords="currentSharing.encryptionPasswords" id="{{device.deviceID}}" label="{{deviceNameMarkUnaccepted(device.deviceID, currentFolder.id)}}" folder-type="{{currentFolder.type}}" untrusted="device.untrusted || pendingIsRemoteEncrypted(currentFolder.id, device.deviceID)" />
</div>
<p class="help-block" ng-if="folderHasUnacceptedDevices(currentFolder)">
<sup>1</sup> <span translate>The remote device has not accepted sharing this folder.</span>
</p>
</div>
<div class="form-horizontal" ng-if="currentSharing.unrelated.length || otherDevices().length <= 0">
<label translate>Unshared Devices</label>

View File

@@ -59,6 +59,8 @@ import (
)
const (
// Default mask excludes these very noisy event types to avoid filling the pipe.
// FIXME: ItemStarted and ItemFinished should be excluded for the same reason.
DefaultEventMask = events.AllEvents &^ events.LocalChangeDetected &^ events.RemoteChangeDetected
DiskEventMask = events.LocalChangeDetected | events.RemoteChangeDetected
EventSubBufferSize = 1000

View File

@@ -35,6 +35,7 @@ import (
"github.com/syncthing/syncthing/lib/locations"
"github.com/syncthing/syncthing/lib/logger"
loggermocks "github.com/syncthing/syncthing/lib/logger/mocks"
"github.com/syncthing/syncthing/lib/model"
modelmocks "github.com/syncthing/syncthing/lib/model/mocks"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/svcutil"
@@ -260,7 +261,7 @@ func TestAPIServiceRequests(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer cancel()
t.Cleanup(cancel)
cases := []httpTestCase{
// /rest/db
@@ -298,6 +299,12 @@ func TestAPIServiceRequests(t *testing.T) {
Type: "application/json",
Prefix: "null",
},
{
URL: "/rest/db/status?folder=default",
Code: 200,
Type: "application/json",
Prefix: "",
},
// /rest/stats
{
@@ -466,14 +473,17 @@ func TestAPIServiceRequests(t *testing.T) {
}
for _, tc := range cases {
t.Log("Testing", tc.URL, "...")
testHTTPRequest(t, baseURL, tc, testAPIKey)
t.Run(cases[0].URL, func(t *testing.T) {
testHTTPRequest(t, baseURL, tc, testAPIKey)
})
}
}
// testHTTPRequest tries the given test case, comparing the result code,
// content type, and result prefix.
func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase, apikey string) {
// Should not be parallelized, as that just causes timeouts eventually with more test-cases
timeout := time.Second
if tc.Timeout > 0 {
timeout = tc.Timeout
@@ -608,7 +618,7 @@ func startHTTP(cfg config.Wrapper) (string, context.CancelFunc, error) {
}
addrChan := make(chan string)
mockedSummary := &modelmocks.FolderSummaryService{}
mockedSummary.SummaryReturns(map[string]interface{}{"mocked": true}, nil)
mockedSummary.SummaryReturns(new(model.FolderSummary), nil)
// Instantiate the API service
urService := ur.New(cfg, m, connections, false)
@@ -1136,11 +1146,7 @@ func TestBrowse(t *testing.T) {
pathSep := string(os.PathSeparator)
tmpDir, err := os.MkdirTemp("", "syncthing")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
tmpDir := t.TempDir()
if err := os.Mkdir(filepath.Join(tmpDir, "dir"), 0755); err != nil {
t.Fatal(err)

View File

@@ -457,7 +457,7 @@ func TestIssue1262(t *testing.T) {
t.Fatal(err)
}
actual := cfg.Folders()["test"].Filesystem().URI()
actual := cfg.Folders()["test"].Filesystem(nil).URI()
expected := `e:\`
if actual != expected {
@@ -494,7 +494,7 @@ func TestFolderPath(t *testing.T) {
Path: "~/tmp",
}
realPath := folder.Filesystem().URI()
realPath := folder.Filesystem(nil).URI()
if !filepath.IsAbs(realPath) {
t.Error(realPath, "should be absolute")
}
@@ -504,13 +504,10 @@ func TestFolderPath(t *testing.T) {
}
func TestFolderCheckPath(t *testing.T) {
n, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
n := t.TempDir()
testFs := fs.NewFilesystem(fs.FilesystemTypeBasic, n)
err = os.MkdirAll(filepath.Join(n, "dir", ".stfolder"), os.FileMode(0777))
err := os.MkdirAll(filepath.Join(n, "dir", ".stfolder"), os.FileMode(0777))
if err != nil {
t.Fatal(err)
}
@@ -602,11 +599,7 @@ func TestWindowsLineEndings(t *testing.T) {
t.Skip("Windows specific")
}
dir, err := os.MkdirTemp("", "syncthing-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
dir := t.TempDir()
path := filepath.Join(dir, "config.xml")
os.Remove(path)

View File

@@ -16,6 +16,7 @@ import (
"github.com/shirou/gopsutil/v3/disk"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/util"
@@ -42,24 +43,29 @@ func (f FolderConfiguration) Copy() FolderConfiguration {
return c
}
func (f FolderConfiguration) Filesystem() fs.Filesystem {
// Filesystem creates a filesystem for the path and options of this folder.
// The fset parameter may be nil, in which case no mtime handling on top of
// the fileystem is provided.
func (f FolderConfiguration) Filesystem(fset *db.FileSet) fs.Filesystem {
// This is intentionally not a pointer method, because things like
// cfg.Folders["default"].Filesystem() should be valid.
var opts []fs.Option
// cfg.Folders["default"].Filesystem(nil) should be valid.
opts := make([]fs.Option, 0, 3)
if f.FilesystemType == fs.FilesystemTypeBasic && f.JunctionsAsDirs {
opts = append(opts, new(fs.OptionJunctionsAsDirs))
}
filesystem := fs.NewFilesystem(f.FilesystemType, f.Path, opts...)
if !f.CaseSensitiveFS {
filesystem = fs.NewCaseFilesystem(filesystem)
opts = append(opts, new(fs.OptionDetectCaseConflicts))
}
return filesystem
if fset != nil {
opts = append(opts, fset.MtimeOption())
}
return fs.NewFilesystem(f.FilesystemType, f.Path, opts...)
}
func (f FolderConfiguration) ModTimeWindow() time.Duration {
dur := time.Duration(f.RawModTimeWindowS) * time.Second
if f.RawModTimeWindowS < 1 && runtime.GOOS == "android" {
if usage, err := disk.Usage(f.Filesystem().URI()); err != nil {
if usage, err := disk.Usage(f.Filesystem(nil).URI()); err != nil {
dur = 2 * time.Second
l.Debugf(`Detecting FS at "%v" on android: Setting mtime window to 2s: err == "%v"`, f.Path, err)
} else if strings.HasPrefix(strings.ToLower(usage.Fstype), "ext2") || strings.HasPrefix(strings.ToLower(usage.Fstype), "ext3") || strings.HasPrefix(strings.ToLower(usage.Fstype), "ext4") {
@@ -89,7 +95,7 @@ func (f *FolderConfiguration) CreateMarker() error {
// begin with.
permBits = 0700
}
fs := f.Filesystem()
fs := f.Filesystem(nil)
err := fs.Mkdir(DefaultMarkerName, permBits)
if err != nil {
return err
@@ -106,7 +112,7 @@ func (f *FolderConfiguration) CreateMarker() error {
// CheckPath returns nil if the folder root exists and contains the marker file
func (f *FolderConfiguration) CheckPath() error {
fi, err := f.Filesystem().Stat(".")
fi, err := f.Filesystem(nil).Stat(".")
if err != nil {
if !fs.IsNotExist(err) {
return err
@@ -124,7 +130,7 @@ func (f *FolderConfiguration) CheckPath() error {
return ErrPathNotDirectory
}
_, err = f.Filesystem().Stat(f.MarkerName)
_, err = f.Filesystem(nil).Stat(f.MarkerName)
if err != nil {
if !fs.IsNotExist(err) {
return err
@@ -145,7 +151,7 @@ func (f *FolderConfiguration) CreateRoot() (err error) {
permBits = 0700
}
filesystem := f.Filesystem()
filesystem := f.Filesystem(nil)
if _, err = filesystem.Stat("."); fs.IsNotExist(err) {
err = filesystem.MkdirAll(".", permBits)
@@ -256,7 +262,7 @@ func (f *FolderConfiguration) CheckAvailableSpace(req uint64) error {
if val <= 0 {
return nil
}
fs := f.Filesystem()
fs := f.Filesystem(nil)
usage, err := fs.Usage(".")
if err != nil {
return nil

View File

@@ -199,7 +199,7 @@ func migrateToConfigV23(cfg *Configuration) {
// marker name in later versions.
for i := range cfg.Folders {
fs := cfg.Folders[i].Filesystem()
fs := cfg.Folders[i].Filesystem(nil)
// Invalid config posted, or tests.
if fs == nil {
continue
@@ -235,18 +235,18 @@ func migrateToConfigV21(cfg *Configuration) {
switch folder.Versioning.Type {
case "simple", "trashcan":
// Clean out symlinks in the known place
cleanSymlinks(folder.Filesystem(), ".stversions")
cleanSymlinks(folder.Filesystem(nil), ".stversions")
case "staggered":
versionDir := folder.Versioning.Params["versionsPath"]
if versionDir == "" {
// default place
cleanSymlinks(folder.Filesystem(), ".stversions")
cleanSymlinks(folder.Filesystem(nil), ".stversions")
} else if filepath.IsAbs(versionDir) {
// absolute
cleanSymlinks(fs.NewFilesystem(fs.FilesystemTypeBasic, versionDir), ".")
} else {
// relative to folder
cleanSymlinks(folder.Filesystem(), versionDir)
cleanSymlinks(folder.Filesystem(nil), versionDir)
}
}
}

View File

@@ -7,10 +7,12 @@
package connections
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"math/rand"
"net"
"net/url"
@@ -21,6 +23,7 @@ import (
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/protocol"
@@ -294,7 +297,7 @@ func TestNextDialRegistryCleanup(t *testing.T) {
}
}
func BenchmarkConnections(pb *testing.B) {
func BenchmarkConnections(b *testing.B) {
addrs := []string{
"tcp://127.0.0.1:0",
"quic://127.0.0.1:0",
@@ -315,9 +318,13 @@ func BenchmarkConnections(pb *testing.B) {
}
for _, addr := range addrs {
for _, sz := range sizes {
data := make([]byte, sz)
if _, err := rand.Read(data); err != nil {
b.Fatal(err)
}
for _, direction := range []string{"cs", "sc"} {
proto := strings.SplitN(addr, ":", 2)[0]
pb.Run(fmt.Sprintf("%s_%d_%s", proto, sz, direction), func(b *testing.B) {
b.Run(fmt.Sprintf("%s_%d_%s", proto, sz, direction), func(b *testing.B) {
if proto == "relay" && !haveRelay {
b.Skip("could not connect to relay")
}
@@ -325,61 +332,79 @@ func BenchmarkConnections(pb *testing.B) {
if direction == "sc" {
server, client = client, server
}
data := make([]byte, sz)
if _, err := rand.Read(data); err != nil {
b.Fatal(err)
}
total := 0
wg := sync.NewWaitGroup()
b.ResetTimer()
for i := 0; i < b.N; i++ {
wg := sync.NewWaitGroup()
wg.Add(2)
errC := make(chan error, 2)
go func() {
if err := sendMsg(client, data); err != nil {
b.Fatal(err)
if _, err := client.Write(data); err != nil {
errC <- err
return
}
wg.Done()
}()
go func() {
if err := recvMsg(server, data); err != nil {
b.Fatal(err)
if _, err := io.ReadFull(server, data); err != nil {
errC <- err
return
}
total += sz
wg.Done()
}()
wg.Wait()
close(errC)
err := <-errC
if err != nil {
b.Fatal(err)
}
}
b.ReportAllocs()
b.SetBytes(int64(total / b.N))
})
})
}
}
}
}
func sendMsg(c internalConn, buf []byte) error {
n, err := c.Write(buf)
if n != len(buf) || err != nil {
return err
func TestConnectionEstablishment(t *testing.T) {
addrs := []string{
"tcp://127.0.0.1:0",
"quic://127.0.0.1:0",
}
send := make([]byte, 128<<10)
if _, err := rand.Read(send); err != nil {
t.Fatal(err)
}
for _, addr := range addrs {
proto := strings.SplitN(addr, ":", 2)[0]
t.Run(proto, func(t *testing.T) {
withConnectionPair(t, addr, func(client, server internalConn) {
if _, err := client.Write(send); err != nil {
t.Fatal(err)
}
recv := make([]byte, len(send))
if _, err := io.ReadFull(server, recv); err != nil {
t.Fatal(err)
}
if !bytes.Equal(recv, send) {
t.Fatal("data mismatch")
}
})
})
}
return nil
}
func recvMsg(c internalConn, buf []byte) error {
for read := 0; read != len(buf); {
n, err := c.Read(buf)
read += n
if err != nil {
return err
}
}
return nil
}
func withConnectionPair(b *testing.B, connUri string, h func(client, server internalConn)) {
func withConnectionPair(b interface{ Fatal(...interface{}) }, connUri string, h func(client, server internalConn)) {
// Root of the service tree.
supervisor := suture.New("main", suture.Spec{
PassThroughPanics: true,
@@ -414,7 +439,7 @@ func withConnectionPair(b *testing.B, connUri string, h func(client, server inte
}
natSvc := nat.NewService(deviceId, wcfg)
conns := make(chan internalConn, 1)
listenSvc := lf.New(uri, wcfg, tlsCfg, conns, natSvc)
listenSvc := lf.New(uri, wcfg, tlsCfg, conns, natSvc, registry.New())
supervisor.Add(listenSvc)
var addr *url.URL
@@ -433,7 +458,8 @@ func withConnectionPair(b *testing.B, connUri string, h func(client, server inte
if err != nil {
b.Fatal(err)
}
dialer := df.New(cfg.Options, tlsCfg)
// Purposely using a different registry: Don't want to reuse port between dialer and listener on the same device
dialer := df.New(cfg.Options, tlsCfg, registry.New())
// Relays might take some time to register the device, so dial multiple times
clientConn, err := dialer.Dial(ctx, deviceId, addr)
@@ -447,19 +473,22 @@ func withConnectionPair(b *testing.B, connUri string, h func(client, server inte
}
}
data := []byte("hello")
// Quic does not start a stream until some data is sent through, so send something for the AcceptStream
// to fire on the other side.
if err := sendMsg(clientConn, data); err != nil {
send := []byte("hello")
if _, err := clientConn.Write(send); err != nil {
b.Fatal(err)
}
serverConn := <-conns
if err := recvMsg(serverConn, data); err != nil {
recv := make([]byte, len(send))
if _, err := io.ReadFull(serverConn, recv); err != nil {
b.Fatal(err)
}
if !bytes.Equal(recv, send) {
b.Fatal("data mismatch")
}
h(clientConn, serverConn)
@@ -467,7 +496,7 @@ func withConnectionPair(b *testing.B, connUri string, h func(client, server inte
_ = serverConn.Close()
}
func mustGetCert(b *testing.B) tls.Certificate {
func mustGetCert(b interface{ Fatal(...interface{}) }) tls.Certificate {
cert, err := tlsutil.NewCertificateInMemory("bench", 10)
if err != nil {
b.Fatal(err)

View File

@@ -41,6 +41,7 @@ func init() {
type quicDialer struct {
commonDialer
registry *registry.Registry
}
func (d *quicDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL) (internalConn, error) {
@@ -58,7 +59,7 @@ func (d *quicDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL
// Given we always pass the connection to quic, it assumes it's a remote connection it never closes it,
// So our wrapper around it needs to close it, but it only needs to close it if it's not the listening connection.
var createdConn net.PacketConn
listenConn := registry.Get(uri.Scheme, packetConnUnspecified)
listenConn := d.registry.Get(uri.Scheme, packetConnUnspecified)
if listenConn != nil {
conn = listenConn.(net.PacketConn)
} else {
@@ -96,7 +97,7 @@ func (d *quicDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL
type quicDialerFactory struct{}
func (quicDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Config) genericDialer {
func (quicDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Config, registry *registry.Registry) genericDialer {
// So the idea is that we should probably try dialing every 20 seconds.
// However it would still be nice if this was adjustable/proportional to ReconnectIntervalS
// But prevent something silly like 1/3 = 0 etc.
@@ -104,10 +105,13 @@ func (quicDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Confi
if quicInterval < 10 {
quicInterval = 10
}
return &quicDialer{commonDialer{
reconnectInterval: time.Duration(quicInterval) * time.Second,
tlsCfg: tlsCfg,
}}
return &quicDialer{
commonDialer: commonDialer{
reconnectInterval: time.Duration(quicInterval) * time.Second,
tlsCfg: tlsCfg,
},
registry: registry,
}
}
func (quicDialerFactory) Priority() int {

View File

@@ -40,11 +40,12 @@ type quicListener struct {
onAddressesChangedNotifier
uri *url.URL
cfg config.Wrapper
tlsCfg *tls.Config
conns chan internalConn
factory listenerFactory
uri *url.URL
cfg config.Wrapper
tlsCfg *tls.Config
conns chan internalConn
factory listenerFactory
registry *registry.Registry
address *url.URL
laddr net.Addr
@@ -100,8 +101,8 @@ func (t *quicListener) serve(ctx context.Context) error {
go svc.Serve(ctx)
registry.Register(t.uri.Scheme, conn)
defer registry.Unregister(t.uri.Scheme, conn)
t.registry.Register(t.uri.Scheme, conn)
defer t.registry.Unregister(t.uri.Scheme, conn)
listener, err := quic.Listen(conn, t.tlsCfg, quicConfig)
if err != nil {
@@ -217,13 +218,14 @@ func (f *quicListenerFactory) Valid(config.Configuration) error {
return nil
}
func (f *quicListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
func (f *quicListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service, registry *registry.Registry) genericListener {
l := &quicListener{
uri: fixupPort(uri, config.DefaultQUICPort),
cfg: cfg,
tlsCfg: tlsCfg,
conns: conns,
factory: f,
uri: fixupPort(uri, config.DefaultQUICPort),
cfg: cfg,
tlsCfg: tlsCfg,
conns: conns,
factory: f,
registry: registry,
}
l.ServiceWithError = svcutil.AsService(l.serve, l.String())
l.nat.Store(stun.NATUnknown)

View File

@@ -36,7 +36,7 @@ func quicNetwork(uri *url.URL) string {
}
type quicTlsConn struct {
quic.Session
quic.Connection
quic.Stream
// If we created this connection, we should be the ones closing it.
createdConn net.PacketConn
@@ -44,7 +44,7 @@ type quicTlsConn struct {
func (q *quicTlsConn) Close() error {
sterr := q.Stream.Close()
seerr := q.Session.CloseWithError(0, "closing")
seerr := q.Connection.CloseWithError(0, "closing")
var pcerr error
if q.createdConn != nil {
pcerr = q.createdConn.Close()
@@ -59,7 +59,7 @@ func (q *quicTlsConn) Close() error {
}
func (q *quicTlsConn) ConnectionState() tls.ConnectionState {
return q.Session.ConnectionState().TLS.ConnectionState
return q.Connection.ConnectionState().TLS.ConnectionState
}
func packetConnUnspecified(conn interface{}) bool {

View File

@@ -15,10 +15,6 @@ import (
"github.com/syncthing/syncthing/lib/sync"
)
var (
Default = New()
)
type Registry struct {
mut sync.Mutex
available map[string][]interface{}
@@ -85,15 +81,3 @@ func (r *Registry) Get(scheme string, preferred func(interface{}) bool) interfac
}
return best
}
func Register(scheme string, item interface{}) {
Default.Register(scheme, item)
}
func Unregister(scheme string, item interface{}) {
Default.Unregister(scheme, item)
}
func Get(scheme string, preferred func(interface{}) bool) interface{} {
return Default.Get(scheme, preferred)
}

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay/client"
@@ -68,7 +69,7 @@ func (d *relayDialer) Dial(ctx context.Context, id protocol.DeviceID, uri *url.U
type relayDialerFactory struct{}
func (relayDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Config) genericDialer {
func (relayDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Config, _ *registry.Registry) genericDialer {
return &relayDialer{commonDialer{
trafficClass: opts.TrafficClass,
reconnectInterval: time.Duration(opts.RelayReconnectIntervalM) * time.Minute,

View File

@@ -16,6 +16,7 @@ import (
"github.com/pkg/errors"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/relay/client"
@@ -177,7 +178,7 @@ func (t *relayListener) NATType() string {
type relayListenerFactory struct{}
func (f *relayListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
func (f *relayListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service, _ *registry.Registry) genericListener {
t := &relayListener{
uri: uri,
cfg: cfg,

View File

@@ -12,6 +12,7 @@ package connections
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"math"
"net"
@@ -22,6 +23,7 @@ import (
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/nat"
@@ -55,6 +57,13 @@ var (
errDisabled = fmt.Errorf("%w: disabled by configuration", errUnsupported)
errDeprecated = fmt.Errorf("%w: deprecated", errUnsupported)
errNotInBuild = fmt.Errorf("%w: disabled at build time", errUnsupported)
// Various reasons to reject a connection
errNetworkNotAllowed = errors.New("network not allowed")
errDeviceAlreadyConnected = errors.New("already connected to this device")
errDeviceIgnored = errors.New("device is ignored")
errConnLimitReached = errors.New("connection limit reached")
errDevicePaused = errors.New("device is paused")
)
const (
@@ -128,6 +137,14 @@ type ConnectionStatusEntry struct {
Error *string `json:"error"`
}
type connWithHello struct {
c internalConn
hello protocol.Hello
err error
remoteID protocol.DeviceID
remoteCert *x509.Certificate
}
type service struct {
*suture.Supervisor
connectionStatusHandler
@@ -138,11 +155,13 @@ type service struct {
tlsCfg *tls.Config
discoverer discover.Finder
conns chan internalConn
hellos chan *connWithHello
bepProtocolName string
tlsDefaultCommonName string
limiter *limiter
natService *nat.Service
evLogger events.Logger
registry *registry.Registry
dialNow chan struct{}
dialNowDevices map[protocol.DeviceID]struct{}
@@ -153,7 +172,7 @@ type service struct {
listenerTokens map[string]suture.ServiceToken
}
func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, bepProtocolName string, tlsDefaultCommonName string, evLogger events.Logger) Service {
func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, bepProtocolName string, tlsDefaultCommonName string, evLogger events.Logger, registry *registry.Registry) Service {
spec := svcutil.SpecWithInfoLogger(l)
service := &service{
Supervisor: suture.New("connections.Service", spec),
@@ -165,11 +184,13 @@ func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *t
tlsCfg: tlsCfg,
discoverer: discoverer,
conns: make(chan internalConn),
hellos: make(chan *connWithHello),
bepProtocolName: bepProtocolName,
tlsDefaultCommonName: tlsDefaultCommonName,
limiter: newLimiter(myID, cfg),
natService: nat.NewService(myID, cfg),
evLogger: evLogger,
registry: registry,
dialNowDevicesMut: sync.NewMutex(),
dialNow: make(chan struct{}, 1),
@@ -194,7 +215,8 @@ func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *t
// incoming or outgoing.
service.Add(svcutil.AsService(service.connect, fmt.Sprintf("%s/connect", service)))
service.Add(svcutil.AsService(service.handle, fmt.Sprintf("%s/handle", service)))
service.Add(svcutil.AsService(service.handleConns, fmt.Sprintf("%s/handleConns", service)))
service.Add(svcutil.AsService(service.handleHellos, fmt.Sprintf("%s/handleHellos", service)))
service.Add(service.natService)
svcutil.OnSupervisorDone(service.Supervisor, func() {
@@ -205,9 +227,9 @@ func NewService(cfg config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *t
return service
}
func (s *service) handle(ctx context.Context) error {
var c internalConn
func (s *service) handleConns(ctx context.Context) error {
for {
var c internalConn
select {
case <-ctx.Done():
return ctx.Err()
@@ -245,8 +267,84 @@ func (s *service) handle(ctx context.Context) error {
continue
}
if err := s.connectionCheckEarly(remoteID, c); err != nil {
l.Infof("Connection from %s at %s (%s) rejected: %v", remoteID, c.RemoteAddr(), c.Type(), err)
c.Close()
continue
}
_ = c.SetDeadline(time.Now().Add(20 * time.Second))
hello, err := protocol.ExchangeHello(c, s.model.GetHello(remoteID))
go func() {
hello, err := protocol.ExchangeHello(c, s.model.GetHello(remoteID))
select {
case s.hellos <- &connWithHello{c, hello, err, remoteID, remoteCert}:
case <-ctx.Done():
}
}()
}
}
func (s *service) connectionCheckEarly(remoteID protocol.DeviceID, c internalConn) error {
if s.cfg.IgnoredDevice(remoteID) {
return errDeviceIgnored
}
if max := s.cfg.Options().ConnectionLimitMax; max > 0 && s.model.NumConnections() >= max {
// We're not allowed to accept any more connections.
return errConnLimitReached
}
cfg, ok := s.cfg.Device(remoteID)
if !ok {
// We do go ahead exchanging hello messages to get information about the device.
return nil
}
if cfg.Paused {
return errDevicePaused
}
if len(cfg.AllowedNetworks) > 0 && !IsAllowedNetwork(c.RemoteAddr().String(), cfg.AllowedNetworks) {
// The connection is not from an allowed network.
return errNetworkNotAllowed
}
// Lower priority is better, just like nice etc.
if ct, ok := s.model.Connection(remoteID); ok {
if ct.Priority() > c.priority || time.Since(ct.Statistics().StartedAt) > minConnectionReplaceAge {
l.Debugf("Switching connections %s (existing: %s new: %s)", remoteID, ct, c)
} else {
// We should not already be connected to the other party. TODO: This
// could use some better handling. If the old connection is dead but
// hasn't timed out yet we may want to drop *that* connection and keep
// this one. But in case we are two devices connecting to each other
// in parallel we don't want to do that or we end up with no
// connections still established...
return errDeviceAlreadyConnected
}
}
return nil
}
func (s *service) handleHellos(ctx context.Context) error {
var c internalConn
var hello protocol.Hello
var err error
var remoteID protocol.DeviceID
var remoteCert *x509.Certificate
for {
select {
case <-ctx.Done():
return ctx.Err()
case withHello := <-s.hellos:
c = withHello.c
hello = withHello.hello
err = withHello.err
remoteID = withHello.remoteID
remoteCert = withHello.remoteCert
}
if err != nil {
if protocol.IsVersionMismatch(err) {
// The error will be a relatively user friendly description
@@ -279,25 +377,6 @@ func (s *service) handle(ctx context.Context) error {
continue
}
// If we have a relay connection, and the new incoming connection is
// not a relay connection, we should drop that, and prefer this one.
ct, connected := s.model.Connection(remoteID)
// Lower priority is better, just like nice etc.
if connected && (ct.Priority() > c.priority || time.Since(ct.Statistics().StartedAt) > minConnectionReplaceAge) {
l.Debugf("Switching connections %s (existing: %s new: %s)", remoteID, ct, c)
} else if connected {
// We should not already be connected to the other party. TODO: This
// could use some better handling. If the old connection is dead but
// hasn't timed out yet we may want to drop *that* connection and keep
// this one. But in case we are two devices connecting to each other
// in parallel we don't want to do that or we end up with no
// connections still established...
l.Infof("Connected to already connected device %s (existing: %s new: %s)", remoteID, ct, c)
c.Close()
continue
}
deviceCfg, ok := s.cfg.Device(remoteID)
if !ok {
l.Infof("Device %s removed from config during connection attempt at %s", remoteID, c)
@@ -346,7 +425,6 @@ func (s *service) handle(ctx context.Context) error {
continue
}
}
func (s *service) connect(ctx context.Context) error {
// Map of when to earliest dial each given device + address again
nextDialAt := make(nextDialRegistry)
@@ -581,7 +659,7 @@ func (s *service) resolveDialTargets(ctx context.Context, now time.Time, cfg con
continue
}
dialer := dialerFactory.New(s.cfg.Options(), s.tlsCfg)
dialer := dialerFactory.New(s.cfg.Options(), s.tlsCfg, s.registry)
nextDialAt.set(deviceID, addr, now.Add(dialer.RedialFrequency()))
// For LAN addresses, increase the priority so that we
@@ -681,7 +759,7 @@ func (s *service) createListener(factory listenerFactory, uri *url.URL) bool {
l.Debugln("Starting listener", uri)
listener := factory.New(uri, s.cfg, s.tlsCfg, s.conns, s.natService)
listener := factory.New(uri, s.cfg, s.tlsCfg, s.conns, s.natService, s.registry)
listener.OnAddressesChanged(s.logListenAddressesChangedEvent)
// Retrying a listener many times in rapid succession is unlikely to help,
@@ -882,7 +960,7 @@ func (s *connectionStatusHandler) ConnectionStatus() map[string]ConnectionStatus
}
func (s *connectionStatusHandler) setConnectionStatus(address string, err error) {
if errors.Cause(err) == context.Canceled {
if errors.Is(err, context.Canceled) {
return
}

View File

@@ -16,6 +16,7 @@ import (
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/stats"
@@ -139,7 +140,7 @@ func (c internalConn) String() string {
}
type dialerFactory interface {
New(config.OptionsConfiguration, *tls.Config) genericDialer
New(config.OptionsConfiguration, *tls.Config, *registry.Registry) genericDialer
Priority() int
AlwaysWAN() bool
Valid(config.Configuration) error
@@ -162,7 +163,7 @@ type genericDialer interface {
}
type listenerFactory interface {
New(*url.URL, config.Wrapper, *tls.Config, chan internalConn, *nat.Service) genericListener
New(*url.URL, config.Wrapper, *tls.Config, chan internalConn, *nat.Service, *registry.Registry) genericListener
Valid(config.Configuration) error
}

View File

@@ -13,6 +13,7 @@ import (
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -28,6 +29,7 @@ func init() {
type tcpDialer struct {
commonDialer
registry *registry.Registry
}
func (d *tcpDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL) (internalConn, error) {
@@ -35,7 +37,7 @@ func (d *tcpDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL)
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
conn, err := dialer.DialContextReusePort(timeoutCtx, uri.Scheme, uri.Host)
conn, err := dialer.DialContextReusePortFunc(d.registry)(timeoutCtx, uri.Scheme, uri.Host)
if err != nil {
return internalConn{}, err
}
@@ -62,12 +64,15 @@ func (d *tcpDialer) Dial(ctx context.Context, _ protocol.DeviceID, uri *url.URL)
type tcpDialerFactory struct{}
func (tcpDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Config) genericDialer {
return &tcpDialer{commonDialer{
trafficClass: opts.TrafficClass,
reconnectInterval: time.Duration(opts.ReconnectIntervalS) * time.Second,
tlsCfg: tlsCfg,
}}
func (tcpDialerFactory) New(opts config.OptionsConfiguration, tlsCfg *tls.Config, registry *registry.Registry) genericDialer {
return &tcpDialer{
commonDialer: commonDialer{
trafficClass: opts.TrafficClass,
reconnectInterval: time.Duration(opts.ReconnectIntervalS) * time.Second,
tlsCfg: tlsCfg,
},
registry: registry,
}
}
func (tcpDialerFactory) Priority() int {

View File

@@ -32,11 +32,12 @@ type tcpListener struct {
svcutil.ServiceWithError
onAddressesChangedNotifier
uri *url.URL
cfg config.Wrapper
tlsCfg *tls.Config
conns chan internalConn
factory listenerFactory
uri *url.URL
cfg config.Wrapper
tlsCfg *tls.Config
conns chan internalConn
factory listenerFactory
registry *registry.Registry
natService *nat.Service
mapping *nat.Mapping
@@ -69,8 +70,8 @@ func (t *tcpListener) serve(ctx context.Context) error {
t.notifyAddressesChanged(t)
defer t.clearAddresses(t)
registry.Register(t.uri.Scheme, tcaddr)
defer registry.Unregister(t.uri.Scheme, tcaddr)
t.registry.Register(t.uri.Scheme, tcaddr)
defer t.registry.Unregister(t.uri.Scheme, tcaddr)
l.Infof("TCP listener (%v) starting", tcaddr)
defer l.Infof("TCP listener (%v) shutting down", tcaddr)
@@ -213,7 +214,7 @@ func (t *tcpListener) NATType() string {
type tcpListenerFactory struct{}
func (f *tcpListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
func (f *tcpListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service, registry *registry.Registry) genericListener {
l := &tcpListener{
uri: fixupPort(uri, config.DefaultTCPPort),
cfg: cfg,
@@ -221,6 +222,7 @@ func (f *tcpListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.C
conns: conns,
natService: natService,
factory: f,
registry: registry,
}
l.ServiceWithError = svcutil.AsService(l.serve, l.String())
return l

View File

@@ -403,8 +403,8 @@ func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
}
}
func (s *FileSet) MtimeFS(filesystem fs.Filesystem) fs.Filesystem {
opStr := fmt.Sprintf("%s MtimeFS()", s.folder)
func (s *FileSet) MtimeOption() fs.Option {
opStr := fmt.Sprintf("%s MtimeOption()", s.folder)
l.Debugf(opStr)
prefix, err := s.db.keyer.GenerateMtimesKey(nil, []byte(s.folder))
if backend.IsClosed(err) {
@@ -413,7 +413,7 @@ func (s *FileSet) MtimeFS(filesystem fs.Filesystem) fs.Filesystem {
fatalError(err, opStr, s.db)
}
kv := NewNamespacedKV(s.db, string(prefix))
return fs.NewMtimeFS(filesystem, kv)
return fs.NewMtimeOption(kv)
}
func (s *FileSet) ListDevices() []protocol.DeviceID {

View File

@@ -104,32 +104,34 @@ func DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
// DialContextReusePort tries dialing via proxy if a proxy is configured, and falls back to
// a direct connection reusing the port from the connections registry, if no proxy is defined, or connecting via proxy
// fails. It also in parallel dials without reusing the port, just in case reusing the port affects routing decisions badly.
func DialContextReusePort(ctx context.Context, network, addr string) (net.Conn, error) {
// If proxy is configured, there is no point trying to reuse listen addresses.
if proxy.FromEnvironment() != proxy.Direct {
return DialContext(ctx, network, addr)
}
func DialContextReusePortFunc(registry *registry.Registry) func(ctx context.Context, network, addr string) (net.Conn, error) {
return func(ctx context.Context, network, addr string) (net.Conn, error) {
// If proxy is configured, there is no point trying to reuse listen addresses.
if proxy.FromEnvironment() != proxy.Direct {
return DialContext(ctx, network, addr)
}
localAddrInterface := registry.Get(network, func(addr interface{}) bool {
return addr.(*net.TCPAddr).IP.IsUnspecified()
})
if localAddrInterface == nil {
// Nothing listening, nothing to reuse.
return DialContext(ctx, network, addr)
}
localAddrInterface := registry.Get(network, func(addr interface{}) bool {
return addr.(*net.TCPAddr).IP.IsUnspecified()
})
if localAddrInterface == nil {
// Nothing listening, nothing to reuse.
return DialContext(ctx, network, addr)
}
laddr, ok := localAddrInterface.(*net.TCPAddr)
if !ok {
return nil, errUnexpectedInterfaceType
}
laddr, ok := localAddrInterface.(*net.TCPAddr)
if !ok {
return nil, errUnexpectedInterfaceType
}
// Dial twice, once reusing the listen address, another time not reusing it, just in case reusing the address
// influences routing and we fail to reach our destination.
dialer := net.Dialer{
Control: ReusePortControl,
LocalAddr: laddr,
// Dial twice, once reusing the listen address, another time not reusing it, just in case reusing the address
// influences routing and we fail to reach our destination.
dialer := net.Dialer{
Control: ReusePortControl,
LocalAddr: laddr,
}
return dialTwicePreferFirst(ctx, dialer.DialContext, (&net.Dialer{}).DialContext, "reuse", "non-reuse", network, addr)
}
return dialTwicePreferFirst(ctx, dialer.DialContext, (&net.Dialer{}).DialContext, "reuse", "non-reuse", network, addr)
}
type dialFunc func(ctx context.Context, network, address string) (net.Conn, error)

View File

@@ -14,6 +14,7 @@ import (
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -23,7 +24,7 @@ func setupCache() *manager {
cfg.Options.LocalAnnEnabled = false
cfg.Options.GlobalAnnEnabled = false
return NewManager(protocol.LocalDeviceID, config.Wrap("", cfg, protocol.LocalDeviceID, events.NoopLogger), tls.Certificate{}, events.NoopLogger, nil).(*manager)
return NewManager(protocol.LocalDeviceID, config.Wrap("", cfg, protocol.LocalDeviceID, events.NoopLogger), tls.Certificate{}, events.NoopLogger, nil, registry.New()).(*manager)
}
func TestCacheUnique(t *testing.T) {

View File

@@ -14,12 +14,14 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"strconv"
stdsync "sync"
"time"
"github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
@@ -44,7 +46,7 @@ type httpClient interface {
const (
defaultReannounceInterval = 30 * time.Minute
announceErrorRetryInterval = 5 * time.Minute
requestTimeout = 5 * time.Second
requestTimeout = 30 * time.Second
maxAddressChangesBetweenAnnouncements = 10
)
@@ -71,7 +73,7 @@ func (e *lookupError) CacheFor() time.Duration {
return e.cacheFor
}
func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, evLogger events.Logger) (FinderService, error) {
func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, evLogger events.Logger, registry *registry.Registry) (FinderService, error) {
server, opts, err := parseOptions(server)
if err != nil {
return nil, err
@@ -88,10 +90,16 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, evLo
// The http.Client used for announcements. It needs to have our
// certificate to prove our identity, and may or may not verify the server
// certificate depending on the insecure setting.
var dialContext func(ctx context.Context, network, addr string) (net.Conn, error)
if registry != nil {
dialContext = dialer.DialContextReusePortFunc(registry)
} else {
dialContext = dialer.DialContext
}
var announceClient httpClient = &contextClient{&http.Client{
Timeout: requestTimeout,
Transport: &http.Transport{
DialContext: dialer.DialContextReusePort,
DialContext: dialContext,
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: opts.insecure,

View File

@@ -16,6 +16,7 @@ import (
"testing"
"time"
"github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/tlsutil"
@@ -56,15 +57,17 @@ func TestGlobalOverHTTP(t *testing.T) {
// is only allowed in combination with the "insecure" and "noannounce"
// parameters.
if _, err := NewGlobal("http://192.0.2.42/", tls.Certificate{}, nil, events.NoopLogger); err == nil {
registry := registry.New()
if _, err := NewGlobal("http://192.0.2.42/", tls.Certificate{}, nil, events.NoopLogger, registry); err == nil {
t.Fatal("http is not allowed without insecure and noannounce")
}
if _, err := NewGlobal("http://192.0.2.42/?insecure", tls.Certificate{}, nil, events.NoopLogger); err == nil {
if _, err := NewGlobal("http://192.0.2.42/?insecure", tls.Certificate{}, nil, events.NoopLogger, registry); err == nil {
t.Fatal("http is not allowed without noannounce")
}
if _, err := NewGlobal("http://192.0.2.42/?noannounce", tls.Certificate{}, nil, events.NoopLogger); err == nil {
if _, err := NewGlobal("http://192.0.2.42/?noannounce", tls.Certificate{}, nil, events.NoopLogger, registry); err == nil {
t.Fatal("http is not allowed without insecure")
}
@@ -185,7 +188,7 @@ func TestGlobalAnnounce(t *testing.T) {
go func() { _ = http.Serve(list, mux) }()
url := "https://" + list.Addr().String() + "?insecure"
disco, err := NewGlobal(url, cert, new(fakeAddressLister), events.NoopLogger)
disco, err := NewGlobal(url, cert, new(fakeAddressLister), events.NoopLogger, registry.New())
if err != nil {
t.Fatal(err)
}
@@ -210,7 +213,7 @@ func TestGlobalAnnounce(t *testing.T) {
}
func testLookup(url string) ([]string, error) {
disco, err := NewGlobal(url, tls.Certificate{}, nil, events.NoopLogger)
disco, err := NewGlobal(url, tls.Certificate{}, nil, events.NoopLogger, registry.New())
if err != nil {
return nil, err
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/thejerf/suture/v4"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections/registry"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/svcutil"
@@ -44,12 +45,13 @@ type manager struct {
cert tls.Certificate
evLogger events.Logger
addressLister AddressLister
registry *registry.Registry
finders map[string]cachedFinder
mut sync.RWMutex
}
func NewManager(myID protocol.DeviceID, cfg config.Wrapper, cert tls.Certificate, evLogger events.Logger, lister AddressLister) Manager {
func NewManager(myID protocol.DeviceID, cfg config.Wrapper, cert tls.Certificate, evLogger events.Logger, lister AddressLister, registry *registry.Registry) Manager {
m := &manager{
Supervisor: suture.New("discover.Manager", svcutil.SpecWithDebugLogger(l)),
myID: myID,
@@ -57,6 +59,7 @@ func NewManager(myID protocol.DeviceID, cfg config.Wrapper, cert tls.Certificate
cert: cert,
evLogger: evLogger,
addressLister: lister,
registry: registry,
finders: make(map[string]cachedFinder),
mut: sync.NewRWMutex(),
@@ -257,7 +260,7 @@ func (m *manager) CommitConfiguration(_, to config.Configuration) (handled bool)
if _, ok := m.finders[identity]; ok {
continue
}
gd, err := NewGlobal(srv, m.cert, m.addressLister, m.evLogger)
gd, err := NewGlobal(srv, m.cert, m.addressLister, m.evLogger, m.registry)
if err != nil {
l.Warnln("Global discovery:", err)
continue

View File

@@ -35,6 +35,7 @@ const (
PendingDevicesChanged
DevicePaused
DeviceResumed
ClusterConfigReceived
LocalChangeDetected
RemoteChangeDetected
LocalIndexUpdated
@@ -118,6 +119,8 @@ func (t EventType) String() string {
return "DevicePaused"
case DeviceResumed:
return "DeviceResumed"
case ClusterConfigReceived:
return "ClusterConfigReceived"
case FolderScanProgress:
return "FolderScanProgress"
case FolderPaused:
@@ -203,6 +206,8 @@ func UnmarshalEventType(s string) EventType {
return DevicePaused
case "DeviceResumed":
return DeviceResumed
case "ClusterConfigReceived":
return ClusterConfigReceived
case "FolderScanProgress":
return FolderScanProgress
case "FolderPaused":

View File

@@ -27,12 +27,13 @@ var (
type OptionJunctionsAsDirs struct{}
func (o *OptionJunctionsAsDirs) apply(fs Filesystem) {
func (o *OptionJunctionsAsDirs) apply(fs Filesystem) Filesystem {
if basic, ok := fs.(*BasicFilesystem); !ok {
l.Warnln("WithJunctionsAsDirs must only be used with FilesystemTypeBasic")
} else {
basic.junctionsAsDirs = true
}
return fs
}
func (o *OptionJunctionsAsDirs) String() string {

View File

@@ -20,17 +20,13 @@ import (
func setup(t *testing.T) (*BasicFilesystem, string) {
t.Helper()
dir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
dir := t.TempDir()
return newBasicFilesystem(dir), dir
}
func TestChmodFile(t *testing.T) {
fs, dir := setup(t)
path := filepath.Join(dir, "file")
defer os.RemoveAll(dir)
defer os.Chmod(path, 0666)
@@ -71,7 +67,6 @@ func TestChownFile(t *testing.T) {
fs, dir := setup(t)
path := filepath.Join(dir, "file")
defer os.RemoveAll(dir)
defer os.Chmod(path, 0666)
@@ -108,7 +103,6 @@ func TestChownFile(t *testing.T) {
func TestChmodDir(t *testing.T) {
fs, dir := setup(t)
path := filepath.Join(dir, "dir")
defer os.RemoveAll(dir)
mode := os.FileMode(0755)
if runtime.GOOS == "windows" {
@@ -141,7 +135,6 @@ func TestChmodDir(t *testing.T) {
func TestChtimes(t *testing.T) {
fs, dir := setup(t)
path := filepath.Join(dir, "file")
defer os.RemoveAll(dir)
fd, err := os.Create(path)
if err != nil {
t.Error(err)
@@ -166,7 +159,6 @@ func TestChtimes(t *testing.T) {
func TestCreate(t *testing.T) {
fs, dir := setup(t)
path := filepath.Join(dir, "file")
defer os.RemoveAll(dir)
if _, err := os.Stat(path); err == nil {
t.Errorf("exists?")
@@ -190,7 +182,6 @@ func TestCreateSymlink(t *testing.T) {
fs, dir := setup(t)
path := filepath.Join(dir, "file")
defer os.RemoveAll(dir)
if err := fs.CreateSymlink("blah", "file"); err != nil {
t.Error(err)
@@ -215,7 +206,6 @@ func TestCreateSymlink(t *testing.T) {
func TestDirNames(t *testing.T) {
fs, dir := setup(t)
defer os.RemoveAll(dir)
// Case differences
testCases := []string{
@@ -244,8 +234,7 @@ func TestDirNames(t *testing.T) {
func TestNames(t *testing.T) {
// Tests that all names are without the root directory.
fs, dir := setup(t)
defer os.RemoveAll(dir)
fs, _ := setup(t)
expected := "file"
fd, err := fs.Create(expected)
@@ -284,8 +273,7 @@ func TestNames(t *testing.T) {
func TestGlob(t *testing.T) {
// Tests that all names are without the root directory.
fs, dir := setup(t)
defer os.RemoveAll(dir)
fs, _ := setup(t)
for _, dirToCreate := range []string{
filepath.Join("a", "test", "b"),
@@ -344,8 +332,7 @@ func TestGlob(t *testing.T) {
}
func TestUsage(t *testing.T) {
fs, dir := setup(t)
defer os.RemoveAll(dir)
fs, _ := setup(t)
usage, err := fs.Usage(".")
if err != nil {
if runtime.GOOS == "netbsd" || runtime.GOOS == "openbsd" || runtime.GOOS == "solaris" {
@@ -577,18 +564,15 @@ func TestRel(t *testing.T) {
func TestBasicWalkSkipSymlink(t *testing.T) {
_, dir := setup(t)
defer os.RemoveAll(dir)
testWalkSkipSymlink(t, FilesystemTypeBasic, dir)
}
func TestWalkTraverseDirJunct(t *testing.T) {
_, dir := setup(t)
defer os.RemoveAll(dir)
testWalkTraverseDirJunct(t, FilesystemTypeBasic, dir)
}
func TestWalkInfiniteRecursion(t *testing.T) {
_, dir := setup(t)
defer os.RemoveAll(dir)
testWalkInfiniteRecursion(t, FilesystemTypeBasic, dir)
}

View File

@@ -12,7 +12,7 @@ package fs
import "github.com/syncthing/notify"
const (
subEventMask = notify.Create | notify.FileModified | notify.FileRenameFrom | notify.FileDelete | notify.FileRenameTo
subEventMask = notify.Create | notify.FileModified | notify.FileRenameFrom | notify.FileDelete | notify.FileRenameTo | notify.FileNoFollow
permEventMask = notify.FileAttrib
rmEventMask = notify.FileDelete | notify.FileRenameFrom
)

View File

@@ -18,4 +18,7 @@ const (
subEventMask = notify.NoteDelete | notify.NoteWrite | notify.NoteRename | notify.Create | notify.NoteAttrib | notify.NoteExtend
permEventMask = 0
rmEventMask = notify.NoteDelete | notify.NoteRename
// WatchKqueue indicates if kqueue is used for filesystem watching
WatchKqueue = true
)

View File

@@ -0,0 +1,13 @@
// Copyright (C) 2022 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 http://mozilla.org/MPL/2.0/.
//go:build !dragonfly && !freebsd && !netbsd && !openbsd
// +build !dragonfly,!freebsd,!netbsd,!openbsd
package fs
// WatchKqueue indicates if kqueue is used for filesystem watching
const WatchKqueue = false

View File

@@ -49,7 +49,6 @@ func TestResolveWindows83(t *testing.T) {
dir = fs.resolveWin83(dir)
fs = newBasicFilesystem(dir)
}
defer os.RemoveAll(dir)
shortAbs, _ := fs.rooted("LFDATA~1")
long := "LFDataTool"
@@ -80,7 +79,6 @@ func TestIsWindows83(t *testing.T) {
dir = fs.resolveWin83(dir)
fs = newBasicFilesystem(dir)
}
defer os.RemoveAll(dir)
tempTop, _ := fs.rooted(TempName("baz"))
tempBelow, _ := fs.rooted(filepath.Join("foo", "bar", TempName("baz")))

View File

@@ -123,21 +123,27 @@ func (r *caseFilesystemRegistry) cleaner() {
var globalCaseFilesystemRegistry = caseFilesystemRegistry{fss: make(map[fskey]*caseFilesystem)}
// caseFilesystem is a BasicFilesystem with additional checks to make a
// potentially case insensitive underlying FS behave like it's case-sensitive.
type caseFilesystem struct {
Filesystem
realCaser
}
// NewCaseFilesystem ensures that the given, potentially case-insensitive filesystem
// OptionDetectCaseConflicts ensures that the potentially case-insensitive filesystem
// behaves like a case-sensitive filesystem. Meaning that it takes into account
// the real casing of a path and returns ErrCaseConflict if the given path differs
// from the real path. It is safe to use with any filesystem, i.e. also a
// case-sensitive one. However it will add some overhead and thus shouldn't be
// used if the filesystem is known to already behave case-sensitively.
func NewCaseFilesystem(fs Filesystem) Filesystem {
return wrapFilesystem(fs, globalCaseFilesystemRegistry.get)
type OptionDetectCaseConflicts struct{}
func (o *OptionDetectCaseConflicts) apply(fs Filesystem) Filesystem {
return globalCaseFilesystemRegistry.get(fs)
}
func (o *OptionDetectCaseConflicts) String() string {
return "detectCaseConflicts"
}
// caseFilesystem is a BasicFilesystem with additional checks to make a
// potentially case insensitive underlying FS behave like it's case-sensitive.
type caseFilesystem struct {
Filesystem
realCaser
}
func (f *caseFilesystem) Chmod(name string, mode FileMode) error {

View File

@@ -9,7 +9,6 @@ package fs
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
@@ -28,14 +27,17 @@ func TestRealCase(t *testing.T) {
testRealCase(t, newFakeFilesystem(t.Name()+"?insens=true"))
})
t.Run("actual", func(t *testing.T) {
fsys, tmpDir := setup(t)
defer os.RemoveAll(tmpDir)
fsys, _ := setup(t)
testRealCase(t, fsys)
})
}
func newCaseFilesystem(fsys Filesystem) *caseFilesystem {
return globalCaseFilesystemRegistry.get(fsys).(*caseFilesystem)
}
func testRealCase(t *testing.T, fsys Filesystem) {
testFs := NewCaseFilesystem(fsys).(*caseFilesystem)
testFs := newCaseFilesystem(fsys)
comps := []string{"Foo", "bar", "BAZ", "bAs"}
path := filepath.Join(comps...)
testFs.MkdirAll(filepath.Join(comps[:len(comps)-1]...), 0777)
@@ -79,14 +81,13 @@ func TestRealCaseSensitive(t *testing.T) {
testRealCaseSensitive(t, newFakeFilesystem(t.Name()))
})
t.Run("actual", func(t *testing.T) {
fsys, tmpDir := setup(t)
defer os.RemoveAll(tmpDir)
fsys, _ := setup(t)
testRealCaseSensitive(t, fsys)
})
}
func testRealCaseSensitive(t *testing.T, fsys Filesystem) {
testFs := NewCaseFilesystem(fsys).(*caseFilesystem)
testFs := newCaseFilesystem(fsys)
names := make([]string, 2)
names[0] = "foo"
@@ -120,8 +121,7 @@ func TestCaseFSStat(t *testing.T) {
testCaseFSStat(t, newFakeFilesystem(t.Name()+"?insens=true"))
})
t.Run("actual", func(t *testing.T) {
fsys, tmpDir := setup(t)
defer os.RemoveAll(tmpDir)
fsys, _ := setup(t)
testCaseFSStat(t, fsys)
})
}
@@ -139,7 +139,7 @@ func testCaseFSStat(t *testing.T, fsys Filesystem) {
sensitive = false
}
testFs := NewCaseFilesystem(fsys)
testFs := newCaseFilesystem(fsys)
_, err = testFs.Stat("FOO")
if sensitive {
if IsNotExist(err) {

View File

@@ -204,7 +204,6 @@ func TestFakeFSCaseSensitive(t *testing.T) {
}
testDir, sensitive := createTestDir(t)
defer removeTestDir(t, testDir)
if sensitive {
filesystems = append(filesystems, testFS{runtime.GOOS, newBasicFilesystem(testDir)})
}
@@ -240,7 +239,6 @@ func TestFakeFSCaseInsensitive(t *testing.T) {
}
testDir, sensitive := createTestDir(t)
defer removeTestDir(t, testDir)
if !sensitive {
filesystems = append(filesystems, testFS{runtime.GOOS, newBasicFilesystem(testDir)})
}
@@ -251,10 +249,7 @@ func TestFakeFSCaseInsensitive(t *testing.T) {
func createTestDir(t *testing.T) (string, bool) {
t.Helper()
testDir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatalf("could not create temporary dir for testing: %s", err)
}
testDir := t.TempDir()
if fd, err := os.Create(filepath.Join(testDir, ".stfolder")); err != nil {
t.Fatalf("could not create .stfolder: %s", err)
@@ -273,14 +268,6 @@ func createTestDir(t *testing.T) (string, bool) {
return testDir, sensitive
}
func removeTestDir(t *testing.T, testDir string) {
t.Helper()
if err := os.RemoveAll(testDir); err != nil {
t.Fatalf("could not remove test directory: %s", err)
}
}
func runTests(t *testing.T, tests []test, filesystems []testFS) {
for _, filesystem := range filesystems {
for _, test := range tests {

View File

@@ -202,10 +202,29 @@ var IsPathSeparator = os.IsPathSeparator
// representation of those must be part of the returned string.
type Option interface {
String() string
apply(Filesystem)
apply(Filesystem) Filesystem
}
func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem {
var caseOpt Option
var mtimeOpt Option
i := 0
for _, opt := range opts {
if caseOpt != nil && mtimeOpt != nil {
break
}
switch opt.(type) {
case *OptionDetectCaseConflicts:
caseOpt = opt
case *optionMtime:
mtimeOpt = opt
default:
opts[i] = opt
i++
}
}
opts = opts[:i]
var fs Filesystem
switch fsType {
case FilesystemTypeBasic:
@@ -221,6 +240,17 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
}
}
// Case handling is the innermost, as any filesystem calls by wrappers should be case-resolved
if caseOpt != nil {
fs = caseOpt.apply(fs)
}
// mtime handling should happen inside walking, as filesystem calls while
// walking should be mtime-resolved too
if mtimeOpt != nil {
fs = mtimeOpt.apply(fs)
}
if l.ShouldDebug("walkfs") {
return NewWalkFilesystem(&logFilesystem{fs})
}
@@ -289,21 +319,6 @@ func Canonicalize(file string) (string, error) {
return file, nil
}
// wrapFilesystem should always be used when wrapping a Filesystem.
// It ensures proper wrapping order, which right now means:
// `logFilesystem` needs to be the outermost wrapper for caller lookup.
func wrapFilesystem(fs Filesystem, wrapFn func(Filesystem) Filesystem) Filesystem {
logFs, ok := fs.(*logFilesystem)
if ok {
fs = logFs.Filesystem
}
fs = wrapFn(fs)
if ok {
fs = &logFilesystem{fs}
}
return fs
}
// unwrapFilesystem removes "wrapping" filesystems to expose the filesystem of the requested wrapperType, if it exists.
func unwrapFilesystem(fs Filesystem, wrapperType filesystemWrapperType) (Filesystem, bool) {
var ok bool

View File

@@ -33,20 +33,34 @@ func WithCaseInsensitivity(v bool) MtimeFSOption {
}
}
// NewMtimeFS returns a filesystem with nanosecond mtime precision, regardless
// of what shenanigans the underlying filesystem gets up to.
func NewMtimeFS(fs Filesystem, db database, options ...MtimeFSOption) Filesystem {
return wrapFilesystem(fs, func(underlying Filesystem) Filesystem {
f := &mtimeFS{
Filesystem: underlying,
chtimes: underlying.Chtimes, // for mocking it out in the tests
db: db,
}
for _, opt := range options {
opt(f)
}
return f
})
type optionMtime struct {
db database
options []MtimeFSOption
}
// NewMtimeOption makes any filesystem provide nanosecond mtime precision,
// regardless of what shenanigans the underlying filesystem gets up to.
func NewMtimeOption(db database, options ...MtimeFSOption) Option {
return &optionMtime{
db: db,
options: options,
}
}
func (o *optionMtime) apply(fs Filesystem) Filesystem {
f := &mtimeFS{
Filesystem: fs,
chtimes: fs.Chtimes, // for mocking it out in the tests
db: o.db,
}
for _, opt := range o.options {
opt(f)
}
return f
}
func (_ *optionMtime) String() string {
return "mtime"
}
func (f *mtimeFS) Chtimes(name string, atime, mtime time.Time) error {
@@ -104,25 +118,6 @@ func (f *mtimeFS) Lstat(name string) (FileInfo, error) {
return info, nil
}
func (f *mtimeFS) Walk(root string, walkFn WalkFunc) error {
return f.Filesystem.Walk(root, func(path string, info FileInfo, err error) error {
if info != nil {
mtimeMapping, loadErr := f.load(path)
if loadErr != nil && err == nil {
// The iterator gets to deal with the error
err = loadErr
}
if mtimeMapping.Real == info.ModTime() {
info = mtimeFileInfo{
FileInfo: info,
mtime: mtimeMapping.Virtual,
}
}
}
return walkFn(path, info, err)
})
}
func (f *mtimeFS) Create(name string) (File, error) {
fd, err := f.Filesystem.Create(name)
if err != nil {

View File

@@ -26,7 +26,7 @@ func TestMtimeFS(t *testing.T) {
// a random time with nanosecond precision
testTime := time.Unix(1234567890, 123456789)
mtimefs := newMtimeFS(newBasicFilesystem("."), make(mapStore))
mtimefs := newMtimeFS(".", make(mapStore))
// Do one Chtimes call that will go through to the normal filesystem
mtimefs.chtimes = os.Chtimes
@@ -82,14 +82,10 @@ func TestMtimeFS(t *testing.T) {
}
func TestMtimeFSWalk(t *testing.T) {
dir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer func() { _ = os.RemoveAll(dir) }()
dir := t.TempDir()
underlying := NewFilesystem(FilesystemTypeBasic, dir)
mtimefs := newMtimeFS(underlying, make(mapStore))
mtimefs, walkFs := newMtimeFSWithWalk(dir, make(mapStore))
underlying := mtimefs.Filesystem
mtimefs.chtimes = failChtimes
if err := os.WriteFile(filepath.Join(dir, "file"), []byte("hello"), 0644); err != nil {
@@ -120,7 +116,7 @@ func TestMtimeFSWalk(t *testing.T) {
}
found := false
_ = mtimefs.Walk("", func(path string, info FileInfo, err error) error {
_ = walkFs.Walk("", func(path string, info FileInfo, err error) error {
if path == "file" {
found = true
if !info.ModTime().Equal(newTime) {
@@ -136,14 +132,10 @@ func TestMtimeFSWalk(t *testing.T) {
}
func TestMtimeFSOpen(t *testing.T) {
dir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer func() { _ = os.RemoveAll(dir) }()
dir := t.TempDir()
underlying := NewFilesystem(FilesystemTypeBasic, dir)
mtimefs := newMtimeFS(underlying, make(mapStore))
mtimefs := newMtimeFS(dir, make(mapStore))
underlying := mtimefs.Filesystem
mtimefs.chtimes = failChtimes
if err := os.WriteFile(filepath.Join(dir, "file"), []byte("hello"), 0644); err != nil {
@@ -177,6 +169,8 @@ func TestMtimeFSOpen(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer fd.Close()
info, err := fd.Stat()
if err != nil {
t.Fatal(err)
@@ -222,12 +216,12 @@ func TestMtimeFSInsensitive(t *testing.T) {
// The test should fail with a case sensitive mtimefs
t.Run("with case sensitive mtimefs", func(t *testing.T) {
theTest(t, newMtimeFS(newBasicFilesystem("."), make(mapStore)), false)
theTest(t, newMtimeFS(".", make(mapStore)), false)
})
// And succeed with a case insensitive one.
t.Run("with case insensitive mtimefs", func(t *testing.T) {
theTest(t, newMtimeFS(newBasicFilesystem("."), make(mapStore), WithCaseInsensitivity(true)), true)
theTest(t, newMtimeFS(".", make(mapStore), WithCaseInsensitivity(true)), true)
})
}
@@ -261,6 +255,12 @@ func evilChtimes(name string, mtime, atime time.Time) error {
return os.Chtimes(name, mtime.Add(300*time.Hour).Truncate(time.Hour), atime.Add(300*time.Hour).Truncate(time.Hour))
}
func newMtimeFS(fs Filesystem, db database, options ...MtimeFSOption) *mtimeFS {
return NewMtimeFS(fs, db, options...).(*mtimeFS)
func newMtimeFS(path string, db database, options ...MtimeFSOption) *mtimeFS {
mtimefs, _ := newMtimeFSWithWalk(path, db, options...)
return mtimefs
}
func newMtimeFSWithWalk(path string, db database, options ...MtimeFSOption) (*mtimeFS, *walkFilesystem) {
wfs := NewFilesystem(FilesystemTypeBasic, path, NewMtimeOption(db, options...)).(*walkFilesystem)
return wfs.Filesystem.(*mtimeFS), wfs
}

View File

@@ -231,10 +231,7 @@ func TestCaseSensitivity(t *testing.T) {
}
func TestCaching(t *testing.T) {
dir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
dir := t.TempDir()
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
@@ -424,10 +421,7 @@ flamingo
*.crow
`
// Caches per file, hence write the patterns to a file.
dir, err := os.MkdirTemp("", "")
if err != nil {
b.Fatal(err)
}
dir := b.TempDir()
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
@@ -465,10 +459,7 @@ flamingo
}
func TestCacheReload(t *testing.T) {
dir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
dir := t.TempDir()
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
@@ -988,12 +979,7 @@ func TestIssue4689(t *testing.T) {
}
func TestIssue4901(t *testing.T) {
dir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
dir := t.TempDir()
stignore := `
#include unicorn-lazor-death
@@ -1023,7 +1009,7 @@ func TestIssue4901(t *testing.T) {
t.Fatalf(err.Error())
}
err = pats.Load(".stignore")
err := pats.Load(".stignore")
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}
@@ -1200,11 +1186,7 @@ func TestWindowsLineEndings(t *testing.T) {
lines := "foo\nbar\nbaz\n"
dir, err := os.MkdirTemp("", "syncthing-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
dir := t.TempDir()
ffs := fs.NewFilesystem(fs.FilesystemTypeBasic, dir)
m := New(ffs)

View File

@@ -31,7 +31,7 @@ const (
)
const (
DefaultFlags = log.Ltime
DefaultFlags = log.Ltime | log.Ldate
DebugFlags = log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile
)

View File

@@ -34,6 +34,9 @@ import (
"github.com/syncthing/syncthing/lib/watchaggregator"
)
// Arbitrary limit that triggers a warning on kqueue systems
const kqueueItemCountThreshold = 10000
type folder struct {
stateTracker
config.FolderConfiguration
@@ -81,6 +84,8 @@ type folder struct {
puller puller
versioner versioner.Versioner
warnedKqueue bool
}
type syncRequest struct {
@@ -103,7 +108,7 @@ func newFolder(model *model, fset *db.FileSet, ignores *ignore.Matcher, cfg conf
shortID: model.shortID,
fset: fset,
ignores: ignores,
mtimefs: fset.MtimeFS(cfg.Filesystem()),
mtimefs: cfg.Filesystem(fset),
modTimeWindow: cfg.ModTimeWindow(),
done: make(chan struct{}),
@@ -382,7 +387,6 @@ func (f *folder) pull() (success bool, err error) {
l.Debugln("Skipping pull of", f.Description(), "due to folder error:", err)
return false, err
}
f.setError(nil)
// Send only folder doesn't do any io, it only checks for out-of-sync
// items that differ in metadata and updates those.
@@ -409,6 +413,7 @@ func (f *folder) pull() (success bool, err error) {
l.Debugln("Skipping pull of", f.Description(), "due to folder error:", err)
return false, err
}
f.setError(nil)
success, err = f.puller.pull()
@@ -431,10 +436,6 @@ func (f *folder) scanSubdirs(subDirs []string) error {
err := f.getHealthErrorAndLoadIgnores()
if err != nil {
// If there is a health error we set it as the folder error. We do not
// clear the folder error if there is no health error, as there might be
// an *other* folder error (failed to load ignores, for example). Hence
// we do not use the CheckHealth() convenience function here.
return err
}
f.setError(nil)
@@ -984,10 +985,23 @@ func (f *folder) monitorWatch(ctx context.Context) {
warnedOutside := false
var lastWatch time.Time
pause := time.Minute
// Subscribe to folder summaries only on kqueue systems, to warn about potential high resource usage
var summarySub events.Subscription
var summaryChan <-chan events.Event
if fs.WatchKqueue && !f.warnedKqueue {
summarySub = f.evLogger.Subscribe(events.FolderSummary)
summaryChan = summarySub.C()
}
defer func() {
aggrCancel() // aggrCancel might e re-assigned -> call within closure
if summaryChan != nil {
summarySub.Unsubscribe()
}
}()
for {
select {
case <-failTimer.C:
eventChan, errChan, err = f.Filesystem().Watch(".", f.ignores, ctx, f.IgnorePerms)
eventChan, errChan, err = f.mtimefs.Watch(".", f.ignores, ctx, f.IgnorePerms)
// We do this once per minute initially increased to
// max one hour in case of repeat failures.
f.scanOnWatchErr()
@@ -1028,6 +1042,15 @@ func (f *folder) monitorWatch(ctx context.Context) {
aggrCancel()
errChan = nil
aggrCtx, aggrCancel = context.WithCancel(ctx)
case ev := <-summaryChan:
if data, ok := ev.Data.(FolderSummaryEventData); !ok {
f.evLogger.Log(events.Failure, "Unexpected type of folder-summary event in folder.monitorWatch")
} else if data.Summary.LocalTotalItems > kqueueItemCountThreshold {
f.warnedKqueue = true
summarySub.Unsubscribe()
summaryChan = nil
l.Warnf("Filesystem watching (kqueue) is enabled on %v with a lot of files/directories, and that requires a lot of resources and might slow down your system significantly", f.Description())
}
case <-ctx.Done():
return
}

View File

@@ -28,7 +28,7 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
@@ -110,7 +110,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
@@ -200,7 +200,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
@@ -270,7 +270,7 @@ func TestRecvOnlyDeletedRemoteDrop(t *testing.T) {
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
@@ -335,7 +335,7 @@ func TestRecvOnlyRemoteUndoChanges(t *testing.T) {
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)
@@ -425,7 +425,7 @@ func TestRecvOnlyRevertOwnID(t *testing.T) {
m, f, wcfgCancel := setupROFolder(t)
defer wcfgCancel()
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
defer cleanupModel(m)
addFakeConn(m, device1, f.ID)

View File

@@ -1264,7 +1264,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
// Hope that it's usually in the same folder, so start with that one.
folders := []string{f.folderID}
for folder, cfg := range f.model.cfg.Folders() {
folderFilesystems[folder] = cfg.Filesystem()
folderFilesystems[folder] = cfg.Filesystem(nil)
if folder != f.folderID {
folders = append(folders, folder)
}
@@ -1849,7 +1849,7 @@ func (f *sendReceiveFolder) moveForConflict(name, lastModBy string, scanChan cha
}
func (f *sendReceiveFolder) newPullError(path string, err error) {
if errors.Cause(err) == f.ctx.Err() {
if errors.Is(err, f.ctx.Err()) {
// Error because the folder stopped - no point logging/tracking
return
}

View File

@@ -89,7 +89,7 @@ func createEmptyFileInfo(t *testing.T, name string, fs fs.Filesystem) protocol.F
// Sets up a folder and model, but makes sure the services aren't actually running.
func setupSendReceiveFolder(t testing.TB, files ...protocol.FileInfo) (*testModel, *sendReceiveFolder, context.CancelFunc) {
w, fcfg, wCancel := tmpDefaultWrapper()
w, fcfg, wCancel := tmpDefaultWrapper(t)
// Initialise model and stop immediately.
model := setupModel(t, w)
model.cancel()
@@ -109,7 +109,7 @@ func setupSendReceiveFolder(t testing.TB, files ...protocol.FileInfo) (*testMode
func cleanupSRFolder(f *sendReceiveFolder, m *testModel, wrapperCancel context.CancelFunc) {
wrapperCancel()
os.Remove(m.cfg.ConfigPath())
os.RemoveAll(f.Filesystem().URI())
os.RemoveAll(f.Filesystem(nil).URI())
}
// Layout of the files: (indexes from the above array)
@@ -172,7 +172,7 @@ func TestHandleFileWithTemp(t *testing.T) {
m, f, wcfgCancel := setupSendReceiveFolder(t, existingFile)
defer cleanupSRFolder(f, m, wcfgCancel)
if _, err := prepareTmpFile(f.Filesystem()); err != nil {
if _, err := prepareTmpFile(f.Filesystem(nil)); err != nil {
t.Fatal(err)
}
@@ -230,7 +230,7 @@ func TestCopierFinder(t *testing.T) {
defer cleanupSRFolder(f, m, wcfgCancel)
if _, err := prepareTmpFile(f.Filesystem()); err != nil {
if _, err := prepareTmpFile(f.Filesystem(nil)); err != nil {
t.Fatal(err)
}
@@ -290,7 +290,7 @@ func TestCopierFinder(t *testing.T) {
}
// Verify that the fetched blocks have actually been written to the temp file
blks, err := scanner.HashFile(context.TODO(), f.Filesystem(), tempFile, protocol.MinBlockSize, nil, false)
blks, err := scanner.HashFile(context.TODO(), f.Filesystem(nil), tempFile, protocol.MinBlockSize, nil, false)
if err != nil {
t.Log(err)
}
@@ -308,7 +308,7 @@ func TestWeakHash(t *testing.T) {
// Setup the model/pull environment
model, fo, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(fo, model, wcfgCancel)
ffs := fo.Filesystem()
ffs := fo.Filesystem(nil)
tempFile := fs.TempName("weakhash")
var shift int64 = 10
@@ -673,7 +673,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
func TestIssue3164(t *testing.T) {
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
tmpDir := ffs.URI()
ignDir := filepath.Join("issue3164", "oktodelete")
@@ -764,7 +764,7 @@ func TestDiffEmpty(t *testing.T) {
func TestDeleteIgnorePerms(t *testing.T) {
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
f.IgnorePerms = true
name := "deleteIgnorePerms"
@@ -806,7 +806,7 @@ func TestCopyOwner(t *testing.T) {
f.folder.FolderConfiguration.CopyOwnershipFromParent = true
f.fset = newFileSet(t, f.ID, m.db)
f.mtimefs = f.fset.MtimeFS(f.Filesystem())
f.mtimefs = f.Filesystem(f.fset)
// Create a parent dir with a certain owner/group.
@@ -905,7 +905,7 @@ func TestCopyOwner(t *testing.T) {
func TestSRConflictReplaceFileByDir(t *testing.T) {
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
name := "foo"
@@ -937,7 +937,7 @@ func TestSRConflictReplaceFileByDir(t *testing.T) {
func TestSRConflictReplaceFileByLink(t *testing.T) {
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
name := "foo"
@@ -970,10 +970,9 @@ func TestSRConflictReplaceFileByLink(t *testing.T) {
func TestDeleteBehindSymlink(t *testing.T) {
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
destDir := createTmpDir()
defer os.RemoveAll(destDir)
destDir := t.TempDir()
destFs := fs.NewFilesystem(fs.FilesystemTypeBasic, destDir)
link := "link"
@@ -1063,7 +1062,7 @@ func TestPullCtxCancel(t *testing.T) {
func TestPullDeleteUnscannedDir(t *testing.T) {
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
dir := "foobar"
must(t, ffs.MkdirAll(dir, 0777))
@@ -1092,7 +1091,7 @@ func TestPullDeleteUnscannedDir(t *testing.T) {
func TestPullCaseOnlyPerformFinish(t *testing.T) {
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
name := "foo"
contents := []byte("contents")
@@ -1153,7 +1152,7 @@ func TestPullCaseOnlySymlink(t *testing.T) {
func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
m, f, wcfgCancel := setupSendReceiveFolder(t)
defer cleanupSRFolder(f, m, wcfgCancel)
ffs := f.Filesystem()
ffs := f.Filesystem(nil)
name := "foo"
if dir {
@@ -1228,6 +1227,8 @@ func TestPullTempFileCaseConflict(t *testing.T) {
cs := <-copyChan
if _, err := cs.tempFile(); err != nil {
t.Error(err)
} else {
cs.finalClose()
}
}

View File

@@ -29,7 +29,7 @@ const maxDurationSinceLastEventReq = time.Minute
type FolderSummaryService interface {
suture.Service
Summary(folder string) (map[string]interface{}, error)
Summary(folder string) (*FolderSummary, error)
OnEventRequest()
}
@@ -76,8 +76,58 @@ func (c *folderSummaryService) String() string {
return fmt.Sprintf("FolderSummaryService@%p", c)
}
func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, error) {
var res = make(map[string]interface{})
// FolderSummary replaces the previously used map[string]interface{}, and needs
// to keep the structure/naming for api backwards compatibility
type FolderSummary struct {
Errors int `json:"errors"`
PullErrors int `json:"pullErrors"` // deprecated
Invalid string `json:"invalid"` // deprecated
GlobalFiles int `json:"globalFiles"`
GlobalDirectories int `json:"globalDirectories"`
GlobalSymlinks int `json:"globalSymlinks"`
GlobalDeleted int `json:"globalDeleted"`
GlobalBytes int64 `json:"globalBytes"`
GlobalTotalItems int `json:"globalTotalItems"`
LocalFiles int `json:"localFiles"`
LocalDirectories int `json:"localDirectories"`
LocalSymlinks int `json:"localSymlinks"`
LocalDeleted int `json:"localDeleted"`
LocalBytes int64 `json:"localBytes"`
LocalTotalItems int `json:"localTotalItems"`
NeedFiles int `json:"needFiles"`
NeedDirectories int `json:"needDirectories"`
NeedSymlinks int `json:"needSymlinks"`
NeedDeletes int `json:"needDeletes"`
NeedBytes int64 `json:"needBytes"`
NeedTotalItems int `json:"needTotalItems"`
ReceiveOnlyChangedFiles int `json:"receiveOnlyChangedFiles"`
ReceiveOnlyChangedDirectories int `json:"receiveOnlyChangedDirectories"`
ReceiveOnlyChangedSymlinks int `json:"receiveOnlyChangedSymlinks"`
ReceiveOnlyChangedDeletes int `json:"receiveOnlyChangedDeletes"`
ReceiveOnlyChangedBytes int64 `json:"receiveOnlyChangedBytes"`
ReceiveOnlyTotalItems int `json:"receiveOnlyTotalItems"`
InSyncFiles int `json:"inSyncFiles"`
InSyncBytes int64 `json:"inSyncBytes"`
State string `json:"state"`
StateChanged time.Time `json:"stateChanged"`
Error string `json:"error"`
Version int64 `json:"version"` // deprecated
Sequence int64 `json:"sequence"`
IgnorePatterns bool `json:"ignorePatterns"`
WatchError string `json:"watchError"`
}
func (c *folderSummaryService) Summary(folder string) (*FolderSummary, error) {
res := new(FolderSummary)
var local, global, need, ro db.Counts
var ourSeq, remoteSeq int64
@@ -101,14 +151,14 @@ func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, e
return nil, err
}
res["errors"] = len(errors)
res["pullErrors"] = len(errors) // deprecated
res.Errors = len(errors)
res.PullErrors = len(errors) // deprecated
res["invalid"] = "" // Deprecated, retains external API for now
res.Invalid = "" // Deprecated, retains external API for now
res["globalFiles"], res["globalDirectories"], res["globalSymlinks"], res["globalDeleted"], res["globalBytes"], res["globalTotalItems"] = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes, global.TotalItems()
res.GlobalFiles, res.GlobalDirectories, res.GlobalSymlinks, res.GlobalDeleted, res.GlobalBytes, res.GlobalTotalItems = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes, global.TotalItems()
res["localFiles"], res["localDirectories"], res["localSymlinks"], res["localDeleted"], res["localBytes"], res["localTotalItems"] = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes, local.TotalItems()
res.LocalFiles, res.LocalDirectories, res.LocalSymlinks, res.LocalDeleted, res.LocalBytes, res.LocalTotalItems = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes, local.TotalItems()
fcfg, haveFcfg := c.cfg.Folder(folder)
@@ -122,41 +172,41 @@ func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, e
if need.Bytes < 0 {
need.Bytes = 0
}
res["needFiles"], res["needDirectories"], res["needSymlinks"], res["needDeletes"], res["needBytes"], res["needTotalItems"] = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes, need.TotalItems()
res.NeedFiles, res.NeedDirectories, res.NeedSymlinks, res.NeedDeletes, res.NeedBytes, res.NeedTotalItems = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes, need.TotalItems()
if haveFcfg && (fcfg.Type == config.FolderTypeReceiveOnly || fcfg.Type == config.FolderTypeReceiveEncrypted) {
// Add statistics for things that have changed locally in a receive
// only or receive encrypted folder.
res["receiveOnlyChangedFiles"] = ro.Files
res["receiveOnlyChangedDirectories"] = ro.Directories
res["receiveOnlyChangedSymlinks"] = ro.Symlinks
res["receiveOnlyChangedDeletes"] = ro.Deleted
res["receiveOnlyChangedBytes"] = ro.Bytes
res["receiveOnlyTotalItems"] = ro.TotalItems()
res.ReceiveOnlyChangedFiles = ro.Files
res.ReceiveOnlyChangedDirectories = ro.Directories
res.ReceiveOnlyChangedSymlinks = ro.Symlinks
res.ReceiveOnlyChangedDeletes = ro.Deleted
res.ReceiveOnlyChangedBytes = ro.Bytes
res.ReceiveOnlyTotalItems = ro.TotalItems()
}
res["inSyncFiles"], res["inSyncBytes"] = global.Files-need.Files, global.Bytes-need.Bytes
res.InSyncFiles, res.InSyncBytes = global.Files-need.Files, global.Bytes-need.Bytes
res["state"], res["stateChanged"], err = c.model.State(folder)
res.State, res.StateChanged, err = c.model.State(folder)
if err != nil {
res["error"] = err.Error()
res.Error = err.Error()
}
res["version"] = ourSeq + remoteSeq // legacy
res["sequence"] = ourSeq + remoteSeq // new name
res.Version = ourSeq + remoteSeq // legacy
res.Sequence = ourSeq + remoteSeq // new name
ignorePatterns, _, _ := c.model.CurrentIgnores(folder)
res["ignorePatterns"] = false
res.IgnorePatterns = false
for _, line := range ignorePatterns {
if len(line) > 0 && !strings.HasPrefix(line, "//") {
res["ignorePatterns"] = true
res.IgnorePatterns = true
break
}
}
err = c.model.WatchError(folder)
if err != nil {
res["watchError"] = err.Error()
res.WatchError = err.Error()
}
return res, nil
@@ -171,7 +221,7 @@ func (c *folderSummaryService) OnEventRequest() {
// listenForUpdates subscribes to the event bus and makes note of folders that
// need their data recalculated.
func (c *folderSummaryService) listenForUpdates(ctx context.Context) error {
sub := c.evLogger.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged | events.RemoteDownloadProgress | events.DeviceConnected | events.FolderWatchStateChanged | events.DownloadProgress)
sub := c.evLogger.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged | events.RemoteDownloadProgress | events.DeviceConnected | events.ClusterConfigReceived | events.FolderWatchStateChanged | events.DownloadProgress)
defer sub.Unsubscribe()
for {
@@ -194,12 +244,18 @@ func (c *folderSummaryService) processUpdate(ev events.Event) {
var folder string
switch ev.Type {
case events.DeviceConnected:
case events.DeviceConnected, events.ClusterConfigReceived:
// When a device connects we schedule a refresh of all
// folders shared with that device.
data := ev.Data.(map[string]string)
deviceID, _ := protocol.DeviceIDFromString(data["id"])
var deviceID protocol.DeviceID
if ev.Type == events.DeviceConnected {
data := ev.Data.(map[string]string)
deviceID, _ = protocol.DeviceIDFromString(data["id"])
} else {
data := ev.Data.(ClusterConfigReceivedEventData)
deviceID = data.Device
}
c.foldersMut.Lock()
nextFolder:
@@ -322,6 +378,11 @@ func (c *folderSummaryService) foldersToHandle() []string {
return res
}
type FolderSummaryEventData struct {
Folder string `json:"folder"`
Summary *FolderSummary `json:"summary"`
}
// sendSummary send the summary events for a single folder
func (c *folderSummaryService) sendSummary(ctx context.Context, folder string) {
// The folder summary contains how many bytes, files etc
@@ -330,9 +391,9 @@ func (c *folderSummaryService) sendSummary(ctx context.Context, folder string) {
if err != nil {
return
}
c.evLogger.Log(events.FolderSummary, map[string]interface{}{
"folder": folder,
"summary": data,
c.evLogger.Log(events.FolderSummary, FolderSummaryEventData{
Folder: folder,
Summary: data,
})
for _, devCfg := range c.cfg.Folders()[folder].Devices {

View File

@@ -52,6 +52,34 @@ func (s folderState) String() string {
}
}
type remoteFolderState int
const (
remoteFolderUnknown remoteFolderState = iota
remoteFolderNotSharing
remoteFolderPaused
remoteFolderValid
)
func (s remoteFolderState) String() string {
switch s {
case remoteFolderUnknown:
return "unknown"
case remoteFolderNotSharing:
return "notSharing"
case remoteFolderPaused:
return "paused"
case remoteFolderValid:
return "valid"
default:
return "unknown"
}
}
func (s remoteFolderState) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}
type stateTracker struct {
folderID string
evLogger events.Logger

View File

@@ -471,7 +471,7 @@ func (r *indexHandlerRegistry) Remove(folder string) {
// 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{}) {
func (r *indexHandlerRegistry) RemoveAllExcept(except map[string]remoteFolderState) {
r.mut.Lock()
defer r.mut.Unlock()

View File

@@ -24,17 +24,17 @@ type FolderSummaryService struct {
serveReturnsOnCall map[int]struct {
result1 error
}
SummaryStub func(string) (map[string]interface{}, error)
SummaryStub func(string) (*model.FolderSummary, error)
summaryMutex sync.RWMutex
summaryArgsForCall []struct {
arg1 string
}
summaryReturns struct {
result1 map[string]interface{}
result1 *model.FolderSummary
result2 error
}
summaryReturnsOnCall map[int]struct {
result1 map[string]interface{}
result1 *model.FolderSummary
result2 error
}
invocations map[string][][]interface{}
@@ -126,7 +126,7 @@ func (fake *FolderSummaryService) ServeReturnsOnCall(i int, result1 error) {
}{result1}
}
func (fake *FolderSummaryService) Summary(arg1 string) (map[string]interface{}, error) {
func (fake *FolderSummaryService) Summary(arg1 string) (*model.FolderSummary, error) {
fake.summaryMutex.Lock()
ret, specificReturn := fake.summaryReturnsOnCall[len(fake.summaryArgsForCall)]
fake.summaryArgsForCall = append(fake.summaryArgsForCall, struct {
@@ -151,7 +151,7 @@ func (fake *FolderSummaryService) SummaryCallCount() int {
return len(fake.summaryArgsForCall)
}
func (fake *FolderSummaryService) SummaryCalls(stub func(string) (map[string]interface{}, error)) {
func (fake *FolderSummaryService) SummaryCalls(stub func(string) (*model.FolderSummary, error)) {
fake.summaryMutex.Lock()
defer fake.summaryMutex.Unlock()
fake.SummaryStub = stub
@@ -164,28 +164,28 @@ func (fake *FolderSummaryService) SummaryArgsForCall(i int) string {
return argsForCall.arg1
}
func (fake *FolderSummaryService) SummaryReturns(result1 map[string]interface{}, result2 error) {
func (fake *FolderSummaryService) SummaryReturns(result1 *model.FolderSummary, result2 error) {
fake.summaryMutex.Lock()
defer fake.summaryMutex.Unlock()
fake.SummaryStub = nil
fake.summaryReturns = struct {
result1 map[string]interface{}
result1 *model.FolderSummary
result2 error
}{result1, result2}
}
func (fake *FolderSummaryService) SummaryReturnsOnCall(i int, result1 map[string]interface{}, result2 error) {
func (fake *FolderSummaryService) SummaryReturnsOnCall(i int, result1 *model.FolderSummary, result2 error) {
fake.summaryMutex.Lock()
defer fake.summaryMutex.Unlock()
fake.SummaryStub = nil
if fake.summaryReturnsOnCall == nil {
fake.summaryReturnsOnCall = make(map[int]struct {
result1 map[string]interface{}
result1 *model.FolderSummary
result2 error
})
}
fake.summaryReturnsOnCall[i] = struct {
result1 map[string]interface{}
result1 *model.FolderSummary
result2 error
}{result1, result2}
}

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