Compare commits

..

93 Commits

Author SHA1 Message Date
Jakob Borg
7b3fa8da60 Update translations & docs 2015-12-27 13:26:14 +01:00
Jakob Borg
2fae7ccf5c Ignore error on os.Chtimes in config archiving 2015-12-26 18:25:36 +01:00
Audrius Butkevicius
f36f48c2cf Merge pull request #2596 from andersonvom/load_config
Centralize config loading logic (also fixes #2509)
2015-12-26 16:12:16 +00:00
Jakob Borg
8b726c7e8b Correct GUI asset dir handling (fixes #2621) 2015-12-26 13:32:46 +01:00
Anderson Mesquita
8eb0687407 Update mtime of config file before upgrading (fixes #2509)
This updates the modified time of the config file before archiving it
during an update so that the clean up routine doesn't delete it if it's
too old, preventing the user from being able to rollback after an
upgrade.
2015-12-24 09:27:31 -05:00
Anderson Mesquita
9b9912ba9e Centralize config loading logic
This gets rid of redundant checks and centralizes the logic of loading
the config files so that we don't have to keep doing the same thing in
multiple places.
2015-12-24 09:27:31 -05:00
Anderson Mesquita
22d0ed8225 Fix typo 2015-12-24 09:27:31 -05:00
Jakob Borg
b7a58d2f87 Merge pull request #2620 from andersonvom/issue-2454
Remove fixed footer at first media break (fixes #2454)
2015-12-24 11:26:40 +01:00
Anderson Mesquita
219ece22fc Remove fixed footer at first media break (fixes #2454)
After the first media break (under 1200px), the footer is too long to
fit in a single line, taking up too much space in small screen devices.
This makes it so that it will stop being fixed at the bottom, freeing up
valuable screen real estate.
2015-12-23 22:19:41 -05:00
Jakob Borg
4874301615 Merge pull request #2618 from AudriusButkevicius/svc
Svc -> Service
2015-12-23 21:33:42 +01:00
Audrius Butkevicius
1827dda0c6 Svc -> Service 2015-12-23 15:31:12 +00:00
Jakob Borg
d088b01f75 Rebuild assets 2015-12-23 16:09:42 +01:00
Jakob Borg
00c5062eab Merge branch 'pr/2613'
* pr/2613:
  Add an icon for Safari's pinned tabs
2015-12-23 16:09:31 +01:00
Jakob Borg
84eacde63a Add jgke 2015-12-23 16:09:11 +01:00
Jakob Borg
d98290c17f Merge pull request #2617 from andersonvom/issue-2598
Refactor rwfolder tests
2015-12-23 07:55:47 +01:00
Anderson Mesquita
6d94a3be05 Refactor rwfolder tests
This creates a few utility functions to avoid repetition and removes
some redundant checks.
2015-12-22 23:43:07 -05:00
Audrius Butkevicius
a2833d18ed Merge pull request #2615 from calmh/jsonvv
Humanize serialization of version vectors
2015-12-22 21:12:23 +00:00
Jakob Borg
6f95afdc59 Humanize serialization of version vectors 2015-12-22 21:53:25 +01:00
Jakob Borg
aaa75a32a5 Temporarily patch kardianos/osext to build on FreeBSD 2015-12-22 13:41:04 +01:00
Jaakko Hannikainen
c300015ac2 Add an icon for Safari's pinned tabs
Safari has its own standard for handling icons for pinned tabs,
which requires a black-and-white .svg and a special tag.
Without using this, pinning a tab to localhost will show just
a blank square, instead of a pre-generated letter.
2015-12-22 12:29:59 +02:00
Jakob Borg
eb4f5e9faa Update deps, step two (because I suck) 2015-12-22 09:43:47 +01:00
Jakob Borg
c7aec839ae Merge pull request #2614 from andersonvom/issue-2598
WIP: Consider tempfile when checking for free space (fixes #2598)
2015-12-22 09:42:34 +01:00
Jakob Borg
37aa89b19d Update kardianos/osext (ref #1272) 2015-12-22 09:39:47 +01:00
Anderson Mesquita
3f94e70488 WIP: Consider tempfile when checking for free space (fixes #2598)
Checks the existing blocks that can be reused when downloading a file so
that it only requires the space corresponding to the missing blocks.
This will prevent syncthing from claiming the folder doesn't have enough
space when resuming download of large files after they have been
partially downloaded.
2015-12-21 13:36:08 -05:00
Jakob Borg
435afa0eea Merge pull request #2610 from calmh/fix2608-2
Correctly set default logfile location on Windows (fixes #2608)
2015-12-21 12:26:49 +01:00
Jakob Borg
fb82a5e086 Correctly set default logfile location on Windows (fixes #2608) 2015-12-21 12:19:28 +01:00
Audrius Butkevicius
71d98c2f26 Merge pull request #2609 from calmh/fix2608
Don't crash on stat error in ensureDir (fixes #2608)
2015-12-21 10:35:55 +00:00
Jakob Borg
4a97aa12d6 Don't crash on stat error in ensureDir (fixes #2608)
I'm not really sure under what circumstances MkdirAll returns a nil
error but a subsequent stat fails, but apparently it can happen and we
need to handle it. The "mode >= 0" was a no-op, and we never call
ensureDir anyway without the intention of ensuring the mode, so removed
that.
2015-12-21 09:35:43 +01:00
Audrius Butkevicius
c9e67fb460 Log when we fail to connect to relay 2015-12-20 22:14:13 +00:00
Jakob Borg
eb1a234a77 Translation & docs update 2015-12-20 10:05:17 +01:00
Jakob Borg
394c2b67d6 Merge pull request #2595 from AudriusButkevicius/leaks
Always exit via error select, making sure reader routine is exits (fi…
2015-12-20 10:03:34 +01:00
Jakob Borg
aeb3af7105 Merge branch 'pr/2594'
* pr/2594:
  Move event timestamp to header for share folder prompt
  Edit device after accepting new connection (fixes #1929)
  Move new device timestamp to box header
  Fix unmatching closing span tag
2015-12-20 09:53:26 +01:00
Jakob Borg
e5cf99e31c Move event timestamp to header for share folder prompt 2015-12-20 09:52:59 +01:00
Anderson Mesquita
8120535a35 Edit device after accepting new connection (fixes #1929)
This will open the "edit device" dialogue after accepting a new device
connection. This will allow the user to specify the name of the device
or leave it blank in case they want to accept whatever the device
advertises once it connects.
2015-12-19 09:48:27 -05:00
Anderson Mesquita
ba01433381 Move new device timestamp to box header
The device ID is already long enough to make it a bit hard to read the
body message, so moving the timestamp to the header unclutters the body
a bit.
2015-12-19 09:42:33 -05:00
Anderson Mesquita
6b99cdb83a Fix unmatching closing span tag 2015-12-19 09:42:33 -05:00
Audrius Butkevicius
e7b71f8743 Merge pull request #2601 from calmh/fix2600
Don't verify free space for files when folder MinDiskFreePct==0 (fixes #2600)
2015-12-19 13:02:57 +00:00
Jakob Borg
03935b2d64 Don't verify free space for files when folder MinDiskFreePct==0 (fixes #2600) 2015-12-19 13:59:08 +01:00
Jakob Borg
2b89f33765 Merge pull request #2592 from andersonvom/remove_globals
Remove some global variables from main.go
2015-12-19 09:05:05 +01:00
Jakob Borg
9c82a4ca60 Merge pull request #2593 from AudriusButkevicius/httpfoo
Only override TLS settings if scheme is specified
2015-12-19 08:47:46 +01:00
Anderson Mesquita
7eb9b9b1c6 Merge pull request #2597 from andersonvom/html
Fix alignment of settings fields
2015-12-18 22:48:00 -05:00
Anderson Mesquita
69b35b2ede Fix alignment of settings fields
When upgrade info is not available and the "Automatic Upgrades" options
is hidden, then "Global Discovery Server" wraps around and gets
misaligned. This fixes all that.
2015-12-18 22:40:56 -05:00
AudriusButkevicius
e8016abd97 Always exit via error select, making sure reader routine is exits (fixes #2547) 2015-12-18 22:29:04 +00:00
Anderson Mesquita
e54036be25 Reuse existing ensureDir function 2015-12-18 16:24:48 -05:00
Anderson Mesquita
1dc894087c Rename noConsole to hideConsole
This avoids the double negative of having noConsole = false to represent
not hiding the console. It is also consistent with the action performed
by osutils.
2015-12-18 16:24:48 -05:00
Anderson Mesquita
b014967550 Rename guiAssets to assetDir for consistency 2015-12-18 16:24:48 -05:00
Anderson Mesquita
490962ccdb Move logFlags into RuntimeOptions 2015-12-18 16:24:48 -05:00
Anderson Mesquita
d10e81fb3d Remove most global variables from main.go
This takes advantage of the newly created parseCommandLineOptions()
function and makes it work so that it now returns a nice struct of
options rather than relying on global variables.

There are a few global variables left, but they will take a bit more
refactoring in order to be removed, so it'll happen in later commits.
2015-12-18 16:24:44 -05:00
AudriusButkevicius
33a87f54bb Only override TLS settings if scheme is specified 2015-12-18 19:42:39 +00:00
Jakob Borg
2cdfa59fbe Merge pull request #2588 from andersonvom/browser-only
Add command line option to open GUI (fixes #2210)
2015-12-17 08:41:31 +01:00
Jakob Borg
bebe74fa4a Merge pull request #2590 from andersonvom/refactor
Refactor main.go
2015-12-17 08:34:21 +01:00
Anderson Mesquita
4098f97735 Extract cmdline option parsing into a new function
Another step towards reducing the general size of the main() function
in favor of shorter, more focused functions.
2015-12-16 17:19:32 -05:00
Anderson Mesquita
a0b7ac402d Refactor main.ensureDir()
ensureDir() did not handle one last error case and there was some logic
in the main() function that belonged to ensureDir() as well. It was also
creating a directory with a hardcoded 0700 mode, regardless of what mode
was passed to it.

This refactors it a little to fix the broken behavior, avoid redundant
checks by taking advantage of the behavior of MkdirAll, and move the
extra logic from main() into ensureDir().
2015-12-16 17:19:26 -05:00
Anderson Mesquita
0ca4482977 Refactor upgrade and check upgrade cmdline options
The main() function is growing too big (142 lines as of the date of this
commit), so this attempts to extract some functionality out of there and
into their own functions to make it easier to reason about them and keep
functions short and concise.
2015-12-16 14:29:37 -05:00
Anderson Mesquita
5cf15db6e4 Add command line option to open GUI (fixes #2210)
--browser-only assumes syncthing is already running and will open the
user's browser in the URL:port currently used in the configuration if
the GUI is enabled.
2015-12-16 13:24:21 -05:00
Jakob Borg
6379d50f07 Sort and retabify AUTHORS & NICKS 2015-12-16 09:16:16 +01:00
Jakob Borg
5a2792ae24 Merge branch 'pr/2587'
* pr/2587:
  Add helper function to format JSON responses
2015-12-16 09:08:16 +01:00
Jakob Borg
9b26f3cd84 Add andersonvom 2015-12-16 09:08:03 +01:00
Anderson Mesquita
316be5ee34 Add helper function to format JSON responses
Every time a JSON object is returned in an HTTP response, the
appropriate header needs to be set and the object itself needs to be
encoded. Doing this in every function is repetitive and error prone
(getDBFile and postDBScan, for instance, never set any headers).

This adds a helper function to centralize the appropriate JSON response
handling.
2015-12-16 09:06:17 +01:00
Audrius Butkevicius
f208e6f0b6 Fix typos 2015-12-15 20:11:07 +00:00
Jakob Borg
7c8c131e1a Merge pull request #2582 from Zillode/debug-2580
Additional output on insufficient error (ref #2580)
2015-12-15 10:19:59 +01:00
Jakob Borg
81e71d7275 Merge pull request #2576 from AudriusButkevicius/reconn
More debug to reconnect loop
2015-12-15 10:18:47 +01:00
Audrius Butkevicius
fc6c4b8e16 Merge pull request #2583 from calmh/globerrors
Better error messages for globbing errors
2015-12-15 09:13:49 +00:00
Jakob Borg
2280566bca Better error messages for globbing errors 2015-12-15 10:04:13 +01:00
Jakob Borg
33173e76a0 Merge pull request #2581 from rumpelsepp/master
systemd: Remove exit code 2, due to #2578
2015-12-15 09:10:36 +01:00
Lode Hoste
cc0b9e5088 Additional output on insufficient error (ref #2580) 2015-12-15 08:45:33 +01:00
Stefan Tatschner
ecc72d7693 systemd: Remove exit code 2, due to #2578
PR #2578 enables us to remove the exit code 2 from the list of
success status codes, because SIGINT will be handled properly.
I have also converted STNORESTART to --no-restart for the sake
of consistency.
2015-12-15 08:38:08 +01:00
Jakob Borg
4ab4aeacb0 Example GUI override address (fixes #2530) 2015-12-14 14:39:07 +01:00
Audrius Butkevicius
348d12bd60 Merge pull request #2578 from calmh/sighup
Implement signal handling
2015-12-14 12:18:51 +00:00
Jakob Borg
7b686c1103 Implement signal handling
With this change, the behavior is as follows:

 - SIGTERM / SIGINT: Exit cleanly (exit code 0)
 - SIGHUP: Restart

This is the case both when hitting the monitor process or the inner
process, or if running with NORESTART (but then we won't restart,
obviously, just exit with code exitRestarting).

It enables "pkill -HUP syncthing" to do the right thing to restart all
Syncthings on package upgrade, for example.
2015-12-14 13:07:42 +01:00
Audrius Butkevicius
403583cfbb More debug to reconnect loop 2015-12-13 12:20:25 +00:00
Jakob Borg
29bff06cd6 Translation update 2015-12-13 10:39:09 +01:00
Jakob Borg
2b80057ac9 Minimal error handling refactor 2015-12-13 10:38:21 +01:00
Jakob Borg
c3625e16d7 Add wkennington 2015-12-13 10:35:34 +01:00
Audrius Butkevicius
f4642e9e66 Merge pull request #2574 from wkennington/master.upnp
upnp: Use a separate error for the error unmarshalling
2015-12-12 18:25:27 +00:00
William A. Kennington III
5bdf4c6143 upnp: Use a separate error for the error unmarshalling
Previously, when unmarshing the SOAP error code data we would overwrite
the original err, typically with null since the parsing of the error
code information succeeds. If we don't have a upnp 725 error, we would fall
back to returning null or no error. This broke our upnp error handling
logic for AddPortMappings as it would think it succeeds if it gets a 718
permission error.
2015-12-12 09:58:23 -08:00
Jakob Borg
80aaf6a065 Merge pull request #2513 from calmh/noandroidhax
Remove Android hacks (fixes #2505)
2015-12-08 12:53:10 +01:00
Jakob Borg
3025caf932 Also remove IP per disco server 2015-12-08 10:10:34 +01:00
Jakob Borg
95cfc50fbd Remove Android hacks (fixes #2505)
Also fixes what I think migh thave been a bug where we did not use the
proxy for usage reports. And removes the BuildEnv field that we don't
need any more.
2015-12-08 10:10:06 +01:00
Jakob Borg
ded0925155 Neater build.sh all 2015-12-08 09:33:34 +01:00
Jakob Borg
931408037c Build should use -i (keep built pkg:s) and -v (verbose) 2015-12-08 09:14:52 +01:00
Jakob Borg
3318651565 Correct type assertion in verbose logger, restart (fixes #2561) 2015-12-07 08:04:09 +01:00
Audrius Butkevicius
2ab07f3aac Merge pull request #2558 from canton7/feature/logging-debug
Only print 'Enabled/Disabled debug data for ...' if it was enabled/disabled
2015-12-06 17:57:17 +00:00
Antony Male
1340e54327 Only print 'Enabled/Disabled debug data for ...' if it was enabled/disabled 2015-12-06 17:15:55 +00:00
Jakob Borg
1b6e4645b1 Update docs & translations 2015-12-06 13:09:22 +01:00
Jakob Borg
a6a573f5dc Fix STTRACE=http (it should use the http debug logger) 2015-12-05 15:59:35 +01:00
Jakob Borg
415415b5b2 Response with Bad Request to bad config posts, not Internal Server Errors
The error is the clients fault, or so we think, so we should not accept
the blame.
2015-12-05 15:56:10 +01:00
Audrius Butkevicius
38e9b92c42 Merge pull request #2550 from canton7/feature/deadline
staticClient.connect(): don't handshake twice (fixes #2547, #2548)
2015-12-04 12:05:28 +00:00
Antony Male
345d727936 staticClient.connect(): don't handshake twice (fixes #2547, #2548)
The first handshake occurred before setting the Deadline, which could
cause an unintended hang.
2015-12-04 12:01:01 +00:00
Nate Morrison
86e8e5199e Filenames added to audit log in the LocalIndexUpdated event (fixes #2549) 2015-12-04 08:42:00 +01:00
Jakob Borg
739979a116 Add nrm21 2015-12-04 08:38:45 +01:00
Jakob Borg
7cbc81adca Merge pull request #2545 from rumpelsepp/master
Add syncthing-relay(7)
2015-12-01 15:54:10 +01:00
Stefan Tatschner
737d0fa23c Add syncthing-relay(7) 2015-12-01 15:34:58 +01:00
64 changed files with 2108 additions and 984 deletions

12
AUTHORS
View File

@@ -1,8 +1,9 @@
# This is the official list of Syncthing authors for copyright purposes.
Aaron Bieber <qbit@deftly.net>
Adam Piggott <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
Adam Piggott <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
Alexander Graf <register-github@alex-graf.de>
Anderson Mesquita <andersonvom@gmail.com>
Andrew Dunham <andrew@du.nham.ca>
Antony Male <antony.male@gmail.com>
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com> <frioux@gmail.com>
@@ -34,9 +35,10 @@ Felix Unterpaintner <bigbear2nd@gmail.com>
Francois-Xavier Gsell <fxgsell@gmail.com>
Frank Isemann <frank@isemann.name>
Gilli Sigurdsson <gilli@vx.is>
Jaakko Hannikainen <jgke@jgke.fi>
Jacek Szafarkiewicz <szafar@linux.pl>
Jakob Borg <jakob@nym.se>
Jake Peterson <jake@acogdev.com>
Jakob Borg <jakob@nym.se>
James Patterson <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
Jaroslav Malec <dzardacz@gmail.com>
Jens Diemer <github.com@jensdiemer.de> <git@jensdiemer.de>
@@ -54,6 +56,7 @@ Matt Burke <mburke@amplify.com> <burkemw3@gmail.com>
Michael Jephcote <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
Michael Ploujnikov <ploujj@gmail.com>
Michael Tilli <pyfisch@gmail.com>
Nate Morrison <natemorrison@gmail.com>
Pascal Jungblut <github@pascalj.com> <mail@pascal-jungblut.com>
Peter Hoeg <peter@speartail.com>
Philippe Schommers <philippe@schommers.be>
@@ -62,14 +65,15 @@ Piotr Bejda <piotrb10@gmail.com>
Ryan Sullivan <kayoticsully@gmail.com>
Scott Klupfel <kluppy@going2blue.com>
Sergey Mishin <ralder@yandex.ru>
Stefan Tatschner <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
Stefan Kuntz <stefan.github@gmail.com> <Stefan.github@gmail.com>
Stefan Tatschner <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
Tim Abell <tim@timwise.co.uk>
Tobias Nygren <tnn@nygren.pp.se>
Tomas Cerveny <kozec@kozec.com>
Tully Robinson <tully@tojr.org>
Tyler Brazier <tyler@tylerbrazier.com>
Veeti Paananen <veeti.paananen@rojekti.fi>
Vil Brekin <vilbrekin@gmail.com>
Victor Buinsky <vix_booja@tut.by>
Vil Brekin <vilbrekin@gmail.com>
William A. Kennington III <william@wkennington.com>
Yannic A. <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>

4
Godeps/Godeps.json generated
View File

@@ -1,6 +1,6 @@
{
"ImportPath": "github.com/syncthing/syncthing",
"GoVersion": "go1.5.1",
"GoVersion": "go1.5.2",
"Packages": [
"./cmd/..."
],
@@ -31,7 +31,7 @@
},
{
"ImportPath": "github.com/kardianos/osext",
"Rev": "345163ffe35aa66560a4cd7dddf00f3ae21c9fda"
"Rev": "431e263e413efe4446ede50cec4819b26659fbe7"
},
{
"ImportPath": "github.com/rcrowley/go-metrics",

View File

@@ -7,12 +7,18 @@ package osext
import "path/filepath"
var cx, ce = executableClean()
func executableClean() (string, error) {
p, err := executable()
return filepath.Clean(p), err
}
// Executable returns an absolute path that can be used to
// re-invoke the current program.
// It may not be valid after the current program exits.
func Executable() (string, error) {
p, err := executable()
return filepath.Clean(p), err
return cx, ce
}
// Returns same path as Executable, returns just the folder

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux netbsd solaris dragonfly
// +build linux netbsd solaris dragonfly freebsd
package osext
@@ -27,6 +27,8 @@ func executable() (string, error) {
return execpath, nil
case "netbsd":
return os.Readlink("/proc/curproc/exe")
case "freebsd":
return os.Readlink("/proc/curproc/file")
case "dragonfly":
return os.Readlink("/proc/curproc/file")
case "solaris":

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin freebsd openbsd
// +build darwin openbsd
package osext
@@ -53,30 +53,30 @@ func executable() (string, error) {
// NULL terminated arguments.
var args []string
argv := uintptr(unsafe.Pointer(&buf[0]))
Loop:
for {
argp := *(**[1<<20]byte)(unsafe.Pointer(argv))
if argp == nil {
break
}
for i := 0; uintptr(i) < n; i++ {
// we don't want the full arguments list
if string(argp[i]) == " " {
break Loop
}
if argp[i] != 0 {
continue
}
args = append(args, string(argp[:i]))
n -= uintptr(i)
break
}
if n < unsafe.Sizeof(argv) {
break
}
argv += unsafe.Sizeof(argv)
n -= unsafe.Sizeof(argv)
Loop:
for {
argp := *(**[1 << 20]byte)(unsafe.Pointer(argv))
if argp == nil {
break
}
for i := 0; uintptr(i) < n; i++ {
// we don't want the full arguments list
if string(argp[i]) == " " {
break Loop
}
if argp[i] != 0 {
continue
}
args = append(args, string(argp[:i]))
n -= uintptr(i)
break
}
if n < unsafe.Sizeof(argv) {
break
}
argv += unsafe.Sizeof(argv)
n -= unsafe.Sizeof(argv)
}
execPath = args[0]
// There is no canonical way to get an executable path on
// OpenBSD, so check PATH in case we are called directly

26
NICKS
View File

@@ -1,19 +1,11 @@
# This file maps email addresses used in commits to nicks used the changelog.
AudriusButkevicius <audrius.butkevicius@gmail.com>
Cathryne <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
KayoticSully <kayoticsully@gmail.com>
LordLandon <lordlandon@gmail.com>
Moter8 <moter8@gmail.com>
Nutomic <me@nutomic.com>
Rewt0r <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
Stefan-Code <stefan.github@gmail.com> <Stefan.github@gmail.com>
Vilbrekin <vilbrekin@gmail.com>
Zillode <zillode@zillode.be>
acogdev <jake@acogdev.com>
alex2108 <register-github@alex-graf.de>
andersonvom <andersonvom@gmail.com>
andrew-d <andrew@du.nham.ca>
asdil12 <dominik@heidler.eu>
AudriusButkevicius <audrius.butkevicius@gmail.com>
bencurthoys <ben@bencurthoys.com>
bigbear2nd <bigbear2nd@gmail.com>
brbecker <brbecker@gmail.com>
@@ -24,6 +16,7 @@ buinsky <vix_booja@tut.by>
burkemw3 <mburke@amplify.com> <burkemw3@gmail.com>
calmh <jakob@nym.se>
canton7 <antony.male@gmail.com>
Cathryne <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
cdata <chris@scriptolo.gy>
cdhowie <me@chrishowie.com>
ceh <emil@hessman.se>
@@ -39,18 +32,24 @@ gillisig <gilli@vx.is>
hadogenes <szafar@linux.pl>
jarlebring <jarlebring@gmail.com>
jedie <github.com@jensdiemer.de> <git@jensdiemer.de>
jgke <jgke@jgke.fi>
jpjp <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
kamadak <kamada@nanohz.org>
KayoticSully <kayoticsully@gmail.com>
kilburn <kilburn@la3.org>
kluppy <kluppy@going2blue.com>
kozec <kozec@kozec.com>
krozycki <rozycki.karol@gmail.com>
LordLandon <lordlandon@gmail.com>
marcindziadus <dziadus.marcin@gmail.com>
marclaporte <marc@marclaporte.com>
mateon1 <matin1111@wp.pl>
mogwa1 <devriesb@gmail.com>
moshen <moshen.colin@gmail.com>
Moter8 <moter8@gmail.com>
mvdan <mvdan@mvdan.cc>
nrm21 <natemorrison@gmail.com>
Nutomic <me@nutomic.com>
pascalj <github@pascalj.com> <mail@pascal-jungblut.com>
peterhoeg <peter@speartail.com>
philips <brandon@ifup.org>
@@ -60,16 +59,21 @@ pluby <phill.luby@newredo.com>
pyfisch <pyfisch@gmail.com>
qbit <qbit@deftly.net>
ralder <ralder@yandex.ru>
Rewt0r <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
rumpelsepp <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
sciurius <jvromans@squirrel.nl>
seehuhn <voss@seehuhn.de>
snnd <dw@risu.io>
simplypeachy <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
snnd <dw@risu.io>
Stefan-Code <stefan.github@gmail.com> <Stefan.github@gmail.com>
timabell <tim@timwise.co.uk>
tnn2 <tnn@nygren.pp.se>
tojrobinson <tully@tojr.org>
tylerbrazier <tyler@tylerbrazier.com>
uok <ueomkail@gmail.com> <uok@users.noreply.github.com>
veeti <veeti.paananen@rojekti.fi>
Vilbrekin <vilbrekin@gmail.com>
wkennington <william@wkennington.com>
wsgcsysadmin <e.meitner@willystreet.coo>
Zillode <zillode@zillode.be>
zukoo <fxgsell@gmail.com>

View File

@@ -219,7 +219,7 @@ func build(pkg string, tags []string) {
}
rmr(binary)
args := []string{"build", "-ldflags", ldflags()}
args := []string{"build", "-i", "-v", "-ldflags", ldflags()}
if len(tags) > 0 {
args = append(args, "-tags", strings.Join(tags, ","))
}
@@ -448,7 +448,6 @@ func ldflags() string {
fmt.Fprintf(b, " -X main.BuildStamp%c%d", sep, buildStamp())
fmt.Fprintf(b, " -X main.BuildUser%c%s", sep, buildUser())
fmt.Fprintf(b, " -X main.BuildHost%c%s", sep, buildHost())
fmt.Fprintf(b, " -X main.BuildEnv%c%s", sep, buildEnvironment())
return b.String()
}

View File

@@ -74,27 +74,26 @@ case "${1:-default}" in
;;
all)
build -goos darwin -goarch amd64 tar
platforms=(
darwin-amd64 dragonfly-amd64 freebsd-amd64 linux-amd64 netbsd-amd64 openbsd-amd64 solaris-amd64 windows-amd64
freebsd-386 linux-386 netbsd-386 openbsd-386 windows-386
linux-arm
)
build -goos dragonfly -goarch amd64 tar
for plat in "${platforms[@]}"; do
echo Building "$plat"
build -goos freebsd -goarch 386 tar
build -goos freebsd -goarch amd64 tar
goos="${plat%-*}"
goarch="${plat#*-}"
dist="tar"
build -goos linux -goarch 386 tar
build -goos linux -goarch amd64 tar
build -goos linux -goarch arm tar
if [[ $goos == "windows" ]]; then
dist="zip"
fi
build -goos netbsd -goarch 386 tar
build -goos netbsd -goarch amd64 tar
build -goos openbsd -goarch 386 tar
build -goos openbsd -goarch amd64 tar
build -goos solaris -goarch amd64 tar
build -goos windows -goarch 386 zip
build -goos windows -goarch amd64 zip
build -goos "$goos" -goarch "$goarch" "$dist"
echo
done
;;
test-cov)

View File

@@ -15,14 +15,14 @@ import (
)
type addressLister struct {
upnpSvc *upnpSvc
cfg *config.Wrapper
upnpService *upnpService
cfg *config.Wrapper
}
func newAddressLister(upnpSvc *upnpSvc, cfg *config.Wrapper) *addressLister {
func newAddressLister(upnpService *upnpService, cfg *config.Wrapper) *addressLister {
return &addressLister{
upnpSvc: upnpSvc,
cfg: cfg,
upnpService: upnpService,
cfg: cfg,
}
}
@@ -73,10 +73,10 @@ func (e *addressLister) addresses(includePrivateIPV4 bool) []string {
}
}
// Get an external port mapping from the upnpSvc, if it has one. If so,
// Get an external port mapping from the upnpService, if it has one. If so,
// add it as another unspecified address.
if e.upnpSvc != nil {
if port := e.upnpSvc.ExternalPort(); port != 0 {
if e.upnpService != nil {
if port := e.upnpService.ExternalPort(); port != 0 {
addrs = append(addrs, fmt.Sprintf("tcp://:%d", port))
}
}

View File

@@ -13,17 +13,17 @@ import (
"github.com/syncthing/syncthing/lib/events"
)
// The auditSvc subscribes to events and writes these in JSON format, one
// The auditService subscribes to events and writes these in JSON format, one
// event per line, to the specified writer.
type auditSvc struct {
type auditService struct {
w io.Writer // audit destination
stop chan struct{} // signals time to stop
started chan struct{} // signals startup complete
stopped chan struct{} // signals stop complete
}
func newAuditSvc(w io.Writer) *auditSvc {
return &auditSvc{
func newAuditService(w io.Writer) *auditService {
return &auditService{
w: w,
stop: make(chan struct{}),
started: make(chan struct{}),
@@ -32,7 +32,7 @@ func newAuditSvc(w io.Writer) *auditSvc {
}
// Serve runs the audit service.
func (s *auditSvc) Serve() {
func (s *auditService) Serve() {
defer close(s.stopped)
sub := events.Default.Subscribe(events.AllEvents)
defer events.Default.Unsubscribe(sub)
@@ -52,18 +52,18 @@ func (s *auditSvc) Serve() {
}
// Stop stops the audit service.
func (s *auditSvc) Stop() {
func (s *auditService) Stop() {
close(s.stop)
}
// WaitForStart returns once the audit service is ready to receive events, or
// immediately if it's already running.
func (s *auditSvc) WaitForStart() {
func (s *auditService) WaitForStart() {
<-s.started
}
// WaitForStop returns once the audit service has stopped.
// (Needed by the tests.)
func (s *auditSvc) WaitForStop() {
func (s *auditService) WaitForStop() {
<-s.stopped
}

View File

@@ -17,13 +17,13 @@ import (
func TestAuditService(t *testing.T) {
buf := new(bytes.Buffer)
svc := newAuditSvc(buf)
service := newAuditService(buf)
// Event sent before start, will not be logged
events.Default.Log(events.Ping, "the first event")
go svc.Serve()
svc.WaitForStart()
go service.Serve()
service.WaitForStart()
// Event that should end up in the audit log
events.Default.Log(events.Ping, "the second event")
@@ -31,8 +31,8 @@ func TestAuditService(t *testing.T) {
// We need to give the events time to arrive, since the channels are buffered etc.
time.Sleep(10 * time.Millisecond)
svc.Stop()
svc.WaitForStop()
service.Stop()
service.WaitForStop()
// This event should not be logged, since we have stopped.
events.Default.Log(events.Ping, "the third event")

View File

@@ -48,16 +48,16 @@ var (
startTime = time.Now()
)
type apiSvc struct {
type apiService struct {
id protocol.DeviceID
cfg *config.Wrapper
assetDir string
model *model.Model
eventSub *events.BufferedSubscription
discoverer *discover.CachingMux
relaySvc *relay.Svc
relayService *relay.Service
listener net.Listener
fss *folderSummarySvc
fss *folderSummaryService
stop chan struct{}
systemConfigMut sync.Mutex
@@ -65,26 +65,26 @@ type apiSvc struct {
systemLog *logger.Recorder
}
func newAPISvc(id protocol.DeviceID, cfg *config.Wrapper, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc, errors, systemLog *logger.Recorder) (*apiSvc, error) {
svc := &apiSvc{
func newAPIService(id protocol.DeviceID, cfg *config.Wrapper, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.CachingMux, relayService *relay.Service, errors, systemLog *logger.Recorder) (*apiService, error) {
service := &apiService{
id: id,
cfg: cfg,
assetDir: assetDir,
model: m,
eventSub: eventSub,
discoverer: discoverer,
relaySvc: relaySvc,
relayService: relayService,
systemConfigMut: sync.NewMutex(),
guiErrors: errors,
systemLog: systemLog,
}
var err error
svc.listener, err = svc.getListener(cfg.GUI())
return svc, err
service.listener, err = service.getListener(cfg.GUI())
return service, err
}
func (s *apiSvc) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
cert, err := tls.LoadX509KeyPair(locations[locHTTPSCertFile], locations[locHTTPSKeyFile])
if err != nil {
l.Infoln("Loading HTTPS certificate:", err)
@@ -130,7 +130,12 @@ func (s *apiSvc) getListener(guiCfg config.GUIConfiguration) (net.Listener, erro
return listener, nil
}
func (s *apiSvc) Serve() {
func sendJSON(w http.ResponseWriter, jsonObject interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(jsonObject)
}
func (s *apiService) Serve() {
s.stop = make(chan struct{})
// The GET handlers
@@ -224,7 +229,7 @@ func (s *apiSvc) Serve() {
ReadTimeout: 10 * time.Second,
}
s.fss = newFolderSummarySvc(s.cfg, s.model)
s.fss = newFolderSummaryService(s.cfg, s.model)
defer s.fss.Stop()
s.fss.ServeBackground()
@@ -242,20 +247,20 @@ func (s *apiSvc) Serve() {
}
}
func (s *apiSvc) Stop() {
func (s *apiService) Stop() {
close(s.stop)
s.listener.Close()
}
func (s *apiSvc) String() string {
return fmt.Sprintf("apiSvc@%p", s)
func (s *apiService) String() string {
return fmt.Sprintf("apiService@%p", s)
}
func (s *apiSvc) VerifyConfiguration(from, to config.Configuration) error {
func (s *apiService) VerifyConfiguration(from, to config.Configuration) error {
return nil
}
func (s *apiSvc) CommitConfiguration(from, to config.Configuration) bool {
func (s *apiService) CommitConfiguration(from, to config.Configuration) bool {
if to.GUI == from.GUI {
return true
}
@@ -317,7 +322,7 @@ func debugMiddleware(h http.Handler) http.Handler {
written = rf.Int()
}
}
l.Debugf("http: %s %q: status %d, %d bytes in %.02f ms", r.Method, r.URL.String(), status, written, ms)
httpl.Debugf("http: %s %q: status %d, %d bytes in %.02f ms", r.Method, r.URL.String(), status, written, ms)
}
})
}
@@ -365,16 +370,12 @@ func withDetailsMiddleware(id protocol.DeviceID, h http.Handler) http.Handler {
})
}
func (s *apiSvc) restPing(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]string{
"ping": "pong",
})
func (s *apiService) restPing(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{"ping": "pong"})
}
func (s *apiSvc) getSystemVersion(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]string{
func (s *apiService) getSystemVersion(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{
"version": Version,
"codename": Codename,
"longVersion": LongVersion,
@@ -383,29 +384,28 @@ func (s *apiSvc) getSystemVersion(w http.ResponseWriter, r *http.Request) {
})
}
func (s *apiSvc) getSystemDebug(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
func (s *apiService) getSystemDebug(w http.ResponseWriter, r *http.Request) {
names := l.Facilities()
enabled := l.FacilityDebugging()
sort.Strings(enabled)
json.NewEncoder(w).Encode(map[string]interface{}{
sendJSON(w, map[string]interface{}{
"facilities": names,
"enabled": enabled,
})
}
func (s *apiSvc) postSystemDebug(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postSystemDebug(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
q := r.URL.Query()
for _, f := range strings.Split(q.Get("enable"), ",") {
if f == "" {
if f == "" || l.ShouldDebug(f) {
continue
}
l.SetDebug(f, true)
l.Infof("Enabled debug data for %q", f)
}
for _, f := range strings.Split(q.Get("disable"), ",") {
if f == "" {
if f == "" || !l.ShouldDebug(f) {
continue
}
l.SetDebug(f, false)
@@ -413,7 +413,7 @@ func (s *apiSvc) postSystemDebug(w http.ResponseWriter, r *http.Request) {
}
}
func (s *apiSvc) getDBBrowse(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getDBBrowse(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
prefix := qs.Get("prefix")
@@ -424,14 +424,10 @@ func (s *apiSvc) getDBBrowse(w http.ResponseWriter, r *http.Request) {
levels = -1
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
tree := s.model.GlobalDirectoryTree(folder, prefix, levels, dirsonly)
json.NewEncoder(w).Encode(tree)
sendJSON(w, s.model.GlobalDirectoryTree(folder, prefix, levels, dirsonly))
}
func (s *apiSvc) getDBCompletion(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getDBCompletion(w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var folder = qs.Get("folder")
var deviceStr = qs.Get("device")
@@ -442,20 +438,15 @@ func (s *apiSvc) getDBCompletion(w http.ResponseWriter, r *http.Request) {
return
}
res := map[string]float64{
sendJSON(w, map[string]float64{
"completion": s.model.Completion(device, folder),
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(res)
})
}
func (s *apiSvc) getDBStatus(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
res := folderSummary(s.cfg, s.model, folder)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(res)
sendJSON(w, folderSummary(s.cfg, s.model, folder))
}
func folderSummary(cfg *config.Wrapper, m *model.Model, folder string) map[string]interface{} {
@@ -497,13 +488,13 @@ func folderSummary(cfg *config.Wrapper, m *model.Model, folder string) map[strin
return res
}
func (s *apiSvc) postDBOverride(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postDBOverride(w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var folder = qs.Get("folder")
go s.model.Override(folder)
}
func (s *apiSvc) getDBNeed(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
@@ -520,38 +511,29 @@ func (s *apiSvc) getDBNeed(w http.ResponseWriter, r *http.Request) {
progress, queued, rest, total := s.model.NeedFolderFiles(folder, page, perpage)
// Convert the struct to a more loose structure, and inject the size.
output := map[string]interface{}{
sendJSON(w, map[string]interface{}{
"progress": s.toNeedSlice(progress),
"queued": s.toNeedSlice(queued),
"rest": s.toNeedSlice(rest),
"total": total,
"page": page,
"perpage": perpage,
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(output)
})
}
func (s *apiSvc) getSystemConnections(w http.ResponseWriter, r *http.Request) {
var res = s.model.ConnectionStats()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(res)
func (s *apiService) getSystemConnections(w http.ResponseWriter, r *http.Request) {
sendJSON(w, s.model.ConnectionStats())
}
func (s *apiSvc) getDeviceStats(w http.ResponseWriter, r *http.Request) {
var res = s.model.DeviceStatistics()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(res)
func (s *apiService) getDeviceStats(w http.ResponseWriter, r *http.Request) {
sendJSON(w, s.model.DeviceStatistics())
}
func (s *apiSvc) getFolderStats(w http.ResponseWriter, r *http.Request) {
var res = s.model.FolderStatistics()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(res)
func (s *apiService) getFolderStats(w http.ResponseWriter, r *http.Request) {
sendJSON(w, s.model.FolderStatistics())
}
func (s *apiSvc) getDBFile(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getDBFile(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
file := qs.Get("file")
@@ -559,26 +541,25 @@ func (s *apiSvc) getDBFile(w http.ResponseWriter, r *http.Request) {
lf, _ := s.model.CurrentFolderFile(folder, file)
av := s.model.Availability(folder, file)
json.NewEncoder(w).Encode(map[string]interface{}{
sendJSON(w, map[string]interface{}{
"global": jsonFileInfo(gf),
"local": jsonFileInfo(lf),
"availability": av,
})
}
func (s *apiSvc) getSystemConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(s.cfg.Raw())
func (s *apiService) getSystemConfig(w http.ResponseWriter, r *http.Request) {
sendJSON(w, s.cfg.Raw())
}
func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
s.systemConfigMut.Lock()
defer s.systemConfigMut.Unlock()
to, err := config.ReadJSON(r.Body, myID)
if err != nil {
l.Warnln("decoding posted config:", err)
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
@@ -587,7 +568,7 @@ func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
hash, err := bcrypt.GenerateFromPassword([]byte(to.GUI.Password), 0)
if err != nil {
l.Warnln("bcrypting password:", err)
http.Error(w, err.Error(), 500)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -614,17 +595,16 @@ func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
s.cfg.Save()
}
func (s *apiSvc) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync})
func (s *apiService) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]bool{"configInSync": configInSync})
}
func (s *apiSvc) postSystemRestart(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postSystemRestart(w http.ResponseWriter, r *http.Request) {
s.flushResponse(`{"ok": "restarting"}`, w)
go restart()
}
func (s *apiSvc) postSystemReset(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postSystemReset(w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
folder := qs.Get("folder")
@@ -650,12 +630,12 @@ func (s *apiSvc) postSystemReset(w http.ResponseWriter, r *http.Request) {
go restart()
}
func (s *apiSvc) postSystemShutdown(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postSystemShutdown(w http.ResponseWriter, r *http.Request) {
s.flushResponse(`{"ok": "shutting down"}`, w)
go shutdown()
}
func (s *apiSvc) flushResponse(resp string, w http.ResponseWriter) {
func (s *apiService) flushResponse(resp string, w http.ResponseWriter) {
w.Write([]byte(resp + "\n"))
f := w.(http.Flusher)
f.Flush()
@@ -664,7 +644,7 @@ func (s *apiSvc) flushResponse(resp string, w http.ResponseWriter) {
var cpuUsagePercent [10]float64 // The last ten seconds
var cpuUsageLock = sync.NewRWMutex()
func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
@@ -688,12 +668,12 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
res["discoveryMethods"] = discoMethods
res["discoveryErrors"] = discoErrors
}
if s.relaySvc != nil {
if s.relayService != nil {
res["relaysEnabled"] = true
relayClientStatus := make(map[string]bool)
relayClientLatency := make(map[string]int)
for _, relay := range s.relaySvc.Relays() {
latency, ok := s.relaySvc.RelayStatus(relay)
for _, relay := range s.relayService.Relays() {
latency, ok := s.relayService.RelayStatus(relay)
relayClientStatus[relay] = ok
relayClientLatency[relay] = int(latency / time.Millisecond)
}
@@ -711,39 +691,35 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
res["uptime"] = int(time.Since(startTime).Seconds())
res["startTime"] = startTime
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(res)
sendJSON(w, res)
}
func (s *apiSvc) getSystemError(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string][]logger.Line{
func (s *apiService) getSystemError(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string][]logger.Line{
"errors": s.guiErrors.Since(time.Time{}),
})
}
func (s *apiSvc) postSystemError(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postSystemError(w http.ResponseWriter, r *http.Request) {
bs, _ := ioutil.ReadAll(r.Body)
r.Body.Close()
l.Warnln(string(bs))
}
func (s *apiSvc) postSystemErrorClear(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postSystemErrorClear(w http.ResponseWriter, r *http.Request) {
s.guiErrors.Clear()
}
func (s *apiSvc) getSystemLog(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getSystemLog(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
since, err := time.Parse(time.RFC3339, q.Get("since"))
l.Debugln(err)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string][]logger.Line{
sendJSON(w, map[string][]logger.Line{
"messages": s.systemLog.Since(since),
})
}
func (s *apiSvc) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
since, err := time.Parse(time.RFC3339, q.Get("since"))
l.Debugln(err)
@@ -754,7 +730,7 @@ func (s *apiSvc) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
}
}
func (s *apiSvc) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request) {
stats := make(map[string]interface{})
metrics.Each(func(name string, intf interface{}) {
if m, ok := intf.(*metrics.StandardTimer); ok {
@@ -774,8 +750,7 @@ func (s *apiSvc) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request) {
w.Write(bs)
}
func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
func (s *apiService) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
devices := make(map[string]discover.CacheEntry)
if s.discoverer != nil {
@@ -787,17 +762,15 @@ func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
}
}
json.NewEncoder(w).Encode(devices)
sendJSON(w, devices)
}
func (s *apiSvc) getReport(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(reportData(s.cfg, s.model))
func (s *apiService) getReport(w http.ResponseWriter, r *http.Request) {
sendJSON(w, reportData(s.cfg, s.model))
}
func (s *apiSvc) getDBIgnores(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
ignores, patterns, err := s.model.GetIgnores(qs.Get("folder"))
if err != nil {
@@ -805,13 +778,13 @@ func (s *apiSvc) getDBIgnores(w http.ResponseWriter, r *http.Request) {
return
}
json.NewEncoder(w).Encode(map[string][]string{
sendJSON(w, map[string][]string{
"ignore": ignores,
"patterns": patterns,
})
}
func (s *apiSvc) postDBIgnores(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postDBIgnores(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
var data map[string][]string
@@ -832,7 +805,7 @@ func (s *apiSvc) postDBIgnores(w http.ResponseWriter, r *http.Request) {
s.getDBIgnores(w, r)
}
func (s *apiSvc) getEvents(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getEvents(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
sinceStr := qs.Get("since")
limitStr := qs.Get("limit")
@@ -841,8 +814,6 @@ func (s *apiSvc) getEvents(w http.ResponseWriter, r *http.Request) {
s.fss.gotEventRequest()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// Flush before blocking, to indicate that we've received the request
// and that it should not be retried.
f := w.(http.Flusher)
@@ -853,10 +824,10 @@ func (s *apiSvc) getEvents(w http.ResponseWriter, r *http.Request) {
evs = evs[len(evs)-limit:]
}
json.NewEncoder(w).Encode(evs)
sendJSON(w, evs)
}
func (s *apiSvc) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
if noUpgrade {
http.Error(w, upgrade.ErrUpgradeUnsupported.Error(), 500)
return
@@ -872,38 +843,36 @@ func (s *apiSvc) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
res["newer"] = upgrade.CompareVersions(rel.Tag, Version) == upgrade.Newer
res["majorNewer"] = upgrade.CompareVersions(rel.Tag, Version) == upgrade.MajorNewer
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(res)
sendJSON(w, res)
}
func (s *apiSvc) getDeviceID(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getDeviceID(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
idStr := qs.Get("id")
id, err := protocol.DeviceIDFromString(idStr)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err == nil {
json.NewEncoder(w).Encode(map[string]string{
sendJSON(w, map[string]string{
"id": id.String(),
})
} else {
json.NewEncoder(w).Encode(map[string]string{
sendJSON(w, map[string]string{
"error": err.Error(),
})
}
}
func (s *apiSvc) getLang(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getLang(w http.ResponseWriter, r *http.Request) {
lang := r.Header.Get("Accept-Language")
var langs []string
for _, l := range strings.Split(lang, ",") {
parts := strings.SplitN(l, ";", 2)
langs = append(langs, strings.ToLower(strings.TrimSpace(parts[0])))
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(langs)
sendJSON(w, langs)
}
func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
rel, err := upgrade.LatestRelease(s.cfg.Options().ReleasesURL, Version)
if err != nil {
l.Warnln("getting latest release:", err)
@@ -925,7 +894,7 @@ func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
}
}
func (s *apiSvc) postSystemPause(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postSystemPause(w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var deviceStr = qs.Get("device")
@@ -938,7 +907,7 @@ func (s *apiSvc) postSystemPause(w http.ResponseWriter, r *http.Request) {
s.model.PauseDevice(device)
}
func (s *apiSvc) postSystemResume(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postSystemResume(w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var deviceStr = qs.Get("device")
@@ -951,7 +920,7 @@ func (s *apiSvc) postSystemResume(w http.ResponseWriter, r *http.Request) {
s.model.ResumeDevice(device)
}
func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postDBScan(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
if folder != "" {
@@ -971,13 +940,13 @@ func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
errors := s.model.ScanFolders()
if len(errors) > 0 {
http.Error(w, "Error scanning folders", 500)
json.NewEncoder(w).Encode(errors)
sendJSON(w, errors)
return
}
}
}
func (s *apiSvc) postDBPrio(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postDBPrio(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
file := qs.Get("file")
@@ -985,7 +954,7 @@ func (s *apiSvc) postDBPrio(w http.ResponseWriter, r *http.Request) {
s.getDBNeed(w, r)
}
func (s *apiSvc) getQR(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getQR(w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var text = qs.Get("text")
code, err := qr.Encode(text, qr.M)
@@ -998,7 +967,7 @@ func (s *apiSvc) getQR(w http.ResponseWriter, r *http.Request) {
w.Write(code.PNG())
}
func (s *apiSvc) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
tot := map[string]float64{}
count := map[string]float64{}
@@ -1019,12 +988,10 @@ func (s *apiSvc) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
comp[device] = int(tot[device] / count[device])
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(comp)
sendJSON(w, comp)
}
func (s *apiSvc) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
current := qs.Get("current")
search, _ := osutil.ExpandTilde(current)
@@ -1043,7 +1010,8 @@ func (s *apiSvc) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
}
}
}
json.NewEncoder(w).Encode(ret)
sendJSON(w, ret)
}
type embeddedStatic struct {
@@ -1129,7 +1097,7 @@ func (s embeddedStatic) mimeTypeForFile(file string) string {
}
}
func (s *apiSvc) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
func (s *apiService) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
res := make([]jsonDBFileInfo, len(fs))
for i, f := range fs {
res[i] = jsonDBFileInfo(f)

View File

@@ -19,6 +19,7 @@ import (
_ "net/http/pprof"
"net/url"
"os"
"os/signal"
"path/filepath"
"regexp"
"runtime"
@@ -26,6 +27,7 @@ import (
"sort"
"strconv"
"strings"
"syscall"
"time"
"github.com/syncthing/syncthing/lib/config"
@@ -49,7 +51,6 @@ import (
var (
Version = "unknown-dev"
Codename = "Beryllium Bedbug"
BuildEnv = "default"
BuildStamp = "0"
BuildDate time.Time
BuildHost = "unknown"
@@ -108,20 +109,14 @@ func init() {
BuildDate = time.Unix(int64(stamp), 0)
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
LongVersion = fmt.Sprintf(`syncthing %s "%s" (%s %s-%s %s) %s@%s %s`, Version, Codename, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildEnv, BuildUser, BuildHost, date)
if os.Getenv("STTRACE") != "" {
logFlags = log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile
}
LongVersion = fmt.Sprintf(`syncthing %s "%s" (%s %s-%s) %s@%s %s`, Version, Codename, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
}
var (
myID protocol.DeviceID
confDir string
logFlags = log.Ltime
stop = make(chan int)
cert tls.Certificate
lans []*net.IPNet
myID protocol.DeviceID
stop = make(chan int)
cert tls.Certificate
lans []*net.IPNet
)
const (
@@ -189,170 +184,188 @@ The following are valid values for the STTRACE variable:
%s`
)
// Command line and environment options
// Environment options
var (
noUpgrade = os.Getenv("STNOUPGRADE") != ""
innerProcess = os.Getenv("STNORESTART") != "" || os.Getenv("STMONITORED") != ""
)
type RuntimeOptions struct {
confDir string
reset bool
showVersion bool
doUpgrade bool
doUpgradeCheck bool
upgradeTo string
noBrowser bool
noConsole bool
browserOnly bool
hideConsole bool
logFile string
auditEnabled bool
verbose bool
paused bool
noRestart = os.Getenv("STNORESTART") != ""
noUpgrade = os.Getenv("STNOUPGRADE") != ""
profiler = os.Getenv("STPROFILER")
guiAssets = os.Getenv("STGUIASSETS")
cpuProfile = os.Getenv("STCPUPROFILE") != ""
stRestarting = os.Getenv("STRESTART") != ""
innerProcess = os.Getenv("STNORESTART") != "" || os.Getenv("STMONITORED") != ""
)
guiAddress string
guiAPIKey string
generateDir string
noRestart bool
profiler string
assetDir string
cpuProfile bool
stRestarting bool
logFlags int
}
func main() {
if runtime.GOOS == "windows" {
// On Windows, we use a log file by default. Setting the -logfile flag
// to "-" disables this behavior.
flag.StringVar(&logFile, "logfile", "", "Log file name (use \"-\" for stdout)")
// We also add an option to hide the console window
flag.BoolVar(&noConsole, "no-console", false, "Hide console window")
} else {
flag.StringVar(&logFile, "logfile", "-", "Log file name (use \"-\" for stdout)")
func defaultRuntimeOptions() RuntimeOptions {
options := RuntimeOptions{
noRestart: os.Getenv("STNORESTART") != "",
profiler: os.Getenv("STPROFILER"),
assetDir: os.Getenv("STGUIASSETS"),
cpuProfile: os.Getenv("STCPUPROFILE") != "",
stRestarting: os.Getenv("STRESTART") != "",
logFlags: log.Ltime,
}
var guiAddress, guiAPIKey string
var generateDir string
flag.StringVar(&generateDir, "generate", "", "Generate key and config in specified dir, then exit")
flag.StringVar(&guiAddress, "gui-address", guiAddress, "Override GUI address")
flag.StringVar(&guiAPIKey, "gui-apikey", guiAPIKey, "Override GUI API key")
flag.StringVar(&confDir, "home", "", "Set configuration directory")
flag.IntVar(&logFlags, "logflags", logFlags, "Select information in log line prefix")
flag.BoolVar(&noBrowser, "no-browser", false, "Do not start browser")
flag.BoolVar(&noRestart, "no-restart", noRestart, "Do not restart; just exit")
flag.BoolVar(&reset, "reset", false, "Reset the database")
flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade")
flag.BoolVar(&doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
flag.BoolVar(&showVersion, "version", false, "Show version")
flag.StringVar(&upgradeTo, "upgrade-to", upgradeTo, "Force upgrade directly from specified URL")
flag.BoolVar(&auditEnabled, "audit", false, "Write events to audit file")
flag.BoolVar(&verbose, "verbose", false, "Print verbose log output")
flag.BoolVar(&paused, "paused", false, "Start with all devices paused")
if os.Getenv("STTRACE") != "" {
options.logFlags = log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile
}
if runtime.GOOS != "windows" {
// On non-Windows, we explicitly default to "-" which means stdout. On
// Windows, the blank options.logFile will later be replaced with the
// default path, unless the user has manually specified "-" or
// something else.
options.logFile = "-"
}
return options
}
func parseCommandLineOptions() RuntimeOptions {
options := defaultRuntimeOptions()
flag.StringVar(&options.generateDir, "generate", "", "Generate key and config in specified dir, then exit")
flag.StringVar(&options.guiAddress, "gui-address", options.guiAddress, "Override GUI address (e.g. \"http://192.0.2.42:8443\")")
flag.StringVar(&options.guiAPIKey, "gui-apikey", options.guiAPIKey, "Override GUI API key")
flag.StringVar(&options.confDir, "home", "", "Set configuration directory")
flag.IntVar(&options.logFlags, "logflags", options.logFlags, "Select information in log line prefix (see below)")
flag.BoolVar(&options.noBrowser, "no-browser", false, "Do not start browser")
flag.BoolVar(&options.browserOnly, "browser-only", false, "Open GUI in browser")
flag.BoolVar(&options.noRestart, "no-restart", options.noRestart, "Do not restart; just exit")
flag.BoolVar(&options.reset, "reset", false, "Reset the database")
flag.BoolVar(&options.doUpgrade, "upgrade", false, "Perform upgrade")
flag.BoolVar(&options.doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
flag.BoolVar(&options.showVersion, "version", false, "Show version")
flag.StringVar(&options.upgradeTo, "upgrade-to", options.upgradeTo, "Force upgrade directly from specified URL")
flag.BoolVar(&options.auditEnabled, "audit", false, "Write events to audit file")
flag.BoolVar(&options.verbose, "verbose", false, "Print verbose log output")
flag.BoolVar(&options.paused, "paused", false, "Start with all devices paused")
flag.StringVar(&options.logFile, "logfile", options.logFile, "Log file name (use \"-\" for stdout)")
if runtime.GOOS == "windows" {
// Allow user to hide the console window
flag.BoolVar(&options.hideConsole, "no-console", false, "Hide console window")
}
longUsage := fmt.Sprintf(extraUsage, baseDirs["config"], debugFacilities())
flag.Usage = usageFor(flag.CommandLine, usage, longUsage)
flag.Parse()
if guiAddress != "" {
return options
}
func main() {
options := parseCommandLineOptions()
l.SetFlags(options.logFlags)
if options.guiAddress != "" {
// The config picks this up from the environment.
os.Setenv("STGUIADDRESS", guiAddress)
os.Setenv("STGUIADDRESS", options.guiAddress)
}
if guiAPIKey != "" {
if options.guiAPIKey != "" {
// The config picks this up from the environment.
os.Setenv("STGUIAPIKEY", guiAPIKey)
os.Setenv("STGUIAPIKEY", options.guiAPIKey)
}
if noConsole {
if options.hideConsole {
osutil.HideConsole()
}
if confDir != "" {
if options.confDir != "" {
// Not set as default above because the string can be really long.
baseDirs["config"] = confDir
baseDirs["config"] = options.confDir
}
if err := expandLocations(); err != nil {
l.Fatalln(err)
}
if guiAssets == "" {
guiAssets = locations[locGUIAssets]
if options.logFile == "" {
// Blank means use the default logfile location. We must set this
// *after* expandLocations above.
options.logFile = locations[locLogFile]
}
if logFile == "" {
// Use the default log file location
logFile = locations[locLogFile]
if options.assetDir == "" {
// The asset dir is blank if STGUIASSETS wasn't set, in which case we
// should look for extra assets in the default place.
options.assetDir = locations[locGUIAssets]
}
if showVersion {
if options.showVersion {
fmt.Println(LongVersion)
return
}
l.SetFlags(logFlags)
if generateDir != "" {
generate(generateDir)
if options.browserOnly {
openGUI()
return
}
if info, err := os.Stat(baseDirs["config"]); err == nil && !info.IsDir() {
l.Fatalln("Config directory", baseDirs["config"], "is not a directory")
if options.generateDir != "" {
generate(options.generateDir)
return
}
// Ensure that our home directory exists.
ensureDir(baseDirs["config"], 0700)
if upgradeTo != "" {
err := upgrade.ToURL(upgradeTo)
if options.upgradeTo != "" {
err := upgrade.ToURL(options.upgradeTo)
if err != nil {
l.Fatalln("Upgrade:", err) // exits 1
}
l.Okln("Upgraded from", upgradeTo)
l.Okln("Upgraded from", options.upgradeTo)
return
}
if doUpgrade || doUpgradeCheck {
releasesURL := "https://api.github.com/repos/syncthing/syncthing/releases?per_page=30"
if cfg, _, err := loadConfig(locations[locConfigFile]); err == nil {
releasesURL = cfg.Options().ReleasesURL
}
rel, err := upgrade.LatestRelease(releasesURL, Version)
if err != nil {
l.Fatalln("Upgrade:", err) // exits 1
}
if upgrade.CompareVersions(rel.Tag, Version) <= 0 {
l.Infof("No upgrade available (current %q >= latest %q).", Version, rel.Tag)
os.Exit(exitNoUpgradeAvailable)
}
l.Infof("Upgrade available (current %q < latest %q)", Version, rel.Tag)
if doUpgrade {
// Use leveldb database locks to protect against concurrent upgrades
_, err = db.Open(locations[locDatabase])
if err != nil {
l.Infoln("Attempting upgrade through running Syncthing...")
err = upgradeViaRest()
if err != nil {
l.Fatalln("Upgrade:", err)
}
l.Okln("Syncthing upgrading")
return
}
err = upgrade.To(rel)
if err != nil {
l.Fatalln("Upgrade:", err) // exits 1
}
l.Okf("Upgraded to %q", rel.Tag)
}
if options.doUpgradeCheck {
checkUpgrade()
return
}
if reset {
if options.doUpgrade {
release := checkUpgrade()
performUpgrade(release)
return
}
if options.reset {
resetDB()
return
}
if noRestart {
syncthingMain()
if options.noRestart {
syncthingMain(options)
} else {
monitorMain()
monitorMain(options)
}
}
func openGUI() {
cfg, _ := loadConfig()
if cfg.GUI().Enabled {
openURL(cfg.GUI().URL())
} else {
l.Warnln("Browser: GUI is currently disabled")
}
}
@@ -361,17 +374,7 @@ func generate(generateDir string) {
if err != nil {
l.Fatalln("generate:", err)
}
info, err := os.Stat(dir)
if err == nil && !info.IsDir() {
l.Fatalln(dir, "is not a directory")
}
if err != nil && os.IsNotExist(err) {
err = osutil.MkdirAll(dir, 0700)
if err != nil {
l.Fatalln("generate:", err)
}
}
ensureDir(dir, 0700)
certFile, keyFile := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
@@ -428,11 +431,46 @@ func debugFacilities() string {
return b.String()
}
func upgradeViaRest() error {
cfg, err := config.Load(locations[locConfigFile], protocol.LocalDeviceID)
func checkUpgrade() upgrade.Release {
cfg, _ := loadConfig()
releasesURL := cfg.Options().ReleasesURL
release, err := upgrade.LatestRelease(releasesURL, Version)
if err != nil {
return err
l.Fatalln("Upgrade:", err)
}
if upgrade.CompareVersions(release.Tag, Version) <= 0 {
noUpgradeMessage := "No upgrade available (current %q >= latest %q)."
l.Infof(noUpgradeMessage, Version, release.Tag)
os.Exit(exitNoUpgradeAvailable)
}
l.Infof("Upgrade available (current %q < latest %q)", Version, release.Tag)
return release
}
func performUpgrade(release upgrade.Release) {
// Use leveldb database locks to protect against concurrent upgrades
_, err := db.Open(locations[locDatabase])
if err == nil {
err = upgrade.To(release)
if err != nil {
l.Fatalln("Upgrade:", err)
}
l.Okf("Upgraded to %q", release.Tag)
} else {
l.Infoln("Attempting upgrade through running Syncthing...")
err = upgradeViaRest()
if err != nil {
l.Fatalln("Upgrade:", err)
}
l.Okln("Syncthing upgrading")
os.Exit(exitUpgrading)
}
}
func upgradeViaRest() error {
cfg, _ := loadConfig()
target := cfg.GUI().URL()
r, _ := http.NewRequest("POST", target+"/rest/system/upgrade", nil)
r.Header.Set("X-API-Key", cfg.GUI().APIKey())
@@ -462,26 +500,28 @@ func upgradeViaRest() error {
return err
}
func syncthingMain() {
func syncthingMain(runtimeOptions RuntimeOptions) {
setupSignalHandling()
// Create a main service manager. We'll add things to this as we go along.
// We want any logging it does to go through our log system.
mainSvc := suture.New("main", suture.Spec{
mainService := suture.New("main", suture.Spec{
Log: func(line string) {
l.Debugln(line)
},
})
mainSvc.ServeBackground()
mainService.ServeBackground()
// Set a log prefix similar to the ID we will have later on, or early log
// lines look ugly.
l.SetPrefix("[start] ")
if auditEnabled {
startAuditing(mainSvc)
if runtimeOptions.auditEnabled {
startAuditing(mainService)
}
if verbose {
mainSvc.Add(newVerboseSvc())
if runtimeOptions.verbose {
mainService.Add(newVerboseService())
}
errors := logger.NewRecorder(l, logger.LevelWarn, maxSystemErrors, 0)
@@ -527,44 +567,17 @@ func syncthingMain() {
"myID": myID.String(),
})
// Prepare to be able to save configuration
cfgFile := locations[locConfigFile]
// Load the configuration file, if it exists.
// If it does not, create a template.
cfg, myName, err := loadConfig(cfgFile)
if err != nil {
if os.IsNotExist(err) {
l.Infoln("No config file; starting with empty defaults")
myName, _ = os.Hostname()
newCfg := defaultConfig(myName)
cfg = config.Wrap(cfgFile, newCfg)
cfg.Save()
l.Infof("Edit %s to taste or use the GUI\n", cfgFile)
} else {
l.Fatalln("Loading config:", err)
}
}
if cfg.Raw().OriginalVersion != config.CurrentVersion {
l.Infoln("Archiving a copy of old config file format")
// Archive a copy
osutil.Rename(cfgFile, cfgFile+fmt.Sprintf(".v%d", cfg.Raw().OriginalVersion))
// Save the new version
cfg.Save()
}
cfg := loadOrCreateConfig()
if err := checkShortIDs(cfg); err != nil {
l.Fatalln("Short device IDs are in conflict. Unlucky!\n Regenerate the device ID of one if the following:\n ", err)
l.Fatalln("Short device IDs are in conflict. Unlucky!\n Regenerate the device ID of one of the following:\n ", err)
}
if len(profiler) > 0 {
if len(runtimeOptions.profiler) > 0 {
go func() {
l.Debugln("Starting profiler on", profiler)
l.Debugln("Starting profiler on", runtimeOptions.profiler)
runtime.SetBlockProfileRate(1)
err := http.ListenAndServe(profiler, nil)
err := http.ListenAndServe(runtimeOptions.profiler, nil)
if err != nil {
l.Fatalln(err)
}
@@ -647,7 +660,7 @@ func syncthingMain() {
l.Infoln("Compacting database:", err)
}
m := model.NewModel(cfg, myID, myName, "syncthing", Version, ldb, protectedFiles)
m := model.NewModel(cfg, myID, myDeviceName(cfg), "syncthing", Version, ldb, protectedFiles)
cfg.Subscribe(m)
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
@@ -659,7 +672,7 @@ func syncthingMain() {
m.StartDeadlockDetector(20 * time.Minute)
}
if paused {
if runtimeOptions.paused {
for device := range cfg.Devices() {
m.PauseDevice(device)
}
@@ -685,7 +698,7 @@ func syncthingMain() {
}
}
mainSvc.Add(m)
mainService.Add(m)
// The default port we announce, possibly modified by setupUPnP next.
@@ -706,33 +719,33 @@ func syncthingMain() {
// Start UPnP
if opts.UPnPEnabled {
upnpSvc := newUPnPSvc(cfg, addr.Port)
mainSvc.Add(upnpSvc)
upnpService := newUPnPService(cfg, addr.Port)
mainService.Add(upnpService)
// The external address tracker needs to know about the UPnP service
// so it can check for an external mapped port.
addrList = newAddressLister(upnpSvc, cfg)
addrList = newAddressLister(upnpService, cfg)
} else {
addrList = newAddressLister(nil, cfg)
}
// Start relay management
var relaySvc *relay.Svc
var relayService *relay.Service
if opts.RelaysEnabled && (opts.GlobalAnnEnabled || opts.RelayWithoutGlobalAnn) {
relaySvc = relay.NewSvc(cfg, tlsCfg)
mainSvc.Add(relaySvc)
relayService = relay.NewService(cfg, tlsCfg)
mainService.Add(relayService)
}
// Start discovery
cachedDiscovery := discover.NewCachingMux()
mainSvc.Add(cachedDiscovery)
mainService.Add(cachedDiscovery)
if cfg.Options().GlobalAnnEnabled {
for _, srv := range cfg.GlobalDiscoveryServers() {
l.Infoln("Using discovery server", srv)
gd, err := discover.NewGlobal(srv, cert, addrList, relaySvc)
gd, err := discover.NewGlobal(srv, cert, addrList, relayService)
if err != nil {
l.Warnln("Global discovery:", err)
continue
@@ -747,14 +760,14 @@ func syncthingMain() {
if cfg.Options().LocalAnnEnabled {
// v4 broadcasts
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), addrList, relaySvc)
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), addrList, relayService)
if err != nil {
l.Warnln("IPv4 local discovery:", err)
} else {
cachedDiscovery.Add(bcd, 0, 0, ipv4LocalDiscoveryPriority)
}
// v6 multicasts
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, addrList, relaySvc)
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, addrList, relayService)
if err != nil {
l.Warnln("IPv6 local discovery:", err)
} else {
@@ -764,14 +777,14 @@ func syncthingMain() {
// GUI
setupGUI(mainSvc, cfg, m, apiSub, cachedDiscovery, relaySvc, errors, systemLog)
setupGUI(mainService, cfg, m, apiSub, cachedDiscovery, relayService, errors, systemLog, runtimeOptions)
// Start connection management
connectionSvc := connections.NewConnectionSvc(cfg, myID, m, tlsCfg, cachedDiscovery, relaySvc, bepProtocolName, tlsDefaultCommonName, lans)
mainSvc.Add(connectionSvc)
connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, relayService, bepProtocolName, tlsDefaultCommonName, lans)
mainService.Add(connectionService)
if cpuProfile {
if runtimeOptions.cpuProfile {
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
if err != nil {
log.Fatal(err)
@@ -829,17 +842,48 @@ func syncthingMain() {
code := <-stop
mainSvc.Stop()
mainService.Stop()
l.Okln("Exiting")
if cpuProfile {
if runtimeOptions.cpuProfile {
pprof.StopCPUProfile()
}
os.Exit(code)
}
func myDeviceName(cfg *config.Wrapper) string {
devices := cfg.Devices()
myName := devices[myID].Name
if myName == "" {
myName, _ = os.Hostname()
}
return myName
}
func setupSignalHandling() {
// Exit cleanly with "restarting" code on SIGHUP.
restartSign := make(chan os.Signal, 1)
sigHup := syscall.Signal(1)
signal.Notify(restartSign, sigHup)
go func() {
<-restartSign
stop <- exitRestarting
}()
// Exit with "success" code (no restart) on INT/TERM
stopSign := make(chan os.Signal, 1)
sigTerm := syscall.Signal(15)
signal.Notify(stopSign, os.Interrupt, sigTerm)
go func() {
<-stopSign
stop <- exitSuccess
}()
}
// printHashRate prints the hashing performance in MB/s, formatting it with
// appropriate precision for the value, i.e. 182 MB/s, 18 MB/s, 1.8 MB/s, 0.18
// MB/s.
@@ -856,47 +900,71 @@ func printHashRate() {
l.Infof("Single thread hash performance is ~%.*f MB/s", decimals, hashRate)
}
func loadConfig(cfgFile string) (*config.Wrapper, string, error) {
info, err := os.Stat(cfgFile)
if err != nil {
return nil, "", err
}
if !info.Mode().IsRegular() {
return nil, "", errors.New("configuration is not a file")
}
func loadConfig() (*config.Wrapper, error) {
cfgFile := locations[locConfigFile]
cfg, err := config.Load(cfgFile, myID)
if err != nil {
return nil, "", err
l.Infoln("Error loading config file; using defaults for now")
myName, _ := os.Hostname()
newCfg := defaultConfig(myName)
cfg = config.Wrap(cfgFile, newCfg)
}
myCfg := cfg.Devices()[myID]
myName := myCfg.Name
if myName == "" {
myName, _ = os.Hostname()
}
return cfg, myName, nil
return cfg, err
}
func startAuditing(mainSvc *suture.Supervisor) {
func loadOrCreateConfig() *config.Wrapper {
cfg, err := loadConfig()
if os.IsNotExist(err) {
cfg.Save()
l.Infof("Defaults saved. Edit %s to taste or use the GUI\n", cfg.ConfigPath())
} else if err != nil {
l.Fatalln("Config:", err)
}
if cfg.Raw().OriginalVersion != config.CurrentVersion {
err = archiveAndSaveConfig(cfg)
if err != nil {
l.Fatalln("Config archive:", err)
}
}
return cfg
}
func archiveAndSaveConfig(cfg *config.Wrapper) error {
// To prevent previous config from being cleaned up, quickly touch it too
now := time.Now()
_ = os.Chtimes(cfg.ConfigPath(), now, now) // May return error on Android etc; no worries
archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", cfg.Raw().OriginalVersion)
l.Infoln("Archiving a copy of old config file format at:", archivePath)
if err := osutil.Rename(cfg.ConfigPath(), archivePath); err != nil {
return err
}
return cfg.Save()
}
func startAuditing(mainService *suture.Supervisor) {
auditFile := timestampedLoc(locAuditLog)
fd, err := os.OpenFile(auditFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
l.Fatalln("Audit:", err)
}
auditSvc := newAuditSvc(fd)
mainSvc.Add(auditSvc)
auditService := newAuditService(fd)
mainService.Add(auditService)
// We wait for the audit service to fully start before we return, to
// ensure we capture all events from the start.
auditSvc.WaitForStart()
auditService.WaitForStart()
l.Infoln("Audit log in", auditFile)
}
func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc, errors, systemLog *logger.Recorder) {
func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.CachingMux, relayService *relay.Service, errors, systemLog *logger.Recorder, runtimeOptions RuntimeOptions) {
guiCfg := cfg.GUI()
if !guiCfg.Enabled {
@@ -907,14 +975,14 @@ func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, a
l.Warnln("Insecure admin access is enabled.")
}
api, err := newAPISvc(myID, cfg, guiAssets, m, apiSub, discoverer, relaySvc, errors, systemLog)
api, err := newAPIService(myID, cfg, runtimeOptions.assetDir, m, apiSub, discoverer, relayService, errors, systemLog)
if err != nil {
l.Fatalln("Cannot start GUI:", err)
}
cfg.Subscribe(api)
mainSvc.Add(api)
mainService.Add(api)
if cfg.Options().StartBrowser && !noBrowser && !stRestarting {
if cfg.Options().StartBrowser && !runtimeOptions.noBrowser && !runtimeOptions.stRestarting {
// Can potentially block if the utility we are invoking doesn't
// fork, and just execs, hence keep it in it's own routine.
go openURL(guiCfg.URL())
@@ -971,18 +1039,23 @@ func shutdown() {
stop <- exitSuccess
}
func ensureDir(dir string, mode int) {
fi, err := os.Stat(dir)
if os.IsNotExist(err) {
err := osutil.MkdirAll(dir, 0700)
if err != nil {
l.Fatalln(err)
}
} else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
err := os.Chmod(dir, os.FileMode(mode))
// This can fail on crappy filesystems, nothing we can do about it.
if err != nil {
l.Warnln(err)
func ensureDir(dir string, mode os.FileMode) {
err := osutil.MkdirAll(dir, mode)
if err != nil {
l.Fatalln(err)
}
if fi, err := os.Stat(dir); err == nil {
// Apprently the stat may fail even though the mkdirall passed. If it
// does, we'll just assume things are in order and let other things
// fail (like loading or creating the config...).
currentMode := fi.Mode() & 0777
if currentMode != mode {
err := os.Chmod(dir, mode)
// This can fail on crappy filesystems, nothing we can do about it.
if err != nil {
l.Warnln(err)
}
}
}
}

View File

@@ -34,13 +34,14 @@ const (
logFileMaxOpenTime = time.Minute
)
func monitorMain() {
func monitorMain(runtimeOptions RuntimeOptions) {
os.Setenv("STNORESTART", "yes")
os.Setenv("STMONITORED", "yes")
l.SetPrefix("[monitor] ")
var dst io.Writer = os.Stdout
logFile := runtimeOptions.logFile
if logFile != "-" {
var fileDst io.Writer = newAutoclosedFile(logFile, logFileAutoCloseDelay, logFileMaxOpenTime)
@@ -62,9 +63,12 @@ func monitorMain() {
args := os.Args
var restarts [countRestarts]time.Time
sign := make(chan os.Signal, 1)
sigTerm := syscall.Signal(0xf)
signal.Notify(sign, os.Interrupt, sigTerm, os.Kill)
stopSign := make(chan os.Signal, 1)
sigTerm := syscall.Signal(15)
signal.Notify(stopSign, os.Interrupt, sigTerm)
restartSign := make(chan os.Signal, 1)
sigHup := syscall.Signal(1)
signal.Notify(restartSign, sigHup)
for {
if t := time.Since(restarts[0]); t < loopThreshold {
@@ -124,12 +128,17 @@ func monitorMain() {
}()
select {
case s := <-sign:
case s := <-stopSign:
l.Infof("Signal %d received; exiting", s)
cmd.Process.Kill()
<-exit
return
case s := <-restartSign:
l.Infof("Signal %d received; restarting", s)
cmd.Process.Signal(sigHup)
err = <-exit
case err = <-exit:
if err == nil {
// Successful exit indicates an intentional shutdown

View File

@@ -16,9 +16,9 @@ import (
"github.com/thejerf/suture"
)
// The folderSummarySvc adds summary information events (FolderSummary and
// The folderSummaryService adds summary information events (FolderSummary and
// FolderCompletion) into the event stream at certain intervals.
type folderSummarySvc struct {
type folderSummaryService struct {
*suture.Supervisor
cfg *config.Wrapper
@@ -35,9 +35,9 @@ type folderSummarySvc struct {
lastEventReqMut sync.Mutex
}
func newFolderSummarySvc(cfg *config.Wrapper, m *model.Model) *folderSummarySvc {
svc := &folderSummarySvc{
Supervisor: suture.NewSimple("folderSummarySvc"),
func newFolderSummaryService(cfg *config.Wrapper, m *model.Model) *folderSummaryService {
service := &folderSummaryService{
Supervisor: suture.NewSimple("folderSummaryService"),
cfg: cfg,
model: m,
stop: make(chan struct{}),
@@ -47,20 +47,20 @@ func newFolderSummarySvc(cfg *config.Wrapper, m *model.Model) *folderSummarySvc
lastEventReqMut: sync.NewMutex(),
}
svc.Add(serviceFunc(svc.listenForUpdates))
svc.Add(serviceFunc(svc.calculateSummaries))
service.Add(serviceFunc(service.listenForUpdates))
service.Add(serviceFunc(service.calculateSummaries))
return svc
return service
}
func (c *folderSummarySvc) Stop() {
func (c *folderSummaryService) Stop() {
c.Supervisor.Stop()
close(c.stop)
}
// listenForUpdates subscribes to the event bus and makes note of folders that
// need their data recalculated.
func (c *folderSummarySvc) listenForUpdates() {
func (c *folderSummaryService) listenForUpdates() {
sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged)
defer events.Default.Unsubscribe(sub)
@@ -110,7 +110,7 @@ func (c *folderSummarySvc) listenForUpdates() {
// calculateSummaries periodically recalculates folder summaries and
// completion percentage, and sends the results on the event bus.
func (c *folderSummarySvc) calculateSummaries() {
func (c *folderSummaryService) calculateSummaries() {
const pumpInterval = 2 * time.Second
pump := time.NewTimer(pumpInterval)
@@ -139,7 +139,7 @@ func (c *folderSummarySvc) calculateSummaries() {
// foldersToHandle returns the list of folders needing a summary update, and
// clears the list.
func (c *folderSummarySvc) foldersToHandle() []string {
func (c *folderSummaryService) foldersToHandle() []string {
// We only recalculate summaries if someone is listening to events
// (a request to /rest/events has been made within the last
// pingEventInterval).
@@ -162,7 +162,7 @@ func (c *folderSummarySvc) foldersToHandle() []string {
}
// sendSummary send the summary events for a single folder
func (c *folderSummarySvc) sendSummary(folder string) {
func (c *folderSummaryService) sendSummary(folder string) {
// The folder summary contains how many bytes, files etc
// are in the folder and how in sync we are.
data := folderSummary(c.cfg, c.model, folder)
@@ -192,7 +192,7 @@ func (c *folderSummarySvc) sendSummary(folder string) {
}
}
func (c *folderSummarySvc) gotEventRequest() {
func (c *folderSummaryService) gotEventRequest() {
c.lastEventReqMut.Lock()
c.lastEventReq = time.Now()
c.lastEventReqMut.Unlock()

View File

@@ -18,7 +18,7 @@ import (
// The UPnP service runs a loop for discovery of IGDs (Internet Gateway
// Devices) and setup/renewal of a port mapping.
type upnpSvc struct {
type upnpService struct {
cfg *config.Wrapper
localPort int
extPort int
@@ -26,15 +26,15 @@ type upnpSvc struct {
stop chan struct{}
}
func newUPnPSvc(cfg *config.Wrapper, localPort int) *upnpSvc {
return &upnpSvc{
func newUPnPService(cfg *config.Wrapper, localPort int) *upnpService {
return &upnpService{
cfg: cfg,
localPort: localPort,
extPortMut: sync.NewMutex(),
}
}
func (s *upnpSvc) Serve() {
func (s *upnpService) Serve() {
foundIGD := true
s.stop = make(chan struct{})
@@ -72,18 +72,18 @@ func (s *upnpSvc) Serve() {
}
}
func (s *upnpSvc) Stop() {
func (s *upnpService) Stop() {
close(s.stop)
}
func (s *upnpSvc) ExternalPort() int {
func (s *upnpService) ExternalPort() int {
s.extPortMut.Lock()
port := s.extPort
s.extPortMut.Unlock()
return port
}
func (s *upnpSvc) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
func (s *upnpService) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
// Lets try all the IGDs we found and use the first one that works.
// TODO: Use all of them, and sort out the resulting mess to the
// discovery announcement code...
@@ -105,7 +105,7 @@ func (s *upnpSvc) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
return 0
}
func (s *upnpSvc) tryIGD(igd upnp.IGD, suggestedPort int) (int, error) {
func (s *upnpService) tryIGD(igd upnp.IGD, suggestedPort int) (int, error) {
var err error
leaseTime := s.cfg.Options().UPnPLeaseM * 60

View File

@@ -13,7 +13,6 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"runtime"
"sort"
@@ -61,9 +60,9 @@ func (m *usageReportingManager) VerifyConfiguration(from, to config.Configuratio
func (m *usageReportingManager) CommitConfiguration(from, to config.Configuration) bool {
if to.Options.URAccepted >= usageReportVersion && m.sup == nil {
// Usage reporting was turned on; lets start it.
svc := newUsageReportingService(m.cfg, m.model)
service := newUsageReportingService(m.cfg, m.model)
m.sup = suture.NewSimple("usageReporting")
m.sup.Add(svc)
m.sup.Add(service)
m.sup.ServeBackground()
} else if to.Options.URAccepted < usageReportVersion && m.sup != nil {
// Usage reporting was turned off
@@ -79,7 +78,7 @@ func (m *usageReportingManager) String() string {
}
// reportData returns the data to be sent in a usage report. It's used in
// various places, so not part of the usageReportingSvc object.
// various places, so not part of the usageReportingManager object.
func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
res := make(map[string]interface{})
res["urVersion"] = usageReportVersion
@@ -184,8 +183,6 @@ func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
for _, addr := range cfg.Options().GlobalAnnServers {
if addr == "default" || addr == "default-v4" || addr == "default-v6" {
defaultAnnounceServersDNS++
} else if stringIn(addr, config.DefaultDiscoveryServersIP) {
defaultAnnounceServersIP++
} else {
otherAnnounceServers++
}
@@ -221,15 +218,6 @@ func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
return res
}
func stringIn(needle string, haystack []string) bool {
for _, s := range haystack {
if needle == s {
return true
}
}
return false
}
type usageReportingService struct {
cfg *config.Wrapper
model *model.Model
@@ -249,19 +237,14 @@ func (s *usageReportingService) sendUsageReport() error {
var b bytes.Buffer
json.NewEncoder(&b).Encode(d)
transp := &http.Transport{}
client := &http.Client{Transport: transp}
if BuildEnv == "android" {
// This works around the lack of DNS resolution on Android... :(
transp.Dial = func(network, addr string) (net.Conn, error) {
return dialer.Dial(network, "194.126.249.13:443")
}
}
if s.cfg.Options().URPostInsecurely {
transp.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
client := &http.Client{
Transport: &http.Transport{
Dial: dialer.Dial,
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: s.cfg.Options().URPostInsecurely,
},
},
}
_, err := client.Post(s.cfg.Options().URURL, "application/json", &b)
return err

View File

@@ -15,25 +15,31 @@ import (
// The verbose logging service subscribes to events and prints these in
// verbose format to the console using INFO level.
type verboseSvc struct {
type verboseService struct {
stop chan struct{} // signals time to stop
started chan struct{} // signals startup complete
}
func newVerboseSvc() *verboseSvc {
return &verboseSvc{
func newVerboseService() *verboseService {
return &verboseService{
stop: make(chan struct{}),
started: make(chan struct{}),
}
}
// Serve runs the verbose logging service.
func (s *verboseSvc) Serve() {
func (s *verboseService) Serve() {
sub := events.Default.Subscribe(events.AllEvents)
defer events.Default.Unsubscribe(sub)
// We're ready to start processing events.
close(s.started)
select {
case <-s.started:
// The started channel has already been closed; do nothing.
default:
// This is the first time around. Indicate that we're ready to start
// processing events.
close(s.started)
}
for {
select {
@@ -49,17 +55,17 @@ func (s *verboseSvc) Serve() {
}
// Stop stops the verbose logging service.
func (s *verboseSvc) Stop() {
func (s *verboseService) Stop() {
close(s.stop)
}
// WaitForStart returns once the verbose logging service is ready to receive
// events, or immediately if it's already running.
func (s *verboseSvc) WaitForStart() {
func (s *verboseService) WaitForStart() {
<-s.started
}
func (s *verboseSvc) formatEvent(ev events.Event) string {
func (s *verboseService) formatEvent(ev events.Event) string {
switch ev.Type {
case events.Ping, events.DownloadProgress, events.LocalIndexUpdated:
// Skip
@@ -96,7 +102,7 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
return fmt.Sprintf("Rejected unshared folder %q from device %v", data["folder"], data["device"])
case events.ItemStarted:
data := ev.Data.(map[string]interface{})
data := ev.Data.(map[string]string)
return fmt.Sprintf("Started syncing %q / %q (%v %v)", data["folder"], data["item"], data["action"], data["type"])
case events.ItemFinished:
data := ev.Data.(map[string]interface{})

View File

@@ -6,10 +6,9 @@ Wants=syncthing-inotify@.service
[Service]
User=%i
Environment=STNORESTART=yes
ExecStart=/usr/bin/syncthing -no-browser -logflags=0
ExecStart=/usr/bin/syncthing -no-browser -no-restart -logflags=0
Restart=on-failure
SuccessExitStatus=2 3 4
SuccessExitStatus=3 4
RestartForceExitStatus=3 4
[Install]

View File

@@ -5,10 +5,9 @@ After=network.target
Wants=syncthing-inotify.service
[Service]
Environment=STNORESTART=yes
ExecStart=/usr/bin/syncthing -no-browser -logflags=0
ExecStart=/usr/bin/syncthing -no-browser -no-restart -logflags=0
Restart=on-failure
SuccessExitStatus=2 3 4
SuccessExitStatus=3 4
RestartForceExitStatus=3 4
[Install]

View File

@@ -245,7 +245,7 @@ ul.three-columns li, ul.two-columns li {
/** Footer nav on small devices **/
@media (max-width: 991px) {
@media (max-width: 1199px) {
body {
padding-bottom: 0;
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,5 +1,5 @@
{
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
"A negative number of days doesn't make sense.": "Un nombre negatiu de dies no té sentit.",
"A new major version may not be compatible with previous versions.": "Una nova versió major pot ser incompatible amb versions anteriors.",
"API Key": "Clau API",
"About": "Sobre",
@@ -10,8 +10,8 @@
"Add new folder?": "Afegir nova carpeta?",
"Address": "Adreça",
"Addresses": "Adreces",
"Advanced": "Advanced",
"Advanced Configuration": "Advanced Configuration",
"Advanced": "Avançat",
"Advanced Configuration": "Configuració Avançada",
"All Data": "Totes les dades",
"Allow Anonymous Usage Reporting?": "Permetre l'enviament anònim d'informes d'ús?",
"Alphabetic": "Alfabètic",
@@ -19,11 +19,11 @@
"Anonymous Usage Reporting": "Informe anònim d'ús",
"Any devices configured on an introducer device will be added to this device as well.": "Qualsevol dispositiu configurat en un dispositiu introductor també s'afegirà a aquest dispositiu.",
"Automatic upgrades": "Actualitzacions automàtiques",
"Be careful!": "Be careful!",
"Be careful!": "Ves amb compte!",
"Bugs": "Bugs",
"CPU Utilization": "Utilització del CPU",
"Changelog": "Historial de canvis",
"Clean out after": "Clean out after",
"Clean out after": "Netejar després",
"Close": "Tancar",
"Command": "Comando",
"Comment, when used at the start of a line": "Comentari quan és usat al principi d'una línia",
@@ -32,16 +32,16 @@
"Copied from elsewhere": "Copiat d'un altre lloc",
"Copied from original": "Copiat de l'original",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 els següents col·laboradors:",
"Danger!": "Danger!",
"Danger!": "Perill!",
"Delete": "Esborrar",
"Deleted": "Deleted",
"Deleted": "Esborrat",
"Device ID": "ID del dispositiu",
"Device Identification": "Identificació del dispositiu",
"Device Name": "Nom del dispositiu",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositiu {{device}} ({{address}}) vol conectar-se. Afegir nou dispositiu?",
"Devices": "Dispositius",
"Disconnected": "Desconnectat",
"Discovery": "Discovery",
"Discovery": "Descobriment",
"Documentation": "Documentació",
"Download Rate": "Tasca de descarrega",
"Downloaded": "Descarregat",
@@ -51,18 +51,18 @@
"Edit Folder": "Modificar carpeta",
"Editing": "Modificant",
"Enable UPnP": "Habilitat UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introdueix adreces separades per comes (\"tcp://ip:port\", \"tcp://host:port\") o \"dinàmic\" per realitzar descobriments automàtics de l'adreça.",
"Enter ignore patterns, one per line.": "Introduex patrons a ignorar, un per línia.",
"Error": "Error",
"External File Versioning": "Versionat de fitxers extern",
"Failed Items": "Failed Items",
"Failed Items": "Elements fallats",
"File Pull Order": "Ordre d'agafar fitxers",
"File Versioning": "Versionat de Fitxers",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Els bits de permisos dels fitxers son ignorats quan es cerquen canvis. Utilitzar en sistemes de fitxers FAT.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Els fitxers són moguts a la carpeta .stversions quan són reemplaçats o esborrats per Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Els fitxers es mouen amb l'estampat de la data a la carpeta .stversions quan son substituïts o esborrats per syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Els fitxers estan protegits de canvis fets per altres dispositius, però els canvis fets en aquest dispositiu seran enviats a la resta del cluster.",
"Folder": "Folder",
"Folder": "Carpeta",
"Folder ID": "ID de carpeta",
"Folder Master": "Carpeta mestre",
"Folder Path": "Camí de carpeta",
@@ -76,12 +76,12 @@
"Global Discovery Server": "Servidor de Descobriment Global",
"Global State": "Estat global",
"Help": "Ajuda",
"Home page": "Home page",
"Home page": "Pàgina d'inici",
"Ignore": "Ignorar",
"Ignore Patterns": "Patrons d'ignoració",
"Ignore Permissions": "Ignora Permisos",
"Incoming Rate Limit (KiB/s)": "Tasca Límit d'Entrada (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configuració incorrecta pot malmetre els continguts de la teva carpeta i que Syncthing esdevingui inoperatiu.",
"Introducer": "Introductor",
"Inversion of the given condition (i.e. do not exclude)": "Inversió del patrò introduït",
"Keep Versions": "Mantenir Versions",
@@ -91,11 +91,11 @@
"Later": "Després",
"Local Discovery": "Descobriment Local",
"Local State": "Estat local",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Estat local (Total)",
"Major Upgrade": "Actualització major",
"Maximum Age": "Antiguitat Màxima",
"Metadata Only": "Només metadades",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Espai de disc lliure mínim",
"Move to top of queue": "Moure al primer de la cua",
"Multi level wildcard (matches multiple directory levels)": "Caràcter comodí de nivell múltiple (aparella en carpetes de nivells múltiples)",
"Never": "Mai",
@@ -108,37 +108,37 @@
"OK": "OK",
"Off": "Desactivar",
"Oldest First": "Més antic primer",
"Options": "Options",
"Out of Sync": "Out of Sync",
"Options": "Opcions",
"Out of Sync": "Fora de sincronia",
"Out of Sync Items": "Arxius encara no sincronitzats",
"Outgoing Rate Limit (KiB/s)": "Tasca Límit de Sortida (KiB/s)",
"Override Changes": "Sobreescriure Canvis",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta de la carpeta a l'equip local. Si no existeix serà creada. El caràcter (~) es pot fer servir com a drecera de",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta on les versions s'haurien de guardar (deixa-ho buit per fer servir el directori .stversions per defecte a la carpeta)",
"Pause": "Pause",
"Paused": "Paused",
"Pause": "Pausa",
"Paused": "Pausat",
"Please consult the release notes before performing a major upgrade.": "Si us plau consulta les notes de llançament abans de realitzar una actualització major.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Si us plau, estableix un usuari i contrasenya al GUI a través del quadre de diàleg de configuració.",
"Please wait": "Si-us-plau espera",
"Preview": "Vista prèvia",
"Preview Usage Report": "Vista Prèvia de l'Informe d'Ús",
"Quick guide to supported patterns": "Guia ràpida per als possibles patrons",
"RAM Utilization": "Utilització de la RAM",
"Random": "Aleatori",
"Relayed via": "Relayed via",
"Relays": "Relays",
"Relayed via": "Retransmés a través",
"Relays": "Repetidors",
"Release Notes": "Notes de llançament",
"Remove": "Remove",
"Remove": "Esborrar",
"Rescan": "Re-escanejar",
"Rescan All": "Re-escanejar tot",
"Rescan Interval": "Interval de re-escaneig",
"Restart": "Reiniciar",
"Restart Needed": "És Necessari Reiniciar",
"Restarting": "Reiniciant",
"Resume": "Resume",
"Resume": "Reprendre",
"Reused": "Reutilitzat",
"Save": "Guardar",
"Scan Time Remaining": "Scan Time Remaining",
"Scan Time Remaining": "Temps d'escanejat restant",
"Scanning": "Escanejant",
"Select the devices to share this folder with.": "Selecciona els dispositius en els quals compartir aquesta carpeta.",
"Select the folders to share with this device.": "Selecciona la carpeta per a compartir en aquest dispositiu.",
@@ -161,7 +161,7 @@
"Source Code": "Codi Font",
"Staggered File Versioning": "Versionat de Fitxers Esglaonat",
"Start Browser": "Arrancar Navegador",
"Statistics": "Statistics",
"Statistics": "Estadístiques",
"Stopped": "Aturat",
"Support": "Suport",
"Sync Protocol Listen Addresses": "Adreça d'escolta del Protocol Sync",
@@ -172,7 +172,7 @@
"Syncthing is upgrading.": "Actualitzant syncthing.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Synthing sembla parat, o hi ha algun problema amb la connexió a Internet. Reintentant...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Sembla ser que Syncthing està tinguent problemes per processar la teva petició. Si us plau, refresca la pàgina o reinicia Syncthing si el problema persisteix.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The Syncthing admin interface is configured to allow remote access without a password.": "La interfície d'administració de Syncthing està configurada per permetre l'accés remot sense contrasenya.",
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan públicament disponibles a {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració s'ha guardar però no s'ha activat. S'ha de reiniciar el synthing per activar la nova configuració.",
"The device ID cannot be blank.": "El ID del dispositiu no pot estar en blanc.",
@@ -185,26 +185,26 @@
"The folder ID must be unique.": "El ID de la carpeta ha de ser únic.",
"The folder path cannot be blank.": "El camí a la carpeta no pot estar en blanc.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Es fan servir els següents intervals: per la primera hora es manté una versió cada 30 segons, pel primer dia es manté una versió cada hora, pel primer cada 30 dies es manté una versió cada dia, fins el màxim d'antiguitat es manté una versió cada setmana.",
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The following items could not be synchronized.": "Els següents elements no es poden sincronitzar.",
"The maximum age must be a number and cannot be blank.": "La màxima antiguitat ha de ser un número i no pot estar en blanc.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Temps màxim en mantenir una versió (en dies, si es deixa en 0 es mantenen les versions per sempre).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "El percentatge d'espai de disc lliure mínim ha de ser un nombre positiu entre 0 i 100 (inclosos).",
"The number of days must be a number and cannot be blank.": "El nombre de dies ha de ser un número i no pot estar en blanc.",
"The number of days to keep files in the trash can. Zero means forever.": "El nombre de dies per guardar els fitxers a la paperera. Zero significa per sempre.",
"The number of old versions to keep, per file.": "El nombre de versions antigues que es mantenen per fitxer.",
"The number of versions must be a number and cannot be blank.": "El nombre de versions ha de ser un número i no es pot deixar en blanc.",
"The path cannot be blank.": "El camí no pot estar en blanc.",
"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 rate limit must be a non-negative number (0: no limit)": "El límit de velocitat ha de ser un nombre positiu (0: sense límit)",
"The rescan interval must be a non-negative number of seconds.": "El interval de re-escaneig ha der ser un nombre positiu de segons.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This 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.",
"They are retried automatically and will be synced when the error is resolved.": "Són reintentats automàticament i seran sincronitzats quan l'error estigui resolt.",
"This can easily give hackers access to read and change any files on your computer.": "Això pot donar facilment accés a hackers per llegir i canviar qualsevol fitxer del teu ordinador.",
"This is a major version upgrade.": "Aquesta és una actualització de versió major.",
"Trash Can File Versioning": "Trash Can File Versioning",
"Trash Can File Versioning": "Paperera de versionat de fitxers",
"Unknown": "Desconegut",
"Unshared": "No compartit",
"Unused": "No usat",
"Up to Date": "Actualitzat",
"Updated": "Updated",
"Updated": "Actualitzat",
"Upgrade": "Actualització",
"Upgrade To {%version%}": "Actualitzar a {{version}}",
"Upgrading": "Actualitzant",
@@ -218,7 +218,7 @@
"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.": "Quan s'afegeix una nova carpeta recorda que el ID d'aquesta s'utilitza per lligar repositoris entre els dispositius. Es distingeix entre majúscules i minúscules i ha de ser exactament iguals entre tots els dispositius.",
"Yes": "Si",
"You must keep at least one version.": "Has de mantenir com a mínim una versió.",
"days": "days",
"days": "dies",
"full documentation": "documentació sencera",
"items": "Elements",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartir la carpeta \"{{folder}}\"."

View File

@@ -10,8 +10,8 @@
"Add new folder?": "Afegir nova carpeta?",
"Address": "Direcció",
"Addresses": "Direccions",
"Advanced": "Advanced",
"Advanced Configuration": "Advanced Configuration",
"Advanced": "Avançat",
"Advanced Configuration": "Configuració avançada",
"All Data": "Totes les dades",
"Allow Anonymous Usage Reporting?": "Permetre informes d'ús anònim?",
"Alphabetic": "Alfabètic",
@@ -19,7 +19,7 @@
"Anonymous Usage Reporting": "Informe d'ús anònim",
"Any devices configured on an introducer device will be added to this device as well.": "Tots els dispositius configurats en un dispositiu presentador seràn afegits també a aquest dispositiu.",
"Automatic upgrades": "Actualitzacions automàtiques",
"Be careful!": "Be careful!",
"Be careful!": "Tin precaució!",
"Bugs": "Errors (Bugs)",
"CPU Utilization": "Utilització de la CPU",
"Changelog": "Registre de canvis",
@@ -32,7 +32,7 @@
"Copied from elsewhere": "Copiat de qualsevol lloc",
"Copied from original": "Copiat de l'original",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 els següents Col·laboradors:",
"Danger!": "Danger!",
"Danger!": "Perill!",
"Delete": "Esborrar",
"Deleted": "Esborrat",
"Device ID": "ID del dispositiu",
@@ -41,7 +41,7 @@
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositiu {{device}} ({{address}}) vol connectar-se. Afegir nou dispositiu?",
"Devices": "Dispositius",
"Disconnected": "Desconnectat",
"Discovery": "Discovery",
"Discovery": "Descobriment",
"Documentation": "Documentació",
"Download Rate": "Velocitat de descàrrega",
"Downloaded": "Descarregat",
@@ -51,23 +51,23 @@
"Edit Folder": "Editar carpeta",
"Editing": "Editant",
"Enable UPnP": "Activar UPnp",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introdueix adreces separades per coma (\"tcp://ip:port\", \"tcp://host:port\") o \"dynamic\" per a realitzar el descobriment automàtic de l'adreça.",
"Enter ignore patterns, one per line.": "Introduïr patrons a ignorar, un per línia.",
"Error": "Error",
"External File Versioning": "Versionat extern de fitxers",
"Failed Items": "Failed Items",
"Failed Items": "Objectes fallits",
"File Pull Order": "Ordre de fitxers del pull",
"File Versioning": "Versionat de fitxer",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Els bits de permís del fitxer són ignorats quant es busquen els canvis. Utilitzar en sistemes de fitxers FAT.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Els arxius es menejen a la carpeta .stversions quant són substituïts o esborrats per Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Els fitxers són canviats a versions amb indicació de data en una carpeta \".stversions\" quant són reemplaçats o esborrats per Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Els fitxers són protegits dels canvis fets en altres dispositius, però els canvis fets en aquest dispositiu seràn enviats a la resta del grup (cluster).",
"Folder": "Folder",
"Folder": "Carpeta",
"Folder ID": "ID de carpeta",
"Folder Master": "Carpeta principal",
"Folder Path": "Ruta de la carpeta",
"Folders": "Carpetes",
"GUI": "GUI",
"GUI": "IGU (Interfície Gràfica d'Usuari)",
"GUI Authentication Password": "Password d'autenticació de l'Interfície Gràfica d'Usuari (GUI)",
"GUI Authentication User": "Autenticació de l'usuari de l'Interfície Gràfica d'Usuari (GUI)",
"GUI Listen Addresses": "Direcció d'escolta de l'Interfície Gràfica d'Usuari (GUI)",
@@ -76,12 +76,12 @@
"Global Discovery Server": "Servidor de descobriment global",
"Global State": "Estat global",
"Help": "Ajuda",
"Home page": "Home page",
"Home page": "Pàgina inicial",
"Ignore": "Ignorar",
"Ignore Patterns": "Patrons a ignorar",
"Ignore Permissions": "Permisos a ignorar",
"Incoming Rate Limit (KiB/s)": "Límit de descàrrega (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "La configuración incorrecta pot danyar el contingut de la teua carpeta i deixar Syncthing inoperatiu.",
"Introducer": "Presentador",
"Inversion of the given condition (i.e. do not exclude)": "Inversió de la condició donada (per exemple no excloure)",
"Keep Versions": "Mantindre versions",
@@ -95,7 +95,7 @@
"Major Upgrade": "Actualització important",
"Maximum Age": "Edat màxima",
"Metadata Only": "Sols metadades",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Minimum Free Disk Space": "Espai minim de disc lliure",
"Move to top of queue": "Moure al principi de la cua",
"Multi level wildcard (matches multiple directory levels)": "Comodí multinivell (coincideix amb múltiples nivells de directoris)",
"Never": "Mai",
@@ -108,37 +108,37 @@
"OK": "OK",
"Off": "Off",
"Oldest First": "El més vell primer",
"Options": "Options",
"Out of Sync": "Out of Sync",
"Options": "Opcions",
"Out of Sync": "Sense sincronització",
"Out of Sync Items": "Dispositius sense sincronitzar",
"Outgoing Rate Limit (KiB/s)": "Límit de pujada (KiB/s)",
"Override Changes": "Sobreescriure els canvis",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta a la carpeta local en l'ordinador. Es crearà si no existeix. El caràcter tilde (~) es pot utilitzar com a drecera",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta on les versions deurien estar emmagatzemades (deixar buit per a la carpeta .stversions en la carpeta).",
"Pause": "Pause",
"Paused": "Paused",
"Pause": "Pausa",
"Paused": "Pausat",
"Please consult the release notes before performing a major upgrade.": "Per favor, consultar les notes de la versió abans de fer una actualització important.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Per favor, estableix un usuari i password per a l'Interfície Gràfica d'Usuari en el menú d'Adjustos.",
"Please wait": "Per favor, espere",
"Preview": "Vista prèvia",
"Preview Usage Report": "Informe d'ús de vista prèvia",
"Quick guide to supported patterns": "Guía ràpida de patrons suportats",
"RAM Utilization": "Utilització de la RAM",
"Random": "Aleatori",
"Relayed via": "Relayed via",
"Relays": "Relays",
"Relayed via": "Transmitit via",
"Relays": "Transmissions",
"Release Notes": "Notes de la versió",
"Remove": "Remove",
"Remove": "Eliminar",
"Rescan": "Tornar a buscar",
"Rescan All": "Tornar a buscar tot",
"Rescan Interval": "Interval de nova busca",
"Restart": "Reiniciar",
"Restart Needed": "Reinici necesari",
"Restarting": "Reiniciant",
"Resume": "Resume",
"Resume": "Continuar",
"Reused": "Reutilitzat",
"Save": "Gravar",
"Scan Time Remaining": "Scan Time Remaining",
"Scan Time Remaining": "Temps d'escaneig restant",
"Scanning": "Rastrejant",
"Select the devices to share this folder with.": "Selecciona els dispositius amb els que compartir aquesta carpeta.",
"Select the folders to share with this device.": "Selecciona les carpetes per a compartir amb aquest dispositiu.",
@@ -161,7 +161,7 @@
"Source Code": "Codi font",
"Staggered File Versioning": "Versionat de fitxers escalonat",
"Start Browser": "Iniciar navegador",
"Statistics": "Statistics",
"Statistics": "Estadístiques",
"Stopped": "Parat",
"Support": "Suport",
"Sync Protocol Listen Addresses": "Direccions d'escolta del protocol de sincronització",
@@ -172,7 +172,7 @@
"Syncthing is upgrading.": "Syncthing està actualitzant-se.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing pareix apagat o hi ha un problema amb la connexió a Internet. Tornant a intentar...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing pareix que té un problema processant la seua sol·licitud. Per favor, refresque la pàgina o reinicie Syncthing si el problema persistix.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfície d'administració de Syncthing està configurat per a permetre l'accés remot sense una contrasenya.",
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan disponibles públicament en {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració ha sigut gravada però no activada. Syncthing deu reiniciar per tal d'activar la nova configuració.",
"The device ID cannot be blank.": "L'ID del dispositiu no pot estar buida.",
@@ -185,19 +185,19 @@
"The folder ID must be unique.": "L'ID de la carpeta deu ser única.",
"The folder path cannot be blank.": "La ruta de la carpeta no pot estar buida.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "S'utilitzen els següents intervals: per a la primera hora es guarda una versió cada 30 segons, per al primer dia es guarda una versió cada hora, per als primers 30 dies es guarda una versió diaria, fins l'edat màxima es guarda una versió cada setmana.",
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The following items could not be synchronized.": "Els següents objectes no s'han pogut sincronitzar.",
"The maximum age must be a number and cannot be blank.": "L'edat màxima deu ser un nombre i no pot estar buida.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El temps màxim per a guardar una versió (en dies, ficar 0 per a guardar les versions per a sempre).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "El porcentatge d'espai mínim lliure en el disc deu ser un nombre no negatiu entre 0 i 100 (ambdós inclosos).",
"The number of days must be a number and cannot be blank.": "El nombre de dies deu ser un nombre i no pot estar en blanc.",
"The number of days to keep files in the trash can. Zero means forever.": "El nombre de dies per a mantindre els arxius a la paperera. Cero vol dir \"per a sempre\".",
"The number of old versions to keep, per file.": "El nombre de versions antigues per a guardar, per cada fitxer.",
"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)": "The rate limit must be a non-negative number (0: no limit)",
"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 rescan interval must be a non-negative number of seconds.": "L'interval de reescaneig deu ser un nombre positiu de segons.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This 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.",
"They are retried automatically and will be synced when the error is resolved.": "Es reintenta automàticament i es sincronitzaràn quant el resolga l'error.",
"This can easily give hackers access to read and change any files on your computer.": "Açò pot donar accés fàcilment als hackers per a llegir i canviar qualsevol fitxer al teu ordinador.",
"This is a major version upgrade.": "Aquesta és una actualització important de la versió.",
"Trash Can File Versioning": "Versionat d'arxius de la paperera",
"Unknown": "Desconegut",
@@ -218,7 +218,7 @@
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quant s'afig una nova carpeta, hi ha que tindre en compte que l'ID de la carpeta s'utilitza per a juntar les carpetes entre dispositius. Són sensibles a les majúscules i deuen coincidir exactament entre tots els dispositius.",
"Yes": "Sí",
"You must keep at least one version.": "Es deu mantindre al menys una versió.",
"days": "days",
"days": "dies",
"full documentation": "Documentació completa",
"items": "Elements",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartit la carpeta \"{{folder}}\"."

View File

@@ -0,0 +1,225 @@
{
"A negative number of days doesn't make sense.": "Et negativt antal dage giver ikke mening.",
"A new major version may not be compatible with previous versions.": "En ny versionsudgivelse er måske ikke kompatibel med tidligere versioner.",
"API Key": "API-nøgle",
"About": "Om",
"Actions": "Handlinger.",
"Add": "Tilføj",
"Add Device": "Tilføj enhed",
"Add Folder": "Tilføj mappe",
"Add new folder?": "Tilføj ny mappe",
"Address": "Adresse",
"Addresses": "Adresser",
"Advanced": "Avanceret",
"Advanced Configuration": "Avanceret konfiguration",
"All Data": "Alt data",
"Allow Anonymous Usage Reporting?": "Tillad anonym brugerstatistik?",
"Alphabetic": "Alfabetisk",
"An external command handles the versioning. It has to remove the file from the synced folder.": " ",
"Anonymous Usage Reporting": "Anonym brugerstatistik",
"Any devices configured on an introducer device will be added to this device as well.": "Alle enheder som er konfigueret som en introducerende enhed, vil også blive tilføjet til denne enhed.",
"Automatic upgrades": "Automatisk opdatering",
"Be careful!": "Vær forsigtig!",
"Bugs": "Fejl",
"CPU Utilization": "CPU-forbrug",
"Changelog": "Udgivelsesnoter",
"Clean out after": "Rens efter",
"Close": "Luk",
"Command": "Kommando",
"Comment, when used at the start of a line": "Kommentering som bruges i starten af en linje",
"Compression": "Anvend komprimering",
"Connection Error": "Tilslutnings fejl",
"Copied from elsewhere": "Kopieret fra et andet sted",
"Copied from original": "Kopieret fra originalen",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 alle bidragsydere:",
"Danger!": "Fare!",
"Delete": "Slet",
"Deleted": "Slettet",
"Device ID": "Enheds-ID",
"Device Identification": "Enhedsidentifikation",
"Device Name": "Enhedsnavn",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enheden {{device}} ({{address}}) ønsker at forbinde. Tilføj denne enhed?",
"Devices": "Enheder",
"Disconnected": "Ikke tilsluttet",
"Discovery": "Opslag",
"Documentation": "Dokumentation",
"Download Rate": "Downloadhastighed",
"Downloaded": "Downloadet",
"Downloading": "Downloader",
"Edit": "Rediger",
"Edit Device": "Rediger enhed",
"Edit Folder": "Rediger mappe",
"Editing": "Redigerer",
"Enable UPnP": "Anvend UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv kommaseparerede adresser (\"tcp://ip:port\", \"tcp://host:port\") eller \"dynamic\" for at benytte automatisk opdagelse af adressen.",
"Enter ignore patterns, one per line.": "Vælg ignorer maske, én per linje.",
"Error": "Fejl",
"External File Versioning": "Ekstern fil-versionskontrol",
"Failed Items": "Mislykkede filer",
"File Pull Order": "Filhentnings rækkefølge",
"File Versioning": "Filversionering",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Filtilladelses bits ignoreres når der søges efter ændringer. Bruges på FAT filsystemer. ",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer flyttes til .stversions mappen når de erstattes eller slettes af Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttes til data-stemplede versioner i en .stversions mappe når de erstattes eller slettes af Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskyttet fra ændringer foretaget på andre enheder, men ændringerne på denne enhed vil blive sendt til alle andre tilknyttede enheder.",
"Folder": "Mappe",
"Folder ID": "Mappe-ID",
"Folder Master": "Mastermappe",
"Folder Path": "Mappesti",
"Folders": "Mapper",
"GUI": "GUI",
"GUI Authentication Password": "GUI-kodeord",
"GUI Authentication User": "GUI-brugernavn",
"GUI Listen Addresses": "GUI-lytteadresse",
"Generate": "Opret",
"Global Discovery": "Globalt opslag",
"Global Discovery Server": "Global opslagsserver",
"Global State": "Global tilstand",
"Help": "Hjælp",
"Home page": "Hjem",
"Ignore": "Ignorer",
"Ignore Patterns": "Ignoreringsmaske",
"Ignore Permissions": "Ignorér filrettigheder",
"Incoming Rate Limit (KiB/s)": "Indgående hastighedsbegrænsning (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Ukorrekt opsætning kan skade dine data og gøre Syncthing ude af stand til at fungere.",
"Introducer": "Introducerende enhed",
"Inversion of the given condition (i.e. do not exclude)": "Det omvendte (dvs. undlad ikke)",
"Keep Versions": "Behold versioner",
"Largest First": "Største først",
"Last File Received": "Sidste modtaget fil",
"Last seen": "Sidst set",
"Later": "Senere",
"Local Discovery": "Lokal opslag",
"Local State": "Lokal tilstand",
"Local State (Total)": "Lokal tilstand (total)",
"Major Upgrade": "Ny version",
"Maximum Age": "Maks alder",
"Metadata Only": "Kun metadata",
"Minimum Free Disk Space": "Mindst ledig diskplads",
"Move to top of queue": "Flyt til toppen af køen",
"Multi level wildcard (matches multiple directory levels)": "Flerniveau wildcard (matcher flere biblioteksniveauer)",
"Never": "Aldrig",
"New Device": "Ny enhed",
"New Folder": "Ny mappe",
"Newest First": "Nyeste først",
"No": "Nej",
"No File Versioning": "Ingen filversion",
"Notice": "OBS",
"OK": "OK",
"Off": "Slå fra",
"Oldest First": "Ældste først",
"Options": "Indstillinger",
"Out of Sync": "Ikke synkroniseret",
"Out of Sync Items": "Endnu ikke synkroniserede filer",
"Outgoing Rate Limit (KiB/s)": "Udgående hastighedsbegrænsning (KiB/s)",
"Override Changes": "Overskriv ændringer",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sti til den lokale mappe. Vil blive oprettet hvis den ikke findes. Tilde tegnet (~) kan bruges som en forkortelse for",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sti hvor versioner skal gemmes (efterlad tom for default .stversions mappe)",
"Pause": "Pause",
"Paused": "Pauset",
"Please consult the release notes before performing a major upgrade.": "Tjek venligst udgivelsesnoterne før opgradering til en ny version.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Sæt vensligt en GUI bruger og kodeord i opsætningsdialogen.",
"Please wait": "Vent venligst",
"Preview": "Forhåndsvisning",
"Preview Usage Report": "Forhåndsvisning af forbrugsrapport",
"Quick guide to supported patterns": "Hurtig guide til supporteret mønstre",
"RAM Utilization": "RAM-forbrug",
"Random": "Tilfældig",
"Relayed via": "Passeret gennem",
"Relays": "Passager",
"Release Notes": "Udgivelsesnoter",
"Remove": "Fjern",
"Rescan": "Skan igen",
"Rescan All": "Skan alt igen",
"Rescan Interval": "Genskannings interval",
"Restart": "Genstart",
"Restart Needed": "Programmet kræver genstart",
"Restarting": "Genstarter",
"Resume": "Genoptag",
"Reused": "Genbrugt",
"Save": "Gem",
"Scan Time Remaining": "Tid tilbage af skanningen",
"Scanning": "Opdaterer",
"Select the devices to share this folder with.": "Vælg hvilke enheder du vil dele denne mappe med",
"Select the folders to share with this device.": "Vælg hvilke mapper du vil dele med denne enhed.",
"Settings": "Indstillinger",
"Share": "Del",
"Share Folder": "Delt mappe",
"Share Folders With Device": "Del mappe med enhed",
"Share With Devices": "Del med enhed",
"Share this folder?": "Del denne mappe",
"Shared With": "Delt med",
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort navn for mappen. Skal være det samme på alle tilknyttede enheder.",
"Show ID": "Vis ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vist istedet for Enheds ID i klynge status. Vil blive vist på andre enheder som valgfrit standard navn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist istedet for Enheds ID i klynge status. Vil blive opdateret til det navn som enheden viser, hvis det ikke er udfyldt.",
"Shutdown": "Luk ned",
"Shutdown Complete": "Nedlukning fuldført",
"Simple File Versioning": "Simpel fil versioner",
"Single level wildcard (matches within a directory only)": "Enkeltnivau wildcard (matcher kun inden for en mapp)",
"Smallest First": "Mindste først",
"Source Code": "Kildekode",
"Staggered File Versioning": "Forskudte filversioner",
"Start Browser": "Start browser",
"Statistics": "Statistikker",
"Stopped": "Stoppet",
"Support": "Support",
"Sync Protocol Listen Addresses": "Lytteadresser for indgående forbindelser",
"Syncing": "Synkroniserer",
"Syncthing has been shut down.": "Syncthing er blevet lukket ned.",
"Syncthing includes the following software or portions thereof:": "Syncthing indeholder følgende software eller dele heraf:",
"Syncthing is restarting.": "Syncthing genstarter",
"Syncthing is upgrading.": "Syncthing opgraderer",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ud til at være stoppet eller oplever problemer med din internetforbindels. Prøver igen...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Det ser ud til at Syncthiing har problemer med at udføre opgaven. Prøv at genopfriske siden eller genstarte Synching hvis problemet vedbliver.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administationsdelen er konfigureret til at blive fjernstyret uden kodeord.",
"The aggregated statistics are publicly available at {%url%}.": "Samlet statistik er offentligt tilgængelig på {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen er gemt, men ikke aktiveret. Syncthing skal genstarte for at aktivere den nye konfiguration.",
"The device ID cannot be blank.": "Enhedens ID må ikke være tom.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enheds ID som som skal bruges her, kan du finde i \"Rediger > Vis ID\"-dialogen på den anden enhed. Mellemrum og bindestreg er valgfri (ignoreres).",
"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.": "Den krypterede forbrugsrapport sendes dagligt. Den benyttes til at spore anvendte platforme, mappestørrelser og versioner. Hvis det typen af opsamlet data ændres på et senere tidspunkt, vil du blive spurgt om tilladelse igen.",
"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.": "Det indtastede node ID ser ikke gyldigt ud. Det skal være en 52 eller 56 tegn streng, bestående af tal og bogstaver, eventuelt med mellemrum og bindestreger.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den første kommandolinjeparameter er mappestien og den anden parameter er den relative sti til mappen.",
"The folder ID cannot be blank.": "Mappe-ID må ikke være tom",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Mappe-ID skal være en kort identificierende streng (64 karakterer eller mindre) bestående af bogstav-, tal-, punktum- (.), bindestreg- (-) og understregskarakteren (_).",
"The folder ID must be unique.": "Mappe-ID skal være unik",
"The folder path cannot be blank.": "Mappestien må ikke være tom",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De følgende intervaller er brugt: i den første time, er en version gemt hvert 30. sekund, for første da er en version gemt hver time, for de første 30 dage er en version gemt hver dag og indtil den maksimale periode er en version hver uge gemt.",
"The following items could not be synchronized.": "Følgende filer kunne ikke synkroniseres.",
"The maximum age must be a number and cannot be blank.": "Maks alder skal være et nummer og må ikke være tom",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maks tid gamle udgaver skal gemmes (i dage, sæt lig med 0 for at beholde gamle versioner for altid)",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Procentsatsen på den mindste ledige diskplads må ikke være negative og skal være mellem 0 og 100 (inklusiv).",
"The number of days must be a number and cannot be blank.": "Antallet af dage skal være et nummer og feltet må ikke være tomt",
"The number of days to keep files in the trash can. Zero means forever.": "Antal dage filer gemmes i skraldespanden. Nul betyder for evigt.",
"The number of old versions to keep, per file.": "Antallet af gamle versioner som gemmes, per fil.",
"The number of versions must be a number and cannot be blank.": "Antallet af versioner skal være et tal, og kan ikke være blankt.",
"The path cannot be blank.": "Stien må ikke være tom",
"The rate limit must be a non-negative number (0: no limit)": "Ratebegrænsningen må ikke være negative tal (0: ingen begrænsning)",
"The rescan interval must be a non-negative number of seconds.": "Genskanningsintervallet skal være et ikke-negativt antal sekunder",
"They are retried automatically and will be synced when the error is resolved.": "De prøves igen automatisk og vil blive synkroniseret når fejlen er løst.",
"This can easily give hackers access to read and change any files on your computer.": "Dette gør det nemt for hackere at få adgang til at læse og ændre filer på din computer.",
"This is a major version upgrade.": "Dette er en ny version",
"Trash Can File Versioning": "Skraldespand fil versioner",
"Unknown": "Ukendt",
"Unshared": "Ikke delt",
"Unused": "Ubrugt",
"Up to Date": "Fuldt opdateret",
"Updated": "Opdateret",
"Upgrade": "Upgradér",
"Upgrade To {%version%}": "Opgradér til {{version}}",
"Upgrading": "Opgradere",
"Upload Rate": "Uploadhastighed",
"Uptime": "Oppetid",
"Use HTTPS for GUI": "Anvend HTTPS til GUI adgang",
"Version": "Version",
"Versions Path": "Versions-sti",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner slettes automatisk, hvis de er ældre end den satte maksimum alder eller overstiger det tilladte antal filer i et interval.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Når der tilføjes en ny enhed, vær da opmærksom på, at denne enhed også skal tilføjes på den anden side.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Når der tilføjes en ny enhed, vær da opmærksom på at samme ID bruges til at forbinde mapperne på de forskellige enheder. Der er forskel på store og små bogstaver, og ID skal være fuldstændig identisk på alle enheder.",
"Yes": "Ja",
"You must keep at least one version.": "Du skal beholde mindst én version.",
"days": "dage",
"full documentation": "Fuld dokumentation",
"items": "poster",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker at dele mappen \"{{folder}}\". "
}

View File

@@ -68,9 +68,9 @@
"Folder Path": "Map-paad",
"Folders": "Mappen",
"GUI": "GUI",
"GUI Authentication Password": "GUI-wachtwurd foar ferifikaasje",
"GUI Authentication User": "GUI-brûker foar ferifikaasje",
"GUI Listen Addresses": "GUI-harkadres",
"GUI Authentication Password": "Wachtwurd foar ferifikaasje yn GUI",
"GUI Authentication User": "Brûkers-ID foar ferifikaasje yn GUI",
"GUI Listen Addresses": "Harkadres foar GUI",
"Generate": "Generearje",
"Global Discovery": "Wrâldwide ûntdekking",
"Global Discovery Server": "Wrâldwide ûntdekkingstsjinner",
@@ -152,7 +152,7 @@
"Short identifier for the folder. Must be the same on all cluster devices.": "Koart opskrift foar de map. Moat op alle apparaten itselde wêze.",
"Show ID": "ID sjen litte",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wurd ynstee fan apparaat-ID sjen litten by de bondeltastân. Wurd nei oare apparaten advertearre as in mooglike standertnamme.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd ynstee fan apparaat-ID sjen litten by de bondeltastân. As it leech litten wurd, wurd it fernijt nei de namme die it apparaat advertearret.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd yn de bondel-tastân sjen litten ynstee fan apparaat-ID. Wannear't leech litten wurd, wurd it fernijt nei de namme die it apparaat útstjoert.",
"Shutdown": "Ofslute",
"Shutdown Complete": "Ofsluten klear",
"Simple File Versioning": "Ienfâldich triemferzjebehear",
@@ -160,7 +160,7 @@
"Smallest First": "Lytste earst",
"Source Code": "Boarnekoade",
"Staggered File Versioning": "Sprieden triemferzjebehear",
"Start Browser": "Browser starte",
"Start Browser": "Browser iepenje wannear't Syncthing start",
"Statistics": "Statistiken",
"Stopped": "Stoppe",
"Support": "Understeuning",

View File

@@ -32,7 +32,7 @@
"Copied from elsewhere": "Kopiert fra et annet sted",
"Copied from original": "Kopiert fra original",
"Copyright © 2015 the following Contributors:": "Opphavsrett © 2015 de følgende bidragsytere:",
"Danger!": "Danger!",
"Danger!": "Fare!",
"Delete": "Slett",
"Deleted": "Slettet",
"Device ID": "Enhets ID",
@@ -148,12 +148,12 @@
"Share Folders With Device": "Del Mapper Med Enhet",
"Share With Devices": "Del Med Enheter",
"Share this folder?": "Dele denne mappen?",
"Shared With": "Delt Med",
"Shared With": "Delt med",
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort kjennemerke på mappen. Må være det samme på alle enheter i en gruppe.",
"Show ID": "Vis ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vis i stedet for Eining ID i gruppestatus. Vil bli kringkastet til andre enheter som et valgfritt standardnavn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist i stedet for Mappe-ID i gruppestatus. Vil bli oppdatert til navnet enheten kringkaster dersom tomt.",
"Shutdown": "Slå Av",
"Shutdown": "Avslutt",
"Shutdown Complete": "Avslutning fullført",
"Simple File Versioning": "Enkel Versjonskontroll",
"Single level wildcard (matches within a directory only)": "Enkeltnivåsøk (søker kun i en mappe)",
@@ -172,7 +172,7 @@
"Syncthing is upgrading.": "Syncthing oppgraderer.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å være nede, eller så er det et problem med nettforbindelsen din. Prøver på ny …",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ser ut til å ha støtt på et problem under behandling av din forespørsel. Vennligst oppfrisk nettleseren eller start Syncthing på nytt dersom problemet vedvarer.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Grensesnittet for administrering av Syncthing er satt til å tillate ekstern tilgang uten et passord.",
"The aggregated statistics are publicly available at {%url%}.": "Samlet statistikk er åpent tilgjengelig på {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Innstillingene har blitt lagret men ikke aktivert. Syncthing må starte på ny for å aktivere de nye innstillingene.",
"The device ID cannot be blank.": "Enhets-ID kan ikke være tom.",
@@ -197,7 +197,7 @@
"The rate limit must be a non-negative number (0: no limit)": "Hastighetsbegrensningen kan ikke være et negativt tall (0: ingen begrensing)",
"The rescan interval must be a non-negative number of seconds.": "Antall sekund i skanneintervallet kan ikke være negativt.",
"They are retried automatically and will be synced when the error is resolved.": "Disse hentes automatisk og vil synkroniseres når feilen er blitt utbedret.",
"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 can easily give hackers access to read and change any files on your computer.": "Dette kan lett gi hackere tilgang til å lese og endre alle filer på datamaskinen din.",
"This is a major version upgrade.": "Dette er en hovedoppgradering",
"Trash Can File Versioning": "Papirkurv Versjonskontroll",
"Unknown": "Ukjent",
@@ -220,6 +220,6 @@
"You must keep at least one version.": "Du må beholde minst én versjon",
"days": "dager",
"full documentation": "all dokumentasjon",
"items": "element",
"items": "elementer",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\"."
}

View File

@@ -138,7 +138,7 @@
"Resume": "Возобновить",
"Reused": "Повторно использовано",
"Save": "Сохранить",
"Scan Time Remaining": "Scan Time Remaining",
"Scan Time Remaining": "Оставшееся время сканирования",
"Scanning": "Сканирование",
"Select the devices to share this folder with.": "Выберите устройства, для которых будет доступна эта папка.",
"Select the folders to share with this device.": "Выберите папку для предоставления доступа данному устройству",

View File

@@ -0,0 +1,225 @@
{
"A negative number of days doesn't make sense.": "Số ngày không thể âm.",
"A new major version may not be compatible with previous versions.": "Phiên bản quan trọng mới có thể sẽ không tương thích với các bản cũ.",
"API Key": "Khoá API",
"About": "Thông tin về",
"Actions": "Hành động",
"Add": "Thêm",
"Add Device": "Thêm thiết bị",
"Add Folder": "Thêm thư mục",
"Add new folder?": "Thêm thư mục mới?",
"Address": "Địa chỉ",
"Addresses": "Các địa chỉ",
"Advanced": "Nâng cao",
"Advanced Configuration": "Cấu hình nâng cao",
"All Data": "Tất cả dữ liệu",
"Allow Anonymous Usage Reporting?": "Cho phép báo cáo sử dụng ẩn danh?",
"Alphabetic": "A-Z",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Một lệnh ngoại vi xử lý việc phiên bản hoá. Nó phải loại bỏ tập tin này khỏi thư mục đã đồng bộ.",
"Anonymous Usage Reporting": "Báo cáo sử dụng ẩn danh",
"Any devices configured on an introducer device will be added to this device as well.": "Bất kỳ thiết bị nào liên kết với thiết bị giới thiệu cũng sẽ được thêm vào.",
"Automatic upgrades": "Cập nhật tự động",
"Be careful!": "Cẩn thận!",
"Bugs": "Lỗi",
"CPU Utilization": "Mức sử dụng CPU",
"Changelog": "Lược sử thay đổi",
"Clean out after": "Dọn dẹp sau",
"Close": "Đóng",
"Command": "Lệnh",
"Comment, when used at the start of a line": "Bình luận, khi dùng trước đầu dòng",
"Compression": "Nén",
"Connection Error": "Lỗi kết nối",
"Copied from elsewhere": "Đã sao chép từ nơi khác",
"Copied from original": "Đã sao chép từ nguồn",
"Copyright © 2015 the following Contributors:": "Bản quyền © 2015 theo các nhà cộng tác sau:",
"Danger!": "Nguy hiểm!",
"Delete": "Xoá",
"Deleted": "Đã xoá",
"Device ID": "ID thiết bị",
"Device Identification": "Danh tính thiết bị",
"Device Name": "Tên thiết bị",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Thiết bị {{thiết bị}} ({{địa chỉ}}) muốn kết nối. Thêm thiết bị mới?",
"Devices": "Các thiết bị",
"Disconnected": "Đã ngắt kết nối",
"Discovery": "Dò tìm",
"Documentation": "Tài liệu",
"Download Rate": "Tốc độ tải xuống",
"Downloaded": "Đã tải xuống",
"Downloading": "Đang tải xuống",
"Edit": "Chỉnh sửa",
"Edit Device": "Chỉnh sửa thiết bị",
"Edit Folder": "Chỉnh sửa thư mục",
"Editing": "Đang chỉnh sửa",
"Enable UPnP": "Bật UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Nhập dấu phẩy ngăn cách các địa chỉ (\"tcp://ip:port\", \"tcp://host:port\") hoặc \"dynamic\" để tiến hành dò tìm địa chỉ tự động.",
"Enter ignore patterns, one per line.": "Nhập quy luật bỏ qua, từng dòng một.",
"Error": "Lỗi",
"External File Versioning": "Phiên bản hoá tập tin ngoại vi",
"Failed Items": "Các nội dung bị lỗi",
"File Pull Order": "Thứ tự pull tập tin",
"File Versioning": "Phiên bản hoá tập tin",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Các phần tử giấy phép tập tin sẽ được bỏ qua khi tìm kiếm thay đổi. Dùng trên hệ thống tập tin FAT.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Các tập tin sẽ được chuyển tới thư mục .stversions khi bị thay thế hoặc xoá bởi Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Các tập tin sẽ được chuyển tới các phiên bản được đánh dấu ngày tháng trong thư mục .stversions khi bị thay thế hoặc xoá bởi Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Các tập tin sẽ được bảo vệ khỏi những thay đổi trên các thiết bị khác, nhưng những thay đổi trên thiết bị này sẽ được chuyển tới những máy còn lại trong cụm.",
"Folder": "Thư mục",
"Folder ID": "ID thư mục",
"Folder Master": "Thư mục chính",
"Folder Path": "Đường dẫn thư mục",
"Folders": "Các thư mục",
"GUI": "GUI",
"GUI Authentication Password": "Mật khẩu xác minh GUI",
"GUI Authentication User": "Người dùng xác minh GUI",
"GUI Listen Addresses": "Các địa chỉ lắng nghe GUI",
"Generate": "Tạo mới",
"Global Discovery": "Dò tìm toàn cầu",
"Global Discovery Server": "Máy chủ dò tìm toàn cầu",
"Global State": "Hiện trạng toàn cầu",
"Help": "Trợ giúp",
"Home page": "Trang chủ",
"Ignore": "Bỏ qua",
"Ignore Patterns": "Bỏ qua các quy luật",
"Ignore Permissions": "Bỏ qua các giấy phép ",
"Incoming Rate Limit (KiB/s)": "Giới hạn tốc độ đầu vào (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Cấu hình không đúng có thể gây mất mát nội dung và khiến Syncthing dừng hoạt động.",
"Introducer": "Thiết bị giới thiệu",
"Inversion of the given condition (i.e. do not exclude)": "Đảo ngược điều kiện cho trước (ví dụ: không được loại trừ)",
"Keep Versions": "Giữ các phiên bản",
"Largest First": "Lớn nhất trước tiên",
"Last File Received": "Tập tin nhận cuối cùng",
"Last seen": "Thấy lần cuối",
"Later": "Để sau",
"Local Discovery": "Dò tìm cục bộ",
"Local State": "Trạng thái cục bộ",
"Local State (Total)": "Trạng thái cục bộ (Tổng)",
"Major Upgrade": "Bản nâng cấp quan trọng",
"Maximum Age": "Thời hạn tối đa",
"Metadata Only": "Chỉ siêu dữ liệu",
"Minimum Free Disk Space": "Dung lượng ổ đĩa trống tối thiểu",
"Move to top of queue": "Chuyển đến đầu hàng chờ",
"Multi level wildcard (matches multiple directory levels)": "Ký tự thay thế đa cấp (phù hợp với đa cấp độ thư mục)",
"Never": "Không bao giờ",
"New Device": "Thiết bị mới",
"New Folder": "Thư mục mới",
"Newest First": "Mới nhất đầu tiên",
"No": "Không",
"No File Versioning": "Không phiên bản hoá tập tin",
"Notice": "Chú ý",
"OK": "OK",
"Off": "Tắt",
"Oldest First": "Cũ nhất đầu tiên",
"Options": "Tuỳ chọn",
"Out of Sync": "Mất đồng bộ",
"Out of Sync Items": "Các nội dung mất đồng bộ",
"Outgoing Rate Limit (KiB/s)": "Giới hạn tốc độ đầu ra (KiB/s)",
"Override Changes": "Ghi đè các thay đổi",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Đường dẫn đến thư mục trên máy tính cục bộ. Sẽ tạo mới nếu chưa có sẵn. Dấu ngã (~) có thể được dùng làm lối tắt",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Đường dẫn nơi các phiên bản được lưu trữ (để trống cho thư mục mặc định .stversions).",
"Pause": "Tạm dừng",
"Paused": "Đã tạm dừng",
"Please consult the release notes before performing a major upgrade.": "Hãy xem kỹ lược sử phát hành trước khi tiến hành bản cập nhật quan trọng.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Hãy thiết lập tên người dùng và mật khẩu xác minh GUI trong hộp thoại Cài đặt.",
"Please wait": "Xin chờ",
"Preview": "Xem trước",
"Preview Usage Report": "Xem trước báo cáo sử dụng",
"Quick guide to supported patterns": "Hướng dẫn sơ lược về các quy luật được hỗ trợ",
"RAM Utilization": "Mức sử dụng RAM",
"Random": "Ngẫu nhiên",
"Relayed via": "Chuyển tiếp qua",
"Relays": "Các máy chuyển tiếp",
"Release Notes": "Lược sử phát hành",
"Remove": "Loại bỏ",
"Rescan": "Quét lại",
"Rescan All": "Quét lại tất cả",
"Rescan Interval": "Khoảng cách thời gian quét lại",
"Restart": "Khởi động lại",
"Restart Needed": "Cần khởi động lại",
"Restarting": "Đang khởi động lại",
"Resume": "Tiếp tục",
"Reused": "Đã dùng lại",
"Save": "Sao lưu",
"Scan Time Remaining": "Thời gian quét còn lại",
"Scanning": "Đang quét",
"Select the devices to share this folder with.": "Chọn các thiết bị để chia sẻ thư mục này.",
"Select the folders to share with this device.": "Chọn các thư mục để chia sẻ với thiết bị này.",
"Settings": "Cài đặt",
"Share": "Chia sẻ",
"Share Folder": "Chia sẻ thư mục",
"Share Folders With Device": "Chia sẻ các thư mục với thiết bị",
"Share With Devices": "Chia sẻ với các thiết bị",
"Share this folder?": "Chia sẻ thư mục này?",
"Shared With": "Đã chia sẻ với",
"Short identifier for the folder. Must be the same on all cluster devices.": "Tên tắt cho thư mục. Phải trùng khớp trên tất cả thiết bị trong cụm.",
"Show ID": "Hiển thị ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Hiển thị thay cho ID thiết bị trong trạng thái cụm. Sẽ được giới thiệu đến các thiết bị khác như tên mặc định tuỳ chọn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Hiển thị thay cho ID thiết bị trong trạng thái cụm. Nếu để trống sẽ được cập nhật thành tên mà thiết bị giới thiệu.",
"Shutdown": "Đóng",
"Shutdown Complete": "Hoàn tất đóng",
"Simple File Versioning": "Phiên bản hoá tập tin dạng đơn giản",
"Single level wildcard (matches within a directory only)": "Ký tự thay thế đơn cấp (phù hợp với chỉ một thư mục)",
"Smallest First": "Nhỏ nhất đầu tiên",
"Source Code": "Mã nguồn",
"Staggered File Versioning": "Phiên bản hoá tập tin theo thời gian",
"Start Browser": "Mở trình duyệt",
"Statistics": "Thống kê",
"Stopped": "Đã dừng",
"Support": "Hỗ trợ",
"Sync Protocol Listen Addresses": "Đồng bộ các địa chỉ lắng nghe giao thức",
"Syncing": "Đang đồng bộ",
"Syncthing has been shut down.": "Đã đóng Syncthing.",
"Syncthing includes the following software or portions thereof:": "Syncthing bao gồm phần mềm hoặc các phần tử của phần mềm sau:",
"Syncthing is restarting.": "Syncthing đang khởi động lại.",
"Syncthing is upgrading.": "Syncthing đang nâng cấp.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Có vẻ như Syncthing đang bị nghẽn hoặc là mạng của bạn có vấn đề. Đang thử lại...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Có vẻ như Syncthing đang gặp phải vấn đề khi xử lý yêu cầu của bạn. Xin làm mới trang hoặc khởi động lại Syncthing nếu vấn đề vẫn còn tiếp diễn.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Giao diện quản trị của Syncthing được cấu hình nhằm cho phép truy cập từ xa không cần mật mã.",
"The aggregated statistics are publicly available at {%url%}.": "Thống kê tổng hợp được đăng công khai trên {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Cấu hình đã được lưu nhưng chưa được kích hoạt. Syncthing phải khởi động lại để kích hoạt cấu hình mới. ",
"The device ID cannot be blank.": "ID thiết bị không thể trống.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ID thiết bị cần nhập có thể được tìm thấy trong hộp thoại \"Chỉnh sửa > Hiển thị ID\" trên thiết bị kia. Khoảng trắng và gạch ngang là tuỳ chọn (bỏ qua).",
"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.": "Báo cáo sử dụng đã mã khoá sẽ được gửi đi hằng ngày. Nó được dùng để thu thập số liệu về các hệ điều hành phổ biến, kích cỡ thư mục và phiên bản ứng dụng. Nếu bộ dữ liệu báo cáo có thay đổi, bạn sẽ được nhắc nhở thông qua hộp thoại này.",
"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 thiết bị đã nhập không hợp lệ. Nó phải là một chuỗi từ 52 đến 56 ký tự, bao gồm chữ cái và các con số, với khoảng trắng và gạch ngang là tuỳ chọn.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Tham số dòng lệnh đầu tiên là đường dẫn thư mục và tham số thứ hai là đường dẫn tương đối trong thư mục.",
"The folder ID cannot be blank.": "ID thư mục không thể trống.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "ID thư mục phải là một tên tắt (64 ký tự hoặc ít hơn) chỉ bao gồm chữ cái, các con số và dấu chấm (.), gạch ngang (-) và gạch chân (_).",
"The folder ID must be unique.": "ID thư mục phải là duy nhất.",
"The folder path cannot be blank.": "Đường dẫn thư mục không thể trống.",
"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.": "Các khoảng thời gian sau đây được sử dụng: một phiên bản, trong giờ đầu tiên, được giữ lại mỗi 30 giây, trong ngày đầu tiên là mỗi giờ, trong 30 ngày đầu tiên là mỗi ngày, cho đến khi thời hạn tối đa mỗi phiên bản được giữ lại là mỗi tuần. ",
"The following items could not be synchronized.": "Những nội dung sau không thể đồng bộ được.",
"The maximum age must be a number and cannot be blank.": "Thời hạn tối đa phải là một con số và không thể để trống.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Thời gian tối đa phiên bản được giữ lại (tính bằng ngày, từ 0 đến mãi mãi).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Phần trăm dung lượng ổ đĩa còn trống tối thiểu phải là một số không âm (trong khoảng) từ 0 đến 100.",
"The number of days must be a number and cannot be blank.": "Số ngày phải là một con số và không thể để trống.",
"The number of days to keep files in the trash can. Zero means forever.": "Số ngày giữ tập tin trong thùng rác. Số 0 nghĩa là mãi mãi.",
"The number of old versions to keep, per file.": "Số phiên bản cũ cần giữ, với mỗi tập tin.",
"The number of versions must be a number and cannot be blank.": "Số phiên bản phải là một con số và không thể để trống.",
"The path cannot be blank.": "Đường dẫn không thể trống.",
"The rate limit must be a non-negative number (0: no limit)": "Giới hạn tốc độ phải là một số không âm (0: không giới hạn)",
"The rescan interval must be a non-negative number of seconds.": "Khoảng thời gian quét lại phải là một số giây không âm. ",
"They are retried automatically and will be synced when the error is resolved.": "Chúng sẽ được tự động thử lại và đồng bộ khi lỗi được khắc phục.",
"This can easily give hackers access to read and change any files on your computer.": "Thao tác này có thể khiến tin tặc truy cập để đọc và thay đổi bất kỳ tập tin nào trên máy tính của bạn một cách dễ dàng.",
"This is a major version upgrade.": "Đây là bản nâng cấp quan trọng.",
"Trash Can File Versioning": "Phiên bản hoá tập tin trong thùng rác",
"Unknown": "Không biết",
"Unshared": "Chưa chia sẻ",
"Unused": "Chưa sử dụng",
"Up to Date": "Đã cập nhật",
"Updated": "Đã cập nhật",
"Upgrade": "Nâng cấp",
"Upgrade To {%version%}": "Nâng cấp lên {{phiên bản}}",
"Upgrading": "Đang nâng cấp",
"Upload Rate": "Tốc độ tải lên",
"Uptime": "Thời gian hoạt động",
"Use HTTPS for GUI": "Sử dụng HTTPS cho GUI",
"Version": "Phiên bản",
"Versions Path": "Đường dẫn các phiên bản",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Các phiên bản sẽ tự động được xoá nếu chúng cũ hơn thời hạn tối đa hoặc vượt quá số tập tin cho phép trong một khoảng thời gian.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Khi thêm một thiết bị mới, hãy nhớ rằng thiết bị này cũng được thêm vào máy khác.",
"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.": "Khi thêm một thư mục mới, hãy nhớ rằng ID thư mục được dùng để gắn kết thư mục giữa các thiết bị với nhau. Chúng phải chính xác từng chữ, cả viết hoa và thường giữa tất cả thiết bị.",
"Yes": "Có",
"You must keep at least one version.": "Bạn phải giữ ít nhất một phiên bản.",
"days": "ngày",
"full documentation": "tài liệu đầy đủ",
"items": "các nội dung",
"{%device%} wants to share folder \"{%folder%}\".": "{{thiết bị}} muốn chia sẻ thư mục \"{{thư mục}}\"."
}

View File

@@ -1,5 +1,5 @@
{
"A negative number of days doesn't make sense.": "天数不能为负",
"A negative number of days doesn't make sense.": "天数不能为负",
"A new major version may not be compatible with previous versions.": "重大更新可能与之前的版本之间无法兼容",
"API Key": "API Key",
"About": "关于",
@@ -20,7 +20,7 @@
"Any devices configured on an introducer device will be added to this device as well.": "在介绍人设备上被添加的其它设备,也将会被添加到本机。",
"Automatic upgrades": "自动升级",
"Be careful!": "小心!",
"Bugs": "Bug汇报",
"Bugs": "问题回报",
"CPU Utilization": "CPU使用率",
"Changelog": "更新日志",
"Clean out after": "在该时间后清除:",
@@ -68,12 +68,12 @@
"Folder Path": "文件夹路径",
"Folders": "文件夹",
"GUI": "图形界面",
"GUI Authentication Password": "登陆web管理页面的密码",
"GUI Authentication User": "登陆web管理页面的用户名",
"GUI Listen Addresses": "web管理面监听地址",
"GUI Authentication Password": "图形管理界面密码",
"GUI Authentication User": "图形管理界面用户名",
"GUI Listen Addresses": "图形管理面监听地址",
"Generate": "生成",
"Global Discovery": "在互联网上寻找设备\n",
"Global Discovery Server": "用以在互联网上寻找设备的Announce服务器地址",
"Global Discovery Server": "全球发现服务器",
"Global State": "全局状态",
"Help": "帮助",
"Home page": "主页",
@@ -138,7 +138,7 @@
"Resume": "恢复",
"Reused": "复用",
"Save": "保存",
"Scan Time Remaining": "Scan Time Remaining",
"Scan Time Remaining": "扫描剩余时间",
"Scanning": "扫描中",
"Select the devices to share this folder with.": "选择将本文件夹共享给哪些设备",
"Select the folders to share with this device.": "选择与该设备共享的文件夹。",
@@ -172,7 +172,7 @@
"Syncthing is upgrading.": "Syncthing 正在升级",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎关闭了,或者您的网络连接存在故障。重试中...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing 在处理您的请求时似乎遇到了问题。如果问题持续,请刷新页面,或重启 Syncthing。",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The Syncthing admin interface is configured to allow remote access without a password.": "当前配置允许在不使用密码的情况下远程访问 Syncthing 管理界面",
"The aggregated statistics are publicly available at {%url%}.": "全局统计公布于 {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "设置已经保存但是还未生效。Syncthing 需要重启以启用新的设置。",
"The device ID cannot be blank.": "设备标识不能为空",
@@ -184,7 +184,7 @@
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "文件夹标识不得长于 64 字符,且仅能包含字母、数字、半角句号(.)、横线(-和下划线_。",
"The folder ID must be unique.": "文件夹标识不得重复",
"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小时的仅保留一份。最近一个月内的历史版本,更新间隔小于1天的仅保留一份。距离现在超过一个月且小于最长保留时间的,更新间隔小于1周的仅保留一份。",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "保留的历史版本会遵循以下条件:最近一小时内的历史版本,更新间隔小于三十秒的仅保留一份。最近一天内的历史版本,更新间隔小于小时的仅保留一份。最近一个月内的历史版本,更新间隔小于天的仅保留一份。距离现在超过一个月且小于最长保留时间的,更新间隔小于周的仅保留一份。",
"The following items could not be synchronized.": "下列项目无法被同步。",
"The maximum age must be a number and cannot be blank.": "最长保留时间必须为数字,且不能为空。",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "历史版本保留的最长天数0为永久保存",
@@ -197,8 +197,8 @@
"The rate limit must be a non-negative number (0: no limit)": "传输速度限制为非负整数0 表示不限制)",
"The rescan interval must be a non-negative number of seconds.": "扫描间隔单位为秒,且不能为负数。",
"They are retried automatically and will be synced when the error is resolved.": "系统将会自动重试,当错误被解决时,它们将会被同步。",
"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 is a major version upgrade.": "这是一个重大版本更新",
"This can easily give hackers access to read and change any files on your computer.": "这会让骇客能够轻而易举地访问及修改您的文件",
"This is a major version upgrade.": "这是一个重大版本更新",
"Trash Can File Versioning": "回收站式版本控制",
"Unknown": "未知",
"Unshared": "未共享",
@@ -210,14 +210,14 @@
"Upgrading": "升级中",
"Upload Rate": "上传速度",
"Uptime": "已启动",
"Use HTTPS for GUI": "使用HTTPS连接web管理页面",
"Use HTTPS for GUI": "使用加密连接到图形管理页面",
"Version": "版本",
"Versions Path": "历史版本路径",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "超过最长保留时间,或者不满足下列条件的历史版本,将会被删除。",
"When adding a new device, keep in mind that this device must be added on the other side too.": "若您在本机添加新设备,记住您也必须在这个设备上添加本机。",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "若你添加了新文件夹,记住文件夹标识是用以在不同设备间建立联系的。在不同设备间拥有相同标识的文件夹将会被同步。且文件夹标识大小写敏感。",
"Yes": "是",
"You must keep at least one version.": "您必须保留至少一个版本",
"You must keep at least one version.": "您必须保留至少一个版本",
"days": "天",
"full documentation": "完整文档",
"items": "条目",

View File

@@ -1 +1 @@
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian (Romania)","ru":"Russian","sv":"Swedish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian (Romania)","ru":"Russian","sv":"Swedish","uk":"Ukrainian","vi":"Vietnamese","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}

View File

@@ -1 +1 @@
var validLangs = ["bg","ca","ca@valencia","cs","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","uk","zh-CN","zh-TW"]
var validLangs = ["bg","ca","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","uk","vi","zh-CN","zh-TW"]

View File

@@ -15,6 +15,7 @@
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="assets/img/favicon.png">
<link rel="mask-icon" href="assets/img/safari-pinned-tab.svg" color="#0882c8">
<title ng-bind="thisDeviceName() + ' | Syncthing'"></title>
<link href="vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
@@ -121,21 +122,22 @@
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">
<identicon data-value="device"></identicon>&nbsp;<span translate>New Device</span>
<identicon data-value="device"></identicon>
<span translate>New Device</span>
<span class="pull-right">{{ event.time | date:"yyyy-MM-dd HH:mm:ss" }}</span>
</h3>
</div>
<div class="panel-body">
<p>
<small>{{ event.time | date:"yyyy-MM-dd HH:mm:ss" }}:</small>
<span translate translate-value-device="{{ device }}" translate-value-address="{{ event.data.address }}">
Device {%device%} ({%address%}) wants to connect. Add new device?
<span>
</span>
</p>
</div>
<div class="panel-footer clearfix">
<div class="pull-right">
<button type="button" class="btn btn-sm btn-success" ng-click="addNewDeviceID(device)">
<span class="fa fa-check"></span>&nbsp;<span translate>Add</span>
<button type="button" class="btn btn-sm btn-success" ng-click="addDevice(device)">
<span class="fa fa-plus"></span>&nbsp;<span translate>Add Device</span>
</button>
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreRejectedDevice(device)">
<span class="fa fa-times"></span>&nbsp;<span translate>Ignore</span>
@@ -158,11 +160,11 @@
<h3 class="panel-title"><span class="fa fa-folder"></span>
<span translate ng-if="!folders[event.data.folder]">New Folder</span>
<span translate ng-if="folders[event.data.folder]">Share Folder</span>
<span class="pull-right">{{ event.time | date:"yyyy-MM-dd HH:mm:ss" }}</span>
</h3>
</div>
<div class="panel-body">
<p>
<small>{{ event.time | date:"yyyy-MM-dd HH:mm:ss" }}:</small>
<span translate translate-value-device="{{ deviceName(findDevice(event.data.device)) }}" translate-value-folder="{{ event.data.folder }}">
{%device%} wants to share folder "{%folder%}".
</span>

View File

@@ -15,6 +15,7 @@
<li class="auto-generated">Aaron Bieber</li>
<li class="auto-generated">Adam Piggott</li>
<li class="auto-generated">Alexander Graf</li>
<li class="auto-generated">Anderson Mesquita</li>
<li class="auto-generated">Andrew Dunham</li>
<li class="auto-generated">Antony Male</li>
<li class="auto-generated">Arthur Axel fREW Schmidt</li>
@@ -46,6 +47,7 @@
<li class="auto-generated">Francois-Xavier Gsell</li>
<li class="auto-generated">Frank Isemann</li>
<li class="auto-generated">Gilli Sigurdsson</li>
<li class="auto-generated">Jaakko Hannikainen</li>
<li class="auto-generated">Jacek Szafarkiewicz</li>
<li class="auto-generated">Jake Peterson</li>
<li class="auto-generated">Jakob Borg</li>
@@ -66,6 +68,7 @@
<li class="auto-generated">Michael Jephcote</li>
<li class="auto-generated">Michael Ploujnikov</li>
<li class="auto-generated">Michael Tilli</li>
<li class="auto-generated">Nate Morrison</li>
<li class="auto-generated">Pascal Jungblut</li>
<li class="auto-generated">Peter Hoeg</li>
<li class="auto-generated">Philippe Schommers</li>
@@ -84,6 +87,7 @@
<li class="auto-generated">Veeti Paananen</li>
<li class="auto-generated">Victor Buinsky</li>
<li class="auto-generated">Vil Brekin</li>
<li class="auto-generated">William A. Kennington III</li>
<li class="auto-generated">Yannic A.</li>
</ul>
</div>

View File

@@ -970,13 +970,14 @@ angular.module('syncthing.core')
$('#idqr').modal('show');
};
$scope.addDevice = function () {
$http.get(urlbase + '/system/discovery')
$scope.addDevice = function (deviceID) {
return $http.get(urlbase + '/system/discovery')
.success(function (registry) {
$scope.discovery = registry;
})
.then(function () {
$scope.currentDevice = {
deviceID: deviceID,
_addressesStr: 'dynamic',
compression: 'metadata',
introducer: false,
@@ -1016,18 +1017,7 @@ angular.module('syncthing.core')
$scope.saveDevice = function () {
$('#editDevice').modal('hide');
$scope.saveDeviceConfig($scope.currentDevice);
};
$scope.addNewDeviceID = function (device) {
var deviceCfg = {
deviceID: device,
_addressesStr: 'dynamic',
compression: 'metadata',
introducer: false,
selectedFolders: {}
};
$scope.saveDeviceConfig(deviceCfg);
$scope.dismissDeviceRejection(device);
$scope.dismissDeviceRejection($scope.currentDevice.deviceID);
};
$scope.saveDeviceConfig = function (deviceCfg) {

View File

@@ -48,13 +48,6 @@
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label ng-if="upgradeInfo">
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.autoUpgradeEnabled"> <span translate>Automatic upgrades</span>
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
@@ -62,7 +55,15 @@
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label ng-if="upgradeInfo">
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.autoUpgradeEnabled"> <span translate>Automatic upgrades</span>
</label>
</div>
</div>
</div>
<div class="clearfix"></div>
<div class="form-group">
<label translate for="GlobalAnnServersStr">Global Discovery Server</label>
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr">

View File

File diff suppressed because one or more lines are too long

View File

@@ -48,17 +48,6 @@ var (
// DefaultDiscoveryServers should be substituted when the configuration
// contains <globalAnnounceServer>default</globalAnnounceServer>.
DefaultDiscoveryServers = append(DefaultDiscoveryServersV4, DefaultDiscoveryServersV6...)
// DefaultDiscoveryServersIP is used by the usage reporting.
// XXX: Detect Android, and use this is we still don't have working DNS?
DefaultDiscoveryServersIP = []string{
"https://194.126.249.5/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA",
"https://45.55.230.38/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS",
"https://128.199.95.124/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ",
"https://[2001:470:28:4d6::5]/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA",
"https://[2604:a880:800:10::182:a001]/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS",
"https://[2400:6180:0:d0::d9:d001]/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ",
}
)
func New(myID protocol.DeviceID) Configuration {

View File

@@ -44,7 +44,7 @@ func (c GUIConfiguration) Address() string {
}
func (c GUIConfiguration) UseTLS() bool {
if override := os.Getenv("STGUIADDRESS"); override != "" {
if override := os.Getenv("STGUIADDRESS"); override != "" && strings.HasPrefix(override, "http") {
return strings.HasPrefix(override, "https:")
}
return c.RawUseTLS

View File

@@ -42,9 +42,9 @@ type Model interface {
IsPaused(remoteID protocol.DeviceID) bool
}
// The connection service listens on TLS and dials configured unconnected
// The connection connectionService listens on TLS and dials configured unconnected
// devices. Successful connections are handed to the model.
type connectionSvc struct {
type connectionService struct {
*suture.Supervisor
cfg *config.Wrapper
myID protocol.DeviceID
@@ -52,7 +52,7 @@ type connectionSvc struct {
tlsCfg *tls.Config
discoverer discover.Finder
conns chan model.IntermediateConnection
relaySvc *relay.Svc
relayService *relay.Service
bepProtocolName string
tlsDefaultCommonName string
lans []*net.IPNet
@@ -66,16 +66,16 @@ type connectionSvc struct {
relaysEnabled bool
}
func NewConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, relaySvc *relay.Svc,
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, relayService *relay.Service,
bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) suture.Service {
svc := &connectionSvc{
Supervisor: suture.NewSimple("connectionSvc"),
service := &connectionService{
Supervisor: suture.NewSimple("connectionService"),
cfg: cfg,
myID: myID,
model: mdl,
tlsCfg: tlsCfg,
discoverer: discoverer,
relaySvc: relaySvc,
relayService: relayService,
conns: make(chan model.IntermediateConnection),
bepProtocolName: bepProtocolName,
tlsDefaultCommonName: tlsDefaultCommonName,
@@ -85,20 +85,20 @@ func NewConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tl
relaysEnabled: cfg.Options().RelaysEnabled,
lastRelayCheck: make(map[protocol.DeviceID]time.Time),
}
cfg.Subscribe(svc)
cfg.Subscribe(service)
if svc.cfg.Options().MaxSendKbps > 0 {
svc.writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*svc.cfg.Options().MaxSendKbps), int64(5*1000*svc.cfg.Options().MaxSendKbps))
if service.cfg.Options().MaxSendKbps > 0 {
service.writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*service.cfg.Options().MaxSendKbps), int64(5*1000*service.cfg.Options().MaxSendKbps))
}
if svc.cfg.Options().MaxRecvKbps > 0 {
svc.readRateLimit = ratelimit.NewBucketWithRate(float64(1000*svc.cfg.Options().MaxRecvKbps), int64(5*1000*svc.cfg.Options().MaxRecvKbps))
if service.cfg.Options().MaxRecvKbps > 0 {
service.readRateLimit = ratelimit.NewBucketWithRate(float64(1000*service.cfg.Options().MaxRecvKbps), int64(5*1000*service.cfg.Options().MaxRecvKbps))
}
// There are several moving parts here; one routine per listening address
// to handle incoming connections, one routine to periodically attempt
// outgoing connections, one routine to the the common handling
// regardless of whether the connection was incoming or outgoing.
// Furthermore, a relay service which handles incoming requests to connect
// Furthermore, a relay connectionService which handles incoming requests to connect
// via the relays.
//
// TODO: Clean shutdown, and/or handling config changes on the fly. We
@@ -106,8 +106,8 @@ func NewConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tl
// not new listen addresses and we don't support disconnecting devices
// that are removed and so on...
svc.Add(serviceFunc(svc.connect))
for _, addr := range svc.cfg.Options().ListenAddress {
service.Add(serviceFunc(service.connect))
for _, addr := range service.cfg.Options().ListenAddress {
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse listen address:", addr, err)
@@ -122,20 +122,20 @@ func NewConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tl
l.Debugln("listening on", uri)
svc.Add(serviceFunc(func() {
listener(uri, svc.tlsCfg, svc.conns)
service.Add(serviceFunc(func() {
listener(uri, service.tlsCfg, service.conns)
}))
}
svc.Add(serviceFunc(svc.handle))
service.Add(serviceFunc(service.handle))
if svc.relaySvc != nil {
svc.Add(serviceFunc(svc.acceptRelayConns))
if service.relayService != nil {
service.Add(serviceFunc(service.acceptRelayConns))
}
return svc
return service
}
func (s *connectionSvc) handle() {
func (s *connectionService) handle() {
next:
for c := range s.conns {
cs := c.Conn.ConnectionState()
@@ -257,15 +257,18 @@ next:
}
}
func (s *connectionSvc) connect() {
func (s *connectionService) connect() {
delay := time.Second
for {
l.Debugln("Reconnect loop")
nextDevice:
for deviceID, deviceCfg := range s.cfg.Devices() {
if deviceID == s.myID {
continue
}
l.Debugln("Reconnect loop for", deviceID)
if s.model.IsPaused(deviceID) {
continue
}
@@ -277,6 +280,7 @@ func (s *connectionSvc) connect() {
relaysEnabled := s.relaysEnabled
s.mut.RUnlock()
if connected && ok && ct.IsDirect() {
l.Debugln("Already connected to", deviceID, "via", ct.String())
continue
}
@@ -284,6 +288,7 @@ func (s *connectionSvc) connect() {
for _, addr := range addrs {
if conn := s.connectDirect(deviceID, addr); conn != nil {
l.Debugln("Connecting to", deviceID, "via", addr, "succeeded")
if connected {
s.model.Close(deviceID, fmt.Errorf("switching connections"))
}
@@ -292,6 +297,7 @@ func (s *connectionSvc) connect() {
}
continue nextDevice
}
l.Debugln("Connecting to", deviceID, "via", addr, "failed")
}
// Only connect via relays if not already connected
@@ -300,6 +306,7 @@ func (s *connectionSvc) connect() {
// wait up to RelayReconnectIntervalM to connect again.
// Also, do not try relays if we are explicitly told not to.
if connected || len(relays) == 0 || !relaysEnabled {
l.Debugln("Not connecting via relay", connected, len(relays) == 0, !relaysEnabled)
continue nextDevice
}
@@ -307,19 +314,21 @@ func (s *connectionSvc) connect() {
if last, ok := s.lastRelayCheck[deviceID]; ok && time.Since(last) < reconIntv {
l.Debugln("Skipping connecting via relay to", deviceID, "last checked at", last)
continue nextDevice
} else {
l.Debugln("Trying relay connections to", deviceID, relays)
}
l.Debugln("Trying relay connections to", deviceID, relays)
s.lastRelayCheck[deviceID] = time.Now()
for _, addr := range relays {
if conn := s.connectViaRelay(deviceID, addr); conn != nil {
l.Debugln("Connecting to", deviceID, "via", addr, "succeeded")
s.conns <- model.IntermediateConnection{
conn, model.ConnectionTypeRelayDial,
}
continue nextDevice
}
l.Debugln("Connecting to", deviceID, "via", addr, "failed")
}
}
@@ -331,7 +340,7 @@ func (s *connectionSvc) connect() {
}
}
func (s *connectionSvc) resolveAddresses(deviceID protocol.DeviceID, inAddrs []string) (addrs []string, relays []discover.Relay) {
func (s *connectionService) resolveAddresses(deviceID protocol.DeviceID, inAddrs []string) (addrs []string, relays []discover.Relay) {
for _, addr := range inAddrs {
if addr == "dynamic" {
if s.discoverer != nil {
@@ -347,7 +356,7 @@ func (s *connectionSvc) resolveAddresses(deviceID protocol.DeviceID, inAddrs []s
return
}
func (s *connectionSvc) connectDirect(deviceID protocol.DeviceID, addr string) *tls.Conn {
func (s *connectionService) connectDirect(deviceID protocol.DeviceID, addr string) *tls.Conn {
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse connection url:", addr, err)
@@ -370,7 +379,7 @@ func (s *connectionSvc) connectDirect(deviceID protocol.DeviceID, addr string) *
return conn
}
func (s *connectionSvc) connectViaRelay(deviceID protocol.DeviceID, addr discover.Relay) *tls.Conn {
func (s *connectionService) connectViaRelay(deviceID protocol.DeviceID, addr discover.Relay) *tls.Conn {
uri, err := url.Parse(addr.URL)
if err != nil {
l.Infoln("Failed to parse relay connection url:", addr, err)
@@ -409,9 +418,9 @@ func (s *connectionSvc) connectViaRelay(deviceID protocol.DeviceID, addr discove
return tc
}
func (s *connectionSvc) acceptRelayConns() {
func (s *connectionService) acceptRelayConns() {
for {
conn := s.relaySvc.Accept()
conn := s.relayService.Accept()
s.conns <- model.IntermediateConnection{
Conn: conn,
Type: model.ConnectionTypeRelayAccept,
@@ -419,7 +428,7 @@ func (s *connectionSvc) acceptRelayConns() {
}
}
func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
func (s *connectionService) shouldLimit(addr net.Addr) bool {
if s.cfg.Options().LimitBandwidthInLan {
return true
}
@@ -436,11 +445,11 @@ func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
return !tcpaddr.IP.IsLoopback()
}
func (s *connectionSvc) VerifyConfiguration(from, to config.Configuration) error {
func (s *connectionService) VerifyConfiguration(from, to config.Configuration) error {
return nil
}
func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool {
func (s *connectionService) CommitConfiguration(from, to config.Configuration) bool {
s.mut.Lock()
s.relaysEnabled = to.Options.RelaysEnabled
s.mut.Unlock()

View File

@@ -65,8 +65,8 @@ func (m *CachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration, p
m.caches = append(m.caches, newCache())
m.mut.Unlock()
if svc, ok := finder.(suture.Service); ok {
m.Supervisor.Add(svc)
if service, ok := finder.(suture.Service); ok {
m.Supervisor.Add(service)
}
}

View File

@@ -1128,10 +1128,16 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
m.fmut.RUnlock()
files.Update(protocol.LocalDeviceID, fs)
filenames := make([]string, len(fs))
for i, file := range fs {
filenames[i] = file.Name
}
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
"folder": folder,
"items": len(fs),
"version": files.LocalVersion(protocol.LocalDeviceID),
"folder": folder,
"items": len(fs),
"filenames": filenames,
"version": files.LocalVersion(protocol.LocalDeviceID),
})
}
@@ -1753,8 +1759,11 @@ func (m *Model) CheckFolderHealth(id string) error {
// Check for free space, if it isn't a master folder. We aren't
// going to change the contents of master folders, so we don't
// care about the amount of free space there.
if free, errDfp := osutil.DiskFreePercentage(folder.Path()); errDfp == nil && free < folder.MinDiskFreePct {
err = errors.New("insufficient free space")
diskFreeP, errDfp := osutil.DiskFreePercentage(folder.Path())
if errDfp == nil && diskFreeP < folder.MinDiskFreePct {
diskFreeBytes, _ := osutil.DiskFreeBytes(folder.Path())
str := fmt.Sprintf("insufficient free space (%d MiB, %.2f%%)", diskFreeBytes/1024/1024, diskFreeP)
err = errors.New(str)
}
}
} else {

View File

@@ -79,19 +79,20 @@ type rwFolder struct {
progressEmitter *ProgressEmitter
virtualMtimeRepo *db.VirtualMtimeRepo
folder string
dir string
scanIntv time.Duration
versioner versioner.Versioner
ignorePerms bool
copiers int
pullers int
shortID uint64
order config.PullOrder
maxConflicts int
sleep time.Duration
pause time.Duration
allowSparse bool
folder string
dir string
scanIntv time.Duration
versioner versioner.Versioner
ignorePerms bool
copiers int
pullers int
shortID uint64
order config.PullOrder
maxConflicts int
sleep time.Duration
pause time.Duration
allowSparse bool
checkFreeSpace bool
stop chan struct{}
queue *jobQueue
@@ -117,16 +118,17 @@ func newRWFolder(m *Model, shortID uint64, cfg config.FolderConfiguration) *rwFo
progressEmitter: m.progressEmitter,
virtualMtimeRepo: db.NewVirtualMtimeRepo(m.db, cfg.ID),
folder: cfg.ID,
dir: cfg.Path(),
scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second,
ignorePerms: cfg.IgnorePerms,
copiers: cfg.Copiers,
pullers: cfg.Pullers,
shortID: shortID,
order: cfg.Order,
maxConflicts: cfg.MaxConflicts,
allowSparse: !cfg.DisableSparseFiles,
folder: cfg.ID,
dir: cfg.Path(),
scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second,
ignorePerms: cfg.IgnorePerms,
copiers: cfg.Copiers,
pullers: cfg.Pullers,
shortID: shortID,
order: cfg.Order,
maxConflicts: cfg.MaxConflicts,
allowSparse: !cfg.DisableSparseFiles,
checkFreeSpace: cfg.MinDiskFreePct != 0,
stop: make(chan struct{}),
queue: newJobQueue(),
@@ -967,23 +969,11 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
}
}
if free, err := osutil.DiskFreeBytes(p.dir); err == nil && free < file.Size() {
l.Warnf(`Folder "%s": insufficient disk space in %s for %s: have %.2f MiB, need %.2f MiB`, p.folder, p.dir, file.Name, float64(free)/1024/1024, float64(file.Size())/1024/1024)
p.newError(file.Name, errors.New("insufficient space"))
return
}
events.Default.Log(events.ItemStarted, map[string]string{
"folder": p.folder,
"item": file.Name,
"type": "file",
"action": "update",
})
scanner.PopulateOffsets(file.Blocks)
reused := 0
var blocks []protocol.BlockInfo
var blocksSize int64
// Check for an old temporary file which might have some blocks we could
// reuse.
@@ -1003,6 +993,7 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
_, ok := existingBlocks[block.String()]
if !ok {
blocks = append(blocks, block)
blocksSize += int64(block.Size)
}
}
@@ -1017,8 +1008,24 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
}
} else {
blocks = file.Blocks
blocksSize = file.Size()
}
if p.checkFreeSpace {
if free, err := osutil.DiskFreeBytes(p.dir); err == nil && free < blocksSize {
l.Warnf(`Folder "%s": insufficient disk space in %s for %s: have %.2f MiB, need %.2f MiB`, p.folder, p.dir, file.Name, float64(free)/1024/1024, float64(blocksSize)/1024/1024)
p.newError(file.Name, errors.New("insufficient space"))
return
}
}
events.Default.Log(events.ItemStarted, map[string]string{
"folder": p.folder,
"item": file.Name,
"type": "file",
"action": "update",
})
s := sharedPullerState{
file: file,
folder: p.folder,

View File

@@ -42,6 +42,40 @@ var blocks = []protocol.BlockInfo{
var folders = []string{"default"}
func setUpFile(filename string, blockNumbers []int) protocol.FileInfo {
// Create existing file
existingBlocks := make([]protocol.BlockInfo, len(blockNumbers))
for i := range blockNumbers {
existingBlocks[i] = blocks[blockNumbers[i]]
}
return protocol.FileInfo{
Name: filename,
Flags: 0,
Modified: 0,
Blocks: existingBlocks,
}
}
func setUpModel(file protocol.FileInfo) *Model {
db := db.OpenMemory()
model := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
model.AddFolder(defaultFolderConfig)
// Update index
model.updateLocals("default", []protocol.FileInfo{file})
return model
}
func setUpRwFolder(model *Model) rwFolder {
return rwFolder{
folder: "default",
dir: "testdata",
model: model,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
}
// Layout of the files: (indexes from the above array)
// 12345678 - Required file
// 02005008 - Existing file (currently in the index)
@@ -52,35 +86,13 @@ func TestHandleFile(t *testing.T) {
// Copy: 2, 5, 8
// Pull: 1, 3, 4, 6, 7
// Create existing file
existingFile := protocol.FileInfo{
Name: "filex",
Flags: 0,
Modified: 0,
Blocks: []protocol.BlockInfo{
blocks[0], blocks[2], blocks[0], blocks[0],
blocks[5], blocks[0], blocks[0], blocks[8],
},
}
// Create target file
existingBlocks := []int{0, 2, 0, 0, 5, 0, 0, 8}
existingFile := setUpFile("filex", existingBlocks)
requiredFile := existingFile
requiredFile.Blocks = blocks[1:]
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
// Update index
m.updateLocals("default", []protocol.FileInfo{existingFile})
p := rwFolder{
folder: "default",
dir: "testdata",
model: m,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
m := setUpModel(existingFile)
p := setUpRwFolder(m)
copyChan := make(chan copyBlocksState, 1)
p.handleFile(requiredFile, copyChan, nil)
@@ -108,35 +120,13 @@ func TestHandleFileWithTemp(t *testing.T) {
// Copy: 5, 8
// Pull: 1, 6
// Create existing file
existingFile := protocol.FileInfo{
Name: "file",
Flags: 0,
Modified: 0,
Blocks: []protocol.BlockInfo{
blocks[0], blocks[2], blocks[0], blocks[0],
blocks[5], blocks[0], blocks[0], blocks[8],
},
}
// Create target file
existingBlocks := []int{0, 2, 0, 0, 5, 0, 0, 8}
existingFile := setUpFile("file", existingBlocks)
requiredFile := existingFile
requiredFile.Blocks = blocks[1:]
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
// Update index
m.updateLocals("default", []protocol.FileInfo{existingFile})
p := rwFolder{
folder: "default",
dir: "testdata",
model: m,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
m := setUpModel(existingFile)
p := setUpRwFolder(m)
copyChan := make(chan copyBlocksState, 1)
p.handleFile(requiredFile, copyChan, nil)
@@ -169,47 +159,14 @@ func TestCopierFinder(t *testing.T) {
t.Error(err)
}
// Create existing file
existingFile := protocol.FileInfo{
Name: defTempNamer.TempName("file"),
Flags: 0,
Modified: 0,
Blocks: []protocol.BlockInfo{
blocks[0], blocks[2], blocks[3], blocks[4],
blocks[0], blocks[0], blocks[7], blocks[0],
},
}
// Create target file
existingBlocks := []int{0, 2, 3, 4, 0, 0, 7, 0}
existingFile := setUpFile(defTempNamer.TempName("file"), existingBlocks)
requiredFile := existingFile
requiredFile.Blocks = blocks[1:]
requiredFile.Name = "file2"
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
// Update index
m.updateLocals("default", []protocol.FileInfo{existingFile})
iterFn := func(folder, file string, index int32) bool {
return true
}
// Verify that the blocks we say exist on file, really exist in the db.
for _, idx := range []int{2, 3, 4, 7} {
if m.finder.Iterate(folders, blocks[idx].Hash, iterFn) == false {
t.Error("Didn't find block")
}
}
p := rwFolder{
folder: "default",
dir: "testdata",
model: m,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
m := setUpModel(existingFile)
p := setUpRwFolder(m)
copyChan := make(chan copyBlocksState)
pullChan := make(chan pullBlockState, 4)
finisherChan := make(chan *sharedPullerState, 1)
@@ -262,24 +219,9 @@ func TestCopierCleanup(t *testing.T) {
return true
}
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
// Create a file
file := protocol.FileInfo{
Name: "test",
Flags: 0,
Modified: 0,
Blocks: []protocol.BlockInfo{blocks[0]},
}
// Add file to index
m.updateLocals("default", []protocol.FileInfo{file})
if !m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
t.Error("Expected block not found")
}
file := setUpFile("test", []int{0})
m := setUpModel(file)
file.Blocks = []protocol.BlockInfo{blocks[1]}
file.Version = file.Version.Update(protocol.LocalDeviceID.Short())
@@ -311,19 +253,10 @@ func TestCopierCleanup(t *testing.T) {
// Make sure that the copier routine hashes the content when asked, and pulls
// if it fails to find the block.
func TestLastResortPulling(t *testing.T) {
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
// Add a file to index (with the incorrect block representation, as content
// doesn't actually match the block list)
file := protocol.FileInfo{
Name: "empty",
Flags: 0,
Modified: 0,
Blocks: []protocol.BlockInfo{blocks[0]},
}
m.updateLocals("default", []protocol.FileInfo{file})
file := setUpFile("empty", []int{0})
m := setUpModel(file)
// Pretend that we are handling a new file of the same content but
// with a different name (causing to copy that particular block)
@@ -333,18 +266,7 @@ func TestLastResortPulling(t *testing.T) {
return true
}
// Check that that particular block is there
if !m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
t.Error("Expected block not found")
}
p := rwFolder{
folder: "default",
dir: "testdata",
model: m,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
p := setUpRwFolder(m)
copyChan := make(chan copyBlocksState)
pullChan := make(chan pullBlockState, 1)
@@ -374,15 +296,7 @@ func TestLastResortPulling(t *testing.T) {
}
func TestDeregisterOnFailInCopy(t *testing.T) {
file := protocol.FileInfo{
Name: "filex",
Flags: 0,
Modified: 0,
Blocks: []protocol.BlockInfo{
blocks[0], blocks[2], blocks[0], blocks[0],
blocks[5], blocks[0], blocks[0], blocks[8],
},
}
file := setUpFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
defer os.Remove("testdata/" + defTempNamer.TempName("filex"))
db := db.OpenMemory()
@@ -467,15 +381,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
}
func TestDeregisterOnFailInPull(t *testing.T) {
file := protocol.FileInfo{
Name: "filex",
Flags: 0,
Modified: 0,
Blocks: []protocol.BlockInfo{
blocks[0], blocks[2], blocks[0], blocks[0],
blocks[5], blocks[0], blocks[0], blocks[8],
},
}
file := setUpFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
defer os.Remove("testdata/" + defTempNamer.TempName("filex"))
db := db.OpenMemory()

View File

@@ -88,7 +88,6 @@ func (c *staticClient) Serve() {
l.Debugln(c, "joined", c.conn.RemoteAddr(), "via", c.conn.LocalAddr())
defer c.cleanup()
c.mut.Lock()
c.connected = true
c.mut.Unlock()
@@ -110,10 +109,10 @@ func (c *staticClient) Serve() {
case protocol.Ping:
if err := protocol.WriteMessage(c.conn, protocol.Pong{}); err != nil {
l.Infoln("Relay write:", err)
return
c.disconnect()
} else {
l.Debugln(c, "sent pong")
}
l.Debugln(c, "sent pong")
case protocol.SessionInvitation:
ip := net.IP(msg.Address)
@@ -124,24 +123,38 @@ func (c *staticClient) Serve() {
case protocol.RelayFull:
l.Infoln("Disconnected from relay due to it becoming full.")
return
c.disconnect()
default:
l.Infoln("Relay: protocol error: unexpected message %v", msg)
return
c.disconnect()
}
case <-c.stop:
l.Debugln(c, "stopping")
return
c.disconnect()
// We always exit via this branch of the select, to make sure the
// the reader routine exits.
case err := <-errors:
l.Infoln("Relay received:", err)
close(errors)
close(messages)
c.mut.Lock()
if c.connected {
c.conn.Close()
c.connected = false
l.Infoln("Relay received:", err)
}
if c.closeInvitationsOnFinish {
close(c.invitations)
c.invitations = make(chan protocol.SessionInvitation)
}
c.mut.Unlock()
return
case <-timeout.C:
l.Debugln(c, "timed out")
return
c.disconnect()
}
}
}
@@ -200,9 +213,6 @@ func (c *staticClient) connect() error {
c.mut.Unlock()
conn := tls.Client(tcpConn, c.config)
if err = conn.Handshake(); err != nil {
return err
}
if err := conn.SetDeadline(time.Now().Add(c.connectTimeout)); err != nil {
conn.Close()
@@ -218,13 +228,9 @@ func (c *staticClient) connect() error {
return nil
}
func (c *staticClient) cleanup() {
l.Debugln(c, "cleaning up")
func (c *staticClient) disconnect() {
l.Debugln(c, "disconnecting")
c.mut.Lock()
if c.closeInvitationsOnFinish {
close(c.invitations)
c.invitations = make(chan protocol.SessionInvitation)
}
c.connected = false
c.mut.Unlock()

View File

@@ -26,7 +26,7 @@ const (
eventBroadcasterCheckInterval = 10 * time.Second
)
type Svc struct {
type Service struct {
*suture.Supervisor
cfg *config.Wrapper
tlsCfg *tls.Config
@@ -38,11 +38,11 @@ type Svc struct {
conns chan *tls.Conn
}
func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config) *Svc {
func NewService(cfg *config.Wrapper, tlsCfg *tls.Config) *Service {
conns := make(chan *tls.Conn)
svc := &Svc{
Supervisor: suture.New("Svc", suture.Spec{
service := &Service{
Supervisor: suture.New("Service", suture.Spec{
Log: func(log string) {
l.Debugln(log)
},
@@ -61,28 +61,28 @@ func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config) *Svc {
}
rcfg := cfg.Raw()
svc.CommitConfiguration(rcfg, rcfg)
cfg.Subscribe(svc)
service.CommitConfiguration(rcfg, rcfg)
cfg.Subscribe(service)
receiver := &invitationReceiver{
tlsCfg: tlsCfg,
conns: conns,
invitations: svc.invitations,
invitations: service.invitations,
stop: make(chan struct{}),
}
eventBc := &eventBroadcaster{
svc: svc,
stop: make(chan struct{}),
Service: service,
stop: make(chan struct{}),
}
svc.Add(receiver)
svc.Add(eventBc)
service.Add(receiver)
service.Add(eventBc)
return svc
return service
}
func (s *Svc) VerifyConfiguration(from, to config.Configuration) error {
func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
for _, addr := range to.Options.RelayServers {
_, err := url.Parse(addr)
if err != nil {
@@ -92,7 +92,7 @@ func (s *Svc) VerifyConfiguration(from, to config.Configuration) error {
return nil
}
func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
existing := make(map[string]*url.URL, len(to.Options.RelayServers))
for _, addr := range to.Options.RelayServers {
@@ -112,7 +112,7 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
l.Debugln("Connecting to relay", uri)
c, err := client.NewClient(uri, s.tlsCfg.Certificates, s.invitations, 10*time.Second)
if err != nil {
l.Debugln("Failed to connect to relay", uri, err)
l.Infoln("Failed to connect to relay", uri, err)
continue
}
s.tokens[key] = s.Add(c)
@@ -142,7 +142,7 @@ type Status struct {
}
// Relays return the list of relays that currently have an OK status.
func (s *Svc) Relays() []string {
func (s *Service) Relays() []string {
if s == nil {
// A nil client does not have a status, really. Yet we may be called
// this way, for raisins...
@@ -162,7 +162,7 @@ func (s *Svc) Relays() []string {
}
// RelayStatus returns the latency and OK status for a given relay.
func (s *Svc) RelayStatus(uri string) (time.Duration, bool) {
func (s *Service) RelayStatus(uri string) (time.Duration, bool) {
if s == nil {
// A nil client does not have a status, really. Yet we may be called
// this way, for raisins...
@@ -182,7 +182,7 @@ func (s *Svc) RelayStatus(uri string) (time.Duration, bool) {
}
// Accept returns a new *tls.Conn. The connection is already handshaken.
func (s *Svc) Accept() *tls.Conn {
func (s *Service) Accept() *tls.Conn {
return <-s.conns
}
@@ -234,8 +234,8 @@ func (r *invitationReceiver) Stop() {
// no way to get the event feed directly from the relay lib. This may be
// something to revisit later, possibly.
type eventBroadcaster struct {
svc *Svc
stop chan struct{}
Service *Service
stop chan struct{}
}
func (e *eventBroadcaster) Serve() {
@@ -247,7 +247,7 @@ func (e *eventBroadcaster) Serve() {
for {
select {
case <-timer.C:
curOKRelays := e.svc.Relays()
curOKRelays := e.Service.Relays()
changed := len(curOKRelays) != len(prevOKRelays)
if !changed {

View File

@@ -58,13 +58,9 @@ func (n *IGD) URL() *url.URL {
// An IGDService is a specific service provided by an IGD.
type IGDService struct {
serviceID string
serviceURL string
serviceURN string
}
func (s *IGDService) ID() string {
return s.serviceID
ID string
URL string
URN string
}
type Protocol string
@@ -75,9 +71,9 @@ const (
)
type upnpService struct {
ServiceID string `xml:"serviceId"`
ServiceType string `xml:"serviceType"`
ControlURL string `xml:"controlURL"`
ID string `xml:"serviceId"`
Type string `xml:"serviceType"`
ControlURL string `xml:"controlURL"`
}
type upnpDevice struct {
@@ -132,8 +128,8 @@ nextResult:
if existingResult.uuid == result.uuid {
if shouldDebug() {
l.Debugf("Skipping duplicate result %s with services:", result.uuid)
for _, svc := range result.services {
l.Debugf("* [%s] %s", svc.serviceID, svc.serviceURL)
for _, service := range result.services {
l.Debugf("* [%s] %s", service.ID, service.URL)
}
}
continue nextResult
@@ -143,8 +139,8 @@ nextResult:
results = append(results, result)
if shouldDebug() {
l.Debugf("UPnP discovery result %s with services:", result.uuid)
for _, svc := range result.services {
l.Debugf("* [%s] %s", svc.serviceID, svc.serviceURL)
for _, service := range result.services {
l.Debugf("* [%s] %s", service.ID, service.URL)
}
}
}
@@ -317,9 +313,9 @@ func getChildDevices(d upnpDevice, deviceType string) []upnpDevice {
func getChildServices(d upnpDevice, serviceType string) []upnpService {
var result []upnpService
for _, svc := range d.Services {
if svc.ServiceType == serviceType {
result = append(result, svc)
for _, service := range d.Services {
if service.Type == serviceType {
result = append(result, service)
}
}
return result
@@ -352,7 +348,7 @@ func getServiceDescriptions(rootURL string, device upnpDevice) ([]IGDService, er
return result, nil
}
func getIGDServices(rootURL string, device upnpDevice, wanDeviceURN string, wanConnectionURN string, serviceURNs []string) []IGDService {
func getIGDServices(rootURL string, device upnpDevice, wanDeviceURN string, wanConnectionURN string, URNs []string) []IGDService {
var result []IGDService
devices := getChildDevices(device, wanDeviceURN)
@@ -370,21 +366,21 @@ func getIGDServices(rootURL string, device upnpDevice, wanDeviceURN string, wanC
}
for _, connection := range connections {
for _, serviceURN := range serviceURNs {
services := getChildServices(connection, serviceURN)
for _, URN := range URNs {
services := getChildServices(connection, URN)
l.Debugln(rootURL, "- no services of type", serviceURN, " found on connection.")
l.Debugln(rootURL, "- no services of type", URN, " found on connection.")
for _, service := range services {
if len(service.ControlURL) == 0 {
l.Infoln(rootURL+"- malformed", service.ServiceType, "description: no control URL.")
l.Infoln(rootURL+"- malformed", service.Type, "description: no control URL.")
} else {
u, _ := url.Parse(rootURL)
replaceRawPath(u, service.ControlURL)
l.Debugln(rootURL, "- found", service.ServiceType, "with URL", u)
l.Debugln(rootURL, "- found", service.Type, "with URL", u)
service := IGDService{serviceID: service.ServiceID, serviceURL: u.String(), serviceURN: service.ServiceType}
service := IGDService{ID: service.ID, URL: u.String(), URN: service.Type}
result = append(result, service)
}
@@ -525,15 +521,14 @@ func (s *IGDService) AddPortMapping(localIPAddress string, protocol Protocol, ex
<NewPortMappingDescription>%s</NewPortMappingDescription>
<NewLeaseDuration>%d</NewLeaseDuration>
</u:AddPortMapping>`
body := fmt.Sprintf(tpl, s.serviceURN, externalPort, protocol, internalPort, localIPAddress, description, timeout)
body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, timeout)
response, err := soapRequest(s.serviceURL, s.serviceURN, "AddPortMapping", body)
response, err := soapRequest(s.URL, s.URN, "AddPortMapping", body)
if err != nil && timeout > 0 {
// Try to repair error code 725 - OnlyPermanentLeasesSupported
envelope := &soapErrorResponse{}
err = xml.Unmarshal(response, envelope)
if err != nil {
return err
if unmarshalErr := xml.Unmarshal(response, envelope); unmarshalErr != nil {
return unmarshalErr
}
if envelope.ErrorCode == 725 {
return s.AddPortMapping(localIPAddress, protocol, externalPort, internalPort, description, 0)
@@ -550,9 +545,9 @@ func (s *IGDService) DeletePortMapping(protocol Protocol, externalPort int) erro
<NewExternalPort>%d</NewExternalPort>
<NewProtocol>%s</NewProtocol>
</u:DeletePortMapping>`
body := fmt.Sprintf(tpl, s.serviceURN, externalPort, protocol)
body := fmt.Sprintf(tpl, s.URN, externalPort, protocol)
_, err := soapRequest(s.serviceURL, s.serviceURN, "DeletePortMapping", body)
_, err := soapRequest(s.URL, s.URN, "DeletePortMapping", body)
if err != nil {
return err
@@ -567,9 +562,9 @@ func (s *IGDService) DeletePortMapping(protocol Protocol, externalPort int) erro
func (s *IGDService) GetExternalIPAddress() (net.IP, error) {
tpl := `<u:GetExternalIPAddress xmlns:u="%s" />`
body := fmt.Sprintf(tpl, s.serviceURN)
body := fmt.Sprintf(tpl, s.URN)
response, err := soapRequest(s.serviceURL, s.serviceURN, "GetExternalIPAddress", body)
response, err := soapRequest(s.URL, s.URN, "GetExternalIPAddress", body)
if err != nil {
return nil, err

View File

@@ -85,16 +85,18 @@ func (v Simple) Archive(filePath string) error {
}
// Glob according to the new file~timestamp.ext pattern.
newVersions, err := osutil.Glob(filepath.Join(dir, taggedFilename(file, TimeGlob)))
pattern := filepath.Join(dir, taggedFilename(file, TimeGlob))
newVersions, err := osutil.Glob(pattern)
if err != nil {
l.Warnln("globbing:", err)
l.Warnln("globbing:", err, "for", pattern)
return nil
}
// Also according to the old file.ext~timestamp pattern.
oldVersions, err := osutil.Glob(filepath.Join(dir, file+"~"+TimeGlob))
pattern = filepath.Join(dir, file+"~"+TimeGlob)
oldVersions, err := osutil.Glob(pattern)
if err != nil {
l.Warnln("globbing:", err)
l.Warnln("globbing:", err, "for", pattern)
return nil
}

View File

@@ -258,16 +258,18 @@ func (v Staggered) Archive(filePath string) error {
}
// Glob according to the new file~timestamp.ext pattern.
newVersions, err := osutil.Glob(filepath.Join(dir, taggedFilename(file, TimeGlob)))
pattern := filepath.Join(dir, taggedFilename(file, TimeGlob))
newVersions, err := osutil.Glob(pattern)
if err != nil {
l.Warnln("globbing:", err)
l.Warnln("globbing:", err, "for", pattern)
return nil
}
// Also according to the old file.ext~timestamp pattern.
oldVersions, err := osutil.Glob(filepath.Join(dir, file+"~"+TimeGlob))
pattern = filepath.Join(dir, file+"~"+TimeGlob)
oldVersions, err := osutil.Glob(pattern)
if err != nil {
l.Warnln("globbing:", err)
l.Warnln("globbing:", err, "for", pattern)
return nil
}

View File

@@ -15,6 +15,7 @@ pages=(
syncthing-bep.7
syncthing-localdisco.7
syncthing-globaldisco.7
syncthing-relay.7
)
for page in "${pages[@]}" ; do

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-BEP" "7" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-BEP" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-bep \- Block Exchange Protocol v1
.
@@ -1003,8 +1003,8 @@ directions.
.UNINDENT
.SS Read Only
.sp
In read only mode, a device does not synchronize the local folder to the
cluster, but publishes changes to its local folder contents as usual.
In read only mode, a device does not apply any updates from the cluster,
but publishes changes of its local folder to the cluster as usual.
The local folder can be seen as a "master copy" that is never affected
by the actions of other cluster devices.
.INDENT 0.0

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-CONFIG" "5" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-CONFIG" "5" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-config \- Syncthing Configuration
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-DEVICE-IDS" "7" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-DEVICE-IDS" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-device-ids \- Understanding Device IDs
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-EVENT-API" "7" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-EVENT-API" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-event-api \- Event API
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-FAQ" "7" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-FAQ" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-faq \- Frequently Asked Questions
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-GLOBALDISCO" "7" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-GLOBALDISCO" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-globaldisco \- Global Discovery Protocol v3
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-LOCALDISCO" "7" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-LOCALDISCO" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-localdisco \- Local Discovery Protocol v3
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-NETWORKING" "7" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-NETWORKING" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-networking \- Firewall Setup
.

699
man/syncthing-relay.7 Normal file
View File

@@ -0,0 +1,699 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-RELAY" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-relay \- Relay Protocol v1
.
.nr rst2man-indent-level 0
.
.de1 rstReportMargin
\\$1 \\n[an-margin]
level \\n[rst2man-indent-level]
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
-
\\n[rst2man-indent0]
\\n[rst2man-indent1]
\\n[rst2man-indent2]
..
.de1 INDENT
.\" .rstReportMargin pre:
. RS \\$1
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
. nr rst2man-indent-level +1
.\" .rstReportMargin post:
..
.de UNINDENT
. RE
.\" indent \\n[an-margin]
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
.nr rst2man-indent-level -1
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.SH WHAT IS A RELAY?
.sp
Relay is a service which relays data between two \fIdevices\fP which are not able to
connect to each other directly otherwise. This is usually due to both devices
being behind a NAT and neither side being able to open a port which would
be directly accessible from the internet.
.sp
A relay was designed to relay BEP protocol, hence the reliance on device ID\(aqs
in the protocol spec, but at the same time it is general enough that could be
reused by other protocols or applications, as the data transferred between two
devices which use a relay is completely obscure and does not affect the
relaying.
.SH OPERATION MODES
.sp
Relay listens on a single TCP socket, but has two different connection modes,
where a connection mode is a predefined set of messages which the relay and
the device are expecting to exchange.
.sp
The first mode is the \fIprotocol\fP mode which allows a client to interact
with the relay, for example join the relay, or request to connect to a device,
given it is available on the relay. Similarly to BEP, protocol mode requires
the device to connect via TLS using a strong suite of ciphers (same as BEP),
which allows the relay to verify and derive the identity (Device ID) of the
device.
.sp
The second mode is the \fIsession\fP mode which after a few initial messages
connects two devices directly to each other via the relay, and is a plain\-text
protocol, which for every byte written by one device, sends the same set of
bytes to the other device and vica versa.
.SH IDENTIFYING THE CONNECTION MODE
.sp
Because both connection modes operate over the same single socket, a method
of detecting the connection mode is required.
.sp
When a new client connects to the relay, the relay checks the first byte
that the client has sent, and if that matches 0x16, that implies to us that
the connection is a protocol mode connection, due to 0x16 being the first byte
in the TLS handshake, and only protocol mode connections use TLS.
.sp
If the first byte is not 0x16, then we assume that the connection is a session
mode connection.
.SH PROTOCOL MODE
.sp
Protocol mode uses TLS and protocol name defined by the TLS header should be
\fIbep\-relay\fP\&.
.sp
Protocol mode has two submodes:
1. Permanent protocol submode \- Joining the relay, and waiting for messages from
the relay asking to connect to some device which is interested in having a
session with you.
2. Temporary protocol submode \- Only used to request a session with a device
which is connected to the relay using the permanent protocol submode.
.SS Permanent protocol submode
.sp
A permanent protocol submode begins with the client sending a JoinRelayRequest
message, which the relay responds to with either a ResponseSuccess or
ResponseAlreadyConnected message if a client with the same device ID already
exists.
.sp
After the client has joined, no more messages are exchanged apart from
Ping/Pong messages for general connection keep alive checking.
.sp
From this point onwards, the client stand\-by\(aqs and waits for SessionInvitation
messages from the relay, which implies that some other device is trying to
connect with you. SessionInvitation message contains the unique session key
which then can be used to establish a connection in session mode.
.sp
If the client fails to send a JoinRelayRequest message within the first ping
interval, the connection is terminated.
If the client fails to send a message (even if its a ping message) every minute
(by default), the connection is terminated.
.SS Temporary protocol submode
.sp
A temporary protocol submode begins with ConnectRequest message, to which the
relay responds with either ResponseNotFound if the device the client it is after
is not available, or with a SessionInvitation, which contains the unique session
key which then can be used to establish a connection in session mode.
.sp
The connection is terminated immediately after that.
.SS Example Exchange
.sp
Client A \- Permanent protocol submode
Client B \- Temporary protocol submode
.TS
center;
|l|l|l|l|.
_
T{
#
T} T{
Client (A)
T} T{
Relay
T} T{
Client (B)
T}
_
T{
1
T} T{
JoinRelayRequest\->
T} T{
T} T{
T}
_
T{
2
T} T{
T} T{
<\-ResponseSuccess
T} T{
T}
_
T{
3
T} T{
Ping\->
T} T{
T} T{
T}
_
T{
4
T} T{
T} T{
<\-Pong
T} T{
T}
_
T{
5
T} T{
T} T{
T} T{
<\-ConnectRequest(A)
T}
_
T{
6
T} T{
T} T{
SessionInvitation(A)\->
T} T{
T}
_
T{
7
T} T{
T} T{
<\-SessionInvitation(B)
T} T{
T}
_
T{
8
T} T{
T} T{
T} T{
(Disconnects)
T}
_
T{
9
T} T{
Ping\->
T} T{
T} T{
T}
_
T{
10
T} T{
T} T{
<\-Pong
T} T{
T}
_
T{
11
T} T{
Ping\->
T} T{
T} T{
T}
_
T{
12
T} T{
T} T{
<\-Pong
T} T{
T}
_
.TE
.SH SESSION MODE
.sp
The first and only message the client sends in the session mode is the
SessionInvitation message which contains the session key identifying which
session you are trying to join. The relay responds with one of the following
Response messages:
.INDENT 0.0
.IP 1. 3
ResponseNotFound \- Session key is invalid
.IP 2. 3
ResponseAlreadyConnected \- Session is full (both sides already connected)
.IP 3. 3
ResponseSuccess \- You have successfully joined the session
.UNINDENT
.sp
After the successful response, all the bytes written and received will be
relayed between the two devices in the session directly.
.SS Example Exchange
.sp
Client A \- Permanent protocol mode
Client B \- Temporary protocol mode
.TS
center;
|l|l|l|l|.
_
T{
#
T} T{
Client (A)
T} T{
Relay
T} T{
Client (B)
T}
_
T{
1
T} T{
SessionInvitation(A)\->
T} T{
T} T{
T}
_
T{
2
T} T{
T} T{
<\-ResponseSuccess
T} T{
T}
_
T{
3
T} T{
Data\->
T} T{
(Buffers data)
T} T{
T}
_
T{
4
T} T{
Data\->
T} T{
(Buffers data)
T} T{
T}
_
T{
5
T} T{
T} T{
T} T{
<\-SessionInvitation(B)
T}
_
T{
6
T} T{
T} T{
ResponseSuccess\->
T} T{
T}
_
T{
7
T} T{
T} T{
Relays data \->
T} T{
T}
_
T{
8
T} T{
T} T{
Relays data \->
T} T{
T}
_
T{
9
T} T{
T} T{
<\-Relays data
T} T{
<\-Data
T}
_
.TE
.SH MESSAGES
.sp
All messages are preceeded by a header message. Header message contains the
magic value 0x9E79BC40, message type integer, and message length.
.sp
\fBWARNING:\fP
.INDENT 0.0
.INDENT 3.5
Some messages have no content, apart from the implied header which allows
us to identify what type of message it is.
.UNINDENT
.UNINDENT
.SS Header structure
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Magic |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Message Type |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Message Length |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
struct Header {
unsigned int Magic;
int MessageType;
int MessageLength;
}
.ft P
.fi
.UNINDENT
.UNINDENT
.SS Ping message (Type = 0)
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
struct Ping {
}
.ft P
.fi
.UNINDENT
.UNINDENT
.SS Pong message (Type = 1)
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
struct Pong {
}
.ft P
.fi
.UNINDENT
.UNINDENT
.SS JoinRelayRequest message (Type = 2)
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
struct JoinRelayRequest {
}
.ft P
.fi
.UNINDENT
.UNINDENT
.SS JoinSessionRequest message (Type = 3)
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Length of Key |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
/ /
\e Key (variable length) \e
/ /
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
struct JoinSessionRequest {
opaque Key<32>;
}
.ft P
.fi
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B : Key
This is a unique random session key generated by the relay server. It is
used to identify which session you are trying to connect to.
.UNINDENT
.SS Response message (Type = 4)
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Code |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Length of Message |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
/ /
\e Message (variable length) \e
/ /
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
struct Response {
int Code;
string Message<>;
}
.ft P
.fi
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B : Code
An integer representing the status code.
.TP
.B : Message
Message associated with the code.
.UNINDENT
.SS ConnectRequest message (Type = 5)
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Length of ID |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
/ /
\e ID (variable length) \e
/ /
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
struct ConnectRequest {
opaque ID<32>;
}
.ft P
.fi
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B : ID
Device ID to which the client would like to connect.
.UNINDENT
.SS SessionInvitation message (Type = 6)
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Length of From |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
/ /
\e From (variable length) \e
/ /
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Length of Key |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
/ /
\e Key (variable length) \e
/ /
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Length of Address |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
/ /
\e Address (variable length) \e
/ /
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| 0x0000 | Port |
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Server Socket (V=0 or 1) |V|
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
struct SessionInvitation {
opaque From<32>;
opaque Key<32>;
opaque Address<32>;
unsigned int Port;
bool ServerSocket;
}
.ft P
.fi
.UNINDENT
.UNINDENT
.INDENT 0.0
.TP
.B : From
Device ID identifying who you will be connecting with.
.TP
.B : Key
A unique random session key generated by the relay server. It is used to
identify which session you are trying to connect to.
.TP
.B : Address
An optional IP address on which the relay server is expecting you to
connect, in order to start a connection in session mode.
Empty/all zero IP should be replaced with the relay\(aqs public IP address that
was used when establishing the protocol mode connection.
.TP
.B : Port
An optional port on which the relay server is expecting you to connect,
in order to start a connection in session mode.
.TP
.B : Server Socket
Because both sides connecting to the relay use the client side of the socket,
and some protocols behave differently depending if the connection starts on
the server side or the client side, this boolean indicates which side of the
connection this client should assume it\(aqs getting. The value is inverted in
the invitation which is sent to the other device, so that there is always
one client socket, and one server socket.
.UNINDENT
.SH HOW SYNCTHING USES RELAYS, AND GENERAL SECURITY
.sp
In the case of Syncthing and BEP, when two devices connect via relay, they
start their standard TLS connection encapsulated within the relay\(aqs plain\-text
session connection, effectively upgrading the plain\-text connection to a TLS
connection.
.sp
Even though the relay could be used for man\-in\-the\-middle attack, using TLS
at the application/BEP level ensures that all the traffic is safely encrypted,
and is completely meaningless to the relay. Furthermore, the secure suite of
ciphers used by BEP provides forward secrecy, meaning that even if the relay
did capture all the traffic, and even if the attacker did get their hands on the
device keys, they would still not be able to recover/decrypt any traffic which
was transported via the relay.
.sp
After establishing a relay session, syncthing looks at the SessionInvitation
message, and depending which side it has received, wraps the raw socket in
either a TLS client socket or a TLS server socket depending on the ServerSocket
boolean value in the SessionInvitation, and starts the TLS handshake.
.sp
From that point onwards it functions exactly the same way as if syncthing was
establishing a direct connection with the other device over the internet,
performing device ID validation, and full TLS encryption, and provides the same
security properties as it would provide when connecting over the internet.
.SH EXAMPLES OF STRONG CIPHER SUITES
.TS
center;
|l|l|l|.
_
T{
ID
T} T{
Name
T} T{
Description
T}
_
T{
0x009F
T} T{
DHE\-RSA\-AES256\-GCM\-SHA384
T} T{
TLSv1.2 DH RSA AESGCM(256) AEAD
T}
_
T{
0x006B
T} T{
DHE\-RSA\-AES256\-SHA256
T} T{
TLSv1.2 DH RSA AES(256) SHA256
T}
_
T{
0xC030
T} T{
ECDHE\-RSA\-AES256\-GCM\-SHA384
T} T{
TLSv1.2 ECDH RSA AESGCM(256) AEAD
T}
_
T{
0xC028
T} T{
ECDHE\-RSA\-AES256\-SHA384
T} T{
TLSv1.2 ECDH RSA AES(256) SHA384
T}
_
T{
0x009E
T} T{
DHE\-RSA\-AES128\-GCM\-SHA256
T} T{
TLSv1.2 DH RSA AESGCM(128) AEAD
T}
_
T{
0x0067
T} T{
DHE\-RSA\-AES128\-SHA256
T} T{
TLSv1.2 DH RSA AES(128) SHA256
T}
_
T{
0xC02F
T} T{
ECDHE\-RSA\-AES128\-GCM\-SHA256
T} T{
TLSv1.2 ECDH RSA AESGCM(128) AEAD
T}
_
T{
0xC027
T} T{
ECDHE\-RSA\-AES128\-SHA256
T} T{
TLSv1.2 ECDH RSA AES(128) SHA256
T}
_
.TE
.SH AUTHOR
The Syncthing Authors
.SH COPYRIGHT
2015, The Syncthing Authors
.\" Generated by docutils manpage writer.
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-REST-API" "7" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-REST-API" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-rest-api \- REST API
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-SECURITY" "7" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-SECURITY" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-security \- Security Principles
.
@@ -115,7 +115,7 @@ certificates are sent in clear text (as in HTTPS etc), meaning that the
certificate Common Name (by default \fBsyncthing\fP) is visible.
.sp
An eavesdropper can deduce that this is a Syncthing connection and calculate the
device ID:s involved based on the hashes of the sent certificates.
device IDs involved based on the hashes of the sent certificates.
.sp
Likewise, if the sync port (default 22000) is accessible from the internet, a
port scanner may discover it, attempt a TLS negotiation and thus obtain the
@@ -129,7 +129,7 @@ web GUI defaults to being reachable from the \fBlocal host only\fP\&.
.sp
Parties doing surveillance on your network (whether that be corporate IT, the
NSA or someone else) will be able to see that you use Syncthing, and your device
ID\(aqs \fI\%are OK to share anyway\fP <\fBhttp://docs.syncthing.net/users/faq.html#should-i-keep-my-device-ids-secret\fP>,
IDs \fI\%are OK to share anyway\fP <\fBhttp://docs.syncthing.net/users/faq.html#should-i-keep-my-device-ids-secret\fP>,
but the actual transmitted data is protected as well as we can. Knowing your
device ID can expose your IP address, using global discovery.
.SH PROTECTING YOUR SYNCTHING KEYS AND IDENTITY
@@ -141,8 +141,6 @@ synced files. Here are some general principles to protect your files:
.IP 1. 3
If a device of yours is lost, make sure to revoke its access from your other
devices.
.UNINDENT
.INDENT 0.0
.IP 2. 3
If you\(aqre syncing confidential data on an encrypted disk to guard against
device theft, put the Syncthing config folder on the same encrypted disk to

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-STIGNORE" "5" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-STIGNORE" "5" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing-stignore \- Prevent files from being synchronized to other nodes
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "TODO" "7" "December 01, 2015" "v0.12" "Syncthing"
.TH "TODO" "7" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
Todo \- Keep automatic backups of deleted files by other nodes
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING" "1" "December 01, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING" "1" "December 21, 2015" "v0.12" "Syncthing"
.SH NAME
syncthing \- Syncthing
.