Compare commits
319
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e15be5c2bf | ||
|
|
cc436dc8cb | ||
|
|
76359da58e | ||
|
|
368cd44558 | ||
|
|
cc1387ec0c | ||
|
|
7c79985a29 | ||
|
|
2b56961b54 | ||
|
|
ff9920cbdc | ||
|
|
953a67bc3a | ||
|
|
29343aec3a | ||
|
|
2972472179 | ||
|
|
240e7b0835 | ||
|
|
baf5191433 | ||
|
|
2645e87766 | ||
|
|
ec8bc02d33 | ||
|
|
054bc970e2 | ||
|
|
c1c41242bb | ||
|
|
169ff73d26 | ||
|
|
d985ed553a | ||
|
|
dea1ef24d9 | ||
|
|
49f29a0453 | ||
|
|
76af9ba53d | ||
|
|
2de364414f | ||
|
|
5ae84970e7 | ||
|
|
141b0d38a6 | ||
|
|
44891b6924 | ||
|
|
f008588307 | ||
|
|
e481d03b5e | ||
|
|
a8a73b60c4 | ||
|
|
7e8b76e8ea | ||
|
|
36c746bd9f | ||
|
|
7476c583e7 | ||
|
|
89928ca8e4 | ||
|
|
38a3bf3ada | ||
|
|
96b3d31b42 | ||
|
|
362ae5c4bb | ||
|
|
dc303c2a71 | ||
|
|
be2ca0ea22 | ||
|
|
460cb19839 | ||
|
|
ddfebb17cf | ||
|
|
375c9dd116 | ||
|
|
15716a0772 | ||
|
|
6f6c1cd330 | ||
|
|
36ac757c3a | ||
|
|
b614cfffcb | ||
|
|
b58a52c7b4 | ||
|
|
a80fc1b062 | ||
|
|
90d18189da | ||
|
|
22a2e95126 | ||
|
|
11e1a99e14 | ||
|
|
3c6bfb880d | ||
|
|
ad2c05c3f5 | ||
|
|
fa75f54a05 | ||
|
|
6405fd4770 | ||
|
|
f42f20c70c | ||
|
|
209bc59e8c | ||
|
|
84ee86f6b3 | ||
|
|
ed7791d824 | ||
|
|
0580b10385 | ||
|
|
47c7351b16 | ||
|
|
b158072a15 | ||
|
|
b6486c26e6 | ||
|
|
da0ac96704 | ||
|
|
af1fbda892 | ||
|
|
2234c45c19 | ||
|
|
fd38fb684a | ||
|
|
e0a16e08dd | ||
|
|
43189dfe3a | ||
|
|
46d4f6037d | ||
|
|
e522811a52 | ||
|
|
6124bbb12a | ||
|
|
fbf911cf7e | ||
|
|
7fdfa81fb8 | ||
|
|
a4673f3007 | ||
|
|
24c499d282 | ||
|
|
4581c57478 | ||
|
|
f177924629 | ||
|
|
633e888ba7 | ||
|
|
4316992d95 | ||
|
|
5ecb8bdd8a | ||
|
|
3b81d4b8a5 | ||
|
|
6e3b3dc4e7 | ||
|
|
8d421a62d2 | ||
|
|
185b0690c8 | ||
|
|
bd0e97023e | ||
|
|
34ff0706a3 | ||
|
|
acba61babb | ||
|
|
f91191218b | ||
|
|
05c79ac8c2 | ||
|
|
24d2a93c0d | ||
|
|
de43080228 | ||
|
|
b0cd7be39b | ||
|
|
a7169a6348 | ||
|
|
6e126fb97e | ||
|
|
22783d8f6c | ||
|
|
1d710bdcd9 | ||
|
|
5feffba1ff | ||
|
|
6c56586a6b | ||
|
|
a32b66c7c3 | ||
|
|
7e3c06191e | ||
|
|
372c96c6b2 | ||
|
|
7073b8721a | ||
|
|
fccb9c0bf4 | ||
|
|
3d09090c4e | ||
|
|
596a49c112 | ||
|
|
95fc253d6b | ||
|
|
e6d5372029 | ||
|
|
8e5c692244 | ||
|
|
e694c664e5 | ||
|
|
8779f93746 | ||
|
|
1f0f5c1e23 | ||
|
|
f9f12131ae | ||
|
|
7e0106da0c | ||
|
|
aaf6bf3cd2 | ||
|
|
fa95c82daf | ||
|
|
0da7142a59 | ||
|
|
446a938b06 | ||
|
|
21c3994650 | ||
|
|
f90f3a5ca6 | ||
|
|
a34d2b72c0 | ||
|
|
9d617dcfec | ||
|
|
55506a0fc3 | ||
|
|
19c03f504f | ||
|
|
985a3436e2 | ||
|
|
9dae87c80c | ||
|
|
46364a38c6 | ||
|
|
148b2b9d02 | ||
|
|
64354b51c9 | ||
|
|
d180bc794b | ||
|
|
eab5fd5bdd | ||
|
|
e3ca797dad | ||
|
|
f2db1c8ab2 | ||
|
|
36b8a75ede | ||
|
|
11b2815b88 | ||
|
|
0a42b85c06 | ||
|
|
baf231e3b6 | ||
|
|
e26e85b6d6 | ||
|
|
b2193b23e5 | ||
|
|
2af3a92833 | ||
|
|
d2af6dcf38 | ||
|
|
dd6b66167e | ||
|
|
51a4a88a81 | ||
|
|
516efb21cf | ||
|
|
2d4397af53 | ||
|
|
06f319a380 | ||
|
|
37ed5a01e0 | ||
|
|
4a9997e449 | ||
|
|
54269553c8 | ||
|
|
541d05df1b | ||
|
|
3d6ea23511 | ||
|
|
c0554c9fbf | ||
|
|
61130ea191 | ||
|
|
2581e56503 | ||
|
|
fea0ae7f2f | ||
|
|
3299438cbd | ||
|
|
5c160048df | ||
|
|
e3e1036dda | ||
|
|
876d7ac85e | ||
|
|
70b37dc469 | ||
|
|
6393f69138 | ||
|
|
0ca39f6c65 | ||
|
|
02493251d5 | ||
|
|
ff04648112 | ||
|
|
55002d7adf | ||
|
|
0664c6b5b0 | ||
|
|
a4ebac147b | ||
|
|
cf802dc67e | ||
|
|
84365882de | ||
|
|
37fb6473b3 | ||
|
|
ba676f2810 | ||
|
|
b3d7c622c3 | ||
|
|
bc016e360e | ||
|
|
594918dd3c | ||
|
|
ec5feb41e8 | ||
|
|
94c52e3a77 | ||
|
|
875de4f637 | ||
|
|
72fa5a69f1 | ||
|
|
d63e54237b | ||
|
|
e256d93b43 | ||
|
|
4d12df5424 | ||
|
|
cae120fd4d | ||
|
|
be332a6223 | ||
|
|
bda0bb6f13 | ||
|
|
68c5dcd83d | ||
|
|
9bdcadf634 | ||
|
|
037be433f8 | ||
|
|
2bed62dd9e | ||
|
|
a27bc4ebea | ||
|
|
d6e34761dc | ||
|
|
98effcd8e3 | ||
|
|
baa87bc823 | ||
|
|
944d9c84a0 | ||
|
|
1e447741ee | ||
|
|
4405ac7386 | ||
|
|
e1190f0f0f | ||
|
|
a7f2416c0c | ||
|
|
40d0100132 | ||
|
|
37f7c48cfc | ||
|
|
21adf752c8 | ||
|
|
aec143b882 | ||
|
|
f691040936 | ||
|
|
42acf0ed60 | ||
|
|
ca4a3589e5 | ||
|
|
2eead17224 | ||
|
|
626b26a227 | ||
|
|
c46db0761e | ||
|
|
cfed06697d | ||
|
|
a0d9183b14 | ||
|
|
d3eb674b30 | ||
|
|
7fe1fdd8c7 | ||
|
|
37cbe68204 | ||
|
|
f76a66fc55 | ||
|
|
d7949aa58e | ||
|
|
f0c0c5483f | ||
|
|
7d444021bb | ||
|
|
388a29bbe2 | ||
|
|
a03dd1bd41 | ||
|
|
4b366f2857 | ||
|
|
c87faace6b | ||
|
|
1e8b185377 | ||
|
|
031804827f | ||
|
|
6cccd9b6fc | ||
|
|
687fbb0a7e | ||
|
|
8f2db99c86 | ||
|
|
2c0f8dc546 | ||
|
|
a388fb0bb7 | ||
|
|
27465353c1 | ||
|
|
c2ccab4361 | ||
|
|
bb876eac82 | ||
|
|
34c04babbe | ||
|
|
7c6a310179 | ||
|
|
6a36ec63d7 | ||
|
|
9c8b907ff1 | ||
|
|
f769df16e8 | ||
|
|
c6f5075721 | ||
|
|
a693698279 | ||
|
|
4949e3ba41 | ||
|
|
ebcdea63c0 | ||
|
|
eb29989dff | ||
|
|
78ef42daa1 | ||
|
|
22e24fc387 | ||
|
|
516d88b072 | ||
|
|
77457e91e9 | ||
|
|
049d92b525 | ||
|
|
f9bd59f031 | ||
|
|
6d0d5bd566 | ||
|
|
35d20a19bc | ||
|
|
dab1c4cfc9 | ||
|
|
f86946c6df | ||
|
|
e97f75cad5 | ||
|
|
2505f82ce5 | ||
|
|
7996ef0d45 | ||
|
|
b05c1a5bb9 | ||
|
|
9dd6f848bd | ||
|
|
e1959afb6b | ||
|
|
c68c78d412 | ||
|
|
b72d31f87f | ||
|
|
95e15c95f2 | ||
|
|
9f871a3726 | ||
|
|
e19e2c123e | ||
|
|
cbe44e1fff | ||
|
|
8e191c8e6b | ||
|
|
19d742b9e4 | ||
|
|
e7db264803 | ||
|
|
3d8a71fdb2 | ||
|
|
6277c0595c | ||
|
|
aa9eda1979 | ||
|
|
1d76efcbcd | ||
|
|
34c2c1ec16 | ||
|
|
bf7fea9a0a | ||
|
|
fdf15f3ca3 | ||
|
|
1cb5875b20 | ||
|
|
1a59a5478f | ||
|
|
5750443371 | ||
|
|
f9132cae85 | ||
|
|
17149741a7 | ||
|
|
d2ec40bb67 | ||
|
|
1a4398cc55 | ||
|
|
529d91fb9d | ||
|
|
108b4e2e10 | ||
|
|
cd0cce4195 | ||
|
|
9f1a72ec88 | ||
|
|
a10c621e33 | ||
|
|
2e2d479103 | ||
|
|
35f0e355bf | ||
|
|
4395711d26 | ||
|
|
aba915037f | ||
|
|
442e93d3fc | ||
|
|
3450b5f80c | ||
|
|
f76b5d8002 | ||
|
|
7e54868206 | ||
|
|
d84a8e6404 | ||
|
|
4833b6085c | ||
|
|
2ceaca8828 | ||
|
|
d02158c0ef | ||
|
|
6213c4f2cd | ||
|
|
cd34eea017 | ||
|
|
7a0a702ec0 | ||
|
|
d9ed8e125e | ||
|
|
36708a5067 | ||
|
|
8c32955da1 | ||
|
|
c111ed4b20 | ||
|
|
a3ea9427d1 | ||
|
|
09b534b8a3 | ||
|
|
190c61ba2f | ||
|
|
dc71ec734d | ||
|
|
3af96e50bd | ||
|
|
ddc56c8a0d | ||
|
|
e0da2764c9 | ||
|
|
ad29093ac1 | ||
|
|
28610a9a42 | ||
|
|
65eb528e2d | ||
|
|
ec9d68960f | ||
|
|
c618eba9a9 | ||
|
|
1ef8378a30 | ||
|
|
43289103cb | ||
|
|
1bc5632771 | ||
|
|
4b488a2d28 | ||
|
|
f28367bcfc |
+4
-4
@@ -1,4 +1,6 @@
|
|||||||
./syncthing
|
syncthing
|
||||||
|
!gui/syncthing
|
||||||
|
!Godeps/_workspace/src/github.com/syncthing
|
||||||
syncthing.exe
|
syncthing.exe
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.zip
|
*.zip
|
||||||
@@ -9,8 +11,6 @@ files/pidx
|
|||||||
bin
|
bin
|
||||||
perfstats*.csv
|
perfstats*.csv
|
||||||
coverage.xml
|
coverage.xml
|
||||||
!gui/scripts/syncthing
|
syncthing.sig
|
||||||
syncthing.md5
|
|
||||||
syncthing.exe.md5
|
|
||||||
RELEASE
|
RELEASE
|
||||||
deb
|
deb
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# This is the official list of Syncthing authors for copyright purposes.
|
# This is the official list of Syncthing authors for copyright purposes.
|
||||||
|
|
||||||
Aaron Bieber <qbit@deftly.net>
|
Aaron Bieber <qbit@deftly.net>
|
||||||
|
Adam Piggott <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
|
||||||
Alexander Graf <register-github@alex-graf.de>
|
Alexander Graf <register-github@alex-graf.de>
|
||||||
Andrew Dunham <andrew@du.nham.ca>
|
Andrew Dunham <andrew@du.nham.ca>
|
||||||
Antony Male <antony.male@gmail.com>
|
Antony Male <antony.male@gmail.com>
|
||||||
@@ -47,6 +48,8 @@ Lord Landon Agahnim <lordlandon@gmail.com>
|
|||||||
Marc Laporte <marc@marclaporte.com> <marc@laporte.name>
|
Marc Laporte <marc@marclaporte.com> <marc@laporte.name>
|
||||||
Marc Pujol <kilburn@la3.org>
|
Marc Pujol <kilburn@la3.org>
|
||||||
Marcin Dziadus <dziadus.marcin@gmail.com>
|
Marcin Dziadus <dziadus.marcin@gmail.com>
|
||||||
|
Mateon1 <matin1111@wp.pl>
|
||||||
|
Matt Burke <mburke@amplify.com>
|
||||||
Michael Jephcote <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
Michael Jephcote <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
||||||
Michael Tilli <pyfisch@gmail.com>
|
Michael Tilli <pyfisch@gmail.com>
|
||||||
Pascal Jungblut <github@pascalj.com> <mail@pascal-jungblut.com>
|
Pascal Jungblut <github@pascalj.com> <mail@pascal-jungblut.com>
|
||||||
@@ -56,10 +59,11 @@ Phill Luby <phill.luby@newredo.com>
|
|||||||
Piotr Bejda <piotrb10@gmail.com>
|
Piotr Bejda <piotrb10@gmail.com>
|
||||||
Ryan Sullivan <kayoticsully@gmail.com>
|
Ryan Sullivan <kayoticsully@gmail.com>
|
||||||
Sergey Mishin <ralder@yandex.ru>
|
Sergey Mishin <ralder@yandex.ru>
|
||||||
Stefan Tatschner <stefan@sevenbyte.org>
|
Stefan Tatschner <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
|
||||||
Tim Abell <tim@timwise.co.uk>
|
Tim Abell <tim@timwise.co.uk>
|
||||||
Tobias Nygren <tnn@nygren.pp.se>
|
Tobias Nygren <tnn@nygren.pp.se>
|
||||||
Tomas Cerveny <kozec@kozec.com>
|
Tomas Cerveny <kozec@kozec.com>
|
||||||
Tully Robinson <tully@tojr.org>
|
Tully Robinson <tully@tojr.org>
|
||||||
Veeti Paananen <veeti.paananen@rojekti.fi>
|
Veeti Paananen <veeti.paananen@rojekti.fi>
|
||||||
Vil Brekin <vilbrekin@gmail.com>
|
Vil Brekin <vilbrekin@gmail.com>
|
||||||
|
Yannic A. <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>
|
||||||
|
|||||||
Generated
+9
-16
@@ -1,22 +1,18 @@
|
|||||||
{
|
{
|
||||||
"ImportPath": "github.com/syncthing/syncthing",
|
"ImportPath": "github.com/syncthing/syncthing",
|
||||||
"GoVersion": "go1.5",
|
"GoVersion": "go1.5.1",
|
||||||
"Packages": [
|
"Packages": [
|
||||||
"./cmd/..."
|
"./cmd/..."
|
||||||
],
|
],
|
||||||
"Deps": [
|
"Deps": [
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/bkaradzic/go-lz4",
|
"ImportPath": "github.com/bkaradzic/go-lz4",
|
||||||
"Rev": "d47913b1412890a261b9fefae99d72d2bf5aebd8"
|
"Rev": "74ddf82598bc4745b965729e9c6a463bedd33049"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/calmh/du",
|
"ImportPath": "github.com/calmh/du",
|
||||||
"Rev": "3c0690cca16228b97741327b1b6781397afbdb24"
|
"Rev": "3c0690cca16228b97741327b1b6781397afbdb24"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"ImportPath": "github.com/calmh/logger",
|
|
||||||
"Rev": "c96f6a1a8c7b6bf2f4860c667867d90174799eb2"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/calmh/luhn",
|
"ImportPath": "github.com/calmh/luhn",
|
||||||
"Rev": "0c8388ff95fa92d4094011e5a04fc99dea3d1632"
|
"Rev": "0c8388ff95fa92d4094011e5a04fc99dea3d1632"
|
||||||
@@ -37,17 +33,14 @@
|
|||||||
"ImportPath": "github.com/kardianos/osext",
|
"ImportPath": "github.com/kardianos/osext",
|
||||||
"Rev": "6e7f843663477789fac7c02def0d0909e969b4e5"
|
"Rev": "6e7f843663477789fac7c02def0d0909e969b4e5"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"ImportPath": "github.com/syncthing/protocol",
|
|
||||||
"Rev": "388a29bbe21d8772ee4c29f4520aa8040309607d"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||||
"Rev": "b743d92d3215f11c9b5ce8830fafe1f16786adf4"
|
"Rev": "1a9d62f03ea92815b46fcaab357cfd4df264b1a0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/thejerf/suture",
|
"ImportPath": "github.com/thejerf/suture",
|
||||||
"Rev": "fc7aaeabdc43fe41c5328efa1479ffea0b820978"
|
"Comment": "v1.0.1",
|
||||||
|
"Rev": "99c1f2d613756768fc4299acd9dc621e11ed3fd7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/vitrun/qart/coding",
|
"ImportPath": "github.com/vitrun/qart/coding",
|
||||||
@@ -63,19 +56,19 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "golang.org/x/crypto/bcrypt",
|
"ImportPath": "golang.org/x/crypto/bcrypt",
|
||||||
"Rev": "c16968172724c0b5e8bdc6ad33f5a79443a44cd7"
|
"Rev": "81bf7719a6b7ce9b665598222362b50122dfc13b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "golang.org/x/crypto/blowfish",
|
"ImportPath": "golang.org/x/crypto/blowfish",
|
||||||
"Rev": "c16968172724c0b5e8bdc6ad33f5a79443a44cd7"
|
"Rev": "81bf7719a6b7ce9b665598222362b50122dfc13b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "golang.org/x/net/internal/iana",
|
"ImportPath": "golang.org/x/net/internal/iana",
|
||||||
"Rev": "66f0418ca41253f8d1a024eb9754e9441a8e79b9"
|
"Rev": "db8e4de5b2d6653f66aea53094624468caad15d2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "golang.org/x/net/ipv6",
|
"ImportPath": "golang.org/x/net/ipv6",
|
||||||
"Rev": "66f0418ca41253f8d1a024eb9754e9441a8e79b9"
|
"Rev": "db8e4de5b2d6653f66aea53094624468caad15d2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "golang.org/x/text/transform",
|
"ImportPath": "golang.org/x/text/transform",
|
||||||
|
|||||||
+1
@@ -5,4 +5,5 @@ go:
|
|||||||
- 1.2
|
- 1.2
|
||||||
- 1.3
|
- 1.3
|
||||||
- 1.4
|
- 1.4
|
||||||
|
- 1.5
|
||||||
- tip
|
- tip
|
||||||
|
|||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
logger
|
|
||||||
======
|
|
||||||
|
|
||||||
A small wrapper around `log` to provide log levels.
|
|
||||||
|
|
||||||
Documentation
|
|
||||||
-------------
|
|
||||||
|
|
||||||
http://godoc.org/github.com/calmh/logger
|
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
||||||
-187
@@ -1,187 +0,0 @@
|
|||||||
// Copyright (C) 2014 Jakob Borg. All rights reserved. Use of this source code
|
|
||||||
// is governed by an MIT-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package logger implements a standardized logger with callback functionality
|
|
||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LogLevel int
|
|
||||||
|
|
||||||
const (
|
|
||||||
LevelDebug LogLevel = iota
|
|
||||||
LevelVerbose
|
|
||||||
LevelInfo
|
|
||||||
LevelOK
|
|
||||||
LevelWarn
|
|
||||||
LevelFatal
|
|
||||||
NumLevels
|
|
||||||
)
|
|
||||||
|
|
||||||
// A MessageHandler is called with the log level and message text.
|
|
||||||
type MessageHandler func(l LogLevel, msg string)
|
|
||||||
|
|
||||||
type Logger struct {
|
|
||||||
logger *log.Logger
|
|
||||||
handlers [NumLevels][]MessageHandler
|
|
||||||
mut sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// The default logger logs to standard output with a time prefix.
|
|
||||||
var DefaultLogger = New()
|
|
||||||
|
|
||||||
func New() *Logger {
|
|
||||||
if os.Getenv("LOGGER_DISCARD") != "" {
|
|
||||||
// Hack to completely disable logging, for example when running benchmarks.
|
|
||||||
return &Logger{
|
|
||||||
logger: log.New(ioutil.Discard, "", 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Logger{
|
|
||||||
logger: log.New(os.Stdout, "", log.Ltime),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddHandler registers a new MessageHandler to receive messages with the
|
|
||||||
// specified log level or above.
|
|
||||||
func (l *Logger) AddHandler(level LogLevel, h MessageHandler) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
l.handlers[level] = append(l.handlers[level], h)
|
|
||||||
}
|
|
||||||
|
|
||||||
// See log.SetFlags
|
|
||||||
func (l *Logger) SetFlags(flag int) {
|
|
||||||
l.logger.SetFlags(flag)
|
|
||||||
}
|
|
||||||
|
|
||||||
// See log.SetPrefix
|
|
||||||
func (l *Logger) SetPrefix(prefix string) {
|
|
||||||
l.logger.SetPrefix(prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Logger) callHandlers(level LogLevel, s string) {
|
|
||||||
for _, h := range l.handlers[level] {
|
|
||||||
h(level, strings.TrimSpace(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debugln logs a line with a DEBUG prefix.
|
|
||||||
func (l *Logger) Debugln(vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintln(vals...)
|
|
||||||
l.logger.Output(2, "DEBUG: "+s)
|
|
||||||
l.callHandlers(LevelDebug, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debugf logs a formatted line with a DEBUG prefix.
|
|
||||||
func (l *Logger) Debugf(format string, vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintf(format, vals...)
|
|
||||||
l.logger.Output(2, "DEBUG: "+s)
|
|
||||||
l.callHandlers(LevelDebug, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Infoln logs a line with a VERBOSE prefix.
|
|
||||||
func (l *Logger) Verboseln(vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintln(vals...)
|
|
||||||
l.logger.Output(2, "VERBOSE: "+s)
|
|
||||||
l.callHandlers(LevelVerbose, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Infof logs a formatted line with a VERBOSE prefix.
|
|
||||||
func (l *Logger) Verbosef(format string, vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintf(format, vals...)
|
|
||||||
l.logger.Output(2, "VERBOSE: "+s)
|
|
||||||
l.callHandlers(LevelVerbose, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Infoln logs a line with an INFO prefix.
|
|
||||||
func (l *Logger) Infoln(vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintln(vals...)
|
|
||||||
l.logger.Output(2, "INFO: "+s)
|
|
||||||
l.callHandlers(LevelInfo, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Infof logs a formatted line with an INFO prefix.
|
|
||||||
func (l *Logger) Infof(format string, vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintf(format, vals...)
|
|
||||||
l.logger.Output(2, "INFO: "+s)
|
|
||||||
l.callHandlers(LevelInfo, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Okln logs a line with an OK prefix.
|
|
||||||
func (l *Logger) Okln(vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintln(vals...)
|
|
||||||
l.logger.Output(2, "OK: "+s)
|
|
||||||
l.callHandlers(LevelOK, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Okf logs a formatted line with an OK prefix.
|
|
||||||
func (l *Logger) Okf(format string, vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintf(format, vals...)
|
|
||||||
l.logger.Output(2, "OK: "+s)
|
|
||||||
l.callHandlers(LevelOK, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warnln logs a formatted line with a WARNING prefix.
|
|
||||||
func (l *Logger) Warnln(vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintln(vals...)
|
|
||||||
l.logger.Output(2, "WARNING: "+s)
|
|
||||||
l.callHandlers(LevelWarn, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warnf logs a formatted line with a WARNING prefix.
|
|
||||||
func (l *Logger) Warnf(format string, vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintf(format, vals...)
|
|
||||||
l.logger.Output(2, "WARNING: "+s)
|
|
||||||
l.callHandlers(LevelWarn, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatalln logs a line with a FATAL prefix and exits the process with exit
|
|
||||||
// code 1.
|
|
||||||
func (l *Logger) Fatalln(vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintln(vals...)
|
|
||||||
l.logger.Output(2, "FATAL: "+s)
|
|
||||||
l.callHandlers(LevelFatal, s)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatalf logs a formatted line with a FATAL prefix and exits the process with
|
|
||||||
// exit code 1.
|
|
||||||
func (l *Logger) Fatalf(format string, vals ...interface{}) {
|
|
||||||
l.mut.Lock()
|
|
||||||
defer l.mut.Unlock()
|
|
||||||
s := fmt.Sprintf(format, vals...)
|
|
||||||
l.logger.Output(2, "FATAL: "+s)
|
|
||||||
l.callHandlers(LevelFatal, s)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
-58
@@ -1,58 +0,0 @@
|
|||||||
// Copyright (C) 2014 Jakob Borg. All rights reserved. Use of this source code
|
|
||||||
// is governed by an MIT-style license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAPI(t *testing.T) {
|
|
||||||
l := New()
|
|
||||||
l.SetFlags(0)
|
|
||||||
l.SetPrefix("testing")
|
|
||||||
|
|
||||||
debug := 0
|
|
||||||
l.AddHandler(LevelDebug, checkFunc(t, LevelDebug, "test 0", &debug))
|
|
||||||
info := 0
|
|
||||||
l.AddHandler(LevelInfo, checkFunc(t, LevelInfo, "test 1", &info))
|
|
||||||
warn := 0
|
|
||||||
l.AddHandler(LevelWarn, checkFunc(t, LevelWarn, "test 2", &warn))
|
|
||||||
ok := 0
|
|
||||||
l.AddHandler(LevelOK, checkFunc(t, LevelOK, "test 3", &ok))
|
|
||||||
|
|
||||||
l.Debugf("test %d", 0)
|
|
||||||
l.Debugln("test", 0)
|
|
||||||
l.Infof("test %d", 1)
|
|
||||||
l.Infoln("test", 1)
|
|
||||||
l.Warnf("test %d", 2)
|
|
||||||
l.Warnln("test", 2)
|
|
||||||
l.Okf("test %d", 3)
|
|
||||||
l.Okln("test", 3)
|
|
||||||
|
|
||||||
if debug != 2 {
|
|
||||||
t.Errorf("Debug handler called %d != 2 times", debug)
|
|
||||||
}
|
|
||||||
if info != 2 {
|
|
||||||
t.Errorf("Info handler called %d != 2 times", info)
|
|
||||||
}
|
|
||||||
if warn != 2 {
|
|
||||||
t.Errorf("Warn handler called %d != 2 times", warn)
|
|
||||||
}
|
|
||||||
if ok != 2 {
|
|
||||||
t.Errorf("Ok handler called %d != 2 times", ok)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkFunc(t *testing.T, expectl LogLevel, expectmsg string, counter *int) func(LogLevel, string) {
|
|
||||||
return func(l LogLevel, msg string) {
|
|
||||||
*counter++
|
|
||||||
if l != expectl {
|
|
||||||
t.Errorf("Incorrect message level %d != %d", l, expectl)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(msg, expectmsg) {
|
|
||||||
t.Errorf("%q does not end with %q", msg, expectmsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
// Copyright (C) 2014 The Protocol Authors.
|
|
||||||
|
|
||||||
package protocol
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/calmh/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
debug = strings.Contains(os.Getenv("STTRACE"), "protocol") || os.Getenv("STTRACE") == "all"
|
|
||||||
l = logger.DefaultLogger
|
|
||||||
)
|
|
||||||
+1
@@ -201,6 +201,7 @@ func (p *BufferPool) String() string {
|
|||||||
|
|
||||||
func (p *BufferPool) drain() {
|
func (p *BufferPool) drain() {
|
||||||
ticker := time.NewTicker(2 * time.Second)
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
|||||||
+13
-8
@@ -6,10 +6,8 @@ Suture
|
|||||||
Suture provides Erlang-ish supervisor trees for Go. "Supervisor trees" ->
|
Suture provides Erlang-ish supervisor trees for Go. "Supervisor trees" ->
|
||||||
"sutree" -> "suture" -> holds your code together when it's trying to die.
|
"sutree" -> "suture" -> holds your code together when it's trying to die.
|
||||||
|
|
||||||
This is intended to be a production-quality library going into code that I
|
This library has hit maturity, and isn't expected to be changed
|
||||||
will be very early on the phone tree to support when it goes down. However,
|
radically. This can also be imported via gopkg.in/thejerf/suture.v1 .
|
||||||
it has not been deployed into something quite that serious yet. (I will
|
|
||||||
update this statement when that changes.)
|
|
||||||
|
|
||||||
It is intended to deal gracefully with the real failure cases that can
|
It is intended to deal gracefully with the real failure cases that can
|
||||||
occur with supervision trees (such as burning all your CPU time endlessly
|
occur with supervision trees (such as burning all your CPU time endlessly
|
||||||
@@ -24,10 +22,6 @@ This module is fully covered with [godoc](http://godoc.org/github.com/thejerf/su
|
|||||||
including an example, usage, and everything else you might expect from a
|
including an example, usage, and everything else you might expect from a
|
||||||
README.md on GitHub. (DRY.)
|
README.md on GitHub. (DRY.)
|
||||||
|
|
||||||
This is not currently tagged with particular git tags for Go as this is
|
|
||||||
currently considered to be alpha code. As I move this into production and
|
|
||||||
feel more confident about it, I'll give it relevant tags.
|
|
||||||
|
|
||||||
Code Signing
|
Code Signing
|
||||||
------------
|
------------
|
||||||
|
|
||||||
@@ -43,3 +37,14 @@ easily fit into the OTP paradigm. It ought to someday be considered a good
|
|||||||
idea to distribute libraries that provide some sort of supervisor tree
|
idea to distribute libraries that provide some sort of supervisor tree
|
||||||
functionality out of the box. It is possible to provide this functionality
|
functionality out of the box. It is possible to provide this functionality
|
||||||
without explicitly depending on the Suture library.
|
without explicitly depending on the Suture library.
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
---------
|
||||||
|
|
||||||
|
suture uses semantic versioning.
|
||||||
|
|
||||||
|
1. 1.0.0
|
||||||
|
* Initial release.
|
||||||
|
2. 1.0.1
|
||||||
|
* Fixed data race on the .state variable.
|
||||||
|
|
||||||
|
|||||||
+23
-4
@@ -59,6 +59,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -123,7 +124,6 @@ type Supervisor struct {
|
|||||||
lastFail time.Time
|
lastFail time.Time
|
||||||
failures float64
|
failures float64
|
||||||
restartQueue []serviceID
|
restartQueue []serviceID
|
||||||
state uint8
|
|
||||||
serviceCounter serviceID
|
serviceCounter serviceID
|
||||||
control chan supervisorMessage
|
control chan supervisorMessage
|
||||||
resumeTimer <-chan time.Time
|
resumeTimer <-chan time.Time
|
||||||
@@ -143,6 +143,9 @@ type Supervisor struct {
|
|||||||
// a minimal chunk.
|
// a minimal chunk.
|
||||||
getNow func() time.Time
|
getNow func() time.Time
|
||||||
getResume func(time.Duration) <-chan time.Time
|
getResume func(time.Duration) <-chan time.Time
|
||||||
|
|
||||||
|
sync.Mutex
|
||||||
|
state uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spec is used to pass arguments to the New function to create a
|
// Spec is used to pass arguments to the New function to create a
|
||||||
@@ -373,6 +376,7 @@ func (s *Supervisor) Add(service Service) ServiceToken {
|
|||||||
supervisor.logBackoff = s.logBackoff
|
supervisor.logBackoff = s.logBackoff
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.Lock()
|
||||||
if s.state == notRunning {
|
if s.state == notRunning {
|
||||||
id := s.serviceCounter
|
id := s.serviceCounter
|
||||||
s.serviceCounter++
|
s.serviceCounter++
|
||||||
@@ -380,8 +384,10 @@ func (s *Supervisor) Add(service Service) ServiceToken {
|
|||||||
s.services[id] = service
|
s.services[id] = service
|
||||||
s.restartQueue = append(s.restartQueue, id)
|
s.restartQueue = append(s.restartQueue, id)
|
||||||
|
|
||||||
|
s.Unlock()
|
||||||
return ServiceToken{uint64(s.id)<<32 | uint64(id)}
|
return ServiceToken{uint64(s.id)<<32 | uint64(id)}
|
||||||
}
|
}
|
||||||
|
s.Unlock()
|
||||||
|
|
||||||
response := make(chan serviceID)
|
response := make(chan serviceID)
|
||||||
s.control <- addService{service, response}
|
s.control <- addService{service, response}
|
||||||
@@ -408,16 +414,19 @@ func (s *Supervisor) Serve() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
s.Lock()
|
||||||
s.state = notRunning
|
s.state = notRunning
|
||||||
|
s.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
s.Lock()
|
||||||
if s.state != notRunning {
|
if s.state != notRunning {
|
||||||
// FIXME: Don't explain why I don't need a semaphore, just use one
|
s.Unlock()
|
||||||
// This doesn't use a semaphore because it's just a sanity check.
|
|
||||||
panic("Running a supervisor while it is already running?")
|
panic("Running a supervisor while it is already running?")
|
||||||
}
|
}
|
||||||
|
|
||||||
s.state = normal
|
s.state = normal
|
||||||
|
s.Unlock()
|
||||||
|
|
||||||
// for all the services I currently know about, start them
|
// for all the services I currently know about, start them
|
||||||
for _, id := range s.restartQueue {
|
for _, id := range s.restartQueue {
|
||||||
@@ -472,7 +481,9 @@ func (s *Supervisor) Serve() {
|
|||||||
// excessive thrashing
|
// excessive thrashing
|
||||||
// FIXME: Ought to permit some spacing of these functions, rather
|
// FIXME: Ought to permit some spacing of these functions, rather
|
||||||
// than simply hammering through them
|
// than simply hammering through them
|
||||||
|
s.Lock()
|
||||||
s.state = normal
|
s.state = normal
|
||||||
|
s.Unlock()
|
||||||
s.failures = 0
|
s.failures = 0
|
||||||
s.logBackoff(s, false)
|
s.logBackoff(s, false)
|
||||||
for _, id := range s.restartQueue {
|
for _, id := range s.restartQueue {
|
||||||
@@ -499,7 +510,9 @@ func (s *Supervisor) handleFailedService(id serviceID, err interface{}, stacktra
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.failures > s.failureThreshold {
|
if s.failures > s.failureThreshold {
|
||||||
|
s.Lock()
|
||||||
s.state = paused
|
s.state = paused
|
||||||
|
s.Unlock()
|
||||||
s.logBackoff(s, true)
|
s.logBackoff(s, true)
|
||||||
s.resumeTimer = s.getResume(s.failureBackoff)
|
s.resumeTimer = s.getResume(s.failureBackoff)
|
||||||
}
|
}
|
||||||
@@ -511,7 +524,13 @@ func (s *Supervisor) handleFailedService(id serviceID, err interface{}, stacktra
|
|||||||
// It is possible for a service to be no longer monitored
|
// It is possible for a service to be no longer monitored
|
||||||
// by the time we get here. In that case, just ignore it.
|
// by the time we get here. In that case, just ignore it.
|
||||||
if monitored {
|
if monitored {
|
||||||
if s.state == normal {
|
// this may look dangerous because the state could change, but this
|
||||||
|
// code is only ever run in the one goroutine that is permitted to
|
||||||
|
// change the state, so nothing else will.
|
||||||
|
s.Lock()
|
||||||
|
curState := s.state
|
||||||
|
s.Unlock()
|
||||||
|
if curState == normal {
|
||||||
s.runService(failedService, id)
|
s.runService(failedService, id)
|
||||||
s.logFailure(s, failedService, s.failures, s.failureThreshold, true, err, stacktrace)
|
s.logFailure(s, failedService, s.failures, s.failureThreshold, true, err, stacktrace)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ func (i *Incrementor) Serve() {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case i.next <- i.current:
|
case i.next <- i.current:
|
||||||
i.current += 1
|
i.current++
|
||||||
case <-i.stop:
|
case <-i.stop:
|
||||||
// We sync here just to guarantee the output of "Stopping the service",
|
// We sync here just to guarantee the output of "Stopping the service",
|
||||||
// so this passes the test reliably.
|
// so this passes the test reliably.
|
||||||
|
|||||||
+20
-4
@@ -478,6 +478,22 @@ func TestNilSupervisorAdd(t *testing.T) {
|
|||||||
s.Add(s)
|
s.Add(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/thejerf/suture/issues/11
|
||||||
|
//
|
||||||
|
// The purpose of this test is to verify that it does not cause data races,
|
||||||
|
// so there are no obvious assertions.
|
||||||
|
func TestIssue11(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := NewSimple("main")
|
||||||
|
s.ServeBackground()
|
||||||
|
|
||||||
|
subsuper := NewSimple("sub")
|
||||||
|
s.Add(subsuper)
|
||||||
|
|
||||||
|
subsuper.Add(NewService("may cause data race"))
|
||||||
|
}
|
||||||
|
|
||||||
// http://golangtutorials.blogspot.com/2011/10/gotest-unit-testing-and-benchmarking-go.html
|
// http://golangtutorials.blogspot.com/2011/10/gotest-unit-testing-and-benchmarking-go.html
|
||||||
// claims test function are run in the same order as the source file...
|
// claims test function are run in the same order as the source file...
|
||||||
// I'm not sure if this is part of the contract, though. Especially in the
|
// I'm not sure if this is part of the contract, though. Especially in the
|
||||||
@@ -509,7 +525,7 @@ func (s *FailableService) Serve() {
|
|||||||
everMultistarted = true
|
everMultistarted = true
|
||||||
panic("Multi-started the same service! " + s.name)
|
panic("Multi-started the same service! " + s.name)
|
||||||
}
|
}
|
||||||
s.existing += 1
|
s.existing++
|
||||||
|
|
||||||
s.started <- true
|
s.started <- true
|
||||||
|
|
||||||
@@ -522,13 +538,13 @@ func (s *FailableService) Serve() {
|
|||||||
case Happy:
|
case Happy:
|
||||||
// Do nothing on purpose. Life is good!
|
// Do nothing on purpose. Life is good!
|
||||||
case Fail:
|
case Fail:
|
||||||
s.existing -= 1
|
s.existing--
|
||||||
if useStopChan {
|
if useStopChan {
|
||||||
s.stop <- true
|
s.stop <- true
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case Panic:
|
case Panic:
|
||||||
s.existing -= 1
|
s.existing--
|
||||||
panic("Panic!")
|
panic("Panic!")
|
||||||
case Hang:
|
case Hang:
|
||||||
// or more specifically, "hang until I release you"
|
// or more specifically, "hang until I release you"
|
||||||
@@ -537,7 +553,7 @@ func (s *FailableService) Serve() {
|
|||||||
useStopChan = true
|
useStopChan = true
|
||||||
}
|
}
|
||||||
case <-s.shutdown:
|
case <-s.shutdown:
|
||||||
s.existing -= 1
|
s.existing--
|
||||||
if useStopChan {
|
if useStopChan {
|
||||||
s.stop <- true
|
s.stop <- true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ brbecker <brbecker@gmail.com>
|
|||||||
brendanlong <self@brendanlong.com>
|
brendanlong <self@brendanlong.com>
|
||||||
brgmnn <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
|
brgmnn <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
|
||||||
bsidhom <bsidhom@gmail.com>
|
bsidhom <bsidhom@gmail.com>
|
||||||
|
burkemw3 <mburke@amplify.com>
|
||||||
calmh <jakob@nym.se>
|
calmh <jakob@nym.se>
|
||||||
canton7 <antony.male@gmail.com>
|
canton7 <antony.male@gmail.com>
|
||||||
cdata <chris@scriptolo.gy>
|
cdata <chris@scriptolo.gy>
|
||||||
@@ -26,6 +27,7 @@ ceh <emil@hessman.se>
|
|||||||
cqcallaw <enlightened.despot@gmail.com>
|
cqcallaw <enlightened.despot@gmail.com>
|
||||||
dva <denisva@gmail.com>
|
dva <denisva@gmail.com>
|
||||||
dzarda <dzardacz@gmail.com>
|
dzarda <dzardacz@gmail.com>
|
||||||
|
eipiminus1 <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>
|
||||||
facastagnini <federico.castagnini@gmail.com>
|
facastagnini <federico.castagnini@gmail.com>
|
||||||
filoozoom <philippe@schommers.be>
|
filoozoom <philippe@schommers.be>
|
||||||
frioux <frew@afoolishmanifesto.com> <frioux@gmail.com>
|
frioux <frew@afoolishmanifesto.com> <frioux@gmail.com>
|
||||||
@@ -41,6 +43,7 @@ kozec <kozec@kozec.com>
|
|||||||
krozycki <rozycki.karol@gmail.com>
|
krozycki <rozycki.karol@gmail.com>
|
||||||
marcindziadus <dziadus.marcin@gmail.com>
|
marcindziadus <dziadus.marcin@gmail.com>
|
||||||
marclaporte <marc@marclaporte.com>
|
marclaporte <marc@marclaporte.com>
|
||||||
|
mateon1 <matin1111@wp.pl>
|
||||||
mogwa1 <devriesb@gmail.com>
|
mogwa1 <devriesb@gmail.com>
|
||||||
moshen <moshen.colin@gmail.com>
|
moshen <moshen.colin@gmail.com>
|
||||||
mvdan <mvdan@mvdan.cc>
|
mvdan <mvdan@mvdan.cc>
|
||||||
@@ -52,10 +55,11 @@ pluby <phill.luby@newredo.com>
|
|||||||
pyfisch <pyfisch@gmail.com>
|
pyfisch <pyfisch@gmail.com>
|
||||||
qbit <qbit@deftly.net>
|
qbit <qbit@deftly.net>
|
||||||
ralder <ralder@yandex.ru>
|
ralder <ralder@yandex.ru>
|
||||||
rumpelsepp <stefan@sevenbyte.org>
|
rumpelsepp <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
|
||||||
sciurius <jvromans@squirrel.nl>
|
sciurius <jvromans@squirrel.nl>
|
||||||
seehuhn <voss@seehuhn.de>
|
seehuhn <voss@seehuhn.de>
|
||||||
snnd <dw@risu.io>
|
snnd <dw@risu.io>
|
||||||
|
simplypeachy <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
|
||||||
timabell <tim@timwise.co.uk>
|
timabell <tim@timwise.co.uk>
|
||||||
tnn2 <tnn@nygren.pp.se>
|
tnn2 <tnn@nygren.pp.se>
|
||||||
tojrobinson <tully@tojr.org>
|
tojrobinson <tully@tojr.org>
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
Syncthing
|
# Syncthing
|
||||||
=========
|
|
||||||
|
|
||||||
[](http://build.syncthing.net/job/syncthing/lastBuild/)
|
[](http://build.syncthing.net/job/syncthing/lastBuild/)
|
||||||
[](http://godoc.org/github.com/syncthing/syncthing)
|
[](https://ci.appveyor.com/project/calmh/syncthing)
|
||||||
[](https://www.mozilla.org/MPL/2.0/)
|
[](http://godoc.org/github.com/syncthing/syncthing)
|
||||||
|
[](https://www.mozilla.org/MPL/2.0/)
|
||||||
|
|
||||||
This is the Syncthing project which pursues the following goals:
|
This is the Syncthing project which pursues the following goals:
|
||||||
|
|
||||||
1. Define a protocol for synchronization of a folder between a number of
|
1. Define a protocol for synchronization of a folder between a number of
|
||||||
collaborating devices. This protocol should be well defined, unambiguous,
|
collaborating devices. This protocol should be well defined, unambiguous,
|
||||||
easily understood, free to use, efficient, secure and language neutral.
|
easily understood, free to use, efficient, secure and language neutral.
|
||||||
This is called the [Block Exchange
|
This is called the [Block Exchange Protocol][1].
|
||||||
Protocol](https://github.com/syncthing/specs/blob/master/BEPv1.md).
|
|
||||||
|
|
||||||
2. Provide the reference implementation to demonstrate the usability of
|
2. Provide the reference implementation to demonstrate the usability of
|
||||||
said protocol. This is the `syncthing` utility. We hope that
|
said protocol. This is the `syncthing` utility. We hope that
|
||||||
@@ -21,38 +20,38 @@ The two are evolving together; the protocol is not to be considered
|
|||||||
stable until Syncthing 1.0 is released, at which point it is locked down
|
stable until Syncthing 1.0 is released, at which point it is locked down
|
||||||
for incompatible changes.
|
for incompatible changes.
|
||||||
|
|
||||||
Getting Started
|
## Getting Started
|
||||||
---------------
|
|
||||||
|
|
||||||
Take a look at the [getting started
|
Take a look at the [getting started guide][2].
|
||||||
guide](http://docs.syncthing.net/intro/getting-started.html).
|
|
||||||
|
|
||||||
There are a few examples for keeping Syncthing running in the background
|
There are a few examples for keeping Syncthing running in the background
|
||||||
on your system in [the etc directory](https://github.com/syncthing/syncthing/blob/master/etc).
|
on your system in [the etc directory][3].
|
||||||
|
|
||||||
There is an IRC channel, `#syncthing` on Freenode, for talking directly
|
There is an IRC channel, `#syncthing` on [Freenode][4], for talking directly
|
||||||
to developers and users.
|
to developers and users.
|
||||||
|
|
||||||
Building
|
## Building
|
||||||
--------
|
|
||||||
|
|
||||||
Building Syncthing from source is easy, and there's a
|
Building Syncthing from source is easy, and there's a [guide][5].
|
||||||
[guide](http://docs.syncthing.net/dev/building.html).
|
|
||||||
that describes it for both Unix and Windows systems.
|
that describes it for both Unix and Windows systems.
|
||||||
|
|
||||||
Signed Releases
|
## Signed Releases
|
||||||
---------------
|
|
||||||
|
|
||||||
As of v0.10.15 and onwards, git tags and release binaries are GPG signed
|
As of v0.10.15 and onwards, git tags and release binaries are GPG signed
|
||||||
with the key D26E6ED000654A3E (see https://syncthing.net/security.html).
|
with the key D26E6ED000654A3E (see https://syncthing.net/security.html).
|
||||||
For release binaries, MD5 and SHA1 checksums are calculated and signed,
|
For release binaries, MD5 and SHA1 checksums are calculated and signed,
|
||||||
available in the md5sum.txt.asc and sha1sum.txt.asc files.
|
available in the md5sum.txt.asc and sha1sum.txt.asc files.
|
||||||
|
|
||||||
Documentation
|
## Documentation
|
||||||
=============
|
|
||||||
|
|
||||||
Please see the [Syncthing
|
Please see the [Syncthing documentation site][6].
|
||||||
documentation site](http://docs.syncthing.net/).
|
|
||||||
|
|
||||||
All code is licensed under the
|
All code is licensed under the [MPLv2 License][7].
|
||||||
[MPLv2 License](https://github.com/syncthing/syncthing/blob/master/LICENSE).
|
|
||||||
|
[1]: https://github.com/syncthing/specs/blob/master/BEPv1.md
|
||||||
|
[2]: http://docs.syncthing.net/intro/getting-started.html
|
||||||
|
[3]: https://github.com/syncthing/syncthing/blob/master/etc
|
||||||
|
[4]: https://webchat.freenode.net/
|
||||||
|
[5]: http://docs.syncthing.net/dev/building.html
|
||||||
|
[6]: http://docs.syncthing.net/
|
||||||
|
[7]: https://github.com/syncthing/syncthing/blob/master/LICENSE
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
version: '{branch}-{build}'
|
||||||
|
clone_folder: C:\src\github.com\syncthing\syncthing
|
||||||
|
init:
|
||||||
|
- go version
|
||||||
|
environment:
|
||||||
|
GOPATH: C:\
|
||||||
|
build_script:
|
||||||
|
- go run build.go zip
|
||||||
|
test_script:
|
||||||
|
- go run build.go test
|
||||||
|
artifacts:
|
||||||
|
- path: '*.zip'
|
||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"crypto/md5"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -187,7 +186,7 @@ func setup() {
|
|||||||
|
|
||||||
func test(pkg string) {
|
func test(pkg string) {
|
||||||
setBuildEnv()
|
setBuildEnv()
|
||||||
runPrint("go", "test", "-short", "-timeout", "60s", pkg)
|
runPrint("go", "test", "-short", "-race", "-timeout", "60s", pkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func bench(pkg string) {
|
func bench(pkg string) {
|
||||||
@@ -215,7 +214,7 @@ func build(pkg string, tags []string) {
|
|||||||
binary += ".exe"
|
binary += ".exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
rmr(binary, binary+".md5")
|
rmr(binary)
|
||||||
args := []string{"build", "-ldflags", ldflags()}
|
args := []string{"build", "-ldflags", ldflags()}
|
||||||
if len(tags) > 0 {
|
if len(tags) > 0 {
|
||||||
args = append(args, "-tags", strings.Join(tags, ","))
|
args = append(args, "-tags", strings.Join(tags, ","))
|
||||||
@@ -226,13 +225,6 @@ func build(pkg string, tags []string) {
|
|||||||
args = append(args, pkg)
|
args = append(args, pkg)
|
||||||
setBuildEnv()
|
setBuildEnv()
|
||||||
runPrint("go", args...)
|
runPrint("go", args...)
|
||||||
|
|
||||||
// Create an md5 checksum of the binary, to be included in the archive for
|
|
||||||
// automatic upgrades.
|
|
||||||
err := md5File(binary)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTar() {
|
func buildTar() {
|
||||||
@@ -249,7 +241,6 @@ func buildTar() {
|
|||||||
{src: "LICENSE", dst: name + "/LICENSE.txt"},
|
{src: "LICENSE", dst: name + "/LICENSE.txt"},
|
||||||
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
|
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
|
||||||
{src: "syncthing", dst: name + "/syncthing"},
|
{src: "syncthing", dst: name + "/syncthing"},
|
||||||
{src: "syncthing.md5", dst: name + "/syncthing.md5"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range listFiles("etc") {
|
for _, file := range listFiles("etc") {
|
||||||
@@ -277,7 +268,6 @@ func buildZip() {
|
|||||||
{src: "LICENSE", dst: name + "/LICENSE.txt"},
|
{src: "LICENSE", dst: name + "/LICENSE.txt"},
|
||||||
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
|
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
|
||||||
{src: "syncthing.exe", dst: name + "/syncthing.exe"},
|
{src: "syncthing.exe", dst: name + "/syncthing.exe"},
|
||||||
{src: "syncthing.exe.md5", dst: name + "/syncthing.exe.md5"},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, file := range listFiles("extra") {
|
for _, file := range listFiles("extra") {
|
||||||
@@ -411,7 +401,7 @@ func assets() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func xdr() {
|
func xdr() {
|
||||||
runPrint("go", "generate", "./lib/discover", "./lib/db")
|
runPrint("go", "generate", "./lib/discover", "./lib/db", "./lib/protocol")
|
||||||
}
|
}
|
||||||
|
|
||||||
func translate() {
|
func translate() {
|
||||||
@@ -712,32 +702,6 @@ func zipFile(out string, files []archiveFile) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func md5File(file string) error {
|
|
||||||
fd, err := os.Open(file)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fd.Close()
|
|
||||||
|
|
||||||
h := md5.New()
|
|
||||||
_, err = io.Copy(h, fd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := os.Create(file + ".md5")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = fmt.Fprintf(out, "%x\n", h.Sum(nil))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return out.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func vet(pkg string) {
|
func vet(pkg string) {
|
||||||
bs, err := runError("go", "vet", pkg)
|
bs, err := runError("go", "vet", pkg)
|
||||||
if err != nil && err.Error() == "exit status 3" || bytes.Contains(bs, []byte("no such tool \"vet\"")) {
|
if err != nil && err.Error() == "exit status 3" || bytes.Contains(bs, []byte("no such tool \"vet\"")) {
|
||||||
|
|||||||
@@ -108,10 +108,10 @@ case "${1:-default}" in
|
|||||||
# For every package in the repo
|
# For every package in the repo
|
||||||
for dir in $(go list ./...) ; do
|
for dir in $(go list ./...) ; do
|
||||||
# run the tests
|
# run the tests
|
||||||
godep go test -coverprofile=profile.out $dir
|
godep go test -race -coverprofile=profile.out $dir
|
||||||
if [ -f profile.out ] ; then
|
if [ -f profile.out ] ; then
|
||||||
# and if there was test output, append it to coverage.out
|
# and if there was test output, append it to coverage.out
|
||||||
grep -v "mode: set" profile.out >> coverage.out
|
grep -v "mode: " profile.out >> coverage.out
|
||||||
rm profile.out
|
rm profile.out
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/scanner"
|
"github.com/syncthing/syncthing/lib/scanner"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ func main() {
|
|||||||
if *standardBlocks || blockSize < protocol.BlockSize {
|
if *standardBlocks || blockSize < protocol.BlockSize {
|
||||||
blockSize = protocol.BlockSize
|
blockSize = protocol.BlockSize
|
||||||
}
|
}
|
||||||
bs, err := scanner.Blocks(fd, blockSize, fi.Size())
|
bs, err := scanner.Blocks(fd, blockSize, fi.Size(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-13
@@ -7,37 +7,105 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/discover"
|
"github.com/syncthing/syncthing/lib/discover"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
var timeout = 5 * time.Second
|
||||||
log.SetFlags(0)
|
|
||||||
log.SetOutput(os.Stdout)
|
|
||||||
|
|
||||||
|
func main() {
|
||||||
var server string
|
var server string
|
||||||
|
|
||||||
flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22026", "Announce server")
|
flag.StringVar(&server, "server", "", "Announce server (blank for default set)")
|
||||||
|
flag.DurationVar(&timeout, "timeout", timeout, "Query timeout")
|
||||||
|
flag.Usage = usage
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if len(flag.Args()) != 1 || server == "" {
|
if flag.NArg() != 1 {
|
||||||
log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22026\"] <device>", os.Args[0])
|
flag.Usage()
|
||||||
os.Exit(64)
|
os.Exit(64)
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := protocol.DeviceIDFromString(flag.Args()[0])
|
id, err := protocol.DeviceIDFromString(flag.Args()[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil)
|
if server != "" {
|
||||||
discoverer.StartGlobal([]string{server}, 1)
|
checkServers(id, server)
|
||||||
for _, addr := range discoverer.Lookup(id) {
|
} else {
|
||||||
log.Println(addr)
|
checkServers(id, config.DefaultDiscoveryServers...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type checkResult struct {
|
||||||
|
server string
|
||||||
|
direct []string
|
||||||
|
relays []discover.Relay
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkServers(deviceID protocol.DeviceID, servers ...string) {
|
||||||
|
t0 := time.Now()
|
||||||
|
resc := make(chan checkResult)
|
||||||
|
for _, srv := range servers {
|
||||||
|
srv := srv
|
||||||
|
go func() {
|
||||||
|
res := checkServer(deviceID, srv)
|
||||||
|
res.server = srv
|
||||||
|
resc <- res
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ = range servers {
|
||||||
|
res := <-resc
|
||||||
|
|
||||||
|
u, _ := url.Parse(res.server)
|
||||||
|
fmt.Printf("%s (%v):\n", u.Host, time.Since(t0))
|
||||||
|
|
||||||
|
if res.error != nil {
|
||||||
|
fmt.Println(" " + res.error.Error())
|
||||||
|
}
|
||||||
|
for _, addr := range res.direct {
|
||||||
|
fmt.Println(" address:", addr)
|
||||||
|
}
|
||||||
|
for _, rel := range res.relays {
|
||||||
|
fmt.Printf(" relay: %s (%d ms)\n", rel.URL, rel.Latency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkServer(deviceID protocol.DeviceID, server string) checkResult {
|
||||||
|
disco, err := discover.NewGlobal(server, tls.Certificate{}, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return checkResult{error: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make(chan checkResult, 1)
|
||||||
|
|
||||||
|
time.AfterFunc(timeout, func() {
|
||||||
|
res <- checkResult{error: errors.New("timeout")}
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
direct, relays, err := disco.Lookup(deviceID)
|
||||||
|
res <- checkResult{direct: direct, relays: relays, error: err}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return <-res
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Printf("Usage:\n\t%s [options] <device ID>\n\nOptions:\n", os.Args[0])
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -13,8 +13,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
// Copyright (C) 2015 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/signature"
|
||||||
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetOutput(os.Stdout)
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() < 1 {
|
||||||
|
log.Println(`Usage:
|
||||||
|
stsigtool <command>
|
||||||
|
|
||||||
|
Where command is one of:
|
||||||
|
|
||||||
|
gen
|
||||||
|
- generate a new key pair
|
||||||
|
|
||||||
|
sign <privkeyfile> <datafile>
|
||||||
|
- sign a file
|
||||||
|
|
||||||
|
verify <signaturefile> <datafile>
|
||||||
|
- verify a signature, using the built in public key
|
||||||
|
|
||||||
|
verify <signaturefile> <datafile> <pubkeyfile>
|
||||||
|
- verify a signature, using the specified public key file
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch flag.Arg(0) {
|
||||||
|
case "gen":
|
||||||
|
gen()
|
||||||
|
case "sign":
|
||||||
|
sign(flag.Arg(1), flag.Arg(2))
|
||||||
|
case "verify":
|
||||||
|
if flag.NArg() == 4 {
|
||||||
|
verifyWithFile(flag.Arg(1), flag.Arg(2), flag.Arg(3))
|
||||||
|
} else {
|
||||||
|
verifyWithKey(flag.Arg(1), flag.Arg(2), upgrade.SigningKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gen() {
|
||||||
|
priv, pub, err := signature.GenerateKeys()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Stdout.Write(priv)
|
||||||
|
os.Stdout.Write(pub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sign(keyname, dataname string) {
|
||||||
|
privkey, err := ioutil.ReadFile(keyname)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := os.Open(dataname)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
sig, err := signature.Sign(privkey, fd)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Stdout.Write(sig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyWithFile(signame, dataname, keyname string) {
|
||||||
|
pubkey, err := ioutil.ReadFile(keyname)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
verifyWithKey(signame, dataname, pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyWithKey(signame, dataname string, pubkey []byte) {
|
||||||
|
sig, err := ioutil.ReadFile(signame)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fd, err := os.Open(dataname)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer fd.Close()
|
||||||
|
|
||||||
|
err = signature.Verify(pubkey, sig, fd)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("correct signature")
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
// Copyright (C) 2015 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type addressLister struct {
|
||||||
|
upnpSvc *upnpSvc
|
||||||
|
cfg *config.Wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAddressLister(upnpSvc *upnpSvc, cfg *config.Wrapper) *addressLister {
|
||||||
|
return &addressLister{
|
||||||
|
upnpSvc: upnpSvc,
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExternalAddresses returns a list of addresses that are our best guess for
|
||||||
|
// where we are reachable from the outside. As a special case, we may return
|
||||||
|
// one or more addresses with an empty IP address (0.0.0.0 or ::) and just
|
||||||
|
// port number - this means that the outside address of a NAT gateway should
|
||||||
|
// be substituted.
|
||||||
|
func (e *addressLister) ExternalAddresses() []string {
|
||||||
|
return e.addresses(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllAddresses returns a list of addresses that are our best guess for where
|
||||||
|
// we are reachable from the local network. Same conditions as
|
||||||
|
// ExternalAddresses, but private IPv4 addresses are included.
|
||||||
|
func (e *addressLister) AllAddresses() []string {
|
||||||
|
return e.addresses(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *addressLister) addresses(includePrivateIPV4 bool) []string {
|
||||||
|
var addrs []string
|
||||||
|
|
||||||
|
// Grab our listen addresses from the config. Unspecified ones are passed
|
||||||
|
// on verbatim (to be interpreted by a global discovery server or local
|
||||||
|
// discovery peer). Public addresses are passed on verbatim. Private
|
||||||
|
// addresses are filtered.
|
||||||
|
for _, addrStr := range e.cfg.Options().ListenAddress {
|
||||||
|
addrURL, err := url.Parse(addrStr)
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln("Listen address", addrStr, "is invalid:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", addrURL.Host)
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln("Listen address", addrStr, "is invalid:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.IP == nil || addr.IP.IsUnspecified() {
|
||||||
|
// Address like 0.0.0.0:22000 or [::]:22000 or :22000; include as is.
|
||||||
|
addrs = append(addrs, "tcp://"+addr.String())
|
||||||
|
} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
|
||||||
|
// A public address; include as is.
|
||||||
|
addrs = append(addrs, "tcp://"+addr.String())
|
||||||
|
} else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() {
|
||||||
|
// A private IPv4 address.
|
||||||
|
addrs = append(addrs, "tcp://"+addr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an external port mapping from the upnpSvc, if it has one. If so,
|
||||||
|
// add it as another unspecified address.
|
||||||
|
if e.upnpSvc != nil {
|
||||||
|
if port := e.upnpSvc.ExternalPort(); port != 0 {
|
||||||
|
addrs = append(addrs, fmt.Sprintf("tcp://:%d", port))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return addrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPublicIPv4(ip net.IP) bool {
|
||||||
|
ip = ip.To4()
|
||||||
|
if ip == nil {
|
||||||
|
// Not an IPv4 address (IPv6)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsGlobalUnicast below only checks that it's not link local or
|
||||||
|
// multicast, and we want to exclude private (NAT:ed) addresses as well.
|
||||||
|
rfc1918 := []net.IPNet{
|
||||||
|
{IP: net.IP{10, 0, 0, 0}, Mask: net.IPMask{255, 0, 0, 0}},
|
||||||
|
{IP: net.IP{172, 16, 0, 0}, Mask: net.IPMask{255, 240, 0, 0}},
|
||||||
|
{IP: net.IP{192, 168, 0, 0}, Mask: net.IPMask{255, 255, 0, 0}},
|
||||||
|
}
|
||||||
|
for _, n := range rfc1918 {
|
||||||
|
if n.Contains(ip) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip.IsGlobalUnicast()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPublicIPv6(ip net.IP) bool {
|
||||||
|
if ip.To4() != nil {
|
||||||
|
// Not an IPv6 address (IPv4)
|
||||||
|
// (To16() returns a v6 mapped v4 address so can't be used to check
|
||||||
|
// that it's an actual v6 address)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip.IsGlobalUnicast()
|
||||||
|
}
|
||||||
@@ -1,372 +0,0 @@
|
|||||||
// Copyright (C) 2015 The Syncthing Authors.
|
|
||||||
//
|
|
||||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
||||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
|
||||||
"github.com/thejerf/suture"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The connection service listens on TLS and dials configured unconnected
|
|
||||||
// devices. Successful connections are handed to the model.
|
|
||||||
type connectionSvc struct {
|
|
||||||
*suture.Supervisor
|
|
||||||
cfg *config.Wrapper
|
|
||||||
myID protocol.DeviceID
|
|
||||||
model *model.Model
|
|
||||||
tlsCfg *tls.Config
|
|
||||||
conns chan *tls.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model.Model, tlsCfg *tls.Config) *connectionSvc {
|
|
||||||
svc := &connectionSvc{
|
|
||||||
Supervisor: suture.NewSimple("connectionSvc"),
|
|
||||||
cfg: cfg,
|
|
||||||
myID: myID,
|
|
||||||
model: model,
|
|
||||||
tlsCfg: tlsCfg,
|
|
||||||
conns: make(chan *tls.Conn),
|
|
||||||
}
|
|
||||||
|
|
||||||
// There are several moving parts here; one routine per listening address
|
|
||||||
// to handle incoming connections, one routine to periodically attempt
|
|
||||||
// outgoing connections, and lastly one routine to the the common handling
|
|
||||||
// regardless of whether the connection was incoming or outgoing. It ends
|
|
||||||
// up as in the diagram below. We embed a Supervisor to manage the
|
|
||||||
// routines (i.e. log and restart if they crash or exit, etc).
|
|
||||||
//
|
|
||||||
// +-----------------+
|
|
||||||
// Incoming | +---------------+-+ +-----------------+
|
|
||||||
// Connections | | | | | Outgoing
|
|
||||||
// -------------->| | svc.listen | | | Connections
|
|
||||||
// | | (1 per listen | | svc.connect |-------------->
|
|
||||||
// | | address) | | |
|
|
||||||
// +-+ | | |
|
|
||||||
// +-----------------+ +-----------------+
|
|
||||||
// v v
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// +------------+-----------+
|
|
||||||
// |
|
|
||||||
// | svc.conns
|
|
||||||
// v
|
|
||||||
// +-----------------+
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// | svc.handle |------> model.AddConnection()
|
|
||||||
// | |
|
|
||||||
// | |
|
|
||||||
// +-----------------+
|
|
||||||
//
|
|
||||||
// TODO: Clean shutdown, and/or handling config changes on the fly. We
|
|
||||||
// partly do this now - new devices and addresses will be picked up, but
|
|
||||||
// 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 {
|
|
||||||
addr := addr
|
|
||||||
listener := serviceFunc(func() {
|
|
||||||
svc.listen(addr)
|
|
||||||
})
|
|
||||||
svc.Add(listener)
|
|
||||||
}
|
|
||||||
svc.Add(serviceFunc(svc.handle))
|
|
||||||
|
|
||||||
return svc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *connectionSvc) handle() {
|
|
||||||
next:
|
|
||||||
for conn := range s.conns {
|
|
||||||
cs := conn.ConnectionState()
|
|
||||||
|
|
||||||
// We should have negotiated the next level protocol "bep/1.0" as part
|
|
||||||
// of the TLS handshake. Unfortunately this can't be a hard error,
|
|
||||||
// because there are implementations out there that don't support
|
|
||||||
// protocol negotiation (iOS for one...).
|
|
||||||
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != bepProtocolName {
|
|
||||||
l.Infof("Peer %s did not negotiate bep/1.0", conn.RemoteAddr())
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should have received exactly one certificate from the other
|
|
||||||
// side. If we didn't, they don't have a device ID and we drop the
|
|
||||||
// connection.
|
|
||||||
certs := cs.PeerCertificates
|
|
||||||
if cl := len(certs); cl != 1 {
|
|
||||||
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, conn.RemoteAddr())
|
|
||||||
conn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
remoteCert := certs[0]
|
|
||||||
remoteID := protocol.NewDeviceID(remoteCert.Raw)
|
|
||||||
|
|
||||||
// The device ID should not be that of ourselves. It can happen
|
|
||||||
// though, especially in the presence of NAT hairpinning, multiple
|
|
||||||
// clients between the same NAT gateway, and global discovery.
|
|
||||||
if remoteID == myID {
|
|
||||||
l.Infof("Connected to myself (%s) - should not happen", remoteID)
|
|
||||||
conn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should not already be connected to the other party. TODO: This
|
|
||||||
// could use some better handling. If the old connection is dead but
|
|
||||||
// hasn't timed out yet we may want to drop *that* connection and keep
|
|
||||||
// this one. But in case we are two devices connecting to each other
|
|
||||||
// in parallel we don't want to do that or we end up with no
|
|
||||||
// connections still established...
|
|
||||||
if s.model.ConnectedTo(remoteID) {
|
|
||||||
l.Infof("Connected to already connected device (%s)", remoteID)
|
|
||||||
conn.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for deviceID, deviceCfg := range s.cfg.Devices() {
|
|
||||||
if deviceID == remoteID {
|
|
||||||
// Verify the name on the certificate. By default we set it to
|
|
||||||
// "syncthing" when generating, but the user may have replaced
|
|
||||||
// the certificate and used another name.
|
|
||||||
certName := deviceCfg.CertName
|
|
||||||
if certName == "" {
|
|
||||||
certName = tlsDefaultCommonName
|
|
||||||
}
|
|
||||||
err := remoteCert.VerifyHostname(certName)
|
|
||||||
if err != nil {
|
|
||||||
// Incorrect certificate name is something the user most
|
|
||||||
// likely wants to know about, since it's an advanced
|
|
||||||
// config. Warn instead of Info.
|
|
||||||
l.Warnf("Bad certificate from %s (%v): %v", remoteID, conn.RemoteAddr(), err)
|
|
||||||
conn.Close()
|
|
||||||
continue next
|
|
||||||
}
|
|
||||||
|
|
||||||
// If rate limiting is set, and based on the address we should
|
|
||||||
// limit the connection, then we wrap it in a limiter.
|
|
||||||
|
|
||||||
limit := s.shouldLimit(conn.RemoteAddr())
|
|
||||||
|
|
||||||
wr := io.Writer(conn)
|
|
||||||
if limit && writeRateLimit != nil {
|
|
||||||
wr = &limitedWriter{conn, writeRateLimit}
|
|
||||||
}
|
|
||||||
|
|
||||||
rd := io.Reader(conn)
|
|
||||||
if limit && readRateLimit != nil {
|
|
||||||
rd = &limitedReader{conn, readRateLimit}
|
|
||||||
}
|
|
||||||
|
|
||||||
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
|
|
||||||
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
|
|
||||||
|
|
||||||
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
|
||||||
if debugNet {
|
|
||||||
l.Debugf("cipher suite: %04X in lan: %t", conn.ConnectionState().CipherSuite, !limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.model.AddConnection(conn, protoConn)
|
|
||||||
continue next
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.cfg.IgnoredDevice(remoteID) {
|
|
||||||
events.Default.Log(events.DeviceRejected, map[string]string{
|
|
||||||
"device": remoteID.String(),
|
|
||||||
"address": conn.RemoteAddr().String(),
|
|
||||||
})
|
|
||||||
l.Infof("Connection from %s with unknown device ID %s", conn.RemoteAddr(), remoteID)
|
|
||||||
} else {
|
|
||||||
l.Infof("Connection from %s with ignored device ID %s", conn.RemoteAddr(), remoteID)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *connectionSvc) listen(addr string) {
|
|
||||||
if debugNet {
|
|
||||||
l.Debugln("listening on", addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
tcaddr, err := net.ResolveTCPAddr("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
l.Fatalln("listen (BEP):", err)
|
|
||||||
}
|
|
||||||
listener, err := net.ListenTCP("tcp", tcaddr)
|
|
||||||
if err != nil {
|
|
||||||
l.Fatalln("listen (BEP):", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
conn, err := listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("Accepting connection:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if debugNet {
|
|
||||||
l.Debugln("connect from", conn.RemoteAddr())
|
|
||||||
}
|
|
||||||
|
|
||||||
tcpConn := conn.(*net.TCPConn)
|
|
||||||
s.setTCPOptions(tcpConn)
|
|
||||||
|
|
||||||
tc := tls.Server(conn, s.tlsCfg)
|
|
||||||
err = tc.Handshake()
|
|
||||||
if err != nil {
|
|
||||||
l.Infoln("TLS handshake:", err)
|
|
||||||
tc.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
s.conns <- tc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *connectionSvc) connect() {
|
|
||||||
delay := time.Second
|
|
||||||
for {
|
|
||||||
nextDevice:
|
|
||||||
for deviceID, deviceCfg := range s.cfg.Devices() {
|
|
||||||
if deviceID == myID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.model.ConnectedTo(deviceID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var addrs []string
|
|
||||||
for _, addr := range deviceCfg.Addresses {
|
|
||||||
if addr == "dynamic" {
|
|
||||||
if discoverer != nil {
|
|
||||||
t := discoverer.Lookup(deviceID)
|
|
||||||
if len(t) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addrs = append(addrs, t...)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
addrs = append(addrs, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
host, port, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
|
|
||||||
// addr is on the form "1.2.3.4"
|
|
||||||
addr = net.JoinHostPort(addr, "22000")
|
|
||||||
} else if err == nil && port == "" {
|
|
||||||
// addr is on the form "1.2.3.4:"
|
|
||||||
addr = net.JoinHostPort(host, "22000")
|
|
||||||
}
|
|
||||||
if debugNet {
|
|
||||||
l.Debugln("dial", deviceCfg.DeviceID, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
raddr, err := net.ResolveTCPAddr("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
if debugNet {
|
|
||||||
l.Debugln(err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.DialTCP("tcp", nil, raddr)
|
|
||||||
if err != nil {
|
|
||||||
if debugNet {
|
|
||||||
l.Debugln(err)
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
s.setTCPOptions(conn)
|
|
||||||
|
|
||||||
tc := tls.Client(conn, s.tlsCfg)
|
|
||||||
err = tc.Handshake()
|
|
||||||
if err != nil {
|
|
||||||
l.Infoln("TLS handshake:", err)
|
|
||||||
tc.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
s.conns <- tc
|
|
||||||
continue nextDevice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(delay)
|
|
||||||
delay *= 2
|
|
||||||
if maxD := time.Duration(s.cfg.Options().ReconnectIntervalS) * time.Second; delay > maxD {
|
|
||||||
delay = maxD
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*connectionSvc) setTCPOptions(conn *net.TCPConn) {
|
|
||||||
var err error
|
|
||||||
if err = conn.SetLinger(0); err != nil {
|
|
||||||
l.Infoln(err)
|
|
||||||
}
|
|
||||||
if err = conn.SetNoDelay(false); err != nil {
|
|
||||||
l.Infoln(err)
|
|
||||||
}
|
|
||||||
if err = conn.SetKeepAlivePeriod(60 * time.Second); err != nil {
|
|
||||||
l.Infoln(err)
|
|
||||||
}
|
|
||||||
if err = conn.SetKeepAlive(true); err != nil {
|
|
||||||
l.Infoln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
|
|
||||||
if s.cfg.Options().LimitBandwidthInLan {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
tcpaddr, ok := addr.(*net.TCPAddr)
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, lan := range lans {
|
|
||||||
if lan.Contains(tcpaddr.IP) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return !tcpaddr.IP.IsLoopback()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *connectionSvc) VerifyConfiguration(from, to config.Configuration) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool {
|
|
||||||
// We require a restart if a device as been removed.
|
|
||||||
|
|
||||||
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
|
|
||||||
for _, dev := range to.Devices {
|
|
||||||
newDevices[dev.DeviceID] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dev := range from.Devices {
|
|
||||||
if !newDevices[dev.DeviceID] {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
+17
-3
@@ -9,10 +9,24 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
debugNet = strings.Contains(os.Getenv("STTRACE"), "net") || os.Getenv("STTRACE") == "all"
|
l = logger.DefaultLogger.NewFacility("main", "Main package")
|
||||||
debugHTTP = strings.Contains(os.Getenv("STTRACE"), "http") || os.Getenv("STTRACE") == "all"
|
httpl = logger.DefaultLogger.NewFacility("http", "REST API")
|
||||||
debugSuture = strings.Contains(os.Getenv("STTRACE"), "suture") || os.Getenv("STTRACE") == "all"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
l.SetDebug("main", strings.Contains(os.Getenv("STTRACE"), "main") || os.Getenv("STTRACE") == "all")
|
||||||
|
l.SetDebug("http", strings.Contains(os.Getenv("STTRACE"), "http") || os.Getenv("STTRACE") == "all")
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldDebugMain() bool {
|
||||||
|
return l.ShouldDebug("main")
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldDebugHTTP() bool {
|
||||||
|
return l.ShouldDebug("http")
|
||||||
|
}
|
||||||
|
|||||||
+157
-68
@@ -20,65 +20,70 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/calmh/logger"
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
"github.com/syncthing/syncthing/lib/auto"
|
"github.com/syncthing/syncthing/lib/auto"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/discover"
|
"github.com/syncthing/syncthing/lib/discover"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/relay"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
|
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||||
"github.com/syncthing/syncthing/lib/upgrade"
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
"github.com/vitrun/qart/qr"
|
"github.com/vitrun/qart/qr"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type guiError struct {
|
|
||||||
Time time.Time `json:"time"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
configInSync = true
|
configInSync = true
|
||||||
guiErrors = []guiError{}
|
|
||||||
guiErrorsMut = sync.NewMutex()
|
|
||||||
startTime = time.Now()
|
startTime = time.Now()
|
||||||
)
|
)
|
||||||
|
|
||||||
type apiSvc struct {
|
type apiSvc struct {
|
||||||
id protocol.DeviceID
|
id protocol.DeviceID
|
||||||
cfg config.GUIConfiguration
|
cfg *config.Wrapper
|
||||||
assetDir string
|
assetDir string
|
||||||
model *model.Model
|
model *model.Model
|
||||||
|
eventSub *events.BufferedSubscription
|
||||||
|
discoverer *discover.CachingMux
|
||||||
|
relaySvc *relay.Svc
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
fss *folderSummarySvc
|
fss *folderSummarySvc
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
systemConfigMut sync.Mutex
|
systemConfigMut sync.Mutex
|
||||||
eventSub *events.BufferedSubscription
|
|
||||||
|
guiErrors *logger.Recorder
|
||||||
|
systemLog *logger.Recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir string, m *model.Model, eventSub *events.BufferedSubscription) (*apiSvc, error) {
|
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{
|
svc := &apiSvc{
|
||||||
id: id,
|
id: id,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
assetDir: assetDir,
|
assetDir: assetDir,
|
||||||
model: m,
|
model: m,
|
||||||
systemConfigMut: sync.NewMutex(),
|
|
||||||
eventSub: eventSub,
|
eventSub: eventSub,
|
||||||
|
discoverer: discoverer,
|
||||||
|
relaySvc: relaySvc,
|
||||||
|
systemConfigMut: sync.NewMutex(),
|
||||||
|
guiErrors: errors,
|
||||||
|
systemLog: systemLog,
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
svc.listener, err = svc.getListener(cfg)
|
svc.listener, err = svc.getListener(cfg.GUI())
|
||||||
return svc, err
|
return svc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) getListener(cfg config.GUIConfiguration) (net.Listener, error) {
|
func (s *apiSvc) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
|
||||||
cert, err := tls.LoadX509KeyPair(locations[locHTTPSCertFile], locations[locHTTPSKeyFile])
|
cert, err := tls.LoadX509KeyPair(locations[locHTTPSCertFile], locations[locHTTPSKeyFile])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Infoln("Loading HTTPS certificate:", err)
|
l.Infoln("Loading HTTPS certificate:", err)
|
||||||
@@ -92,7 +97,7 @@ func (s *apiSvc) getListener(cfg config.GUIConfiguration) (net.Listener, error)
|
|||||||
name = tlsDefaultCommonName
|
name = tlsDefaultCommonName
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err = newCertificate(locations[locHTTPSCertFile], locations[locHTTPSKeyFile], name)
|
cert, err = tlsutil.NewCertificate(locations[locHTTPSCertFile], locations[locHTTPSKeyFile], name, tlsRSABits)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -115,20 +120,18 @@ func (s *apiSvc) getListener(cfg config.GUIConfiguration) (net.Listener, error)
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
rawListener, err := net.Listen("tcp", cfg.Address)
|
rawListener, err := net.Listen("tcp", guiCfg.Address())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
listener := &DowngradingListener{rawListener, tlsCfg}
|
listener := &tlsutil.DowngradingListener{rawListener, tlsCfg}
|
||||||
return listener, nil
|
return listener, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) Serve() {
|
func (s *apiSvc) Serve() {
|
||||||
s.stop = make(chan struct{})
|
s.stop = make(chan struct{})
|
||||||
|
|
||||||
l.AddHandler(logger.LevelWarn, s.showGuiError)
|
|
||||||
|
|
||||||
// The GET handlers
|
// The GET handlers
|
||||||
getRestMux := http.NewServeMux()
|
getRestMux := http.NewServeMux()
|
||||||
getRestMux.HandleFunc("/rest/db/completion", s.getDBCompletion) // device folder
|
getRestMux.HandleFunc("/rest/db/completion", s.getDBCompletion) // device folder
|
||||||
@@ -153,6 +156,9 @@ func (s *apiSvc) Serve() {
|
|||||||
getRestMux.HandleFunc("/rest/system/status", s.getSystemStatus) // -
|
getRestMux.HandleFunc("/rest/system/status", s.getSystemStatus) // -
|
||||||
getRestMux.HandleFunc("/rest/system/upgrade", s.getSystemUpgrade) // -
|
getRestMux.HandleFunc("/rest/system/upgrade", s.getSystemUpgrade) // -
|
||||||
getRestMux.HandleFunc("/rest/system/version", s.getSystemVersion) // -
|
getRestMux.HandleFunc("/rest/system/version", s.getSystemVersion) // -
|
||||||
|
getRestMux.HandleFunc("/rest/system/debug", s.getSystemDebug) // -
|
||||||
|
getRestMux.HandleFunc("/rest/system/log", s.getSystemLog) // [since]
|
||||||
|
getRestMux.HandleFunc("/rest/system/log.txt", s.getSystemLogTxt) // [since]
|
||||||
|
|
||||||
// The POST handlers
|
// The POST handlers
|
||||||
postRestMux := http.NewServeMux()
|
postRestMux := http.NewServeMux()
|
||||||
@@ -161,7 +167,6 @@ func (s *apiSvc) Serve() {
|
|||||||
postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder
|
postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder
|
||||||
postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...] [delay]
|
postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...] [delay]
|
||||||
postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // <body>
|
postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // <body>
|
||||||
postRestMux.HandleFunc("/rest/system/discovery", s.postSystemDiscovery) // device addr
|
|
||||||
postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // <body>
|
postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // <body>
|
||||||
postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
|
postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
|
||||||
postRestMux.HandleFunc("/rest/system/ping", s.restPing) // -
|
postRestMux.HandleFunc("/rest/system/ping", s.restPing) // -
|
||||||
@@ -169,6 +174,9 @@ func (s *apiSvc) Serve() {
|
|||||||
postRestMux.HandleFunc("/rest/system/restart", s.postSystemRestart) // -
|
postRestMux.HandleFunc("/rest/system/restart", s.postSystemRestart) // -
|
||||||
postRestMux.HandleFunc("/rest/system/shutdown", s.postSystemShutdown) // -
|
postRestMux.HandleFunc("/rest/system/shutdown", s.postSystemShutdown) // -
|
||||||
postRestMux.HandleFunc("/rest/system/upgrade", s.postSystemUpgrade) // -
|
postRestMux.HandleFunc("/rest/system/upgrade", s.postSystemUpgrade) // -
|
||||||
|
postRestMux.HandleFunc("/rest/system/pause", s.postSystemPause) // device
|
||||||
|
postRestMux.HandleFunc("/rest/system/resume", s.postSystemResume) // device
|
||||||
|
postRestMux.HandleFunc("/rest/system/debug", s.postSystemDebug) // [enable] [disable]
|
||||||
|
|
||||||
// Debug endpoints, not for general use
|
// Debug endpoints, not for general use
|
||||||
getRestMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion)
|
getRestMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion)
|
||||||
@@ -188,37 +196,38 @@ func (s *apiSvc) Serve() {
|
|||||||
assets: auto.Assets(),
|
assets: auto.Assets(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
guiCfg := s.cfg.GUI()
|
||||||
|
|
||||||
// Wrap everything in CSRF protection. The /rest prefix should be
|
// Wrap everything in CSRF protection. The /rest prefix should be
|
||||||
// protected, other requests will grant cookies.
|
// protected, other requests will grant cookies.
|
||||||
handler := csrfMiddleware(s.id.String()[:5], "/rest", s.cfg.APIKey, mux)
|
handler := csrfMiddleware(s.id.String()[:5], "/rest", guiCfg.APIKey(), mux)
|
||||||
|
|
||||||
// Add our version and ID as a header to responses
|
// Add our version and ID as a header to responses
|
||||||
handler = withDetailsMiddleware(s.id, handler)
|
handler = withDetailsMiddleware(s.id, handler)
|
||||||
|
|
||||||
// Wrap everything in basic auth, if user/password is set.
|
// Wrap everything in basic auth, if user/password is set.
|
||||||
if len(s.cfg.User) > 0 && len(s.cfg.Password) > 0 {
|
if len(guiCfg.User) > 0 && len(guiCfg.Password) > 0 {
|
||||||
handler = basicAuthAndSessionMiddleware("sessionid-"+s.id.String()[:5], s.cfg, handler)
|
handler = basicAuthAndSessionMiddleware("sessionid-"+s.id.String()[:5], guiCfg, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to HTTPS if we are supposed to
|
// Redirect to HTTPS if we are supposed to
|
||||||
if s.cfg.UseTLS {
|
if guiCfg.UseTLS() {
|
||||||
handler = redirectToHTTPSMiddleware(handler)
|
handler = redirectToHTTPSMiddleware(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
if debugHTTP {
|
|
||||||
handler = debugMiddleware(handler)
|
handler = debugMiddleware(handler)
|
||||||
}
|
|
||||||
|
|
||||||
srv := http.Server{
|
srv := http.Server{
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
ReadTimeout: 10 * time.Second,
|
ReadTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.fss = newFolderSummarySvc(s.model)
|
s.fss = newFolderSummarySvc(s.cfg, s.model)
|
||||||
defer s.fss.Stop()
|
defer s.fss.Stop()
|
||||||
s.fss.ServeBackground()
|
s.fss.ServeBackground()
|
||||||
|
|
||||||
l.Infoln("API listening on", s.listener.Addr())
|
l.Infoln("API listening on", s.listener.Addr())
|
||||||
|
l.Infoln("GUI URL is", guiCfg.URL())
|
||||||
err := srv.Serve(s.listener)
|
err := srv.Serve(s.listener)
|
||||||
|
|
||||||
// The return could be due to an intentional close. Wait for the stop
|
// The return could be due to an intentional close. Wait for the stop
|
||||||
@@ -266,7 +275,6 @@ func (s *apiSvc) CommitConfiguration(from, to config.Configuration) bool {
|
|||||||
// method.
|
// method.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
s.cfg = to.GUI
|
|
||||||
|
|
||||||
close(s.stop)
|
close(s.stop)
|
||||||
|
|
||||||
@@ -362,6 +370,36 @@ 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")
|
||||||
|
names := l.Facilities()
|
||||||
|
enabled := l.FacilityDebugging()
|
||||||
|
sort.Strings(enabled)
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"facilities": names,
|
||||||
|
"enabled": enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *apiSvc) 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 == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.SetDebug(f, true)
|
||||||
|
l.Infof("Enabled debug data for %q", f)
|
||||||
|
}
|
||||||
|
for _, f := range strings.Split(q.Get("disable"), ",") {
|
||||||
|
if f == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.SetDebug(f, false)
|
||||||
|
l.Infof("Disabled debug data for %q", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *apiSvc) getDBBrowse(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) getDBBrowse(w http.ResponseWriter, r *http.Request) {
|
||||||
qs := r.URL.Query()
|
qs := r.URL.Query()
|
||||||
folder := qs.Get("folder")
|
folder := qs.Get("folder")
|
||||||
@@ -402,12 +440,12 @@ func (s *apiSvc) getDBCompletion(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *apiSvc) getDBStatus(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) getDBStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
qs := r.URL.Query()
|
qs := r.URL.Query()
|
||||||
folder := qs.Get("folder")
|
folder := qs.Get("folder")
|
||||||
res := folderSummary(s.model, folder)
|
res := folderSummary(s.cfg, s.model, folder)
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(res)
|
json.NewEncoder(w).Encode(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func folderSummary(m *model.Model, folder string) map[string]interface{} {
|
func folderSummary(cfg *config.Wrapper, m *model.Model, folder string) map[string]interface{} {
|
||||||
var res = make(map[string]interface{})
|
var res = make(map[string]interface{})
|
||||||
|
|
||||||
res["invalid"] = cfg.Folders()[folder].Invalid
|
res["invalid"] = cfg.Folders()[folder].Invalid
|
||||||
@@ -517,7 +555,7 @@ func (s *apiSvc) getDBFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *apiSvc) getSystemConfig(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) getSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(cfg.Raw())
|
json.NewEncoder(w).Encode(s.cfg.Raw())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -532,7 +570,7 @@ func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if to.GUI.Password != cfg.GUI().Password {
|
if to.GUI.Password != s.cfg.GUI().Password {
|
||||||
if to.GUI.Password != "" {
|
if to.GUI.Password != "" {
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(to.GUI.Password), 0)
|
hash, err := bcrypt.GenerateFromPassword([]byte(to.GUI.Password), 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -547,7 +585,7 @@ func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Fixup usage reporting settings
|
// Fixup usage reporting settings
|
||||||
|
|
||||||
if curAcc := cfg.Options().URAccepted; to.Options.URAccepted > curAcc {
|
if curAcc := s.cfg.Options().URAccepted; to.Options.URAccepted > curAcc {
|
||||||
// UR was enabled
|
// UR was enabled
|
||||||
to.Options.URAccepted = usageReportVersion
|
to.Options.URAccepted = usageReportVersion
|
||||||
to.Options.URUniqueID = randomString(8)
|
to.Options.URUniqueID = randomString(8)
|
||||||
@@ -559,9 +597,9 @@ func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Activate and save
|
// Activate and save
|
||||||
|
|
||||||
resp := cfg.Replace(to)
|
resp := s.cfg.Replace(to)
|
||||||
configInSync = !resp.RequiresRestart
|
configInSync = !resp.RequiresRestart
|
||||||
cfg.Save()
|
s.cfg.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -579,7 +617,7 @@ func (s *apiSvc) postSystemReset(w http.ResponseWriter, r *http.Request) {
|
|||||||
folder := qs.Get("folder")
|
folder := qs.Get("folder")
|
||||||
|
|
||||||
if len(folder) > 0 {
|
if len(folder) > 0 {
|
||||||
if _, ok := cfg.Folders()[folder]; !ok {
|
if _, ok := s.cfg.Folders()[folder]; !ok {
|
||||||
http.Error(w, "Invalid folder ID", 500)
|
http.Error(w, "Invalid folder ID", 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -587,7 +625,7 @@ func (s *apiSvc) postSystemReset(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if len(folder) == 0 {
|
if len(folder) == 0 {
|
||||||
// Reset all folders.
|
// Reset all folders.
|
||||||
for folder := range cfg.Folders() {
|
for folder := range s.cfg.Folders() {
|
||||||
s.model.ResetFolder(folder)
|
s.model.ResetFolder(folder)
|
||||||
}
|
}
|
||||||
s.flushResponse(`{"ok": "resetting database"}`, w)
|
s.flushResponse(`{"ok": "resetting database"}`, w)
|
||||||
@@ -625,8 +663,30 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
res["alloc"] = m.Alloc
|
res["alloc"] = m.Alloc
|
||||||
res["sys"] = m.Sys - m.HeapReleased
|
res["sys"] = m.Sys - m.HeapReleased
|
||||||
res["tilde"] = tilde
|
res["tilde"] = tilde
|
||||||
if cfg.Options().GlobalAnnEnabled && discoverer != nil {
|
if s.cfg.Options().LocalAnnEnabled || s.cfg.Options().GlobalAnnEnabled {
|
||||||
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
|
res["discoveryEnabled"] = true
|
||||||
|
discoErrors := make(map[string]string)
|
||||||
|
discoMethods := 0
|
||||||
|
for disco, err := range s.discoverer.ChildErrors() {
|
||||||
|
discoMethods++
|
||||||
|
if err != nil {
|
||||||
|
discoErrors[disco] = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res["discoveryMethods"] = discoMethods
|
||||||
|
res["discoveryErrors"] = discoErrors
|
||||||
|
}
|
||||||
|
if s.relaySvc != 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)
|
||||||
|
relayClientStatus[relay] = ok
|
||||||
|
relayClientLatency[relay] = int(latency / time.Millisecond)
|
||||||
|
}
|
||||||
|
res["relayClientStatus"] = relayClientStatus
|
||||||
|
res["relayClientLatency"] = relayClientLatency
|
||||||
}
|
}
|
||||||
cpuUsageLock.RLock()
|
cpuUsageLock.RLock()
|
||||||
var cpusum float64
|
var cpusum float64
|
||||||
@@ -637,6 +697,7 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
res["cpuPercent"] = cpusum / float64(len(cpuUsagePercent)) / float64(runtime.NumCPU())
|
res["cpuPercent"] = cpusum / float64(len(cpuUsagePercent)) / float64(runtime.NumCPU())
|
||||||
res["pathSeparator"] = string(filepath.Separator)
|
res["pathSeparator"] = string(filepath.Separator)
|
||||||
res["uptime"] = int(time.Since(startTime).Seconds())
|
res["uptime"] = int(time.Since(startTime).Seconds())
|
||||||
|
res["startTime"] = startTime
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(res)
|
json.NewEncoder(w).Encode(res)
|
||||||
@@ -644,51 +705,53 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *apiSvc) getSystemError(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) getSystemError(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
guiErrorsMut.Lock()
|
json.NewEncoder(w).Encode(map[string][]logger.Line{
|
||||||
json.NewEncoder(w).Encode(map[string][]guiError{"errors": guiErrors})
|
"errors": s.guiErrors.Since(time.Time{}),
|
||||||
guiErrorsMut.Unlock()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) postSystemError(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) postSystemError(w http.ResponseWriter, r *http.Request) {
|
||||||
bs, _ := ioutil.ReadAll(r.Body)
|
bs, _ := ioutil.ReadAll(r.Body)
|
||||||
r.Body.Close()
|
r.Body.Close()
|
||||||
s.showGuiError(0, string(bs))
|
l.Warnln(string(bs))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) postSystemErrorClear(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) postSystemErrorClear(w http.ResponseWriter, r *http.Request) {
|
||||||
guiErrorsMut.Lock()
|
s.guiErrors.Clear()
|
||||||
guiErrors = []guiError{}
|
|
||||||
guiErrorsMut.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) showGuiError(l logger.LogLevel, err string) {
|
func (s *apiSvc) getSystemLog(w http.ResponseWriter, r *http.Request) {
|
||||||
guiErrorsMut.Lock()
|
q := r.URL.Query()
|
||||||
guiErrors = append(guiErrors, guiError{time.Now(), err})
|
since, err := time.Parse(time.RFC3339, q.Get("since"))
|
||||||
if len(guiErrors) > 5 {
|
l.Debugln(err)
|
||||||
guiErrors = guiErrors[len(guiErrors)-5:]
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
}
|
|
||||||
guiErrorsMut.Unlock()
|
json.NewEncoder(w).Encode(map[string][]logger.Line{
|
||||||
|
"messages": s.systemLog.Since(since),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) postSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
|
||||||
var qs = r.URL.Query()
|
q := r.URL.Query()
|
||||||
var device = qs.Get("device")
|
since, err := time.Parse(time.RFC3339, q.Get("since"))
|
||||||
var addr = qs.Get("addr")
|
l.Debugln(err)
|
||||||
if len(device) != 0 && len(addr) != 0 && discoverer != nil {
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
discoverer.Hint(device, []string{addr})
|
|
||||||
|
for _, line := range s.systemLog.Since(since) {
|
||||||
|
fmt.Fprintf(w, "%s: %s\n", line.When.Format(time.RFC3339), line.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
devices := map[string][]discover.CacheEntry{}
|
devices := make(map[string]discover.CacheEntry)
|
||||||
|
|
||||||
if discoverer != nil {
|
if s.discoverer != nil {
|
||||||
// Device ids can't be marshalled as keys so we need to manually
|
// Device ids can't be marshalled as keys so we need to manually
|
||||||
// rebuild this map using strings. Discoverer may be nil if discovery
|
// rebuild this map using strings. Discoverer may be nil if discovery
|
||||||
// has not started yet.
|
// has not started yet.
|
||||||
for device, entries := range discoverer.All() {
|
for device, entry := range s.discoverer.Cache() {
|
||||||
devices[device.String()] = entries
|
devices[device.String()] = entry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,7 +760,7 @@ func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (s *apiSvc) getReport(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) getReport(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(reportData(s.model))
|
json.NewEncoder(w).Encode(reportData(s.cfg, s.model))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) getDBIgnores(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) getDBIgnores(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -766,7 +829,7 @@ func (s *apiSvc) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, upgrade.ErrUpgradeUnsupported.Error(), 500)
|
http.Error(w, upgrade.ErrUpgradeUnsupported.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rel, err := upgrade.LatestRelease(Version)
|
rel, err := upgrade.LatestRelease(s.cfg.Options().ReleasesURL, Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
@@ -809,7 +872,7 @@ func (s *apiSvc) getLang(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||||
rel, err := upgrade.LatestRelease(Version)
|
rel, err := upgrade.LatestRelease(s.cfg.Options().ReleasesURL, Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warnln("getting latest release:", err)
|
l.Warnln("getting latest release:", err)
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
@@ -830,6 +893,32 @@ func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *apiSvc) postSystemPause(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var qs = r.URL.Query()
|
||||||
|
var deviceStr = qs.Get("device")
|
||||||
|
|
||||||
|
device, err := protocol.DeviceIDFromString(deviceStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.model.PauseDevice(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *apiSvc) postSystemResume(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var qs = r.URL.Query()
|
||||||
|
var deviceStr = qs.Get("device")
|
||||||
|
|
||||||
|
device, err := protocol.DeviceIDFromString(deviceStr)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.model.ResumeDevice(device)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
|
func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
|
||||||
qs := r.URL.Query()
|
qs := r.URL.Query()
|
||||||
folder := qs.Get("folder")
|
folder := qs.Get("folder")
|
||||||
@@ -881,7 +970,7 @@ func (s *apiSvc) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
|
|||||||
tot := map[string]float64{}
|
tot := map[string]float64{}
|
||||||
count := map[string]float64{}
|
count := map[string]float64{}
|
||||||
|
|
||||||
for _, folder := range cfg.Folders() {
|
for _, folder := range s.cfg.Folders() {
|
||||||
for _, device := range folder.DeviceIDs() {
|
for _, device := range folder.DeviceIDs() {
|
||||||
deviceStr := device.String()
|
deviceStr := device.String()
|
||||||
if s.model.ConnectedTo(device) {
|
if s.model.ConnectedTo(device) {
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguration, next http.Handler) http.Handler {
|
func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguration, next http.Handler) http.Handler {
|
||||||
|
apiKey := cfg.APIKey()
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if cfg.APIKey != "" && r.Header.Get("X-API-Key") == cfg.APIKey {
|
if apiKey != "" && r.Header.Get("X-API-Key") == apiKey {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -43,9 +43,7 @@ func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguratio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if debugHTTP {
|
httpl.Debugln("Sessionless HTTP request with authentication; this is expensive.")
|
||||||
l.Debugln("Sessionless HTTP request with authentication; this is expensive.")
|
|
||||||
}
|
|
||||||
|
|
||||||
error := func() {
|
error := func() {
|
||||||
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
|
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
|
||||||
|
|||||||
+219
-196
@@ -7,6 +7,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -21,31 +22,34 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/calmh/logger"
|
|
||||||
"github.com/juju/ratelimit"
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/connections"
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/discover"
|
"github.com/syncthing/syncthing/lib/discover"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/relay"
|
||||||
"github.com/syncthing/syncthing/lib/symlinks"
|
"github.com/syncthing/syncthing/lib/symlinks"
|
||||||
|
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||||
"github.com/syncthing/syncthing/lib/upgrade"
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
|
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||||
"github.com/thejerf/suture"
|
"github.com/thejerf/suture"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = "unknown-dev"
|
Version = "unknown-dev"
|
||||||
Codename = "Aluminium Ant"
|
Codename = "Beryllium Bedbug"
|
||||||
BuildEnv = "default"
|
BuildEnv = "default"
|
||||||
BuildStamp = "0"
|
BuildStamp = "0"
|
||||||
BuildDate time.Time
|
BuildDate time.Time
|
||||||
@@ -66,10 +70,20 @@ const (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
bepProtocolName = "bep/1.0"
|
bepProtocolName = "bep/1.0"
|
||||||
|
tlsDefaultCommonName = "syncthing"
|
||||||
|
tlsRSABits = 3072
|
||||||
pingEventInterval = time.Minute
|
pingEventInterval = time.Minute
|
||||||
|
maxSystemErrors = 5
|
||||||
|
initialSystemLog = 10
|
||||||
|
maxSystemLog = 250
|
||||||
)
|
)
|
||||||
|
|
||||||
var l = logger.DefaultLogger
|
// The discovery results are sorted by their source priority.
|
||||||
|
const (
|
||||||
|
ipv6LocalDiscoveryPriority = iota
|
||||||
|
ipv4LocalDiscoveryPriority
|
||||||
|
globalDiscoveryPriority
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if Version != "unknown-dev" {
|
if Version != "unknown-dev" {
|
||||||
@@ -102,14 +116,10 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfg *config.Wrapper
|
|
||||||
myID protocol.DeviceID
|
myID protocol.DeviceID
|
||||||
confDir string
|
confDir string
|
||||||
logFlags = log.Ltime
|
logFlags = log.Ltime
|
||||||
writeRateLimit *ratelimit.Bucket
|
|
||||||
readRateLimit *ratelimit.Bucket
|
|
||||||
stop = make(chan int)
|
stop = make(chan int)
|
||||||
discoverer *discover.Discoverer
|
|
||||||
cert tls.Certificate
|
cert tls.Certificate
|
||||||
lans []*net.IPNet
|
lans []*net.IPNet
|
||||||
)
|
)
|
||||||
@@ -141,25 +151,11 @@ Development Settings
|
|||||||
The following environment variables modify syncthing's behavior in ways that
|
The following environment variables modify syncthing's behavior in ways that
|
||||||
are mostly useful for developers. Use with care.
|
are mostly useful for developers. Use with care.
|
||||||
|
|
||||||
STGUIASSETS Directory to load GUI assets from. Overrides compiled in assets.
|
STGUIASSETS Directory to load GUI assets from. Overrides compiled in
|
||||||
|
assets.
|
||||||
|
|
||||||
STTRACE A comma separated string of facilities to trace. The valid
|
STTRACE A comma separated string of facilities to trace. The valid
|
||||||
facility strings are:
|
facility strings listed below.
|
||||||
|
|
||||||
- "beacon" (the beacon package)
|
|
||||||
- "discover" (the discover package)
|
|
||||||
- "events" (the events package)
|
|
||||||
- "files" (the files package)
|
|
||||||
- "http" (the main package; HTTP requests)
|
|
||||||
- "locks" (the sync package; trace long held locks)
|
|
||||||
- "net" (the main package; connections & network messages)
|
|
||||||
- "model" (the model package)
|
|
||||||
- "scanner" (the scanner package)
|
|
||||||
- "stats" (the stats package)
|
|
||||||
- "suture" (the suture package; service management)
|
|
||||||
- "upnp" (the upnp package)
|
|
||||||
- "xdr" (the xdr package)
|
|
||||||
- "all" (all of the above)
|
|
||||||
|
|
||||||
STPROFILER Set to a listen address such as "127.0.0.1:9090" to start the
|
STPROFILER Set to a listen address such as "127.0.0.1:9090" to start the
|
||||||
profiler with HTTP access.
|
profiler with HTTP access.
|
||||||
@@ -182,7 +178,15 @@ are mostly useful for developers. Use with care.
|
|||||||
|
|
||||||
GOGC Percentage of heap growth at which to trigger GC. Default is
|
GOGC Percentage of heap growth at which to trigger GC. Default is
|
||||||
100. Lower numbers keep peak memory usage down, at the price
|
100. Lower numbers keep peak memory usage down, at the price
|
||||||
of CPU usage (ie. performance).`
|
of CPU usage (ie. performance).
|
||||||
|
|
||||||
|
|
||||||
|
Debugging Facilities
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The following are valid values for the STTRACE variable:
|
||||||
|
|
||||||
|
%s`
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command line and environment options
|
// Command line and environment options
|
||||||
@@ -198,11 +202,9 @@ var (
|
|||||||
logFile string
|
logFile string
|
||||||
auditEnabled bool
|
auditEnabled bool
|
||||||
verbose bool
|
verbose bool
|
||||||
|
paused bool
|
||||||
noRestart = os.Getenv("STNORESTART") != ""
|
noRestart = os.Getenv("STNORESTART") != ""
|
||||||
noUpgrade = os.Getenv("STNOUPGRADE") != ""
|
noUpgrade = os.Getenv("STNOUPGRADE") != ""
|
||||||
guiAddress = os.Getenv("STGUIADDRESS") // legacy
|
|
||||||
guiAuthentication = os.Getenv("STGUIAUTH") // legacy
|
|
||||||
guiAPIKey = os.Getenv("STGUIAPIKEY") // legacy
|
|
||||||
profiler = os.Getenv("STPROFILER")
|
profiler = os.Getenv("STPROFILER")
|
||||||
guiAssets = os.Getenv("STGUIASSETS")
|
guiAssets = os.Getenv("STGUIASSETS")
|
||||||
cpuProfile = os.Getenv("STCPUPROFILE") != ""
|
cpuProfile = os.Getenv("STCPUPROFILE") != ""
|
||||||
@@ -214,16 +216,17 @@ func main() {
|
|||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// On Windows, we use a log file by default. Setting the -logfile flag
|
// On Windows, we use a log file by default. Setting the -logfile flag
|
||||||
// to "-" disables this behavior.
|
// to "-" disables this behavior.
|
||||||
|
|
||||||
flag.StringVar(&logFile, "logfile", "", "Log file name (use \"-\" for stdout)")
|
flag.StringVar(&logFile, "logfile", "", "Log file name (use \"-\" for stdout)")
|
||||||
|
|
||||||
// We also add an option to hide the console window
|
// We also add an option to hide the console window
|
||||||
flag.BoolVar(&noConsole, "no-console", false, "Hide console window")
|
flag.BoolVar(&noConsole, "no-console", false, "Hide console window")
|
||||||
|
} else {
|
||||||
|
flag.StringVar(&logFile, "logfile", "-", "Log file name (use \"-\" for stdout)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var guiAddress, guiAPIKey string
|
||||||
flag.StringVar(&generateDir, "generate", "", "Generate key and config in specified dir, then exit")
|
flag.StringVar(&generateDir, "generate", "", "Generate key and config in specified dir, then exit")
|
||||||
flag.StringVar(&guiAddress, "gui-address", guiAddress, "Override GUI address")
|
flag.StringVar(&guiAddress, "gui-address", guiAddress, "Override GUI address")
|
||||||
flag.StringVar(&guiAuthentication, "gui-authentication", guiAuthentication, "Override GUI authentication; username:password")
|
|
||||||
flag.StringVar(&guiAPIKey, "gui-apikey", guiAPIKey, "Override GUI API key")
|
flag.StringVar(&guiAPIKey, "gui-apikey", guiAPIKey, "Override GUI API key")
|
||||||
flag.StringVar(&confDir, "home", "", "Set configuration directory")
|
flag.StringVar(&confDir, "home", "", "Set configuration directory")
|
||||||
flag.IntVar(&logFlags, "logflags", logFlags, "Select information in log line prefix")
|
flag.IntVar(&logFlags, "logflags", logFlags, "Select information in log line prefix")
|
||||||
@@ -236,10 +239,21 @@ func main() {
|
|||||||
flag.StringVar(&upgradeTo, "upgrade-to", upgradeTo, "Force upgrade directly from specified URL")
|
flag.StringVar(&upgradeTo, "upgrade-to", upgradeTo, "Force upgrade directly from specified URL")
|
||||||
flag.BoolVar(&auditEnabled, "audit", false, "Write events to audit file")
|
flag.BoolVar(&auditEnabled, "audit", false, "Write events to audit file")
|
||||||
flag.BoolVar(&verbose, "verbose", false, "Print verbose log output")
|
flag.BoolVar(&verbose, "verbose", false, "Print verbose log output")
|
||||||
|
flag.BoolVar(&paused, "paused", false, "Start with all devices paused")
|
||||||
|
|
||||||
flag.Usage = usageFor(flag.CommandLine, usage, fmt.Sprintf(extraUsage, baseDirs["config"]))
|
longUsage := fmt.Sprintf(extraUsage, baseDirs["config"], debugFacilities())
|
||||||
|
flag.Usage = usageFor(flag.CommandLine, usage, longUsage)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if guiAddress != "" {
|
||||||
|
// The config picks this up from the environment.
|
||||||
|
os.Setenv("STGUIADDRESS", guiAddress)
|
||||||
|
}
|
||||||
|
if guiAPIKey != "" {
|
||||||
|
// The config picks this up from the environment.
|
||||||
|
os.Setenv("STGUIAPIKEY", guiAPIKey)
|
||||||
|
}
|
||||||
|
|
||||||
if noConsole {
|
if noConsole {
|
||||||
osutil.HideConsole()
|
osutil.HideConsole()
|
||||||
}
|
}
|
||||||
@@ -257,14 +271,9 @@ func main() {
|
|||||||
guiAssets = locations[locGUIAssets]
|
guiAssets = locations[locGUIAssets]
|
||||||
}
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
if logFile == "" {
|
if logFile == "" {
|
||||||
// Use the default log file location
|
// Use the default log file location
|
||||||
logFile = locations[locLogFile]
|
logFile = locations[locLogFile]
|
||||||
} else if logFile == "-" {
|
|
||||||
// Don't use a logFile
|
|
||||||
logFile = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if showVersion {
|
if showVersion {
|
||||||
@@ -297,10 +306,13 @@ func main() {
|
|||||||
l.Warnln("Key exists; will not overwrite.")
|
l.Warnln("Key exists; will not overwrite.")
|
||||||
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
|
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
|
||||||
} else {
|
} else {
|
||||||
cert, err = newCertificate(certFile, keyFile, tlsDefaultCommonName)
|
cert, err = tlsutil.NewCertificate(certFile, keyFile, tlsDefaultCommonName, tlsRSABits)
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("Create certificate:", err)
|
||||||
|
}
|
||||||
myID = protocol.NewDeviceID(cert.Certificate[0])
|
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("load cert:", err)
|
l.Fatalln("Load certificate:", err)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
|
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
|
||||||
@@ -340,7 +352,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if doUpgrade || doUpgradeCheck {
|
if doUpgrade || doUpgradeCheck {
|
||||||
rel, err := upgrade.LatestRelease(Version)
|
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 {
|
if err != nil {
|
||||||
l.Fatalln("Upgrade:", err) // exits 1
|
l.Fatalln("Upgrade:", err) // exits 1
|
||||||
}
|
}
|
||||||
@@ -387,19 +403,36 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func debugFacilities() string {
|
||||||
|
facilities := l.Facilities()
|
||||||
|
|
||||||
|
// Get a sorted list of names
|
||||||
|
var names []string
|
||||||
|
maxLen := 0
|
||||||
|
for name := range facilities {
|
||||||
|
names = append(names, name)
|
||||||
|
if len(name) > maxLen {
|
||||||
|
maxLen = len(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
// Format the choices
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
for _, name := range names {
|
||||||
|
fmt.Fprintf(b, " %-*s - %s\n", maxLen, name, facilities[name])
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
func upgradeViaRest() error {
|
func upgradeViaRest() error {
|
||||||
cfg, err := config.Load(locations[locConfigFile], protocol.LocalDeviceID)
|
cfg, err := config.Load(locations[locConfigFile], protocol.LocalDeviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
target := cfg.GUI().Address
|
target := cfg.GUI().URL()
|
||||||
if cfg.GUI().UseTLS {
|
|
||||||
target = "https://" + target
|
|
||||||
} else {
|
|
||||||
target = "http://" + target
|
|
||||||
}
|
|
||||||
r, _ := http.NewRequest("POST", target+"/rest/system/upgrade", nil)
|
r, _ := http.NewRequest("POST", target+"/rest/system/upgrade", nil)
|
||||||
r.Header.Set("X-API-Key", cfg.GUI().APIKey)
|
r.Header.Set("X-API-Key", cfg.GUI().APIKey())
|
||||||
|
|
||||||
tr := &http.Transport{
|
tr := &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
@@ -429,9 +462,7 @@ func syncthingMain() {
|
|||||||
// We want any logging it does to go through our log system.
|
// We want any logging it does to go through our log system.
|
||||||
mainSvc := suture.New("main", suture.Spec{
|
mainSvc := suture.New("main", suture.Spec{
|
||||||
Log: func(line string) {
|
Log: func(line string) {
|
||||||
if debugSuture {
|
|
||||||
l.Debugln(line)
|
l.Debugln(line)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
mainSvc.ServeBackground()
|
mainSvc.ServeBackground()
|
||||||
@@ -448,6 +479,9 @@ func syncthingMain() {
|
|||||||
mainSvc.Add(newVerboseSvc())
|
mainSvc.Add(newVerboseSvc())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errors := logger.NewRecorder(l, logger.LevelWarn, maxSystemErrors, 0)
|
||||||
|
systemLog := logger.NewRecorder(l, logger.LevelDebug, maxSystemLog, initialSystemLog)
|
||||||
|
|
||||||
// Event subscription for the API; must start early to catch the early events.
|
// Event subscription for the API; must start early to catch the early events.
|
||||||
apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents), 1000)
|
apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents), 1000)
|
||||||
|
|
||||||
@@ -463,9 +497,10 @@ func syncthingMain() {
|
|||||||
// Ensure that that we have a certificate and key.
|
// Ensure that that we have a certificate and key.
|
||||||
cert, err := tls.LoadX509KeyPair(locations[locCertFile], locations[locKeyFile])
|
cert, err := tls.LoadX509KeyPair(locations[locCertFile], locations[locKeyFile])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cert, err = newCertificate(locations[locCertFile], locations[locKeyFile], tlsDefaultCommonName)
|
l.Infof("Generating RSA key and certificate for %s...", tlsDefaultCommonName)
|
||||||
|
cert, err = tlsutil.NewCertificate(locations[locCertFile], locations[locKeyFile], tlsDefaultCommonName, tlsRSABits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("load cert:", err)
|
l.Fatalln(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,33 +525,21 @@ func syncthingMain() {
|
|||||||
|
|
||||||
cfgFile := locations[locConfigFile]
|
cfgFile := locations[locConfigFile]
|
||||||
|
|
||||||
var myName string
|
|
||||||
|
|
||||||
// Load the configuration file, if it exists.
|
// Load the configuration file, if it exists.
|
||||||
// If it does not, create a template.
|
// If it does not, create a template.
|
||||||
|
|
||||||
if info, err := os.Stat(cfgFile); err == nil {
|
cfg, myName, err := loadConfig(cfgFile)
|
||||||
if !info.Mode().IsRegular() {
|
if err != nil {
|
||||||
l.Fatalln("Config file is not a file?")
|
if os.IsNotExist(err) {
|
||||||
}
|
|
||||||
cfg, err = config.Load(cfgFile, myID)
|
|
||||||
if err == nil {
|
|
||||||
myCfg := cfg.Devices()[myID]
|
|
||||||
if myCfg.Name == "" {
|
|
||||||
myName, _ = os.Hostname()
|
|
||||||
} else {
|
|
||||||
myName = myCfg.Name
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
l.Fatalln("Configuration:", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
l.Infoln("No config file; starting with empty defaults")
|
l.Infoln("No config file; starting with empty defaults")
|
||||||
myName, _ = os.Hostname()
|
myName, _ = os.Hostname()
|
||||||
newCfg := defaultConfig(myName)
|
newCfg := defaultConfig(myName)
|
||||||
cfg = config.Wrap(cfgFile, newCfg)
|
cfg = config.Wrap(cfgFile, newCfg)
|
||||||
cfg.Save()
|
cfg.Save()
|
||||||
l.Infof("Edit %s to taste or use the GUI\n", cfgFile)
|
l.Infof("Edit %s to taste or use the GUI\n", cfgFile)
|
||||||
|
} else {
|
||||||
|
l.Fatalln("Loading config:", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Raw().OriginalVersion != config.CurrentVersion {
|
if cfg.Raw().OriginalVersion != config.CurrentVersion {
|
||||||
@@ -571,29 +594,28 @@ func syncthingMain() {
|
|||||||
symlinks.Supported = false
|
symlinks.Supported = false
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol.PingTimeout = time.Duration(opts.PingTimeoutS) * time.Second
|
|
||||||
protocol.PingIdleTime = time.Duration(opts.PingIdleTimeS) * time.Second
|
|
||||||
|
|
||||||
if opts.MaxSendKbps > 0 {
|
|
||||||
writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*opts.MaxSendKbps), int64(5*1000*opts.MaxSendKbps))
|
|
||||||
}
|
|
||||||
if opts.MaxRecvKbps > 0 {
|
|
||||||
readRateLimit = ratelimit.NewBucketWithRate(float64(1000*opts.MaxRecvKbps), int64(5*1000*opts.MaxRecvKbps))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.MaxRecvKbps > 0 || opts.MaxSendKbps > 0) && !opts.LimitBandwidthInLan {
|
if (opts.MaxRecvKbps > 0 || opts.MaxSendKbps > 0) && !opts.LimitBandwidthInLan {
|
||||||
lans, _ = osutil.GetLans()
|
lans, _ = osutil.GetLans()
|
||||||
networks := make([]string, 0, len(lans))
|
networks := make([]string, 0, len(lans))
|
||||||
for _, lan := range lans {
|
for _, lan := range lans {
|
||||||
networks = append(networks, lan.String())
|
networks = append(networks, lan.String())
|
||||||
}
|
}
|
||||||
|
for _, lan := range opts.AlwaysLocalNets {
|
||||||
|
_, ipnet, err := net.ParseCIDR(lan)
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln("Network", lan, "is malformed:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
networks = append(networks, ipnet.String())
|
||||||
|
}
|
||||||
l.Infoln("Local networks:", strings.Join(networks, ", "))
|
l.Infoln("Local networks:", strings.Join(networks, ", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
dbFile := locations[locDatabase]
|
dbFile := locations[locDatabase]
|
||||||
ldb, err := leveldb.OpenFile(dbFile, dbOpts())
|
dbOpts := dbOpts(cfg)
|
||||||
|
ldb, err := leveldb.OpenFile(dbFile, dbOpts)
|
||||||
if leveldbIsCorrupted(err) {
|
if leveldbIsCorrupted(err) {
|
||||||
ldb, err = leveldb.RecoverFile(dbFile, dbOpts())
|
ldb, err = leveldb.RecoverFile(dbFile, dbOpts)
|
||||||
}
|
}
|
||||||
if leveldbIsCorrupted(err) {
|
if leveldbIsCorrupted(err) {
|
||||||
// The database is corrupted, and we've tried to recover it but it
|
// The database is corrupted, and we've tried to recover it but it
|
||||||
@@ -603,7 +625,7 @@ func syncthingMain() {
|
|||||||
if err := resetDB(); err != nil {
|
if err := resetDB(); err != nil {
|
||||||
l.Fatalln("Remove database:", err)
|
l.Fatalln("Remove database:", err)
|
||||||
}
|
}
|
||||||
ldb, err = leveldb.OpenFile(dbFile, dbOpts())
|
ldb, err = leveldb.OpenFile(dbFile, dbOpts)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
|
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
|
||||||
@@ -627,7 +649,13 @@ func syncthingMain() {
|
|||||||
m.StartDeadlockDetector(time.Duration(it) * time.Second)
|
m.StartDeadlockDetector(time.Duration(it) * time.Second)
|
||||||
}
|
}
|
||||||
} else if !IsRelease || IsBeta {
|
} else if !IsRelease || IsBeta {
|
||||||
m.StartDeadlockDetector(20 * 60 * time.Second)
|
m.StartDeadlockDetector(20 * time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
if paused {
|
||||||
|
for device := range cfg.Devices() {
|
||||||
|
m.PauseDevice(device)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear out old indexes for other devices. Otherwise we'll start up and
|
// Clear out old indexes for other devices. Otherwise we'll start up and
|
||||||
@@ -652,32 +680,88 @@ func syncthingMain() {
|
|||||||
|
|
||||||
mainSvc.Add(m)
|
mainSvc.Add(m)
|
||||||
|
|
||||||
// GUI
|
|
||||||
|
|
||||||
setupGUI(mainSvc, cfg, m, apiSub)
|
|
||||||
|
|
||||||
// The default port we announce, possibly modified by setupUPnP next.
|
// The default port we announce, possibly modified by setupUPnP next.
|
||||||
|
|
||||||
addr, err := net.ResolveTCPAddr("tcp", opts.ListenAddress[0])
|
uri, err := url.Parse(opts.ListenAddress[0])
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalf("Failed to parse listen address %s: %v", opts.ListenAddress[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("Bad listen address:", err)
|
l.Fatalln("Bad listen address:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start discovery
|
// The externalAddr tracks our external addresses for discovery purposes.
|
||||||
|
|
||||||
localPort := addr.Port
|
var addrList *addressLister
|
||||||
discoverer = discovery(localPort)
|
|
||||||
|
|
||||||
// Start UPnP. The UPnP service will restart global discovery if the
|
// Start UPnP
|
||||||
// external port changes.
|
|
||||||
|
|
||||||
if opts.UPnPEnabled {
|
if opts.UPnPEnabled {
|
||||||
upnpSvc := newUPnPSvc(cfg, localPort)
|
upnpSvc := newUPnPSvc(cfg, addr.Port)
|
||||||
mainSvc.Add(upnpSvc)
|
mainSvc.Add(upnpSvc)
|
||||||
|
|
||||||
|
// The external address tracker needs to know about the UPnP service
|
||||||
|
// so it can check for an external mapped port.
|
||||||
|
addrList = newAddressLister(upnpSvc, cfg)
|
||||||
|
} else {
|
||||||
|
addrList = newAddressLister(nil, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg)
|
// Start relay management
|
||||||
cfg.Subscribe(connectionSvc)
|
|
||||||
|
var relaySvc *relay.Svc
|
||||||
|
if opts.RelaysEnabled && (opts.GlobalAnnEnabled || opts.RelayWithoutGlobalAnn) {
|
||||||
|
relaySvc = relay.NewSvc(cfg, tlsCfg)
|
||||||
|
mainSvc.Add(relaySvc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start discovery
|
||||||
|
|
||||||
|
cachedDiscovery := discover.NewCachingMux()
|
||||||
|
mainSvc.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)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Global discovery:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each global discovery server gets its results cached for five
|
||||||
|
// minutes, and is not asked again for a minute when it's returned
|
||||||
|
// unsuccessfully.
|
||||||
|
cachedDiscovery.Add(gd, 5*time.Minute, time.Minute, globalDiscoveryPriority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Options().LocalAnnEnabled {
|
||||||
|
// v4 broadcasts
|
||||||
|
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), addrList, relaySvc)
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("IPv6 local discovery:", err)
|
||||||
|
} else {
|
||||||
|
cachedDiscovery.Add(mcd, 0, 0, ipv6LocalDiscoveryPriority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GUI
|
||||||
|
|
||||||
|
setupGUI(mainSvc, cfg, m, apiSub, cachedDiscovery, relaySvc, errors, systemLog)
|
||||||
|
|
||||||
|
// Start connection management
|
||||||
|
|
||||||
|
connectionSvc := connections.NewConnectionSvc(cfg, myID, m, tlsCfg, cachedDiscovery, relaySvc, bepProtocolName, tlsDefaultCommonName, lans)
|
||||||
mainSvc.Add(connectionSvc)
|
mainSvc.Add(connectionSvc)
|
||||||
|
|
||||||
if cpuProfile {
|
if cpuProfile {
|
||||||
@@ -713,7 +797,7 @@ func syncthingMain() {
|
|||||||
// The usageReportingManager registers itself to listen to configuration
|
// The usageReportingManager registers itself to listen to configuration
|
||||||
// changes, and there's nothing more we need to tell it from the outside.
|
// changes, and there's nothing more we need to tell it from the outside.
|
||||||
// Hence we don't keep the returned pointer.
|
// Hence we don't keep the returned pointer.
|
||||||
newUsageReportingManager(m, cfg)
|
newUsageReportingManager(cfg, m)
|
||||||
|
|
||||||
if opts.RestartOnWakeup {
|
if opts.RestartOnWakeup {
|
||||||
go standbyMonitor()
|
go standbyMonitor()
|
||||||
@@ -723,7 +807,7 @@ func syncthingMain() {
|
|||||||
if noUpgrade {
|
if noUpgrade {
|
||||||
l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.")
|
l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.")
|
||||||
} else if IsRelease {
|
} else if IsRelease {
|
||||||
go autoUpgrade()
|
go autoUpgrade(cfg)
|
||||||
} else {
|
} else {
|
||||||
l.Infof("No automatic upgrades; %s is not a release version.", Version)
|
l.Infof("No automatic upgrades; %s is not a release version.", Version)
|
||||||
}
|
}
|
||||||
@@ -749,7 +833,30 @@ func syncthingMain() {
|
|||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbOpts() *opt.Options {
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(cfgFile, myID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
myCfg := cfg.Devices()[myID]
|
||||||
|
myName := myCfg.Name
|
||||||
|
if myName == "" {
|
||||||
|
myName, _ = os.Hostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, myName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbOpts(cfg *config.Wrapper) *opt.Options {
|
||||||
// Calculate a suitable database block cache capacity.
|
// Calculate a suitable database block cache capacity.
|
||||||
|
|
||||||
// Default is 8 MiB.
|
// Default is 8 MiB.
|
||||||
@@ -800,49 +907,24 @@ func startAuditing(mainSvc *suture.Supervisor) {
|
|||||||
l.Infoln("Audit log in", auditFile)
|
l.Infoln("Audit log in", auditFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription) {
|
func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc, errors, systemLog *logger.Recorder) {
|
||||||
opts := cfg.Options()
|
guiCfg := cfg.GUI()
|
||||||
guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey)
|
|
||||||
|
|
||||||
if guiCfg.Enabled && guiCfg.Address != "" {
|
if !guiCfg.Enabled {
|
||||||
addr, err := net.ResolveTCPAddr("tcp", guiCfg.Address)
|
return
|
||||||
if err != nil {
|
|
||||||
l.Fatalf("Cannot start GUI on %q: %v", guiCfg.Address, err)
|
|
||||||
} else {
|
|
||||||
var hostOpen, hostShow string
|
|
||||||
switch {
|
|
||||||
case addr.IP == nil:
|
|
||||||
hostOpen = "localhost"
|
|
||||||
hostShow = "0.0.0.0"
|
|
||||||
case addr.IP.IsUnspecified():
|
|
||||||
hostOpen = "localhost"
|
|
||||||
hostShow = addr.IP.String()
|
|
||||||
default:
|
|
||||||
hostOpen = addr.IP.String()
|
|
||||||
hostShow = hostOpen
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var proto = "http"
|
api, err := newAPISvc(myID, cfg, guiAssets, m, apiSub, discoverer, relaySvc, errors, systemLog)
|
||||||
if guiCfg.UseTLS {
|
|
||||||
proto = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
urlShow := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
|
|
||||||
l.Infoln("Starting web GUI on", urlShow)
|
|
||||||
api, err := newAPISvc(myID, guiCfg, guiAssets, m, apiSub)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("Cannot start GUI:", err)
|
l.Fatalln("Cannot start GUI:", err)
|
||||||
}
|
}
|
||||||
cfg.Subscribe(api)
|
cfg.Subscribe(api)
|
||||||
mainSvc.Add(api)
|
mainSvc.Add(api)
|
||||||
|
|
||||||
if opts.StartBrowser && !noBrowser && !stRestarting {
|
if cfg.Options().StartBrowser && !noBrowser && !stRestarting {
|
||||||
urlOpen := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostOpen, strconv.Itoa(addr.Port)))
|
|
||||||
// Can potentially block if the utility we are invoking doesn't
|
// Can potentially block if the utility we are invoking doesn't
|
||||||
// fork, and just execs, hence keep it in it's own routine.
|
// fork, and just execs, hence keep it in it's own routine.
|
||||||
go openURL(urlOpen)
|
go openURL(guiCfg.URL())
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,13 +951,13 @@ func defaultConfig(myName string) config.Configuration {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("get free port (GUI):", err)
|
l.Fatalln("get free port (GUI):", err)
|
||||||
}
|
}
|
||||||
newCfg.GUI.Address = fmt.Sprintf("127.0.0.1:%d", port)
|
newCfg.GUI.RawAddress = fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
|
|
||||||
port, err = getFreePort("0.0.0.0", 22000)
|
port, err = getFreePort("0.0.0.0", 22000)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("get free port (BEP):", err)
|
l.Fatalln("get free port (BEP):", err)
|
||||||
}
|
}
|
||||||
newCfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)}
|
newCfg.Options.ListenAddress = []string{fmt.Sprintf("tcp://0.0.0.0:%d", port)}
|
||||||
return newCfg
|
return newCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -900,23 +982,6 @@ func shutdown() {
|
|||||||
stop <- exitSuccess
|
stop <- exitSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
func discovery(extPort int) *discover.Discoverer {
|
|
||||||
opts := cfg.Options()
|
|
||||||
disc := discover.NewDiscoverer(myID, opts.ListenAddress)
|
|
||||||
|
|
||||||
if opts.LocalAnnEnabled {
|
|
||||||
l.Infoln("Starting local discovery announcements")
|
|
||||||
disc.StartLocal(opts.LocalAnnPort, opts.LocalAnnMCAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.GlobalAnnEnabled {
|
|
||||||
l.Infoln("Starting global discovery announcements")
|
|
||||||
disc.StartGlobal(opts.GlobalAnnServers, uint16(extPort))
|
|
||||||
}
|
|
||||||
|
|
||||||
return disc
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureDir(dir string, mode int) {
|
func ensureDir(dir string, mode int) {
|
||||||
fi, err := os.Stat(dir)
|
fi, err := os.Stat(dir)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -954,48 +1019,6 @@ func getFreePort(host string, ports ...int) (int, error) {
|
|||||||
return addr.Port, nil
|
return addr.Port, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func overrideGUIConfig(cfg config.GUIConfiguration, address, authentication, apikey string) config.GUIConfiguration {
|
|
||||||
if address != "" {
|
|
||||||
cfg.Enabled = true
|
|
||||||
|
|
||||||
if !strings.Contains(address, "//") {
|
|
||||||
// Assume just an IP was given. Don't touch he TLS setting.
|
|
||||||
cfg.Address = address
|
|
||||||
} else {
|
|
||||||
parsed, err := url.Parse(address)
|
|
||||||
if err != nil {
|
|
||||||
l.Fatalln(err)
|
|
||||||
}
|
|
||||||
cfg.Address = parsed.Host
|
|
||||||
switch parsed.Scheme {
|
|
||||||
case "http":
|
|
||||||
cfg.UseTLS = false
|
|
||||||
case "https":
|
|
||||||
cfg.UseTLS = true
|
|
||||||
default:
|
|
||||||
l.Fatalln("Unknown scheme:", parsed.Scheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if authentication != "" {
|
|
||||||
authenticationParts := strings.SplitN(authentication, ":", 2)
|
|
||||||
|
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(authenticationParts[1]), 0)
|
|
||||||
if err != nil {
|
|
||||||
l.Fatalln("Invalid GUI password:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.User = authenticationParts[0]
|
|
||||||
cfg.Password = string(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
if apikey != "" {
|
|
||||||
cfg.APIKey = apikey
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func standbyMonitor() {
|
func standbyMonitor() {
|
||||||
restartDelay := time.Duration(60 * time.Second)
|
restartDelay := time.Duration(60 * time.Second)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -1016,7 +1039,7 @@ func standbyMonitor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func autoUpgrade() {
|
func autoUpgrade(cfg *config.Wrapper) {
|
||||||
timer := time.NewTimer(0)
|
timer := time.NewTimer(0)
|
||||||
sub := events.Default.Subscribe(events.DeviceConnected)
|
sub := events.Default.Subscribe(events.DeviceConnected)
|
||||||
for {
|
for {
|
||||||
@@ -1030,7 +1053,7 @@ func autoUpgrade() {
|
|||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
}
|
}
|
||||||
|
|
||||||
rel, err := upgrade.LatestRelease(Version)
|
rel, err := upgrade.LatestRelease(cfg.Options().ReleasesURL, Version)
|
||||||
if err == upgrade.ErrUpgradeUnsupported {
|
if err == upgrade.ErrUpgradeUnsupported {
|
||||||
events.Default.Unsubscribe(sub)
|
events.Default.Unsubscribe(sub)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func monitorMain() {
|
|||||||
var err error
|
var err error
|
||||||
var dst io.Writer = os.Stdout
|
var dst io.Writer = os.Stdout
|
||||||
|
|
||||||
if logFile != "" {
|
if logFile != "-" {
|
||||||
var fileDst io.Writer
|
var fileDst io.Writer
|
||||||
|
|
||||||
fileDst, err = os.Create(logFile)
|
fileDst, err = os.Create(logFile)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
type folderSummarySvc struct {
|
type folderSummarySvc struct {
|
||||||
*suture.Supervisor
|
*suture.Supervisor
|
||||||
|
|
||||||
|
cfg *config.Wrapper
|
||||||
model *model.Model
|
model *model.Model
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
immediate chan string
|
immediate chan string
|
||||||
@@ -33,9 +35,10 @@ type folderSummarySvc struct {
|
|||||||
lastEventReqMut sync.Mutex
|
lastEventReqMut sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFolderSummarySvc(m *model.Model) *folderSummarySvc {
|
func newFolderSummarySvc(cfg *config.Wrapper, m *model.Model) *folderSummarySvc {
|
||||||
svc := &folderSummarySvc{
|
svc := &folderSummarySvc{
|
||||||
Supervisor: suture.NewSimple("folderSummarySvc"),
|
Supervisor: suture.NewSimple("folderSummarySvc"),
|
||||||
|
cfg: cfg,
|
||||||
model: m,
|
model: m,
|
||||||
stop: make(chan struct{}),
|
stop: make(chan struct{}),
|
||||||
immediate: make(chan string),
|
immediate: make(chan string),
|
||||||
@@ -162,13 +165,13 @@ func (c *folderSummarySvc) foldersToHandle() []string {
|
|||||||
func (c *folderSummarySvc) sendSummary(folder string) {
|
func (c *folderSummarySvc) sendSummary(folder string) {
|
||||||
// The folder summary contains how many bytes, files etc
|
// The folder summary contains how many bytes, files etc
|
||||||
// are in the folder and how in sync we are.
|
// are in the folder and how in sync we are.
|
||||||
data := folderSummary(c.model, folder)
|
data := folderSummary(c.cfg, c.model, folder)
|
||||||
events.Default.Log(events.FolderSummary, map[string]interface{}{
|
events.Default.Log(events.FolderSummary, map[string]interface{}{
|
||||||
"folder": folder,
|
"folder": folder,
|
||||||
"summary": data,
|
"summary": data,
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, devCfg := range cfg.Folders()[folder].Devices {
|
for _, devCfg := range c.cfg.Folders()[folder].Devices {
|
||||||
if devCfg.DeviceID.Equals(myID) {
|
if devCfg.DeviceID.Equals(myID) {
|
||||||
// We already know about ourselves.
|
// We already know about ourselves.
|
||||||
continue
|
continue
|
||||||
|
|||||||
+22
-10
@@ -11,6 +11,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
"github.com/syncthing/syncthing/lib/upnp"
|
"github.com/syncthing/syncthing/lib/upnp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +21,8 @@ import (
|
|||||||
type upnpSvc struct {
|
type upnpSvc struct {
|
||||||
cfg *config.Wrapper
|
cfg *config.Wrapper
|
||||||
localPort int
|
localPort int
|
||||||
|
extPort int
|
||||||
|
extPortMut sync.Mutex
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,11 +30,11 @@ func newUPnPSvc(cfg *config.Wrapper, localPort int) *upnpSvc {
|
|||||||
return &upnpSvc{
|
return &upnpSvc{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
localPort: localPort,
|
localPort: localPort,
|
||||||
|
extPortMut: sync.NewMutex(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *upnpSvc) Serve() {
|
func (s *upnpSvc) Serve() {
|
||||||
extPort := 0
|
|
||||||
foundIGD := true
|
foundIGD := true
|
||||||
s.stop = make(chan struct{})
|
s.stop = make(chan struct{})
|
||||||
|
|
||||||
@@ -38,7 +42,15 @@ func (s *upnpSvc) Serve() {
|
|||||||
igds := upnp.Discover(time.Duration(s.cfg.Options().UPnPTimeoutS) * time.Second)
|
igds := upnp.Discover(time.Duration(s.cfg.Options().UPnPTimeoutS) * time.Second)
|
||||||
if len(igds) > 0 {
|
if len(igds) > 0 {
|
||||||
foundIGD = true
|
foundIGD = true
|
||||||
extPort = s.tryIGDs(igds, extPort)
|
s.extPortMut.Lock()
|
||||||
|
oldExtPort := s.extPort
|
||||||
|
s.extPortMut.Unlock()
|
||||||
|
|
||||||
|
newExtPort := s.tryIGDs(igds, oldExtPort)
|
||||||
|
|
||||||
|
s.extPortMut.Lock()
|
||||||
|
s.extPort = newExtPort
|
||||||
|
s.extPortMut.Unlock()
|
||||||
} else if foundIGD {
|
} else if foundIGD {
|
||||||
// Only print a notice if we've previously found an IGD or this is
|
// Only print a notice if we've previously found an IGD or this is
|
||||||
// the first time around.
|
// the first time around.
|
||||||
@@ -64,6 +76,13 @@ func (s *upnpSvc) Stop() {
|
|||||||
close(s.stop)
|
close(s.stop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *upnpSvc) 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 *upnpSvc) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
|
||||||
// Lets try all the IGDs we found and use the first one that works.
|
// 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
|
// TODO: Use all of them, and sort out the resulting mess to the
|
||||||
@@ -76,17 +95,10 @@ func (s *upnpSvc) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if extPort != prevExtPort {
|
if extPort != prevExtPort {
|
||||||
// External port changed; refresh the discovery announcement.
|
|
||||||
// TODO: Don't reach out to some magic global here?
|
|
||||||
l.Infof("New UPnP port mapping: external port %d to local port %d.", extPort, s.localPort)
|
l.Infof("New UPnP port mapping: external port %d to local port %d.", extPort, s.localPort)
|
||||||
if s.cfg.Options().GlobalAnnEnabled {
|
events.Default.Log(events.ExternalPortMappingChanged, map[string]int{"port": extPort})
|
||||||
discoverer.StopGlobal()
|
|
||||||
discoverer.StartGlobal(s.cfg.Options().GlobalAnnServers, uint16(extPort))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if debugNet {
|
|
||||||
l.Debugf("Created/updated UPnP port mapping for external port %d on device %s.", extPort, igd.FriendlyIdentifier())
|
l.Debugf("Created/updated UPnP port mapping for external port %d on device %s.", extPort, igd.FriendlyIdentifier())
|
||||||
}
|
|
||||||
return extPort
|
return extPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+140
-14
@@ -10,30 +10,36 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
"github.com/thejerf/suture"
|
"github.com/thejerf/suture"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Current version number of the usage report, for acceptance purposes. If
|
// Current version number of the usage report, for acceptance purposes. If
|
||||||
// fields are added or changed this integer must be incremented so that users
|
// fields are added or changed this integer must be incremented so that users
|
||||||
// are prompted for acceptance of the new report.
|
// are prompted for acceptance of the new report.
|
||||||
const usageReportVersion = 1
|
const usageReportVersion = 2
|
||||||
|
|
||||||
type usageReportingManager struct {
|
type usageReportingManager struct {
|
||||||
|
cfg *config.Wrapper
|
||||||
model *model.Model
|
model *model.Model
|
||||||
sup *suture.Supervisor
|
sup *suture.Supervisor
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUsageReportingManager(m *model.Model, cfg *config.Wrapper) *usageReportingManager {
|
func newUsageReportingManager(cfg *config.Wrapper, m *model.Model) *usageReportingManager {
|
||||||
mgr := &usageReportingManager{
|
mgr := &usageReportingManager{
|
||||||
|
cfg: cfg,
|
||||||
model: m,
|
model: m,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +60,7 @@ func (m *usageReportingManager) VerifyConfiguration(from, to config.Configuratio
|
|||||||
func (m *usageReportingManager) CommitConfiguration(from, to config.Configuration) bool {
|
func (m *usageReportingManager) CommitConfiguration(from, to config.Configuration) bool {
|
||||||
if to.Options.URAccepted >= usageReportVersion && m.sup == nil {
|
if to.Options.URAccepted >= usageReportVersion && m.sup == nil {
|
||||||
// Usage reporting was turned on; lets start it.
|
// Usage reporting was turned on; lets start it.
|
||||||
svc := &usageReportingService{
|
svc := newUsageReportingService(m.cfg, m.model)
|
||||||
model: m.model,
|
|
||||||
}
|
|
||||||
m.sup = suture.NewSimple("usageReporting")
|
m.sup = suture.NewSimple("usageReporting")
|
||||||
m.sup.Add(svc)
|
m.sup.Add(svc)
|
||||||
m.sup.ServeBackground()
|
m.sup.ServeBackground()
|
||||||
@@ -75,8 +79,9 @@ func (m *usageReportingManager) String() string {
|
|||||||
|
|
||||||
// reportData returns the data to be sent in a usage report. It's used in
|
// 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 usageReportingSvc object.
|
||||||
func reportData(m *model.Model) map[string]interface{} {
|
func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
|
||||||
res := make(map[string]interface{})
|
res := make(map[string]interface{})
|
||||||
|
res["urVersion"] = usageReportVersion
|
||||||
res["uniqueID"] = cfg.Options().URUniqueID
|
res["uniqueID"] = cfg.Options().URUniqueID
|
||||||
res["version"] = Version
|
res["version"] = Version
|
||||||
res["longVersion"] = LongVersion
|
res["longVersion"] = LongVersion
|
||||||
@@ -120,31 +125,152 @@ func reportData(m *model.Model) map[string]interface{} {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
res["memorySize"] = bytes / 1024 / 1024
|
res["memorySize"] = bytes / 1024 / 1024
|
||||||
}
|
}
|
||||||
|
res["numCPU"] = runtime.NumCPU()
|
||||||
|
|
||||||
|
var rescanIntvs []int
|
||||||
|
folderUses := map[string]int{
|
||||||
|
"readonly": 0,
|
||||||
|
"ignorePerms": 0,
|
||||||
|
"ignoreDelete": 0,
|
||||||
|
"autoNormalize": 0,
|
||||||
|
}
|
||||||
|
for _, cfg := range cfg.Folders() {
|
||||||
|
rescanIntvs = append(rescanIntvs, cfg.RescanIntervalS)
|
||||||
|
|
||||||
|
if cfg.ReadOnly {
|
||||||
|
folderUses["readonly"]++
|
||||||
|
}
|
||||||
|
if cfg.IgnorePerms {
|
||||||
|
folderUses["ignorePerms"]++
|
||||||
|
}
|
||||||
|
if cfg.IgnoreDelete {
|
||||||
|
folderUses["ignoreDelete"]++
|
||||||
|
}
|
||||||
|
if cfg.AutoNormalize {
|
||||||
|
folderUses["autoNormalize"]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Ints(rescanIntvs)
|
||||||
|
res["rescanIntvs"] = rescanIntvs
|
||||||
|
res["folderUses"] = folderUses
|
||||||
|
|
||||||
|
deviceUses := map[string]int{
|
||||||
|
"introducer": 0,
|
||||||
|
"customCertName": 0,
|
||||||
|
"compressAlways": 0,
|
||||||
|
"compressMetadata": 0,
|
||||||
|
"compressNever": 0,
|
||||||
|
"dynamicAddr": 0,
|
||||||
|
"staticAddr": 0,
|
||||||
|
}
|
||||||
|
for _, cfg := range cfg.Devices() {
|
||||||
|
if cfg.Introducer {
|
||||||
|
deviceUses["introducer"]++
|
||||||
|
}
|
||||||
|
if cfg.CertName != "" && cfg.CertName != "syncthing" {
|
||||||
|
deviceUses["customCertName"]++
|
||||||
|
}
|
||||||
|
if cfg.Compression == protocol.CompressAlways {
|
||||||
|
deviceUses["compressAlways"]++
|
||||||
|
} else if cfg.Compression == protocol.CompressMetadata {
|
||||||
|
deviceUses["compressMetadata"]++
|
||||||
|
} else if cfg.Compression == protocol.CompressNever {
|
||||||
|
deviceUses["compressNever"]++
|
||||||
|
}
|
||||||
|
for _, addr := range cfg.Addresses {
|
||||||
|
if addr == "dynamic" {
|
||||||
|
deviceUses["dynamicAddr"]++
|
||||||
|
} else {
|
||||||
|
deviceUses["staticAddr"]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res["deviceUses"] = deviceUses
|
||||||
|
|
||||||
|
defaultAnnounceServersDNS, defaultAnnounceServersIP, otherAnnounceServers := 0, 0, 0
|
||||||
|
for _, addr := range cfg.Options().GlobalAnnServers {
|
||||||
|
if addr == "default" {
|
||||||
|
defaultAnnounceServersDNS++
|
||||||
|
} else if stringIn(addr, config.DefaultDiscoveryServersIP) {
|
||||||
|
defaultAnnounceServersIP++
|
||||||
|
} else {
|
||||||
|
otherAnnounceServers++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res["announce"] = map[string]interface{}{
|
||||||
|
"globalEnabled": cfg.Options().GlobalAnnEnabled,
|
||||||
|
"localEnabled": cfg.Options().LocalAnnEnabled,
|
||||||
|
"defaultServersDNS": defaultAnnounceServersDNS,
|
||||||
|
"defaultServersIP": defaultAnnounceServersIP,
|
||||||
|
"otherServers": otherAnnounceServers,
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultRelayServers, otherRelayServers := 0, 0
|
||||||
|
for _, addr := range cfg.Options().RelayServers {
|
||||||
|
switch addr {
|
||||||
|
case "dynamic+https://relays.syncthing.net":
|
||||||
|
defaultRelayServers++
|
||||||
|
default:
|
||||||
|
otherRelayServers++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res["relays"] = map[string]interface{}{
|
||||||
|
"enabled": cfg.Options().RelaysEnabled,
|
||||||
|
"defaultServers": defaultRelayServers,
|
||||||
|
"otherServers": otherRelayServers,
|
||||||
|
}
|
||||||
|
|
||||||
|
res["usesRateLimit"] = cfg.Options().MaxRecvKbps > 0 || cfg.Options().MaxSendKbps > 0
|
||||||
|
|
||||||
|
res["upgradeAllowedManual"] = !(upgrade.DisabledByCompilation || noUpgrade)
|
||||||
|
res["upgradeAllowedAuto"] = !(upgrade.DisabledByCompilation || noUpgrade) && cfg.Options().AutoUpgradeIntervalH > 0
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stringIn(needle string, haystack []string) bool {
|
||||||
|
for _, s := range haystack {
|
||||||
|
if needle == s {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type usageReportingService struct {
|
type usageReportingService struct {
|
||||||
|
cfg *config.Wrapper
|
||||||
model *model.Model
|
model *model.Model
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newUsageReportingService(cfg *config.Wrapper, model *model.Model) *usageReportingService {
|
||||||
|
return &usageReportingService{
|
||||||
|
cfg: cfg,
|
||||||
|
model: model,
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *usageReportingService) sendUsageReport() error {
|
func (s *usageReportingService) sendUsageReport() error {
|
||||||
d := reportData(s.model)
|
d := reportData(s.cfg, s.model)
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
json.NewEncoder(&b).Encode(d)
|
json.NewEncoder(&b).Encode(d)
|
||||||
|
|
||||||
var client = http.DefaultClient
|
transp := &http.Transport{}
|
||||||
|
client := &http.Client{Transport: transp}
|
||||||
if BuildEnv == "android" {
|
if BuildEnv == "android" {
|
||||||
// This works around the lack of DNS resolution on Android... :(
|
// This works around the lack of DNS resolution on Android... :(
|
||||||
tr := &http.Transport{
|
transp.Dial = func(network, addr string) (net.Conn, error) {
|
||||||
Dial: func(network, addr string) (net.Conn, error) {
|
|
||||||
return net.Dial(network, "194.126.249.13:443")
|
return net.Dial(network, "194.126.249.13:443")
|
||||||
},
|
|
||||||
}
|
}
|
||||||
client = &http.Client{Transport: tr}
|
|
||||||
}
|
}
|
||||||
_, err := client.Post("https://data.syncthing.net/newdata", "application/json", &b)
|
|
||||||
|
if s.cfg.Options().URPostInsecurely {
|
||||||
|
transp.TLSClientConfig = &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := client.Post(s.cfg.Options().URURL, "application/json", &b)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +280,7 @@ func (s *usageReportingService) Serve() {
|
|||||||
l.Infoln("Starting usage reporting")
|
l.Infoln("Starting usage reporting")
|
||||||
defer l.Infoln("Stopping usage reporting")
|
defer l.Infoln("Stopping usage reporting")
|
||||||
|
|
||||||
t := time.NewTimer(10 * time.Minute) // time to initial report at start
|
t := time.NewTimer(time.Duration(s.cfg.Options().URInitialDelayS) * time.Second) // time to initial report at start
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-s.stop:
|
case <-s.stop:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
)
|
)
|
||||||
@@ -60,7 +61,7 @@ func (s *verboseSvc) WaitForStart() {
|
|||||||
|
|
||||||
func (s *verboseSvc) formatEvent(ev events.Event) string {
|
func (s *verboseSvc) formatEvent(ev events.Event) string {
|
||||||
switch ev.Type {
|
switch ev.Type {
|
||||||
case events.Ping, events.DownloadProgress:
|
case events.Ping, events.DownloadProgress, events.LocalIndexUpdated:
|
||||||
// Skip
|
// Skip
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
|
|||||||
return fmt.Sprintf("Discovered device %v at %v", data["device"], data["addrs"])
|
return fmt.Sprintf("Discovered device %v at %v", data["device"], data["addrs"])
|
||||||
case events.DeviceConnected:
|
case events.DeviceConnected:
|
||||||
data := ev.Data.(map[string]string)
|
data := ev.Data.(map[string]string)
|
||||||
return fmt.Sprintf("Connected to device %v at %v", data["id"], data["addr"])
|
return fmt.Sprintf("Connected to device %v at %v (type %s)", data["id"], data["addr"], data["type"])
|
||||||
case events.DeviceDisconnected:
|
case events.DeviceDisconnected:
|
||||||
data := ev.Data.(map[string]string)
|
data := ev.Data.(map[string]string)
|
||||||
return fmt.Sprintf("Disconnected from device %v", data["id"])
|
return fmt.Sprintf("Disconnected from device %v", data["id"])
|
||||||
@@ -86,9 +87,6 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
|
|||||||
case events.RemoteIndexUpdated:
|
case events.RemoteIndexUpdated:
|
||||||
data := ev.Data.(map[string]interface{})
|
data := ev.Data.(map[string]interface{})
|
||||||
return fmt.Sprintf("Device %v sent an index update for %q with %d items", data["device"], data["folder"], data["items"])
|
return fmt.Sprintf("Device %v sent an index update for %q with %d items", data["device"], data["folder"], data["items"])
|
||||||
case events.LocalIndexUpdated:
|
|
||||||
data := ev.Data.(map[string]interface{})
|
|
||||||
return fmt.Sprintf("Updated index for folder %q with %v items", data["folder"], data["items"])
|
|
||||||
|
|
||||||
case events.DeviceRejected:
|
case events.DeviceRejected:
|
||||||
data := ev.Data.(map[string]interface{})
|
data := ev.Data.(map[string]interface{})
|
||||||
@@ -123,6 +121,35 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
|
|||||||
delete(sum, "ignorePatterns")
|
delete(sum, "ignorePatterns")
|
||||||
delete(sum, "stateChanged")
|
delete(sum, "stateChanged")
|
||||||
return fmt.Sprintf("Summary for folder %q is %v", data["folder"], data["summary"])
|
return fmt.Sprintf("Summary for folder %q is %v", data["folder"], data["summary"])
|
||||||
|
case events.FolderScanProgress:
|
||||||
|
data := ev.Data.(map[string]interface{})
|
||||||
|
folder := data["folder"].(string)
|
||||||
|
current := data["current"].(int64)
|
||||||
|
total := data["total"].(int64)
|
||||||
|
var pct int64
|
||||||
|
if total > 0 {
|
||||||
|
pct = 100 * current / total
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Scanning folder %q, %d%% done", folder, pct)
|
||||||
|
|
||||||
|
case events.DevicePaused:
|
||||||
|
data := ev.Data.(map[string]string)
|
||||||
|
device := data["device"]
|
||||||
|
return fmt.Sprintf("Device %v was paused", device)
|
||||||
|
case events.DeviceResumed:
|
||||||
|
data := ev.Data.(map[string]string)
|
||||||
|
device := data["device"]
|
||||||
|
return fmt.Sprintf("Device %v was resumed", device)
|
||||||
|
|
||||||
|
case events.ExternalPortMappingChanged:
|
||||||
|
data := ev.Data.(map[string]int)
|
||||||
|
port := data["port"]
|
||||||
|
return fmt.Sprintf("External port mapping changed; new port is %d.", port)
|
||||||
|
case events.RelayStateChanged:
|
||||||
|
data := ev.Data.(map[string][]string)
|
||||||
|
newRelays := data["new"]
|
||||||
|
return fmt.Sprintf("Relay state changed; connected relay(s) are %s.", strings.Join(newRelays, ", "))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s %#v", ev.Type, ev)
|
return fmt.Sprintf("%s %#v", ev.Type, ev)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# Systemd Configuration
|
# Systemd Configuration
|
||||||
|
|
||||||
This directory contains configuration files for running syncthing under the
|
This directory contains configuration files for running Syncthing under the
|
||||||
"systemd" service manager on Linux both under either a systemd system service or
|
"systemd" service manager on Linux both under either a systemd system service or
|
||||||
systemd user service. For further documentation take a look at the [systemd
|
systemd user service. For further documentation take a look at the [systemd
|
||||||
section][1] on the Github Wiki.
|
section][1] on http://docs.syncthing.net.
|
||||||
|
|
||||||
[1]: http://docs.syncthing.net/users/autostart.html#systemd
|
[1]: http://docs.syncthing.net/users/autostart.html#systemd
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
Description=Syncthing - Open Source Continuous File Synchronization for %I
|
Description=Syncthing - Open Source Continuous File Synchronization for %I
|
||||||
Documentation=http://docs.syncthing.net/
|
Documentation=http://docs.syncthing.net/
|
||||||
After=network.target
|
After=network.target
|
||||||
|
Wants=syncthing-inotify@.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=%i
|
User=%i
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
Description=Syncthing - Open Source Continuous File Synchronization
|
Description=Syncthing - Open Source Continuous File Synchronization
|
||||||
Documentation=http://docs.syncthing.net/
|
Documentation=http://docs.syncthing.net/
|
||||||
After=network.target
|
After=network.target
|
||||||
|
Wants=syncthing-inotify.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Environment=STNORESTART=yes
|
Environment=STNORESTART=yes
|
||||||
|
|||||||
@@ -14,12 +14,17 @@ body {
|
|||||||
|
|
||||||
h1, h2, h3, h4, h5 {
|
h1, h2, h3, h4, h5 {
|
||||||
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul+h5 {
|
ul+h5 {
|
||||||
margin-top: 1.5em;
|
margin-top: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-progress {
|
.panel-progress {
|
||||||
background: #3498db;
|
background: #3498db;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
@@ -40,8 +45,17 @@ identicon {
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
line-height: 1em;
|
line-height: 1;
|
||||||
overflow: visible;
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identicon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.identicon rect {
|
||||||
|
fill: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
@@ -54,17 +68,7 @@ identicon {
|
|||||||
|
|
||||||
.popover {
|
.popover {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
min-width: 250px;
|
||||||
|
|
||||||
.identicon {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-heading .identicon {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-heading .fa, .modal-header .fa {
|
.panel-heading .fa, .modal-header .fa {
|
||||||
@@ -76,10 +80,6 @@ identicon {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.identicon rect {
|
|
||||||
fill: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-monospace {
|
.text-monospace {
|
||||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Замени Промените",
|
"Override Changes": "Замени Промените",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Пътят до папката на този компютър. Ще бъде създадена ако не съществува. Символът тилда (~) може да бъде използван като заместител на",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Пътят до папката на този компютър. Ще бъде създадена ако не съществува. Символът тилда (~) може да бъде използван като заместител на",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Пътят, където версиите да бъдат складирани(остави празно за папката .stversions).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Пътят, където версиите да бъдат складирани(остави празно за папката .stversions).",
|
||||||
|
"Pause": "Пауза",
|
||||||
|
"Paused": "На пауза",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Моля прочети бележките по обновяването преди да започнеш.",
|
"Please consult the release notes before performing a major upgrade.": "Моля прочети бележките по обновяването преди да започнеш.",
|
||||||
"Please wait": "Моля изчакай",
|
"Please wait": "Моля изчакай",
|
||||||
"Preview": "Преглед",
|
"Preview": "Преглед",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Рестартирай",
|
"Restart": "Рестартирай",
|
||||||
"Restart Needed": "Изискава се Рестартиране",
|
"Restart Needed": "Изискава се Рестартиране",
|
||||||
"Restarting": "Рестартиране",
|
"Restarting": "Рестартиране",
|
||||||
|
"Resume": "Пусни",
|
||||||
"Reused": "Повторно използван",
|
"Reused": "Повторно използван",
|
||||||
"Save": "Запази",
|
"Save": "Запази",
|
||||||
"Scanning": "Сканиране",
|
"Scanning": "Сканиране",
|
||||||
@@ -209,6 +212,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.": "Когато добавяш нов идентификатор на папка помни, че той се използва за свързване на папките на различни устройства. Главни/малки букви са от значение и трябва да са еднакви на всички устройства.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Когато добавяш нов идентификатор на папка помни, че той се използва за свързване на папките на различни устройства. Главни/малки букви са от значение и трябва да са еднакви на всички устройства.",
|
||||||
"Yes": "Да",
|
"Yes": "Да",
|
||||||
"You must keep at least one version.": "Трябва да пазиш поне една версия.",
|
"You must keep at least one version.": "Трябва да пазиш поне една версия.",
|
||||||
|
"days": "дни",
|
||||||
"full documentation": "пълна документация",
|
"full documentation": "пълна документация",
|
||||||
"items": "артикула",
|
"items": "артикула",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} желае да сподели папка \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} желае да сподели папка \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Sobreescriure Canvis",
|
"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 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)",
|
"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",
|
||||||
"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 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 wait": "Si-us-plau espera",
|
"Please wait": "Si-us-plau espera",
|
||||||
"Preview": "Vista prèvia",
|
"Preview": "Vista prèvia",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Reiniciar",
|
"Restart": "Reiniciar",
|
||||||
"Restart Needed": "És Necessari Reiniciar",
|
"Restart Needed": "És Necessari Reiniciar",
|
||||||
"Restarting": "Reiniciant",
|
"Restarting": "Reiniciant",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Reutilitzat",
|
"Reused": "Reutilitzat",
|
||||||
"Save": "Guardar",
|
"Save": "Guardar",
|
||||||
"Scanning": "Escanejant",
|
"Scanning": "Escanejant",
|
||||||
@@ -209,6 +212,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.",
|
"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",
|
"Yes": "Si",
|
||||||
"You must keep at least one version.": "Has de mantenir com a mínim una versió.",
|
"You must keep at least one version.": "Has de mantenir com a mínim una versió.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "documentació sencera",
|
"full documentation": "documentació sencera",
|
||||||
"items": "Elements",
|
"items": "Elements",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartir la carpeta \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartir la carpeta \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Sobreescriure els canvis",
|
"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 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).",
|
"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",
|
||||||
"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 consult the release notes before performing a major upgrade.": "Per favor, consultar les notes de la versió abans de fer una actualització important.",
|
||||||
"Please wait": "Per favor, espere",
|
"Please wait": "Per favor, espere",
|
||||||
"Preview": "Vista prèvia",
|
"Preview": "Vista prèvia",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Reiniciar",
|
"Restart": "Reiniciar",
|
||||||
"Restart Needed": "Reinici necesari",
|
"Restart Needed": "Reinici necesari",
|
||||||
"Restarting": "Reiniciant",
|
"Restarting": "Reiniciant",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Reutilitzat",
|
"Reused": "Reutilitzat",
|
||||||
"Save": "Gravar",
|
"Save": "Gravar",
|
||||||
"Scanning": "Rastrejant",
|
"Scanning": "Rastrejant",
|
||||||
@@ -209,6 +212,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.",
|
"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í",
|
"Yes": "Sí",
|
||||||
"You must keep at least one version.": "Es deu mantindre al menys una versió.",
|
"You must keep at least one version.": "Es deu mantindre al menys una versió.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "Documentació completa",
|
"full documentation": "Documentació completa",
|
||||||
"items": "Elements",
|
"items": "Elements",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartit la carpeta \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartit la carpeta \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Přepsat změny",
|
"Override Changes": "Přepsat změny",
|
||||||
"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": "Cesta k adresáři na lokálním počítači. Pokud neexistuje, bude vytvořen. Znak vlnovky (~) může být použit jako zkratka pro",
|
"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": "Cesta k adresáři na lokálním počítači. Pokud neexistuje, bude vytvořen. Znak vlnovky (~) může být použit jako zkratka pro",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Cesta pro ukládání verzí (nechat prázdné pro výchozí podadresář .stversions v adresářích).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Cesta pro ukládání verzí (nechat prázdné pro výchozí podadresář .stversions v adresářích).",
|
||||||
|
"Pause": "Pozastavit",
|
||||||
|
"Paused": "Pozastaveno",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Před spuštěním důležité aktualizace si nejdříve přečtěte poznámky k vydání nové verze.",
|
"Please consult the release notes before performing a major upgrade.": "Před spuštěním důležité aktualizace si nejdříve přečtěte poznámky k vydání nové verze.",
|
||||||
"Please wait": "Chvíli strpení",
|
"Please wait": "Chvíli strpení",
|
||||||
"Preview": "Náhled",
|
"Preview": "Náhled",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Restart",
|
"Restart": "Restart",
|
||||||
"Restart Needed": "Je nutný restart",
|
"Restart Needed": "Je nutný restart",
|
||||||
"Restarting": "Restartuji",
|
"Restarting": "Restartuji",
|
||||||
|
"Resume": "Pokračovat",
|
||||||
"Reused": "Opakovaně použité",
|
"Reused": "Opakovaně použité",
|
||||||
"Save": "Uložit",
|
"Save": "Uložit",
|
||||||
"Scanning": "Skenování",
|
"Scanning": "Skenování",
|
||||||
@@ -154,7 +157,7 @@
|
|||||||
"Source Code": "Zdrojový kód",
|
"Source Code": "Zdrojový kód",
|
||||||
"Staggered File Versioning": "Postupné verzování souborů",
|
"Staggered File Versioning": "Postupné verzování souborů",
|
||||||
"Start Browser": "Otevřít prohlížeč",
|
"Start Browser": "Otevřít prohlížeč",
|
||||||
"Statistics": "Statistika",
|
"Statistics": "Statistiky",
|
||||||
"Stopped": "Pozastaveno",
|
"Stopped": "Pozastaveno",
|
||||||
"Support": "Podpora",
|
"Support": "Podpora",
|
||||||
"Sync Protocol Listen Addresses": "Adresa naslouchání synchronizačního protokolu",
|
"Sync Protocol Listen Addresses": "Adresa naslouchání synchronizačního protokolu",
|
||||||
@@ -209,6 +212,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.": "Při přidávání nového adresáře mějte na paměti, že jeho ID je použito ke svázání adresářů napříč přístoji. Rozlišují se malá a velká písmena a musí přesně souhlasit mezi všemi přístroji.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Při přidávání nového adresáře mějte na paměti, že jeho ID je použito ke svázání adresářů napříč přístoji. Rozlišují se malá a velká písmena a musí přesně souhlasit mezi všemi přístroji.",
|
||||||
"Yes": "Ano",
|
"Yes": "Ano",
|
||||||
"You must keep at least one version.": "Je třeba ponechat alespoň jednu verzi.",
|
"You must keep at least one version.": "Je třeba ponechat alespoň jednu verzi.",
|
||||||
|
"days": "dní",
|
||||||
"full documentation": "plná dokumentace",
|
"full documentation": "plná dokumentace",
|
||||||
"items": "položky",
|
"items": "položky",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce sdílet adresář \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce sdílet adresář \"{{folder}}\"."
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
"All Data": "Alle Daten",
|
"All Data": "Alle Daten",
|
||||||
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsberichten erlauben?",
|
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsberichten erlauben?",
|
||||||
"Alphabetic": "Alphabetisch",
|
"Alphabetic": "Alphabetisch",
|
||||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein externer Programmaufruf handhabt die Versionierung. Es muss die Datei aus dem zu synchronisierendem Ordner entfernen.",
|
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein externer Programmaufruf handhabt die Versionierung. Es muss die Datei aus dem zu synchronisierendem Verzeichnis entfernen.",
|
||||||
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
|
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
|
||||||
"Any devices configured on an introducer device will be added to this device as well.": "Alle Geräte, die beim Verteiler eingetragen sind, werden auch bei diesem Gerät eingetragen",
|
"Any devices configured on an introducer device will be added to this device as well.": "Alle Geräte, die beim Verteiler eingetragen sind, werden auch bei diesem Gerät eingetragen",
|
||||||
"Automatic upgrades": "automatische Updates",
|
"Automatic upgrades": "automatische Updates",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"Bugs": "Fehler",
|
"Bugs": "Fehler",
|
||||||
"CPU Utilization": "Prozessorauslastung",
|
"CPU Utilization": "Prozessorauslastung",
|
||||||
"Changelog": "Änderungsprotokoll",
|
"Changelog": "Änderungsprotokoll",
|
||||||
"Clean out after": "Aufräumen nach",
|
"Clean out after": "Löschen nach",
|
||||||
"Close": "Schließen",
|
"Close": "Schließen",
|
||||||
"Command": "Kommando",
|
"Command": "Kommando",
|
||||||
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
|
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
|
||||||
@@ -33,9 +33,9 @@
|
|||||||
"Copied from original": "Vom Original kopiert",
|
"Copied from original": "Vom Original kopiert",
|
||||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 die folgenden Unterstützer:",
|
"Copyright © 2015 the following Contributors:": "Copyright © 2015 die folgenden Unterstützer:",
|
||||||
"Delete": "Löschen",
|
"Delete": "Löschen",
|
||||||
"Deleted": "gelöscht",
|
"Deleted": "Gelöscht",
|
||||||
"Device ID": "Geräte ID",
|
"Device ID": "Geräte ID",
|
||||||
"Device Identification": "Gerät Identifikation",
|
"Device Identification": "Geräte Identifikation",
|
||||||
"Device Name": "Gerätename",
|
"Device Name": "Gerätename",
|
||||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Gerät {{device}} ({{address}}) möchte sich verbinden. Gerät hinzufügen?",
|
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Gerät {{device}} ({{address}}) möchte sich verbinden. Gerät hinzufügen?",
|
||||||
"Devices": "Geräte",
|
"Devices": "Geräte",
|
||||||
@@ -47,19 +47,19 @@
|
|||||||
"Edit": "Bearbeiten",
|
"Edit": "Bearbeiten",
|
||||||
"Edit Device": "Gerät bearbeiten",
|
"Edit Device": "Gerät bearbeiten",
|
||||||
"Edit Folder": "Verzeichnis bearbeiten",
|
"Edit Folder": "Verzeichnis bearbeiten",
|
||||||
"Editing": "Bearbeiten",
|
"Editing": "Bearbeitet",
|
||||||
"Enable UPnP": "UPnP aktivieren",
|
"Enable UPnP": "UPnP aktivieren",
|
||||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Geben Sie durch Komma getrennte Adressen an (\"tcp://ip:port\", \"tcp://host:port\") oder \"dynamic\", um die Adresse automatisch zu ermitteln.",
|
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Kommagetrennte Adressen (\"tcp://ip:port\", \"tcp://host:port\") oder \"dynamic\" eingeben, um die Adresse automatisch zu ermitteln.",
|
||||||
"Enter ignore patterns, one per line.": "Geben Sie Ignoriermuster ein, eines pro Zeile.",
|
"Enter ignore patterns, one per line.": "Geben Sie Ignoriermuster ein, eines pro Zeile.",
|
||||||
"Error": "Fehler",
|
"Error": "Fehler",
|
||||||
"External File Versioning": "Externe Dateiversionierung",
|
"External File Versioning": "Externe Dateiversionierung",
|
||||||
"Failed Items": "Fehlgeschlagene Dateien",
|
"Failed Items": "Fehlgeschlagene Objekte",
|
||||||
"File Pull Order": "Dateiübertragungsreihenfolge",
|
"File Pull Order": "Dateiübertragungsreihenfolge",
|
||||||
"File Versioning": "Dateiversionierung",
|
"File Versioning": "Dateiversionierung",
|
||||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Dateizugriffsrechte beim Suchen nach Veränderungen ignorieren. Bei FAT-Dateisystemen zu verwenden.",
|
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Dateizugriffsrechte beim Suchen nach Veränderungen ignorieren. Bei FAT-Dateisystemen zu verwenden.",
|
||||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Dateien werden, bevor Syncthing sie löscht oder ersetzt, als datierte Versionen in ein Verzeichnis namens .stversions verschoben.",
|
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Wenn Dateien von Syncthing ersetzt oder gelöscht werden sollen, werden sie vorher in den .stversions Ordner verschoben.",
|
||||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dateien werden, bevor Syncthing sie löscht oder ersetzt, als datierte Versionen in ein Verzeichnis namens .stversions verschoben.",
|
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dateien werden, bevor Syncthing sie löscht oder ersetzt, datiert in das Verzeichnis .stversions verschoben.",
|
||||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dateien sind vor Veränderung durch andere Geräte geschützt. Auf diesem Gerät durchgeführte Veränderungen werden aber auf den Rest des Verbunds übertragen.",
|
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dateien sind auf diesem Gerät schreibgeschützt. Auf diesem Gerät durchgeführte Veränderungen werden aber auf den Rest des Verbunds übertragen.",
|
||||||
"Folder": "Verzeichnis",
|
"Folder": "Verzeichnis",
|
||||||
"Folder ID": "Verzeichnis ID",
|
"Folder ID": "Verzeichnis ID",
|
||||||
"Folder Master": "Master Verzeichnis - schreibgeschützt",
|
"Folder Master": "Master Verzeichnis - schreibgeschützt",
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
"GUI Listen Addresses": "Adresse(n) für die Benutzeroberfläche",
|
"GUI Listen Addresses": "Adresse(n) für die Benutzeroberfläche",
|
||||||
"Generate": "Generieren",
|
"Generate": "Generieren",
|
||||||
"Global Discovery": "Globale Gerätesuche",
|
"Global Discovery": "Globale Gerätesuche",
|
||||||
"Global Discovery Server": "Globale(r) Indexserver",
|
"Global Discovery Server": "Globaler Gerätesuchserver",
|
||||||
"Global State": "Globaler Status",
|
"Global State": "Globaler Status",
|
||||||
"Help": "Hilfe",
|
"Help": "Hilfe",
|
||||||
"Home page": "Homepage",
|
"Home page": "Homepage",
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
"Major Upgrade": "Hauptversionsupgrade",
|
"Major Upgrade": "Hauptversionsupgrade",
|
||||||
"Maximum Age": "Höchstalter",
|
"Maximum Age": "Höchstalter",
|
||||||
"Metadata Only": "Nur Metadaten",
|
"Metadata Only": "Nur Metadaten",
|
||||||
"Minimum Free Disk Space": "Minimal freier Fetsplattenspeicher",
|
"Minimum Free Disk Space": "Minimal freier Festplattenspeicher",
|
||||||
"Move to top of queue": "An den Anfang der Warteschlange setzen",
|
"Move to top of queue": "An den Anfang der Warteschlange setzen",
|
||||||
"Multi level wildcard (matches multiple directory levels)": "Verschachteltes Maskenzeichen (wird für verschachtelte Verzeichnisse verwendet)",
|
"Multi level wildcard (matches multiple directory levels)": "Verschachteltes Maskenzeichen (wird für verschachtelte Verzeichnisse verwendet)",
|
||||||
"Never": "Nie",
|
"Never": "Nie",
|
||||||
@@ -111,17 +111,19 @@
|
|||||||
"Out of Sync Items": "Nicht synchronisierte Objekte",
|
"Out of Sync Items": "Nicht synchronisierte Objekte",
|
||||||
"Outgoing Rate Limit (KiB/s)": "Limit Datenrate (ausgehend) (KB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Limit Datenrate (ausgehend) (KB/s)",
|
||||||
"Override Changes": "Änderungen überschreiben",
|
"Override Changes": "Änderungen überschreiben",
|
||||||
"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": "Pfad zum Verzeichnis auf dem lokalen Rechner. Wird erzeugt, wenn es nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
|
"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": "Pfad zum Verzeichnis auf dem lokalen Gerät. Ordner werden erzeugt, wenn sie nicht existieren. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Pfad in dem die Versionen gespeichert werden sollen (ohne Angabe wird das Verzeichnis .stversions im Verzeichnis verwendet).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Pfad in dem alte Dateiversionen gespeichert werden sollen (ohne Angabe wird das Verzeichnis .stversions im Verzeichnis verwendet).",
|
||||||
"Please consult the release notes before performing a major upgrade.": "BItte lesen Sie die Veröffentlichungsnotizen bevor Sie eine neue Hauptversion installieren.",
|
"Pause": "Pause",
|
||||||
|
"Paused": "Pausiert",
|
||||||
|
"Please consult the release notes before performing a major upgrade.": "Bitte lesen Sie die Veröffentlichungsnotizen bevor Sie eine neue Hauptversion installieren.",
|
||||||
"Please wait": "Bitte warten",
|
"Please wait": "Bitte warten",
|
||||||
"Preview": "Vorschau",
|
"Preview": "Vorschau",
|
||||||
"Preview Usage Report": "Vorschau des Nutzungsberichts",
|
"Preview Usage Report": "Vorschau des Nutzungsberichts",
|
||||||
"Quick guide to supported patterns": "Schnellanleitung zu den unterstützten Suchstrukturen",
|
"Quick guide to supported patterns": "Schnellanleitung zu den unterstützten Mustern",
|
||||||
"RAM Utilization": "RAM Auslastung",
|
"RAM Utilization": "RAM Auslastung",
|
||||||
"Random": "Zufall",
|
"Random": "Zufall",
|
||||||
"Relayed via": "Relayed via",
|
"Relayed via": "Weitergeleitet über",
|
||||||
"Relays": "Relays",
|
"Relays": "Weiterleitungen",
|
||||||
"Release Notes": "Veröffentlichungsnotizen",
|
"Release Notes": "Veröffentlichungsnotizen",
|
||||||
"Remove": "Entfernen",
|
"Remove": "Entfernen",
|
||||||
"Rescan": "Neu scannen",
|
"Rescan": "Neu scannen",
|
||||||
@@ -130,9 +132,10 @@
|
|||||||
"Restart": "Neustart",
|
"Restart": "Neustart",
|
||||||
"Restart Needed": "Neustart benötigt",
|
"Restart Needed": "Neustart benötigt",
|
||||||
"Restarting": "Wird neu gestartet",
|
"Restarting": "Wird neu gestartet",
|
||||||
|
"Resume": "Fortfahren",
|
||||||
"Reused": "Erneut benutzt",
|
"Reused": "Erneut benutzt",
|
||||||
"Save": "Speichern",
|
"Save": "Speichern",
|
||||||
"Scanning": "Scanning",
|
"Scanning": "Scannen",
|
||||||
"Select the devices to share this folder with.": "Wähle die Geräte aus, mit denen Du dieses Verzeichnis teilen willst.",
|
"Select the devices to share this folder with.": "Wähle die Geräte aus, mit denen Du dieses Verzeichnis teilen willst.",
|
||||||
"Select the folders to share with this device.": "Wähle die Verzeichnisse aus, die du mit diesem Gerät teilen möchtest",
|
"Select the folders to share with this device.": "Wähle die Verzeichnisse aus, die du mit diesem Gerät teilen möchtest",
|
||||||
"Settings": "Einstellungen",
|
"Settings": "Einstellungen",
|
||||||
@@ -144,8 +147,8 @@
|
|||||||
"Shared With": "Geteilt mit",
|
"Shared With": "Geteilt mit",
|
||||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Kurze ID für das Verzeichnis. Muss auf allen Verbunds-Geräten gleich sein.",
|
"Short identifier for the folder. Must be the same on all cluster devices.": "Kurze ID für das Verzeichnis. Muss auf allen Verbunds-Geräten gleich sein.",
|
||||||
"Show ID": "ID anzeigen",
|
"Show ID": "ID anzeigen",
|
||||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstatt der Geräte ID angezeigt. Wird als optionaler Gerätename an die anderen Clients weitergegeben.",
|
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstatt der Geräte ID angezeigt. Wird als optionaler Gerätename an die anderen Clients im Cluster weitergegeben.",
|
||||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird anstatt der Geräte ID im Verbunds-Status angezeigt. Wird auf den Namen aktualisiert, den das Gerät angibt.",
|
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird anstatt der Geräte ID im Verbunds-Status angezeigt.Wird auf den Namen aktualisiert, den das Gerät angibt, wenn nichts eingetragen wird.",
|
||||||
"Shutdown": "Herunterfahren",
|
"Shutdown": "Herunterfahren",
|
||||||
"Shutdown Complete": "Vollständig Heruntergefahren",
|
"Shutdown Complete": "Vollständig Heruntergefahren",
|
||||||
"Simple File Versioning": "Einfache Dateiversionierung",
|
"Simple File Versioning": "Einfache Dateiversionierung",
|
||||||
@@ -160,13 +163,13 @@
|
|||||||
"Sync Protocol Listen Addresses": "Adresse(n) für das Synchronisierungsprotokoll",
|
"Sync Protocol Listen Addresses": "Adresse(n) für das Synchronisierungsprotokoll",
|
||||||
"Syncing": "Synchronisiere",
|
"Syncing": "Synchronisiere",
|
||||||
"Syncthing has been shut down.": "Syncthing wurde heruntergefahren.",
|
"Syncthing has been shut down.": "Syncthing wurde heruntergefahren.",
|
||||||
"Syncthing includes the following software or portions thereof:": "Syncthing enthält die folgende Software oder Teile davon:",
|
"Syncthing includes the following software or portions thereof:": "Syncthing enthält die folgende Software oder Teile von:",
|
||||||
"Syncthing is restarting.": "Syncthing wird neu gestartet",
|
"Syncthing is restarting.": "Syncthing wird neu gestartet",
|
||||||
"Syncthing is upgrading.": "Syncthing wird aktualisiert",
|
"Syncthing is upgrading.": "Syncthing wird aktualisiert",
|
||||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Deiner Internetverbindung. Versuche erneut...",
|
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Deiner Internetverbindung. Versuche erneut...",
|
||||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Es scheint als ob Syncthing ein Problem mit der Verarbeitung ihrer Eingabe hat. Bitte laden sie die Seite neu oder führen sie einen Neustart von Syncthing durch, falls das Problem weiterhin besteht.",
|
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing scheint ein Problem mit der Verarbeitung Deiner Eingabe zu haben. Bitte lade die Seite neu oder führe einen Neustart durch, falls das Problem weiterhin besteht.",
|
||||||
"The aggregated statistics are publicly available at {%url%}.": "Die gesammelten Statistiken sind öffentlich verfügbar unter {{url}}.",
|
"The aggregated statistics are publicly available at {%url%}.": "Die gesammelten Statistiken sind öffentlich verfügbar unter {{url}}.",
|
||||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber nicht aktiviert. Syncthing muss neugestartet werden um die neue Konfiguration zu übernehmen.",
|
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber noch nicht aktiviert. Syncthing muss neugestartet werden, um die neue Konfiguration zu übernehmen.",
|
||||||
"The device ID cannot be blank.": "Die Geräte ID darf nicht leer sein.",
|
"The device ID cannot be blank.": "Die Geräte ID darf nicht leer sein.",
|
||||||
"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).": "Die hier einzutragende Geräte ID kann im \"Bearbeiten > Zeige ID\"-Dialog auf dem anderen Gerät gefunden werden. Leerzeichen und Bindestriche sind optional (werden ignoriert).",
|
"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).": "Die hier einzutragende Geräte ID kann im \"Bearbeiten > Zeige ID\"-Dialog auf dem anderen Gerät gefunden werden. Leerzeichen und Bindestriche sind optional (werden ignoriert).",
|
||||||
"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.": "Der verschlüsselte Nutzungsbericht wird täglich gesendet. Er wird verwendet, um Statistiken über verwendete Betriebssysteme, Verzeichnis-Größen und Programm-Versionen zu erstellen. Sollte der Bericht in Zukunft weitere Daten erfassen, wird dieses Fenster erneut angezeigt.",
|
"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.": "Der verschlüsselte Nutzungsbericht wird täglich gesendet. Er wird verwendet, um Statistiken über verwendete Betriebssysteme, Verzeichnis-Größen und Programm-Versionen zu erstellen. Sollte der Bericht in Zukunft weitere Daten erfassen, wird dieses Fenster erneut angezeigt.",
|
||||||
@@ -175,19 +178,19 @@
|
|||||||
"The folder ID cannot be blank.": "Die Verzeichnis ID darf nicht leer sein.",
|
"The folder ID cannot be blank.": "Die Verzeichnis ID darf nicht leer sein.",
|
||||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis ID muss eine kurze Bezeichnung haben (64 Zeichen oder weniger). Sie darf nur aus Buchstaben, Zahlen und dem Punkt- (.), Bindestrich- (-), und Unterstrich- (_) Zeichen bestehen.",
|
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis ID muss eine kurze Bezeichnung haben (64 Zeichen oder weniger). Sie darf nur aus Buchstaben, Zahlen und dem Punkt- (.), Bindestrich- (-), und Unterstrich- (_) Zeichen bestehen.",
|
||||||
"The folder ID must be unique.": "Die Verzeichnis ID darf nur einmal existieren.",
|
"The folder ID must be unique.": "Die Verzeichnis ID darf nur einmal existieren.",
|
||||||
"The folder path cannot be blank.": "Der Verzeichnispfad darf nicht leer sein",
|
"The folder path cannot be blank.": "Der Verzeichnispfad darf nicht leer sein.",
|
||||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Es wird in folgenden Abständen versioniert: in der ersten Stunde wird alle 30 Sekunden eine Version behalten, am ersten Tag eine jede Stunde, in den ersten 30 Tagen eine jeden Tag, danach wird bis zum Höchstalter eine Version pro Woche beibehalten.",
|
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Es wird in folgenden Abständen versioniert: In der ersten Stunde wird alle 30 Sekunden eine Version behalten, am ersten Tag eine jede Stunde, in den ersten 30 Tagen eine jeden Tag. Danach wird bis zum angegebenen Höchstalter eine Version pro Woche behalten.",
|
||||||
"The following items could not be synchronized.": "Die folgenden Dateien konnten nicht synchronisiert werden.",
|
"The following items could not be synchronized.": "Die folgenden Objekte konnten nicht synchronisiert werden.",
|
||||||
"The maximum age must be a number and cannot be blank.": "Das Höchstalter muss angegeben werden und eine Zahl sein.",
|
"The maximum age must be a number and cannot be blank.": "Das Höchstalter muss angegeben werden und eine Zahl sein.",
|
||||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Die längste Zeit, die alte Versionen vorgehalten werden (in Tagen, 0 bedeutet, alte Versionen für immer zu behalten).",
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Die längste Zeit, die alte Versionen vorgehalten werden (in Tagen) (0 um alte Versionen für immer zu behalten).",
|
||||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Der Prozentsatz des minimal freien Festplattenspeichers muss eine nichtnegative Zahl zwischen (inklusive) 0 und 100 sein. ",
|
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Der Prozentsatz des minimal freien Festplattenspeichers muss eine nichtnegative Zahl zwischen (inklusive) 0 und 100 sein. ",
|
||||||
"The number of days must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Zahl und darf nicht leer sein.",
|
"The number of days must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Ganzzahl und darf nicht leer sein.",
|
||||||
"The number of days to keep files in the trash can. Zero means forever.": "Dauer in Tagen für welche die Dateien aufgehoben werden sollen. 0 bedeutet für immer.",
|
"The number of days to keep files in the trash can. Zero means forever.": "Dauer in Tagen für welche die Dateien aufgehoben werden sollen. 0 bedeutet für immer.",
|
||||||
"The number of old versions to keep, per file.": "Anzahl der alten Versionen, die von jeder Datei gespeichert werden sollen.",
|
"The number of old versions to keep, per file.": "Anzahl der alten Versionen, die von jeder Datei behalten werden sollen.",
|
||||||
"The number of versions must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Zahl und darf nicht leer sein.",
|
"The number of versions must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Ganzzahl und darf nicht leer sein.",
|
||||||
"The path cannot be blank.": "Der Pfad darf nicht leer sein.",
|
"The path cannot be blank.": "Der Pfad darf nicht leer sein.",
|
||||||
"The rate limit must be a non-negative number (0: no limit)": "Das Daterate-Limit muss eine nicht negative Anzahl sein (0: kein Limit)",
|
"The rate limit must be a non-negative number (0: no limit)": "Das Daterate-Limit muss eine nicht negative Anzahl sein (0 = kein Limit).",
|
||||||
"The rescan interval must be a non-negative number of seconds.": "Das Scanintervall muss eine nicht negative Anzahl von Sekunden sein.",
|
"The rescan interval must be a non-negative number of seconds.": "Das Scanintervall muss eine nicht negative Anzahl (in Sekunden) sein.",
|
||||||
"They are retried automatically and will be synced when the error is resolved.": "Sie werden automatisch heruntergeladen und werden synchronisiert, wenn der Fehler behoben wurde.",
|
"They are retried automatically and will be synced when the error is resolved.": "Sie werden automatisch heruntergeladen und werden synchronisiert, wenn der Fehler behoben wurde.",
|
||||||
"This is a major version upgrade.": "Dies ist eine neue Hauptversion.",
|
"This is a major version upgrade.": "Dies ist eine neue Hauptversion.",
|
||||||
"Trash Can File Versioning": "Papierkorb Dateiversionierung",
|
"Trash Can File Versioning": "Papierkorb Dateiversionierung",
|
||||||
@@ -195,7 +198,7 @@
|
|||||||
"Unshared": "Ungeteilt",
|
"Unshared": "Ungeteilt",
|
||||||
"Unused": "Ungenutzt",
|
"Unused": "Ungenutzt",
|
||||||
"Up to Date": "Aktuell",
|
"Up to Date": "Aktuell",
|
||||||
"Updated": "aktualisiert",
|
"Updated": "Aktualisiert",
|
||||||
"Upgrade": "Upgrade",
|
"Upgrade": "Upgrade",
|
||||||
"Upgrade To {%version%}": "Update auf {{version}}",
|
"Upgrade To {%version%}": "Update auf {{version}}",
|
||||||
"Upgrading": "Wird aktualisiert",
|
"Upgrading": "Wird aktualisiert",
|
||||||
@@ -204,12 +207,13 @@
|
|||||||
"Use HTTPS for GUI": "HTTPS für Benutzeroberfläche benutzen",
|
"Use HTTPS for GUI": "HTTPS für Benutzeroberfläche benutzen",
|
||||||
"Version": "Version",
|
"Version": "Version",
|
||||||
"Versions Path": "Versionierungspfad",
|
"Versions Path": "Versionierungspfad",
|
||||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Alte Versionen werden automatisch gelöscht, wenn sie älter als das angegebene Höchstalter sind oder die Höchstzahl der Dateien je Zeitabschnitt überschritten wird.",
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Alte Dateiversionen werden automatisch gelöscht, wenn sie älter als das angegebene Höchstalter sind oder die angegebene Höchstzahl an Dateien erreicht ist.",
|
||||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Gerätes, dass dieses Gerät auch auf der Gegenseite hinzugefügt werden muss.",
|
"When adding a new device, keep in mind that this device must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Gerätes, dass dieses Gerät auch auf den anderen Geräten hinzugefügt werden muss.",
|
||||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Beim Hinzufügen eines neuen Verzeichnisses beachte, dass die Verzeichnis ID dazu verwendet wird, Verzeichnisse zwischen Geräten zu verbinden. Die ID muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Beachte bitte beim Hinzufügen eines neuen Verzeichnisses, dass die Verzeichnis ID dazu verwendet wird, Verzeichnisse zwischen Geräten zu verbinden. Die ID muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"You must keep at least one version.": "Du musst mindestens eine Version behalten.",
|
"You must keep at least one version.": "Du musst mindestens eine Version behalten.",
|
||||||
|
"days": "Tage",
|
||||||
"full documentation": "Komplette Dokumentation",
|
"full documentation": "Komplette Dokumentation",
|
||||||
"items": "Einträge",
|
"items": "Objekte",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} möchte das Verzeichnis \"{{folder}}\" teilen."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} möchte das Verzeichnis \"{{folder}}\" teilen."
|
||||||
}
|
}
|
||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Να αντικατασταθούν οι αλλαγές",
|
"Override Changes": "Να αντικατασταθούν οι αλλαγές",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Μονοπάτι του φακέλου σε αυτόν τον υπολογιστή. Αν δεν υπάρχει θα δημιουργηθεί. Η περισπωμένη (~) μπορεί να μπει σαν συντόμευση για το",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Μονοπάτι του φακέλου σε αυτόν τον υπολογιστή. Αν δεν υπάρχει θα δημιουργηθεί. Η περισπωμένη (~) μπορεί να μπει σαν συντόμευση για το",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ο φάκελος στον οποίο θα αποθηκεύονται οι εκδόσεις των αρχείων (αν δεν οριστεί θα αποθηκεύονται στον υποφάκελο .stversions)",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ο φάκελος στον οποίο θα αποθηκεύονται οι εκδόσεις των αρχείων (αν δεν οριστεί θα αποθηκεύονται στον υποφάκελο .stversions)",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Παρακαλούμε, πριν από την εκτέλεση μιας σημαντικής αναβάθμισης, να συμβουλευτείς το σημείωμα που τη συνοδεύει. ",
|
"Please consult the release notes before performing a major upgrade.": "Παρακαλούμε, πριν από την εκτέλεση μιας σημαντικής αναβάθμισης, να συμβουλευτείς το σημείωμα που τη συνοδεύει. ",
|
||||||
"Please wait": "Παρακαλώ περιμένετε",
|
"Please wait": "Παρακαλώ περιμένετε",
|
||||||
"Preview": "Προεπισκόπηση",
|
"Preview": "Προεπισκόπηση",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Επανεκκίνηση",
|
"Restart": "Επανεκκίνηση",
|
||||||
"Restart Needed": "Απαιτείται επανεκκίνηση",
|
"Restart Needed": "Απαιτείται επανεκκίνηση",
|
||||||
"Restarting": "Επανεκκίνηση",
|
"Restarting": "Επανεκκίνηση",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Χρησιμοποιήθηκε ξανά",
|
"Reused": "Χρησιμοποιήθηκε ξανά",
|
||||||
"Save": "Αποθήκευση",
|
"Save": "Αποθήκευση",
|
||||||
"Scanning": "Έλεγχος για αλλαγές",
|
"Scanning": "Έλεγχος για αλλαγές",
|
||||||
@@ -209,6 +212,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.": "Όταν προσθέτεις έναν νέο φάκελο, θυμήσου πως η ταυτότητα ενός φακέλου χρησιμοποιείται για να να συσχετίσει φακέλους μεταξύ συσκευών. Η ταυτότητα του φακέλου θα πρέπει να είναι η ίδια σε όλες τις συσκευές και έχουν σημασία τα πεζά ή κεφαλαία γράμματα.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Όταν προσθέτεις έναν νέο φάκελο, θυμήσου πως η ταυτότητα ενός φακέλου χρησιμοποιείται για να να συσχετίσει φακέλους μεταξύ συσκευών. Η ταυτότητα του φακέλου θα πρέπει να είναι η ίδια σε όλες τις συσκευές και έχουν σημασία τα πεζά ή κεφαλαία γράμματα.",
|
||||||
"Yes": "Ναι",
|
"Yes": "Ναι",
|
||||||
"You must keep at least one version.": "Πρέπει να τηρήσεις τουλάχιστον μια έκδοση.",
|
"You must keep at least one version.": "Πρέπει να τηρήσεις τουλάχιστον μια έκδοση.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "πλήρης τεκμηρίωση",
|
"full documentation": "πλήρης τεκμηρίωση",
|
||||||
"items": "εγγραφές",
|
"items": "εγγραφές",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "Η συσκευή {{device}} θέλει να μοιράσει τον φάκελο «{{folder}}»."
|
"{%device%} wants to share folder \"{%folder%}\".": "Η συσκευή {{device}} θέλει να μοιράσει τον φάκελο «{{folder}}»."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Override Changes",
|
"Override Changes": "Override Changes",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
||||||
"Please wait": "Please wait",
|
"Please wait": "Please wait",
|
||||||
"Preview": "Preview",
|
"Preview": "Preview",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Restart",
|
"Restart": "Restart",
|
||||||
"Restart Needed": "Restart Needed",
|
"Restart Needed": "Restart Needed",
|
||||||
"Restarting": "Restarting",
|
"Restarting": "Restarting",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Reused",
|
"Reused": "Reused",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Scanning": "Scanning",
|
"Scanning": "Scanning",
|
||||||
@@ -209,6 +212,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.": "When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.",
|
||||||
"Yes": "Yes",
|
"Yes": "Yes",
|
||||||
"You must keep at least one version.": "You must keep at least one version.",
|
"You must keep at least one version.": "You must keep at least one version.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "full documentation",
|
"full documentation": "full documentation",
|
||||||
"items": "items",
|
"items": "items",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"Edit Folder": "Edit Folder",
|
"Edit Folder": "Edit Folder",
|
||||||
"Editing": "Editing",
|
"Editing": "Editing",
|
||||||
"Enable UPnP": "Enable UPnP",
|
"Enable UPnP": "Enable UPnP",
|
||||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated \"ip: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.",
|
||||||
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"External File Versioning": "External File Versioning",
|
"External File Versioning": "External File Versioning",
|
||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Override Changes",
|
"Override Changes": "Override Changes",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
||||||
"Please wait": "Please wait",
|
"Please wait": "Please wait",
|
||||||
"Preview": "Preview",
|
"Preview": "Preview",
|
||||||
@@ -120,6 +122,8 @@
|
|||||||
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
"RAM Utilization": "RAM Utilization",
|
"RAM Utilization": "RAM Utilization",
|
||||||
"Random": "Random",
|
"Random": "Random",
|
||||||
|
"Relayed via": "Relayed via",
|
||||||
|
"Relays": "Relays",
|
||||||
"Release Notes": "Release Notes",
|
"Release Notes": "Release Notes",
|
||||||
"Remove": "Remove",
|
"Remove": "Remove",
|
||||||
"Rescan": "Rescan",
|
"Rescan": "Rescan",
|
||||||
@@ -128,6 +132,7 @@
|
|||||||
"Restart": "Restart",
|
"Restart": "Restart",
|
||||||
"Restart Needed": "Restart Needed",
|
"Restart Needed": "Restart Needed",
|
||||||
"Restarting": "Restarting",
|
"Restarting": "Restarting",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Reused",
|
"Reused": "Reused",
|
||||||
"Save": "Save",
|
"Save": "Save",
|
||||||
"Scanning": "Scanning",
|
"Scanning": "Scanning",
|
||||||
@@ -207,6 +212,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.": "When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.",
|
||||||
"Yes": "Yes",
|
"Yes": "Yes",
|
||||||
"You must keep at least one version.": "You must keep at least one version.",
|
"You must keep at least one version.": "You must keep at least one version.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "full documentation",
|
"full documentation": "full documentation",
|
||||||
"items": "items",
|
"items": "items",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Anular cambios",
|
"Override Changes": "Anular cambios",
|
||||||
"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 en la máquina local. Se creará si no existe. El carácter de la tilde (~) puede usarse como atajo.",
|
"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 en la máquina local. Se creará si no existe. El carácter de la tilde (~) puede usarse como atajo.",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta donde se almacenarán las versiones (dejar vacío para usar la carpeta por defecto \".stversions\").",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta donde se almacenarán las versiones (dejar vacío para usar la carpeta por defecto \".stversions\").",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Por favor, consultar las notas de la versión antes de realizar una actualización importante.",
|
"Please consult the release notes before performing a major upgrade.": "Por favor, consultar las notas de la versión antes de realizar una actualización importante.",
|
||||||
"Please wait": "Por favor, espere",
|
"Please wait": "Por favor, espere",
|
||||||
"Preview": "Vista previa",
|
"Preview": "Vista previa",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Reiniciar",
|
"Restart": "Reiniciar",
|
||||||
"Restart Needed": "Reinicio necesario",
|
"Restart Needed": "Reinicio necesario",
|
||||||
"Restarting": "Reiniciando",
|
"Restarting": "Reiniciando",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Reutilizado",
|
"Reused": "Reutilizado",
|
||||||
"Save": "Guardar",
|
"Save": "Guardar",
|
||||||
"Scanning": "Rastreando",
|
"Scanning": "Rastreando",
|
||||||
@@ -209,6 +212,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.": "Cuando añada una nueva carpeta, tenga en cuenta que su ID se usa para unir carpetas entre dispositivos. Son sensibles a las mayúsculas y deben coincidir exactamente entre todos los dispositivos.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Cuando añada una nueva carpeta, tenga en cuenta que su ID se usa para unir carpetas entre dispositivos. Son sensibles a las mayúsculas y deben coincidir exactamente entre todos los dispositivos.",
|
||||||
"Yes": "Si",
|
"Yes": "Si",
|
||||||
"You must keep at least one version.": "Debes mantener al menos una versión.",
|
"You must keep at least one version.": "Debes mantener al menos una versión.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "Documentación completa",
|
"full documentation": "Documentación completa",
|
||||||
"items": "Elementos",
|
"items": "Elementos",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{folder}}\"."
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
"Add": "Agregar",
|
"Add": "Agregar",
|
||||||
"Add Device": "Agregar Dispositivo",
|
"Add Device": "Agregar Dispositivo",
|
||||||
"Add Folder": "Agregar Repositorio",
|
"Add Folder": "Agregar Repositorio",
|
||||||
"Add new folder?": "¿Agregar nuevo repositorio?",
|
"Add new folder?": "¿Agregar nueva carpeta?",
|
||||||
"Address": "Dirección",
|
"Address": "Dirección",
|
||||||
"Addresses": "Direcciones",
|
"Addresses": "Direcciones",
|
||||||
"Advanced": "Advanced",
|
"Advanced": "Avanzada",
|
||||||
"Advanced Configuration": "Advanced Configuration",
|
"Advanced Configuration": "Configuración avanzada",
|
||||||
"All Data": "Todos los datos",
|
"All Data": "Todos los datos",
|
||||||
"Allow Anonymous Usage Reporting?": "Permitir reporte anónimo de uso?",
|
"Allow Anonymous Usage Reporting?": "Permitir reporte anónimo de uso?",
|
||||||
"Alphabetic": "Alfabético",
|
"Alphabetic": "Alfabético",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"Anonymous Usage Reporting": "Reporte anónimo de uso",
|
"Anonymous Usage Reporting": "Reporte anónimo de uso",
|
||||||
"Any devices configured on an introducer device will be added to this device as well.": "Cualquier dispositivo configurado en un dispositivo introductor será también agregado a este dispositivo.",
|
"Any devices configured on an introducer device will be added to this device as well.": "Cualquier dispositivo configurado en un dispositivo introductor será también agregado a este dispositivo.",
|
||||||
"Automatic upgrades": "Actualizaciones automáticas",
|
"Automatic upgrades": "Actualizaciones automáticas",
|
||||||
"Be careful!": "Be careful!",
|
"Be careful!": "¡Sé cuidadoso!",
|
||||||
"Bugs": "Errores",
|
"Bugs": "Errores",
|
||||||
"CPU Utilization": "Uso de CPU",
|
"CPU Utilization": "Uso de CPU",
|
||||||
"Changelog": "Registro de cambios",
|
"Changelog": "Registro de cambios",
|
||||||
@@ -53,14 +53,14 @@
|
|||||||
"Enter ignore patterns, one per line.": "Añadir patrones de exclusión, uno por línea.",
|
"Enter ignore patterns, one per line.": "Añadir patrones de exclusión, uno por línea.",
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"External File Versioning": "Control de versiones externo",
|
"External File Versioning": "Control de versiones externo",
|
||||||
"Failed Items": "Failed Items",
|
"Failed Items": "Artículos fallidos",
|
||||||
"File Pull Order": "Orden para coger ficheros",
|
"File Pull Order": "Orden para coger ficheros",
|
||||||
"File Versioning": "Control de versiones",
|
"File Versioning": "Control de versiones",
|
||||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Los permisos de archivo son ignorados al buscar cambios. Usar el sistemas de archivos FAT.",
|
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Los permisos de archivo son ignorados al buscar cambios. Usar el sistemas de archivos FAT.",
|
||||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Los archivos son movidos al directorio .stversions cuando son reemplazados o eliminados por Syncthing.",
|
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Los archivos son movidos al directorio .stversions cuando son reemplazados o eliminados por Syncthing.",
|
||||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Lo archivos son movidos al directorio .stversions y renombrados a versiones marcadas por fecha cuando son reemplazados o eliminados por Syncthing,",
|
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Lo archivos son movidos al directorio .stversions y renombrados a versiones marcadas por fecha cuando son reemplazados o eliminados por 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.": "Los archivos están protegidos frente a los cambios realizados en otros dispositivos, peros los cambios realizados en este dispositivo serán envíados al resto del grupo",
|
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Los archivos están protegidos frente a los cambios realizados en otros dispositivos, peros los cambios realizados en este dispositivo serán envíados al resto del grupo",
|
||||||
"Folder": "Folder",
|
"Folder": "Carpeta",
|
||||||
"Folder ID": "ID del repositorio",
|
"Folder ID": "ID del repositorio",
|
||||||
"Folder Master": "Repositorio maestro",
|
"Folder Master": "Repositorio maestro",
|
||||||
"Folder Path": "Ruta del repositorio",
|
"Folder Path": "Ruta del repositorio",
|
||||||
@@ -106,13 +106,15 @@
|
|||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
"Off": "Apagado",
|
"Off": "Apagado",
|
||||||
"Oldest First": "Antiguo primero",
|
"Oldest First": "Antiguo primero",
|
||||||
"Options": "Options",
|
"Options": "Opciones",
|
||||||
"Out of Sync": "Out of Sync",
|
"Out of Sync": "Out of Sync",
|
||||||
"Out of Sync Items": "Ítems no sincronizados",
|
"Out of Sync Items": "Ítems no sincronizados",
|
||||||
"Outgoing Rate Limit (KiB/s)": "Tasa máxima de envío (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Tasa máxima de envío (KiB/s)",
|
||||||
"Override Changes": "Reemplazar los cambios",
|
"Override Changes": "Reemplazar los cambios",
|
||||||
"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 del repositorio en el equipo local. Será creado si no existe. El carácter tilde (~) puede ser utilizado como atajo de ",
|
"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 del repositorio en el equipo local. Será creado si no existe. El carácter tilde (~) puede ser utilizado como atajo de ",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta donde serán guardas las versiones (dejar vacío para usar el directorio predifinido \".stversions\" en el repositorio)",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta donde serán guardas las versiones (dejar vacío para usar el directorio predifinido \".stversions\" en el repositorio)",
|
||||||
|
"Pause": "Pausa",
|
||||||
|
"Paused": "En pausa",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Por favor consulta las notas de lanzamiento antes de realizar una actualizacón mayor.",
|
"Please consult the release notes before performing a major upgrade.": "Por favor consulta las notas de lanzamiento antes de realizar una actualizacón mayor.",
|
||||||
"Please wait": "Aguarde por favor",
|
"Please wait": "Aguarde por favor",
|
||||||
"Preview": "Vista previa",
|
"Preview": "Vista previa",
|
||||||
@@ -123,13 +125,14 @@
|
|||||||
"Relayed via": "Relayed via",
|
"Relayed via": "Relayed via",
|
||||||
"Relays": "Relays",
|
"Relays": "Relays",
|
||||||
"Release Notes": "Notas de lanzamiento",
|
"Release Notes": "Notas de lanzamiento",
|
||||||
"Remove": "Remove",
|
"Remove": "Eliminar",
|
||||||
"Rescan": "Reescanear",
|
"Rescan": "Reescanear",
|
||||||
"Rescan All": "Reescanear todo",
|
"Rescan All": "Reescanear todo",
|
||||||
"Rescan Interval": "Intervalo de reescaneo",
|
"Rescan Interval": "Intervalo de reescaneo",
|
||||||
"Restart": "Reiniciar",
|
"Restart": "Reiniciar",
|
||||||
"Restart Needed": "Es necesario reiniciar",
|
"Restart Needed": "Es necesario reiniciar",
|
||||||
"Restarting": "Reiniciando",
|
"Restarting": "Reiniciando",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Reutilizado",
|
"Reused": "Reutilizado",
|
||||||
"Save": "Guardar",
|
"Save": "Guardar",
|
||||||
"Scanning": "Actualización",
|
"Scanning": "Actualización",
|
||||||
@@ -154,7 +157,7 @@
|
|||||||
"Source Code": "Código fuente",
|
"Source Code": "Código fuente",
|
||||||
"Staggered File Versioning": "Versiones del archivo escalonado",
|
"Staggered File Versioning": "Versiones del archivo escalonado",
|
||||||
"Start Browser": "Iniciar navegador",
|
"Start Browser": "Iniciar navegador",
|
||||||
"Statistics": "Statistics",
|
"Statistics": "Estadísticas",
|
||||||
"Stopped": "Parado",
|
"Stopped": "Parado",
|
||||||
"Support": "Soporte",
|
"Support": "Soporte",
|
||||||
"Sync Protocol Listen Addresses": "Dirección de escucha del protocolo de sincronización",
|
"Sync Protocol Listen Addresses": "Dirección de escucha del protocolo de sincronización",
|
||||||
@@ -177,7 +180,7 @@
|
|||||||
"The folder ID must be unique.": "La ID del repositorio debe ser única.",
|
"The folder ID must be unique.": "La ID del repositorio debe ser única.",
|
||||||
"The folder path cannot be blank.": "La ruta del repositorio no puede estar vacía.",
|
"The folder path cannot be blank.": "La ruta del repositorio no puede estar vacía.",
|
||||||
"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.": "Los siguientes intervalos se utilizan: para la primera hora una versión se mantiene cada 30 segundos, para el primer día de una versión se mantiene cada hora, durante los primeros 30 días de la versión se mantiene todos los días, hasta que la edad máxima de una versión se mantiene cada semana.",
|
"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.": "Los siguientes intervalos se utilizan: para la primera hora una versión se mantiene cada 30 segundos, para el primer día de una versión se mantiene cada hora, durante los primeros 30 días de la versión se mantiene todos los días, hasta que la edad máxima de una versión se mantiene cada semana.",
|
||||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
"The following items could not be synchronized.": "Los siguientes artículos no pueden ser sincronizados.",
|
||||||
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar en blanco.",
|
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar en blanco.",
|
||||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El tiempo máximo para mantener una versión (en días, establece en 0 para mantener versiones para siempre).",
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El tiempo máximo para mantener una versión (en días, establece en 0 para mantener versiones para siempre).",
|
||||||
"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).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||||
@@ -209,6 +212,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.": "Al agregar un nuevo repositorio, tenga en cuenta que la ID del repositorio se utiliza para conectar los repositorios entre dispositivos. Se distingue entre mayúsculas y minúsculas y debe ser exactamente igual en todos los dispositivos.",
|
"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.": "Al agregar un nuevo repositorio, tenga en cuenta que la ID del repositorio se utiliza para conectar los repositorios entre dispositivos. Se distingue entre mayúsculas y minúsculas y debe ser exactamente igual en todos los dispositivos.",
|
||||||
"Yes": "Sí",
|
"Yes": "Sí",
|
||||||
"You must keep at least one version.": "Debe mantener al menos una versión",
|
"You must keep at least one version.": "Debe mantener al menos una versión",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "documentación completa",
|
"full documentation": "documentación completa",
|
||||||
"items": "ítems",
|
"items": "ítems",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir repositorio \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir repositorio \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Ohita muutokset",
|
"Override Changes": "Ohita muutokset",
|
||||||
"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": "Polku kansioon paikallisella tietokoneella. Kansio luodaan, ellei sitä ole olemassa. Tilde-merkkiä (~) voidaan käyttää oikotienä polulle",
|
"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": "Polku kansioon paikallisella tietokoneella. Kansio luodaan, ellei sitä ole olemassa. Tilde-merkkiä (~) voidaan käyttää oikotienä polulle",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Polku jonne versiot tullaan tallentamaan (jätä tyhjäksi oletusvalintaa .stversions varten).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Polku jonne versiot tullaan tallentamaan (jätä tyhjäksi oletusvalintaa .stversions varten).",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
||||||
"Please wait": "Ole hyvä ja odota",
|
"Please wait": "Ole hyvä ja odota",
|
||||||
"Preview": "Esikatselu",
|
"Preview": "Esikatselu",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Käynnistä uudelleen",
|
"Restart": "Käynnistä uudelleen",
|
||||||
"Restart Needed": "Uudelleenkäynnistys tarvitaan",
|
"Restart Needed": "Uudelleenkäynnistys tarvitaan",
|
||||||
"Restarting": "Käynnistetään uudelleen",
|
"Restarting": "Käynnistetään uudelleen",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Uudelleenkäytetty",
|
"Reused": "Uudelleenkäytetty",
|
||||||
"Save": "Tallenna",
|
"Save": "Tallenna",
|
||||||
"Scanning": "Skannataan",
|
"Scanning": "Skannataan",
|
||||||
@@ -209,6 +212,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.": "Lisättäessä uutta kansiota, muista että kansion ID:tä käytetään solmimaan kansiot yhteen laitteiden välillä. Ne ovat riippuvaisia kirjankoosta ja niiden tulee täsmätä kaikkien laitteiden välillä.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lisättäessä uutta kansiota, muista että kansion ID:tä käytetään solmimaan kansiot yhteen laitteiden välillä. Ne ovat riippuvaisia kirjankoosta ja niiden tulee täsmätä kaikkien laitteiden välillä.",
|
||||||
"Yes": "Kyllä",
|
"Yes": "Kyllä",
|
||||||
"You must keep at least one version.": "Sinun tulee säilyttää ainakin yksi versio.",
|
"You must keep at least one version.": "Sinun tulee säilyttää ainakin yksi versio.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "täysi dokumentaatio",
|
"full documentation": "täysi dokumentaatio",
|
||||||
"items": "kohteet",
|
"items": "kohteet",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} haluaa jakaa kansion \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} haluaa jakaa kansion \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Écraser les changements",
|
"Override Changes": "Écraser les changements",
|
||||||
"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": "Le chemin du dossier sur l'ordinateur local sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
|
"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": "Le chemin du dossier sur l'ordinateur local sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "En pause",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Veuillez consulter les notes de version avant de réaliser une mise à jour majeure.",
|
"Please consult the release notes before performing a major upgrade.": "Veuillez consulter les notes de version avant de réaliser une mise à jour majeure.",
|
||||||
"Please wait": "Merci de patienter",
|
"Please wait": "Merci de patienter",
|
||||||
"Preview": "Aperçu",
|
"Preview": "Aperçu",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Redémarrer",
|
"Restart": "Redémarrer",
|
||||||
"Restart Needed": "Redémarrage nécessaire",
|
"Restart Needed": "Redémarrage nécessaire",
|
||||||
"Restarting": "Redémarrage",
|
"Restarting": "Redémarrage",
|
||||||
|
"Resume": "Résumer",
|
||||||
"Reused": "Réutilisé",
|
"Reused": "Réutilisé",
|
||||||
"Save": "Sauver",
|
"Save": "Sauver",
|
||||||
"Scanning": "En cours de scan",
|
"Scanning": "En cours de scan",
|
||||||
@@ -209,6 +212,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.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que son ID est utilisé pour lier les répertoires à travers les appareils. Les ID sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que son ID est utilisé pour lier les répertoires à travers les appareils. Les ID sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
|
||||||
"Yes": "Oui",
|
"Yes": "Oui",
|
||||||
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
||||||
|
"days": "Jours",
|
||||||
"full documentation": "documentation complète",
|
"full documentation": "documentation complète",
|
||||||
"items": "éléments",
|
"items": "éléments",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Écraser les changements",
|
"Override Changes": "Écraser les changements",
|
||||||
"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": "Le chemin du dossier sur l'ordinateur local sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
|
"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": "Le chemin du dossier sur l'ordinateur local sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "En pause",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Veuillez consulter les notes de version avant de réaliser une mise à jour majeure.",
|
"Please consult the release notes before performing a major upgrade.": "Veuillez consulter les notes de version avant de réaliser une mise à jour majeure.",
|
||||||
"Please wait": "Merci de patienter",
|
"Please wait": "Merci de patienter",
|
||||||
"Preview": "Aperçu",
|
"Preview": "Aperçu",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Redémarrer",
|
"Restart": "Redémarrer",
|
||||||
"Restart Needed": "Redémarrage nécessaire",
|
"Restart Needed": "Redémarrage nécessaire",
|
||||||
"Restarting": "Redémarrage",
|
"Restarting": "Redémarrage",
|
||||||
|
"Resume": "Résumer",
|
||||||
"Reused": "Réutilisé",
|
"Reused": "Réutilisé",
|
||||||
"Save": "Sauver",
|
"Save": "Sauver",
|
||||||
"Scanning": "En cours de scan",
|
"Scanning": "En cours de scan",
|
||||||
@@ -209,6 +212,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.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que son ID est utilisé pour lier les répertoires à travers les appareils. Les ID sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que son ID est utilisé pour lier les répertoires à travers les appareils. Les ID sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
|
||||||
"Yes": "Oui",
|
"Yes": "Oui",
|
||||||
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
||||||
|
"days": "Jours",
|
||||||
"full documentation": "documentation complète",
|
"full documentation": "documentation complète",
|
||||||
"items": "éléments",
|
"items": "éléments",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\"."
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
"A new major version may not be compatible with previous versions.": "Az új főverzió nem kompatibilis az előző főverzióval.",
|
"A new major version may not be compatible with previous versions.": "Az új főverzió nem kompatibilis az előző főverzióval.",
|
||||||
"API Key": "API kulcs",
|
"API Key": "API kulcs",
|
||||||
"About": "Névjegy",
|
"About": "Névjegy",
|
||||||
"Actions": "Tevékenységek",
|
"Actions": "Műveletek",
|
||||||
"Add": "Hozzáadás",
|
"Add": "Hozzáadás",
|
||||||
"Add Device": "Eszköz hozzáadása",
|
"Add Device": "Eszköz hozzáadása",
|
||||||
"Add Folder": "Mappa hozzáadása",
|
"Add Folder": "Mappa hozzáadása",
|
||||||
"Add new folder?": " ",
|
"Add new folder?": " Új mappa hozzáadás?",
|
||||||
"Address": "Cím",
|
"Address": "Cím",
|
||||||
"Addresses": "Címek",
|
"Addresses": "Címek",
|
||||||
"Advanced": "Speciális",
|
"Advanced": "Haladó",
|
||||||
"Advanced Configuration": "Speciális beállítások",
|
"Advanced Configuration": "Haladó beállítások",
|
||||||
"All Data": "Minden adat",
|
"All Data": "Minden adat",
|
||||||
"Allow Anonymous Usage Reporting?": "Engedélyezed a névtelen felhasználási adatok küldését?",
|
"Allow Anonymous Usage Reporting?": "Engedélyezed a névtelen felhasználási adatok küldését?",
|
||||||
"Alphabetic": "ABC rendben",
|
"Alphabetic": "ABC rendben",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"Bugs": "Hibák",
|
"Bugs": "Hibák",
|
||||||
"CPU Utilization": "Processzor használat",
|
"CPU Utilization": "Processzor használat",
|
||||||
"Changelog": "Változások",
|
"Changelog": "Változások",
|
||||||
"Clean out after": "Clean out after",
|
"Clean out after": "Takarítás",
|
||||||
"Close": "Bezárás",
|
"Close": "Bezárás",
|
||||||
"Command": "Parancs",
|
"Command": "Parancs",
|
||||||
"Comment, when used at the start of a line": "Megjegyzés, a sor elején használva",
|
"Comment, when used at the start of a line": "Megjegyzés, a sor elején használva",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"Device ID": "Eszköz azonosító",
|
"Device ID": "Eszköz azonosító",
|
||||||
"Device Identification": "Eszköz azonosító",
|
"Device Identification": "Eszköz azonosító",
|
||||||
"Device Name": "Eszköz neve",
|
"Device Name": "Eszköz neve",
|
||||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "{{device}} ({{address}}) csatlakozni szeretne.",
|
"Device {%device%} ({%address%}) wants to connect. Add new device?": "{{device}} ({{address}}) csatlakozni szeretne. Hozzáadod az új eszközt?",
|
||||||
"Devices": "Eszközök",
|
"Devices": "Eszközök",
|
||||||
"Disconnected": "Kapcsolat bontva",
|
"Disconnected": "Kapcsolat bontva",
|
||||||
"Documentation": "Dokumentáció",
|
"Documentation": "Dokumentáció",
|
||||||
@@ -49,14 +49,14 @@
|
|||||||
"Edit Folder": "Mappa szerkesztése",
|
"Edit Folder": "Mappa szerkesztése",
|
||||||
"Editing": "Szerkesztés",
|
"Editing": "Szerkesztés",
|
||||||
"Enable UPnP": "UPnP engedélyezése",
|
"Enable UPnP": "UPnP engedélyezése",
|
||||||
"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.": "Vesszővel elválasztva több cím is bevihető (\"tcp://ip:port\", \"tcp://host:port\"), az automatikus felderítéshez a 'dynamic' kulcsszó használatos. ",
|
||||||
"Enter ignore patterns, one per line.": "Figyelmen kívül hagyáshoz ide írhatod a mintákat, soronként egyet",
|
"Enter ignore patterns, one per line.": "Figyelmen kívül hagyáshoz ide írhatod a mintákat, soronként egyet",
|
||||||
"Error": "Hiba",
|
"Error": "Hiba",
|
||||||
"External File Versioning": "Külső fájl verziózás",
|
"External File Versioning": "Külső fájl verziózás",
|
||||||
"Failed Items": "Failed Items",
|
"Failed Items": "Hibás elemek",
|
||||||
"File Pull Order": "Fájl küldési sorrend",
|
"File Pull Order": "Fájl küldési sorrend",
|
||||||
"File Versioning": "Fájl verziózás",
|
"File Versioning": "Fájl verziózás",
|
||||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Fájl jogosultságok figyelmen kívül hagyása változások keresésekor. FAT fájlrendszereken használatakor.",
|
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Fájl jogosultságok figyelmen kívül hagyása változások keresésekor. FAT fájlrendszerek használatakor.",
|
||||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Ha a Syncthing áthelyezi vagy törli a fájlokat, akkor azok a .stversions mappába lesznek áthelyezve.",
|
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Ha a Syncthing áthelyezi vagy törli a fájlokat, akkor azok a .stversions mappába lesznek áthelyezve.",
|
||||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Ha a Syncthing áthelyezi vagy törli a fájlokat, akkor azok a .stversions mappába lesznek áthelyezve, időbélyegzővel ellátva.",
|
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Ha a Syncthing áthelyezi vagy törli a fájlokat, akkor azok a .stversions mappába lesznek áthelyezve, időbélyegzővel ellátva.",
|
||||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "A fájlok védve vannak a más eszközökön történt változásokkal szemben, de az ezen az eszközön történt változások érvényesek lesznek a többire.",
|
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "A fájlok védve vannak a más eszközökön történt változásokkal szemben, de az ezen az eszközön történt változások érvényesek lesznek a többire.",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"Folder Master": "Központi mappa",
|
"Folder Master": "Központi mappa",
|
||||||
"Folder Path": "Mappa elérési útja",
|
"Folder Path": "Mappa elérési útja",
|
||||||
"Folders": "Mappák",
|
"Folders": "Mappák",
|
||||||
"GUI": "Felület",
|
"GUI": "Grafikus felület",
|
||||||
"GUI Authentication Password": "Grafikus felület jelszava",
|
"GUI Authentication Password": "Grafikus felület jelszava",
|
||||||
"GUI Authentication User": "Grafikus felület felhasználó neve ",
|
"GUI Authentication User": "Grafikus felület felhasználó neve ",
|
||||||
"GUI Listen Addresses": "Grafikus felület címe",
|
"GUI Listen Addresses": "Grafikus felület címe",
|
||||||
@@ -75,11 +75,11 @@
|
|||||||
"Global State": "Globális állapot",
|
"Global State": "Globális állapot",
|
||||||
"Help": "Segítség",
|
"Help": "Segítség",
|
||||||
"Home page": "Főoldal",
|
"Home page": "Főoldal",
|
||||||
"Ignore": "Visszautasítás",
|
"Ignore": "Figyelmen kívül hagyás",
|
||||||
"Ignore Patterns": "Figyelmen kívül hagyás",
|
"Ignore Patterns": "Minták figyelmen kívül hagyása",
|
||||||
"Ignore Permissions": "Jogosultságok figyelmen kívül hagyása",
|
"Ignore Permissions": "Jogosultságok figyelmen kívül hagyása",
|
||||||
"Incoming Rate Limit (KiB/s)": "Bejövő sebesség korlát (KIB/mp)",
|
"Incoming Rate Limit (KiB/s)": "Bejövő sebesség korlát (KIB/mp)",
|
||||||
"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.": "Helytelen konfiguráció esetén károsodhat a mappák tartalma és működésképtelenné válhat a Syncthing.",
|
||||||
"Introducer": "Bevezető",
|
"Introducer": "Bevezető",
|
||||||
"Inversion of the given condition (i.e. do not exclude)": "A feltétel ellentéte (pl. ki nem hagyás)",
|
"Inversion of the given condition (i.e. do not exclude)": "A feltétel ellentéte (pl. ki nem hagyás)",
|
||||||
"Keep Versions": "Megtartott verziók",
|
"Keep Versions": "Megtartott verziók",
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
"Major Upgrade": "Főverzió frissítés",
|
"Major Upgrade": "Főverzió frissítés",
|
||||||
"Maximum Age": "Maximális kor",
|
"Maximum Age": "Maximális kor",
|
||||||
"Metadata Only": "Csak metaadatok",
|
"Metadata Only": "Csak metaadatok",
|
||||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
"Minimum Free Disk Space": "Minimális szabad lemezterület",
|
||||||
"Move to top of queue": "Sor elejére mozgatás",
|
"Move to top of queue": "Sor elejére mozgatás",
|
||||||
"Multi level wildcard (matches multiple directory levels)": "Több szintű helyettesítő karakter (több könyvtár szintre érvényesül)",
|
"Multi level wildcard (matches multiple directory levels)": "Több szintű helyettesítő karakter (több könyvtár szintre érvényesül)",
|
||||||
"Never": "Soha",
|
"Never": "Soha",
|
||||||
@@ -106,30 +106,33 @@
|
|||||||
"OK": "Rendben",
|
"OK": "Rendben",
|
||||||
"Off": "Kikapcsolva",
|
"Off": "Kikapcsolva",
|
||||||
"Oldest First": "Régebbi először",
|
"Oldest First": "Régebbi először",
|
||||||
"Options": "Options",
|
"Options": "Opciók",
|
||||||
"Out of Sync": "Out of Sync",
|
"Out of Sync": "Nincs szinkronban",
|
||||||
"Out of Sync Items": "Nem szinkronizált elemek",
|
"Out of Sync Items": "Nem szinkronizált elemek",
|
||||||
"Outgoing Rate Limit (KiB/s)": "Kimenő sávszélesség (KiB/mp)",
|
"Outgoing Rate Limit (KiB/s)": "Kimenő sávszélesség (KiB/mp)",
|
||||||
"Override Changes": "Változtatások felülbírálása",
|
"Override Changes": "Változtatások felülbírálása",
|
||||||
"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": "A mappa elérési útja az eszközön. Amennyiben nem létezik, a program automatikusan létrehozza. A hullámvonal (~) a következő helyettesítésre használható: ",
|
"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": "A mappa elérési útja az eszközön. Amennyiben nem létezik, a program automatikusan létrehozza. A hullámvonal (~) a következő helyettesítésre használható: ",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Elérési út ahol a verziók tárolásra kerülnek (szabadon hagyva az alapértelmezett .stversions mappa lesz használva)",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Elérési út ahol a verziók tárolásra kerülnek (szabadon hagyva az alapértelmezett .stversions mappa lesz használva)",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
"Pause": "Szünet",
|
||||||
|
"Paused": "Szünetel",
|
||||||
|
"Please consult the release notes before performing a major upgrade.": "Nagyobb frissítés előtt ellenőrizni kell a kiadási megjegyzéseket.",
|
||||||
"Please wait": "Kérlek, várj",
|
"Please wait": "Kérlek, várj",
|
||||||
"Preview": "Előnézet",
|
"Preview": "Előnézet",
|
||||||
"Preview Usage Report": "Felhasználási adatok átnézése",
|
"Preview Usage Report": "Felhasználási adatok átnézése",
|
||||||
"Quick guide to supported patterns": "Rövid útmutató a használható mintákról",
|
"Quick guide to supported patterns": "Rövid útmutató a használható mintákról",
|
||||||
"RAM Utilization": "Memória használat",
|
"RAM Utilization": "Memória használat",
|
||||||
"Random": "Véletlenszerű",
|
"Random": "Véletlenszerű",
|
||||||
"Relayed via": "Relayed via",
|
"Relayed via": "Közvetítve",
|
||||||
"Relays": "Relays",
|
"Relays": "Közvetítések",
|
||||||
"Release Notes": "Kiadási megjegyzések",
|
"Release Notes": "Kiadási megjegyzések",
|
||||||
"Remove": "Remove",
|
"Remove": "Eltávolítás",
|
||||||
"Rescan": "Átnézés",
|
"Rescan": "Átnézés",
|
||||||
"Rescan All": "Összes átnézése",
|
"Rescan All": "Összes átnézése",
|
||||||
"Rescan Interval": "Átnézési intervallum",
|
"Rescan Interval": "Átnézési intervallum",
|
||||||
"Restart": "Újraindítás",
|
"Restart": "Újraindítás",
|
||||||
"Restart Needed": "Újraindítás szükséges",
|
"Restart Needed": "Újraindítás szükséges",
|
||||||
"Restarting": "Újraindulás",
|
"Restarting": "Újraindulás",
|
||||||
|
"Resume": "Folytatás",
|
||||||
"Reused": "Újrafelhasználva",
|
"Reused": "Újrafelhasználva",
|
||||||
"Save": "Mentés",
|
"Save": "Mentés",
|
||||||
"Scanning": "Átnézés",
|
"Scanning": "Átnézés",
|
||||||
@@ -147,7 +150,7 @@
|
|||||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Az eszköz azonosító helyett jelenik meg. A többi eszközön alapértelmezett névként használható. ",
|
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Az eszköz azonosító helyett jelenik meg. A többi eszközön alapértelmezett névként használható. ",
|
||||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Az eszköz azonosító helyett jelenik meg. Üresen hagyva az eszköz saját neve lesz használva ",
|
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Az eszköz azonosító helyett jelenik meg. Üresen hagyva az eszköz saját neve lesz használva ",
|
||||||
"Shutdown": "Leállítás",
|
"Shutdown": "Leállítás",
|
||||||
"Shutdown Complete": "Leállítás",
|
"Shutdown Complete": "Leállítás kész",
|
||||||
"Simple File Versioning": "Egyszerű fájl verziókövetés",
|
"Simple File Versioning": "Egyszerű fájl verziókövetés",
|
||||||
"Single level wildcard (matches within a directory only)": "Egyszintű helyettesítő karakter (csak egy mappára érvényes)",
|
"Single level wildcard (matches within a directory only)": "Egyszintű helyettesítő karakter (csak egy mappára érvényes)",
|
||||||
"Smallest First": "Kisebb előbb",
|
"Smallest First": "Kisebb előbb",
|
||||||
@@ -164,7 +167,7 @@
|
|||||||
"Syncthing is restarting.": "Syncthing újraindul",
|
"Syncthing is restarting.": "Syncthing újraindul",
|
||||||
"Syncthing is upgrading.": "Syncthing frissül",
|
"Syncthing is upgrading.": "Syncthing frissül",
|
||||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Úgy tűnik, hogy a Syncthing nem működik, vagy valami probléma van az hálózati kapcsolattal. Újra próbálom...",
|
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Úgy tűnik, hogy a Syncthing nem működik, vagy valami probléma van az hálózati kapcsolattal. Újra próbálom...",
|
||||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Úgy tűnik, hogy a Syncthing problémába ütközött a kérés feldolgozása során. Ha a probléma továbbra is fennáll, akkor frissíteni kell az oldalt, vagy újra kell indítani a Syncthinget.",
|
||||||
"The aggregated statistics are publicly available at {%url%}.": "Az összevont statisztikák nyilvánosan elérhetők a {{url}} címen.",
|
"The aggregated statistics are publicly available at {%url%}.": "Az összevont statisztikák nyilvánosan elérhetők a {{url}} címen.",
|
||||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A beállítások elmentésre kerültek, de nem lettek aktiválva. Indítsd újra a Syncthing-et, hogy aktiváld őket.",
|
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A beállítások elmentésre kerültek, de nem lettek aktiválva. Indítsd újra a Syncthing-et, hogy aktiváld őket.",
|
||||||
"The device ID cannot be blank.": "Az eszköz azonosító nem lehet üres.",
|
"The device ID cannot be blank.": "Az eszköz azonosító nem lehet üres.",
|
||||||
@@ -180,17 +183,17 @@
|
|||||||
"The following items could not be synchronized.": "A következő elemek nem szinkronizálhatóak.",
|
"The following items could not be synchronized.": "A következő elemek nem szinkronizálhatóak.",
|
||||||
"The maximum age must be a number and cannot be blank.": "A maximális kornak számnak kell lenni és nem lehet üres",
|
"The maximum age must be a number and cannot be blank.": "A maximális kornak számnak kell lenni és nem lehet üres",
|
||||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "A verziók megtartásának maximális ideje (napokban, ha 0-t adsz meg örökre megmaradnak).",
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "A verziók megtartásának maximális ideje (napokban, ha 0-t adsz meg örökre megmaradnak).",
|
||||||
"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).": "A minimális szabad terület százalékos. nem-negatív érték 0 és 100 között",
|
||||||
"The number of days must be a number and cannot be blank.": "A napok száma szám kell legyen és nem lehet üres.",
|
"The number of days must be a number and cannot be blank.": "A napok száma szám kell legyen és nem lehet üres.",
|
||||||
"The number of days to keep files in the trash can. Zero means forever.": "A napok száma ameddig a fájlok meg lesznek tartva a lomtárban. A 0 azt jelenti örökre.",
|
"The number of days to keep files in the trash can. Zero means forever.": "A napok száma ameddig a fájlok meg lesznek tartva a szemetesben. A 0 azt jelenti örökre.",
|
||||||
"The number of old versions to keep, per file.": "A megtartott régi verziók száma, fájlonként.",
|
"The number of old versions to keep, per file.": "A megtartott régi verziók száma, fájlonként.",
|
||||||
"The number of versions must be a number and cannot be blank.": "A megtartott verziók száma nem lehet üres",
|
"The number of versions must be a number and cannot be blank.": "A megtartott verziók száma nem lehet üres",
|
||||||
"The path cannot be blank.": "Elérési út nem lehet üres.",
|
"The path cannot be blank.": "Elérési út nem lehet üres.",
|
||||||
"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)": "Az arány limitnek pozitív számnak kell lennie (0: nincs limit)",
|
||||||
"The rescan interval must be a non-negative number of seconds.": "Az átnézési intervallum nullánál nagyobb másodperc érték kell legyen",
|
"The rescan interval must be a non-negative number of seconds.": "Az átnézési intervallum nullánál nagyobb másodperc érték kell legyen",
|
||||||
"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.",
|
"They are retried automatically and will be synced when the error is resolved.": "A hiba javítása után automatikusan újra megpróbálja a szinkronizálást.",
|
||||||
"This is a major version upgrade.": "Ez egy főverzió frissítés.",
|
"This is a major version upgrade.": "Ez egy főverzió frissítés.",
|
||||||
"Trash Can File Versioning": "Lomtár fájl verziózás",
|
"Trash Can File Versioning": "Szemetes fájl verziózás",
|
||||||
"Unknown": "Ismeretlen",
|
"Unknown": "Ismeretlen",
|
||||||
"Unshared": "Nincs megosztva",
|
"Unshared": "Nincs megosztva",
|
||||||
"Unused": "Nincs használatban",
|
"Unused": "Nincs használatban",
|
||||||
@@ -201,7 +204,7 @@
|
|||||||
"Upgrading": "Frissítés",
|
"Upgrading": "Frissítés",
|
||||||
"Upload Rate": "Feltöltési sebesség",
|
"Upload Rate": "Feltöltési sebesség",
|
||||||
"Uptime": "Üzemidő",
|
"Uptime": "Üzemidő",
|
||||||
"Use HTTPS for GUI": "HTTPS használata a GUI-hoz",
|
"Use HTTPS for GUI": "HTTPS használata a felülethez",
|
||||||
"Version": "Verzió",
|
"Version": "Verzió",
|
||||||
"Versions Path": "Verziók útvonala",
|
"Versions Path": "Verziók útvonala",
|
||||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "A régi verziók automatikusan törlődnek, amennyiben öregebbek mint a maximum kor, vagy már több van belőlük mint az adott időszakban megtartható maximum.",
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "A régi verziók automatikusan törlődnek, amennyiben öregebbek mint a maximum kor, vagy már több van belőlük mint az adott időszakban megtartható maximum.",
|
||||||
@@ -209,7 +212,8 @@
|
|||||||
"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.": "Amikor új mappát adsz hozzá, tartsd észben, hogy a mappa azonosító arra való hogy összekösd a mappákat az eszközeiden. Az azonosító kisbetű-nagybetű érzékeny és pontosan egyeznie kell az eszközökön.",
|
"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.": "Amikor új mappát adsz hozzá, tartsd észben, hogy a mappa azonosító arra való hogy összekösd a mappákat az eszközeiden. Az azonosító kisbetű-nagybetű érzékeny és pontosan egyeznie kell az eszközökön.",
|
||||||
"Yes": "Igen",
|
"Yes": "Igen",
|
||||||
"You must keep at least one version.": "Legalább egy verziót meg kell tartanod",
|
"You must keep at least one version.": "Legalább egy verziót meg kell tartanod",
|
||||||
|
"days": "nap",
|
||||||
"full documentation": "teljes dokumentáció",
|
"full documentation": "teljes dokumentáció",
|
||||||
"items": "elemek",
|
"items": "elem",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} meg szeretné osztani a \"{{folder}}\" nevű mappát."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} meg szeretné osztani a \"{{folder}}\" nevű mappát."
|
||||||
}
|
}
|
||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Ignora Modifiche",
|
"Override Changes": "Ignora Modifiche",
|
||||||
"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": "Percorso della cartella nel computer locale. Verrà creata se non esiste già. Il carattere tilde (~) può essere utilizzato come scorciatoia per",
|
"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": "Percorso della cartella nel computer locale. Verrà creata se non esiste già. Il carattere tilde (~) può essere utilizzato come scorciatoia per",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Percorso di salvataggio delle versioni (lasciare vuoto per utilizzare la cartella predefinita .stversions in questa cartella).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Percorso di salvataggio delle versioni (lasciare vuoto per utilizzare la cartella predefinita .stversions in questa cartella).",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Si prega di consultare le note di rilascio prima di eseguire un aggiornamento principale.",
|
"Please consult the release notes before performing a major upgrade.": "Si prega di consultare le note di rilascio prima di eseguire un aggiornamento principale.",
|
||||||
"Please wait": "Attendere prego",
|
"Please wait": "Attendere prego",
|
||||||
"Preview": "Anteprima",
|
"Preview": "Anteprima",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Riavvia",
|
"Restart": "Riavvia",
|
||||||
"Restart Needed": "Riavvio Necessario",
|
"Restart Needed": "Riavvio Necessario",
|
||||||
"Restarting": "Riavvio",
|
"Restarting": "Riavvio",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Riutilizzato",
|
"Reused": "Riutilizzato",
|
||||||
"Save": "Salva",
|
"Save": "Salva",
|
||||||
"Scanning": "Scansione in corso",
|
"Scanning": "Scansione in corso",
|
||||||
@@ -209,6 +212,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.": "Quando aggiungi una nuova cartella, ricordati che gli ID vengono utilizzati per collegare le cartelle nei dispositivi. Distinguono maiuscole e minuscole e devono corrispondere esattamente su tutti i dispositivi.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quando aggiungi una nuova cartella, ricordati che gli ID vengono utilizzati per collegare le cartelle nei dispositivi. Distinguono maiuscole e minuscole e devono corrispondere esattamente su tutti i dispositivi.",
|
||||||
"Yes": "Sì",
|
"Yes": "Sì",
|
||||||
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
|
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "documentazione completa",
|
"full documentation": "documentazione completa",
|
||||||
"items": "elementi",
|
"items": "elementi",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vuole condividere la cartella \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vuole condividere la cartella \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "他のデバイスの変更を上書きする",
|
"Override Changes": "他のデバイスの変更を上書きする",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "ローカルコンピュータ上のフォルダーパス。存在しない場合は作成されます。チルダ (~) で次のフォルダーを短縮入力できます:",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "ローカルコンピュータ上のフォルダーパス。存在しない場合は作成されます。チルダ (~) で次のフォルダーを短縮入力できます:",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "古いバージョンを保存するパス (空欄の場合、デフォルトで .stversions になります)。",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "古いバージョンを保存するパス (空欄の場合、デフォルトで .stversions になります)。",
|
||||||
|
"Pause": "一時停止",
|
||||||
|
"Paused": "一時停止中",
|
||||||
"Please consult the release notes before performing a major upgrade.": "メジャーアップグレードを行う前にリリースノートを参照してください。",
|
"Please consult the release notes before performing a major upgrade.": "メジャーアップグレードを行う前にリリースノートを参照してください。",
|
||||||
"Please wait": "お待ちください",
|
"Please wait": "お待ちください",
|
||||||
"Preview": "プレビュー",
|
"Preview": "プレビュー",
|
||||||
@@ -120,8 +122,8 @@
|
|||||||
"Quick guide to supported patterns": "サポートされているパターンの簡易ガイド",
|
"Quick guide to supported patterns": "サポートされているパターンの簡易ガイド",
|
||||||
"RAM Utilization": "メモリ使用量",
|
"RAM Utilization": "メモリ使用量",
|
||||||
"Random": "ランダム",
|
"Random": "ランダム",
|
||||||
"Relayed via": "Relayed via",
|
"Relayed via": "経由している中継サーバーのアドレス",
|
||||||
"Relays": "Relays",
|
"Relays": "中継サーバー",
|
||||||
"Release Notes": "リリースノート",
|
"Release Notes": "リリースノート",
|
||||||
"Remove": "除去",
|
"Remove": "除去",
|
||||||
"Rescan": "再スキャン",
|
"Rescan": "再スキャン",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "再起動",
|
"Restart": "再起動",
|
||||||
"Restart Needed": "再起動が必要です",
|
"Restart Needed": "再起動が必要です",
|
||||||
"Restarting": "再起動中",
|
"Restarting": "再起動中",
|
||||||
|
"Resume": "再開",
|
||||||
"Reused": "中断後再利用",
|
"Reused": "中断後再利用",
|
||||||
"Save": "保存",
|
"Save": "保存",
|
||||||
"Scanning": "スキャン中",
|
"Scanning": "スキャン中",
|
||||||
@@ -209,6 +212,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.": "新しいフォルダーを追加する際、フォルダーIDはデバイス間でフォルダーの対応づけに使われることに注意してください。フォルダーIDは大文字と小文字が区別され、共有するすべてのデバイスの間で完全に一致しなくてはなりません。",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "新しいフォルダーを追加する際、フォルダーIDはデバイス間でフォルダーの対応づけに使われることに注意してください。フォルダーIDは大文字と小文字が区別され、共有するすべてのデバイスの間で完全に一致しなくてはなりません。",
|
||||||
"Yes": "はい",
|
"Yes": "はい",
|
||||||
"You must keep at least one version.": "少なくとも一つのバージョンを保存してください。",
|
"You must keep at least one version.": "少なくとも一つのバージョンを保存してください。",
|
||||||
|
"days": "日",
|
||||||
"full documentation": "詳細なマニュアル",
|
"full documentation": "詳細なマニュアル",
|
||||||
"items": "項目",
|
"items": "項目",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} がフォルダー \"{{folder}}\" を共有するよう求めています。"
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} がフォルダー \"{{folder}}\" を共有するよう求めています。"
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "덮어쓰기",
|
"Override Changes": "덮어쓰기",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "로컬 컴퓨터에 있는 폴더의 경로를 지정합니다. 존재하지 않는 폴더일 경우 자동으로 생성됩니다. 물결 기호 (~)는 아래와 같은 폴더를 나타냅니다.",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "로컬 컴퓨터에 있는 폴더의 경로를 지정합니다. 존재하지 않는 폴더일 경우 자동으로 생성됩니다. 물결 기호 (~)는 아래와 같은 폴더를 나타냅니다.",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "버전을 보관할 경로 (비워둘 시 기본값 .stversions 폴더로 지정됨)",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "버전을 보관할 경로 (비워둘 시 기본값 .stversions 폴더로 지정됨)",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "메이저 업데이트를 하기 전에 먼저 릴리즈 노트를 살펴보세요.",
|
"Please consult the release notes before performing a major upgrade.": "메이저 업데이트를 하기 전에 먼저 릴리즈 노트를 살펴보세요.",
|
||||||
"Please wait": "기다려 주십시오",
|
"Please wait": "기다려 주십시오",
|
||||||
"Preview": "미리보기",
|
"Preview": "미리보기",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "재시작",
|
"Restart": "재시작",
|
||||||
"Restart Needed": "재시작 필요함",
|
"Restart Needed": "재시작 필요함",
|
||||||
"Restarting": "재시작 중",
|
"Restarting": "재시작 중",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "재개",
|
"Reused": "재개",
|
||||||
"Save": "저장",
|
"Save": "저장",
|
||||||
"Scanning": "탐색중",
|
"Scanning": "탐색중",
|
||||||
@@ -209,6 +212,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.": "새 폴더를 추가할 시 폴더 ID는 장치간에 폴더를 묶을 때 사용됩니다. 대소문자를 구분하며 모든 장치에서 같은 ID를 사용해야 합니다.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "새 폴더를 추가할 시 폴더 ID는 장치간에 폴더를 묶을 때 사용됩니다. 대소문자를 구분하며 모든 장치에서 같은 ID를 사용해야 합니다.",
|
||||||
"Yes": "예",
|
"Yes": "예",
|
||||||
"You must keep at least one version.": "최소 한 개의 버전은 유지해야 합니다.",
|
"You must keep at least one version.": "최소 한 개의 버전은 유지해야 합니다.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "전체 문서",
|
"full documentation": "전체 문서",
|
||||||
"items": "항목",
|
"items": "항목",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 에서 폴더 \\\"{{folder}}\\\" 를 공유하길 원합니다."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 에서 폴더 \\\"{{folder}}\\\" 를 공유하길 원합니다."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Perrašyti pakeitimus",
|
"Override Changes": "Perrašyti pakeitimus",
|
||||||
"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": "Kelias iki aplanko šiame kompiuteryje. Bus sukurtas, jei neegzistuoja. Tildės simbolis (~) gali būti naudojamas kaip trumpinys",
|
"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": "Kelias iki aplanko šiame kompiuteryje. Bus sukurtas, jei neegzistuoja. Tildės simbolis (~) gali būti naudojamas kaip trumpinys",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Kelias, kur bus saugomos versijos (palikite tuščią numatytam .stversions aplankui).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Kelias, kur bus saugomos versijos (palikite tuščią numatytam .stversions aplankui).",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Peržvelkite laidos informaciją prieš atlikdami stambų atnaujinimą.",
|
"Please consult the release notes before performing a major upgrade.": "Peržvelkite laidos informaciją prieš atlikdami stambų atnaujinimą.",
|
||||||
"Please wait": "Prašome palaukti",
|
"Please wait": "Prašome palaukti",
|
||||||
"Preview": "Peržiūra",
|
"Preview": "Peržiūra",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Perleisti",
|
"Restart": "Perleisti",
|
||||||
"Restart Needed": "Reikalingas perleidimas",
|
"Restart Needed": "Reikalingas perleidimas",
|
||||||
"Restarting": "Persileidžia",
|
"Restarting": "Persileidžia",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Pakartotinas",
|
"Reused": "Pakartotinas",
|
||||||
"Save": "Išsaugoti",
|
"Save": "Išsaugoti",
|
||||||
"Scanning": "Skenuojama",
|
"Scanning": "Skenuojama",
|
||||||
@@ -186,7 +189,7 @@
|
|||||||
"The number of old versions to keep, per file.": "Kiek failo versijų saugoti.",
|
"The number of old versions to keep, per file.": "Kiek failo versijų saugoti.",
|
||||||
"The number of versions must be a number and cannot be blank.": "Versijų skaičius turi būti skaitmuo ir negali būti tuščias laukelis.",
|
"The number of versions must be a number and cannot be blank.": "Versijų skaičius turi būti skaitmuo ir negali būti tuščias laukelis.",
|
||||||
"The path cannot be blank.": "Kelias negali būti tuščias.",
|
"The path cannot be blank.": "Kelias negali būti tuščias.",
|
||||||
"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)": "Srauto maksimalus greitis privalo būti ne neigiamas skaičius (0: nėra apribojimo)",
|
||||||
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
|
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
|
||||||
"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.",
|
"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 is a major version upgrade.": "Tai yra stambus atnaujinimas.",
|
"This is a major version upgrade.": "Tai yra stambus atnaujinimas.",
|
||||||
@@ -209,6 +212,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.": "Kai įvedate naują aplanką neužmirškite, kad jis bus naudojamas visuose įrenginiuose. Svarbu visur įvesti visiškai tokį pat aplanko vardą neužmirštant apie didžiąsias ir mažąsias raides.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Kai įvedate naują aplanką neužmirškite, kad jis bus naudojamas visuose įrenginiuose. Svarbu visur įvesti visiškai tokį pat aplanko vardą neužmirštant apie didžiąsias ir mažąsias raides.",
|
||||||
"Yes": "Taip",
|
"Yes": "Taip",
|
||||||
"You must keep at least one version.": "Būtina saugoti bent vieną versiją.",
|
"You must keep at least one version.": "Būtina saugoti bent vieną versiją.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "pilna dokumentacija",
|
"full documentation": "pilna dokumentacija",
|
||||||
"items": "įrašai",
|
"items": "įrašai",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} nori dalintis aplanku \"{{folder}}\""
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} nori dalintis aplanku \"{{folder}}\""
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
"About": "Om",
|
"About": "Om",
|
||||||
"Actions": "Handlinger",
|
"Actions": "Handlinger",
|
||||||
"Add": "Legg til",
|
"Add": "Legg til",
|
||||||
"Add Device": "Legg Til Enhet",
|
"Add Device": "Legg til Enhet",
|
||||||
"Add Folder": "Legg Til Mappe",
|
"Add Folder": "Legg til Mappe",
|
||||||
"Add new folder?": "Legg til ny mappe?",
|
"Add new folder?": "Legg til ny mappe?",
|
||||||
"Address": "Adresse",
|
"Address": "Adresse",
|
||||||
"Addresses": "Adresser",
|
"Addresses": "Adresser",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"Be careful!": "Vær forsiktig!",
|
"Be careful!": "Vær forsiktig!",
|
||||||
"Bugs": "Programfeil",
|
"Bugs": "Programfeil",
|
||||||
"CPU Utilization": "CPU-utnyttelse",
|
"CPU Utilization": "CPU-utnyttelse",
|
||||||
"Changelog": "Endringslog",
|
"Changelog": "Endringslogg",
|
||||||
"Clean out after": "Tøm etter",
|
"Clean out after": "Tøm etter",
|
||||||
"Close": "Lukk",
|
"Close": "Lukk",
|
||||||
"Command": "Kommando",
|
"Command": "Kommando",
|
||||||
@@ -31,28 +31,28 @@
|
|||||||
"Connection Error": "Tilkoblingsfeil",
|
"Connection Error": "Tilkoblingsfeil",
|
||||||
"Copied from elsewhere": "Kopiert fra et annet sted",
|
"Copied from elsewhere": "Kopiert fra et annet sted",
|
||||||
"Copied from original": "Kopiert fra original",
|
"Copied from original": "Kopiert fra original",
|
||||||
"Copyright © 2015 the following Contributors:": "Kopirett © 2015 de følgende bidragsytere:",
|
"Copyright © 2015 the following Contributors:": "Opphavsrett © 2015 de følgende bidragsytere:",
|
||||||
"Delete": "Slett",
|
"Delete": "Slett",
|
||||||
"Deleted": "Slettet",
|
"Deleted": "Slettet",
|
||||||
"Device ID": "Enhets ID",
|
"Device ID": "Enhets ID",
|
||||||
"Device Identification": "Enhetskjennemerke",
|
"Device Identification": "Enhetskjennemerke",
|
||||||
"Device Name": "Navn På Enhet",
|
"Device Name": "Navn på Enhet",
|
||||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enhet {{device}} ({{address}}) ønsker å koble seg til. Legg til ny enhet?",
|
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enhet {{device}} ({{address}}) ønsker å koble seg til. Legg til ny enhet?",
|
||||||
"Devices": "Enheter",
|
"Devices": "Enheter",
|
||||||
"Disconnected": "Frakoblet",
|
"Disconnected": "Frakoblet",
|
||||||
"Documentation": "Dokumentasjon",
|
"Documentation": "Dokumentasjon",
|
||||||
"Download Rate": "Nedlastingsrate",
|
"Download Rate": "Nedlastingsrate",
|
||||||
"Downloaded": "Lasted ned",
|
"Downloaded": "Lastet ned",
|
||||||
"Downloading": "Laster ned",
|
"Downloading": "Laster ned",
|
||||||
"Edit": "Rediger",
|
"Edit": "Rediger",
|
||||||
"Edit Device": "Rediger Enhet",
|
"Edit Device": "Rediger Enhet",
|
||||||
"Edit Folder": "Rediger Mappe",
|
"Edit Folder": "Rediger Mappe",
|
||||||
"Editing": "Redigerer",
|
"Editing": "Redigerer",
|
||||||
"Enable UPnP": "Aktiver UPnP",
|
"Enable UPnP": "Aktiver 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.": "Skriv inn kommaseparerte (\"tcp://ip:port\", \"tcp://host:port\") adresser, eller \"dynamic\" for å automatisk finne adressen.",
|
||||||
"Enter ignore patterns, one per line.": "Skriv inn mønster som skal utelates, ett per linje.",
|
"Enter ignore patterns, one per line.": "Skriv inn mønster som skal utelates, ett per linje.",
|
||||||
"Error": "Feilmelding",
|
"Error": "Feilmelding",
|
||||||
"External File Versioning": "Ekstern versjonskontroll",
|
"External File Versioning": "Ekstern Versjonskontroll",
|
||||||
"Failed Items": "Elementsynkronisering som har feilet",
|
"Failed Items": "Elementsynkronisering som har feilet",
|
||||||
"File Pull Order": "Filenes Henterekkefølge",
|
"File Pull Order": "Filenes Henterekkefølge",
|
||||||
"File Versioning": "Versjonskontroll",
|
"File Versioning": "Versjonskontroll",
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
"Maximum Age": "Maksimal Levetid",
|
"Maximum Age": "Maksimal Levetid",
|
||||||
"Metadata Only": "Kun metadata",
|
"Metadata Only": "Kun metadata",
|
||||||
"Minimum Free Disk Space": "Nødvendig ledig diskplass",
|
"Minimum Free Disk Space": "Nødvendig ledig diskplass",
|
||||||
"Move to top of queue": "Flytt til topp av kø",
|
"Move to top of queue": "Flytt fremst i køen",
|
||||||
"Multi level wildcard (matches multiple directory levels)": "Multinivåsøk (søker på flere mappenivå)",
|
"Multi level wildcard (matches multiple directory levels)": "Multinivåsøk (søker på flere mappenivå)",
|
||||||
"Never": "Aldri",
|
"Never": "Aldri",
|
||||||
"New Device": "Ny Enhet",
|
"New Device": "Ny Enhet",
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
"Newest First": "Den nyeste først",
|
"Newest First": "Den nyeste først",
|
||||||
"No": "Nei",
|
"No": "Nei",
|
||||||
"No File Versioning": "Ingen Versjonskontroll",
|
"No File Versioning": "Ingen Versjonskontroll",
|
||||||
"Notice": "Merknad",
|
"Notice": "Merknader",
|
||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
"Off": "Av",
|
"Off": "Av",
|
||||||
"Oldest First": "Den eldste først",
|
"Oldest First": "Den eldste først",
|
||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Overstyr Endringer",
|
"Override Changes": "Overstyr Endringer",
|
||||||
"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": "Plasseringen av mappen på datamaskinen. Denne vil bli opprettet dersom den ikke finnes. Krøllstrektegnet (~) kan brukes som forkortelse for",
|
"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": "Plasseringen av mappen på datamaskinen. Denne vil bli opprettet dersom den ikke finnes. Krøllstrektegnet (~) kan brukes som forkortelse for",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Plasseringen for lagrede versjoner (la denne være tom for å bruke standard .stversions-mappen i mappen).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Plasseringen for lagrede versjoner (la denne være tom for å bruke standard .stversions-mappen i mappen).",
|
||||||
|
"Pause": "Oppholde",
|
||||||
|
"Paused": "Oppholdt",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Se \"release notes\" før en hovedoppgradering utføres.",
|
"Please consult the release notes before performing a major upgrade.": "Se \"release notes\" før en hovedoppgradering utføres.",
|
||||||
"Please wait": "Vennligst vent",
|
"Please wait": "Vennligst vent",
|
||||||
"Preview": "Forhåndsvisning",
|
"Preview": "Forhåndsvisning",
|
||||||
@@ -120,8 +122,8 @@
|
|||||||
"Quick guide to supported patterns": "Kjapp innføring i godkjente mønster",
|
"Quick guide to supported patterns": "Kjapp innføring i godkjente mønster",
|
||||||
"RAM Utilization": "RAM-utnyttelse",
|
"RAM Utilization": "RAM-utnyttelse",
|
||||||
"Random": "Tilfeldig",
|
"Random": "Tilfeldig",
|
||||||
"Relayed via": "Relayed via",
|
"Relayed via": "Relé via",
|
||||||
"Relays": "Relays",
|
"Relays": "Reléer",
|
||||||
"Release Notes": "Utgivelsesnotat",
|
"Release Notes": "Utgivelsesnotat",
|
||||||
"Remove": "Fjern",
|
"Remove": "Fjern",
|
||||||
"Rescan": "Skann på nytt",
|
"Rescan": "Skann på nytt",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Omstart",
|
"Restart": "Omstart",
|
||||||
"Restart Needed": "Omstart Kreves",
|
"Restart Needed": "Omstart Kreves",
|
||||||
"Restarting": "Starter På Ny",
|
"Restarting": "Starter På Ny",
|
||||||
|
"Resume": "Gjenoppta",
|
||||||
"Reused": "Gjenbrukt",
|
"Reused": "Gjenbrukt",
|
||||||
"Save": "Lagre",
|
"Save": "Lagre",
|
||||||
"Scanning": "Skanner",
|
"Scanning": "Skanner",
|
||||||
@@ -141,7 +144,7 @@
|
|||||||
"Share Folders With Device": "Del Mapper Med Enhet",
|
"Share Folders With Device": "Del Mapper Med Enhet",
|
||||||
"Share With Devices": "Del Med Enheter",
|
"Share With Devices": "Del Med Enheter",
|
||||||
"Share this folder?": "Dele denne mappen?",
|
"Share this folder?": "Dele denne mappen?",
|
||||||
"Shared With": "Del 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.",
|
"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",
|
"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 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.",
|
||||||
@@ -190,7 +193,7 @@
|
|||||||
"The rescan interval must be a non-negative number of seconds.": "Antall sekund i skanneintervallet kan ikke være negativt.",
|
"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.",
|
"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 is a major version upgrade.": "Dette er en hovedoppgradering",
|
"This is a major version upgrade.": "Dette er en hovedoppgradering",
|
||||||
"Trash Can File Versioning": "Fil versjonskontroll i papirkurven",
|
"Trash Can File Versioning": "Fil Versjonskontroll i papirkurven",
|
||||||
"Unknown": "Ukjent",
|
"Unknown": "Ukjent",
|
||||||
"Unshared": "Ikke delt",
|
"Unshared": "Ikke delt",
|
||||||
"Unused": "Ikke i bruk",
|
"Unused": "Ikke i bruk",
|
||||||
@@ -209,6 +212,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.": "Når en ny mappe blir lagt til, husk at Mappe-ID blir brukt til å binde sammen mapper mellom enheter. Det er forskjell på store og små bokstaver, så IDene må være identiske på alle enhetene.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Når en ny mappe blir lagt til, husk at Mappe-ID blir brukt til å binde sammen mapper mellom enheter. Det er forskjell på store og små bokstaver, så IDene må være identiske på alle enhetene.",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"You must keep at least one version.": "Du må beholde minst én versjon",
|
"You must keep at least one version.": "Du må beholde minst én versjon",
|
||||||
|
"days": "dager",
|
||||||
"full documentation": "all dokumentasjon",
|
"full documentation": "all dokumentasjon",
|
||||||
"items": "element",
|
"items": "element",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\"."
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"Edit Folder": "Bewerk map",
|
"Edit Folder": "Bewerk map",
|
||||||
"Editing": "Bezig met bewerken",
|
"Editing": "Bezig met bewerken",
|
||||||
"Enable UPnP": "UPnP gebruiken",
|
"Enable UPnP": "UPnP gebruiken",
|
||||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Voer door komma's gescheiden (\"tcp://ip:port\", \"tcp://host:port\") adressen of \"dynamisch\" in om automatische ontdekking van het adres uit te voeren.",
|
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Voer door komma's gescheiden (\"tcp://ip:port\", \"tcp://host:port\") adressen in of voer \"dynamisch\" in om automatische ontdekking van het adres uit te voeren.",
|
||||||
"Enter ignore patterns, one per line.": "Voer negeerpatronen in, één per regel.",
|
"Enter ignore patterns, one per line.": "Voer negeerpatronen in, één per regel.",
|
||||||
"Error": "Fout",
|
"Error": "Fout",
|
||||||
"External File Versioning": "Extern versiebeheer voor bestanden",
|
"External File Versioning": "Extern versiebeheer voor bestanden",
|
||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Veranderingen overschrijven",
|
"Override Changes": "Veranderingen overschrijven",
|
||||||
"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": "Locatie van de map op de lokale computer. Als deze niet bestaat zal de map aangemaakt worden. De tilde (~) kan gebruikt worden als snelkoppeling voor ",
|
"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": "Locatie van de map op de lokale computer. Als deze niet bestaat zal de map aangemaakt worden. De tilde (~) kan gebruikt worden als snelkoppeling voor ",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Betandspad waar de versies opgeslagen moeten worden (leeg laten voor de standaard .stversions subfolder).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Betandspad waar de versies opgeslagen moeten worden (leeg laten voor de standaard .stversions subfolder).",
|
||||||
|
"Pause": "Pauze",
|
||||||
|
"Paused": "Gepauseerd",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Lees eerst de release notes voordat u een grote update uitvoert.",
|
"Please consult the release notes before performing a major upgrade.": "Lees eerst de release notes voordat u een grote update uitvoert.",
|
||||||
"Please wait": "Even geduld",
|
"Please wait": "Even geduld",
|
||||||
"Preview": "Preview",
|
"Preview": "Preview",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Herstart",
|
"Restart": "Herstart",
|
||||||
"Restart Needed": "Herstart nodig",
|
"Restart Needed": "Herstart nodig",
|
||||||
"Restarting": "Herstarten",
|
"Restarting": "Herstarten",
|
||||||
|
"Resume": "Hervatten",
|
||||||
"Reused": "Hergebruikt",
|
"Reused": "Hergebruikt",
|
||||||
"Save": "Bewaar",
|
"Save": "Bewaar",
|
||||||
"Scanning": "Aan het zoeken",
|
"Scanning": "Aan het zoeken",
|
||||||
@@ -209,6 +212,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.": "Houd er bij het toevoegen van nieuwe mappen rekening mee dat het map-ID gebruikt wordt om mappen tussen apparaten te verbinden. Dit ID is hoofdlettergevoelig en moet identiek zijn op andere apparaten.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Houd er bij het toevoegen van nieuwe mappen rekening mee dat het map-ID gebruikt wordt om mappen tussen apparaten te verbinden. Dit ID is hoofdlettergevoelig en moet identiek zijn op andere apparaten.",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"You must keep at least one version.": "Minstens 1 versie moet bewaard blijven.",
|
"You must keep at least one version.": "Minstens 1 versie moet bewaard blijven.",
|
||||||
|
"days": "dagen",
|
||||||
"full documentation": "volledige documentatie",
|
"full documentation": "volledige documentatie",
|
||||||
"items": "objecten",
|
"items": "objecten",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wil de map \"{{folder}}\" delen."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wil de map \"{{folder}}\" delen."
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"A new major version may not be compatible with previous versions.": "A new major version may not be compatible with previous versions.",
|
"A new major version may not be compatible with previous versions.": "A new major version may not be compatible with previous versions.",
|
||||||
"API Key": "API-nøkkel",
|
"API Key": "API-nøkkel",
|
||||||
"About": "Om",
|
"About": "Om",
|
||||||
"Actions": "Actions",
|
"Actions": "Handlingar",
|
||||||
"Add": "Legg til",
|
"Add": "Legg til",
|
||||||
"Add Device": "Legg Til Eining",
|
"Add Device": "Legg Til Eining",
|
||||||
"Add Folder": "Legg Til Mappe",
|
"Add Folder": "Legg Til Mappe",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"Address": "Adresse",
|
"Address": "Adresse",
|
||||||
"Addresses": "Adresser",
|
"Addresses": "Adresser",
|
||||||
"Advanced": "Avansert",
|
"Advanced": "Avansert",
|
||||||
"Advanced Configuration": "Advanced Configuration",
|
"Advanced Configuration": "Avansert konfigurasjon",
|
||||||
"All Data": "Alle data",
|
"All Data": "Alle data",
|
||||||
"Allow Anonymous Usage Reporting?": "Tillata anonymisert bruksrapportering?",
|
"Allow Anonymous Usage Reporting?": "Tillata anonymisert bruksrapportering?",
|
||||||
"Alphabetic": "Alfabetisk",
|
"Alphabetic": "Alfabetisk",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"Connection Error": "Tilkoplingsfeil",
|
"Connection Error": "Tilkoplingsfeil",
|
||||||
"Copied from elsewhere": "Kopiert frå ein annan stad",
|
"Copied from elsewhere": "Kopiert frå ein annan stad",
|
||||||
"Copied from original": "Kopiert frå originalen",
|
"Copied from original": "Kopiert frå originalen",
|
||||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
"Copyright © 2015 the following Contributors:": "Opphavsrett © 2015 og desse bidragsytarane:",
|
||||||
"Delete": "Slett",
|
"Delete": "Slett",
|
||||||
"Deleted": "Sletta",
|
"Deleted": "Sletta",
|
||||||
"Device ID": "Eining ID",
|
"Device ID": "Eining ID",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"Edit Folder": "Rediger Mappe",
|
"Edit Folder": "Rediger Mappe",
|
||||||
"Editing": "Redigerer",
|
"Editing": "Redigerer",
|
||||||
"Enable UPnP": "Aktiver UPnP",
|
"Enable UPnP": "Aktiver 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.": "Skriv inn adresser med komma mellom kvar adresse (\"tcp://ip:port\", \"tcp://host:port\"), eller \"dynamic\" for å automatisk søkja opp adressa.",
|
||||||
"Enter ignore patterns, one per line.": "Skriv inn mønster som skal utelatast, eitt per linje.",
|
"Enter ignore patterns, one per line.": "Skriv inn mønster som skal utelatast, eitt per linje.",
|
||||||
"Error": "Feilmelding",
|
"Error": "Feilmelding",
|
||||||
"External File Versioning": "Ekstern filutgåvehandtering",
|
"External File Versioning": "Ekstern filutgåvehandtering",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"Folder Master": "Styrande Mappe",
|
"Folder Master": "Styrande Mappe",
|
||||||
"Folder Path": "Mappeplassering",
|
"Folder Path": "Mappeplassering",
|
||||||
"Folders": "Mapper",
|
"Folders": "Mapper",
|
||||||
"GUI": "GUI",
|
"GUI": "grafisk brukargrensesnitt",
|
||||||
"GUI Authentication Password": "GUI Passord",
|
"GUI Authentication Password": "GUI Passord",
|
||||||
"GUI Authentication User": "GUI Brukar",
|
"GUI Authentication User": "GUI Brukar",
|
||||||
"GUI Listen Addresses": "GUI Lytteadresse",
|
"GUI Listen Addresses": "GUI Lytteadresse",
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
"Introducer": "Introduktør",
|
"Introducer": "Introduktør",
|
||||||
"Inversion of the given condition (i.e. do not exclude)": "Det motsette av den gitte tilstanden (dvs. ekskluder ikkje)",
|
"Inversion of the given condition (i.e. do not exclude)": "Det motsette av den gitte tilstanden (dvs. ekskluder ikkje)",
|
||||||
"Keep Versions": "Behald Versjonar",
|
"Keep Versions": "Behald Versjonar",
|
||||||
"Largest First": "Largest First",
|
"Largest First": "Største fyrst",
|
||||||
"Last File Received": "Siste mottatte fila",
|
"Last File Received": "Siste mottatte fila",
|
||||||
"Last seen": "Sist sett",
|
"Last seen": "Sist sett",
|
||||||
"Later": "Seinare",
|
"Later": "Seinare",
|
||||||
@@ -106,13 +106,15 @@
|
|||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
"Off": "Av",
|
"Off": "Av",
|
||||||
"Oldest First": "Elste fyrst",
|
"Oldest First": "Elste fyrst",
|
||||||
"Options": "Options",
|
"Options": "Val",
|
||||||
"Out of Sync": "Ikkje synkronisert",
|
"Out of Sync": "Ikkje synkronisert",
|
||||||
"Out of Sync Items": "Ikkje-synkroniserte element",
|
"Out of Sync Items": "Ikkje-synkroniserte element",
|
||||||
"Outgoing Rate Limit (KiB/s)": "Utgåande hastigheitsgrense (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Utgåande hastigheitsgrense (KiB/s)",
|
||||||
"Override Changes": "Overstyr endringar",
|
"Override Changes": "Overstyr endringar",
|
||||||
"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": "Plasseringa av mappa på datamaskinen. Vert oppretta om ho ikkje finst. Krøllstrekteiknet (~) kan brukast som forkorting for",
|
"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": "Plasseringa av mappa på datamaskinen. Vert oppretta om ho ikkje finst. Krøllstrekteiknet (~) kan brukast som forkorting for",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Plasseringa for lagra versjonar (la denne vera tom for å bruka standard .stversions-mappa i mappa).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Plasseringa for lagra versjonar (la denne vera tom for å bruka standard .stversions-mappa i mappa).",
|
||||||
|
"Pause": "Stans",
|
||||||
|
"Paused": "Stansa",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
||||||
"Please wait": "Gjer vel og vent",
|
"Please wait": "Gjer vel og vent",
|
||||||
"Preview": "Førehandsvisning",
|
"Preview": "Førehandsvisning",
|
||||||
@@ -122,14 +124,15 @@
|
|||||||
"Random": "Tilfeldig",
|
"Random": "Tilfeldig",
|
||||||
"Relayed via": "Relayed via",
|
"Relayed via": "Relayed via",
|
||||||
"Relays": "Relays",
|
"Relays": "Relays",
|
||||||
"Release Notes": "Release Notes",
|
"Release Notes": "Utgivingsnotat",
|
||||||
"Remove": "Remove",
|
"Remove": "Fjern",
|
||||||
"Rescan": "Skann På Ny",
|
"Rescan": "Skann På Ny",
|
||||||
"Rescan All": "Skann alle på nytt",
|
"Rescan All": "Skann alle på nytt",
|
||||||
"Rescan Interval": "Skanneintervall",
|
"Rescan Interval": "Skanneintervall",
|
||||||
"Restart": "Omstart",
|
"Restart": "Omstart",
|
||||||
"Restart Needed": "Omstart Trengs",
|
"Restart Needed": "Omstart Trengs",
|
||||||
"Restarting": "Startar På Ny",
|
"Restarting": "Startar På Ny",
|
||||||
|
"Resume": "Start",
|
||||||
"Reused": "Gjenbrukt",
|
"Reused": "Gjenbrukt",
|
||||||
"Save": "Lagre",
|
"Save": "Lagre",
|
||||||
"Scanning": "Skannar",
|
"Scanning": "Skannar",
|
||||||
@@ -154,7 +157,7 @@
|
|||||||
"Source Code": "Kildekode",
|
"Source Code": "Kildekode",
|
||||||
"Staggered File Versioning": "Forskuva filutgåvehandtering",
|
"Staggered File Versioning": "Forskuva filutgåvehandtering",
|
||||||
"Start Browser": "Start Nettlesar",
|
"Start Browser": "Start Nettlesar",
|
||||||
"Statistics": "Statistics",
|
"Statistics": "Statistikk",
|
||||||
"Stopped": "Stoppa",
|
"Stopped": "Stoppa",
|
||||||
"Support": "Brukarstøtte",
|
"Support": "Brukarstøtte",
|
||||||
"Sync Protocol Listen Addresses": "Lytteadresse For Synkroniseringsprotokoll",
|
"Sync Protocol Listen Addresses": "Lytteadresse For Synkroniseringsprotokoll",
|
||||||
@@ -177,16 +180,16 @@
|
|||||||
"The folder ID must be unique.": "Mappe ID må vera unik.",
|
"The folder ID must be unique.": "Mappe ID må vera unik.",
|
||||||
"The folder path cannot be blank.": "Mappeplasseringa kan ikkje vera tom.",
|
"The folder path cannot be blank.": "Mappeplasseringa kan ikkje vera 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.": "Desse intervalla vert nytta: den fyrste timen vert ei utgåve lagra kvart 30. sekund, den fyrste dagen vert ei utgåve lagra kvar time, dei fyrste 30 dagane vert ei utgåve lagra kvar dag, og inntil høgaste alderen vert ei utgåve lagra kvar veke.",
|
"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.": "Desse intervalla vert nytta: den fyrste timen vert ei utgåve lagra kvart 30. sekund, den fyrste dagen vert ei utgåve lagra kvar time, dei fyrste 30 dagane vert ei utgåve lagra kvar dag, og inntil høgaste alderen vert ei utgåve lagra kvar veke.",
|
||||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
"The following items could not be synchronized.": "Fyljande filer kunne ikkje synkroniserast.",
|
||||||
"The maximum age must be a number and cannot be blank.": "Maksimal levetid må vera eit tal og kan ikkje vera tomt.",
|
"The maximum age must be a number and cannot be blank.": "Maksimal levetid må vera eit tal og kan ikkje vera tomt.",
|
||||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Høgaste tidsrom å behalda ei utgåve (i dagar, set til 0 for å behalda versjonane for alltid).",
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Høgaste tidsrom å behalda ei utgåve (i dagar, set til 0 for å behalda versjonane for alltid).",
|
||||||
"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).": "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 must be a number and cannot be blank.": "Tal på dagar må ver eit tal og kan ikkje vera tomt.",
|
||||||
"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 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 number of old versions to keep, per file.": "Tal på gamle versjonar ein skal behalda, per fil.",
|
"The number of old versions to keep, per file.": "Tal på gamle versjonar ein skal behalda, per fil.",
|
||||||
"The number of versions must be a number and cannot be blank.": "Tal på versjonar må vera eit tal og kan ikkje vera tomt.",
|
"The number of versions must be a number and cannot be blank.": "Tal på versjonar må vera eit tal og kan ikkje vera tomt.",
|
||||||
"The path cannot be blank.": "Bana kan ikkje vera tom.",
|
"The path cannot be blank.": "Bana kan ikkje vera tom.",
|
||||||
"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)": "Hastigheitsgrensa må ver eit positivt tall (0: ingen grensa)",
|
||||||
"The rescan interval must be a non-negative number of seconds.": "Talet på sekund i skanneintervallet kan ikkje vera negativt.",
|
"The rescan interval must be a non-negative number of seconds.": "Talet på sekund i skanneintervallet kan ikkje vera negativt.",
|
||||||
"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.",
|
"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 is a major version upgrade.": "This is a major version upgrade.",
|
"This is a major version upgrade.": "This is a major version upgrade.",
|
||||||
@@ -209,6 +212,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.": "Hugs at når ei ny mappe vert lagt til, vert mappe-ID-en brukt til å binda saman mappene mellom einingane. Det er skilnad på store og små bokstavar, så ID-ane må vera identiske på alle einingane.",
|
"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.": "Hugs at når ei ny mappe vert lagt til, vert mappe-ID-en brukt til å binda saman mappene mellom einingane. Det er skilnad på store og små bokstavar, så ID-ane må vera identiske på alle einingane.",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"You must keep at least one version.": "Du må behalda minst ein versjon.",
|
"You must keep at least one version.": "Du må behalda minst ein versjon.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "all dokumentasjon",
|
"full documentation": "all dokumentasjon",
|
||||||
"items": "element",
|
"items": "element",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønskjer å dela mappa \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønskjer å dela mappa \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Nadpisz zmiany",
|
"Override Changes": "Nadpisz zmiany",
|
||||||
"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": "Ścieżka do lokalnego folderu. Zostanie utworzona jeżeli nie istnieje.\nZnak tyldy (~) może zostać użyty jako skrót do",
|
"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": "Ścieżka do lokalnego folderu. Zostanie utworzona jeżeli nie istnieje.\nZnak tyldy (~) może zostać użyty jako skrót do",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ścieżka gdzie będą przechowywane wersje (pozostaw puste dla domyślnego folderu .stversions)",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ścieżka gdzie będą przechowywane wersje (pozostaw puste dla domyślnego folderu .stversions)",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Zaleca się przeanalizowanie \"release notes\" przed przeprowadzeniem znaczącej aktualizacji.",
|
"Please consult the release notes before performing a major upgrade.": "Zaleca się przeanalizowanie \"release notes\" przed przeprowadzeniem znaczącej aktualizacji.",
|
||||||
"Please wait": "Proszę czekać",
|
"Please wait": "Proszę czekać",
|
||||||
"Preview": "Podgląd",
|
"Preview": "Podgląd",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Uruchom ponownie",
|
"Restart": "Uruchom ponownie",
|
||||||
"Restart Needed": "Wymagane ponowne uruchomienie",
|
"Restart Needed": "Wymagane ponowne uruchomienie",
|
||||||
"Restarting": "Uruchamianie ponowne",
|
"Restarting": "Uruchamianie ponowne",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Ponownie użyte",
|
"Reused": "Ponownie użyte",
|
||||||
"Save": "Zapisz",
|
"Save": "Zapisz",
|
||||||
"Scanning": "Skanowanie",
|
"Scanning": "Skanowanie",
|
||||||
@@ -209,6 +212,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.": "Przy dodawaniu nowego folderu, pamiętaj, że ID użyte jest do łączenia folderów pomiędzy urządzeniami. Wielkość liter ciągu ma znaczenie musi zgadzać się na wszystkich urządzeniach.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Przy dodawaniu nowego folderu, pamiętaj, że ID użyte jest do łączenia folderów pomiędzy urządzeniami. Wielkość liter ciągu ma znaczenie musi zgadzać się na wszystkich urządzeniach.",
|
||||||
"Yes": "Tak",
|
"Yes": "Tak",
|
||||||
"You must keep at least one version.": "Musisz posiadać przynajmniej jedną wersję",
|
"You must keep at least one version.": "Musisz posiadać przynajmniej jedną wersję",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "pełna dokumentacja",
|
"full documentation": "pełna dokumentacja",
|
||||||
"items": "pozycji",
|
"items": "pozycji",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{folder}}\""
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{folder}}\""
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
"Device ID": "ID do dispositivo",
|
"Device ID": "ID do dispositivo",
|
||||||
"Device Identification": "Identificação do dispositivo",
|
"Device Identification": "Identificação do dispositivo",
|
||||||
"Device Name": "Nome do dispositivo",
|
"Device Name": "Nome do dispositivo",
|
||||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Dispositivo {{device}} ({{address}}) quer se conectar. Adicionar novo dispositivo?",
|
"Device {%device%} ({%address%}) wants to connect. Add new device?": "O dispositivo {{device}} ({{address}}) quer se conectar. Adicionar novo dispositivo?",
|
||||||
"Devices": "Dispositivos",
|
"Devices": "Dispositivos",
|
||||||
"Disconnected": "Desconectado",
|
"Disconnected": "Desconectado",
|
||||||
"Documentation": "Documentação",
|
"Documentation": "Documentação",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"Edit Folder": "Editar pasta",
|
"Edit Folder": "Editar pasta",
|
||||||
"Editing": "Editando",
|
"Editing": "Editando",
|
||||||
"Enable UPnP": "Habilitar UPnP",
|
"Enable UPnP": "Habilitar UPnP",
|
||||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Insira endereços (\"tcp://ip:porta\", \"tcp://host:porta\") separados por vírgula ou \"dynamic\" para executar a descoberta automática de endereço.",
|
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Insira endereços (\"tcp://ip:porta\", \"tcp://host:porta\") separados por vírgula ou \"dynamic\" para executar a descoberta automática do endereço.",
|
||||||
"Enter ignore patterns, one per line.": "Insira os padrões de exclusão, um por linha.",
|
"Enter ignore patterns, one per line.": "Insira os padrões de exclusão, um por linha.",
|
||||||
"Error": "Erro",
|
"Error": "Erro",
|
||||||
"External File Versioning": "Versionamento externo de arquivo",
|
"External File Versioning": "Versionamento externo de arquivo",
|
||||||
@@ -57,9 +57,9 @@
|
|||||||
"File Pull Order": "Ordem de retirada do arquivo",
|
"File Pull Order": "Ordem de retirada do arquivo",
|
||||||
"File Versioning": "Versionamento de arquivos",
|
"File Versioning": "Versionamento de arquivos",
|
||||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Os bits de permissão de um arquivo são ignorados durante as verificações. Use em sistemas de arquivo FAT.",
|
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Os bits de permissão de um arquivo são ignorados durante as verificações. Use em sistemas de arquivo FAT.",
|
||||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Os arquivos são motivos para a pasta .stversions quando substituídos ou apagados pelo Syncthing.",
|
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Os arquivos são movidos para a pasta .stversions quando substituídos ou apagados pelo Syncthing.",
|
||||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Os arquivos são renomeados com suas datas na pasta .stversions quando são substituídos ou removidos pelo Syncthing.",
|
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Os arquivos são renomeados com suas datas na pasta .stversions após serem substituídos ou removidos pelo 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.": "Os arquivos estão protegidos contra alterações feitas em outros dispositivos, mas alterações feitas neste dispositivo serão enviadas ao resto do grupo.",
|
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Os arquivos estão protegidos contra alterações feitas em outros dispositivos, mas alterações feitas neste dispositivo serão enviadas ao resto dos dispositivos.",
|
||||||
"Folder": "Pasta",
|
"Folder": "Pasta",
|
||||||
"Folder ID": "ID da pasta",
|
"Folder ID": "ID da pasta",
|
||||||
"Folder Master": "Pasta mestre",
|
"Folder Master": "Pasta mestre",
|
||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Sobrescrever mudanças",
|
"Override Changes": "Sobrescrever mudanças",
|
||||||
"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": "Caminho para a pasta na máquina local. Será criado caso não exista. O caractere til (~) pode ser usado como um atalho para",
|
"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": "Caminho para a pasta na máquina local. Será criado caso não exista. O caractere til (~) pode ser usado como um atalho para",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "O caminho onde as versões serão salvas (deixe vazio para usar a pasta padrão .stversions dentro desta pasta).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "O caminho onde as versões serão salvas (deixe vazio para usar a pasta padrão .stversions dentro desta pasta).",
|
||||||
|
"Pause": "Pausar",
|
||||||
|
"Paused": "Pausada",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Por favor, consulte as notas de lançamento antes de atualizar para uma versão \"major\".",
|
"Please consult the release notes before performing a major upgrade.": "Por favor, consulte as notas de lançamento antes de atualizar para uma versão \"major\".",
|
||||||
"Please wait": "Aguarde",
|
"Please wait": "Aguarde",
|
||||||
"Preview": "Visualizar",
|
"Preview": "Visualizar",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Reiniciar",
|
"Restart": "Reiniciar",
|
||||||
"Restart Needed": "É necessário reiniciar",
|
"Restart Needed": "É necessário reiniciar",
|
||||||
"Restarting": "Reiniciando",
|
"Restarting": "Reiniciando",
|
||||||
|
"Resume": "Resumir",
|
||||||
"Reused": "Reutilizado",
|
"Reused": "Reutilizado",
|
||||||
"Save": "Salvar",
|
"Save": "Salvar",
|
||||||
"Scanning": "Verificando",
|
"Scanning": "Verificando",
|
||||||
@@ -137,14 +140,14 @@
|
|||||||
"Select the folders to share with this device.": "Selecione as pastas a serem compartilhadas com este dispositivo.",
|
"Select the folders to share with this device.": "Selecione as pastas a serem compartilhadas com este dispositivo.",
|
||||||
"Settings": "Configurações",
|
"Settings": "Configurações",
|
||||||
"Share": "Compartilhar",
|
"Share": "Compartilhar",
|
||||||
"Share Folder": "Compartilhar Pasta",
|
"Share Folder": "Compartilhar pasta",
|
||||||
"Share Folders With Device": "Compartilhar pastas com o dispositivo",
|
"Share Folders With Device": "Compartilhar pastas com o dispositivo",
|
||||||
"Share With Devices": "Compartilhar com os dispositivos",
|
"Share With Devices": "Compartilhar com os dispositivos",
|
||||||
"Share this folder?": "Compartilhar esta pasta?",
|
"Share this folder?": "Compartilhar esta pasta?",
|
||||||
"Shared With": "Compartilhada com",
|
"Shared With": "Compartilhada com",
|
||||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador curto para a pasta. Deve ser igual em todos os dispositivos.",
|
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador curto para a pasta. Deve ser igual em todos os dispositivos.",
|
||||||
"Show ID": "Mostrar ID",
|
"Show ID": "Mostrar ID",
|
||||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrado no lugar do ID do dispositivo no indicador de estado do grupo. Será divulgado aos outros dispositivos como um nome pré-definido opcional.",
|
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrado no lugar do ID do dispositivo no indicador de estado do grupo. Será divulgado aos outros dispositivos como um nome opcional e pré-definido.",
|
||||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrado no lugar do ID do dispositivo no indicador de estado do grupo. Será atualizado para o nome que o dispositivo divulga, caso seja deixado em branco.",
|
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrado no lugar do ID do dispositivo no indicador de estado do grupo. Será atualizado para o nome que o dispositivo divulga, caso seja deixado em branco.",
|
||||||
"Shutdown": "Desligar",
|
"Shutdown": "Desligar",
|
||||||
"Shutdown Complete": "Desligamento completado",
|
"Shutdown Complete": "Desligamento completado",
|
||||||
@@ -169,11 +172,11 @@
|
|||||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi salva mas ainda não foi ativada. O Syncthing precisa ser reiniciado para a ativação da nova configuração.",
|
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi salva mas ainda não foi ativada. O Syncthing precisa ser reiniciado para a ativação da nova configuração.",
|
||||||
"The device ID cannot be blank.": "O ID de dispositivo não pode ficar vazio.",
|
"The device ID cannot be blank.": "O ID de dispositivo não pode ficar vazio.",
|
||||||
"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).": "O ID do dispositivo a ser inserido aqui pode ser encontrado no menu \"Editar > Mostrar ID\" do outro dispositivo. Espaços e hífens são opcionais (ignorados).",
|
"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).": "O ID do dispositivo a ser inserido aqui pode ser encontrado no menu \"Editar > Mostrar ID\" do outro dispositivo. Espaços e hífens são opcionais (ignorados).",
|
||||||
"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.": "O relatório criptografado de uso é enviado diariamente. É utilizado para rastrear plataformas, tamanhos de pastas e versões da aplicação. Caso o formato dos dados seja alterado, esta janela te avisará.",
|
"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.": "O relatório de uso é criptografado e enviado diariamente. É utilizado para rastrear plataformas, tamanhos de pastas e versões da aplicação. Caso o formato dos dados seja alterado, esta janela te avisará.",
|
||||||
"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.": "O ID de dispositivo inserido não parece ser válido. Ele deve ter entre 52 e 56 caracteres e ser composto de letras e números, com espaços e hífens opcionais.",
|
"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.": "O ID de dispositivo inserido não parece ser válido. Ele deve ter entre 52 e 56 caracteres e ser composto de letras e números, com espaços e hífens opcionais.",
|
||||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "O primeiro argumento da linha de comando é o caminho da pasta e o segundo é o caminho relativo à pasta.",
|
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "O primeiro argumento da linha de comando é o caminho da pasta e o segundo é o caminho relativo à pasta.",
|
||||||
"The folder ID cannot be blank.": "O ID da pasta não pode ficar vazio.",
|
"The folder ID cannot be blank.": "O ID da pasta não pode ficar vazio.",
|
||||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "O ID da pasta deve ser um identificador curto (com 64 caracteres ou menos), composto somente de letras, números, pontos (.), hífens (-) ou underscores (_).",
|
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "O ID da pasta deve ser um identificador curto (com 64 caracteres ou menos) e composto somente de letras, números, pontos (.), hífens (-) ou underscores (_).",
|
||||||
"The folder ID must be unique.": "O ID da pasta deve ser único.",
|
"The folder ID must be unique.": "O ID da pasta deve ser único.",
|
||||||
"The folder path cannot be blank.": "O caminho da pasta não pode ficar vazio.",
|
"The folder path cannot be blank.": "O caminho da pasta não pode ficar vazio.",
|
||||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "São utilizados os seguintes intervalos: na primeira hora é guardada uma versão a cada 30 segundos, no primeiro dia é guardada uma versão a cada hora, nos primeiros 30 dias é guardada uma versão por dia e, até que atinja a idade máxima, é guardada uma versão por semana.",
|
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "São utilizados os seguintes intervalos: na primeira hora é guardada uma versão a cada 30 segundos, no primeiro dia é guardada uma versão a cada hora, nos primeiros 30 dias é guardada uma versão por dia e, até que atinja a idade máxima, é guardada uma versão por semana.",
|
||||||
@@ -181,7 +184,7 @@
|
|||||||
"The maximum age must be a number and cannot be blank.": "A idade máxima deve ser um valor numérico. O campo não pode ficar vazio.",
|
"The maximum age must be a number and cannot be blank.": "A idade máxima deve ser um valor numérico. O campo não pode ficar vazio.",
|
||||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "O número máximo de dias em que uma versão é guardada. (Use 0 para manter para sempre).",
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "O número máximo de dias em que uma versão é guardada. (Use 0 para manter para sempre).",
|
||||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "A porcentagem de espaço livre mínimo no disco deve ser um número positivo entre 0 e 100 (inclusive).",
|
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "A porcentagem de espaço livre mínimo no disco deve ser um número positivo entre 0 e 100 (inclusive).",
|
||||||
"The number of days must be a number and cannot be blank.": "O número de dias deve ser um número valido e não pode ficar em branco.",
|
"The number of days must be a number and cannot be blank.": "O número de dias deve ser um número válido e não pode ficar em branco.",
|
||||||
"The number of days to keep files in the trash can. Zero means forever.": "O número de dias em que são mantidos os arquivos da lixeira. Zero significa para sempre.",
|
"The number of days to keep files in the trash can. Zero means forever.": "O número de dias em que são mantidos os arquivos da lixeira. Zero significa para sempre.",
|
||||||
"The number of old versions to keep, per file.": "O número de versões antigas a serem mantidas, por arquivo.",
|
"The number of old versions to keep, per file.": "O número de versões antigas a serem mantidas, por arquivo.",
|
||||||
"The number of versions must be a number and cannot be blank.": "O número de versões deve ser um valor numérico. O campo não pode ficar vazio.",
|
"The number of versions must be a number and cannot be blank.": "O número de versões deve ser um valor numérico. O campo não pode ficar vazio.",
|
||||||
@@ -206,9 +209,10 @@
|
|||||||
"Versions Path": "Caminho das versões",
|
"Versions Path": "Caminho das versões",
|
||||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são automaticamente apagadas se elas são mais antigas do que a idade máxima ou excederem o número de arquivos permitido em um intervalo.",
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são automaticamente apagadas se elas são mais antigas do que a idade máxima ou excederem o número de arquivos permitido em um intervalo.",
|
||||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando estiver adicionando um dispositivo, lembre-se de que este dispositivo deve ser adicionado do outro lado também.",
|
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando estiver adicionando um dispositivo, lembre-se de que este dispositivo deve ser adicionado do outro lado também.",
|
||||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quando adicionar uma nova pasta, lembre-se que o ID da pasta é utilizado para ligar pastas entre dispositivos. Ele é sensível às diferenças entre maiúsculas e minúsculas e devem ser iguais em todos os dispositivos.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quando adicionar uma nova pasta, lembre-se que o ID da pasta é utilizado para ligar pastas entre dispositivos. Ele é sensível às diferenças entre maiúsculas e minúsculas e deve ser o mesmo em todos os dispositivos.",
|
||||||
"Yes": "Sim",
|
"Yes": "Sim",
|
||||||
"You must keep at least one version.": "Você deve manter pelo menos uma versão.",
|
"You must keep at least one version.": "Você deve manter pelo menos uma versão.",
|
||||||
|
"days": "dias",
|
||||||
"full documentation": "documentação completa",
|
"full documentation": "documentação completa",
|
||||||
"items": "itens",
|
"items": "itens",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer compartilhar a pasta \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer compartilhar a pasta \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Sobrepor alterações",
|
"Override Changes": "Sobrepor alterações",
|
||||||
"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": "Caminho para a pasta no computador local. Será criada, caso não exista. O caractere (~) pode ser utilizado como atalho para",
|
"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": "Caminho para a pasta no computador local. Será criada, caso não exista. O caractere (~) pode ser utilizado como atalho para",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Caminho onde as versões são guardadas (deixe vazio para usar a pasta pré-definida .stversions dentro da pasta a que se refere).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Caminho onde as versões são guardadas (deixe vazio para usar a pasta pré-definida .stversions dentro da pasta a que se refere).",
|
||||||
|
"Pause": "Pausar",
|
||||||
|
"Paused": "Em pausa",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Consulte as notas de lançamento antes de fazer uma actualização importante.",
|
"Please consult the release notes before performing a major upgrade.": "Consulte as notas de lançamento antes de fazer uma actualização importante.",
|
||||||
"Please wait": "Aguarde",
|
"Please wait": "Aguarde",
|
||||||
"Preview": "Previsão",
|
"Preview": "Previsão",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Reiniciar",
|
"Restart": "Reiniciar",
|
||||||
"Restart Needed": "É preciso reiniciar",
|
"Restart Needed": "É preciso reiniciar",
|
||||||
"Restarting": "Reiniciando",
|
"Restarting": "Reiniciando",
|
||||||
|
"Resume": "Retomar",
|
||||||
"Reused": "Reutilizado",
|
"Reused": "Reutilizado",
|
||||||
"Save": "Gravar",
|
"Save": "Gravar",
|
||||||
"Scanning": "Verificando",
|
"Scanning": "Verificando",
|
||||||
@@ -209,6 +212,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.": "Quando adicionar uma nova pasta, lembre-se que o ID da pasta é utilizado para ligar as pastas entre dispositivos. É sensível às diferenças entre maiúsculas e minúsculas e tem que ter uma correspondência perfeita entre todos os dispositivos.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quando adicionar uma nova pasta, lembre-se que o ID da pasta é utilizado para ligar as pastas entre dispositivos. É sensível às diferenças entre maiúsculas e minúsculas e tem que ter uma correspondência perfeita entre todos os dispositivos.",
|
||||||
"Yes": "Sim",
|
"Yes": "Sim",
|
||||||
"You must keep at least one version.": "Tem que manter pelo menos uma versão.",
|
"You must keep at least one version.": "Tem que manter pelo menos uma versão.",
|
||||||
|
"days": "dias",
|
||||||
"full documentation": "documentação completa",
|
"full documentation": "documentação completa",
|
||||||
"items": "itens",
|
"items": "itens",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer partilhar a pasta \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer partilhar a pasta \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Suprascrie Schimbări",
|
"Override Changes": "Suprascrie Schimbări",
|
||||||
"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": "Localizarea fișierului în acest computer. Dacă nu există, va fi creat. Tilda (~) înlocuiește ",
|
"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": "Localizarea fișierului în acest computer. Dacă nu există, va fi creat. Tilda (~) înlocuiește ",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Locul unde vor fi stocate versiunile (a se lăsa neschimbat pentru fișierul .stversions din fișier). ",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Locul unde vor fi stocate versiunile (a se lăsa neschimbat pentru fișierul .stversions din fișier). ",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
||||||
"Please wait": "Aşteaptă",
|
"Please wait": "Aşteaptă",
|
||||||
"Preview": "Previzualizează",
|
"Preview": "Previzualizează",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Restart",
|
"Restart": "Restart",
|
||||||
"Restart Needed": "Restart Necesar",
|
"Restart Needed": "Restart Necesar",
|
||||||
"Restarting": "Se restartează",
|
"Restarting": "Se restartează",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Refolosit",
|
"Reused": "Refolosit",
|
||||||
"Save": "Salvează",
|
"Save": "Salvează",
|
||||||
"Scanning": "Scanează",
|
"Scanning": "Scanează",
|
||||||
@@ -209,6 +212,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.": "Cînd adăugaţi un fişier nou, nu uitaţi că ID-ul fişierului va rămîne acelaşi pe toate dispozitivele. Iar literele mari sînt diferite de literele mici. ",
|
"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.": "Cînd adăugaţi un fişier nou, nu uitaţi că ID-ul fişierului va rămîne acelaşi pe toate dispozitivele. Iar literele mari sînt diferite de literele mici. ",
|
||||||
"Yes": "Da",
|
"Yes": "Da",
|
||||||
"You must keep at least one version.": "Trebuie să păstrezi cel puţin o versiune.",
|
"You must keep at least one version.": "Trebuie să păstrezi cel puţin o versiune.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "toată documentaţia",
|
"full documentation": "toată documentaţia",
|
||||||
"items": "obiecte",
|
"items": "obiecte",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{Dispozitivul}} vrea să transmită mapa {{Mapa}}"
|
"{%device%} wants to share folder \"{%folder%}\".": "{{Dispozitivul}} vrea să transmită mapa {{Mapa}}"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"Edit Folder": "Изменить папку",
|
"Edit Folder": "Изменить папку",
|
||||||
"Editing": "Редактирование",
|
"Editing": "Редактирование",
|
||||||
"Enable UPnP": "Включить UPnP",
|
"Enable UPnP": "Включить 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.": "Введите адреса через запятую (\"tcp://ip:port\", \"tcp://host:port\") или \"dynamic\" для автоматического поиска адресов.",
|
||||||
"Enter ignore patterns, one per line.": "Введите шаблоны игнорирования, по одному на строку.",
|
"Enter ignore patterns, one per line.": "Введите шаблоны игнорирования, по одному на строку.",
|
||||||
"Error": "Ошибка",
|
"Error": "Ошибка",
|
||||||
"External File Versioning": "Внешний контроль версий файлов",
|
"External File Versioning": "Внешний контроль версий файлов",
|
||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Перезаписать изменения",
|
"Override Changes": "Перезаписать изменения",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Путь к папке на локальном компьютере. Если её не существует, то она будет создана. Тильда (~) может использоваться как сокращение для",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Путь к папке на локальном компьютере. Если её не существует, то она будет создана. Тильда (~) может использоваться как сокращение для",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Путь, где должны храниться версии (оставьте пустым, чтобы использовать папку по умолчанию .stversions внутри папки).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Путь, где должны храниться версии (оставьте пустым, чтобы использовать папку по умолчанию .stversions внутри папки).",
|
||||||
|
"Pause": "Пауза",
|
||||||
|
"Paused": "Остановлено",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Перед проведением обновления основной версии ознакомтесь, пожалуйста, с Замечаниями к версии",
|
"Please consult the release notes before performing a major upgrade.": "Перед проведением обновления основной версии ознакомтесь, пожалуйста, с Замечаниями к версии",
|
||||||
"Please wait": "Пожалуйста, подождите",
|
"Please wait": "Пожалуйста, подождите",
|
||||||
"Preview": "Предварительный просмотр",
|
"Preview": "Предварительный просмотр",
|
||||||
@@ -120,8 +122,8 @@
|
|||||||
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
|
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
|
||||||
"RAM Utilization": "Использование ОЗУ",
|
"RAM Utilization": "Использование ОЗУ",
|
||||||
"Random": "Случайно",
|
"Random": "Случайно",
|
||||||
"Relayed via": "Relayed via",
|
"Relayed via": "Релей через",
|
||||||
"Relays": "Relays",
|
"Relays": "Релеи",
|
||||||
"Release Notes": "Замечания к версии",
|
"Release Notes": "Замечания к версии",
|
||||||
"Remove": "Удалить",
|
"Remove": "Удалить",
|
||||||
"Rescan": "Пересканирование",
|
"Rescan": "Пересканирование",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Перезапуск",
|
"Restart": "Перезапуск",
|
||||||
"Restart Needed": "Требуется перезапуск",
|
"Restart Needed": "Требуется перезапуск",
|
||||||
"Restarting": "Перезапуск",
|
"Restarting": "Перезапуск",
|
||||||
|
"Resume": "Возобновить",
|
||||||
"Reused": "Повторно использовано",
|
"Reused": "Повторно использовано",
|
||||||
"Save": "Сохранить",
|
"Save": "Сохранить",
|
||||||
"Scanning": "Сканирование",
|
"Scanning": "Сканирование",
|
||||||
@@ -186,7 +189,7 @@
|
|||||||
"The number of old versions to keep, per file.": "Количество хранимых версий файла.",
|
"The number of old versions to keep, per file.": "Количество хранимых версий файла.",
|
||||||
"The number of versions must be a number and cannot be blank.": "Количество версий должно быть числом и не может быть пустым.",
|
"The number of versions must be a number and cannot be blank.": "Количество версий должно быть числом и не может быть пустым.",
|
||||||
"The path cannot be blank.": "Путь не может быть пустым.",
|
"The path cannot be blank.": "Путь не может быть пустым.",
|
||||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
"The rate limit must be a non-negative number (0: no limit)": "Скорость должна быть неотрицательным числом (0: нет ограничения)",
|
||||||
"The rescan interval must be a non-negative number of seconds.": "Интервал пересканирования должен быть неотрицательным количеством секунд.",
|
"The rescan interval must be a non-negative number of seconds.": "Интервал пересканирования должен быть неотрицательным количеством секунд.",
|
||||||
"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 is a major version upgrade.": "Это обновление основной версии продукта.",
|
"This is a major version upgrade.": "Это обновление основной версии продукта.",
|
||||||
@@ -209,6 +212,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.": "Когда добавляете новую папку, помните, что ID папок используются для того, чтобы связывать папки между всеми устройствами. Они чувствительны к регистру и должны совпадать на всех используемых устройствах.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Когда добавляете новую папку, помните, что ID папок используются для того, чтобы связывать папки между всеми устройствами. Они чувствительны к регистру и должны совпадать на всех используемых устройствах.",
|
||||||
"Yes": "Да",
|
"Yes": "Да",
|
||||||
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
|
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
|
||||||
|
"days": "Дней",
|
||||||
"full documentation": "полная документация",
|
"full documentation": "полная документация",
|
||||||
"items": "элементы",
|
"items": "элементы",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Skriv över ändringar",
|
"Override Changes": "Skriv över ändringar",
|
||||||
"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": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
"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": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sökväg där versioner sparas (lämna tomt för att använda .stversions i den ordinarie katalogen).",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sökväg där versioner sparas (lämna tomt för att använda .stversions i den ordinarie katalogen).",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
|
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
|
||||||
"Please wait": "Var god vänta",
|
"Please wait": "Var god vänta",
|
||||||
"Preview": "Förhandsgranska",
|
"Preview": "Förhandsgranska",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "Starta om",
|
"Restart": "Starta om",
|
||||||
"Restart Needed": "Omstart behövs",
|
"Restart Needed": "Omstart behövs",
|
||||||
"Restarting": "Startar om",
|
"Restarting": "Startar om",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Återanvänt",
|
"Reused": "Återanvänt",
|
||||||
"Save": "Spara",
|
"Save": "Spara",
|
||||||
"Scanning": "Uppdaterar",
|
"Scanning": "Uppdaterar",
|
||||||
@@ -209,6 +212,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.": "När du lägger till ny katalog, tänk på att katalog-ID:t knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny katalog, tänk på att katalog-ID:t knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
|
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "fullständig dokumentation",
|
"full documentation": "fullständig dokumentation",
|
||||||
"items": "poster",
|
"items": "poster",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\"."
|
||||||
|
|||||||
@@ -1,215 +0,0 @@
|
|||||||
{
|
|
||||||
"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.": "A new major version may not be compatible with previous versions.",
|
|
||||||
"API Key": "API Anahtarı",
|
|
||||||
"About": "Hakkında",
|
|
||||||
"Actions": "Actions",
|
|
||||||
"Add": "Ekle",
|
|
||||||
"Add Device": "Cihaz Ekle",
|
|
||||||
"Add Folder": "Klasör Ekle",
|
|
||||||
"Add new folder?": "Yeni klasör ekle?",
|
|
||||||
"Address": "Adres",
|
|
||||||
"Addresses": "Adresler",
|
|
||||||
"Advanced": "Advanced",
|
|
||||||
"Advanced Configuration": "Advanced Configuration",
|
|
||||||
"All Data": "All Data",
|
|
||||||
"Allow Anonymous Usage Reporting?": "Anonim kullanım raporlarına izin ver ?",
|
|
||||||
"Alphabetic": "Alphabetic",
|
|
||||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
|
||||||
"Anonymous Usage Reporting": "Anonim Kullanım Raporlama",
|
|
||||||
"Any devices configured on an introducer device will be added to this device as well.": "Tanıtıcı bir cihazda yapılandırılan cihazlar bu cihaza da eklenecektir.",
|
|
||||||
"Automatic upgrades": "Otomatik güncellemeler",
|
|
||||||
"Be careful!": "Be careful!",
|
|
||||||
"Bugs": "Hatalar",
|
|
||||||
"CPU Utilization": "İşlemci Kullanımı",
|
|
||||||
"Changelog": "Değişim Günlüğü",
|
|
||||||
"Clean out after": "Clean out after",
|
|
||||||
"Close": "Kapat",
|
|
||||||
"Command": "Command",
|
|
||||||
"Comment, when used at the start of a line": "Satır başında kullanıldığında açıklama özelliği taşır",
|
|
||||||
"Compression": "Sıkıştırma",
|
|
||||||
"Connection Error": "Bağlantı hatası",
|
|
||||||
"Copied from elsewhere": "Başka bir yerden kopyalanmış",
|
|
||||||
"Copied from original": "Aslından kopyalanmış",
|
|
||||||
"Copyright © 2015 the following Contributors:": "Telif Hakkı © 2015 Katkıda bulunanlar:",
|
|
||||||
"Delete": "Sil",
|
|
||||||
"Deleted": "Deleted",
|
|
||||||
"Device ID": "Cihaz ID",
|
|
||||||
"Device Identification": "Cihaz Kimliği",
|
|
||||||
"Device Name": "Cihaz Adı",
|
|
||||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Cihaz {{device}} ({{address}}) bağlanmak istiyor. Yeni cihaz ekle?",
|
|
||||||
"Devices": "Cihazlar",
|
|
||||||
"Disconnected": "Bağlantı Kesik",
|
|
||||||
"Documentation": "Dokümantasyon",
|
|
||||||
"Download Rate": "İndirme Hızı",
|
|
||||||
"Downloaded": "İndirilmiş",
|
|
||||||
"Downloading": "İndiriliyor",
|
|
||||||
"Edit": "Seçenekler",
|
|
||||||
"Edit Device": "Cihaz Düzenle",
|
|
||||||
"Edit Folder": "Klasör Düzenle",
|
|
||||||
"Editing": "Düzenleniyor",
|
|
||||||
"Enable UPnP": "UPnP Etkinleştir",
|
|
||||||
"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 ignore patterns, one per line.": "Yoksayılacak kalıp dizilerini her satıra bir tane olacak şekilde girin.",
|
|
||||||
"Error": "Hata",
|
|
||||||
"External File Versioning": "Harici Dosya Sürümlendirme",
|
|
||||||
"Failed Items": "Failed Items",
|
|
||||||
"File Pull Order": "File Pull Order",
|
|
||||||
"File Versioning": "Dosya Sürümlendirme",
|
|
||||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Değişimleri yoklarken dosya izin bilgilerini ihmal et. FAT dosya sistemlerinde kullanın.",
|
|
||||||
"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 date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dosyalar Syncthing tarafından değiştirildiğinde ya da silindiğinde, tarih damgalı sürümleri .stversions dizinine taşınır.",
|
|
||||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosyalar diğer cihazlarda yapılan değişikliklerden korunur, ancak bu cihazdaki değişiklikler kümedeki diğer cihazlara gönderilir.",
|
|
||||||
"Folder": "Folder",
|
|
||||||
"Folder ID": "Klasör ID",
|
|
||||||
"Folder Master": "Ana Klasör",
|
|
||||||
"Folder Path": "Klasör Yolu",
|
|
||||||
"Folders": "Klasörler",
|
|
||||||
"GUI": "GUI",
|
|
||||||
"GUI Authentication Password": "Kullanıcı arayüzü şifresi",
|
|
||||||
"GUI Authentication User": "Kullanıcı arayüzü kullanıcı ismi",
|
|
||||||
"GUI Listen Addresses": "Kullanıcı arayüzü bağlantı adresi",
|
|
||||||
"Generate": "Oluştur",
|
|
||||||
"Global Discovery": "Küresel Keşif",
|
|
||||||
"Global Discovery Server": "Küresel Keşif Sunucusu",
|
|
||||||
"Global State": "Küresel Durum",
|
|
||||||
"Help": "Help",
|
|
||||||
"Home page": "Home page",
|
|
||||||
"Ignore": "Yoksay",
|
|
||||||
"Ignore Patterns": "Kalıpları Yoksay",
|
|
||||||
"Ignore Permissions": "İzinleri yoksay",
|
|
||||||
"Incoming Rate Limit (KiB/s)": "İndirme Oranı Limiti (KiB/s)",
|
|
||||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
|
||||||
"Introducer": "Tanıtıcı",
|
|
||||||
"Inversion of the given condition (i.e. do not exclude)": "Verilen koşulun ters çevirilmesi (örneğin: yok sayma)",
|
|
||||||
"Keep Versions": "Sürüm tut",
|
|
||||||
"Largest First": "Largest First",
|
|
||||||
"Last File Received": "Alınan Son Dosya",
|
|
||||||
"Last seen": "Son Görülen",
|
|
||||||
"Later": "Sonra",
|
|
||||||
"Local Discovery": "Yerel bulma",
|
|
||||||
"Local State": "Yerel Durum",
|
|
||||||
"Local State (Total)": "Local State (Total)",
|
|
||||||
"Major Upgrade": "Major Upgrade",
|
|
||||||
"Maximum Age": "Azami Süre",
|
|
||||||
"Metadata Only": "Metadata Only",
|
|
||||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
|
||||||
"Move to top of queue": "Move to top of queue",
|
|
||||||
"Multi level wildcard (matches multiple directory levels)": "Çoklu düzey wildcard (çok sayıda dizin düzeyinde eşleşme)",
|
|
||||||
"Never": "Asla",
|
|
||||||
"New Device": "Yeni Cihaz",
|
|
||||||
"New Folder": "Yeni Klasör",
|
|
||||||
"Newest First": "Newest First",
|
|
||||||
"No": "Hayır",
|
|
||||||
"No File Versioning": "Sürümlendirme Yok",
|
|
||||||
"Notice": "Uyarı",
|
|
||||||
"OK": "Tamam",
|
|
||||||
"Off": "Off",
|
|
||||||
"Oldest First": "Oldest First",
|
|
||||||
"Options": "Options",
|
|
||||||
"Out of Sync": "Out of Sync",
|
|
||||||
"Out of Sync Items": "Eşzamanlanmayan Öğeler",
|
|
||||||
"Outgoing Rate Limit (KiB/s)": "Yükleme hız sınırı (KB/sn)",
|
|
||||||
"Override Changes": "Değişiklikleri Geçersiz kıl",
|
|
||||||
"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": "Yerel bilgisayardaki klasöre ulaşım yolu. Dizin yoksa yaratılacak. (~) karakterinin kısayol olarak kullanılabileceği yol",
|
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sürümlerin saklanması gereken yol (klasördeki öntanımlı .stversions klasörü için boş bırakın)",
|
|
||||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
|
||||||
"Please wait": "Lütfen Bekleyin",
|
|
||||||
"Preview": "Önizleme",
|
|
||||||
"Preview Usage Report": "Kullanım raporunu gözden geçir",
|
|
||||||
"Quick guide to supported patterns": "Desteklenen kalıplar için hızlı rehber",
|
|
||||||
"RAM Utilization": "RAM Kullanımı",
|
|
||||||
"Random": "Random",
|
|
||||||
"Relayed via": "Relayed via",
|
|
||||||
"Relays": "Relays",
|
|
||||||
"Release Notes": "Release Notes",
|
|
||||||
"Remove": "Remove",
|
|
||||||
"Rescan": "Tekrar Tara",
|
|
||||||
"Rescan All": "Tümünü Tekrar Tara",
|
|
||||||
"Rescan Interval": "Tarama Aralığı",
|
|
||||||
"Restart": "Yeniden Başlat",
|
|
||||||
"Restart Needed": "Yeniden başlatma gereklidir",
|
|
||||||
"Restarting": "Yeniden başlatılıyor",
|
|
||||||
"Reused": "Yeniden Kullanılan",
|
|
||||||
"Save": "Kaydet",
|
|
||||||
"Scanning": "Taranıyor",
|
|
||||||
"Select the devices to share this folder with.": "Bu klasörü paylaşacağın cihazları seç.",
|
|
||||||
"Select the folders to share with this device.": "Bu cihazla paylaşılacak klasörleri seç.",
|
|
||||||
"Settings": "Ayarlar",
|
|
||||||
"Share": "Paylaş",
|
|
||||||
"Share Folder": "Paylaşım Klasörü",
|
|
||||||
"Share Folders With Device": "Klasörü Cihazla Paylaş",
|
|
||||||
"Share With Devices": "Cihazlar İle Paylaş",
|
|
||||||
"Share this folder?": "Bu klasörü paylaşmak istiyor musun?",
|
|
||||||
"Shared With": "Paylaşılan düğümler",
|
|
||||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Klasörü için kısa tanımlayıcı. Tüm küme cihazlarda aynı olmalıdır.",
|
|
||||||
"Show ID": "ID Göster",
|
|
||||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Küme durumunda Cihaz ID yerine bunu göster. Varsayılan isim isteğe bağlı olarak diğer cihazlara ilan edilecektir.",
|
|
||||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Küme durumunda Cihaz ID yerine bunu göster. Eğer düğüm ismi boş bırakılırsa düğüm ismi güncellenip ilan edilecektir.",
|
|
||||||
"Shutdown": "Kapat",
|
|
||||||
"Shutdown Complete": "Kapatma İşlemi Tamamlandı",
|
|
||||||
"Simple File Versioning": "Basit Dosya Sürümlendirme",
|
|
||||||
"Single level wildcard (matches within a directory only)": "Tekli düzey wildcard (sadece bir dizin içinde eşleşme)",
|
|
||||||
"Smallest First": "Smallest First",
|
|
||||||
"Source Code": "Kaynak Kodu",
|
|
||||||
"Staggered File Versioning": "Aşamalı Dosya Sürümlendirme",
|
|
||||||
"Start Browser": "Tarayıcıyı Başlat",
|
|
||||||
"Statistics": "Statistics",
|
|
||||||
"Stopped": "Durduruldu",
|
|
||||||
"Support": "Destek",
|
|
||||||
"Sync Protocol Listen Addresses": "Sync Protokol Dinleme Adresleri",
|
|
||||||
"Syncing": "Senkronize ediliyor",
|
|
||||||
"Syncthing has been shut down.": "Syncthing durduruldu",
|
|
||||||
"Syncthing includes the following software or portions thereof:": "Syncthing aşağıdaki yazılımları veya bunların bölümlerini içermektedir:",
|
|
||||||
"Syncthing is restarting.": "Syncthing yeniden başlatılıyor.",
|
|
||||||
"Syncthing is upgrading.": "Syncthing yükseltiliyor.",
|
|
||||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing görünüşe durdu veya internetin bağlantınızda problem var. Tekrar deniyor....",
|
|
||||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing isteminizi işleme alırken bir sorunla karşılaştı. Lütfen sayfanızı yenileyin veya sorun devam ediyorsa Syncthing'i yeniden başlatın.",
|
|
||||||
"The aggregated statistics are publicly available at {%url%}.": "Toplanan halka açık istatistiklere ulaşabileceğiniz adres {{url}}.",
|
|
||||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Ayarlar kaydedildi ancak aktifleştirilmedi. Aktifleştirmek için Syncthing yeniden başlatılmalı.",
|
|
||||||
"The device ID cannot be blank.": "Cihaz ID boş olamaz.",
|
|
||||||
"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).": "Buraya girilecek cihaz ID'si diğer düğümde \"Düzenle > ID Göster\" menüsünden bulunabilir. Boşluk ve kısa çizginin olup olmaması önemli değildir. (İhmal edilir)",
|
|
||||||
"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.": "Şifrelenmiş kullanım bilgisi günlük olarak gönderilir. Platform, klasör büyüklüğü ve uygulama sürümü hakkında bilgi toplanır. Toplanan bilgi çeşidi değişecek olursa, sizden tekrar onay istenecek.",
|
|
||||||
"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.": "Girilen cihaz ID'si geçerli gibi gözükmüyor. 52 ya da 56 karakter uzunluğunda, harf ve rakamlardan oluşmalı. Boşlukların ve kısa çizgilerin olup olmaması önemli değildir.",
|
|
||||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
|
|
||||||
"The folder ID cannot be blank.": "Klasör ID boş olamaz.",
|
|
||||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Klasör ID uzun olmamalı (64 karakter ya da daha az). Sadece harf, rakam, nokta (.), kısa çizgi (-) ve alt çizgi (_) kullanabilirsiniz.",
|
|
||||||
"The folder ID must be unique.": "Klasör ID benzersiz olmalıdır.",
|
|
||||||
"The folder path cannot be blank.": "Klasör dizini boş bırakılamaz.",
|
|
||||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Kullanılan zaman aralıkları : ilk bir saat içinde her 30 saniyede, ilk günde her saatte, ilk 30 günde her gün, azami süreye kadar geçen zamanda ise her hafta yeni sürüm değeri oluşturulur.",
|
|
||||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
|
||||||
"The maximum age must be a number and cannot be blank.": "Azami süre tanımı boş bırakılmamalı ve bir sayı olarak tanımlanmalıdır.",
|
|
||||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Bir sürümün tutulması için belirlenen azami süre (sürümleri daimi olarak tutabilmek için 0 değeri atayın)",
|
|
||||||
"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 number of old versions to keep, per file.": "Dosya başına saklanacak eski sürüm.",
|
|
||||||
"The number of versions must be a number and cannot be blank.": "Sürümlerin sayısı sayı olmalı ve boş bırakılamaz.",
|
|
||||||
"The path cannot be blank.": "Dizin yolu boş bırakılamaz.",
|
|
||||||
"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 rescan interval must be a non-negative number of seconds.": "Tarama zaman aralığı, saniye cinsinden negatif olmayan bir sayı olmalıdır.",
|
|
||||||
"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 is a major version upgrade.": "This is a major version upgrade.",
|
|
||||||
"Trash Can File Versioning": "Trash Can File Versioning",
|
|
||||||
"Unknown": "Bilinmiyor",
|
|
||||||
"Unshared": "Paylaşılmayan",
|
|
||||||
"Unused": "Kullanılmayan",
|
|
||||||
"Up to Date": "Güncel",
|
|
||||||
"Updated": "Updated",
|
|
||||||
"Upgrade": "Upgrade",
|
|
||||||
"Upgrade To {%version%}": "{{version}} sürümüne yükselt",
|
|
||||||
"Upgrading": "Yükseltiliyor",
|
|
||||||
"Upload Rate": "Yükleme hızı",
|
|
||||||
"Uptime": "Uptime",
|
|
||||||
"Use HTTPS for GUI": "GUI için HTTPS kullan",
|
|
||||||
"Version": "Sürüm",
|
|
||||||
"Versions Path": "Sürüm Dizini",
|
|
||||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Sürümler, tanımlı azami süre veya belirlenen zaman aralığı için izin verilen dosya sayısı aşılmışsa kendiliğinden silinir.",
|
|
||||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Yeni bir cihaz eklendiğinde, bu cihazın karşı tarafa da eklenmesi gerektiğini unutmayın.",
|
|
||||||
"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.": "Yeni bir klasör eklendiğinde, Klasör ID'nin klasörleri cihazlar arasında bağlantılandırmak için kullanıldığını unutmayın. Klasör ID'ler büyük - küçük harf duyarlıdır ve bütün cihazlarda tamı tamına eşleşmelidir.",
|
|
||||||
"Yes": "Evet",
|
|
||||||
"You must keep at least one version.": "En az bir sürümü tutmalısınız.",
|
|
||||||
"full documentation": "belgelendirme içeriğinin tümü",
|
|
||||||
"items": "öğeler",
|
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} \"{{folder}}\" klasörünü paylaşmak istiyor."
|
|
||||||
}
|
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"Edit Folder": "Редагувати директорію",
|
"Edit Folder": "Редагувати директорію",
|
||||||
"Editing": "Редагування",
|
"Editing": "Редагування",
|
||||||
"Enable UPnP": "Увімкнути UPnP",
|
"Enable UPnP": "Увімкнути 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.": "Введіть розділені комою (\"tcp://ip:port\", \"tcp://host:port\") адреси або \"dynamic\" для автоматичного визначення адреси.",
|
||||||
"Enter ignore patterns, one per line.": "Введіть шаблони ігнорування, по одному на рядок.",
|
"Enter ignore patterns, one per line.": "Введіть шаблони ігнорування, по одному на рядок.",
|
||||||
"Error": "Помилка",
|
"Error": "Помилка",
|
||||||
"External File Versioning": "Зовнішне керування версіями",
|
"External File Versioning": "Зовнішне керування версіями",
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
"Global Discovery Server": "Сервер для глобального виявлення",
|
"Global Discovery Server": "Сервер для глобального виявлення",
|
||||||
"Global State": "Глобальний статус",
|
"Global State": "Глобальний статус",
|
||||||
"Help": "Допомога",
|
"Help": "Допомога",
|
||||||
"Home page": "Home page",
|
"Home page": "Домашня сторінка",
|
||||||
"Ignore": "Ігнорувати",
|
"Ignore": "Ігнорувати",
|
||||||
"Ignore Patterns": "Ігнорувати шаблони",
|
"Ignore Patterns": "Ігнорувати шаблони",
|
||||||
"Ignore Permissions": "Ігнорувати права доступу до файлів",
|
"Ignore Permissions": "Ігнорувати права доступу до файлів",
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
"Major Upgrade": "Мажорне оновлення",
|
"Major Upgrade": "Мажорне оновлення",
|
||||||
"Maximum Age": "Максимальний вік",
|
"Maximum Age": "Максимальний вік",
|
||||||
"Metadata Only": "Тільки метадані",
|
"Metadata Only": "Тільки метадані",
|
||||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
"Minimum Free Disk Space": "Мінімальний вільний простір на диску",
|
||||||
"Move to top of queue": "Пересунути у початок черги",
|
"Move to top of queue": "Пересунути у початок черги",
|
||||||
"Multi level wildcard (matches multiple directory levels)": "Багаторівнева маска (пошук збігів в усіх піддиректоріях) ",
|
"Multi level wildcard (matches multiple directory levels)": "Багаторівнева маска (пошук збігів в усіх піддиректоріях) ",
|
||||||
"Never": "Ніколи",
|
"Never": "Ніколи",
|
||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "Перезаписати зміни",
|
"Override Changes": "Перезаписати зміни",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Шлях до директорії на локальному комп’ютері. Буде створений, якщо такий не існує. Символ тильди (~) може бути використаний як ярлик для",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Шлях до директорії на локальному комп’ютері. Буде створений, якщо такий не існує. Символ тильди (~) може бути використаний як ярлик для",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Шлях, де повинні зберігатися версії (залиште порожнім для зберігання в .stversions в середині директорії)",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Шлях, де повинні зберігатися версії (залиште порожнім для зберігання в .stversions в середині директорії)",
|
||||||
|
"Pause": "Пауза",
|
||||||
|
"Paused": "Призупинено",
|
||||||
"Please consult the release notes before performing a major upgrade.": "Будь ласка перегляньте примітки до випуску перед мажорним оновленням. ",
|
"Please consult the release notes before performing a major upgrade.": "Будь ласка перегляньте примітки до випуску перед мажорним оновленням. ",
|
||||||
"Please wait": "Будь ласка, зачекайте",
|
"Please wait": "Будь ласка, зачекайте",
|
||||||
"Preview": "Попередній перегляд",
|
"Preview": "Попередній перегляд",
|
||||||
@@ -120,16 +122,17 @@
|
|||||||
"Quick guide to supported patterns": "Швидкий посібник по шаблонам, що підтримуються",
|
"Quick guide to supported patterns": "Швидкий посібник по шаблонам, що підтримуються",
|
||||||
"RAM Utilization": "Використання RAM",
|
"RAM Utilization": "Використання RAM",
|
||||||
"Random": "Випадково",
|
"Random": "Випадково",
|
||||||
"Relayed via": "Relayed via",
|
"Relayed via": "Ретранслювати через",
|
||||||
"Relays": "Relays",
|
"Relays": "Ретрансляція",
|
||||||
"Release Notes": "Примітки до випуску",
|
"Release Notes": "Примітки до випуску",
|
||||||
"Remove": "Remove",
|
"Remove": "Видалити",
|
||||||
"Rescan": "Пересканувати",
|
"Rescan": "Пересканувати",
|
||||||
"Rescan All": "Пересканувати усе",
|
"Rescan All": "Пересканувати усе",
|
||||||
"Rescan Interval": "Інтервал для повторного сканування",
|
"Rescan Interval": "Інтервал для повторного сканування",
|
||||||
"Restart": "Перезапуск",
|
"Restart": "Перезапуск",
|
||||||
"Restart Needed": "Необхідний перезапуск",
|
"Restart Needed": "Необхідний перезапуск",
|
||||||
"Restarting": "Відбувається перезапуск",
|
"Restarting": "Відбувається перезапуск",
|
||||||
|
"Resume": "Продовжити",
|
||||||
"Reused": "Використано вдруге",
|
"Reused": "Використано вдруге",
|
||||||
"Save": "Зберегти",
|
"Save": "Зберегти",
|
||||||
"Scanning": "Сканування",
|
"Scanning": "Сканування",
|
||||||
@@ -154,7 +157,7 @@
|
|||||||
"Source Code": "Сирцевий код",
|
"Source Code": "Сирцевий код",
|
||||||
"Staggered File Versioning": "Поступове версіонування",
|
"Staggered File Versioning": "Поступове версіонування",
|
||||||
"Start Browser": "Запустити браузер",
|
"Start Browser": "Запустити браузер",
|
||||||
"Statistics": "Statistics",
|
"Statistics": "Статистика",
|
||||||
"Stopped": "Зупинено",
|
"Stopped": "Зупинено",
|
||||||
"Support": "Підтримка",
|
"Support": "Підтримка",
|
||||||
"Sync Protocol Listen Addresses": "Адреса панелі управління",
|
"Sync Protocol Listen Addresses": "Адреса панелі управління",
|
||||||
@@ -180,13 +183,13 @@
|
|||||||
"The following items could not be synchronized.": "Наступні пункти не можуть бути синхронізовані.",
|
"The following items could not be synchronized.": "Наступні пункти не можуть бути синхронізовані.",
|
||||||
"The maximum age must be a number and cannot be blank.": "Максимальний термін повинен бути числом та не може бути пустим.",
|
"The maximum age must be a number and cannot be blank.": "Максимальний термін повинен бути числом та не може бути пустим.",
|
||||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максимальний термін, щоб зберігати версію (у днях, вствновіть в 0, щоби зберігати версії назавжди).",
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максимальний термін, щоб зберігати версію (у днях, вствновіть в 0, щоби зберігати версії назавжди).",
|
||||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "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).": "Відсоток мінімального вільного простору на диску має бути додатнім числом від 0 до 100 включно.",
|
||||||
"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 number of old versions to keep, per file.": "Кількість старих версій, яку необхідно зберігати для кожного файлу.",
|
"The number of old versions to keep, per file.": "Кількість старих версій, яку необхідно зберігати для кожного файлу.",
|
||||||
"The number of versions must be a number and cannot be blank.": "Кількість версій повинна бути цифрою та не може бути порожньою.",
|
"The number of versions must be a number and cannot be blank.": "Кількість версій повинна бути цифрою та не може бути порожньою.",
|
||||||
"The path cannot be blank.": "Шлях не може бути порожнім.",
|
"The path cannot be blank.": "Шлях не може бути порожнім.",
|
||||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
"The rate limit must be a non-negative number (0: no limit)": "Швидкість має бути додатнім числом.",
|
||||||
"The rescan interval must be a non-negative number of seconds.": "Інтервал повторного сканування повинен бути неід’ємною величиною.",
|
"The rescan interval must be a non-negative number of seconds.": "Інтервал повторного сканування повинен бути неід’ємною величиною.",
|
||||||
"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 is a major version upgrade.": "Це оновлення мажорної версії",
|
"This is a major version upgrade.": "Це оновлення мажорної версії",
|
||||||
@@ -209,6 +212,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.": "Коли додаєте нову директорію, пам’ятайте, що ID цієї директорії використовується для того, щоб зв’язувати директорії разом між вузлами. Назви є чутливими до регістра та повинні співпадати точно між усіма вузлами.",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Коли додаєте нову директорію, пам’ятайте, що ID цієї директорії використовується для того, щоб зв’язувати директорії разом між вузлами. Назви є чутливими до регістра та повинні співпадати точно між усіма вузлами.",
|
||||||
"Yes": "Так",
|
"Yes": "Так",
|
||||||
"You must keep at least one version.": "Ви повинні зберігати щонайменше одну версію.",
|
"You must keep at least one version.": "Ви повинні зберігати щонайменше одну версію.",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "повна документація",
|
"full documentation": "повна документація",
|
||||||
"items": "елементи",
|
"items": "елементи",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хоче поділитися директорією \"{{folder}}\"."
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хоче поділитися директорією \"{{folder}}\"."
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "撤销改变",
|
"Override Changes": "撤销改变",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "文件夹在本地的路径。如果不存在,则会被创建。波浪线符号(~)是如下路径的缩略符:",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "文件夹在本地的路径。如果不存在,则会被创建。波浪线符号(~)是如下路径的缩略符:",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "用来存储历史版本的文件夹(留空则将默认会存储在.stversions文件夹中)",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "用来存储历史版本的文件夹(留空则将默认会存储在.stversions文件夹中)",
|
||||||
|
"Pause": "暂停",
|
||||||
|
"Paused": "暂停中",
|
||||||
"Please consult the release notes before performing a major upgrade.": "请在进行重大更新前查看发布说明。",
|
"Please consult the release notes before performing a major upgrade.": "请在进行重大更新前查看发布说明。",
|
||||||
"Please wait": "请稍候",
|
"Please wait": "请稍候",
|
||||||
"Preview": "预览",
|
"Preview": "预览",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "重启 Syncthing",
|
"Restart": "重启 Syncthing",
|
||||||
"Restart Needed": "需要重启 Syncthing",
|
"Restart Needed": "需要重启 Syncthing",
|
||||||
"Restarting": "重启中",
|
"Restarting": "重启中",
|
||||||
|
"Resume": "恢复",
|
||||||
"Reused": "复用",
|
"Reused": "复用",
|
||||||
"Save": "保存",
|
"Save": "保存",
|
||||||
"Scanning": "扫描中",
|
"Scanning": "扫描中",
|
||||||
@@ -209,6 +212,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.": "若你添加了新文件夹,记住文件夹标识是用以在不同设备间建立联系的。在不同设备间拥有相同标识的文件夹将会被同步。且文件夹标识大小写敏感。",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "若你添加了新文件夹,记住文件夹标识是用以在不同设备间建立联系的。在不同设备间拥有相同标识的文件夹将会被同步。且文件夹标识大小写敏感。",
|
||||||
"Yes": "是",
|
"Yes": "是",
|
||||||
"You must keep at least one version.": "您必须保留至少一个版本。",
|
"You must keep at least one version.": "您必须保留至少一个版本。",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "完整文档",
|
"full documentation": "完整文档",
|
||||||
"items": "条目",
|
"items": "条目",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想将 “{{folder}}” 文件夹共享给您"
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想将 “{{folder}}” 文件夹共享给您"
|
||||||
|
|||||||
@@ -113,6 +113,8 @@
|
|||||||
"Override Changes": "置換改變",
|
"Override Changes": "置換改變",
|
||||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "資料夾在本地電腦的路徑。若資料夾不存在則會建立。波浪符號 (~) 可用作下列資料夾的捷徑:",
|
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "資料夾在本地電腦的路徑。若資料夾不存在則會建立。波浪符號 (~) 可用作下列資料夾的捷徑:",
|
||||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "儲存歷史版本的路徑 (若為空,則預設使用資料夾中的 .stversions 資料夾)。",
|
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "儲存歷史版本的路徑 (若為空,則預設使用資料夾中的 .stversions 資料夾)。",
|
||||||
|
"Pause": "Pause",
|
||||||
|
"Paused": "Paused",
|
||||||
"Please consult the release notes before performing a major upgrade.": "執行重大升級前請先參閱版本資訊。",
|
"Please consult the release notes before performing a major upgrade.": "執行重大升級前請先參閱版本資訊。",
|
||||||
"Please wait": "請稍後",
|
"Please wait": "請稍後",
|
||||||
"Preview": "預覽",
|
"Preview": "預覽",
|
||||||
@@ -130,6 +132,7 @@
|
|||||||
"Restart": "重新啟動",
|
"Restart": "重新啟動",
|
||||||
"Restart Needed": "需要重新啟動",
|
"Restart Needed": "需要重新啟動",
|
||||||
"Restarting": "正在重新啟動",
|
"Restarting": "正在重新啟動",
|
||||||
|
"Resume": "Resume",
|
||||||
"Reused": "Reused",
|
"Reused": "Reused",
|
||||||
"Save": "儲存",
|
"Save": "儲存",
|
||||||
"Scanning": "正在掃描",
|
"Scanning": "正在掃描",
|
||||||
@@ -209,6 +212,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.": "當新增一個資料夾時,請記住,資料夾識別碼是用來將裝置之間的資料夾綁定在一起的。它們有區分大小寫,且必須在所有裝置之間完全相同。",
|
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "當新增一個資料夾時,請記住,資料夾識別碼是用來將裝置之間的資料夾綁定在一起的。它們有區分大小寫,且必須在所有裝置之間完全相同。",
|
||||||
"Yes": "是",
|
"Yes": "是",
|
||||||
"You must keep at least one version.": "您必須保留至少一個版本。",
|
"You must keep at least one version.": "您必須保留至少一個版本。",
|
||||||
|
"days": "days",
|
||||||
"full documentation": "完整說明文件",
|
"full documentation": "完整說明文件",
|
||||||
"items": "個項目",
|
"items": "個項目",
|
||||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想要分享資料夾 \"{{folder}}\"。"
|
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想要分享資料夾 \"{{folder}}\"。"
|
||||||
|
|||||||
@@ -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)","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","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
|
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)","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)"}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
var validLangs = ["bg","ca","ca@valencia","cs","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","tr","uk","zh-CN","zh-TW"]
|
var validLangs = ["bg","ca","ca@valencia","cs","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","uk","zh-CN","zh-TW"]
|
||||||
|
|||||||
+49
-18
@@ -70,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container" id="content">
|
||||||
|
|
||||||
<!-- Panel: Restart Needed -->
|
<!-- Panel: Restart Needed -->
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
<div class="panel panel-warning">
|
<div class="panel panel-warning">
|
||||||
<div class="panel-heading"><h3 class="panel-title"><span class="fa fa-exclamation-circle"></span><span translate>Notice</span></h3></div>
|
<div class="panel-heading"><h3 class="panel-title"><span class="fa fa-exclamation-circle"></span><span translate>Notice</span></h3></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p ng-repeat="err in errorList()"><small>{{err.time | date:"yyyy-MM-dd HH:mm:ss"}}:</small> {{friendlyDevices(err.error)}}</p>
|
<p ng-repeat="err in errorList()"><small>{{err.when | date:"yyyy-MM-dd HH:mm:ss"}}:</small> {{friendlyDevices(err.message)}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
<button type="button" class="btn btn-sm btn-default pull-right" ng-click="clearErrors()">
|
<button type="button" class="btn btn-sm btn-default pull-right" ng-click="clearErrors()">
|
||||||
@@ -190,18 +190,25 @@
|
|||||||
<!-- Folder list (top left) -->
|
<!-- Folder list (top left) -->
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
<h3 translate>Folders</h3>
|
||||||
<div class="panel-group" id="folders">
|
<div class="panel-group" id="folders">
|
||||||
<div class="visible-xs"><h3><span translate>Folders</span></h3><hr></div>
|
|
||||||
<div class="panel panel-default" ng-repeat="folder in folderList()">
|
<div class="panel panel-default" ng-repeat="folder in folderList()">
|
||||||
<div class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer">
|
<div class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer">
|
||||||
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id)}}%"></div>
|
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id)}}%"></div>
|
||||||
|
<div class="panel-progress" ng-show="folderStatus(folder) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id)}}%"></div>
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
<span class="fa fa-folder hidden-xs"></span>{{folder.id}}
|
<span class="fa fa-folder hidden-xs"></span>{{folder.id}}
|
||||||
<span class="pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
|
<span class="pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
|
||||||
<span ng-switch-when="unknown"><span class="hidden-xs" translate>Unknown</span><span class="visible-xs">◼</span></span>
|
<span ng-switch-when="unknown"><span class="hidden-xs" translate>Unknown</span><span class="visible-xs">◼</span></span>
|
||||||
<span ng-switch-when="unshared"><span class="hidden-xs" translate>Unshared</span><span class="visible-xs">◼</span></span>
|
<span ng-switch-when="unshared"><span class="hidden-xs" translate>Unshared</span><span class="visible-xs">◼</span></span>
|
||||||
<span ng-switch-when="stopped"><span class="hidden-xs" translate>Stopped</span><span class="visible-xs">◼</span></span>
|
<span ng-switch-when="stopped"><span class="hidden-xs" translate>Stopped</span><span class="visible-xs">◼</span></span>
|
||||||
<span ng-switch-when="scanning"><span class="hidden-xs" translate>Scanning</span><span class="visible-xs">◼</span></span>
|
<span ng-switch-when="scanning">
|
||||||
|
<span class="hidden-xs" translate>Scanning</span>
|
||||||
|
<span class="hidden-xs" ng-if="scanPercentage(folder.id) != undefined">
|
||||||
|
({{scanPercentage(folder.id)}}%)
|
||||||
|
</span>
|
||||||
|
<span class="visible-xs">◼</span>
|
||||||
|
</span>
|
||||||
<span ng-switch-when="idle"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">◼</span></span>
|
<span ng-switch-when="idle"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">◼</span></span>
|
||||||
<span ng-switch-when="syncing">
|
<span ng-switch-when="syncing">
|
||||||
<span class="hidden-xs" translate>Syncing</span>
|
<span class="hidden-xs" translate>Syncing</span>
|
||||||
@@ -232,7 +239,7 @@
|
|||||||
<td class="text-right">{{model[folder.id].localFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].localBytes | binary}}B</td>
|
<td class="text-right">{{model[folder.id].localFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].localBytes | binary}}B</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="model[folder.id].needFiles > 0">
|
<tr ng-if="model[folder.id].needFiles > 0">
|
||||||
<th><span class="fa fa-fw fa-cloud-download"></span> <span translate>Out of Sync</span></th>
|
<th><span class="fa fa-fw fa-cloud-download"></span> <span translate>Out of Sync Items</span></th>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<a href="" ng-click="showNeed(folder.id)">{{model[folder.id].needFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].needBytes | binary}}B</a>
|
<a href="" ng-click="showNeed(folder.id)">{{model[folder.id].needFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].needBytes | binary}}B</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -341,7 +348,7 @@
|
|||||||
<!-- This device -->
|
<!-- This device -->
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="visible-xs"><h3><span translate>Devices</span></h3><hr></div>
|
<h3 translate>Devices</h3>
|
||||||
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
|
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
|
||||||
<div class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer">
|
<div class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
@@ -372,15 +379,28 @@
|
|||||||
<th><span class="fa fa-fw fa-tachometer"></span> <span translate>CPU Utilization</span></th>
|
<th><span class="fa fa-fw fa-tachometer"></span> <span translate>CPU Utilization</span></th>
|
||||||
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="system.extAnnounceOK != undefined && announceServersTotal > 0">
|
<tr ng-if="system.discoveryEnabled">
|
||||||
<th><span class="fa fa-fw fa-bullhorn"></span> <span translate>Global Discovery</span></th>
|
<th><span class="fa fa-fw fa-map-signs"></span> <span translate>Discovery</span></th>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span ng-if="announceServersFailed.length == 0" class="data text-success">
|
<span ng-if="discoveryFailed.length == 0" class="data text-success">
|
||||||
<span>OK</span>
|
<span>{{discoveryTotal}}/{{discoveryTotal}}</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="announceServersFailed.length != 0" class="data" ng-class="{'text-danger': announceServersFailed.length == announceServersTotal}">
|
<span ng-if="discoveryFailed.length != 0" class="data" ng-class="{'text-danger': discoveryFailed.length == discoveryTotal}">
|
||||||
<span popover data-trigger="hover" data-placement="bottom" data-content="{{announceServersFailed.join('\n')}}">
|
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{discoveryFailed.join('<br>\n')}}">
|
||||||
{{announceServersTotal-announceServersFailed.length}}/{{announceServersTotal}}
|
{{discoveryTotal-discoveryFailed.length}}/{{discoveryTotal}}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="system.relaysEnabled">
|
||||||
|
<th><span class="fa fa-fw fa-sitemap"></span> <span translate>Relays</span></th>
|
||||||
|
<td class="text-right">
|
||||||
|
<span ng-if="relaysFailed.length == 0" class="data text-success">
|
||||||
|
<span>{{relaysTotal}}/{{relaysTotal}}</span>
|
||||||
|
</span>
|
||||||
|
<span ng-if="relaysFailed.length != 0" class="data" ng-class="{'text-danger': relaysFailed.length == relaysTotal}">
|
||||||
|
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{relaysFailed.join('<br>\n')}}">
|
||||||
|
{{relaysTotal-relaysFailed.length}}/{{relaysTotal}}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -412,6 +432,7 @@
|
|||||||
<span ng-switch-when="syncing">
|
<span ng-switch-when="syncing">
|
||||||
<span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | number:0}}%)
|
<span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | number:0}}%)
|
||||||
</span>
|
</span>
|
||||||
|
<span ng-switch-when="paused"><span class="hidden-xs" translate>Paused</span><span class="visible-xs">◼</span></span>
|
||||||
<span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">◼</span></span>
|
<span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">◼</span></span>
|
||||||
<span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">◼</span></span>
|
<span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">◼</span></span>
|
||||||
</span>
|
</span>
|
||||||
@@ -421,16 +442,20 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<table class="table table-condensed table-striped">
|
<table class="table table-condensed table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-if="connections[deviceCfg.deviceID]">
|
<tr ng-if="connections[deviceCfg.deviceID].connected">
|
||||||
<th><span class="fa fa-fw fa-cloud-download"></span> <span translate>Download Rate</span></th>
|
<th><span class="fa fa-fw fa-cloud-download"></span> <span translate>Download Rate</span></th>
|
||||||
<td class="text-right">{{connections[deviceCfg.deviceID].inbps | binary}}B/s ({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)</td>
|
<td class="text-right">{{connections[deviceCfg.deviceID].inbps | binary}}B/s ({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="connections[deviceCfg.deviceID]">
|
<tr ng-if="connections[deviceCfg.deviceID].connected">
|
||||||
<th><span class="fa fa-fw fa-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
<th><span class="fa fa-fw fa-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
||||||
<td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
|
<td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><span class="fa fa-fw fa-link"></span> <span translate>Address</span></th>
|
<th>
|
||||||
|
<span class="fa fa-fw fa-link"></span>
|
||||||
|
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('direct') == 0" >Address</span>
|
||||||
|
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('relay') == 0" >Relayed via</span>
|
||||||
|
</th>
|
||||||
<td class="text-right">{{deviceAddr(deviceCfg)}}</td>
|
<td class="text-right">{{deviceAddr(deviceCfg)}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="deviceCfg.compression != 'metadata'">
|
<tr ng-if="deviceCfg.compression != 'metadata'">
|
||||||
@@ -444,11 +469,11 @@
|
|||||||
<th><span class="fa fa-fw fa-thumbs-o-up"></span> <span translate>Introducer</span></th>
|
<th><span class="fa fa-fw fa-thumbs-o-up"></span> <span translate>Introducer</span></th>
|
||||||
<td translate class="text-right">Yes</td>
|
<td translate class="text-right">Yes</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="connections[deviceCfg.deviceID]">
|
<tr ng-if="connections[deviceCfg.deviceID].version">
|
||||||
<th><span class="fa fa-fw fa-tag"></span> <span translate>Version</span></th>
|
<th><span class="fa fa-fw fa-tag"></span> <span translate>Version</span></th>
|
||||||
<td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
|
<td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!connections[deviceCfg.deviceID]">
|
<tr ng-if="!connections[deviceCfg.deviceID].connected">
|
||||||
<th><span class="fa fa-fw fa-eye"></span> <span translate>Last seen</span></th>
|
<th><span class="fa fa-fw fa-eye"></span> <span translate>Last seen</span></th>
|
||||||
<td translate ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays || deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" class="text-right">Never</td>
|
<td translate ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays || deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" class="text-right">Never</td>
|
||||||
<td ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}</td>
|
<td ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm:ss"}}</td>
|
||||||
@@ -465,6 +490,12 @@
|
|||||||
<button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
|
<button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
|
||||||
<span class="fa fa-pencil"></span> <span translate>Edit</span>
|
<span class="fa fa-pencil"></span> <span translate>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button ng-if="!connections[deviceCfg.deviceID].paused" type="button" class="btn btn-sm btn-default" ng-click="pauseDevice(deviceCfg.deviceID)">
|
||||||
|
<span class="fa fa-pause"></span> <span translate>Pause</span>
|
||||||
|
</button>
|
||||||
|
<button ng-if="connections[deviceCfg.deviceID].paused" type="button" class="btn btn-sm btn-default" ng-click="resumeDevice(deviceCfg.deviceID)">
|
||||||
|
<span class="fa fa-play"></span> <span translate>Resume</span>
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ var urlbase = 'rest';
|
|||||||
|
|
||||||
syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvider) {
|
syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvider) {
|
||||||
$httpProvider.interceptors.push(function xHeadersResponseInterceptor() {
|
$httpProvider.interceptors.push(function xHeadersResponseInterceptor() {
|
||||||
var guiVersion = null;
|
|
||||||
var deviceId = null;
|
var deviceId = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -38,14 +37,6 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
responseVersion = headers['x-syncthing-version'];
|
|
||||||
|
|
||||||
if (!guiVersion) {
|
|
||||||
guiVersion = responseVersion;
|
|
||||||
} else if (guiVersion != responseVersion) {
|
|
||||||
document.location.reload(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!deviceId) {
|
if (!deviceId) {
|
||||||
deviceId = headers['x-syncthing-id'];
|
deviceId = headers['x-syncthing-id'];
|
||||||
if (deviceId) {
|
if (deviceId) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<ul class="list-unstyled three-columns" id="contributor-list">
|
<ul class="list-unstyled three-columns" id="contributor-list">
|
||||||
<li class="auto-generated">Aaron Bieber</li>
|
<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">Alexander Graf</li>
|
||||||
<li class="auto-generated">Andrew Dunham</li>
|
<li class="auto-generated">Andrew Dunham</li>
|
||||||
<li class="auto-generated">Antony Male</li>
|
<li class="auto-generated">Antony Male</li>
|
||||||
@@ -59,6 +60,8 @@
|
|||||||
<li class="auto-generated">Marc Laporte</li>
|
<li class="auto-generated">Marc Laporte</li>
|
||||||
<li class="auto-generated">Marc Pujol</li>
|
<li class="auto-generated">Marc Pujol</li>
|
||||||
<li class="auto-generated">Marcin Dziadus</li>
|
<li class="auto-generated">Marcin Dziadus</li>
|
||||||
|
<li class="auto-generated">Mateon1</li>
|
||||||
|
<li class="auto-generated">Matt Burke</li>
|
||||||
<li class="auto-generated">Michael Jephcote</li>
|
<li class="auto-generated">Michael Jephcote</li>
|
||||||
<li class="auto-generated">Michael Tilli</li>
|
<li class="auto-generated">Michael Tilli</li>
|
||||||
<li class="auto-generated">Pascal Jungblut</li>
|
<li class="auto-generated">Pascal Jungblut</li>
|
||||||
@@ -75,6 +78,7 @@
|
|||||||
<li class="auto-generated">Tully Robinson</li>
|
<li class="auto-generated">Tully Robinson</li>
|
||||||
<li class="auto-generated">Veeti Paananen</li>
|
<li class="auto-generated">Veeti Paananen</li>
|
||||||
<li class="auto-generated">Vil Brekin</li>
|
<li class="auto-generated">Vil Brekin</li>
|
||||||
|
<li class="auto-generated">Yannic A.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ angular.module('syncthing.core')
|
|||||||
DEVICE_DISCONNECTED: 'DeviceDisconnected', // Generated each time a connection to a device has been terminated
|
DEVICE_DISCONNECTED: 'DeviceDisconnected', // Generated each time a connection to a device has been terminated
|
||||||
DEVICE_DISCOVERED: 'DeviceDiscovered', // Emitted when a new device is discovered using local discovery
|
DEVICE_DISCOVERED: 'DeviceDiscovered', // Emitted when a new device is discovered using local discovery
|
||||||
DEVICE_REJECTED: 'DeviceRejected', // Emitted when there is a connection from a device we are not configured to talk to
|
DEVICE_REJECTED: 'DeviceRejected', // Emitted when there is a connection from a device we are not configured to talk to
|
||||||
|
DEVICE_PAUSED: 'DevicePaused', // Emitted when a device has been paused
|
||||||
|
DEVICE_RESUMED: 'DeviceResumed', // Emitted when a device has been resumed
|
||||||
DOWNLOAD_PROGRESS: 'DownloadProgress', // Emitted during file downloads for each folder for each file
|
DOWNLOAD_PROGRESS: 'DownloadProgress', // Emitted during file downloads for each folder for each file
|
||||||
FOLDER_COMPLETION: 'FolderCompletion', //Emitted when the local or remote contents for a folder changes
|
FOLDER_COMPLETION: 'FolderCompletion', //Emitted when the local or remote contents for a folder changes
|
||||||
FOLDER_REJECTED: 'FolderRejected', // Emitted when a device sends index information for a folder we do not have, or have but do not share with the device in question
|
FOLDER_REJECTED: 'FolderRejected', // Emitted when a device sends index information for a folder we do not have, or have but do not share with the device in question
|
||||||
@@ -76,6 +78,7 @@ angular.module('syncthing.core')
|
|||||||
STARTUP_COMPLETED: 'StartupCompleted', // Emitted exactly once, when initialization is complete and Syncthing is ready to start exchanging data with other devices
|
STARTUP_COMPLETED: 'StartupCompleted', // Emitted exactly once, when initialization is complete and Syncthing is ready to start exchanging data with other devices
|
||||||
STATE_CHANGED: 'StateChanged', // Emitted when a folder changes state
|
STATE_CHANGED: 'StateChanged', // Emitted when a folder changes state
|
||||||
FOLDER_ERRORS: 'FolderErrors', // Emitted when a folder has errors preventing a full sync
|
FOLDER_ERRORS: 'FolderErrors', // Emitted when a folder has errors preventing a full sync
|
||||||
|
FOLDER_SCAN_PROGRESS: 'FolderScanProgress', // Emitted every ScanProgressIntervalS seconds, indicating how far into the scan it is at.
|
||||||
|
|
||||||
start: function() {
|
start: function() {
|
||||||
$http.get(urlbase + '/events?limit=1')
|
$http.get(urlbase + '/events?limit=1')
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ angular.module('syncthing.core')
|
|||||||
$scope.failedCurrentPage = 1;
|
$scope.failedCurrentPage = 1;
|
||||||
$scope.failedCurrentFolder = undefined;
|
$scope.failedCurrentFolder = undefined;
|
||||||
$scope.failedPageSize = 10;
|
$scope.failedPageSize = 10;
|
||||||
|
$scope.scanProgress = {};
|
||||||
|
|
||||||
$scope.localStateTotal = {
|
$scope.localStateTotal = {
|
||||||
bytes: 0,
|
bytes: 0,
|
||||||
@@ -89,6 +90,12 @@ angular.module('syncthing.core')
|
|||||||
refreshFolderStats();
|
refreshFolderStats();
|
||||||
|
|
||||||
$http.get(urlbase + '/system/version').success(function (data) {
|
$http.get(urlbase + '/system/version').success(function (data) {
|
||||||
|
if ($scope.version.version && $scope.version.version != data.version) {
|
||||||
|
// We already have a version response, but it differs from
|
||||||
|
// the new one. Reload the full GUI in case it's changed.
|
||||||
|
document.location.reload(true);
|
||||||
|
}
|
||||||
|
|
||||||
$scope.version = data;
|
$scope.version = data;
|
||||||
}).error($scope.emitHTTPError);
|
}).error($scope.emitHTTPError);
|
||||||
|
|
||||||
@@ -157,6 +164,12 @@ angular.module('syncthing.core')
|
|||||||
if (data.to === 'syncing') {
|
if (data.to === 'syncing') {
|
||||||
$scope.failed[data.folder] = [];
|
$scope.failed[data.folder] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a folder has started scanning, then any scan progress is
|
||||||
|
// also obsolete.
|
||||||
|
if (data.to === 'scanning') {
|
||||||
|
delete $scope.scanProgress[data.folder];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -165,7 +178,7 @@ angular.module('syncthing.core')
|
|||||||
});
|
});
|
||||||
|
|
||||||
$scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
|
$scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
|
||||||
delete $scope.connections[arg.data.id];
|
$scope.connections[arg.data.id].connected = false;
|
||||||
refreshDeviceStats();
|
refreshDeviceStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,6 +189,7 @@ angular.module('syncthing.core')
|
|||||||
outbps: 0,
|
outbps: 0,
|
||||||
inBytesTotal: 0,
|
inBytesTotal: 0,
|
||||||
outBytesTotal: 0,
|
outBytesTotal: 0,
|
||||||
|
type: arg.data.type,
|
||||||
address: arg.data.addr
|
address: arg.data.addr
|
||||||
};
|
};
|
||||||
$scope.completion[arg.data.id] = {
|
$scope.completion[arg.data.id] = {
|
||||||
@@ -208,6 +222,14 @@ angular.module('syncthing.core')
|
|||||||
$scope.deviceRejections[arg.data.device] = arg;
|
$scope.deviceRejections[arg.data.device] = arg;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.$on(Events.DEVICE_PAUSED, function (event, arg) {
|
||||||
|
$scope.connections[arg.data.device].paused = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$on(Events.DEVICE_RESUMED, function (event, arg) {
|
||||||
|
$scope.connections[arg.data.device].paused = false;
|
||||||
|
});
|
||||||
|
|
||||||
$scope.$on(Events.FOLDER_REJECTED, function (event, arg) {
|
$scope.$on(Events.FOLDER_REJECTED, function (event, arg) {
|
||||||
$scope.folderRejections[arg.data.folder + "-" + arg.data.device] = arg;
|
$scope.folderRejections[arg.data.folder + "-" + arg.data.device] = arg;
|
||||||
});
|
});
|
||||||
@@ -295,6 +317,15 @@ angular.module('syncthing.core')
|
|||||||
$scope.failed[data.folder] = data.errors;
|
$scope.failed[data.folder] = data.errors;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$scope.$on(Events.FOLDER_SCAN_PROGRESS, function (event, arg) {
|
||||||
|
var data = arg.data;
|
||||||
|
$scope.scanProgress[data.folder] = {
|
||||||
|
current: data.current,
|
||||||
|
total: data.total
|
||||||
|
};
|
||||||
|
console.log("FolderScanProgress", data);
|
||||||
|
});
|
||||||
|
|
||||||
$scope.emitHTTPError = function (data, status, headers, config) {
|
$scope.emitHTTPError = function (data, status, headers, config) {
|
||||||
$scope.$emit('HTTPError', {data: data, status: status, headers: headers, config: config});
|
$scope.$emit('HTTPError', {data: data, status: status, headers: headers, config: config});
|
||||||
};
|
};
|
||||||
@@ -346,15 +377,26 @@ angular.module('syncthing.core')
|
|||||||
$http.get(urlbase + '/system/status').success(function (data) {
|
$http.get(urlbase + '/system/status').success(function (data) {
|
||||||
$scope.myID = data.myID;
|
$scope.myID = data.myID;
|
||||||
$scope.system = data;
|
$scope.system = data;
|
||||||
$scope.announceServersTotal = data.extAnnounceOK ? Object.keys(data.extAnnounceOK).length : 0;
|
|
||||||
var failed = [];
|
|
||||||
for (var server in data.extAnnounceOK) {
|
|
||||||
if (!data.extAnnounceOK[server]) {
|
|
||||||
failed.push(server);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$scope.announceServersFailed = failed;
|
|
||||||
|
|
||||||
|
$scope.discoveryTotal = data.discoveryMethods;
|
||||||
|
var discoveryFailed = [];
|
||||||
|
for (var disco in data.discoveryErrors) {
|
||||||
|
if (data.discoveryErrors[disco]) {
|
||||||
|
discoveryFailed.push(disco + ": " + data.discoveryErrors[disco]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$scope.discoveryFailed = discoveryFailed;
|
||||||
|
|
||||||
|
var relaysFailed = [];
|
||||||
|
var relaysTotal = 0;
|
||||||
|
for (var relay in data.relayClientStatus) {
|
||||||
|
if (!data.relayClientStatus[relay]) {
|
||||||
|
relaysFailed.push(relay);
|
||||||
|
}
|
||||||
|
relaysTotal++;
|
||||||
|
}
|
||||||
|
$scope.relaysFailed = relaysFailed;
|
||||||
|
$scope.relaysTotal = relaysTotal;
|
||||||
|
|
||||||
console.log("refreshSystem", data);
|
console.log("refreshSystem", data);
|
||||||
}).error($scope.emitHTTPError);
|
}).error($scope.emitHTTPError);
|
||||||
@@ -609,12 +651,28 @@ angular.module('syncthing.core')
|
|||||||
return Math.floor(pct);
|
return Math.floor(pct);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.scanPercentage = function (folder) {
|
||||||
|
if (!$scope.scanProgress[folder]) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
var pct = 100 * $scope.scanProgress[folder].current / $scope.scanProgress[folder].total;
|
||||||
|
return Math.floor(pct);
|
||||||
|
}
|
||||||
|
|
||||||
$scope.deviceStatus = function (deviceCfg) {
|
$scope.deviceStatus = function (deviceCfg) {
|
||||||
if ($scope.deviceFolders(deviceCfg).length === 0) {
|
if ($scope.deviceFolders(deviceCfg).length === 0) {
|
||||||
return 'unused';
|
return 'unused';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($scope.connections[deviceCfg.deviceID]) {
|
if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.connections[deviceCfg.deviceID].paused) {
|
||||||
|
return 'paused';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.connections[deviceCfg.deviceID].connected) {
|
||||||
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
|
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
|
||||||
return 'insync';
|
return 'insync';
|
||||||
} else {
|
} else {
|
||||||
@@ -632,7 +690,15 @@ angular.module('syncthing.core')
|
|||||||
return 'warning';
|
return 'warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($scope.connections[deviceCfg.deviceID]) {
|
if (typeof $scope.connections[deviceCfg.deviceID] === 'undefined') {
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.connections[deviceCfg.deviceID].paused) {
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.connections[deviceCfg.deviceID].connected) {
|
||||||
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
|
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
|
||||||
return 'success';
|
return 'success';
|
||||||
} else {
|
} else {
|
||||||
@@ -646,7 +712,7 @@ angular.module('syncthing.core')
|
|||||||
|
|
||||||
$scope.deviceAddr = function (deviceCfg) {
|
$scope.deviceAddr = function (deviceCfg) {
|
||||||
var conn = $scope.connections[deviceCfg.deviceID];
|
var conn = $scope.connections[deviceCfg.deviceID];
|
||||||
if (conn) {
|
if (conn && conn.connected) {
|
||||||
return conn.address;
|
return conn.address;
|
||||||
}
|
}
|
||||||
return '?';
|
return '?';
|
||||||
@@ -691,6 +757,14 @@ angular.module('syncthing.core')
|
|||||||
return device.deviceID.substr(0, 6);
|
return device.deviceID.substr(0, 6);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.pauseDevice = function (device) {
|
||||||
|
$http.post(urlbase + "/system/pause?device=" + device);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.resumeDevice = function (device) {
|
||||||
|
$http.post(urlbase + "/system/resume?device=" + device);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.editSettings = function () {
|
$scope.editSettings = function () {
|
||||||
// Make a working copy
|
// Make a working copy
|
||||||
$scope.tmpOptions = angular.copy($scope.config.options);
|
$scope.tmpOptions = angular.copy($scope.config.options);
|
||||||
@@ -969,13 +1043,16 @@ angular.module('syncthing.core')
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.errorList = function () {
|
$scope.errorList = function () {
|
||||||
|
if (!$scope.errors) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
return $scope.errors.filter(function (e) {
|
return $scope.errors.filter(function (e) {
|
||||||
return e.time > $scope.seenError;
|
return e.when > $scope.seenError;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.clearErrors = function () {
|
$scope.clearErrors = function () {
|
||||||
$scope.seenError = $scope.errors[$scope.errors.length - 1].time;
|
$scope.seenError = $scope.errors[$scope.errors.length - 1].when;
|
||||||
$http.post(urlbase + '/system/error/clear');
|
$http.post(urlbase + '/system/error/clear');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
<label translate for="deviceID">Device ID</label>
|
<label translate for="deviceID">Device ID</label>
|
||||||
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required valid-deviceid list="discovery-list" />
|
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required valid-deviceid list="discovery-list" />
|
||||||
<datalist id="discovery-list" ng-if="!editingExisting">
|
<datalist id="discovery-list" ng-if="!editingExisting">
|
||||||
<option ng-repeat="(id,address) in discovery" value="{{ id }}" />
|
<option ng-repeat="(id, data) in discovery" value="{{id}}" />
|
||||||
</datalist>
|
</datalist>
|
||||||
<div ng-if="editingExisting" class="well well-sm text-monospace">{{currentDevice.deviceID}}</div>
|
<div ng-if="editingExisting" class="well well-sm text-monospace" select-on-click>{{currentDevice.deviceID}}</div>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
<span translate ng-if="deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine">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).</span>
|
<span translate ng-if="deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine">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).</span>
|
||||||
<span translate ng-show="!editingExisting && (deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine)">When adding a new device, keep in mind that this device must be added on the other side too.</span>
|
<span translate ng-show="!editingExisting && (deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine)">When adding a new device, keep in mind that this device must be added on the other side too.</span>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate for="addresses">Addresses</label>
|
<label translate for="addresses">Addresses</label>
|
||||||
<input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice._addressesStr"></input>
|
<input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice._addressesStr"></input>
|
||||||
<p translate class="help-block">Enter comma separated "ip:port" addresses or "dynamic" to perform automatic discovery of the address.</p>
|
<p translate class="help-block">Enter comma separated ("tcp://ip:port", "tcp://host:port") addresses or "dynamic" to perform automatic discovery of the address.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate>Compression</label>
|
<label translate>Compression</label>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<modal id="idqr" status="info" icon="qrcode" title="{{'Device Identification' | translate}} - {{deviceName(thisDevice())}}" large="yes" close="yes">
|
<modal id="idqr" status="info" icon="qrcode" title="{{'Device Identification' | translate}} - {{deviceName(thisDevice())}}" large="yes" close="yes">
|
||||||
<div class="well well-sm text-monospace text-center">{{myID}}</div>
|
<div class="well well-sm text-monospace text-center" select-on-click>{{myID}}</div>
|
||||||
<img ng-if="myID" class="center-block img-thumbnail" ng-src="qr/?text={{myID}}"/>
|
<img ng-if="myID" class="center-block img-thumbnail" ng-src="qr/?text={{myID}}"/>
|
||||||
</modal>
|
</modal>
|
||||||
|
|||||||
@@ -13,12 +13,11 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
|
<div class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
|
||||||
<label for="folderID"><span translate>Folder ID</span></label>
|
<label for="folderID"><span translate>Folder ID</span></label>
|
||||||
<input name="folderID" ng-readonly="editingExisting" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required unique-folder ng-pattern="/^[a-zA-Z0-9-_.]{1,64}$/"></input>
|
<input name="folderID" ng-readonly="editingExisting" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required unique-folder></input>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
<span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Short identifier for the folder. Must be the same on all cluster devices.</span>
|
<span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Short identifier for the folder. Must be the same on all cluster devices.</span>
|
||||||
<span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
|
<span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
|
||||||
<span translate ng-if="folderEditor.folderID.$error.required && folderEditor.folderID.$dirty">The folder ID cannot be blank.</span>
|
<span translate ng-if="folderEditor.folderID.$error.required && folderEditor.folderID.$dirty">The folder ID cannot be blank.</span>
|
||||||
<span translate ng-if="folderEditor.folderID.$error.pattern && folderEditor.folderID.$dirty">The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.</span>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty}">
|
<div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty}">
|
||||||
@@ -40,8 +39,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-class="{'has-error': folderEditor.minDiskFreePct.$invalid && folderEditor.minDiskFreePct.$dirty}">
|
<div class="form-group" ng-class="{'has-error': folderEditor.minDiskFreePct.$invalid && folderEditor.minDiskFreePct.$dirty}">
|
||||||
<label for="minDiskFreePct"><span translate>Minimum Free Disk Space</span> (0-100%)</label>
|
<label for="minDiskFreePct"><span translate>Minimum Free Disk Space</span> (0.0 - 100.0%)</label>
|
||||||
<input name="minDiskFreePct" id="minDiskFreePct" class="form-control" type="number" ng-model="currentFolder.minDiskFreePct" required min="0" max="100"></input>
|
<input name="minDiskFreePct" id="minDiskFreePct" class="form-control" type="number" ng-model="currentFolder.minDiskFreePct" required min="0.0" max="100.0"></input>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
<span translate ng-if="!folderEditor.minDiskFreePct.$valid && folderEditor.minDiskFreePct.$dirty">The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).</span>
|
<span translate ng-if="!folderEditor.minDiskFreePct.$valid && folderEditor.minDiskFreePct.$dirty">The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -98,7 +97,7 @@
|
|||||||
<label translate for="trashcanClean">Clean out after</label>
|
<label translate for="trashcanClean">Clean out after</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input name="trashcanClean" id="trashcanClean" class="form-control text-right" type="number" ng-model="currentFolder.trashcanClean" required min="0"></input>
|
<input name="trashcanClean" id="trashcanClean" class="form-control text-right" type="number" ng-model="currentFolder.trashcanClean" required min="0"></input>
|
||||||
<div class="input-group-addon">days</div>
|
<div class="input-group-addon" translate>days</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
<span translate ng-if="folderEditor.trashcanClean.$valid || folderEditor.trashcanClean.$pristine">The number of days to keep files in the trash can. Zero means forever.</span>
|
<span translate ng-if="folderEditor.trashcanClean.$valid || folderEditor.trashcanClean.$pristine">The number of days to keep files in the trash can. Zero means forever.</span>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate>API Key</label>
|
<label translate>API Key</label>
|
||||||
<div class="well well-sm text-monospace">{{tmpGUI.apiKey || "-"}}</div>
|
<div class="well well-sm text-monospace" select-on-click>{{tmpGUI.apiKey || "-"}}</div>
|
||||||
<button type="button" class="btn btn-sm btn-default" ng-click="setAPIKey(tmpGUI)">
|
<button type="button" class="btn btn-sm btn-default" ng-click="setAPIKey(tmpGUI)">
|
||||||
<span class="fa fa-repeat"></span> <span translate>Generate</span>
|
<span class="fa fa-repeat"></span> <span translate>Generate</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<modal id="urPreview" status="success" icon="bar-chart" title="{{'Anonymous Usage Reporting' | translate}}" large="yes" close="yes" tabindex="-1">
|
<modal id="urPreview" status="success" icon="bar-chart" title="{{'Anonymous Usage Reporting' | translate}}" large="yes" close="yes" tabindex="-1">
|
||||||
<p translate>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.</p>
|
<p translate>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.</p>
|
||||||
<p translate translate-value-url="<a href="https://data.syncthing.net" target="_blank">https://data.syncthing.net</a>">The aggregated statistics are publicly available at {%url%}.</p>
|
<p translate translate-value-url="<a href="https://data.syncthing.net" target="_blank">https://data.syncthing.net</a>">The aggregated statistics are publicly available at {%url%}.</p>
|
||||||
<pre><small>{{reportData | json}}</small></pre>
|
<form>
|
||||||
|
<textarea class="form-control" rows="20">{{reportData | json}}</textarea>
|
||||||
|
</form>
|
||||||
</modal>
|
</modal>
|
||||||
|
|||||||
+45
-46
File diff suppressed because one or more lines are too long
+21
-20
@@ -6,7 +6,12 @@
|
|||||||
|
|
||||||
package beacon
|
package beacon
|
||||||
|
|
||||||
import "net"
|
import (
|
||||||
|
"net"
|
||||||
|
stdsync "sync"
|
||||||
|
|
||||||
|
"github.com/thejerf/suture"
|
||||||
|
)
|
||||||
|
|
||||||
type recv struct {
|
type recv struct {
|
||||||
data []byte
|
data []byte
|
||||||
@@ -14,34 +19,30 @@ type recv struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
|
suture.Service
|
||||||
Send(data []byte)
|
Send(data []byte)
|
||||||
Recv() ([]byte, net.Addr)
|
Recv() ([]byte, net.Addr)
|
||||||
|
Error() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type readerFrom interface {
|
type readerFrom interface {
|
||||||
ReadFrom([]byte) (int, net.Addr, error)
|
ReadFrom([]byte) (int, net.Addr, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func genericReader(conn readerFrom, outbox chan<- recv) {
|
type errorHolder struct {
|
||||||
bs := make([]byte, 65536)
|
err error
|
||||||
for {
|
mut stdsync.Mutex // uses stdlib sync as I want this to be trivially embeddable, and there is no risk of blocking
|
||||||
n, addr, err := conn.ReadFrom(bs)
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("multicast read:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if debug {
|
|
||||||
l.Debugf("recv %d bytes from %s", n, addr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c := make([]byte, n)
|
func (e *errorHolder) setError(err error) {
|
||||||
copy(c, bs)
|
e.mut.Lock()
|
||||||
select {
|
e.err = err
|
||||||
case outbox <- recv{c, addr}:
|
e.mut.Unlock()
|
||||||
default:
|
|
||||||
if debug {
|
|
||||||
l.Debugln("dropping message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *errorHolder) Error() error {
|
||||||
|
e.mut.Lock()
|
||||||
|
err := e.err
|
||||||
|
e.mut.Unlock()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-51
@@ -19,6 +19,8 @@ type Broadcast struct {
|
|||||||
port int
|
port int
|
||||||
inbox chan []byte
|
inbox chan []byte
|
||||||
outbox chan recv
|
outbox chan recv
|
||||||
|
br *broadcastReader
|
||||||
|
bw *broadcastWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBroadcast(port int) *Broadcast {
|
func NewBroadcast(port int) *Broadcast {
|
||||||
@@ -31,9 +33,7 @@ func NewBroadcast(port int) *Broadcast {
|
|||||||
FailureBackoff: 60 * time.Second,
|
FailureBackoff: 60 * time.Second,
|
||||||
// Only log restarts in debug mode.
|
// Only log restarts in debug mode.
|
||||||
Log: func(line string) {
|
Log: func(line string) {
|
||||||
if debug {
|
|
||||||
l.Debugln(line)
|
l.Debugln(line)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
port: port,
|
port: port,
|
||||||
@@ -41,14 +41,16 @@ func NewBroadcast(port int) *Broadcast {
|
|||||||
outbox: make(chan recv, 16),
|
outbox: make(chan recv, 16),
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Add(&broadcastReader{
|
b.br = &broadcastReader{
|
||||||
port: port,
|
port: port,
|
||||||
outbox: b.outbox,
|
outbox: b.outbox,
|
||||||
})
|
}
|
||||||
b.Add(&broadcastWriter{
|
b.Add(b.br)
|
||||||
|
b.bw = &broadcastWriter{
|
||||||
port: port,
|
port: port,
|
||||||
inbox: b.inbox,
|
inbox: b.inbox,
|
||||||
})
|
}
|
||||||
|
b.Add(b.bw)
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
@@ -62,38 +64,38 @@ func (b *Broadcast) Recv() ([]byte, net.Addr) {
|
|||||||
return recv.data, recv.src
|
return recv.data, recv.src
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Broadcast) Error() error {
|
||||||
|
if err := b.br.Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return b.bw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
type broadcastWriter struct {
|
type broadcastWriter struct {
|
||||||
port int
|
port int
|
||||||
inbox chan []byte
|
inbox chan []byte
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
failed bool // Have we already logged a failure reason?
|
errorHolder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *broadcastWriter) Serve() {
|
func (w *broadcastWriter) Serve() {
|
||||||
if debug {
|
|
||||||
l.Debugln(w, "starting")
|
l.Debugln(w, "starting")
|
||||||
defer l.Debugln(w, "stopping")
|
defer l.Debugln(w, "stopping")
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
w.conn, err = net.ListenUDP("udp4", nil)
|
w.conn, err = net.ListenUDP("udp4", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !w.failed {
|
l.Debugln(err)
|
||||||
l.Warnln("Local discovery over IPv4 unavailable:", err)
|
w.setError(err)
|
||||||
w.failed = true
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer w.conn.Close()
|
defer w.conn.Close()
|
||||||
|
|
||||||
w.failed = false
|
|
||||||
|
|
||||||
for bs := range w.inbox {
|
for bs := range w.inbox {
|
||||||
addrs, err := net.InterfaceAddrs()
|
addrs, err := net.InterfaceAddrs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if debug {
|
l.Debugln(err)
|
||||||
l.Debugln("Local discovery (broadcast writer):", err)
|
w.setError(err)
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,35 +112,43 @@ func (w *broadcastWriter) Serve() {
|
|||||||
dsts = append(dsts, net.IP{0xff, 0xff, 0xff, 0xff})
|
dsts = append(dsts, net.IP{0xff, 0xff, 0xff, 0xff})
|
||||||
}
|
}
|
||||||
|
|
||||||
if debug {
|
|
||||||
l.Debugln("addresses:", dsts)
|
l.Debugln("addresses:", dsts)
|
||||||
}
|
|
||||||
|
|
||||||
|
success := 0
|
||||||
for _, ip := range dsts {
|
for _, ip := range dsts {
|
||||||
dst := &net.UDPAddr{IP: ip, Port: w.port}
|
dst := &net.UDPAddr{IP: ip, Port: w.port}
|
||||||
|
|
||||||
w.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
w.conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||||
_, err := w.conn.WriteTo(bs, dst)
|
_, err := w.conn.WriteTo(bs, dst)
|
||||||
|
w.conn.SetWriteDeadline(time.Time{})
|
||||||
|
|
||||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||||
// Write timeouts should not happen. We treat it as a fatal
|
// Write timeouts should not happen. We treat it as a fatal
|
||||||
// error on the socket.
|
// error on the socket.
|
||||||
l.Infoln("Local discovery (broadcast writer):", err)
|
|
||||||
w.failed = true
|
|
||||||
return
|
|
||||||
} else if err, ok := err.(net.Error); ok && err.Temporary() {
|
|
||||||
// A transient error. Lets hope for better luck in the future.
|
|
||||||
if debug {
|
|
||||||
l.Debugln(err)
|
l.Debugln(err)
|
||||||
}
|
w.setError(err)
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
// Some other error that we don't expect. Bail and retry.
|
|
||||||
l.Infoln("Local discovery (broadcast writer):", err)
|
|
||||||
w.failed = true
|
|
||||||
return
|
return
|
||||||
} else if debug {
|
|
||||||
l.Debugf("sent %d bytes to %s", len(bs), dst)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err, ok := err.(net.Error); ok && err.Temporary() {
|
||||||
|
// A transient error. Lets hope for better luck in the future.
|
||||||
|
l.Debugln(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Some other error that we don't expect. Bail and retry.
|
||||||
|
l.Debugln(err)
|
||||||
|
w.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debugf("sent %d bytes to %s", len(bs), dst)
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
|
||||||
|
if success > 0 {
|
||||||
|
w.setError(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,22 +165,18 @@ type broadcastReader struct {
|
|||||||
port int
|
port int
|
||||||
outbox chan recv
|
outbox chan recv
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
failed bool
|
errorHolder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *broadcastReader) Serve() {
|
func (r *broadcastReader) Serve() {
|
||||||
if debug {
|
|
||||||
l.Debugln(r, "starting")
|
l.Debugln(r, "starting")
|
||||||
defer l.Debugln(r, "stopping")
|
defer l.Debugln(r, "stopping")
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
r.conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: r.port})
|
r.conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: r.port})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !r.failed {
|
l.Debugln(err)
|
||||||
l.Warnln("Local discovery over IPv4 unavailable:", err)
|
r.setError(err)
|
||||||
r.failed = true
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer r.conn.Close()
|
defer r.conn.Close()
|
||||||
@@ -179,29 +185,23 @@ func (r *broadcastReader) Serve() {
|
|||||||
for {
|
for {
|
||||||
n, addr, err := r.conn.ReadFrom(bs)
|
n, addr, err := r.conn.ReadFrom(bs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !r.failed {
|
l.Debugln(err)
|
||||||
l.Infoln("Local discovery (broadcast reader):", err)
|
r.setError(err)
|
||||||
r.failed = true
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r.failed = false
|
r.setError(nil)
|
||||||
|
|
||||||
if debug {
|
|
||||||
l.Debugf("recv %d bytes from %s", n, addr)
|
l.Debugf("recv %d bytes from %s", n, addr)
|
||||||
}
|
|
||||||
|
|
||||||
c := make([]byte, n)
|
c := make([]byte, n)
|
||||||
copy(c, bs)
|
copy(c, bs)
|
||||||
select {
|
select {
|
||||||
case r.outbox <- recv{c, addr}:
|
case r.outbox <- recv{c, addr}:
|
||||||
default:
|
default:
|
||||||
if debug {
|
|
||||||
l.Debugln("dropping message")
|
l.Debugln("dropping message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -10,10 +10,13 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/calmh/logger"
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
debug = strings.Contains(os.Getenv("STTRACE"), "beacon") || os.Getenv("STTRACE") == "all"
|
l = logger.DefaultLogger.NewFacility("beacon", "Multicast and broadcast discovery")
|
||||||
l = logger.DefaultLogger
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
l.SetDebug("beacon", strings.Contains(os.Getenv("STTRACE"), "beacon") || os.Getenv("STTRACE") == "all")
|
||||||
|
}
|
||||||
|
|||||||
+175
-56
@@ -10,100 +10,219 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/thejerf/suture"
|
||||||
"golang.org/x/net/ipv6"
|
"golang.org/x/net/ipv6"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Multicast struct {
|
type Multicast struct {
|
||||||
conn *ipv6.PacketConn
|
*suture.Supervisor
|
||||||
addr *net.UDPAddr
|
addr *net.UDPAddr
|
||||||
inbox chan []byte
|
inbox chan []byte
|
||||||
outbox chan recv
|
outbox chan recv
|
||||||
intfs []net.Interface
|
mr *multicastReader
|
||||||
|
mw *multicastWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMulticast(addr string) (*Multicast, error) {
|
func NewMulticast(addr string) *Multicast {
|
||||||
gaddr, err := net.ResolveUDPAddr("udp6", addr)
|
m := &Multicast{
|
||||||
if err != nil {
|
Supervisor: suture.New("multicastBeacon", suture.Spec{
|
||||||
return nil, err
|
// Don't retry too frenetically: an error to open a socket or
|
||||||
|
// whatever is usually something that is either permanent or takes
|
||||||
|
// a while to get solved...
|
||||||
|
FailureThreshold: 2,
|
||||||
|
FailureBackoff: 60 * time.Second,
|
||||||
|
// Only log restarts in debug mode.
|
||||||
|
Log: func(line string) {
|
||||||
|
l.Debugln(line)
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
inbox: make(chan []byte),
|
||||||
|
outbox: make(chan recv, 16),
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := net.ListenPacket("udp6", fmt.Sprintf("[::]:%d", gaddr.Port))
|
m.mr = &multicastReader{
|
||||||
|
addr: addr,
|
||||||
|
outbox: m.outbox,
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
}
|
||||||
|
m.Add(m.mr)
|
||||||
|
|
||||||
|
m.mw = &multicastWriter{
|
||||||
|
addr: addr,
|
||||||
|
inbox: m.inbox,
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
}
|
||||||
|
m.Add(m.mw)
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multicast) Send(data []byte) {
|
||||||
|
m.inbox <- data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multicast) Recv() ([]byte, net.Addr) {
|
||||||
|
recv := <-m.outbox
|
||||||
|
return recv.data, recv.src
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Multicast) Error() error {
|
||||||
|
if err := m.mr.Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.mw.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
type multicastWriter struct {
|
||||||
|
addr string
|
||||||
|
inbox <-chan []byte
|
||||||
|
errorHolder
|
||||||
|
stop chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multicastWriter) Serve() {
|
||||||
|
l.Debugln(w, "starting")
|
||||||
|
defer l.Debugln(w, "stopping")
|
||||||
|
|
||||||
|
gaddr, err := net.ResolveUDPAddr("udp6", w.addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
l.Debugln(err)
|
||||||
|
w.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.ListenPacket("udp6", ":0")
|
||||||
|
if err != nil {
|
||||||
|
l.Debugln(err)
|
||||||
|
w.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pconn := ipv6.NewPacketConn(conn)
|
||||||
|
|
||||||
|
wcm := &ipv6.ControlMessage{
|
||||||
|
HopLimit: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
for bs := range w.inbox {
|
||||||
|
intfs, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
l.Debugln(err)
|
||||||
|
w.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
success := 0
|
||||||
|
for _, intf := range intfs {
|
||||||
|
wcm.IfIndex = intf.Index
|
||||||
|
pconn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||||
|
_, err = pconn.WriteTo(bs, wcm, gaddr)
|
||||||
|
pconn.SetWriteDeadline(time.Time{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
l.Debugln(err, "on write to", gaddr, intf.Name)
|
||||||
|
w.setError(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debugf("sent %d bytes to %v on %s", len(bs), gaddr, intf.Name)
|
||||||
|
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
|
||||||
|
if success > 0 {
|
||||||
|
w.setError(nil)
|
||||||
|
} else {
|
||||||
|
l.Debugln(err)
|
||||||
|
w.setError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multicastWriter) Stop() {
|
||||||
|
close(w.stop)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *multicastWriter) String() string {
|
||||||
|
return fmt.Sprintf("multicastWriter@%p", w)
|
||||||
|
}
|
||||||
|
|
||||||
|
type multicastReader struct {
|
||||||
|
addr string
|
||||||
|
outbox chan<- recv
|
||||||
|
errorHolder
|
||||||
|
stop chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *multicastReader) Serve() {
|
||||||
|
l.Debugln(r, "starting")
|
||||||
|
defer l.Debugln(r, "stopping")
|
||||||
|
|
||||||
|
gaddr, err := net.ResolveUDPAddr("udp6", r.addr)
|
||||||
|
if err != nil {
|
||||||
|
l.Debugln(err)
|
||||||
|
r.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.ListenPacket("udp6", r.addr)
|
||||||
|
if err != nil {
|
||||||
|
l.Debugln(err)
|
||||||
|
r.setError(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
intfs, err := net.Interfaces()
|
intfs, err := net.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
l.Debugln(err)
|
||||||
|
r.setError(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p := ipv6.NewPacketConn(conn)
|
pconn := ipv6.NewPacketConn(conn)
|
||||||
joined := 0
|
joined := 0
|
||||||
for _, intf := range intfs {
|
for _, intf := range intfs {
|
||||||
err := p.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
|
err := pconn.JoinGroup(&intf, &net.UDPAddr{IP: gaddr.IP})
|
||||||
if debug {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugln("IPv6 join", intf.Name, "failed:", err)
|
l.Debugln("IPv6 join", intf.Name, "failed:", err)
|
||||||
} else {
|
} else {
|
||||||
l.Debugln("IPv6 join", intf.Name, "success")
|
l.Debugln("IPv6 join", intf.Name, "success")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
joined++
|
joined++
|
||||||
}
|
}
|
||||||
|
|
||||||
if joined == 0 {
|
if joined == 0 {
|
||||||
return nil, errors.New("no multicast interfaces available")
|
l.Debugln("no multicast interfaces available")
|
||||||
|
r.setError(errors.New("no multicast interfaces available"))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b := &Multicast{
|
bs := make([]byte, 65536)
|
||||||
conn: p,
|
for {
|
||||||
addr: gaddr,
|
n, _, addr, err := pconn.ReadFrom(bs)
|
||||||
inbox: make(chan []byte),
|
if err != nil {
|
||||||
outbox: make(chan recv, 16),
|
l.Debugln(err)
|
||||||
intfs: intfs,
|
r.setError(err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
l.Debugf("recv %d bytes from %s", n, addr)
|
||||||
|
|
||||||
go genericReader(ipv6ReaderAdapter{b.conn}, b.outbox)
|
c := make([]byte, n)
|
||||||
go b.writer()
|
copy(c, bs)
|
||||||
|
select {
|
||||||
return b, nil
|
case r.outbox <- recv{c, addr}:
|
||||||
}
|
default:
|
||||||
|
l.Debugln("dropping message")
|
||||||
func (b *Multicast) Send(data []byte) {
|
|
||||||
b.inbox <- data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Multicast) Recv() ([]byte, net.Addr) {
|
|
||||||
recv := <-b.outbox
|
|
||||||
return recv.data, recv.src
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Multicast) writer() {
|
|
||||||
wcm := &ipv6.ControlMessage{
|
|
||||||
HopLimit: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
for bs := range b.inbox {
|
|
||||||
for _, intf := range b.intfs {
|
|
||||||
wcm.IfIndex = intf.Index
|
|
||||||
_, err := b.conn.WriteTo(bs, wcm, b.addr)
|
|
||||||
if err != nil && debug {
|
|
||||||
l.Debugln(err, "on write to", b.addr)
|
|
||||||
} else if debug {
|
|
||||||
l.Debugf("sent %d bytes to %v on %s", len(bs), b.addr, intf.Name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This makes ReadFrom on an *ipv6.PacketConn behave like ReadFrom on a
|
func (r *multicastReader) Stop() {
|
||||||
// net.PacketConn.
|
close(r.stop)
|
||||||
type ipv6ReaderAdapter struct {
|
|
||||||
c *ipv6.PacketConn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i ipv6ReaderAdapter) ReadFrom(bs []byte) (int, net.Addr, error) {
|
func (r *multicastReader) String() string {
|
||||||
n, _, src, err := i.c.ReadFrom(bs)
|
return fmt.Sprintf("multicastReader@%p", r)
|
||||||
return n, src, err
|
|
||||||
}
|
}
|
||||||
|
|||||||
+175
-18
@@ -9,8 +9,10 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -19,17 +21,43 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
OldestHandledVersion = 5
|
OldestHandledVersion = 5
|
||||||
CurrentVersion = 11
|
CurrentVersion = 12
|
||||||
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultDiscoveryServers should be substituted when the configuration
|
||||||
|
// contains <globalAnnounceServer>default</globalAnnounceServer>. This is
|
||||||
|
// done by the "consumer" of the configuration, as we don't want these
|
||||||
|
// saved to the config.
|
||||||
|
DefaultDiscoveryServers = []string{
|
||||||
|
"https://discovery-v4-1.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden
|
||||||
|
"https://discovery-v4-2.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 45.55.230.38, USA
|
||||||
|
"https://discovery-v4-3.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 128.199.95.124, Singapore
|
||||||
|
"https://discovery-v6-1.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden
|
||||||
|
"https://discovery-v6-2.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 2604:a880:800:10::182:a001, USA
|
||||||
|
"https://discovery-v6-3.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 2400:6180:0:d0::d9:d001, Singapore
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4",
|
||||||
|
"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=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Version int `xml:"version,attr" json:"version"`
|
Version int `xml:"version,attr" json:"version"`
|
||||||
Folders []FolderConfiguration `xml:"folder" json:"folders"`
|
Folders []FolderConfiguration `xml:"folder" json:"folders"`
|
||||||
@@ -74,13 +102,16 @@ type FolderConfiguration struct {
|
|||||||
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
|
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
|
||||||
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
|
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
|
||||||
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
|
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
|
||||||
MinDiskFreePct int `xml:"minDiskFreePct" json:"minDiskFreePct"`
|
MinDiskFreePct float64 `xml:"minDiskFreePct" json:"minDiskFreePct"`
|
||||||
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
|
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
|
||||||
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
|
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
|
||||||
Pullers int `xml:"pullers" json:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
|
Pullers int `xml:"pullers" json:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
|
||||||
Hashers int `xml:"hashers" json:"hashers"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
|
Hashers int `xml:"hashers" json:"hashers"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
|
||||||
Order PullOrder `xml:"order" json:"order"`
|
Order PullOrder `xml:"order" json:"order"`
|
||||||
IgnoreDelete bool `xml:"ignoreDelete" json:"ignoreDelete"`
|
IgnoreDelete bool `xml:"ignoreDelete" json:"ignoreDelete"`
|
||||||
|
ScanProgressIntervalS int `xml:"scanProgressIntervalS" json:"scanProgressIntervalS"` // Set to a negative value to disable. Value of 0 will get replaced with value of 2 (default value)
|
||||||
|
PullerSleepS int `xml:"pullerSleepS" json:"pullerSleepS"`
|
||||||
|
PullerPauseS int `xml:"pullerPauseS" json:"pullerPauseS"`
|
||||||
|
|
||||||
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
|
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
|
||||||
}
|
}
|
||||||
@@ -212,15 +243,19 @@ type FolderDeviceConfiguration struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OptionsConfiguration struct {
|
type OptionsConfiguration struct {
|
||||||
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"0.0.0.0:22000"`
|
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
|
||||||
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22026, udp6://announce-v6.syncthing.net:22026"`
|
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"`
|
||||||
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
|
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
|
||||||
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
|
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
|
||||||
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21025"`
|
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
|
||||||
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff32::5222]:21026"`
|
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027"`
|
||||||
|
RelayServers []string `xml:"relayServer" json:"relayServers" default:"dynamic+https://relays.syncthing.net"`
|
||||||
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
|
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
|
||||||
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
|
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
|
||||||
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
|
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
|
||||||
|
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
|
||||||
|
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
|
||||||
|
RelayWithoutGlobalAnn bool `xml:"relayWithoutGlobalAnn" json:"relayWithoutGlobalAnn" default:"false"`
|
||||||
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
|
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
|
||||||
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
|
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
|
||||||
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"`
|
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"`
|
||||||
@@ -228,6 +263,9 @@ type OptionsConfiguration struct {
|
|||||||
UPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"upnpTimeoutSeconds" default:"10"`
|
UPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"upnpTimeoutSeconds" default:"10"`
|
||||||
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
|
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
|
||||||
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
|
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
|
||||||
|
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
|
||||||
|
URPostInsecurely bool `xml:"urPostInsecurely" json:"urPostInsecurely" default:"false"` // For testing
|
||||||
|
URInitialDelayS int `xml:"urInitialDelayS" json:"urInitialDelayS" default:"1800"`
|
||||||
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
|
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
|
||||||
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12"` // 0 for off
|
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12"` // 0 for off
|
||||||
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
|
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
|
||||||
@@ -236,9 +274,9 @@ type OptionsConfiguration struct {
|
|||||||
SymlinksEnabled bool `xml:"symlinksEnabled" json:"symlinksEnabled" default:"true"`
|
SymlinksEnabled bool `xml:"symlinksEnabled" json:"symlinksEnabled" default:"true"`
|
||||||
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
|
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
|
||||||
DatabaseBlockCacheMiB int `xml:"databaseBlockCacheMiB" json:"databaseBlockCacheMiB" default:"0"`
|
DatabaseBlockCacheMiB int `xml:"databaseBlockCacheMiB" json:"databaseBlockCacheMiB" default:"0"`
|
||||||
PingTimeoutS int `xml:"pingTimeoutS" json:"pingTimeoutS" default:"30"`
|
MinHomeDiskFreePct float64 `xml:"minHomeDiskFreePct" json:"minHomeDiskFreePct" default:"1"`
|
||||||
PingIdleTimeS int `xml:"pingIdleTimeS" json:"pingIdleTimeS" default:"60"`
|
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=30"`
|
||||||
MinHomeDiskFreePct int `xml:"minHomeDiskFreePct" json:"minHomeDiskFreePct" default:"1"`
|
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
|
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
|
||||||
@@ -252,11 +290,71 @@ func (orig OptionsConfiguration) Copy() OptionsConfiguration {
|
|||||||
|
|
||||||
type GUIConfiguration struct {
|
type GUIConfiguration struct {
|
||||||
Enabled bool `xml:"enabled,attr" json:"enabled" default:"true"`
|
Enabled bool `xml:"enabled,attr" json:"enabled" default:"true"`
|
||||||
Address string `xml:"address" json:"address" default:"127.0.0.1:8384"`
|
RawAddress string `xml:"address" json:"address" default:"127.0.0.1:8384"`
|
||||||
User string `xml:"user,omitempty" json:"user"`
|
User string `xml:"user,omitempty" json:"user"`
|
||||||
Password string `xml:"password,omitempty" json:"password"`
|
Password string `xml:"password,omitempty" json:"password"`
|
||||||
UseTLS bool `xml:"tls,attr" json:"useTLS"`
|
RawUseTLS bool `xml:"tls,attr" json:"useTLS"`
|
||||||
APIKey string `xml:"apikey,omitempty" json:"apiKey"`
|
RawAPIKey string `xml:"apikey,omitempty" json:"apiKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c GUIConfiguration) Address() string {
|
||||||
|
if override := os.Getenv("STGUIADDRESS"); override != "" {
|
||||||
|
// This value may be of the form "scheme://address:port" or just
|
||||||
|
// "address:port". We need to chop off the scheme. We try to parse it as
|
||||||
|
// an URL if it contains a slash. If that fails, return it as is and let
|
||||||
|
// some other error handling handle it.
|
||||||
|
|
||||||
|
if strings.Contains(override, "/") {
|
||||||
|
url, err := url.Parse(override)
|
||||||
|
if err != nil {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
return url.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.RawAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c GUIConfiguration) UseTLS() bool {
|
||||||
|
if override := os.Getenv("STGUIADDRESS"); override != "" {
|
||||||
|
return strings.HasPrefix(override, "https:")
|
||||||
|
}
|
||||||
|
return c.RawUseTLS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c GUIConfiguration) URL() string {
|
||||||
|
u := url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: c.Address(),
|
||||||
|
Path: "/",
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.UseTLS() {
|
||||||
|
u.Scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(u.Host, ":") {
|
||||||
|
// Empty host, i.e. ":port", use IPv4 localhost
|
||||||
|
u.Host = "127.0.0.1" + u.Host
|
||||||
|
} else if strings.HasPrefix(u.Host, "0.0.0.0:") {
|
||||||
|
// IPv4 all zeroes host, convert to IPv4 localhost
|
||||||
|
u.Host = "127.0.0.1" + u.Host[7:]
|
||||||
|
} else if strings.HasPrefix(u.Host, "[::]:") {
|
||||||
|
// IPv6 all zeroes host, convert to IPv6 localhost
|
||||||
|
u.Host = "[::1]" + u.Host[4:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c GUIConfiguration) APIKey() string {
|
||||||
|
if override := os.Getenv("STGUIAPIKEY"); override != "" {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
return c.RawAPIKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(myID protocol.DeviceID) Configuration {
|
func New(myID protocol.DeviceID) Configuration {
|
||||||
@@ -326,6 +424,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
|||||||
// This way in the tests, we get away without OS specific separators
|
// This way in the tests, we get away without OS specific separators
|
||||||
// in the test configs.
|
// in the test configs.
|
||||||
folder.RawPath = filepath.Dir(folder.RawPath + string(filepath.Separator))
|
folder.RawPath = filepath.Dir(folder.RawPath + string(filepath.Separator))
|
||||||
|
if folder.RawPath[len(folder.RawPath)-1] != filepath.Separator {
|
||||||
|
folder.RawPath = folder.RawPath + string(filepath.Separator)
|
||||||
|
}
|
||||||
|
|
||||||
if folder.ID == "" {
|
if folder.ID == "" {
|
||||||
folder.ID = "default"
|
folder.ID = "default"
|
||||||
@@ -346,6 +447,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
|
||||||
|
cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers)
|
||||||
|
|
||||||
if cfg.Version < OldestHandledVersion {
|
if cfg.Version < OldestHandledVersion {
|
||||||
l.Warnf("Configuration version %d is deprecated. Attempting best effort conversion, but please verify manually.", cfg.Version)
|
l.Warnf("Configuration version %d is deprecated. Attempting best effort conversion, but please verify manually.", cfg.Version)
|
||||||
}
|
}
|
||||||
@@ -369,6 +473,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
|||||||
if cfg.Version == 10 {
|
if cfg.Version == 10 {
|
||||||
convertV10V11(cfg)
|
convertV10V11(cfg)
|
||||||
}
|
}
|
||||||
|
if cfg.Version == 11 {
|
||||||
|
convertV11V12(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
// Hash old cleartext passwords
|
// Hash old cleartext passwords
|
||||||
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
|
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
|
||||||
@@ -420,11 +527,8 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
|||||||
cfg.Options.ReconnectIntervalS = 5
|
cfg.Options.ReconnectIntervalS = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
|
if cfg.GUI.RawAPIKey == "" {
|
||||||
cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers)
|
cfg.GUI.RawAPIKey = randomString(32)
|
||||||
|
|
||||||
if cfg.GUI.APIKey == "" {
|
|
||||||
cfg.GUI.APIKey = randomString(32)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,6 +571,52 @@ func convertV10V11(cfg *Configuration) {
|
|||||||
cfg.Version = 11
|
cfg.Version = 11
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertV11V12(cfg *Configuration) {
|
||||||
|
// Change listen address schema
|
||||||
|
for i, addr := range cfg.Options.ListenAddress {
|
||||||
|
if len(addr) > 0 && !strings.HasPrefix(addr, "tcp://") {
|
||||||
|
cfg.Options.ListenAddress[i] = fmt.Sprintf("tcp://%s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, device := range cfg.Devices {
|
||||||
|
for j, addr := range device.Addresses {
|
||||||
|
if addr != "dynamic" && addr != "" {
|
||||||
|
cfg.Devices[i].Addresses[j] = fmt.Sprintf("tcp://%s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use new discovery server
|
||||||
|
var newDiscoServers []string
|
||||||
|
var useDefault bool
|
||||||
|
for _, addr := range cfg.Options.GlobalAnnServers {
|
||||||
|
if addr == "udp4://announce.syncthing.net:22026" {
|
||||||
|
useDefault = true
|
||||||
|
} else if addr == "udp6://announce-v6.syncthing.net:22026" {
|
||||||
|
useDefault = true
|
||||||
|
} else {
|
||||||
|
newDiscoServers = append(newDiscoServers, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if useDefault {
|
||||||
|
newDiscoServers = append(newDiscoServers, "default")
|
||||||
|
}
|
||||||
|
cfg.Options.GlobalAnnServers = newDiscoServers
|
||||||
|
|
||||||
|
// Use new multicast group
|
||||||
|
if cfg.Options.LocalAnnMCAddr == "[ff32::5222]:21026" {
|
||||||
|
cfg.Options.LocalAnnMCAddr = "[ff12::8384]:21027"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use new local discovery port
|
||||||
|
if cfg.Options.LocalAnnPort == 21025 {
|
||||||
|
cfg.Options.LocalAnnPort = 21027
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Version = 12
|
||||||
|
}
|
||||||
|
|
||||||
func convertV9V10(cfg *Configuration) {
|
func convertV9V10(cfg *Configuration) {
|
||||||
// Enable auto normalization on existing folders.
|
// Enable auto normalization on existing folders.
|
||||||
for i := range cfg.Folders {
|
for i := range cfg.Folders {
|
||||||
@@ -534,6 +684,13 @@ func setDefaults(data interface{}) error {
|
|||||||
}
|
}
|
||||||
f.SetInt(i)
|
f.SetInt(i)
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
i, err := strconv.ParseFloat(v, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.SetFloat(i)
|
||||||
|
|
||||||
case bool:
|
case bool:
|
||||||
f.SetBool(v == "true")
|
f.SetBool(v == "true")
|
||||||
|
|
||||||
|
|||||||
+58
-24
@@ -17,7 +17,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
var device1, device2, device3, device4 protocol.DeviceID
|
var device1, device2, device3, device4 protocol.DeviceID
|
||||||
@@ -31,15 +31,19 @@ func init() {
|
|||||||
|
|
||||||
func TestDefaultValues(t *testing.T) {
|
func TestDefaultValues(t *testing.T) {
|
||||||
expected := OptionsConfiguration{
|
expected := OptionsConfiguration{
|
||||||
ListenAddress: []string{"0.0.0.0:22000"},
|
ListenAddress: []string{"tcp://0.0.0.0:22000"},
|
||||||
GlobalAnnServers: []string{"udp4://announce.syncthing.net:22026", "udp6://announce-v6.syncthing.net:22026"},
|
GlobalAnnServers: []string{"default"},
|
||||||
GlobalAnnEnabled: true,
|
GlobalAnnEnabled: true,
|
||||||
LocalAnnEnabled: true,
|
LocalAnnEnabled: true,
|
||||||
LocalAnnPort: 21025,
|
LocalAnnPort: 21027,
|
||||||
LocalAnnMCAddr: "[ff32::5222]:21026",
|
LocalAnnMCAddr: "[ff12::8384]:21027",
|
||||||
|
RelayServers: []string{"dynamic+https://relays.syncthing.net"},
|
||||||
MaxSendKbps: 0,
|
MaxSendKbps: 0,
|
||||||
MaxRecvKbps: 0,
|
MaxRecvKbps: 0,
|
||||||
ReconnectIntervalS: 60,
|
ReconnectIntervalS: 60,
|
||||||
|
RelaysEnabled: true,
|
||||||
|
RelayReconnectIntervalM: 10,
|
||||||
|
RelayWithoutGlobalAnn: false,
|
||||||
StartBrowser: true,
|
StartBrowser: true,
|
||||||
UPnPEnabled: true,
|
UPnPEnabled: true,
|
||||||
UPnPLeaseM: 60,
|
UPnPLeaseM: 60,
|
||||||
@@ -53,9 +57,11 @@ func TestDefaultValues(t *testing.T) {
|
|||||||
SymlinksEnabled: true,
|
SymlinksEnabled: true,
|
||||||
LimitBandwidthInLan: false,
|
LimitBandwidthInLan: false,
|
||||||
DatabaseBlockCacheMiB: 0,
|
DatabaseBlockCacheMiB: 0,
|
||||||
PingTimeoutS: 30,
|
|
||||||
PingIdleTimeS: 60,
|
|
||||||
MinHomeDiskFreePct: 1,
|
MinHomeDiskFreePct: 1,
|
||||||
|
URURL: "https://data.syncthing.net/newdata",
|
||||||
|
URInitialDelayS: 1800,
|
||||||
|
URPostInsecurely: false,
|
||||||
|
ReleasesURL: "https://api.github.com/repos/syncthing/syncthing/releases?per_page=30",
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := New(device1)
|
cfg := New(device1)
|
||||||
@@ -85,7 +91,7 @@ func TestDeviceConfig(t *testing.T) {
|
|||||||
expectedFolders := []FolderConfiguration{
|
expectedFolders := []FolderConfiguration{
|
||||||
{
|
{
|
||||||
ID: "test",
|
ID: "test",
|
||||||
RawPath: "testdata",
|
RawPath: "testdata" + string(filepath.Separator),
|
||||||
Devices: []FolderDeviceConfiguration{{DeviceID: device1}, {DeviceID: device4}},
|
Devices: []FolderDeviceConfiguration{{DeviceID: device1}, {DeviceID: device4}},
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
RescanIntervalS: 600,
|
RescanIntervalS: 600,
|
||||||
@@ -100,13 +106,13 @@ func TestDeviceConfig(t *testing.T) {
|
|||||||
{
|
{
|
||||||
DeviceID: device1,
|
DeviceID: device1,
|
||||||
Name: "node one",
|
Name: "node one",
|
||||||
Addresses: []string{"a"},
|
Addresses: []string{"tcp://a"},
|
||||||
Compression: protocol.CompressMetadata,
|
Compression: protocol.CompressMetadata,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
DeviceID: device4,
|
DeviceID: device4,
|
||||||
Name: "node two",
|
Name: "node two",
|
||||||
Addresses: []string{"b"},
|
Addresses: []string{"tcp://b"},
|
||||||
Compression: protocol.CompressMetadata,
|
Compression: protocol.CompressMetadata,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -142,15 +148,19 @@ func TestNoListenAddress(t *testing.T) {
|
|||||||
|
|
||||||
func TestOverriddenValues(t *testing.T) {
|
func TestOverriddenValues(t *testing.T) {
|
||||||
expected := OptionsConfiguration{
|
expected := OptionsConfiguration{
|
||||||
ListenAddress: []string{":23000"},
|
ListenAddress: []string{"tcp://:23000"},
|
||||||
GlobalAnnServers: []string{"udp4://syncthing.nym.se:22026"},
|
GlobalAnnServers: []string{"udp4://syncthing.nym.se:22026"},
|
||||||
GlobalAnnEnabled: false,
|
GlobalAnnEnabled: false,
|
||||||
LocalAnnEnabled: false,
|
LocalAnnEnabled: false,
|
||||||
LocalAnnPort: 42123,
|
LocalAnnPort: 42123,
|
||||||
LocalAnnMCAddr: "quux:3232",
|
LocalAnnMCAddr: "quux:3232",
|
||||||
|
RelayServers: []string{"relay://123.123.123.123:1234", "relay://125.125.125.125:1255"},
|
||||||
MaxSendKbps: 1234,
|
MaxSendKbps: 1234,
|
||||||
MaxRecvKbps: 2341,
|
MaxRecvKbps: 2341,
|
||||||
ReconnectIntervalS: 6000,
|
ReconnectIntervalS: 6000,
|
||||||
|
RelaysEnabled: false,
|
||||||
|
RelayReconnectIntervalM: 20,
|
||||||
|
RelayWithoutGlobalAnn: true,
|
||||||
StartBrowser: false,
|
StartBrowser: false,
|
||||||
UPnPEnabled: false,
|
UPnPEnabled: false,
|
||||||
UPnPLeaseM: 90,
|
UPnPLeaseM: 90,
|
||||||
@@ -164,9 +174,11 @@ func TestOverriddenValues(t *testing.T) {
|
|||||||
SymlinksEnabled: false,
|
SymlinksEnabled: false,
|
||||||
LimitBandwidthInLan: true,
|
LimitBandwidthInLan: true,
|
||||||
DatabaseBlockCacheMiB: 42,
|
DatabaseBlockCacheMiB: 42,
|
||||||
PingTimeoutS: 60,
|
MinHomeDiskFreePct: 5.2,
|
||||||
PingIdleTimeS: 120,
|
URURL: "https://localhost/newdata",
|
||||||
MinHomeDiskFreePct: 5,
|
URInitialDelayS: 800,
|
||||||
|
URPostInsecurely: true,
|
||||||
|
ReleasesURL: "https://localhost/releases",
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := Load("testdata/overridenvalues.xml", device1)
|
cfg, err := Load("testdata/overridenvalues.xml", device1)
|
||||||
@@ -255,15 +267,15 @@ func TestDeviceAddressesStatic(t *testing.T) {
|
|||||||
expected := map[protocol.DeviceID]DeviceConfiguration{
|
expected := map[protocol.DeviceID]DeviceConfiguration{
|
||||||
device1: {
|
device1: {
|
||||||
DeviceID: device1,
|
DeviceID: device1,
|
||||||
Addresses: []string{"192.0.2.1", "192.0.2.2"},
|
Addresses: []string{"tcp://192.0.2.1", "tcp://192.0.2.2"},
|
||||||
},
|
},
|
||||||
device2: {
|
device2: {
|
||||||
DeviceID: device2,
|
DeviceID: device2,
|
||||||
Addresses: []string{"192.0.2.3:6070", "[2001:db8::42]:4242"},
|
Addresses: []string{"tcp://192.0.2.3:6070", "tcp://[2001:db8::42]:4242"},
|
||||||
},
|
},
|
||||||
device3: {
|
device3: {
|
||||||
DeviceID: device3,
|
DeviceID: device3,
|
||||||
Addresses: []string{"[2001:db8::44]:4444", "192.0.2.4:6090"},
|
Addresses: []string{"tcp://[2001:db8::44]:4444", "tcp://192.0.2.4:6090"},
|
||||||
},
|
},
|
||||||
device4: {
|
device4: {
|
||||||
DeviceID: device4,
|
DeviceID: device4,
|
||||||
@@ -314,7 +326,7 @@ func TestIssue1262(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual := cfg.Folders()["test"].RawPath
|
actual := cfg.Folders()["test"].RawPath
|
||||||
expected := "e:"
|
expected := "e:/"
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
expected = `e:\`
|
expected = `e:\`
|
||||||
}
|
}
|
||||||
@@ -330,12 +342,12 @@ func TestIssue1750(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Options().ListenAddress[0] != ":23000" {
|
if cfg.Options().ListenAddress[0] != "tcp://:23000" {
|
||||||
t.Errorf("%q != %q", cfg.Options().ListenAddress[0], ":23000")
|
t.Errorf("%q != %q", cfg.Options().ListenAddress[0], "tcp://:23000")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Options().ListenAddress[1] != ":23001" {
|
if cfg.Options().ListenAddress[1] != "tcp://:23001" {
|
||||||
t.Errorf("%q != %q", cfg.Options().ListenAddress[1], ":23001")
|
t.Errorf("%q != %q", cfg.Options().ListenAddress[1], "tcp://:23001")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026" {
|
if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026" {
|
||||||
@@ -516,7 +528,7 @@ func TestRequiresRestart(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newCfg = cfg
|
newCfg = cfg
|
||||||
newCfg.GUI.UseTLS = !cfg.GUI.UseTLS
|
newCfg.GUI.RawUseTLS = !cfg.GUI.RawUseTLS
|
||||||
if !ChangeRequiresRestart(cfg, newCfg) {
|
if !ChangeRequiresRestart(cfg, newCfg) {
|
||||||
t.Error("Changing GUI options requires restart")
|
t.Error("Changing GUI options requires restart")
|
||||||
}
|
}
|
||||||
@@ -539,7 +551,7 @@ func TestCopy(t *testing.T) {
|
|||||||
cfg.Devices[0].Addresses[0] = "wrong"
|
cfg.Devices[0].Addresses[0] = "wrong"
|
||||||
cfg.Folders[0].Devices[0].DeviceID = protocol.DeviceID{0, 1, 2, 3}
|
cfg.Folders[0].Devices[0].DeviceID = protocol.DeviceID{0, 1, 2, 3}
|
||||||
cfg.Options.ListenAddress[0] = "wrong"
|
cfg.Options.ListenAddress[0] = "wrong"
|
||||||
cfg.GUI.APIKey = "wrong"
|
cfg.GUI.RawAPIKey = "wrong"
|
||||||
|
|
||||||
bsChanged, err := json.MarshalIndent(cfg, "", " ")
|
bsChanged, err := json.MarshalIndent(cfg, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -622,3 +634,25 @@ func TestLargeRescanInterval(t *testing.T) {
|
|||||||
t.Error("negative rescan interval should become zero")
|
t.Error("negative rescan interval should become zero")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGUIConfigURL(t *testing.T) {
|
||||||
|
testcases := [][2]string{
|
||||||
|
{"192.0.2.42:8080", "http://192.0.2.42:8080/"},
|
||||||
|
{":8080", "http://127.0.0.1:8080/"},
|
||||||
|
{"0.0.0.0:8080", "http://127.0.0.1:8080/"},
|
||||||
|
{"127.0.0.1:8080", "http://127.0.0.1:8080/"},
|
||||||
|
{"127.0.0.2:8080", "http://127.0.0.2:8080/"},
|
||||||
|
{"[::]:8080", "http://[::1]:8080/"},
|
||||||
|
{"[2001::42]:8080", "http://[2001::42]:8080/"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
c := GUIConfiguration{
|
||||||
|
RawAddress: tc[0],
|
||||||
|
}
|
||||||
|
u := c.URL()
|
||||||
|
if u != tc[1] {
|
||||||
|
t.Errorf("Incorrect URL %s != %s for addr %s", u, tc[1], tc[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+6
-3
@@ -10,10 +10,13 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/calmh/logger"
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
debug = strings.Contains(os.Getenv("STTRACE"), "config") || os.Getenv("STTRACE") == "all"
|
l = logger.DefaultLogger.NewFacility("config", "Configuration loading and saving")
|
||||||
l = logger.DefaultLogger
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
l.SetDebug("config", strings.Contains(os.Getenv("STTRACE"), "config") || os.Getenv("STTRACE") == "all")
|
||||||
|
}
|
||||||
|
|||||||
+10
-3
@@ -7,10 +7,15 @@
|
|||||||
<localAnnounceEnabled>false</localAnnounceEnabled>
|
<localAnnounceEnabled>false</localAnnounceEnabled>
|
||||||
<localAnnouncePort>42123</localAnnouncePort>
|
<localAnnouncePort>42123</localAnnouncePort>
|
||||||
<localAnnounceMCAddr>quux:3232</localAnnounceMCAddr>
|
<localAnnounceMCAddr>quux:3232</localAnnounceMCAddr>
|
||||||
|
<relayServer>relay://123.123.123.123:1234</relayServer>
|
||||||
|
<relayServer>relay://125.125.125.125:1255</relayServer>
|
||||||
<parallelRequests>32</parallelRequests>
|
<parallelRequests>32</parallelRequests>
|
||||||
<maxSendKbps>1234</maxSendKbps>
|
<maxSendKbps>1234</maxSendKbps>
|
||||||
<maxRecvKbps>2341</maxRecvKbps>
|
<maxRecvKbps>2341</maxRecvKbps>
|
||||||
<reconnectionIntervalS>6000</reconnectionIntervalS>
|
<reconnectionIntervalS>6000</reconnectionIntervalS>
|
||||||
|
<relaysEnabled>false</relaysEnabled>
|
||||||
|
<relayReconnectIntervalM>20</relayReconnectIntervalM>
|
||||||
|
<relayWithoutGlobalAnn>true</relayWithoutGlobalAnn>
|
||||||
<startBrowser>false</startBrowser>
|
<startBrowser>false</startBrowser>
|
||||||
<upnpEnabled>false</upnpEnabled>
|
<upnpEnabled>false</upnpEnabled>
|
||||||
<upnpLeaseMinutes>90</upnpLeaseMinutes>
|
<upnpLeaseMinutes>90</upnpLeaseMinutes>
|
||||||
@@ -24,8 +29,10 @@
|
|||||||
<symlinksEnabled>false</symlinksEnabled>
|
<symlinksEnabled>false</symlinksEnabled>
|
||||||
<limitBandwidthInLan>true</limitBandwidthInLan>
|
<limitBandwidthInLan>true</limitBandwidthInLan>
|
||||||
<databaseBlockCacheMiB>42</databaseBlockCacheMiB>
|
<databaseBlockCacheMiB>42</databaseBlockCacheMiB>
|
||||||
<pingTimeoutS>60</pingTimeoutS>
|
<minHomeDiskFreePct>5.2</minHomeDiskFreePct>
|
||||||
<pingIdleTimeS>120</pingIdleTimeS>
|
<urURL>https://localhost/newdata</urURL>
|
||||||
<minHomeDiskFreePct>5</minHomeDiskFreePct>
|
<urInitialDelayS>800</urInitialDelayS>
|
||||||
|
<urPostInsecurely>true</urPostInsecurely>
|
||||||
|
<releasesURL>https://localhost/releases</releasesURL>
|
||||||
</options>
|
</options>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<configuration version="12">
|
||||||
|
<folder id="test" path="testdata" ro="true" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
|
||||||
|
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
|
||||||
|
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
|
||||||
|
<minDiskFreePct>1</minDiskFreePct>
|
||||||
|
</folder>
|
||||||
|
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
|
||||||
|
<address>tcp://a</address>
|
||||||
|
</device>
|
||||||
|
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
|
||||||
|
<address>tcp://b</address>
|
||||||
|
</device>
|
||||||
|
</configuration>
|
||||||
+31
-15
@@ -9,9 +9,9 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,10 +60,8 @@ type Wrapper struct {
|
|||||||
deviceMap map[protocol.DeviceID]DeviceConfiguration
|
deviceMap map[protocol.DeviceID]DeviceConfiguration
|
||||||
folderMap map[string]FolderConfiguration
|
folderMap map[string]FolderConfiguration
|
||||||
replaces chan Configuration
|
replaces chan Configuration
|
||||||
mut sync.Mutex
|
|
||||||
|
|
||||||
subs []Committer
|
subs []Committer
|
||||||
sMut sync.Mutex
|
mut sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap wraps an existing Configuration structure and ties it to a file on
|
// Wrap wraps an existing Configuration structure and ties it to a file on
|
||||||
@@ -73,7 +71,6 @@ func Wrap(path string, cfg Configuration) *Wrapper {
|
|||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
path: path,
|
path: path,
|
||||||
mut: sync.NewMutex(),
|
mut: sync.NewMutex(),
|
||||||
sMut: sync.NewMutex(),
|
|
||||||
}
|
}
|
||||||
w.replaces = make(chan Configuration)
|
w.replaces = make(chan Configuration)
|
||||||
return w
|
return w
|
||||||
@@ -109,9 +106,24 @@ func (w *Wrapper) Stop() {
|
|||||||
// Subscribe registers the given handler to be called on any future
|
// Subscribe registers the given handler to be called on any future
|
||||||
// configuration changes.
|
// configuration changes.
|
||||||
func (w *Wrapper) Subscribe(c Committer) {
|
func (w *Wrapper) Subscribe(c Committer) {
|
||||||
w.sMut.Lock()
|
w.mut.Lock()
|
||||||
w.subs = append(w.subs, c)
|
w.subs = append(w.subs, c)
|
||||||
w.sMut.Unlock()
|
w.mut.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe de-registers the given handler from any future calls to
|
||||||
|
// configuration changes
|
||||||
|
func (w *Wrapper) Unsubscribe(c Committer) {
|
||||||
|
w.mut.Lock()
|
||||||
|
for i := range w.subs {
|
||||||
|
if w.subs[i] == c {
|
||||||
|
copy(w.subs[i:], w.subs[i+1:])
|
||||||
|
w.subs[len(w.subs)-1] = nil
|
||||||
|
w.subs = w.subs[:len(w.subs)-1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.mut.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Raw returns the currently wrapped Configuration object.
|
// Raw returns the currently wrapped Configuration object.
|
||||||
@@ -130,13 +142,9 @@ func (w *Wrapper) replaceLocked(to Configuration) CommitResponse {
|
|||||||
from := w.cfg
|
from := w.cfg
|
||||||
|
|
||||||
for _, sub := range w.subs {
|
for _, sub := range w.subs {
|
||||||
if debug {
|
|
||||||
l.Debugln(sub, "verifying configuration")
|
l.Debugln(sub, "verifying configuration")
|
||||||
}
|
|
||||||
if err := sub.VerifyConfiguration(from, to); err != nil {
|
if err := sub.VerifyConfiguration(from, to); err != nil {
|
||||||
if debug {
|
|
||||||
l.Debugln(sub, "rejected config:", err)
|
l.Debugln(sub, "rejected config:", err)
|
||||||
}
|
|
||||||
return CommitResponse{
|
return CommitResponse{
|
||||||
ValidationError: err,
|
ValidationError: err,
|
||||||
}
|
}
|
||||||
@@ -145,14 +153,10 @@ func (w *Wrapper) replaceLocked(to Configuration) CommitResponse {
|
|||||||
|
|
||||||
allOk := true
|
allOk := true
|
||||||
for _, sub := range w.subs {
|
for _, sub := range w.subs {
|
||||||
if debug {
|
|
||||||
l.Debugln(sub, "committing configuration")
|
l.Debugln(sub, "committing configuration")
|
||||||
}
|
|
||||||
ok := sub.CommitConfiguration(from, to)
|
ok := sub.CommitConfiguration(from, to)
|
||||||
if !ok {
|
if !ok {
|
||||||
if debug {
|
|
||||||
l.Debugln(sub, "requires restart")
|
l.Debugln(sub, "requires restart")
|
||||||
}
|
|
||||||
allOk = false
|
allOk = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,3 +306,15 @@ func (w *Wrapper) Save() error {
|
|||||||
events.Default.Log(events.ConfigSaved, w.cfg)
|
events.Default.Log(events.ConfigSaved, w.cfg)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Wrapper) GlobalDiscoveryServers() []string {
|
||||||
|
var servers []string
|
||||||
|
for _, srv := range w.cfg.Options.GlobalAnnServers {
|
||||||
|
if srv == "default" {
|
||||||
|
servers = append(servers, DefaultDiscoveryServers...)
|
||||||
|
} else {
|
||||||
|
servers = append(servers, srv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueStrings(servers)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,461 @@
|
|||||||
|
// Copyright (C) 2015 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package connections
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/juju/ratelimit"
|
||||||
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/discover"
|
||||||
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/relay"
|
||||||
|
"github.com/syncthing/syncthing/lib/relay/client"
|
||||||
|
|
||||||
|
"github.com/thejerf/suture"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DialerFactory func(*url.URL, *tls.Config) (*tls.Conn, error)
|
||||||
|
type ListenerFactory func(*url.URL, *tls.Config, chan<- model.IntermediateConnection)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dialers = make(map[string]DialerFactory, 0)
|
||||||
|
listeners = make(map[string]ListenerFactory, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Model interface {
|
||||||
|
protocol.Model
|
||||||
|
AddConnection(conn model.Connection)
|
||||||
|
ConnectedTo(remoteID protocol.DeviceID) bool
|
||||||
|
IsPaused(remoteID protocol.DeviceID) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// The connection service listens on TLS and dials configured unconnected
|
||||||
|
// devices. Successful connections are handed to the model.
|
||||||
|
type connectionSvc struct {
|
||||||
|
*suture.Supervisor
|
||||||
|
cfg *config.Wrapper
|
||||||
|
myID protocol.DeviceID
|
||||||
|
model Model
|
||||||
|
tlsCfg *tls.Config
|
||||||
|
discoverer discover.Finder
|
||||||
|
conns chan model.IntermediateConnection
|
||||||
|
relaySvc *relay.Svc
|
||||||
|
bepProtocolName string
|
||||||
|
tlsDefaultCommonName string
|
||||||
|
lans []*net.IPNet
|
||||||
|
writeRateLimit *ratelimit.Bucket
|
||||||
|
readRateLimit *ratelimit.Bucket
|
||||||
|
|
||||||
|
lastRelayCheck map[protocol.DeviceID]time.Time
|
||||||
|
|
||||||
|
mut sync.RWMutex
|
||||||
|
connType map[protocol.DeviceID]model.ConnectionType
|
||||||
|
relaysEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, relaySvc *relay.Svc,
|
||||||
|
bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) suture.Service {
|
||||||
|
svc := &connectionSvc{
|
||||||
|
Supervisor: suture.NewSimple("connectionSvc"),
|
||||||
|
cfg: cfg,
|
||||||
|
myID: myID,
|
||||||
|
model: mdl,
|
||||||
|
tlsCfg: tlsCfg,
|
||||||
|
discoverer: discoverer,
|
||||||
|
relaySvc: relaySvc,
|
||||||
|
conns: make(chan model.IntermediateConnection),
|
||||||
|
bepProtocolName: bepProtocolName,
|
||||||
|
tlsDefaultCommonName: tlsDefaultCommonName,
|
||||||
|
lans: lans,
|
||||||
|
|
||||||
|
connType: make(map[protocol.DeviceID]model.ConnectionType),
|
||||||
|
relaysEnabled: cfg.Options().RelaysEnabled,
|
||||||
|
lastRelayCheck: make(map[protocol.DeviceID]time.Time),
|
||||||
|
}
|
||||||
|
cfg.Subscribe(svc)
|
||||||
|
|
||||||
|
if svc.cfg.Options().MaxSendKbps > 0 {
|
||||||
|
svc.writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*svc.cfg.Options().MaxSendKbps), int64(5*1000*svc.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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// via the relays.
|
||||||
|
//
|
||||||
|
// TODO: Clean shutdown, and/or handling config changes on the fly. We
|
||||||
|
// partly do this now - new devices and addresses will be picked up, but
|
||||||
|
// 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 {
|
||||||
|
uri, err := url.Parse(addr)
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln("Failed to parse listen address:", addr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, ok := listeners[uri.Scheme]
|
||||||
|
if !ok {
|
||||||
|
l.Infoln("Unknown listen address scheme:", uri.String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debugln("listening on", uri)
|
||||||
|
|
||||||
|
svc.Add(serviceFunc(func() {
|
||||||
|
listener(uri, svc.tlsCfg, svc.conns)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
svc.Add(serviceFunc(svc.handle))
|
||||||
|
|
||||||
|
if svc.relaySvc != nil {
|
||||||
|
svc.Add(serviceFunc(svc.acceptRelayConns))
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *connectionSvc) handle() {
|
||||||
|
next:
|
||||||
|
for c := range s.conns {
|
||||||
|
cs := c.Conn.ConnectionState()
|
||||||
|
|
||||||
|
// We should have negotiated the next level protocol "bep/1.0" as part
|
||||||
|
// of the TLS handshake. Unfortunately this can't be a hard error,
|
||||||
|
// because there are implementations out there that don't support
|
||||||
|
// protocol negotiation (iOS for one...).
|
||||||
|
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != s.bepProtocolName {
|
||||||
|
l.Infof("Peer %s did not negotiate bep/1.0", c.Conn.RemoteAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should have received exactly one certificate from the other
|
||||||
|
// side. If we didn't, they don't have a device ID and we drop the
|
||||||
|
// connection.
|
||||||
|
certs := cs.PeerCertificates
|
||||||
|
if cl := len(certs); cl != 1 {
|
||||||
|
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.Conn.RemoteAddr())
|
||||||
|
c.Conn.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remoteCert := certs[0]
|
||||||
|
remoteID := protocol.NewDeviceID(remoteCert.Raw)
|
||||||
|
|
||||||
|
// The device ID should not be that of ourselves. It can happen
|
||||||
|
// though, especially in the presence of NAT hairpinning, multiple
|
||||||
|
// clients between the same NAT gateway, and global discovery.
|
||||||
|
if remoteID == s.myID {
|
||||||
|
l.Infof("Connected to myself (%s) - should not happen", remoteID)
|
||||||
|
c.Conn.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a relay connection, and the new incoming connection is
|
||||||
|
// not a relay connection, we should drop that, and prefer the this one.
|
||||||
|
s.mut.RLock()
|
||||||
|
ct, ok := s.connType[remoteID]
|
||||||
|
s.mut.RUnlock()
|
||||||
|
if ok && !ct.IsDirect() && c.Type.IsDirect() {
|
||||||
|
l.Debugln("Switching connections", remoteID)
|
||||||
|
s.model.Close(remoteID, fmt.Errorf("switching connections"))
|
||||||
|
} else if s.model.ConnectedTo(remoteID) {
|
||||||
|
// We should not already be connected to the other party. TODO: This
|
||||||
|
// could use some better handling. If the old connection is dead but
|
||||||
|
// hasn't timed out yet we may want to drop *that* connection and keep
|
||||||
|
// this one. But in case we are two devices connecting to each other
|
||||||
|
// in parallel we don't want to do that or we end up with no
|
||||||
|
// connections still established...
|
||||||
|
l.Infof("Connected to already connected device (%s)", remoteID)
|
||||||
|
c.Conn.Close()
|
||||||
|
continue
|
||||||
|
} else if s.model.IsPaused(remoteID) {
|
||||||
|
l.Infof("Connection from paused device (%s)", remoteID)
|
||||||
|
c.Conn.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for deviceID, deviceCfg := range s.cfg.Devices() {
|
||||||
|
if deviceID == remoteID {
|
||||||
|
// Verify the name on the certificate. By default we set it to
|
||||||
|
// "syncthing" when generating, but the user may have replaced
|
||||||
|
// the certificate and used another name.
|
||||||
|
certName := deviceCfg.CertName
|
||||||
|
if certName == "" {
|
||||||
|
certName = s.tlsDefaultCommonName
|
||||||
|
}
|
||||||
|
err := remoteCert.VerifyHostname(certName)
|
||||||
|
if err != nil {
|
||||||
|
// Incorrect certificate name is something the user most
|
||||||
|
// likely wants to know about, since it's an advanced
|
||||||
|
// config. Warn instead of Info.
|
||||||
|
l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.Conn.RemoteAddr(), err)
|
||||||
|
c.Conn.Close()
|
||||||
|
continue next
|
||||||
|
}
|
||||||
|
|
||||||
|
// If rate limiting is set, and based on the address we should
|
||||||
|
// limit the connection, then we wrap it in a limiter.
|
||||||
|
|
||||||
|
limit := s.shouldLimit(c.Conn.RemoteAddr())
|
||||||
|
|
||||||
|
wr := io.Writer(c.Conn)
|
||||||
|
if limit && s.writeRateLimit != nil {
|
||||||
|
wr = NewWriteLimiter(c.Conn, s.writeRateLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
rd := io.Reader(c.Conn)
|
||||||
|
if limit && s.readRateLimit != nil {
|
||||||
|
rd = NewReadLimiter(c.Conn, s.readRateLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := fmt.Sprintf("%s-%s (%s)", c.Conn.LocalAddr(), c.Conn.RemoteAddr(), c.Type)
|
||||||
|
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
|
||||||
|
|
||||||
|
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
||||||
|
l.Debugf("cipher suite: %04X in lan: %t", c.Conn.ConnectionState().CipherSuite, !limit)
|
||||||
|
|
||||||
|
s.model.AddConnection(model.Connection{
|
||||||
|
c.Conn,
|
||||||
|
protoConn,
|
||||||
|
c.Type,
|
||||||
|
})
|
||||||
|
s.mut.Lock()
|
||||||
|
s.connType[remoteID] = c.Type
|
||||||
|
s.mut.Unlock()
|
||||||
|
continue next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.cfg.IgnoredDevice(remoteID) {
|
||||||
|
events.Default.Log(events.DeviceRejected, map[string]string{
|
||||||
|
"device": remoteID.String(),
|
||||||
|
"address": c.Conn.RemoteAddr().String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Infof("Connection from %s (%s) with ignored device ID %s", c.Conn.RemoteAddr(), c.Type, remoteID)
|
||||||
|
c.Conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *connectionSvc) connect() {
|
||||||
|
delay := time.Second
|
||||||
|
for {
|
||||||
|
nextDevice:
|
||||||
|
for deviceID, deviceCfg := range s.cfg.Devices() {
|
||||||
|
if deviceID == s.myID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.model.IsPaused(deviceID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
connected := s.model.ConnectedTo(deviceID)
|
||||||
|
|
||||||
|
s.mut.RLock()
|
||||||
|
ct, ok := s.connType[deviceID]
|
||||||
|
relaysEnabled := s.relaysEnabled
|
||||||
|
s.mut.RUnlock()
|
||||||
|
if connected && ok && ct.IsDirect() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var addrs []string
|
||||||
|
var relays []discover.Relay
|
||||||
|
for _, addr := range deviceCfg.Addresses {
|
||||||
|
if addr == "dynamic" {
|
||||||
|
if s.discoverer != nil {
|
||||||
|
if t, r, err := s.discoverer.Lookup(deviceID); err == nil {
|
||||||
|
addrs = append(addrs, t...)
|
||||||
|
relays = append(relays, r...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
uri, err := url.Parse(addr)
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln("Failed to parse connection url:", addr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer, ok := dialers[uri.Scheme]
|
||||||
|
if !ok {
|
||||||
|
l.Infoln("Unknown address schema", uri)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debugln("dial", deviceCfg.DeviceID, uri)
|
||||||
|
conn, err := dialer(uri, s.tlsCfg)
|
||||||
|
if err != nil {
|
||||||
|
l.Debugln("dial failed", deviceCfg.DeviceID, uri, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if connected {
|
||||||
|
s.model.Close(deviceID, fmt.Errorf("switching connections"))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.conns <- model.IntermediateConnection{
|
||||||
|
conn, model.ConnectionTypeDirectDial,
|
||||||
|
}
|
||||||
|
continue nextDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only connect via relays if not already connected
|
||||||
|
// Also, do not set lastRelayCheck time if we have no relays,
|
||||||
|
// as otherwise when we do discover relays, we might have to
|
||||||
|
// 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 {
|
||||||
|
continue nextDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
reconIntv := time.Duration(s.cfg.Options().RelayReconnectIntervalM) * time.Minute
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastRelayCheck[deviceID] = time.Now()
|
||||||
|
|
||||||
|
for _, addr := range relays {
|
||||||
|
uri, err := url.Parse(addr.URL)
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln("Failed to parse relay connection url:", addr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
inv, err := client.GetInvitationFromRelay(uri, deviceID, s.tlsCfg.Certificates)
|
||||||
|
if err != nil {
|
||||||
|
l.Debugf("Failed to get invitation for %s from %s: %v", deviceID, uri, err)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
l.Debugln("Succesfully retrieved relay invitation", inv, "from", uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := client.JoinSession(inv)
|
||||||
|
if err != nil {
|
||||||
|
l.Debugf("Failed to join relay session %s: %v", inv, err)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
l.Debugln("Sucessfully joined relay session", inv)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = osutil.SetTCPOptions(conn.(*net.TCPConn))
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tc *tls.Conn
|
||||||
|
|
||||||
|
if inv.ServerSocket {
|
||||||
|
tc = tls.Server(conn, s.tlsCfg)
|
||||||
|
} else {
|
||||||
|
tc = tls.Client(conn, s.tlsCfg)
|
||||||
|
}
|
||||||
|
err = tc.Handshake()
|
||||||
|
if err != nil {
|
||||||
|
l.Infof("TLS handshake (BEP/relay %s): %v", inv, err)
|
||||||
|
tc.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.conns <- model.IntermediateConnection{
|
||||||
|
tc, model.ConnectionTypeRelayDial,
|
||||||
|
}
|
||||||
|
continue nextDevice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(delay)
|
||||||
|
delay *= 2
|
||||||
|
if maxD := time.Duration(s.cfg.Options().ReconnectIntervalS) * time.Second; delay > maxD {
|
||||||
|
delay = maxD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *connectionSvc) acceptRelayConns() {
|
||||||
|
for {
|
||||||
|
conn := s.relaySvc.Accept()
|
||||||
|
s.conns <- model.IntermediateConnection{
|
||||||
|
Conn: conn,
|
||||||
|
Type: model.ConnectionTypeRelayAccept,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
|
||||||
|
if s.cfg.Options().LimitBandwidthInLan {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpaddr, ok := addr.(*net.TCPAddr)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, lan := range s.lans {
|
||||||
|
if lan.Contains(tcpaddr.IP) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !tcpaddr.IP.IsLoopback()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *connectionSvc) VerifyConfiguration(from, to config.Configuration) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool {
|
||||||
|
s.mut.Lock()
|
||||||
|
s.relaysEnabled = to.Options.RelaysEnabled
|
||||||
|
s.mut.Unlock()
|
||||||
|
|
||||||
|
// We require a restart if a device as been removed.
|
||||||
|
|
||||||
|
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
|
||||||
|
for _, dev := range to.Devices {
|
||||||
|
newDevices[dev.DeviceID] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dev := range from.Devices {
|
||||||
|
if !newDevices[dev.DeviceID] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceFunc wraps a function to create a suture.Service without stop
|
||||||
|
// functionality.
|
||||||
|
type serviceFunc func()
|
||||||
|
|
||||||
|
func (f serviceFunc) Serve() { f() }
|
||||||
|
func (f serviceFunc) Stop() {}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (C) 2015 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package connections
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dialers["tcp"] = tcpDialer
|
||||||
|
listeners["tcp"] = tcpListener
|
||||||
|
}
|
||||||
|
|
||||||
|
func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) {
|
||||||
|
host, port, err := net.SplitHostPort(uri.Host)
|
||||||
|
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
|
||||||
|
// addr is on the form "1.2.3.4"
|
||||||
|
uri.Host = net.JoinHostPort(uri.Host, "22000")
|
||||||
|
} else if err == nil && port == "" {
|
||||||
|
// addr is on the form "1.2.3.4:"
|
||||||
|
uri.Host = net.JoinHostPort(host, "22000")
|
||||||
|
}
|
||||||
|
|
||||||
|
raddr, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||||
|
if err != nil {
|
||||||
|
l.Debugln(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialTCP("tcp", nil, raddr)
|
||||||
|
if err != nil {
|
||||||
|
l.Debugln(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = osutil.SetTCPOptions(conn)
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tc := tls.Client(conn, tlsCfg)
|
||||||
|
err = tc.Handshake()
|
||||||
|
if err != nil {
|
||||||
|
tc.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tcpListener(uri *url.URL, tlsCfg *tls.Config, conns chan<- model.IntermediateConnection) {
|
||||||
|
tcaddr, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("listen (BEP/tcp):", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listener, err := net.ListenTCP("tcp", tcaddr)
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("listen (BEP/tcp):", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Accepting connection (BEP/tcp):", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debugln("connect from", conn.RemoteAddr())
|
||||||
|
|
||||||
|
err = osutil.SetTCPOptions(conn.(*net.TCPConn))
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tc := tls.Server(conn, tlsCfg)
|
||||||
|
err = tc.Handshake()
|
||||||
|
if err != nil {
|
||||||
|
l.Infoln("TLS handshake (BEP/tcp):", err)
|
||||||
|
tc.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
conns <- model.IntermediateConnection{
|
||||||
|
tc, model.ConnectionTypeDirectAccept,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (C) 2014 The Syncthing Authors.
|
||||||
|
//
|
||||||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
package connections
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
l = logger.DefaultLogger.NewFacility("connections", "Connection handling")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
l.SetDebug("connections", strings.Contains(os.Getenv("STTRACE"), "connections") || os.Getenv("STTRACE") == "all")
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package connections
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -12,13 +12,20 @@ import (
|
|||||||
"github.com/juju/ratelimit"
|
"github.com/juju/ratelimit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type limitedReader struct {
|
type LimitedReader struct {
|
||||||
r io.Reader
|
reader io.Reader
|
||||||
bucket *ratelimit.Bucket
|
bucket *ratelimit.Bucket
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *limitedReader) Read(buf []byte) (int, error) {
|
func NewReadLimiter(r io.Reader, b *ratelimit.Bucket) *LimitedReader {
|
||||||
n, err := r.r.Read(buf)
|
return &LimitedReader{
|
||||||
|
reader: r,
|
||||||
|
bucket: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LimitedReader) Read(buf []byte) (int, error) {
|
||||||
|
n, err := r.reader.Read(buf)
|
||||||
if r.bucket != nil {
|
if r.bucket != nil {
|
||||||
r.bucket.Wait(int64(n))
|
r.bucket.Wait(int64(n))
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
package main
|
package connections
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
@@ -12,14 +12,21 @@ import (
|
|||||||
"github.com/juju/ratelimit"
|
"github.com/juju/ratelimit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type limitedWriter struct {
|
type LimitedWriter struct {
|
||||||
w io.Writer
|
writer io.Writer
|
||||||
bucket *ratelimit.Bucket
|
bucket *ratelimit.Bucket
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *limitedWriter) Write(buf []byte) (int, error) {
|
func NewWriteLimiter(w io.Writer, b *ratelimit.Bucket) *LimitedWriter {
|
||||||
|
return &LimitedWriter{
|
||||||
|
writer: w,
|
||||||
|
bucket: b,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LimitedWriter) Write(buf []byte) (int, error) {
|
||||||
if w.bucket != nil {
|
if w.bucket != nil {
|
||||||
w.bucket.Wait(int64(len(buf)))
|
w.bucket.Wait(int64(len(buf)))
|
||||||
}
|
}
|
||||||
return w.w.Write(buf)
|
return w.writer.Write(buf)
|
||||||
}
|
}
|
||||||
+3
-36
@@ -16,12 +16,9 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
"github.com/syndtr/goleveldb/leveldb/util"
|
"github.com/syndtr/goleveldb/leveldb/util"
|
||||||
@@ -113,47 +110,20 @@ func (m *BlockMap) blockKey(hash []byte, file string) []byte {
|
|||||||
|
|
||||||
type BlockFinder struct {
|
type BlockFinder struct {
|
||||||
db *leveldb.DB
|
db *leveldb.DB
|
||||||
folders []string
|
|
||||||
mut sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBlockFinder(db *leveldb.DB, cfg *config.Wrapper) *BlockFinder {
|
func NewBlockFinder(db *leveldb.DB) *BlockFinder {
|
||||||
if blockFinder != nil {
|
if blockFinder != nil {
|
||||||
return blockFinder
|
return blockFinder
|
||||||
}
|
}
|
||||||
|
|
||||||
f := &BlockFinder{
|
f := &BlockFinder{
|
||||||
db: db,
|
db: db,
|
||||||
mut: sync.NewRWMutex(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
f.CommitConfiguration(config.Configuration{}, cfg.Raw())
|
|
||||||
cfg.Subscribe(f)
|
|
||||||
|
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyConfiguration implementes the config.Committer interface
|
|
||||||
func (f *BlockFinder) VerifyConfiguration(from, to config.Configuration) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitConfiguration implementes the config.Committer interface
|
|
||||||
func (f *BlockFinder) CommitConfiguration(from, to config.Configuration) bool {
|
|
||||||
folders := make([]string, len(to.Folders))
|
|
||||||
for i, folder := range to.Folders {
|
|
||||||
folders[i] = folder.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(folders)
|
|
||||||
|
|
||||||
f.mut.Lock()
|
|
||||||
f.folders = folders
|
|
||||||
f.mut.Unlock()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *BlockFinder) String() string {
|
func (f *BlockFinder) String() string {
|
||||||
return fmt.Sprintf("BlockFinder@%p", f)
|
return fmt.Sprintf("BlockFinder@%p", f)
|
||||||
}
|
}
|
||||||
@@ -163,10 +133,7 @@ func (f *BlockFinder) String() string {
|
|||||||
// they are happy with the block) or false to continue iterating for whatever
|
// they are happy with the block) or false to continue iterating for whatever
|
||||||
// reason. The iterator finally returns the result, whether or not a
|
// reason. The iterator finally returns the result, whether or not a
|
||||||
// satisfying block was eventually found.
|
// satisfying block was eventually found.
|
||||||
func (f *BlockFinder) Iterate(hash []byte, iterFn func(string, string, int32) bool) bool {
|
func (f *BlockFinder) Iterate(folders []string, hash []byte, iterFn func(string, string, int32) bool) bool {
|
||||||
f.mut.RLock()
|
|
||||||
folders := f.folders
|
|
||||||
f.mut.RUnlock()
|
|
||||||
for _, folder := range folders {
|
for _, folder := range folders {
|
||||||
key := toBlockKey(hash, folder, "")
|
key := toBlockKey(hash, folder, "")
|
||||||
iter := f.db.NewIterator(util.BytesPrefix(key), nil)
|
iter := f.db.NewIterator(util.BytesPrefix(key), nil)
|
||||||
|
|||||||
+16
-23
@@ -9,8 +9,7 @@ package db
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/syncthing/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
|
||||||
|
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||||
@@ -30,6 +29,7 @@ func genBlocks(n int) []protocol.BlockInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var f1, f2, f3 protocol.FileInfo
|
var f1, f2, f3 protocol.FileInfo
|
||||||
|
var folders = []string{"folder1", "folder2"}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
blocks := genBlocks(30)
|
blocks := genBlocks(30)
|
||||||
@@ -57,16 +57,7 @@ func setup() (*leveldb.DB, *BlockFinder) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
return db, NewBlockFinder(db)
|
||||||
wrapper := config.Wrap("", config.Configuration{})
|
|
||||||
wrapper.SetFolder(config.FolderConfiguration{
|
|
||||||
ID: "folder1",
|
|
||||||
})
|
|
||||||
wrapper.SetFolder(config.FolderConfiguration{
|
|
||||||
ID: "folder2",
|
|
||||||
})
|
|
||||||
|
|
||||||
return db, NewBlockFinder(db, wrapper)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbEmpty(db *leveldb.DB) bool {
|
func dbEmpty(db *leveldb.DB) bool {
|
||||||
@@ -94,21 +85,21 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||||
if folder != "folder1" || file != "f1" || index != 0 {
|
if folder != "folder1" || file != "f1" || index != 0 {
|
||||||
t.Fatal("Mismatch")
|
t.Fatal("Mismatch")
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
f.Iterate(f2.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
f.Iterate(folders, f2.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||||
if folder != "folder1" || file != "f2" || index != 0 {
|
if folder != "folder1" || file != "f2" || index != 0 {
|
||||||
t.Fatal("Mismatch")
|
t.Fatal("Mismatch")
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
f.Iterate(f3.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
f.Iterate(folders, f3.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||||
t.Fatal("Unexpected block")
|
t.Fatal("Unexpected block")
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -123,17 +114,17 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||||
t.Fatal("Unexpected block")
|
t.Fatal("Unexpected block")
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
f.Iterate(f2.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
f.Iterate(folders, f2.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||||
t.Fatal("Unexpected block")
|
t.Fatal("Unexpected block")
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
f.Iterate(f3.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
f.Iterate(folders, f3.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||||
if folder != "folder1" || file != "f3" || index != 0 {
|
if folder != "folder1" || file != "f3" || index != 0 {
|
||||||
t.Fatal("Mismatch")
|
t.Fatal("Mismatch")
|
||||||
}
|
}
|
||||||
@@ -180,7 +171,7 @@ func TestBlockFinderLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
counter := 0
|
counter := 0
|
||||||
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||||
counter++
|
counter++
|
||||||
switch counter {
|
switch counter {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -196,6 +187,7 @@ func TestBlockFinderLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
if counter != 2 {
|
if counter != 2 {
|
||||||
t.Fatal("Incorrect count", counter)
|
t.Fatal("Incorrect count", counter)
|
||||||
}
|
}
|
||||||
@@ -208,7 +200,7 @@ func TestBlockFinderLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
counter = 0
|
counter = 0
|
||||||
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
f.Iterate(folders, f1.Blocks[0].Hash, func(folder, file string, index int32) bool {
|
||||||
counter++
|
counter++
|
||||||
switch counter {
|
switch counter {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -220,6 +212,7 @@ func TestBlockFinderLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
if counter != 1 {
|
if counter != 1 {
|
||||||
t.Fatal("Incorrect count")
|
t.Fatal("Incorrect count")
|
||||||
}
|
}
|
||||||
@@ -240,7 +233,7 @@ func TestBlockFinderFix(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !f.Iterate(f1.Blocks[0].Hash, iterFn) {
|
if !f.Iterate(folders, f1.Blocks[0].Hash, iterFn) {
|
||||||
t.Fatal("Block not found")
|
t.Fatal("Block not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,11 +242,11 @@ func TestBlockFinderFix(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Iterate(f1.Blocks[0].Hash, iterFn) {
|
if f.Iterate(folders, f1.Blocks[0].Hash, iterFn) {
|
||||||
t.Fatal("Unexpected block")
|
t.Fatal("Unexpected block")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !f.Iterate(f2.Blocks[0].Hash, iterFn) {
|
if !f.Iterate(folders, f2.Blocks[0].Hash, iterFn) {
|
||||||
t.Fatal("Block not found")
|
t.Fatal("Block not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user