Compare commits

..

1 Commits

Author SHA1 Message Date
Jakob Borg
f2f9113fd9 nothing: Dummy commit 2016-05-14 08:02:02 +02:00
393 changed files with 6379 additions and 68563 deletions

7
.gitignore vendored
View File

@@ -1,7 +1,8 @@
/syncthing
/discosrv
syncthing
!gui/syncthing
!debian/syncthing
!Godeps/_workspace/src/github.com/syncthing
syncthing.exe
discosrv.exe
*.tar.gz
*.zip
*.asc

183
AUTHORS
View File

@@ -1,98 +1,89 @@
# This is the official list of Syncthing authors for copyright purposes.
# The format is:
#
# Name Name Name (nickname) <email1@example.com> <email2@example.com>
#
# The NICKS list is auto generated from this file.
Aaron Bieber (qbit) <qbit@deftly.net>
Adam Piggott (simplypeachy) <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
Alessandro G. (alessandro.g89) <alessandro.g89@gmail.com>
Alexander Graf (alex2108) <register-github@alex-graf.de>
Alexandre Viau (aviau) <alexandre@alexandreviau.net> <aviau@debian.org>
Anderson Mesquita (andersonvom) <andersonvom@gmail.com>
Andrew Dunham (andrew-d) <andrew@du.nham.ca>
Andrey D (scienmind) <scintertech@cryptolab.net>
Antony Male (canton7) <antony.male@gmail.com>
Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com>
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com>
Bart De Vries (mogwa1) <devriesb@gmail.com>
Ben Curthoys (bencurthoys) <ben@bencurthoys.com>
Ben Schulz (uok) <ueomkail@gmail.com> <uok@users.noreply.github.com>
Ben Sidhom (bsidhom) <bsidhom@gmail.com>
Benny Ng (tpng) <benny.tpng@gmail.com>
Brandon Philips (philips) <brandon@ifup.org>
Brendan Long (brendanlong) <self@brendanlong.com>
Brian R. Becker (brbecker) <brbecker@gmail.com>
Caleb Callaway (cqcallaw) <enlightened.despot@gmail.com>
Carsten Hagemann (Moter8) <moter8@gmail.com>
Cathryne Linenweaver (Cathryne) <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
Cedric Staniewski (xduugu) <cedric@gmx.ca>
Chris Howie (cdhowie) <me@chrishowie.com>
Chris Joel (cdata) <chris@scriptolo.gy>
Colin Kennedy (moshen) <moshen.colin@gmail.com>
Daniel Bergmann (brgmnn) <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
Daniel Harte (norgeous) <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@users.noreply.github.com>
Daniel Martí (mvdan) <mvdan@mvdan.cc>
David Rimmer (dinosore) <dinosore@dbrsoftware.co.uk>
Denis A. (dva) <denisva@gmail.com>
Dennis Wilson (snnd) <dw@risu.io>
Dominik Heidler (asdil12) <dominik@heidler.eu>
Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
Emil Hessman (ceh) <emil@hessman.se>
Erik Meitner (WSGCSysadmin) <e.meitner@willystreet.coop>
Federico Castagnini (facastagnini) <federico.castagnini@gmail.com>
Felix Ableitner (Nutomic) <me@nutomic.com>
Felix Unterpaintner (bigbear2nd) <bigbear2nd@gmail.com>
Francois-Xavier Gsell (zukoo) <fxgsell@gmail.com>
Frank Isemann (fti7) <frank@isemann.name>
Gilli Sigurdsson (gillisig) <gilli@vx.is>
Jaakko Hannikainen (jgke) <jgke@jgke.fi>
Jacek Szafarkiewicz (hadogenes) <szafar@linux.pl>
Jake Peterson (acogdev) <jake@acogdev.com>
Jakob Borg (calmh) <jakob@nym.se>
James Patterson (jpjp) <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
Jaroslav Malec (dzarda) <dzardacz@gmail.com>
Jens Diemer (jedie) <github.com@jensdiemer.de> <git@jensdiemer.de>
Jochen Voss (seehuhn) <voss@seehuhn.de>
Johan Vromans (sciurius) <jvromans@squirrel.nl>
Karol Różycki (krozycki) <rozycki.karol@gmail.com>
Kelong Cong (kc1212) <kc04bc@gmx.com> <kc1212@users.noreply.github.com>
Ken'ichi Kamada (kamadak) <kamada@nanohz.org>
Kevin Allen (ironmig) <kma1660@gmail.com>
Lars K.W. Gohlke (lkwg82) <lkwg82@gmx.de>
Laurent Etiemble (letiemble) <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net>
Lode Hoste (Zillode) <zillode@zillode.be>
Lord Landon Agahnim (LordLandon) <lordlandon@gmail.com>
Majed Abdulaziz (majedev) <majed.alhajry@gmail.com>
Marc Laporte (marclaporte) <marc@marclaporte.com> <marc@laporte.name>
Marc Pujol (kilburn) <kilburn@la3.org>
Marcin Dziadus (marcindziadus) <dziadus.marcin@gmail.com>
Mateusz Naściszewski (mateon1) <matin1111@wp.pl>
Matt Burke (burkemw3) <mburke@amplify.com> <burkemw3@gmail.com>
Max Schulze (kralo) <max.schulze@online.de> <kralo@users.noreply.github.com>
Michael Jephcote (Rewt0r) <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
Michael Ploujnikov (plouj) <ploujj@gmail.com>
Michael Tilli (pyfisch) <pyfisch@gmail.com>
Nate Morrison (nrm21) <natemorrison@gmail.com>
Pascal Jungblut (pascalj) <github@pascalj.com> <mail@pascal-jungblut.com>
Peter Hoeg (peterhoeg) <peter@speartail.com>
Philippe Schommers (filoozoom) <philippe@schommers.be>
Phill Luby (pluby) <phill.luby@newredo.com>
Piotr Bejda (piobpl) <piotrb10@gmail.com>
Ryan Sullivan (KayoticSully) <kayoticsully@gmail.com>
Scott Klupfel (kluppy) <kluppy@going2blue.com>
Sergey Mishin (ralder) <ralder@yandex.ru>
Stefan Kuntz (Stefan-Code) <stefan.github@gmail.com> <Stefan.github@gmail.com>
Stefan Tatschner (rumpelsepp) <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
Tim Abell (timabell) <tim@timwise.co.uk>
Tobias Nygren (tnn2) <tnn@nygren.pp.se>
Tomas Cerveny (kozec) <kozec@kozec.com>
Tully Robinson (tojrobinson) <tully@tojr.org>
Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com>
Veeti Paananen (veeti) <veeti.paananen@rojekti.fi>
Victor Buinsky (buinsky) <vix_booja@tut.by>
Vil Brekin (Vilbrekin) <vilbrekin@gmail.com>
William A. Kennington III (wkennington) <william@wkennington.com>
Wulf Weich (wweich) <wweich@users.noreply.github.com> <wweich@gmx.de>
Yannic A. (eipiminus1) <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>
Aaron Bieber <qbit@deftly.net>
Adam Piggott <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
Alessandro G. <alessandro.g89@gmail.com>
Alexander Graf <register-github@alex-graf.de>
Anderson Mesquita <andersonvom@gmail.com>
Andrew Dunham <andrew@du.nham.ca>
Antony Male <antony.male@gmail.com>
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com> <frioux@gmail.com>
Audrius Butkevicius <audrius.butkevicius@gmail.com>
Bart De Vries <devriesb@gmail.com>
Ben Curthoys <ben@bencurthoys.com>
Ben Schulz <ueomkail@gmail.com> <uok@users.noreply.github.com>
Ben Sidhom <bsidhom@gmail.com>
Benny Ng <benny.tpng@gmail.com>
Brandon Philips <brandon@ifup.org>
Brendan Long <self@brendanlong.com>
Brian R. Becker <brbecker@gmail.com>
Caleb Callaway <enlightened.despot@gmail.com>
Carsten Hagemann <moter8@gmail.com>
Cathryne Linenweaver <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
Chris Howie <me@chrishowie.com>
Chris Joel <chris@scriptolo.gy>
Colin Kennedy <moshen.colin@gmail.com>
Daniel Bergmann <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
Daniel Harte <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@users.noreply.github.com>
Daniel Martí <mvdan@mvdan.cc>
David Rimmer <dinosore@dbrsoftware.co.uk>
Denis A. <denisva@gmail.com>
Dennis Wilson <dw@risu.io>
Dominik Heidler <dominik@heidler.eu>
Elias Jarlebring <jarlebring@gmail.com>
Emil Hessman <emil@hessman.se>
Erik Meitner <e.meitner@willystreet.coop>
Federico Castagnini <federico.castagnini@gmail.com>
Felix Ableitner <me@nutomic.com>
Felix Unterpaintner <bigbear2nd@gmail.com>
Francois-Xavier Gsell <fxgsell@gmail.com>
Frank Isemann <frank@isemann.name>
Gilli Sigurdsson <gilli@vx.is>
Jaakko Hannikainen <jgke@jgke.fi>
Jacek Szafarkiewicz <szafar@linux.pl>
Jake Peterson <jake@acogdev.com>
Jakob Borg <jakob@nym.se>
James Patterson <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
Jaroslav Malec <dzardacz@gmail.com>
Jens Diemer <github.com@jensdiemer.de> <git@jensdiemer.de>
Jochen Voss <voss@seehuhn.de>
Johan Vromans <jvromans@squirrel.nl>
Karol Różycki <rozycki.karol@gmail.com>
Kelong Cong <kc04bc@gmx.com> <kc1212@users.noreply.github.com>
Ken'ichi Kamada <kamada@nanohz.org>
Kevin Allen <kma1660@gmail.com>
Lars K.W. Gohlke <lkwg82@gmx.de>
Laurent Etiemble <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net>
Lode Hoste <zillode@zillode.be>
Lord Landon Agahnim <lordlandon@gmail.com>
Marc Laporte <marc@marclaporte.com> <marc@laporte.name>
Marc Pujol <kilburn@la3.org>
Marcin Dziadus <dziadus.marcin@gmail.com>
Mateusz Naściszewski <matin1111@wp.pl>
Matt Burke <mburke@amplify.com> <burkemw3@gmail.com>
Max Schulze <max.schulze@online.de> <kralo@users.noreply.github.com>
Michael Jephcote <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
Michael Ploujnikov <ploujj@gmail.com>
Michael Tilli <pyfisch@gmail.com>
Nate Morrison <natemorrison@gmail.com>
Pascal Jungblut <github@pascalj.com> <mail@pascal-jungblut.com>
Peter Hoeg <peter@speartail.com>
Philippe Schommers <philippe@schommers.be>
Phill Luby <phill.luby@newredo.com>
Piotr Bejda <piotrb10@gmail.com>
Ryan Sullivan <kayoticsully@gmail.com>
Scott Klupfel <kluppy@going2blue.com>
Sergey Mishin <ralder@yandex.ru>
Stefan Kuntz <stefan.github@gmail.com> <Stefan.github@gmail.com>
Stefan Tatschner <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
Tim Abell <tim@timwise.co.uk>
Tobias Nygren <tnn@nygren.pp.se>
Tomas Cerveny <kozec@kozec.com>
Tully Robinson <tully@tojr.org>
Tyler Brazier <tyler@tylerbrazier.com>
Veeti Paananen <veeti.paananen@rojekti.fi>
Victor Buinsky <vix_booja@tut.by>
Vil Brekin <vilbrekin@gmail.com>
William A. Kennington III <william@wkennington.com>
Wulf Weich <wweich@users.noreply.github.com> <wweich@gmx.de>
Yannic A. <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>

View File

@@ -44,20 +44,9 @@ repository](https://github.com/syncthing/docs).
## Licensing
All contributions are made available under the same license as the already
existing material being contributed to. For most of the project and unless
otherwise stated this means MPLv2, but there are exceptions:
- Certain commands (under cmd/...) may have a separate license, indicated by
the presence of a LICENSE file in the corresponding directory.
- The documentation (man/...) is licensed under the Creative Commons
Attribution 4.0 International License.
- Projects under vendor/... are copyright by and licensed from their
respective original authors. Contributions should be made to the original
project, not here.
Regardless of the license in effect, you retain the copyright to your
contribution.
All contributions are made under the same MPLv2 license as the rest of
the project, except documentation, user interface text and translation
strings which are licensed under the Creative Commons Attribution 4.0
International License. You retain the copyright to code you have
written.

View File

@@ -1,11 +1,7 @@
Do not report security issues in this bug tracker. Instead, contact
security@syncthing.net directly - see https://syncthing.net/security.html
for more information.
If your issue is a support request ("How do I get my devices to connect?"
or similar), please use the support forum at https://forum.syncthing.net/
where a large number of helpful people hang out. This issue tracker is for
reporting bugs or feature requests directly to the developers.
reporting bugs or feature requests directly to the developers.
If your issue is a bug report, replace this boilerplate with a description
of the problem, being sure to include at least:

197
NICKS
View File

@@ -1,115 +1,88 @@
# This file maps email addresses used in commits to nicks used the changelog.
# It is auto generated from the AUTHORS file by script/authors.go.
acogdev <jake@acogdev.com>
alessandro.g89 <alessandro.g89@gmail.com>
alex2108 <register-github@alex-graf.de>
andersonvom <andersonvom@gmail.com>
andrew-d <andrew@du.nham.ca>
asdil12 <dominik@heidler.eu>
AudriusButkevicius <audrius.butkevicius@gmail.com>
aviau <alexandre@alexandreviau.net>
aviau <aviau@debian.org>
bencurthoys <ben@bencurthoys.com>
bigbear2nd <bigbear2nd@gmail.com>
brbecker <brbecker@gmail.com>
brendanlong <self@brendanlong.com>
brgmnn <dan.arne.bergmann@gmail.com>
brgmnn <brgmnn@users.noreply.github.com>
bsidhom <bsidhom@gmail.com>
buinsky <vix_booja@tut.by>
burkemw3 <mburke@amplify.com>
burkemw3 <burkemw3@gmail.com>
calmh <jakob@nym.se>
canton7 <antony.male@gmail.com>
Cathryne <cathryne.linenweaver@gmail.com>
Cathryne <Cathryne@users.noreply.github.com>
cdata <chris@scriptolo.gy>
cdhowie <me@chrishowie.com>
ceh <emil@hessman.se>
cqcallaw <enlightened.despot@gmail.com>
dinosore <dinosore@dbrsoftware.co.uk>
dva <denisva@gmail.com>
dzarda <dzardacz@gmail.com>
eipiminus1 <eipiminusone+github@gmail.com>
eipiminus1 <eipiminus1@users.noreply.github.com>
facastagnini <federico.castagnini@gmail.com>
filoozoom <philippe@schommers.be>
frioux <frew@afoolishmanifesto.com>
frioux <frioux@gmail.com>
fti7 <frank@isemann.name>
gillisig <gilli@vx.is>
hadogenes <szafar@linux.pl>
ironmig <kma1660@gmail.com>
jarlebring <jarlebring@gmail.com>
jedie <github.com@jensdiemer.de>
jedie <git@jensdiemer.de>
jgke <jgke@jgke.fi>
jpjp <jamespatterson@operamail.com>
jpjp <jpjp@users.noreply.github.com>
kamadak <kamada@nanohz.org>
KayoticSully <kayoticsully@gmail.com>
kc1212 <kc04bc@gmx.com>
kc1212 <kc1212@users.noreply.github.com>
kilburn <kilburn@la3.org>
kluppy <kluppy@going2blue.com>
kozec <kozec@kozec.com>
kralo <max.schulze@online.de>
kralo <kralo@users.noreply.github.com>
krozycki <rozycki.karol@gmail.com>
letiemble <laurent.etiemble@gmail.com>
letiemble <laurent.etiemble@monobjc.net>
lkwg82 <lkwg82@gmx.de>
LordLandon <lordlandon@gmail.com>
majedev <majed.alhajry@gmail.com>
marcindziadus <dziadus.marcin@gmail.com>
acogdev <jake@acogdev.com>
alex2108 <register-github@alex-graf.de>
alessandro.g89 <alessandro.g89@gmail.com>
andersonvom <andersonvom@gmail.com>
andrew-d <andrew@du.nham.ca>
asdil12 <dominik@heidler.eu>
AudriusButkevicius <audrius.butkevicius@gmail.com>
bencurthoys <ben@bencurthoys.com>
bigbear2nd <bigbear2nd@gmail.com>
brbecker <brbecker@gmail.com>
brendanlong <self@brendanlong.com>
brgmnn <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
bsidhom <bsidhom@gmail.com>
buinsky <vix_booja@tut.by>
burkemw3 <mburke@amplify.com> <burkemw3@gmail.com>
calmh <jakob@nym.se>
canton7 <antony.male@gmail.com>
Cathryne <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
cdata <chris@scriptolo.gy>
cdhowie <me@chrishowie.com>
ceh <emil@hessman.se>
cqcallaw <enlightened.despot@gmail.com>
dinosore <dinosore@dbrsoftware.co.uk>
dva <denisva@gmail.com>
dzarda <dzardacz@gmail.com>
eipiminus1 <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>
facastagnini <federico.castagnini@gmail.com>
filoozoom <philippe@schommers.be>
frioux <frew@afoolishmanifesto.com> <frioux@gmail.com>
fti7 <frank@isemann.name>
gillisig <gilli@vx.is>
hadogenes <szafar@linux.pl>
ironmig <kma1660@gmail.com>
jarlebring <jarlebring@gmail.com>
jedie <github.com@jensdiemer.de> <git@jensdiemer.de>
jgke <jgke@jgke.fi>
jpjp <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
kamadak <kamada@nanohz.org>
KayoticSully <kayoticsully@gmail.com>
kilburn <kilburn@la3.org>
kluppy <kluppy@going2blue.com>
kozec <kozec@kozec.com>
kralo <max.schulze@online.de>
krozycki <rozycki.karol@gmail.com>
letiemble <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net>
LordLandon <lordlandon@gmail.com>
lkwg82 <lkwg82@gmx.de>
marcindziadus <dziadus.marcin@gmail.com>
marclaporte <marc@marclaporte.com>
marclaporte <marc@laporte.name>
mateon1 <matin1111@wp.pl>
mogwa1 <devriesb@gmail.com>
moshen <moshen.colin@gmail.com>
Moter8 <moter8@gmail.com>
mvdan <mvdan@mvdan.cc>
norgeous <daniel@harte.me>
norgeous <daniel@danielharte.co.uk>
norgeous <norgeous@users.noreply.github.com>
nrm21 <natemorrison@gmail.com>
Nutomic <me@nutomic.com>
pascalj <github@pascalj.com>
pascalj <mail@pascal-jungblut.com>
peterhoeg <peter@speartail.com>
philips <brandon@ifup.org>
piobpl <piotrb10@gmail.com>
plouj <ploujj@gmail.com>
pluby <phill.luby@newredo.com>
pyfisch <pyfisch@gmail.com>
qbit <qbit@deftly.net>
ralder <ralder@yandex.ru>
Rewt0r <rewt0r@gmx.com>
Rewt0r <Rewt0r@users.noreply.github.com>
rumpelsepp <stefan@sevenbyte.org>
rumpelsepp <rumpelsepp@sevenbyte.org>
scienmind <scintertech@cryptolab.net>
sciurius <jvromans@squirrel.nl>
seehuhn <voss@seehuhn.de>
simplypeachy <aD@simplypeachy.co.uk>
simplypeachy <simplypeachy@users.noreply.github.com>
snnd <dw@risu.io>
Stefan-Code <stefan.github@gmail.com>
Stefan-Code <Stefan.github@gmail.com>
timabell <tim@timwise.co.uk>
tnn2 <tnn@nygren.pp.se>
tojrobinson <tully@tojr.org>
tpng <benny.tpng@gmail.com>
tylerbrazier <tyler@tylerbrazier.com>
uok <ueomkail@gmail.com>
uok <uok@users.noreply.github.com>
veeti <veeti.paananen@rojekti.fi>
Vilbrekin <vilbrekin@gmail.com>
wkennington <william@wkennington.com>
WSGCSysadmin <e.meitner@willystreet.coop>
wweich <wweich@users.noreply.github.com>
wweich <wweich@gmx.de>
xduugu <cedric@gmx.ca>
Zillode <zillode@zillode.be>
zukoo <fxgsell@gmail.com>
mateon1 <matin1111@wp.pl>
mogwa1 <devriesb@gmail.com>
moshen <moshen.colin@gmail.com>
Moter8 <moter8@gmail.com>
mvdan <mvdan@mvdan.cc>
norgeous <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@users.noreply.github.com>
nrm21 <natemorrison@gmail.com>
Nutomic <me@nutomic.com>
pascalj <github@pascalj.com> <mail@pascal-jungblut.com>
peterhoeg <peter@speartail.com>
philips <brandon@ifup.org>
piobpl <piotrb10@gmail.com>
plouj <ploujj@gmail.com>
pluby <phill.luby@newredo.com>
pyfisch <pyfisch@gmail.com>
qbit <qbit@deftly.net>
ralder <ralder@yandex.ru>
Rewt0r <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
rumpelsepp <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
sciurius <jvromans@squirrel.nl>
seehuhn <voss@seehuhn.de>
simplypeachy <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
snnd <dw@risu.io>
Stefan-Code <stefan.github@gmail.com> <Stefan.github@gmail.com>
timabell <tim@timwise.co.uk>
tnn2 <tnn@nygren.pp.se>
tojrobinson <tully@tojr.org>
tpng <benny.tpng@gmail.com>
tylerbrazier <tyler@tylerbrazier.com>
uok <ueomkail@gmail.com> <uok@users.noreply.github.com>
veeti <veeti.paananen@rojekti.fi>
Vilbrekin <vilbrekin@gmail.com>
wkennington <william@wkennington.com>
wsgcsysadmin <e.meitner@willystreet.coo>
wweich <wweich@users.noreply.github.com> <wweich@gmx.de>
Zillode <zillode@zillode.be>
zukoo <fxgsell@gmail.com>

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

226
build.go
View File

@@ -39,7 +39,6 @@ var (
version string
goVersion float64
race bool
debug = os.Getenv("BUILDDEBUG") != ""
)
type target struct {
@@ -48,7 +47,6 @@ type target struct {
binaryName string
archiveFiles []archiveFile
debianFiles []archiveFile
tags []string
}
type archiveFile struct {
@@ -62,7 +60,6 @@ var targets = map[string]target{
// Only valid for the "build" and "install" commands as it lacks all
// the archive creation stuff.
buildPkg: "./cmd/...",
tags: []string{"purego"},
},
"syncthing": {
// The default target for "build", "install", "tar", "zip", "deb", etc.
@@ -96,41 +93,6 @@ var targets = map[string]target{
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
},
},
"discosrv": {
name: "discosrv",
buildPkg: "./cmd/discosrv",
binaryName: "discosrv", // .exe will be added automatically for Windows builds
archiveFiles: []archiveFile{
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
{src: "cmd/discosrv/README.md", dst: "README.txt", perm: 0644},
{src: "cmd/discosrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
},
debianFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "cmd/discosrv/README.md", dst: "deb/usr/share/doc/discosrv/README.txt", perm: 0644},
{src: "cmd/discosrv/LICENSE", dst: "deb/usr/share/doc/discosrv/LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "deb/usr/share/doc/discosrv/AUTHORS.txt", perm: 0644},
},
tags: []string{"purego"},
},
"relaysrv": {
name: "relaysrv",
buildPkg: "./cmd/relaysrv",
binaryName: "relaysrv", // .exe will be added automatically for Windows builds
archiveFiles: []archiveFile{
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
{src: "cmd/relaysrv/README.md", dst: "README.txt", perm: 0644},
{src: "cmd/relaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
},
debianFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "cmd/relaysrv/README.md", dst: "deb/usr/share/doc/relaysrv/README.txt", perm: 0644},
{src: "cmd/relaysrv/LICENSE", dst: "deb/usr/share/doc/relaysrv/LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "deb/usr/share/doc/relaysrv/AUTHORS.txt", perm: 0644},
},
},
}
func init() {
@@ -155,15 +117,16 @@ func main() {
log.SetOutput(os.Stdout)
log.SetFlags(0)
if debug {
t0 := time.Now()
defer func() {
log.Println("... build completed in", time.Since(t0))
}()
}
// If GOPATH isn't set, set it correctly with the assumption that we are
// in $GOPATH/src/github.com/syncthing/syncthing.
if os.Getenv("GOPATH") == "" {
setGoPath()
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
gopath := filepath.Clean(filepath.Join(cwd, "../../../../"))
log.Println("GOPATH is", gopath)
os.Setenv("GOPATH", gopath)
}
// We use Go 1.5+ vendoring.
@@ -173,44 +136,51 @@ func main() {
// might have installed during "build.go setup".
os.Setenv("PATH", fmt.Sprintf("%s%cbin%c%s", os.Getenv("GOPATH"), os.PathSeparator, os.PathListSeparator, os.Getenv("PATH")))
parseFlags()
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
flag.BoolVar(&noupgrade, "no-upgrade", noupgrade, "Disable upgrade functionality")
flag.StringVar(&version, "version", getVersion(), "Set compiled in version string")
flag.BoolVar(&race, "race", race, "Use race detector")
flag.Parse()
checkArchitecture()
goVersion, _ = checkRequiredGoVersion()
// Invoking build.go with no parameters at all builds everything (incrementally),
// which is what you want for maximum error checking during development.
if flag.NArg() == 0 {
runCommand("install", targets["all"])
runCommand("vet", target{})
runCommand("lint", target{})
} else {
// with any command given but not a target, the target is
// "syncthing". So "go run build.go install" is "go run build.go install
// syncthing" etc.
targetName := "syncthing"
if flag.NArg() > 1 {
targetName = flag.Arg(1)
}
target, ok := targets[targetName]
if !ok {
log.Fatalln("Unknown target", target)
}
runCommand(flag.Arg(0), target)
}
}
func checkArchitecture() {
switch goarch {
case "386", "amd64", "arm", "arm64", "ppc64", "ppc64le":
break
default:
log.Printf("Unknown goarch %q; proceed with caution!", goarch)
}
}
func runCommand(cmd string, target target) {
goVersion, _ = checkRequiredGoVersion()
// Invoking build.go with no parameters at all is equivalent to "go run
// build.go install all" as that builds everything (incrementally),
// which is what you want for maximum error checking during development.
if flag.NArg() == 0 {
var tags []string
if noupgrade {
tags = []string{"noupgrade"}
}
install(targets["all"], tags)
vet("cmd", "lib")
lint("./cmd/...")
lint("./lib/...")
return
}
// Otherwise, with any command given but not a target, the target is
// "syncthing". So "go run build.go install" is "go run build.go install
// syncthing" etc.
targetName := "syncthing"
if flag.NArg() > 1 {
targetName = flag.Arg(1)
}
target, ok := targets[targetName]
if !ok {
log.Fatalln("Unknown target", target)
}
cmd := flag.Arg(0)
switch cmd {
case "setup":
setup()
@@ -260,48 +230,17 @@ func runCommand(cmd string, target target) {
clean()
case "vet":
vet("build.go")
vet("cmd", "lib")
case "lint":
lint(".")
lint("./cmd/...")
lint("./lib/...")
case "metalint":
if isGometalinterInstalled() {
dirs := []string{".", "./cmd/...", "./lib/..."}
gometalinter("deadcode", dirs, "test/util.go")
gometalinter("structcheck", dirs)
gometalinter("varcheck", dirs)
}
default:
log.Fatalf("Unknown command %q", cmd)
}
}
// setGoPath sets GOPATH correctly with the assumption that we are
// in $GOPATH/src/github.com/syncthing/syncthing.
func setGoPath() {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
gopath := filepath.Clean(filepath.Join(cwd, "../../../../"))
log.Println("GOPATH is", gopath)
os.Setenv("GOPATH", gopath)
}
func parseFlags() {
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
flag.BoolVar(&noupgrade, "no-upgrade", noupgrade, "Disable upgrade functionality")
flag.StringVar(&version, "version", getVersion(), "Set compiled in version string")
flag.BoolVar(&race, "race", race, "Use race detector")
flag.Parse()
}
func checkRequiredGoVersion() (float64, bool) {
re := regexp.MustCompile(`go(\d+\.\d+)`)
ver := runtime.Version()
@@ -332,8 +271,6 @@ func setup() {
runPrint("go", "get", "-v", "github.com/axw/gocov/gocov")
runPrint("go", "get", "-v", "github.com/AlekSi/gocov-xml")
runPrint("go", "get", "-v", "bitbucket.org/tebeka/go2xunit")
runPrint("go", "get", "-v", "github.com/alecthomas/gometalinter")
runPrint("go", "get", "-v", "github.com/mitchellh/go-wordwrap")
}
func test(pkgs ...string) {
@@ -347,9 +284,9 @@ func test(pkgs ...string) {
}
if useRace {
runPrint("go", append([]string{"test", "-short", "-race", "-timeout", "60s", "-tags", "purego"}, pkgs...)...)
runPrint("go", append([]string{"test", "-short", "-race", "-timeout", "60s"}, pkgs...)...)
} else {
runPrint("go", append([]string{"test", "-short", "-timeout", "60s", "-tags", "purego"}, pkgs...)...)
runPrint("go", append([]string{"test", "-short", "-timeout", "60s"}, pkgs...)...)
}
}
@@ -361,8 +298,6 @@ func bench(pkgs ...string) {
func install(target target, tags []string) {
lazyRebuildAssets()
tags = append(target.tags, tags...)
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
@@ -370,7 +305,7 @@ func install(target target, tags []string) {
os.Setenv("GOBIN", filepath.Join(cwd, "bin"))
args := []string{"install", "-v", "-ldflags", ldflags()}
if len(tags) > 0 {
args = append(args, "-tags", strings.Join(tags, " "))
args = append(args, "-tags", strings.Join(tags, ","))
}
if race {
args = append(args, "-race")
@@ -385,12 +320,10 @@ func install(target target, tags []string) {
func build(target target, tags []string) {
lazyRebuildAssets()
tags = append(target.tags, tags...)
rmr(target.binaryName)
args := []string{"build", "-i", "-v", "-ldflags", ldflags()}
if len(tags) > 0 {
args = append(args, "-tags", strings.Join(tags, " "))
args = append(args, "-tags", strings.Join(tags, ","))
}
if race {
args = append(args, "-race")
@@ -488,7 +421,7 @@ func buildDeb(target target) {
"date": time.Now().Format(time.RFC1123),
}
debTemplateFiles := append(listFiles("debtpl/common"), listFiles("debtpl/"+target.name)...)
debTemplateFiles := append(listFiles("debian/common"), listFiles("debian/"+target.name)...)
for _, file := range debTemplateFiles {
tpl, err := template.New(filepath.Base(file)).ParseFiles(file)
if err != nil {
@@ -754,26 +687,13 @@ func archiveName(target target) string {
}
func runError(cmd string, args ...string) ([]byte, error) {
if debug {
t0 := time.Now()
log.Println("runError:", cmd, strings.Join(args, " "))
defer func() {
log.Println("... in", time.Since(t0))
}()
}
ecmd := exec.Command(cmd, args...)
bs, err := ecmd.CombinedOutput()
return bytes.TrimSpace(bs), err
}
func runPrint(cmd string, args ...string) {
if debug {
t0 := time.Now()
log.Println("runPrint:", cmd, strings.Join(args, " "))
defer func() {
log.Println("... in", time.Since(t0))
}()
}
log.Println(cmd, strings.Join(args, " "))
ecmd := exec.Command(cmd, args...)
ecmd.Stdout = os.Stdout
ecmd.Stderr = os.Stderr
@@ -784,13 +704,7 @@ func runPrint(cmd string, args ...string) {
}
func runPipe(file, cmd string, args ...string) {
if debug {
t0 := time.Now()
log.Println("runPipe:", cmd, strings.Join(args, " "))
defer func() {
log.Println("... in", time.Since(t0))
}()
}
log.Println(cmd, strings.Join(args, " "), ">", file)
fd, err := os.Create(file)
if err != nil {
log.Fatal(err)
@@ -879,7 +793,7 @@ func zipFile(out string, files []archiveFile) {
if err != nil {
log.Fatal(err)
}
fh.Name = filepath.ToSlash(f.dst)
fh.Name = f.dst
fh.Method = zip.Deflate
if strings.HasSuffix(f.dst, ".txt") {
@@ -938,6 +852,7 @@ func vet(dirs ...string) {
// A genuine error exit from the vet tool.
log.Fatal(err)
}
}
func lint(pkg string) {
@@ -986,34 +901,3 @@ func exitStatus(err error) int {
return -1
}
func isGometalinterInstalled() bool {
if _, err := runError("gometalinter", "--disable-all"); err != nil {
log.Println("gometalinter is not installed")
return false
}
return true
}
func gometalinter(linter string, dirs []string, excludes ...string) {
params := []string{"--disable-all"}
params = append(params, fmt.Sprintf("--deadline=%ds", 60))
params = append(params, "--enable="+linter)
for _, exclude := range excludes {
params = append(params, "--exclude="+exclude)
}
for _, dir := range dirs {
params = append(params, dir)
}
bs, err := runError("gometalinter", params...)
if len(bs) > 0 {
log.Printf("%s", bs)
}
if err != nil {
log.Printf("%v", err)
}
}

View File

@@ -1,19 +0,0 @@
Copyright (C) 2014-2015 The Discosrv Authors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,40 +0,0 @@
discosrv
========
[![Latest Build](http://img.shields.io/jenkins/s/http/build.syncthing.net/discosrv.svg?style=flat-square)](http://build.syncthing.net/job/discosrv/lastBuild/)
This is the global discovery server for the `syncthing` project.
To get it, run `go get github.com/syncthing/discosrv` or download the
[latest build](http://build.syncthing.net/job/discosrv/lastSuccessfulBuild/artifact/)
from the build server.
Usage
-----
The discovery server supports `ql` and `postgres` backends.
Specify the backend via `-db-backend` and the database DSN via `-db-dsn`.
By default it will use in-memory `ql` backend. If you wish to persist the
information on disk between restarts in `ql`, specify a file DSN:
```bash
$ discosrv -db-dsn="file:///var/run/discosrv.db"
```
For `postgres`, you will need to create a database and a user with permissions
to create tables in it, then start the discosrv as follows:
```bash
$ export DISCOSRV_DB_DSN="postgres://user:password@localhost/databasename"
$ discosrv -db-backend="postgres"
```
You can pass the DSN as command line option, but the value what you pass in will
be visible in most process managers, potentially exposing the database password
to other users.
In all cases, the appropriate tables and indexes will be created at first
startup. If it doesn't exit with an error, you're fine.
See `discosrv -help` for other options.

View File

@@ -1,75 +0,0 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
package main
import (
"database/sql"
"log"
"time"
)
type cleansrv struct {
intv time.Duration
db *sql.DB
prep map[string]*sql.Stmt
}
func (s *cleansrv) Serve() {
for {
time.Sleep(next(s.intv))
err := s.cleanOldEntries()
if err != nil {
log.Println("Clean:", err)
}
}
}
func (s *cleansrv) Stop() {
panic("stop unimplemented")
}
func (s *cleansrv) cleanOldEntries() (err error) {
var tx *sql.Tx
tx, err = s.db.Begin()
if err != nil {
return err
}
defer func() {
if err == nil {
err = tx.Commit()
} else {
tx.Rollback()
}
}()
res, err := tx.Stmt(s.prep["cleanAddress"]).Exec()
if err != nil {
return err
}
if rows, _ := res.RowsAffected(); rows > 0 {
log.Printf("Clean: %d old addresses", rows)
}
res, err = tx.Stmt(s.prep["cleanDevice"]).Exec()
if err != nil {
return err
}
if rows, _ := res.RowsAffected(); rows > 0 {
log.Printf("Clean: %d old devices", rows)
}
var devs, addrs int
row := tx.Stmt(s.prep["countDevice"]).QueryRow()
if err = row.Scan(&devs); err != nil {
return err
}
row = tx.Stmt(s.prep["countAddress"]).QueryRow()
if err = row.Scan(&addrs); err != nil {
return err
}
log.Printf("Database: %d devices, %d addresses", devs, addrs)
return nil
}

View File

@@ -1,32 +0,0 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
package main
import (
"database/sql"
"fmt"
)
type setupFunc func(db *sql.DB) error
type compileFunc func(db *sql.DB) (map[string]*sql.Stmt, error)
var (
setupFuncs = make(map[string]setupFunc)
compileFuncs = make(map[string]compileFunc)
)
func register(name string, setup setupFunc, compile compileFunc) {
setupFuncs[name] = setup
compileFuncs[name] = compile
}
func setup(backend string, db *sql.DB) (map[string]*sql.Stmt, error) {
setup, ok := setupFuncs[backend]
if !ok {
return nil, fmt.Errorf("Unsupported backend")
}
if err := setup(db); err != nil {
return nil, err
}
return compileFuncs[backend](db)
}

View File

@@ -1,141 +0,0 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
package main
import (
"crypto/tls"
"database/sql"
"flag"
"fmt"
"log"
"os"
"runtime"
"strconv"
"time"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/thejerf/suture"
)
const (
minNegCache = 60 // seconds
maxNegCache = 3600 // seconds
maxDeviceAge = 7 * 86400 // one week, in seconds
)
var (
Version string
BuildStamp string
BuildUser string
BuildHost string
BuildDate time.Time
LongVersion string
)
func init() {
stamp, _ := strconv.Atoi(BuildStamp)
BuildDate = time.Unix(int64(stamp), 0)
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
LongVersion = fmt.Sprintf(`discosrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
}
var (
lruSize = 10240
limitAvg = 5
limitBurst = 20
globalStats stats
statsFile string
backend = "ql"
dsn = getEnvDefault("DISCOSRV_DB_DSN", "memory://discosrv")
certFile = "cert.pem"
keyFile = "key.pem"
debug = false
useHTTP = false
)
func main() {
const (
cleanIntv = 1 * time.Hour
statsIntv = 5 * time.Minute
)
var listen string
log.SetOutput(os.Stdout)
log.SetFlags(0)
flag.StringVar(&listen, "listen", ":8443", "Listen address")
flag.IntVar(&lruSize, "limit-cache", lruSize, "Limiter cache entries")
flag.IntVar(&limitAvg, "limit-avg", limitAvg, "Allowed average package rate, per 10 s")
flag.IntVar(&limitBurst, "limit-burst", limitBurst, "Allowed burst size, packets")
flag.StringVar(&statsFile, "stats-file", statsFile, "File to write periodic operation stats to")
flag.StringVar(&backend, "db-backend", backend, "Database backend to use")
flag.StringVar(&dsn, "db-dsn", dsn, "Database DSN")
flag.StringVar(&certFile, "cert", certFile, "Certificate file")
flag.StringVar(&keyFile, "key", keyFile, "Key file")
flag.BoolVar(&debug, "debug", debug, "Debug")
flag.BoolVar(&useHTTP, "http", useHTTP, "Listen on HTTP (behind an HTTPS proxy)")
flag.Parse()
log.Println(LongVersion)
var cert tls.Certificate
var err error
if !useHTTP {
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatalln("Failed to load X509 key pair:", err)
}
devID := protocol.NewDeviceID(cert.Certificate[0])
log.Println("Server device ID is", devID)
}
db, err := sql.Open(backend, dsn)
if err != nil {
log.Fatalln("sql.Open:", err)
}
prep, err := setup(backend, db)
if err != nil {
log.Fatalln("Setup:", err)
}
main := suture.NewSimple("main")
main.Add(&querysrv{
addr: listen,
cert: cert,
db: db,
prep: prep,
})
main.Add(&cleansrv{
intv: cleanIntv,
db: db,
prep: prep,
})
main.Add(&statssrv{
intv: statsIntv,
file: statsFile,
db: db,
})
globalStats.Reset()
main.Serve()
}
func getEnvDefault(key, def string) string {
if val := os.Getenv(key); val != "" {
return val
}
return def
}
func next(intv time.Duration) time.Duration {
t0 := time.Now()
t1 := t0.Add(intv).Truncate(intv)
return t1.Sub(t0)
}

View File

@@ -1,97 +0,0 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
package main
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
func init() {
register("postgres", postgresSetup, postgresCompile)
}
func postgresSetup(db *sql.DB) error {
var err error
db.SetMaxIdleConns(4)
db.SetMaxOpenConns(8)
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS Devices (
DeviceID CHAR(63) NOT NULL PRIMARY KEY,
Seen TIMESTAMP NOT NULL
)`)
if err != nil {
return err
}
row := db.QueryRow(`SELECT 'DevicesDeviceIDIndex'::regclass`)
if err := row.Scan(nil); err != nil {
_, err = db.Exec(`CREATE INDEX DevicesDeviceIDIndex ON Devices (DeviceID)`)
}
if err != nil {
return err
}
row = db.QueryRow(`SELECT 'DevicesSeenIndex'::regclass`)
if err := row.Scan(nil); err != nil {
_, err = db.Exec(`CREATE INDEX DevicesSeenIndex ON Devices (Seen)`)
}
if err != nil {
return err
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS Addresses (
DeviceID CHAR(63) NOT NULL,
Seen TIMESTAMP NOT NULL,
Address VARCHAR(256) NOT NULL
)`)
if err != nil {
return err
}
row = db.QueryRow(`SELECT 'AddressesDeviceIDSeenIndex'::regclass`)
if err := row.Scan(nil); err != nil {
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDSeenIndex ON Addresses (DeviceID, Seen)`)
}
if err != nil {
return err
}
row = db.QueryRow(`SELECT 'AddressesDeviceIDAddressIndex'::regclass`)
if err := row.Scan(nil); err != nil {
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDAddressIndex ON Addresses (DeviceID, Address)`)
}
if err != nil {
return err
}
return nil
}
func postgresCompile(db *sql.DB) (map[string]*sql.Stmt, error) {
stmts := map[string]string{
"cleanAddress": "DELETE FROM Addresses WHERE Seen < now() - '2 hour'::INTERVAL",
"cleanDevice": fmt.Sprintf("DELETE FROM Devices WHERE Seen < now() - '%d hour'::INTERVAL", maxDeviceAge/3600),
"countAddress": "SELECT count(*) FROM Addresses",
"countDevice": "SELECT count(*) FROM Devices",
"insertAddress": "INSERT INTO Addresses (DeviceID, Seen, Address) VALUES ($1, now(), $2)",
"insertDevice": "INSERT INTO Devices (DeviceID, Seen) VALUES ($1, now())",
"selectAddress": "SELECT Address FROM Addresses WHERE DeviceID=$1 AND Seen > now() - '1 hour'::INTERVAL ORDER BY random() LIMIT 16",
"selectDevice": "SELECT Seen FROM Devices WHERE DeviceID=$1",
"updateAddress": "UPDATE Addresses SET Seen=now() WHERE DeviceID=$1 AND Address=$2",
"updateDevice": "UPDATE Devices SET Seen=now() WHERE DeviceID=$1",
}
res := make(map[string]*sql.Stmt, len(stmts))
for key, stmt := range stmts {
prep, err := db.Prepare(stmt)
if err != nil {
return nil, err
}
res[key] = prep
}
return res, nil
}

View File

@@ -1,81 +0,0 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
package main
import (
"database/sql"
"fmt"
"log"
"github.com/cznic/ql"
)
func init() {
ql.RegisterDriver()
register("ql", qlSetup, qlCompile)
}
func qlSetup(db *sql.DB) (err error) {
tx, err := db.Begin()
if err != nil {
return
}
defer func() {
if err == nil {
err = tx.Commit()
} else {
tx.Rollback()
}
}()
_, err = tx.Exec(`CREATE TABLE IF NOT EXISTS Devices (
DeviceID STRING NOT NULL,
Seen TIME NOT NULL
)`)
if err != nil {
return
}
if _, err = tx.Exec(`CREATE INDEX IF NOT EXISTS DevicesDeviceIDIndex ON Devices (DeviceID)`); err != nil {
return
}
_, err = tx.Exec(`CREATE TABLE IF NOT EXISTS Addresses (
DeviceID STRING NOT NULL,
Seen TIME NOT NULL,
Address STRING NOT NULL,
)`)
if err != nil {
return
}
_, err = tx.Exec(`CREATE INDEX IF NOT EXISTS AddressesDeviceIDAddressIndex ON Addresses (DeviceID, Address)`)
return
}
func qlCompile(db *sql.DB) (map[string]*sql.Stmt, error) {
stmts := map[string]string{
"cleanAddress": `DELETE FROM Addresses WHERE Seen < now() - duration("2h")`,
"cleanDevice": fmt.Sprintf(`DELETE FROM Devices WHERE Seen < now() - duration("%dh")`, maxDeviceAge/3600),
"countAddress": "SELECT count(*) FROM Addresses",
"countDevice": "SELECT count(*) FROM Devices",
"insertAddress": "INSERT INTO Addresses (DeviceID, Seen, Address) VALUES ($1, now(), $2)",
"insertDevice": "INSERT INTO Devices (DeviceID, Seen) VALUES ($1, now())",
"selectAddress": `SELECT Address from Addresses WHERE DeviceID==$1 AND Seen > now() - duration("1h") LIMIT 16`,
"selectDevice": "SELECT Seen FROM Devices WHERE DeviceID==$1",
"updateAddress": "UPDATE Addresses Seen=now() WHERE DeviceID==$1 AND Address==$2",
"updateDevice": "UPDATE Devices Seen=now() WHERE DeviceID==$1",
}
res := make(map[string]*sql.Stmt, len(stmts))
for key, stmt := range stmts {
prep, err := db.Prepare(stmt)
if err != nil {
log.Println("Failed to compile", stmt)
return nil, err
}
res[key] = prep
}
return res, nil
}

View File

@@ -1,478 +0,0 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
package main
import (
"bytes"
"crypto/tls"
"database/sql"
"encoding/json"
"encoding/pem"
"fmt"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"strconv"
"sync"
"time"
"github.com/golang/groupcache/lru"
"github.com/juju/ratelimit"
"github.com/syncthing/syncthing/lib/protocol"
"golang.org/x/net/context"
)
type querysrv struct {
addr string
db *sql.DB
prep map[string]*sql.Stmt
limiter *safeCache
cert tls.Certificate
listener net.Listener
}
type announcement struct {
Seen time.Time `json:"seen"`
Addresses []string `json:"addresses"`
}
type safeCache struct {
*lru.Cache
mut sync.Mutex
}
func (s *safeCache) Get(key string) (val interface{}, ok bool) {
s.mut.Lock()
val, ok = s.Cache.Get(key)
s.mut.Unlock()
return
}
func (s *safeCache) Add(key string, val interface{}) {
s.mut.Lock()
s.Cache.Add(key, val)
s.mut.Unlock()
}
type requestID int64
func (i requestID) String() string {
return fmt.Sprintf("%016x", int64(i))
}
func negCacheFor(lastSeen time.Time) int {
since := time.Since(lastSeen).Seconds()
if since >= maxDeviceAge {
return maxNegCache
}
if since < 0 {
// That's weird
return minNegCache
}
// Return a value linearly scaled from minNegCache (at zero seconds ago)
// to maxNegCache (at maxDeviceAge seconds ago).
r := since / maxDeviceAge
return int(minNegCache + r*(maxNegCache-minNegCache))
}
func (s *querysrv) Serve() {
s.limiter = &safeCache{
Cache: lru.New(lruSize),
}
if useHTTP {
listener, err := net.Listen("tcp", s.addr)
if err != nil {
log.Println("Listen:", err)
return
}
s.listener = listener
} else {
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{s.cert},
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true,
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
},
}
tlsListener, err := tls.Listen("tcp", s.addr, tlsCfg)
if err != nil {
log.Println("Listen:", err)
return
}
s.listener = tlsListener
}
http.HandleFunc("/v2/", s.handler)
http.HandleFunc("/ping", handlePing)
srv := &http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
MaxHeaderBytes: 1 << 10,
}
if err := srv.Serve(s.listener); err != nil {
log.Println("Serve:", err)
}
}
var topCtx = context.Background()
func (s *querysrv) handler(w http.ResponseWriter, req *http.Request) {
reqID := requestID(rand.Int63())
ctx := context.WithValue(topCtx, "id", reqID)
if debug {
log.Println(reqID, req.Method, req.URL)
}
t0 := time.Now()
defer func() {
diff := time.Since(t0)
var comment string
if diff > time.Second {
comment = "(very slow request)"
} else if diff > 100*time.Millisecond {
comment = "(slow request)"
}
if comment != "" || debug {
log.Println(reqID, req.Method, req.URL, "completed in", diff, comment)
}
}()
var remoteIP net.IP
if useHTTP {
remoteIP = net.ParseIP(req.Header.Get("X-Forwarded-For"))
} else {
addr, err := net.ResolveTCPAddr("tcp", req.RemoteAddr)
if err != nil {
log.Println("remoteAddr:", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
remoteIP = addr.IP
}
if s.limit(remoteIP) {
if debug {
log.Println(remoteIP, "is limited")
}
w.Header().Set("Retry-After", "60")
http.Error(w, "Too Many Requests", 429)
return
}
switch req.Method {
case "GET":
s.handleGET(ctx, w, req)
case "POST":
s.handlePOST(ctx, remoteIP, w, req)
default:
globalStats.Error()
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
}
func (s *querysrv) handleGET(ctx context.Context, w http.ResponseWriter, req *http.Request) {
reqID := ctx.Value("id").(requestID)
deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device"))
if err != nil {
if debug {
log.Println(reqID, "bad device param")
}
globalStats.Error()
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
var ann announcement
ann.Seen, err = s.getDeviceSeen(deviceID)
negCache := strconv.Itoa(negCacheFor(ann.Seen))
w.Header().Set("Retry-After", negCache)
w.Header().Set("Cache-Control", "public, max-age="+negCache)
if err != nil {
// The device is not in the database.
globalStats.Query()
http.Error(w, "Not Found", http.StatusNotFound)
return
}
t0 := time.Now()
ann.Addresses, err = s.getAddresses(ctx, deviceID)
if err != nil {
log.Println(reqID, "getAddresses:", err)
globalStats.Error()
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
if debug {
log.Println(reqID, "getAddresses in", time.Since(t0))
}
globalStats.Query()
if len(ann.Addresses) == 0 {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
globalStats.Answer()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ann)
}
func (s *querysrv) handlePOST(ctx context.Context, remoteIP net.IP, w http.ResponseWriter, req *http.Request) {
reqID := ctx.Value("id").(requestID)
rawCert := certificateBytes(req)
if rawCert == nil {
if debug {
log.Println(reqID, "no certificates")
}
globalStats.Error()
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var ann announcement
if err := json.NewDecoder(req.Body).Decode(&ann); err != nil {
if debug {
log.Println(reqID, "decode:", err)
}
globalStats.Error()
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
deviceID := protocol.NewDeviceID(rawCert)
// handleAnnounce returns *two* errors. The first indicates a problem with
// something the client posted to us. We should return a 400 Bad Request
// and not worry about it. The second indicates that the request was fine,
// but something internal messed up. We should log it and respond with a
// more apologetic 500 Internal Server Error.
userErr, internalErr := s.handleAnnounce(ctx, remoteIP, deviceID, ann.Addresses)
if userErr != nil {
if debug {
log.Println(reqID, "handleAnnounce:", userErr)
}
globalStats.Error()
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if internalErr != nil {
log.Println(reqID, "handleAnnounce:", internalErr)
globalStats.Error()
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
globalStats.Announce()
// TODO: Slowly increase this for stable clients
w.Header().Set("Reannounce-After", "1800")
// We could return the lookup result here, but it's kind of unnecessarily
// expensive to go query the database again so we let the client decide to
// do a lookup if they really care.
w.WriteHeader(http.StatusNoContent)
}
func (s *querysrv) Stop() {
s.listener.Close()
}
func (s *querysrv) handleAnnounce(ctx context.Context, remote net.IP, deviceID protocol.DeviceID, addresses []string) (userErr, internalErr error) {
reqID := ctx.Value("id").(requestID)
tx, err := s.db.Begin()
if err != nil {
internalErr = err
return
}
defer func() {
// Since we return from a bunch of different places, we handle
// rollback in the defer.
if internalErr != nil || userErr != nil {
tx.Rollback()
}
}()
for _, annAddr := range addresses {
uri, err := url.Parse(annAddr)
if err != nil {
userErr = err
return
}
host, port, err := net.SplitHostPort(uri.Host)
if err != nil {
userErr = err
return
}
ip := net.ParseIP(host)
if host == "" || ip.IsUnspecified() {
host = remote.String()
}
uri.Host = net.JoinHostPort(host, port)
if err := s.updateAddress(ctx, tx, deviceID, uri.String()); err != nil {
internalErr = err
return
}
}
if err := s.updateDevice(ctx, tx, deviceID); err != nil {
internalErr = err
return
}
t0 := time.Now()
internalErr = tx.Commit()
if debug {
log.Println(reqID, "commit in", time.Since(t0))
}
return
}
func (s *querysrv) limit(remote net.IP) bool {
key := remote.String()
bkt, ok := s.limiter.Get(key)
if ok {
bkt := bkt.(*ratelimit.Bucket)
if bkt.TakeAvailable(1) != 1 {
// Rate limit exceeded; ignore packet
return true
}
} else {
// One packet per ten seconds average rate, burst ten packets
s.limiter.Add(key, ratelimit.NewBucket(10*time.Second/time.Duration(limitAvg), int64(limitBurst)))
}
return false
}
func (s *querysrv) updateDevice(ctx context.Context, tx *sql.Tx, device protocol.DeviceID) error {
reqID := ctx.Value("id").(requestID)
t0 := time.Now()
res, err := tx.Stmt(s.prep["updateDevice"]).Exec(device.String())
if err != nil {
return err
}
if debug {
log.Println(reqID, "updateDevice in", time.Since(t0))
}
if rows, _ := res.RowsAffected(); rows == 0 {
t0 = time.Now()
_, err := tx.Stmt(s.prep["insertDevice"]).Exec(device.String())
if err != nil {
return err
}
if debug {
log.Println(reqID, "insertDevice in", time.Since(t0))
}
}
return nil
}
func (s *querysrv) updateAddress(ctx context.Context, tx *sql.Tx, device protocol.DeviceID, uri string) error {
res, err := tx.Stmt(s.prep["updateAddress"]).Exec(device.String(), uri)
if err != nil {
return err
}
if rows, _ := res.RowsAffected(); rows == 0 {
_, err := tx.Stmt(s.prep["insertAddress"]).Exec(device.String(), uri)
if err != nil {
return err
}
}
return nil
}
func (s *querysrv) getAddresses(ctx context.Context, device protocol.DeviceID) ([]string, error) {
rows, err := s.prep["selectAddress"].Query(device.String())
if err != nil {
return nil, err
}
defer rows.Close()
var res []string
for rows.Next() {
var addr string
err := rows.Scan(&addr)
if err != nil {
log.Println("Scan:", err)
continue
}
res = append(res, addr)
}
return res, nil
}
func (s *querysrv) getDeviceSeen(device protocol.DeviceID) (time.Time, error) {
row := s.prep["selectDevice"].QueryRow(device.String())
var seen time.Time
if err := row.Scan(&seen); err != nil {
return time.Time{}, err
}
return seen, nil
}
func handlePing(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(204)
}
func certificateBytes(req *http.Request) []byte {
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
return req.TLS.PeerCertificates[0].Raw
}
if hdr := req.Header.Get("X-SSL-Cert"); hdr != "" {
bs := []byte(hdr)
// The certificate is in PEM format but with spaces for newlines. We
// need to reinstate the newlines for the PEM decoder. But we need to
// leave the spaces in the BEGIN and END lines - the first and last
// space - alone.
firstSpace := bytes.Index(bs, []byte(" "))
lastSpace := bytes.LastIndex(bs, []byte(" "))
for i := firstSpace + 1; i < lastSpace; i++ {
if bs[i] == ' ' {
bs[i] = '\n'
}
}
block, _ := pem.Decode(bs)
if block == nil {
// Decoding failed
return nil
}
return block.Bytes
}
return nil
}

View File

@@ -1,141 +0,0 @@
// Copyright (C) 2014-2015 Jakob Borg and Contributors (see the CONTRIBUTORS file).
package main
import (
"bytes"
"database/sql"
"fmt"
"io/ioutil"
"log"
"os"
"sync/atomic"
"time"
)
type stats struct {
// Incremented atomically
announces int64
queries int64
answers int64
errors int64
}
func (s *stats) Announce() {
atomic.AddInt64(&s.announces, 1)
}
func (s *stats) Query() {
atomic.AddInt64(&s.queries, 1)
}
func (s *stats) Answer() {
atomic.AddInt64(&s.answers, 1)
}
func (s *stats) Error() {
atomic.AddInt64(&s.errors, 1)
}
// Reset returns a copy of the current stats and resets the counters to
// zero.
func (s *stats) Reset() stats {
// Create a copy of the stats using atomic reads
copy := stats{
announces: atomic.LoadInt64(&s.announces),
queries: atomic.LoadInt64(&s.queries),
answers: atomic.LoadInt64(&s.answers),
errors: atomic.LoadInt64(&s.errors),
}
// Reset the stats by subtracting the values that we copied
atomic.AddInt64(&s.announces, -copy.announces)
atomic.AddInt64(&s.queries, -copy.queries)
atomic.AddInt64(&s.answers, -copy.answers)
atomic.AddInt64(&s.errors, -copy.errors)
return copy
}
type statssrv struct {
intv time.Duration
file string
db *sql.DB
}
func (s *statssrv) Serve() {
lastReset := time.Now()
for {
time.Sleep(next(s.intv))
stats := globalStats.Reset()
d := time.Since(lastReset).Seconds()
lastReset = time.Now()
log.Printf("Stats: %.02f announces/s, %.02f queries/s, %.02f answers/s, %.02f errors/s",
float64(stats.announces)/d, float64(stats.queries)/d, float64(stats.answers)/d, float64(stats.errors)/d)
if s.file != "" {
s.writeToFile(stats, d)
}
}
}
func (s *statssrv) Stop() {
panic("stop unimplemented")
}
func (s *statssrv) writeToFile(stats stats, secs float64) {
newLine := []byte("\n")
var addrs int
row := s.db.QueryRow("SELECT COUNT(*) FROM Addresses")
if err := row.Scan(&addrs); err != nil {
log.Println("stats query:", err)
return
}
fd, err := os.OpenFile(s.file, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Println("stats file:", err)
return
}
defer func() {
err = fd.Close()
if err != nil {
log.Println("stats file:", err)
}
}()
bs, err := ioutil.ReadAll(fd)
if err != nil {
log.Println("stats file:", err)
return
}
lines := bytes.Split(bytes.TrimSpace(bs), newLine)
if len(lines) > 12 {
lines = lines[len(lines)-12:]
}
latest := fmt.Sprintf("%v: %6d addresses, %8.02f announces/s, %8.02f queries/s, %8.02f answers/s, %8.02f errors/s\n",
time.Now().UTC().Format(time.RFC3339), addrs,
float64(stats.announces)/secs, float64(stats.queries)/secs, float64(stats.answers)/secs, float64(stats.errors)/secs)
lines = append(lines, []byte(latest))
_, err = fd.Seek(0, 0)
if err != nil {
log.Println("stats file:", err)
return
}
err = fd.Truncate(0)
if err != nil {
log.Println("stats file:", err)
return
}
_, err = fd.Write(bytes.Join(lines, newLine))
if err != nil {
log.Println("stats file:", err)
return
}
}

View File

@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 The Syncthing Project
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,141 +0,0 @@
relaysrv
========
[![Latest Build](http://img.shields.io/jenkins/s/http/build.syncthing.net/relaysrv.svg?style=flat-square)](http://build.syncthing.net/job/relaysrv/lastBuild/)
This is the relay server for the `syncthing` project.
To get it, run `go get github.com/syncthing/relaysrv` or download the
[latest build](http://build.syncthing.net/job/relaysrv/lastSuccessfulBuild/artifact/)
from the build server.
:exclamation:Warnings:exclamation: - Read or regret
-----
By default, all relay servers will join the default public relay pool, which means that the relay server will be availble for public use, and **will consume your bandwidth** helping others to connect.
If you wish to disable this behaviour, please specify `-pools=""` argument.
Please note that `relaysrv` is only usable by `syncthing` **version v0.12 and onwards**.
To run `relaysrv` you need to have port 22067 available to the internet, which means you might need to allow it through your firewall if you **have a public IP, or setup a port-forwarding** (22067 to 22067) if you are behind a router.
Furthermore, **by default relaysrv will also expose a /status HTTP endpoint on port 22070**, which is used by the pool servers to peek at metrics of the relaysrv, such as what are the current transfer rates, how many clients are connected, etc, etc. If you wish this information to be available, similarlly you might want to allow it through your firewall, or port-forward it (22070 to 22070) on your NAT device.
This is **not mandatory** for the relaysrv to function, and is used only to gather metrics and present them in the overview page of the pool server, displaying stats about the specific relay.
At the point of writing the endpoint output looks as follows:
```
{
"bytesProxied": 0,
"goArch": "amd64",
"goMaxProcs": 1,
"goNumRoutine": 13,
"goOS": "linux",
"goVersion": "go1.6",
"kbps10s1m5m15m30m60m": [
0,
0,
0,
0,
0,
0
],
"numActiveSessions": 0,
"numConnections": 0,
"numPendingSessionKeys": 2,
"numProxies": 0,
"options": {
"global-rate": 0,
"message-timeout": 60,
"network-timeout": 120,
"per-session-rate": 0,
"ping-interval": 60,
"pools": [
"https://relays.syncthing.net/endpoint"
],
"provided-by": ""
},
"startTime": "2016-03-06T12:53:07.090847749-05:00",
"uptimeSeconds": 17
}
```
If you wish to disable the /status endpoint, provide `-status-srv=""` as one of the arguments when starting the relaysrv.
Running for public use
----
Make sure you have a public IP with port 22067 open, or make sure you have port-forwarding (22067 to 22067) if you are behind a router.
Run the `relaysrv` with no arguments (or `-debug` if you want more output), and that should be enough for the server to join the public relay pool.
You should see a message saying:
```
2015/09/21 22:45:46 pool.go:60: Joined https://relays.syncthing.net/endpoint rejoining in 48m0s
```
See `relaysrv -help` for other options, such as rate limits, timeout intervals, etc.
Running for private use
-----
Once you've started the `relaysrv`, it will generate a key pair and print an URI:
```bash
relay://:22067/?id=EZQOIDM-6DDD4ZI-DJ65NSM-4OQWRAT-EIKSMJO-OZ552BO-WQZEGYY-STS5RQM&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070
```
This URI contains partial address of the relay server, as well as it's options which in the future may be taken into account when choosing the best suitable relay out of multiple available.
Because `-listen` option was not used, the `relaysrv` does not know it's external IP, therefore you should replace the host part of the URI with your public IP address on which the `relaysrv` will be available:
```bash
relay://123.123.123.123:22067/?id=EZQOIDM-6DDD4ZI-DJ65NSM-4OQWRAT-EIKSMJO-OZ552BO-WQZEGYY-STS5RQM&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070
```
If you do not care about certificate pinning (improved security) or do not care about passing verbose settings to the clients, you can shorten the URL to just the host part:
```bash
relay://123.123.123.123:22067
```
This URI can then be used in `syncthing` as one of the relay servers.
See `relaysrv -help` for other options, such as rate limits, timeout intervals, etc.
Other items available in this repo
----
##### testutil
A test utility which can be used to test connectivity of a relay server.
You need to generate two x509 key pairs (key.pem and cert.pem), one for the client, another one for the server, in separate directories.
Afterwards, start the client:
```bash
./testutil -relay="relay://uri.of.relay" -keys=certs/client/ -join
```
This prints out the client ID:
```
2015/09/21 23:00:52 main.go:42: ID: BG2C5ZA-W7XPFDO-LH222Z6-65F3HJX-ADFTGRT-3SBFIGM-KV26O2Q-E5RMRQ2
```
In the other terminal run the following:
```bash
./testutil -relay="relay://uri.of.relay" -keys=certs/server/ -connect=BG2C5ZA-W7XPFDO-LH222Z6-65F3HJX-ADFTGRT-3SBFIGM-KV26O2Q-E5RMRQ2
```
Which should then give you an interactive prompt, where you can type things in one terminal, and they get relayed to the other terminal.
Relay related libraries used by this repo
----
##### Relay protocol definition.
[Available here](https://github.com/syncthing/syncthing/tree/master/lib/relay/protocol)
##### Relay client
Only used by the testutil.
[Available here](https://github.com/syncthing/syncthing/tree/master/lib/relay/client)

View File

@@ -1,17 +0,0 @@
[Unit]
Description=Syncthing relay server
After=network.target
[Service]
User=syncthing-relaysrv
Group=syncthing-relaysrv
ExecStart=/usr/bin/relaysrv
WorkingDirectory=/var/lib/syncthing-relaysrv
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target

View File

@@ -1,345 +0,0 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors.
package main
import (
"crypto/tls"
"encoding/hex"
"log"
"net"
"sync"
"sync/atomic"
"time"
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/relay/protocol"
)
var (
outboxesMut = sync.RWMutex{}
outboxes = make(map[syncthingprotocol.DeviceID]chan interface{})
numConnections int64
)
func listener(addr string, config *tls.Config) {
tcpListener, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalln(err)
}
listener := tlsutil.DowngradingListener{
Listener: tcpListener,
}
for {
conn, isTLS, err := listener.AcceptNoWrapTLS()
if err != nil {
if debug {
log.Println("Listener failed to accept connection from", conn.RemoteAddr(), ". Possibly a TCP Ping.")
}
continue
}
setTCPOptions(conn)
if debug {
log.Println("Listener accepted connection from", conn.RemoteAddr(), "tls", isTLS)
}
if isTLS {
go protocolConnectionHandler(conn, config)
} else {
go sessionConnectionHandler(conn)
}
}
}
func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
conn := tls.Server(tcpConn, config)
err := conn.Handshake()
if err != nil {
if debug {
log.Println("Protocol connection TLS handshake:", conn.RemoteAddr(), err)
}
conn.Close()
return
}
state := conn.ConnectionState()
if (!state.NegotiatedProtocolIsMutual || state.NegotiatedProtocol != protocol.ProtocolName) && debug {
log.Println("Protocol negotiation error")
}
certs := state.PeerCertificates
if len(certs) != 1 {
if debug {
log.Println("Certificate list error")
}
conn.Close()
return
}
id := syncthingprotocol.NewDeviceID(certs[0].Raw)
messages := make(chan interface{})
errors := make(chan error, 1)
outbox := make(chan interface{})
// Read messages from the connection and send them on the messages
// channel. When there is an error, send it on the error channel and
// return. Applies also when the connection gets closed, so the pattern
// below is to close the connection on error, then wait for the error
// signal from messageReader to exit.
go messageReader(conn, messages, errors)
pingTicker := time.NewTicker(pingInterval)
timeoutTicker := time.NewTimer(networkTimeout)
joined := false
for {
select {
case message := <-messages:
timeoutTicker.Reset(networkTimeout)
if debug {
log.Printf("Message %T from %s", message, id)
}
switch msg := message.(type) {
case protocol.JoinRelayRequest:
if atomic.LoadInt32(&overLimit) > 0 {
protocol.WriteMessage(conn, protocol.RelayFull{})
if debug {
log.Println("Refusing join request from", id, "due to being over limits")
}
conn.Close()
limitCheckTimer.Reset(time.Second)
continue
}
outboxesMut.RLock()
_, ok := outboxes[id]
outboxesMut.RUnlock()
if ok {
protocol.WriteMessage(conn, protocol.ResponseAlreadyConnected)
if debug {
log.Println("Already have a peer with the same ID", id, conn.RemoteAddr())
}
conn.Close()
continue
}
outboxesMut.Lock()
outboxes[id] = outbox
outboxesMut.Unlock()
joined = true
protocol.WriteMessage(conn, protocol.ResponseSuccess)
case protocol.ConnectRequest:
requestedPeer := syncthingprotocol.DeviceIDFromBytes(msg.ID)
outboxesMut.RLock()
peerOutbox, ok := outboxes[requestedPeer]
outboxesMut.RUnlock()
if !ok {
if debug {
log.Println(id, "is looking for", requestedPeer, "which does not exist")
}
protocol.WriteMessage(conn, protocol.ResponseNotFound)
conn.Close()
continue
}
// requestedPeer is the server, id is the client
ses := newSession(requestedPeer, id, sessionLimiter, globalLimiter)
go ses.Serve()
clientInvitation := ses.GetClientInvitationMessage()
serverInvitation := ses.GetServerInvitationMessage()
if err := protocol.WriteMessage(conn, clientInvitation); err != nil {
if debug {
log.Printf("Error sending invitation from %s to client: %s", id, err)
}
conn.Close()
continue
}
peerOutbox <- serverInvitation
if debug {
log.Println("Sent invitation from", id, "to", requestedPeer)
}
conn.Close()
case protocol.Ping:
if err := protocol.WriteMessage(conn, protocol.Pong{}); err != nil {
if debug {
log.Println("Error writing pong:", err)
}
conn.Close()
continue
}
case protocol.Pong:
// Nothing
default:
if debug {
log.Printf("Unknown message %s: %T", id, message)
}
protocol.WriteMessage(conn, protocol.ResponseUnexpectedMessage)
conn.Close()
}
case err := <-errors:
if debug {
log.Printf("Closing connection %s: %s", id, err)
}
close(outbox)
// Potentially closing a second time.
conn.Close()
if joined {
// Only delete the outbox if the client is joined, as it might be
// a lookup request coming from the same client.
outboxesMut.Lock()
delete(outboxes, id)
outboxesMut.Unlock()
// Also, kill all sessions related to this node, as it probably
// went offline. This is for the other end to realize the client
// is no longer there faster. This also helps resolve
// 'already connected' errors when one of the sides is
// restarting, and connecting to the other peer before the other
// peer even realised that the node has gone away.
dropSessions(id)
}
return
case <-pingTicker.C:
if !joined {
if debug {
log.Println(id, "didn't join within", pingInterval)
}
conn.Close()
continue
}
if err := protocol.WriteMessage(conn, protocol.Ping{}); err != nil {
if debug {
log.Println(id, err)
}
conn.Close()
}
if atomic.LoadInt32(&overLimit) > 0 && !hasSessions(id) {
if debug {
log.Println("Dropping", id, "as it has no sessions and we are over our limits")
}
protocol.WriteMessage(conn, protocol.RelayFull{})
conn.Close()
limitCheckTimer.Reset(time.Second)
}
case <-timeoutTicker.C:
// We should receive a error from the reader loop, which will cause
// us to quit this loop.
if debug {
log.Printf("%s timed out", id)
}
conn.Close()
case msg := <-outbox:
if msg == nil {
conn.Close()
return
}
if debug {
log.Printf("Sending message %T to %s", msg, id)
}
if err := protocol.WriteMessage(conn, msg); err != nil {
if debug {
log.Println(id, err)
}
conn.Close()
}
}
}
}
func sessionConnectionHandler(conn net.Conn) {
if err := conn.SetDeadline(time.Now().Add(messageTimeout)); err != nil {
if debug {
log.Println("Weird error setting deadline:", err, "on", conn.RemoteAddr())
}
return
}
message, err := protocol.ReadMessage(conn)
if err != nil {
return
}
switch msg := message.(type) {
case protocol.JoinSessionRequest:
ses := findSession(string(msg.Key))
if debug {
log.Println(conn.RemoteAddr(), "session lookup", ses, hex.EncodeToString(msg.Key)[:5])
}
if ses == nil {
protocol.WriteMessage(conn, protocol.ResponseNotFound)
conn.Close()
return
}
if !ses.AddConnection(conn) {
if debug {
log.Println("Failed to add", conn.RemoteAddr(), "to session", ses)
}
protocol.WriteMessage(conn, protocol.ResponseAlreadyConnected)
conn.Close()
return
}
if err := protocol.WriteMessage(conn, protocol.ResponseSuccess); err != nil {
if debug {
log.Println("Failed to send session join response to ", conn.RemoteAddr(), "for", ses)
}
return
}
if err := conn.SetDeadline(time.Time{}); err != nil {
if debug {
log.Println("Weird error setting deadline:", err, "on", conn.RemoteAddr())
}
conn.Close()
return
}
default:
if debug {
log.Println("Unexpected message from", conn.RemoteAddr(), message)
}
protocol.WriteMessage(conn, protocol.ResponseUnexpectedMessage)
conn.Close()
}
}
func messageReader(conn net.Conn, messages chan<- interface{}, errors chan<- error) {
atomic.AddInt64(&numConnections, 1)
defer atomic.AddInt64(&numConnections, -1)
for {
msg, err := protocol.ReadMessage(conn)
if err != nil {
errors <- err
return
}
messages <- msg
}
}

View File

@@ -1,224 +0,0 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors.
package main
import (
"crypto/tls"
"flag"
"fmt"
"log"
"net"
"net/url"
"os"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/juju/ratelimit"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/relay/protocol"
"github.com/syncthing/syncthing/lib/tlsutil"
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
)
var (
Version string
BuildStamp string
BuildUser string
BuildHost string
BuildDate time.Time
LongVersion string
)
func init() {
stamp, _ := strconv.Atoi(BuildStamp)
BuildDate = time.Unix(int64(stamp), 0)
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
LongVersion = fmt.Sprintf(`relaysrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
}
var (
listen string
debug bool
sessionAddress []byte
sessionPort uint16
networkTimeout = 2 * time.Minute
pingInterval = time.Minute
messageTimeout = time.Minute
limitCheckTimer *time.Timer
sessionLimitBps int
globalLimitBps int
overLimit int32
descriptorLimit int64
sessionLimiter *ratelimit.Bucket
globalLimiter *ratelimit.Bucket
statusAddr string
poolAddrs string
pools []string
providedBy string
defaultPoolAddrs = "https://relays.syncthing.net/endpoint"
)
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
var dir, extAddress string
flag.StringVar(&listen, "listen", ":22067", "Protocol listen address")
flag.StringVar(&dir, "keys", ".", "Directory where cert.pem and key.pem is stored")
flag.DurationVar(&networkTimeout, "network-timeout", networkTimeout, "Timeout for network operations between the client and the relay.\n\tIf no data is received between the client and the relay in this period of time, the connection is terminated.\n\tFurthermore, if no data is sent between either clients being relayed within this period of time, the session is also terminated.")
flag.DurationVar(&pingInterval, "ping-interval", pingInterval, "How often pings are sent")
flag.DurationVar(&messageTimeout, "message-timeout", messageTimeout, "Maximum amount of time we wait for relevant messages to arrive")
flag.IntVar(&sessionLimitBps, "per-session-rate", sessionLimitBps, "Per session rate limit, in bytes/s")
flag.IntVar(&globalLimitBps, "global-rate", globalLimitBps, "Global rate limit, in bytes/s")
flag.BoolVar(&debug, "debug", debug, "Enable debug output")
flag.StringVar(&statusAddr, "status-srv", ":22070", "Listen address for status service (blank to disable)")
flag.StringVar(&poolAddrs, "pools", defaultPoolAddrs, "Comma separated list of relay pool addresses to join")
flag.StringVar(&providedBy, "provided-by", "", "An optional description about who provides the relay")
flag.StringVar(&extAddress, "ext-address", "", "An optional address to advertising as being available on.\n\tAllows listening on an unprivileged port with port forwarding from e.g. 443, and be connected to on port 443.")
flag.Parse()
if extAddress == "" {
extAddress = listen
}
addr, err := net.ResolveTCPAddr("tcp", extAddress)
if err != nil {
log.Fatal(err)
}
log.Println(LongVersion)
maxDescriptors, err := osutil.MaximizeOpenFileLimit()
if maxDescriptors > 0 {
// Assume that 20% of FD's are leaked/unaccounted for.
descriptorLimit = int64(maxDescriptors*80) / 100
log.Println("Connection limit", descriptorLimit)
go monitorLimits()
} else if err != nil && runtime.GOOS != "windows" {
log.Println("Assuming no connection limit, due to error retrievign rlimits:", err)
}
sessionAddress = addr.IP[:]
sessionPort = uint16(addr.Port)
certFile, keyFile := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Println("Failed to load keypair. Generating one, this might take a while...")
cert, err = tlsutil.NewCertificate(certFile, keyFile, "relaysrv", 3072)
if err != nil {
log.Fatalln("Failed to generate X509 key pair:", err)
}
}
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
NextProtos: []string{protocol.ProtocolName},
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
},
}
id := syncthingprotocol.NewDeviceID(cert.Certificate[0])
if debug {
log.Println("ID:", id)
}
if sessionLimitBps > 0 {
sessionLimiter = ratelimit.NewBucketWithRate(float64(sessionLimitBps), int64(2*sessionLimitBps))
}
if globalLimitBps > 0 {
globalLimiter = ratelimit.NewBucketWithRate(float64(globalLimitBps), int64(2*globalLimitBps))
}
if statusAddr != "" {
go statusService(statusAddr)
}
uri, err := url.Parse(fmt.Sprintf("relay://%s/?id=%s&pingInterval=%s&networkTimeout=%s&sessionLimitBps=%d&globalLimitBps=%d&statusAddr=%s&providedBy=%s", extAddress, id, pingInterval, networkTimeout, sessionLimitBps, globalLimitBps, statusAddr, providedBy))
if err != nil {
log.Fatalln("Failed to construct URI", err)
}
log.Println("URI:", uri.String())
if poolAddrs == defaultPoolAddrs {
log.Println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
log.Println("!! Joining default relay pools, this relay will be available for public use. !!")
log.Println(`!! Use the -pools="" command line option to make the relay private. !!`)
log.Println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
}
pools = strings.Split(poolAddrs, ",")
for _, pool := range pools {
pool = strings.TrimSpace(pool)
if len(pool) > 0 {
go poolHandler(pool, uri)
}
}
go listener(listen, tlsCfg)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
// Gracefully close all connections, hoping that clients will be faster
// to realize that the relay is now gone.
sessionMut.RLock()
for _, session := range activeSessions {
session.CloseConns()
}
for _, session := range pendingSessions {
session.CloseConns()
}
sessionMut.RUnlock()
outboxesMut.RLock()
for _, outbox := range outboxes {
close(outbox)
}
outboxesMut.RUnlock()
time.Sleep(500 * time.Millisecond)
}
func monitorLimits() {
limitCheckTimer = time.NewTimer(time.Minute)
for _ = range limitCheckTimer.C {
if atomic.LoadInt64(&numConnections)+atomic.LoadInt64(&numProxies) > descriptorLimit {
atomic.StoreInt32(&overLimit, 1)
log.Println("Gone past our connection limits. Starting to refuse new/drop idle connections.")
} else if atomic.CompareAndSwapInt32(&overLimit, 1, 0) {
log.Println("Dropped below our connection limits. Accepting new connections.")
}
limitCheckTimer.Reset(time.Minute)
}
}

View File

@@ -1,63 +0,0 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors.
package main
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"net/url"
"time"
)
func poolHandler(pool string, uri *url.URL) {
if debug {
log.Println("Joining", pool)
}
for {
var b bytes.Buffer
json.NewEncoder(&b).Encode(struct {
URL string `json:"url"`
}{
uri.String(),
})
resp, err := http.Post(pool, "application/json", &b)
if err != nil {
log.Println("Error joining pool", pool, err)
} else if resp.StatusCode == 500 {
bs, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("Failed to join", pool, "due to an internal server error. Could not read response:", err)
} else {
log.Println("Failed to join", pool, "due to an internal server error:", string(bs))
}
resp.Body.Close()
} else if resp.StatusCode == 429 {
log.Println(pool, "under load, will retry in a minute")
time.Sleep(time.Minute)
continue
} else if resp.StatusCode == 403 {
log.Println(pool, "failed to join due to IP address not matching external address. Aborting")
return
} else if resp.StatusCode == 200 {
var x struct {
EvictionIn time.Duration `json:"evictionIn"`
}
err := json.NewDecoder(resp.Body).Decode(&x)
if err == nil {
rejoin := x.EvictionIn - (x.EvictionIn / 5)
log.Println("Joined", pool, "rejoining in", rejoin)
time.Sleep(rejoin)
continue
} else {
log.Println("Failed to deserialize response", err)
}
} else {
log.Println(pool, "unknown response type from server", resp.StatusCode)
}
time.Sleep(time.Hour)
}
}

View File

@@ -1,326 +0,0 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors.
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"net"
"sync"
"sync/atomic"
"time"
"github.com/juju/ratelimit"
"github.com/syncthing/syncthing/lib/relay/protocol"
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
)
var (
sessionMut = sync.RWMutex{}
activeSessions = make([]*session, 0)
pendingSessions = make(map[string]*session, 0)
numProxies int64
bytesProxied int64
)
func newSession(serverid, clientid syncthingprotocol.DeviceID, sessionRateLimit, globalRateLimit *ratelimit.Bucket) *session {
serverkey := make([]byte, 32)
_, err := rand.Read(serverkey)
if err != nil {
return nil
}
clientkey := make([]byte, 32)
_, err = rand.Read(clientkey)
if err != nil {
return nil
}
ses := &session{
serverkey: serverkey,
serverid: serverid,
clientkey: clientkey,
clientid: clientid,
rateLimit: makeRateLimitFunc(sessionRateLimit, globalRateLimit),
connsChan: make(chan net.Conn),
conns: make([]net.Conn, 0, 2),
}
if debug {
log.Println("New session", ses)
}
sessionMut.Lock()
pendingSessions[string(ses.serverkey)] = ses
pendingSessions[string(ses.clientkey)] = ses
sessionMut.Unlock()
return ses
}
func findSession(key string) *session {
sessionMut.Lock()
defer sessionMut.Unlock()
ses, ok := pendingSessions[key]
if !ok {
return nil
}
delete(pendingSessions, key)
return ses
}
func dropSessions(id syncthingprotocol.DeviceID) {
sessionMut.RLock()
for _, session := range activeSessions {
if session.HasParticipant(id) {
if debug {
log.Println("Dropping session", session, "involving", id)
}
session.CloseConns()
}
}
sessionMut.RUnlock()
}
func hasSessions(id syncthingprotocol.DeviceID) bool {
sessionMut.RLock()
has := false
for _, session := range activeSessions {
if session.HasParticipant(id) {
has = true
break
}
}
sessionMut.RUnlock()
return has
}
type session struct {
mut sync.Mutex
serverkey []byte
serverid syncthingprotocol.DeviceID
clientkey []byte
clientid syncthingprotocol.DeviceID
rateLimit func(bytes int64)
connsChan chan net.Conn
conns []net.Conn
}
func (s *session) AddConnection(conn net.Conn) bool {
if debug {
log.Println("New connection for", s, "from", conn.RemoteAddr())
}
select {
case s.connsChan <- conn:
return true
default:
}
return false
}
func (s *session) Serve() {
timedout := time.After(messageTimeout)
if debug {
log.Println("Session", s, "serving")
}
for {
select {
case conn := <-s.connsChan:
s.mut.Lock()
s.conns = append(s.conns, conn)
s.mut.Unlock()
// We're the only ones mutating s.conns, hence we are free to read it.
if len(s.conns) < 2 {
continue
}
close(s.connsChan)
if debug {
log.Println("Session", s, "starting between", s.conns[0].RemoteAddr(), "and", s.conns[1].RemoteAddr())
}
wg := sync.WaitGroup{}
wg.Add(2)
var err0 error
go func() {
err0 = s.proxy(s.conns[0], s.conns[1])
wg.Done()
}()
var err1 error
go func() {
err1 = s.proxy(s.conns[1], s.conns[0])
wg.Done()
}()
sessionMut.Lock()
activeSessions = append(activeSessions, s)
sessionMut.Unlock()
wg.Wait()
if debug {
log.Println("Session", s, "ended, outcomes:", err0, "and", err1)
}
goto done
case <-timedout:
if debug {
log.Println("Session", s, "timed out")
}
goto done
}
}
done:
// We can end up here in 3 cases:
// 1. Timeout joining, in which case there are potentially entries in pendingSessions
// 2. General session end/timeout, in which case there are entries in activeSessions
// 3. Protocol handler calls dropSession as one of it's clients disconnects.
sessionMut.Lock()
delete(pendingSessions, string(s.serverkey))
delete(pendingSessions, string(s.clientkey))
for i, session := range activeSessions {
if session == s {
l := len(activeSessions) - 1
activeSessions[i] = activeSessions[l]
activeSessions[l] = nil
activeSessions = activeSessions[:l]
}
}
sessionMut.Unlock()
// If we are here because of case 2 or 3, we are potentially closing some or
// all connections a second time.
s.CloseConns()
if debug {
log.Println("Session", s, "stopping")
}
}
func (s *session) GetClientInvitationMessage() protocol.SessionInvitation {
return protocol.SessionInvitation{
From: s.serverid[:],
Key: []byte(s.clientkey),
Address: sessionAddress,
Port: sessionPort,
ServerSocket: false,
}
}
func (s *session) GetServerInvitationMessage() protocol.SessionInvitation {
return protocol.SessionInvitation{
From: s.clientid[:],
Key: []byte(s.serverkey),
Address: sessionAddress,
Port: sessionPort,
ServerSocket: true,
}
}
func (s *session) HasParticipant(id syncthingprotocol.DeviceID) bool {
return s.clientid == id || s.serverid == id
}
func (s *session) CloseConns() {
s.mut.Lock()
for _, conn := range s.conns {
conn.Close()
}
s.mut.Unlock()
}
func (s *session) proxy(c1, c2 net.Conn) error {
if debug {
log.Println("Proxy", c1.RemoteAddr(), "->", c2.RemoteAddr())
}
atomic.AddInt64(&numProxies, 1)
defer atomic.AddInt64(&numProxies, -1)
buf := make([]byte, 65536)
for {
c1.SetReadDeadline(time.Now().Add(networkTimeout))
n, err := c1.Read(buf)
if err != nil {
return err
}
atomic.AddInt64(&bytesProxied, int64(n))
if debug {
log.Printf("%d bytes from %s to %s", n, c1.RemoteAddr(), c2.RemoteAddr())
}
if s.rateLimit != nil {
s.rateLimit(int64(n))
}
c2.SetWriteDeadline(time.Now().Add(networkTimeout))
_, err = c2.Write(buf[:n])
if err != nil {
return err
}
}
}
func (s *session) String() string {
return fmt.Sprintf("<%s/%s>", hex.EncodeToString(s.clientkey)[:5], hex.EncodeToString(s.serverkey)[:5])
}
func makeRateLimitFunc(sessionRateLimit, globalRateLimit *ratelimit.Bucket) func(int64) {
// This may be a case of super duper premature optimization... We build an
// optimized function to do the rate limiting here based on what we need
// to do and then use it in the loop.
if sessionRateLimit == nil && globalRateLimit == nil {
// No limiting needed. We could equally well return a func(int64){} and
// not do a nil check were we use it, but I think the nil check there
// makes it clear that there will be no limiting if none is
// configured...
return nil
}
if sessionRateLimit == nil {
// We only have a global limiter
return func(bytes int64) {
globalRateLimit.Wait(bytes)
}
}
if globalRateLimit == nil {
// We only have a session limiter
return func(bytes int64) {
sessionRateLimit.Wait(bytes)
}
}
// We have both. Queue the bytes on both the global and session specific
// rate limiters. Wait for both in parallell, so that the actual send
// happens when both conditions are satisfied. In practice this just means
// wait the longer of the two times.
return func(bytes int64) {
t0 := sessionRateLimit.Take(bytes)
t1 := globalRateLimit.Take(bytes)
if t0 > t1 {
time.Sleep(t0)
} else {
time.Sleep(t1)
}
}
}

View File

@@ -1,111 +0,0 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors.
package main
import (
"encoding/json"
"log"
"net/http"
"runtime"
"sync/atomic"
"time"
)
var rc *rateCalculator
func statusService(addr string) {
rc = newRateCalculator(360, 10*time.Second, &bytesProxied)
http.HandleFunc("/status", getStatus)
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatal(err)
}
}
func getStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
status := make(map[string]interface{})
sessionMut.Lock()
// This can potentially be double the number of pending sessions, as each session has two keys, one for each side.
status["startTime"] = rc.startTime
status["uptimeSeconds"] = time.Since(rc.startTime) / time.Second
status["numPendingSessionKeys"] = len(pendingSessions)
status["numActiveSessions"] = len(activeSessions)
sessionMut.Unlock()
status["numConnections"] = atomic.LoadInt64(&numConnections)
status["numProxies"] = atomic.LoadInt64(&numProxies)
status["bytesProxied"] = atomic.LoadInt64(&bytesProxied)
status["goVersion"] = runtime.Version()
status["goOS"] = runtime.GOOS
status["goArch"] = runtime.GOARCH
status["goMaxProcs"] = runtime.GOMAXPROCS(-1)
status["goNumRoutine"] = runtime.NumGoroutine()
status["kbps10s1m5m15m30m60m"] = []int64{
rc.rate(10/10) * 8 / 1000,
rc.rate(60/10) * 8 / 1000,
rc.rate(5*60/10) * 8 / 1000,
rc.rate(15*60/10) * 8 / 1000,
rc.rate(30*60/10) * 8 / 1000,
rc.rate(60*60/10) * 8 / 1000,
}
status["options"] = map[string]interface{}{
"network-timeout": networkTimeout / time.Second,
"ping-interval": pingInterval / time.Second,
"message-timeout": messageTimeout / time.Second,
"per-session-rate": sessionLimitBps,
"global-rate": globalLimitBps,
"pools": pools,
"provided-by": providedBy,
}
bs, err := json.MarshalIndent(status, "", " ")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(bs)
}
type rateCalculator struct {
rates []int64
prev int64
counter *int64
startTime time.Time
}
func newRateCalculator(keepIntervals int, interval time.Duration, counter *int64) *rateCalculator {
r := &rateCalculator{
rates: make([]int64, keepIntervals),
counter: counter,
startTime: time.Now(),
}
go r.updateRates(interval)
return r
}
func (r *rateCalculator) updateRates(interval time.Duration) {
for {
now := time.Now()
next := now.Truncate(interval).Add(interval)
time.Sleep(next.Sub(now))
cur := atomic.LoadInt64(r.counter)
rate := int64(float64(cur-r.prev) / interval.Seconds())
copy(r.rates[1:], r.rates)
r.rates[0] = rate
r.prev = cur
}
}
func (r *rateCalculator) rate(periods int) int64 {
var tot int64
for i := 0; i < periods; i++ {
tot += r.rates[i]
}
return tot / int64(periods)
}

View File

@@ -1,152 +0,0 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
package main
import (
"bufio"
"crypto/tls"
"flag"
"log"
"net"
"net/url"
"os"
"path/filepath"
"time"
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay/client"
"github.com/syncthing/syncthing/lib/relay/protocol"
)
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.LstdFlags | log.Lshortfile)
var connect, relay, dir string
var join, test bool
flag.StringVar(&connect, "connect", "", "Device ID to which to connect to")
flag.BoolVar(&join, "join", false, "Join relay")
flag.BoolVar(&test, "test", false, "Generic relay test")
flag.StringVar(&relay, "relay", "relay://127.0.0.1:22067", "Relay address")
flag.StringVar(&dir, "keys", ".", "Directory where cert.pem and key.pem is stored")
flag.Parse()
certFile, keyFile := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
log.Fatalln("Failed to load X509 key pair:", err)
}
id := syncthingprotocol.NewDeviceID(cert.Certificate[0])
log.Println("ID:", id)
uri, err := url.Parse(relay)
if err != nil {
log.Fatal(err)
}
stdin := make(chan string)
go stdinReader(stdin)
if join {
log.Println("Creating client")
relay, err := client.NewClient(uri, []tls.Certificate{cert}, nil, 10*time.Second)
if err != nil {
log.Fatal(err)
}
log.Println("Created client")
go relay.Serve()
recv := make(chan protocol.SessionInvitation)
go func() {
log.Println("Starting invitation receiver")
for invite := range relay.Invitations() {
select {
case recv <- invite:
log.Println("Received invitation", invite)
default:
log.Println("Discarding invitation", invite)
}
}
}()
for {
conn, err := client.JoinSession(<-recv)
if err != nil {
log.Fatalln("Failed to join", err)
}
log.Println("Joined", conn.RemoteAddr(), conn.LocalAddr())
connectToStdio(stdin, conn)
log.Println("Finished", conn.RemoteAddr(), conn.LocalAddr())
}
} else if connect != "" {
id, err := syncthingprotocol.DeviceIDFromString(connect)
if err != nil {
log.Fatal(err)
}
invite, err := client.GetInvitationFromRelay(uri, id, []tls.Certificate{cert}, 10*time.Second)
if err != nil {
log.Fatal(err)
}
log.Println("Received invitation", invite)
conn, err := client.JoinSession(invite)
if err != nil {
log.Fatalln("Failed to join", err)
}
log.Println("Joined", conn.RemoteAddr(), conn.LocalAddr())
connectToStdio(stdin, conn)
log.Println("Finished", conn.RemoteAddr(), conn.LocalAddr())
} else if test {
if client.TestRelay(uri, []tls.Certificate{cert}, time.Second, 2*time.Second, 4) {
log.Println("OK")
} else {
log.Println("FAIL")
}
} else {
log.Fatal("Requires either join or connect")
}
}
func stdinReader(c chan<- string) {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
c <- scanner.Text()
c <- "\n"
}
}
func connectToStdio(stdin <-chan string, conn net.Conn) {
go func() {
}()
buf := make([]byte, 1024)
for {
conn.SetReadDeadline(time.Now().Add(time.Millisecond))
n, err := conn.Read(buf[0:])
if err != nil {
nerr, ok := err.(net.Error)
if !ok || !nerr.Timeout() {
log.Println(err)
return
}
}
os.Stdout.Write(buf[:n])
select {
case msg := <-stdin:
_, err := conn.Write([]byte(msg))
if err != nil {
return
}
default:
}
}
}

View File

@@ -1,28 +0,0 @@
// Copyright (C) 2015 Audrius Butkevicius and Contributors.
package main
import (
"errors"
"net"
)
func setTCPOptions(conn net.Conn) error {
tcpConn, ok := conn.(*net.TCPConn)
if !ok {
return errors.New("Not a TCP connection")
}
if err := tcpConn.SetLinger(0); err != nil {
return err
}
if err := tcpConn.SetNoDelay(true); err != nil {
return err
}
if err := tcpConn.SetKeepAlivePeriod(networkTimeout); err != nil {
return err
}
if err := tcpConn.SetKeepAlive(true); err != nil {
return err
}
return nil
}

View File

@@ -39,9 +39,7 @@ func dump(ldb *leveldb.DB) {
case db.KeyTypeGlobal:
folder := nulString(key[1 : 1+64])
name := nulString(key[1+64:])
var flv db.VersionList
flv.UnmarshalXDR(it.Value())
fmt.Printf("[global] F:%q N:%q V: %s\n", folder, name, flv)
fmt.Printf("[global] F:%q N:%q V:%x\n", folder, name, it.Value())
case db.KeyTypeBlock:
folder := nulString(key[1 : 1+64])

View File

@@ -7,10 +7,13 @@
package main
import (
"bytes"
"compress/gzip"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"mime"
"net"
"net/http"
"os"
@@ -23,6 +26,7 @@ import (
"time"
"github.com/rcrowley/go-metrics"
"github.com/syncthing/syncthing/lib/auto"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/discover"
@@ -31,11 +35,11 @@ import (
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/stats"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/util"
"github.com/vitrun/qart/qr"
"golang.org/x/crypto/bcrypt"
)
@@ -50,7 +54,8 @@ type apiService struct {
cfg configIntf
httpsCertFile string
httpsKeyFile string
statics *staticsServer
assetDir string
themes []string
model modelIntf
eventSub events.BufferedSubscription
discoverer discover.CachingMux
@@ -59,8 +64,10 @@ type apiService struct {
systemConfigMut sync.Mutex // serializes posts to /rest/system/config
stop chan struct{} // signals intentional stop
configChanged chan struct{} // signals intentional listener close due to config change
started chan string // signals startup complete by sending the listener address, for testing only
startedOnce bool // the service has started successfully at least once
started chan struct{} // signals startup complete, for testing only
listener net.Listener
listenerMut sync.Mutex
guiErrors logger.Recorder
systemLog logger.Recorder
@@ -112,13 +119,13 @@ type connectionsIntf interface {
Status() map[string]interface{}
}
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder) *apiService {
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder) (*apiService, error) {
service := &apiService{
id: id,
cfg: cfg,
httpsCertFile: httpsCertFile,
httpsKeyFile: httpsKeyFile,
statics: newStaticsServer(cfg.GUI().Theme, assetDir),
assetDir: assetDir,
model: m,
eventSub: eventSub,
discoverer: discoverer,
@@ -126,11 +133,33 @@ func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKey
systemConfigMut: sync.NewMutex(),
stop: make(chan struct{}),
configChanged: make(chan struct{}),
listenerMut: sync.NewMutex(),
guiErrors: errors,
systemLog: systemLog,
}
return service
seen := make(map[string]struct{})
// Load themes from compiled in assets.
for file := range auto.Assets() {
theme := strings.Split(file, "/")[0]
if _, ok := seen[theme]; !ok {
seen[theme] = struct{}{}
service.themes = append(service.themes, theme)
}
}
if assetDir != "" {
// Load any extra themes from the asset override dir.
for _, dir := range dirNames(assetDir) {
if _, ok := seen[dir]; !ok {
seen[dir] = struct{}{}
service.themes = append(service.themes, dir)
}
}
}
var err error
service.listener, err = service.getListener(cfg.GUI())
return service, err
}
func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
@@ -197,22 +226,9 @@ func sendJSON(w http.ResponseWriter, jsonObject interface{}) {
}
func (s *apiService) Serve() {
listener, err := s.getListener(s.cfg.GUI())
if err != nil {
if !s.startedOnce {
// This is during initialization. A failure here should be fatal
// as there will be no way for the user to communicate with us
// otherwise anyway.
l.Fatalln("Starting API/GUI:", err)
}
// We let this be a loud user-visible warning as it may be the only
// indication they get that the GUI won't be available on startup.
l.Warnln("Starting API/GUI:", err)
return
}
s.startedOnce = true
defer listener.Close()
s.listenerMut.Lock()
listener := s.listener
s.listenerMut.Unlock()
if listener == nil {
// Not much we can do here other than exit quickly. The supervisor
@@ -234,7 +250,6 @@ func (s *apiService) Serve() {
getRestMux.HandleFunc("/rest/svc/deviceid", s.getDeviceID) // id
getRestMux.HandleFunc("/rest/svc/lang", s.getLang) // -
getRestMux.HandleFunc("/rest/svc/report", s.getReport) // -
getRestMux.HandleFunc("/rest/svc/random/string", s.getRandomString) // [length]
getRestMux.HandleFunc("/rest/system/browse", s.getSystemBrowse) // current
getRestMux.HandleFunc("/rest/system/config", s.getSystemConfig) // -
getRestMux.HandleFunc("/rest/system/config/insync", s.getSystemConfigInsync) // -
@@ -281,10 +296,16 @@ func (s *apiService) Serve() {
mux.HandleFunc("/qr/", s.getQR)
// Serve compiled in assets unless an asset directory was set (for development)
mux.Handle("/", s.statics)
assets := &embeddedStatic{
theme: s.cfg.GUI().Theme,
lastModified: time.Now(),
mut: sync.NewRWMutex(),
assetDir: s.assetDir,
assets: auto.Assets(),
}
mux.Handle("/", assets)
// Handle the special meta.js path
mux.HandleFunc("/meta.js", s.getJSMetadata)
s.cfg.Subscribe(assets)
guiCfg := s.cfg.GUI()
@@ -323,33 +344,33 @@ func (s *apiService) Serve() {
l.Infoln("Access the GUI via the following URL:", guiCfg.URL())
if s.started != nil {
// only set when run by the tests
s.started <- listener.Addr().String()
close(s.started)
}
err := srv.Serve(listener)
// Serve in the background
serveError := make(chan error, 1)
go func() {
serveError <- srv.Serve(listener)
}()
// Wait for stop, restart or error signals
// The return could be due to an intentional close. Wait for the stop
// signal before returning. IF there is no stop signal within a second, we
// assume it was unintentional and log the error before retrying.
select {
case <-s.stop:
// Shutting down permanently
l.Debugln("shutting down (stop)")
case <-s.configChanged:
// Soft restart due to configuration change
l.Debugln("restarting (config changed)")
case <-serveError:
// Restart due to listen/serve failure
l.Warnln("GUI/API:", err, "(restarting)")
case <-time.After(time.Second):
l.Warnln("API:", err)
}
}
func (s *apiService) Stop() {
s.listenerMut.Lock()
listener := s.listener
s.listenerMut.Unlock()
close(s.stop)
// listener may be nil here if we've had a config change to a broken
// configuration, in which case we shouldn't try to close it.
if listener != nil {
listener.Close()
}
}
func (s *apiService) String() string {
@@ -357,9 +378,6 @@ func (s *apiService) String() string {
}
func (s *apiService) VerifyConfiguration(from, to config.Configuration) error {
if _, err := net.ResolveTCPAddr("tcp", to.GUI.Address()); err != nil {
return err
}
return nil
}
@@ -368,11 +386,27 @@ func (s *apiService) CommitConfiguration(from, to config.Configuration) bool {
return true
}
if to.GUI.Theme != from.GUI.Theme {
s.statics.setTheme(to.GUI.Theme)
// Order here is important. We must close the listener to stop Serve(). We
// must create a new listener before Serve() starts again. We can't create
// a new listener on the same port before the previous listener is closed.
// To assist in this little dance the Serve() method will wait for a
// signal on the configChanged channel after the listener has closed.
s.listenerMut.Lock()
defer s.listenerMut.Unlock()
s.listener.Close()
var err error
s.listener, err = s.getListener(to.GUI)
if err != nil {
// Ideally this should be a verification error, but we check it by
// creating a new listener which requires shutting down the previous
// one first, which is too destructive for the VerifyConfiguration
// method.
return false
}
// Tell the serve loop to restart
s.configChanged <- struct{}{}
return true
@@ -427,6 +461,10 @@ func corsMiddleware(next http.Handler) http.Handler {
//
// See https://www.w3.org/TR/cors/ for details.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Add a generous access-control-allow-origin header since we may be
// redirecting REST requests over protocols
w.Header().Add("Access-Control-Allow-Origin", "*")
// Process OPTIONS requests
if r.Method == "OPTIONS" {
// Only GET/POST Methods are supported
@@ -491,14 +529,6 @@ func (s *apiService) restPing(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{"ping": "pong"})
}
func (s *apiService) getJSMetadata(w http.ResponseWriter, r *http.Request) {
meta, _ := json.Marshal(map[string]string{
"deviceID": s.id.String(),
})
w.Header().Set("Content-Type", "application/javascript")
fmt.Fprintf(w, "var metadata = %s;\n", meta)
}
func (s *apiService) getSystemVersion(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{
"version": Version,
@@ -713,7 +743,7 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
if curAcc := s.cfg.Options().URAccepted; to.Options.URAccepted > curAcc {
// UR was enabled
to.Options.URAccepted = usageReportVersion
to.Options.URUniqueID = rand.String(8)
to.Options.URUniqueID = util.RandomString(8)
} else if to.Options.URAccepted < curAcc {
// UR was disabled
to.Options.URAccepted = -1
@@ -813,6 +843,7 @@ func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
res["pathSeparator"] = string(filepath.Separator)
res["uptime"] = int(time.Since(startTime).Seconds())
res["startTime"] = startTime
res["themes"] = s.themes
sendJSON(w, res)
}
@@ -892,16 +923,6 @@ func (s *apiService) getReport(w http.ResponseWriter, r *http.Request) {
sendJSON(w, reportData(s.cfg, s.model))
}
func (s *apiService) getRandomString(w http.ResponseWriter, r *http.Request) {
length := 32
if val, _ := strconv.Atoi(r.URL.Query().Get("length")); val > 0 {
length = val
}
str := rand.String(length)
sendJSON(w, map[string]string{"random": str})
}
func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
@@ -1134,31 +1155,152 @@ func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
current := qs.Get("current")
if current == "" {
if roots, err := osutil.GetFilesystemRoots(); err == nil {
sendJSON(w, roots)
} else {
http.Error(w, err.Error(), 500)
}
return
}
search, _ := osutil.ExpandTilde(current)
pathSeparator := string(os.PathSeparator)
if strings.HasSuffix(current, pathSeparator) && !strings.HasSuffix(search, pathSeparator) {
search = search + pathSeparator
}
subdirectories, _ := osutil.Glob(search + "*")
ret := make([]string, 0, len(subdirectories))
ret := make([]string, 0, 10)
for _, subdirectory := range subdirectories {
info, err := os.Stat(subdirectory)
if err == nil && info.IsDir() {
ret = append(ret, subdirectory+pathSeparator)
if len(ret) > 9 {
break
}
}
}
sendJSON(w, ret)
}
type embeddedStatic struct {
theme string
lastModified time.Time
mut sync.RWMutex
assetDir string
assets map[string][]byte
}
func (s embeddedStatic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
file := r.URL.Path
if file[0] == '/' {
file = file[1:]
}
if len(file) == 0 {
file = "index.html"
}
s.mut.RLock()
theme := s.theme
modified := s.lastModified
s.mut.RUnlock()
// Check for an override for the current theme.
if s.assetDir != "" {
p := filepath.Join(s.assetDir, s.theme, filepath.FromSlash(file))
if _, err := os.Stat(p); err == nil {
http.ServeFile(w, r, p)
return
}
}
// Check for a compiled in asset for the current theme.
bs, ok := s.assets[theme+"/"+file]
if !ok {
// Check for an overridden default asset.
if s.assetDir != "" {
p := filepath.Join(s.assetDir, config.DefaultTheme, filepath.FromSlash(file))
if _, err := os.Stat(p); err == nil {
http.ServeFile(w, r, p)
return
}
}
// Check for a compiled in default asset.
bs, ok = s.assets[config.DefaultTheme+"/"+file]
if !ok {
http.NotFound(w, r)
return
}
}
if modifiedSince, err := time.Parse(r.Header.Get("If-Modified-Since"), http.TimeFormat); err == nil && modified.Before(modifiedSince) {
w.WriteHeader(http.StatusNotModified)
return
}
mtype := s.mimeTypeForFile(file)
if len(mtype) != 0 {
w.Header().Set("Content-Type", mtype)
}
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
} else {
// ungzip if browser not send gzip accepted header
var gr *gzip.Reader
gr, _ = gzip.NewReader(bytes.NewReader(bs))
bs, _ = ioutil.ReadAll(gr)
gr.Close()
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
w.Header().Set("Last-Modified", modified.Format(http.TimeFormat))
w.Header().Set("Cache-Control", "public")
w.Write(bs)
}
func (s embeddedStatic) mimeTypeForFile(file string) string {
// We use a built in table of the common types since the system
// TypeByExtension might be unreliable. But if we don't know, we delegate
// to the system.
ext := filepath.Ext(file)
switch ext {
case ".htm", ".html":
return "text/html"
case ".css":
return "text/css"
case ".js":
return "application/javascript"
case ".json":
return "application/json"
case ".png":
return "image/png"
case ".ttf":
return "application/x-font-ttf"
case ".woff":
return "application/x-font-woff"
case ".svg":
return "image/svg+xml"
default:
return mime.TypeByExtension(ext)
}
}
// VerifyConfiguration implements the config.Committer interface
func (s *embeddedStatic) VerifyConfiguration(from, to config.Configuration) error {
return nil
}
// CommitConfiguration implements the config.Committer interface
func (s *embeddedStatic) CommitConfiguration(from, to config.Configuration) bool {
s.mut.Lock()
if s.theme != to.GUI.Theme {
s.theme = to.GUI.Theme
s.lastModified = time.Now()
}
s.mut.Unlock()
return true
}
func (s *embeddedStatic) String() string {
return fmt.Sprintf("embeddedStatic@%p", s)
}
func (s *apiService) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
res := make([]jsonDBFileInfo, len(fs))
for i, f := range fs {

View File

@@ -9,14 +9,15 @@ package main
import (
"bytes"
"encoding/base64"
"math/rand"
"net/http"
"strings"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/util"
"golang.org/x/crypto/bcrypt"
)
@@ -113,7 +114,7 @@ func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguratio
return
passwordOK:
sessionid := rand.String(32)
sessionid := util.RandomString(32)
sessionsMut.Lock()
sessions[sessionid] = true
sessionsMut.Unlock()

View File

@@ -15,8 +15,8 @@ import (
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/util"
)
// csrfTokens is a list of valid tokens. It is sorted so that the most
@@ -41,8 +41,7 @@ func csrfMiddleware(unique string, prefix string, cfg config.GUIConfiguration, n
return
}
// Allow requests for anything not under the protected path prefix,
// and set a CSRF cookie if there isn't already a valid one.
// Allow requests for the front page, and set a CSRF cookie if there isn't already a valid one.
if !strings.HasPrefix(r.URL.Path, prefix) {
cookie, err := r.Cookie("CSRF-Token-" + unique)
if err != nil || !validCsrfToken(cookie.Value) {
@@ -57,6 +56,18 @@ func csrfMiddleware(unique string, prefix string, cfg config.GUIConfiguration, n
return
}
if r.Method == "GET" {
// Allow GET requests unconditionally, but if we got the CSRF
// token cookie do the verification anyway so we keep the
// csrfTokens list sorted by recent usage. We don't care about the
// outcome of the validity check.
if cookie, err := r.Cookie("CSRF-Token-" + unique); err == nil {
validCsrfToken(cookie.Value)
}
next.ServeHTTP(w, r)
return
}
// Verify the CSRF token
token := r.Header.Get("X-CSRF-Token-" + unique)
if !validCsrfToken(token) {
@@ -87,7 +98,7 @@ func validCsrfToken(token string) bool {
}
func newCsrfToken() string {
token := rand.String(32)
token := util.RandomString(32)
csrfMut.Lock()
csrfTokens = append([]string{token}, csrfTokens...)

View File

@@ -1,176 +0,0 @@
// 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 main
import (
"bytes"
"compress/gzip"
"fmt"
"io/ioutil"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/syncthing/syncthing/lib/auto"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/sync"
)
type staticsServer struct {
assetDir string
assets map[string][]byte
availableThemes []string
mut sync.RWMutex
theme string
}
func newStaticsServer(theme, assetDir string) *staticsServer {
s := &staticsServer{
assetDir: assetDir,
assets: auto.Assets(),
mut: sync.NewRWMutex(),
theme: theme,
}
seen := make(map[string]struct{})
// Load themes from compiled in assets.
for file := range auto.Assets() {
theme := strings.Split(file, "/")[0]
if _, ok := seen[theme]; !ok {
seen[theme] = struct{}{}
s.availableThemes = append(s.availableThemes, theme)
}
}
if assetDir != "" {
// Load any extra themes from the asset override dir.
for _, dir := range dirNames(assetDir) {
if _, ok := seen[dir]; !ok {
seen[dir] = struct{}{}
s.availableThemes = append(s.availableThemes, dir)
}
}
}
return s
}
func (s *staticsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/themes.json":
s.serveThemes(w, r)
default:
s.serveAsset(w, r)
}
}
func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
file := r.URL.Path
if file[0] == '/' {
file = file[1:]
}
if len(file) == 0 {
file = "index.html"
}
s.mut.RLock()
theme := s.theme
s.mut.RUnlock()
// Check for an override for the current theme.
if s.assetDir != "" {
p := filepath.Join(s.assetDir, theme, filepath.FromSlash(file))
if _, err := os.Stat(p); err == nil {
http.ServeFile(w, r, p)
return
}
}
// Check for a compiled in asset for the current theme.
bs, ok := s.assets[theme+"/"+file]
if !ok {
// Check for an overridden default asset.
if s.assetDir != "" {
p := filepath.Join(s.assetDir, config.DefaultTheme, filepath.FromSlash(file))
if _, err := os.Stat(p); err == nil {
http.ServeFile(w, r, p)
return
}
}
// Check for a compiled in default asset.
bs, ok = s.assets[config.DefaultTheme+"/"+file]
if !ok {
http.NotFound(w, r)
return
}
}
mtype := s.mimeTypeForFile(file)
if len(mtype) != 0 {
w.Header().Set("Content-Type", mtype)
}
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
} else {
// ungzip if browser not send gzip accepted header
var gr *gzip.Reader
gr, _ = gzip.NewReader(bytes.NewReader(bs))
bs, _ = ioutil.ReadAll(gr)
gr.Close()
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
w.Write(bs)
}
func (s *staticsServer) serveThemes(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string][]string{
"themes": s.availableThemes,
})
}
func (s *staticsServer) mimeTypeForFile(file string) string {
// We use a built in table of the common types since the system
// TypeByExtension might be unreliable. But if we don't know, we delegate
// to the system.
ext := filepath.Ext(file)
switch ext {
case ".htm", ".html":
return "text/html"
case ".css":
return "text/css"
case ".js":
return "application/javascript"
case ".json":
return "application/json"
case ".png":
return "image/png"
case ".ttf":
return "application/x-font-ttf"
case ".woff":
return "application/x-font-woff"
case ".svg":
return "image/svg+xml"
default:
return mime.TypeByExtension(ext)
}
}
func (s *staticsServer) setTheme(theme string) {
s.mut.Lock()
s.theme = theme
s.mut.Unlock()
}
func (s *staticsServer) String() string {
return fmt.Sprintf("staticsServer@%p", s)
}

View File

@@ -9,7 +9,6 @@ package main
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"net"
@@ -68,8 +67,11 @@ func TestStopAfterBrokenConfig(t *testing.T) {
}
w := config.Wrap("/dev/null", cfg)
srv := newAPIService(protocol.LocalDeviceID, w, "../../test/h1/https-cert.pem", "../../test/h1/https-key.pem", "", nil, nil, nil, nil, nil, nil)
srv.started = make(chan string)
srv, err := newAPIService(protocol.LocalDeviceID, w, "../../test/h1/https-cert.pem", "../../test/h1/https-key.pem", "", nil, nil, nil, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
srv.started = make(chan struct{})
sup := suture.NewSimple("test")
sup.Add(srv)
@@ -87,8 +89,8 @@ func TestStopAfterBrokenConfig(t *testing.T) {
RawUseTLS: false,
},
}
if err := srv.VerifyConfiguration(cfg, newCfg); err == nil {
t.Fatal("Verify config should have failed")
if srv.CommitConfiguration(cfg, newCfg) {
t.Fatal("Config commit should have failed")
}
// Nonetheless, it should be fine to Stop() it without panic.
@@ -116,7 +118,7 @@ func TestAssetsDir(t *testing.T) {
gw.Close()
foo := buf.Bytes()
e := &staticsServer{
e := embeddedStatic{
theme: "foo",
mut: sync.NewRWMutex(),
assetDir: "testdata",
@@ -187,9 +189,7 @@ type httpTestCase struct {
}
func TestAPIServiceRequests(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
@@ -344,13 +344,13 @@ func TestAPIServiceRequests(t *testing.T) {
for _, tc := range cases {
t.Log("Testing", tc.URL, "...")
testHTTPRequest(t, baseURL, tc, testAPIKey)
testHTTPRequest(t, baseURL, tc)
}
}
// testHTTPRequest tries the given test case, comparing the result code,
// content type, and result prefix.
func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase, apikey string) {
func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase) {
timeout := time.Second
if tc.Timeout > 0 {
timeout = tc.Timeout
@@ -359,14 +359,7 @@ func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase, apikey strin
Timeout: timeout,
}
req, err := http.NewRequest("GET", baseURL+tc.URL, nil)
if err != nil {
t.Errorf("Unexpected error requesting %s: %v", tc.URL, err)
return
}
req.Header.Set("X-API-Key", apikey)
resp, err := cli.Do(req)
resp, err := cli.Get(baseURL + tc.URL)
if err != nil {
t.Errorf("Unexpected error requesting %s: %v", tc.URL, err)
return
@@ -407,7 +400,7 @@ func TestHTTPLogin(t *testing.T) {
// Verify rejection when not using authorization
req, _ := http.NewRequest("GET", baseURL, nil)
req, _ := http.NewRequest("GET", baseURL+"/rest/system/status", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
@@ -472,144 +465,29 @@ func startHTTP(cfg *mockedConfig) (string, error) {
connections := new(mockedConnections)
errorLog := new(mockedLoggerRecorder)
systemLog := new(mockedLoggerRecorder)
addrChan := make(chan string)
// Instantiate the API service
svc := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
svc, err := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
eventSub, discoverer, connections, errorLog, systemLog)
svc.started = addrChan
if err != nil {
return "", err
}
// Make sure the API service is listening, and get the URL to use.
addr := svc.listener.Addr()
if addr == nil {
return "", fmt.Errorf("Nil listening address from API service")
}
tcpAddr, err := net.ResolveTCPAddr("tcp", addr.String())
if err != nil {
return "", fmt.Errorf("Weird address from API service: %v", err)
}
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
// Actually start the API service
supervisor := suture.NewSimple("API test")
supervisor.Add(svc)
supervisor.ServeBackground()
// Make sure the API service is listening, and get the URL to use.
addr := <-addrChan
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return "", fmt.Errorf("Weird address from API service: %v", err)
}
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
return baseURL, nil
}
func TestCSRFRequired(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
cli := &http.Client{
Timeout: time.Second,
}
// Getting the base URL (i.e. "/") should succeed.
resp, err := cli.Get(baseURL)
if err != nil {
t.Fatal("Unexpected error from getting base URL:", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatal("Getting base URL should succeed, not", resp.Status)
}
// Find the returned CSRF token for future use
var csrfTokenName, csrfTokenValue string
for _, cookie := range resp.Cookies() {
if strings.HasPrefix(cookie.Name, "CSRF-Token") {
csrfTokenName = cookie.Name
csrfTokenValue = cookie.Value
break
}
}
// Calling on /rest without a token should fail
resp, err = cli.Get(baseURL + "/rest/system/config")
if err != nil {
t.Fatal("Unexpected error from getting /rest/system/config:", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Fatal("Getting /rest/system/config without CSRF token should fail, not", resp.Status)
}
// Calling on /rest with a token should succeed
req, _ := http.NewRequest("GET", baseURL+"/rest/system/config", nil)
req.Header.Set("X-"+csrfTokenName, csrfTokenValue)
resp, err = cli.Do(req)
if err != nil {
t.Fatal("Unexpected error from getting /rest/system/config:", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatal("Getting /rest/system/config with CSRF token should succeed, not", resp.Status)
}
// Calling on /rest with the API key should succeed
req, _ = http.NewRequest("GET", baseURL+"/rest/system/config", nil)
req.Header.Set("X-API-Key", testAPIKey)
resp, err = cli.Do(req)
if err != nil {
t.Fatal("Unexpected error from getting /rest/system/config:", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatal("Getting /rest/system/config with API key should succeed, not", resp.Status)
}
}
func TestRandomString(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
cli := &http.Client{
Timeout: time.Second,
}
// The default should be to return a 32 character random string
for _, url := range []string{"/rest/svc/random/string", "/rest/svc/random/string?length=-1", "/rest/svc/random/string?length=yo"} {
req, _ := http.NewRequest("GET", baseURL+url, nil)
req.Header.Set("X-API-Key", testAPIKey)
resp, err := cli.Do(req)
if err != nil {
t.Fatal(err)
}
var res map[string]string
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
t.Fatal(err)
}
if len(res["random"]) != 32 {
t.Errorf("Expected 32 random characters, got %q of length %d", res["random"], len(res["random"]))
}
}
// We can ask for a different length if we like
req, _ := http.NewRequest("GET", baseURL+"/rest/svc/random/string?length=27", nil)
req.Header.Set("X-API-Key", testAPIKey)
resp, err := cli.Do(req)
if err != nil {
t.Fatal(err)
}
var res map[string]string
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
t.Fatal(err)
}
if len(res["random"]) != 27 {
t.Errorf("Expected 27 random characters, got %q of length %d", res["random"], len(res["random"]))
}
}

View File

@@ -17,10 +17,8 @@ import (
"net"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"os/signal"
"path"
"path/filepath"
"regexp"
"runtime"
@@ -41,10 +39,10 @@ import (
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/symlinks"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/util"
"github.com/thejerf/suture"
)
@@ -478,13 +476,8 @@ func performUpgrade(release upgrade.Release) {
func upgradeViaRest() error {
cfg, _ := loadConfig()
u, err := url.Parse(cfg.GUI().URL())
if err != nil {
return err
}
u.Path = path.Join(u.Path, "rest/system/upgrade")
target := u.String()
r, _ := http.NewRequest("POST", target, nil)
target := cfg.GUI().URL()
r, _ := http.NewRequest("POST", target+"/rest/system/upgrade", nil)
r.Header.Set("X-API-Key", cfg.GUI().APIKey)
tr := &http.Transport{
@@ -539,9 +532,8 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
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. The LocalDiskUpdated
// event might overwhelm the event reciever in some situations so we will not subscribe to it here.
apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents&^events.LocalChangeDetected), 1000)
// Event subscription for the API; must start early to catch the early events.
apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents), 1000)
if len(os.Getenv("GOMAXPROCS")) == 0 {
runtime.GOMAXPROCS(runtime.NumCPU())
@@ -768,7 +760,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
if opts.URUniqueID == "" {
// Previously the ID was generated from the node ID. We now need
// to generate a new one.
opts.URUniqueID = rand.String(8)
opts.URUniqueID = util.RandomString(8)
cfg.SetOptions(opts)
cfg.Save()
}
@@ -786,8 +778,10 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
if opts.AutoUpgradeIntervalH > 0 {
if noUpgrade {
l.Infof("No automatic upgrades; STNOUPGRADE environment variable defined.")
} else {
} else if IsRelease {
go autoUpgrade(cfg)
} else {
l.Infof("No automatic upgrades; %s is not a release version.", Version)
}
}
@@ -933,7 +927,10 @@ func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Mode
l.Warnln("Insecure admin access is enabled.")
}
api := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, connectionsService, errors, systemLog)
api, err := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, connectionsService, errors, systemLog)
if err != nil {
l.Fatalln("Cannot start GUI:", err)
}
cfg.Subscribe(api)
mainService.Add(api)
@@ -949,7 +946,7 @@ func defaultConfig(myName string) config.Configuration {
if !noDefaultFolder {
l.Infoln("Default folder created and/or linked to new config")
folderID := rand.String(5) + "-" + rand.String(5)
folderID := util.RandomString(5) + "-" + util.RandomString(5)
defaultFolder = config.NewFolderConfiguration(folderID, locations[locDefFolder])
defaultFolder.Label = "Default Folder (" + folderID + ")"
defaultFolder.RescanIntervalS = 60

View File

@@ -7,12 +7,151 @@
package main
import (
"os"
"testing"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/protocol"
)
func TestFolderErrors(t *testing.T) {
// This test intentionally avoids starting the folders. If they are
// started, they will perform an initial scan, which will create missing
// folder markers and race with the stuff we do in the test.
fcfg := config.FolderConfiguration{
ID: "folder",
RawPath: "testdata/testfolder",
}
cfg := config.Wrap("/tmp/test", config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
})
for _, file := range []string{".stfolder", "testfolder/.stfolder", "testfolder"} {
if err := os.Remove("testdata/" + file); err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
}
ldb := db.OpenMemory()
// Case 1 - new folder, directory and marker created
m := model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
m.AddFolder(fcfg)
if err := m.CheckFolderHealth("folder"); err != nil {
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
}
s, err := os.Stat("testdata/testfolder")
if err != nil || !s.IsDir() {
t.Error(err)
}
_, err = os.Stat("testdata/testfolder/.stfolder")
if err != nil {
t.Error(err)
}
if err := os.Remove("testdata/testfolder/.stfolder"); err != nil {
t.Fatal(err)
}
if err := os.Remove("testdata/testfolder/"); err != nil {
t.Fatal(err)
}
// Case 2 - new folder, marker created
fcfg.RawPath = "testdata/"
cfg = config.Wrap("/tmp/test", config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
})
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
m.AddFolder(fcfg)
if err := m.CheckFolderHealth("folder"); err != nil {
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
}
_, err = os.Stat("testdata/.stfolder")
if err != nil {
t.Error(err)
}
if err := os.Remove("testdata/.stfolder"); err != nil {
t.Fatal(err)
}
// Case 3 - Folder marker missing
set := db.NewFileSet("folder", ldb)
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
{Name: "dummyfile"},
})
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
m.AddFolder(fcfg)
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder marker missing" {
t.Error("Incorrect error: Folder marker missing !=", m.CheckFolderHealth("folder"))
}
// Case 3.1 - recover after folder marker missing
if err = fcfg.CreateMarker(); err != nil {
t.Error(err)
}
if err := m.CheckFolderHealth("folder"); err != nil {
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
}
// Case 4 - Folder path missing
if err := os.Remove("testdata/testfolder/.stfolder"); err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
if err := os.Remove("testdata/testfolder"); err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
fcfg.RawPath = "testdata/testfolder"
cfg = config.Wrap("testdata/subfolder", config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
})
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
m.AddFolder(fcfg)
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder path missing" {
t.Error("Incorrect error: Folder path missing !=", m.CheckFolderHealth("folder"))
}
// Case 4.1 - recover after folder path missing
if err := os.Mkdir("testdata/testfolder", 0700); err != nil {
t.Fatal(err)
}
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder marker missing" {
t.Error("Incorrect error: Folder marker missing !=", m.CheckFolderHealth("folder"))
}
// Case 4.2 - recover after missing marker
if err = fcfg.CreateMarker(); err != nil {
t.Error(err)
}
if err := m.CheckFolderHealth("folder"); err != nil {
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
}
}
func TestShortIDCheck(t *testing.T) {
cfg := config.Wrap("/tmp/test", config.Configuration{
Devices: []config.DeviceConfiguration{

View File

@@ -59,7 +59,7 @@ func (c *folderSummaryService) Stop() {
// listenForUpdates subscribes to the event bus and makes note of folders that
// need their data recalculated.
func (c *folderSummaryService) listenForUpdates() {
sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged | events.RemoteDownloadProgress)
sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged)
defer events.Default.Unsubscribe(sub)
for {

View File

@@ -72,18 +72,15 @@ func (s *verboseService) formatEvent(ev events.Event) string {
case events.Starting:
return fmt.Sprintf("Starting up (%s)", ev.Data.(map[string]string)["home"])
case events.StartupComplete:
return "Startup complete"
case events.DeviceDiscovered:
data := ev.Data.(map[string]interface{})
return fmt.Sprintf("Discovered device %v at %v", data["device"], data["addrs"])
case events.DeviceConnected:
data := ev.Data.(map[string]string)
return fmt.Sprintf("Connected to device %v at %v (type %s)", data["id"], data["addr"], data["type"])
case events.DeviceDisconnected:
data := ev.Data.(map[string]string)
return fmt.Sprintf("Disconnected from device %v", data["id"])
@@ -92,11 +89,6 @@ func (s *verboseService) formatEvent(ev events.Event) string {
data := ev.Data.(map[string]interface{})
return fmt.Sprintf("Folder %q is now %v", data["folder"], data["to"])
case events.LocalChangeDetected:
data := ev.Data.(map[string]string)
// Local change detected in folder "foo": modified file /Users/jb/whatever
return fmt.Sprintf("Local change detected in folder %q: %s %s %s", data["folder"], data["action"], data["type"], data["path"])
case events.RemoteIndexUpdated:
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"])
@@ -104,7 +96,6 @@ func (s *verboseService) formatEvent(ev events.Event) string {
case events.DeviceRejected:
data := ev.Data.(map[string]interface{})
return fmt.Sprintf("Rejected connection from device %v at %v", data["device"], data["address"])
case events.FolderRejected:
data := ev.Data.(map[string]string)
return fmt.Sprintf("Rejected unshared folder %q from device %v", data["folder"], data["device"])
@@ -112,7 +103,6 @@ func (s *verboseService) formatEvent(ev events.Event) string {
case events.ItemStarted:
data := ev.Data.(map[string]string)
return fmt.Sprintf("Started syncing %q / %q (%v %v)", data["folder"], data["item"], data["action"], data["type"])
case events.ItemFinished:
data := ev.Data.(map[string]interface{})
if err, ok := data["error"].(*string); ok && err != nil {
@@ -129,18 +119,13 @@ func (s *verboseService) formatEvent(ev events.Event) string {
case events.FolderCompletion:
data := ev.Data.(map[string]interface{})
return fmt.Sprintf("Completion for folder %q on device %v is %v%%", data["folder"], data["device"], data["completion"])
case events.FolderSummary:
data := ev.Data.(map[string]interface{})
sum := make(map[string]interface{})
for k, v := range data["summary"].(map[string]interface{}) {
if k == "invalid" || k == "ignorePatterns" || k == "stateChanged" {
continue
}
sum[k] = v
}
return fmt.Sprintf("Summary for folder %q is %v", data["folder"], sum)
sum := data["summary"].(map[string]interface{})
delete(sum, "invalid")
delete(sum, "ignorePatterns")
delete(sum, "stateChanged")
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)
@@ -157,19 +142,16 @@ func (s *verboseService) formatEvent(ev events.Event) string {
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.ListenAddressesChanged:
data := ev.Data.(map[string]interface{})
address := data["address"]
lan := data["lan"]
wan := data["wan"]
return fmt.Sprintf("Listen address %s resolution has changed: lan addresses: %s wan addresses: %s", address, lan, wan)
case events.LoginAttempt:
data := ev.Data.(map[string]interface{})
username := data["username"].(string)
@@ -180,6 +162,7 @@ func (s *verboseService) formatEvent(ev events.Event) string {
success = "failed"
}
return fmt.Sprintf("Login %s for username %s.", success, username)
}
return fmt.Sprintf("%s %#v", ev.Type, ev)

View File

View File

View File

View File

View File

@@ -1,6 +1,5 @@
[Unit]
Description=Restart Syncthing after resume
Documentation=man:syncthing(1)
After=suspend.target
[Service]

View File

@@ -71,18 +71,11 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
border-color: #222 !important;
}
.panel-default > .panel-heading {
.panel-default>.panel-heading {
color: #aaa !important;
border-color: #222 !important;
background-color: #222 !important;
}
.panel-warning > .panel-heading {
color: #222 !important;
}
.panel-progress {
background: #3498db;
}
.panel-footer {
background-color: #111 !important;
@@ -97,19 +90,10 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
border-top: 1px solid #222 !important;
}
.identicon rect {
fill: #aaa;
.identicon>rect {
fill: #aaa !important;
}
.panel-warning .identicon rect {
fill: #222;
}
.panel-heading:hover, .panel-heading:focus {
text-decoration: none;
}
/* buttons */
.btn {
border-radius: 3px !important;
@@ -156,26 +140,10 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
/* modal dialogs */
.modal-header {
border-bottom-color: #222 !important;
}
.modal-header:not(.alert) {
border-color: #222 !important;
background-color: #222;
}
.alert-info {
color: #222 !important;
}
.alert-warning {
color: #222 !important;
}
.alert-danger {
color: #222 !important;
background-color: #d62c1a !important;
}
.modal-content {
border-color: #666 !important;
border-width: 2px !important;
@@ -187,6 +155,14 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
background-color: #111 !important;
}
.alert-warning {
background-color: #c29d0b !important;
}
.alert-danger {
background-color: #d62c1a !important;
}
.help-block {
color: #aaa !important;
}
@@ -238,8 +214,4 @@ code.ng-binding{
.progress-bar-danger {
background-color: #d62c1a !important;
}
.progress .frontal {
color: #222;
}
}

View File

@@ -1,4 +1,5 @@
.dev-top-bar{
display: none;
background-color: yellow;
}

View File

@@ -33,6 +33,29 @@ ul+h5 {
display: block;
}
.panel-title {
position: relative;
text-overflow: ellipsis;
overflow: hidden;
}
.panel-title a:hover {
text-decoration: none;
}
identicon {
display: inline-block;
position: relative;
width: 1em;
height: 1em;
line-height: 1;
margin-right: 5px;
}
.identicon {
width: 1em;
height: 1em;
}
.checkbox {
margin-top: 0px;
}
@@ -50,6 +73,15 @@ ul+h5 {
word-wrap:break-word;
}
.panel-heading .fa, .modal-header .fa {
margin-right: 10px;
}
.panel-heading {
position: relative;
overflow: hidden;
}
.text-monospace {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
@@ -93,7 +125,7 @@ ul+h5 {
}
.table td {
/*padding-left: 20px !important;*/
padding-left: 20px !important;
}
.table td.small-data {
@@ -131,27 +163,12 @@ table.table-condensed td.no-overflow-ellipse {
display: none;
}
*[language-select] > .dropdown-menu {
width: 450px;
}
*[language-select] > .dropdown-menu > li {
float: left;
width: 50%;
}
*[language-select] > .dropdown-menu > li > a {
overflow: hidden;
text-overflow: ellipsis;
}
.nav>li{
float: left;
}
.navbar-right {
/* to align with panel */
padding-right: 15px;
float: right;
}
.panel-body .table-condensed {
@@ -166,56 +183,6 @@ table.table-condensed td.no-overflow-ellipse {
margin-left: 60px;
}
/**
* Panel, Model and Accordion Title bars
*/
.panel-icon {
float: left;
margin-right: 15px;
margin-top: 0.125em;
margin-bottom: 0.125em;
line-height: 1;
}
.modal-title .panel-icon {
margin-top: 0.25em;
margin-bottom: 0.25em;
}
button.panel-heading {
display: block;
position: relative;
width: 100%;
text-align: left;
border-top-width: 0;
border-left-width: 0;
border-right-width: 0;
border-radius: 0 !important;
}
.panel-heading .panel-title-text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.panel-heading .panel-status {
margin-left:15px;
}
identicon {
width: 1em;
height: 1em;
line-height: 1;
}
.identicon {
width: 1em;
height: 1em;
}
/**
* Progress bars with centered text
*/
@@ -276,37 +243,12 @@ ul.three-columns li, ul.two-columns li {
padding-bottom: 0px;
}
.navbar-brand {
margin: 3.25px -15px;
}
.navbar-fixed-bottom {
position: relative;
}
.navbar-nav .open .dropdown-menu {
position: absolute;
left: auto;
right: 0;
background-color: #ffffff;
border: 1px solid #cccccc;
border: 1px solid rgba(0, 0, 0, 0.15);
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
border-radius: 2px;
}
*[language-select] {
position: static !important;
}
*[language-select] > .dropdown-menu {
margin-left: 15px;
margin-right: 15px;
margin-top: -12px !important;
max-width: 450px;
height: 265px;
overflow-y: scroll;
.nav>li {
float:right;
}
table.table-condensed td {
@@ -316,17 +258,47 @@ ul.three-columns li, ul.two-columns li {
}
}
@media (min-width:650px) {
*[language-select] > .dropdown-menu > li {
width: 50%;
float: left;
}
*[language-select] > .dropdown-menu {
width: 440px;
}
}
/**
* Menu for select language
*/
@media (min-width:480px) and (max-width:649px) {
*[language-select] > .dropdown-menu {
width: 230px;
}
}
@media (max-width:479px) {
.dropdown-menu {
padding-top: 55px;
}
nav .dropdown-toggle {
font-size: 1em;
}
.dropdown-toggle {
float: left;
}
.logo{
margin:auto;
}
.navbar-nav .open .dropdown-menu > li > a {
padding: 12px 15px 12px 25px;
}
.navbar-fixed-bottom li {
width: 100%;
}
.navbar-fixed-bottom li{
width:100%;
}
}

View File

@@ -15,15 +15,7 @@
fill: #333;
}
.panel-warning .identicon rect {
fill: #fff;
}
.li-column {
background-color: rgb(236, 240, 241);
border-radius: 3px;
}
.panel-heading:hover, .panel-heading:focus {
text-decoration: none;
}
}

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -4,11 +4,11 @@
"A new major version may not be compatible with previous versions.": "Нова основна версия, която може да не е съвмеситима с предишни версии.",
"API Key": "API Ключ",
"About": "За програмата",
"Actions": "Меню",
"Actions": "Действия",
"Add": "Добави",
"Add Device": "Добави устройство",
"Add Folder": "Добави папка",
"Add Remote Device": "Добави ново устройство",
"Add Remote Device": "Добави отдалечено устройство",
"Add new folder?": "Добави нова папка?",
"Address": "Адрес",
"Addresses": "Адреси",
@@ -20,11 +20,11 @@
"Alphabetic": "Азбучен ред",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Друга команда се занимава с версиите. Тази команда трябва да премахни файла от синхронизираната папка.",
"Anonymous Usage Reporting": "Анонимен доклад",
"Any devices configured on an introducer device will be added to this device as well.": "Устройства настроени да представят други устройства също ще бъдат добавени към това устройство.",
"Any devices configured on an introducer device will be added to this device as well.": "Устройства настроени на introducer компютъра също ще бъдат добавени към този компютър.",
"Automatic upgrades": "Автоматично обновяване",
"Be careful!": "Внимание!",
"Bugs": "Бъгове",
"CPU Utilization": "Използван процесор",
"CPU Utilization": "Процесор в употреба",
"Changelog": "Списък с промени",
"Clean out after": "Изчисти след",
"Close": "Затвори",
@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Коментар, използван в началото на реда",
"Compression": "Компресиране",
"Connection Error": "Грешка при свързването",
"Connection Type": "Вид връзка",
"Copied from elsewhere": "Копиране от някъде другаде",
"Copied from original": "Копиран от оригинала",
"Copyright © 2014-2016 the following Contributors:": "Всички правата запазени © 2014-2016 Сътрудници:",
@@ -43,7 +42,7 @@
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство \"{{name}}\" ({{device}}) на {{address}} желае да се свърже. Добави ново устройство?",
"Device ID": "Идентификатор на устройство",
"Device Identification": "Идентификатор на устройство",
"Device Name": "Име на устройството",
"Device Name": "Име на устройство",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Устройство {{device}} ({{address}}) желае да се свърже. Добави ново устройство?",
"Devices": "Устройства",
"Disconnected": "Не е свързано",
@@ -75,11 +74,10 @@
"Folder Label": "Етикет на папката",
"Folder Master": "Главна папка",
"Folder Path": "Път до папката",
"Folder Type": "Вид папка",
"Folders": "Папки",
"GUI": "Потребителски интерфейс",
"GUI Authentication Password": "Парола за интерфейса",
"GUI Authentication User": "Потребителско име за интерфейса",
"GUI Authentication Password": "Парола за потребителския интерфейс",
"GUI Authentication User": "Потребител за потребителския интерфейс",
"GUI Listen Addresses": "Адрес за свързване с потребителския интерфейс",
"Generate": "Генерирай",
"Global Discovery": "Глобално откриване",
@@ -93,39 +91,35 @@
"Ignore Permissions": "Игнорирай правата за достъп",
"Incoming Rate Limit (KiB/s)": "Лимит на скоростта за сваляне (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Неправилни настройки могат да повредят файловете и да попречат на синхронизирането.",
"Introducer": "Може да предлага други устройства",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Обратното на даденото условие (пр. не изключвай)",
"Keep Versions": "Пази версии",
"Largest First": " Първо най-големите",
"Last File Received": "Последния получен файл",
"Last Scan": "Последно сканиран",
"Last seen": "Последно видяно",
"Last seen": "Последно видян",
"Later": "По-късно",
"Listeners": "Синхронизиращи устройства",
"Local Discovery": "Локално откриване",
"Local State": "Локално състояние",
"Local State (Total)": "Локално състояние (общо)",
"Local State (Total)": "Локално състояние (Общо)",
"Major Upgrade": "Основно Обновяване",
"Master": "Главен",
"Maximum Age": "Максимална възраст",
"Metadata Only": "Само мета информация",
"Minimum Free Disk Space": "Минимално свободно дисково пространство",
"Move to top of queue": "Премести в началото на опашката",
"Multi level wildcard (matches multiple directory levels)": "Маска на много нива (покрива папки с много нива)",
"Never": "никога",
"Never": "Никога",
"New Device": "Ново устройство",
"New Folder": "Нова папка",
"Newest First": "Първо най-новите",
"No": "Не",
"No File Versioning": "Без версии",
"Normal": "Нормален",
"Notice": "Известие",
"OK": "ОК",
"Off": "Изключено",
"Oldest First": "Първо най-старите",
"Optional descriptive label for the folder. Can be different on each device.": "Допълнително разяснеие за етикета на папката. Може да бъде различно всяко устройство.",
"Options": "Настройки",
"Out of Sync": "Несинхронизирана",
"Out of Sync": "Несинхронизирано",
"Out of Sync Items": "Несинхронизирани елементи",
"Outgoing Rate Limit (KiB/s)": "Лимит на скорост за качване (KiB/s)",
"Override Changes": "Наложи локалните промени",
@@ -139,17 +133,17 @@
"Preview": "Преглед",
"Preview Usage Report": "Разгледай доклада за използване",
"Quick guide to supported patterns": "Бърз наръчник към поддържаните шаблони",
"RAM Utilization": "Използван RAM",
"RAM Utilization": "RAM в употреба",
"Random": "Произволен",
"Relay Servers": "Препращащи сървъри",
"Relayed via": "Препратено през",
"Relays": "Препращачи",
"Release Notes": "Бележки по обновяването",
"Remote Devices": "Чужди устройства",
"Remote Devices": "Отделечени устройства",
"Remove": "Премахни",
"Required identifier for the folder. Must be the same on all cluster devices.": "Задължителен идентификатор за тази папка. Трябва да бъде един и същ на всички устройства.",
"Rescan": "Сканирай",
"Rescan All": "Сканирай всички",
"Rescan": "Сканирай повторно",
"Rescan All": "Сканирай повторно всички",
"Rescan Interval": "Интервал за повторно сканиране",
"Restart": "Рестартирай",
"Restart Needed": "Изисква се рестартиране",
@@ -187,13 +181,12 @@
"Sync Protocol Listen Addresses": "Адрес за слушане на синхронизиращия протокол",
"Syncing": "Синхронизиране",
"Syncthing has been shut down.": "Syncthing е спрян.",
"Syncthing includes the following software or portions thereof:": "Syncthing уползотворява частично или изцяло следните софтуерни продукти:",
"Syncthing includes the following software or portions thereof:": "Syncthing включва следният софтуер пълно или частично:",
"Syncthing is restarting.": "Syncthing се рестартира",
"Syncthing is upgrading.": "Syncthing се обновява.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing изглежда не е включен, или има проблем с интерент връзката. Повторен опит...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing има проблем при обработването на заявката. Моля, презаредете браузъра или рестартирайте Syncthing ако проблемът продължи.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Администраторския панел на Syncthing е настроен да приема дистанционни връзки без парола.",
"The aggregated statistics are publicly available at the URL below.": "Сумарната статистика е публично достъпна на посочения по-долу адрес.",
"The aggregated statistics are publicly available at {%url%}.": "Сумарната статистика е публично достъпна на {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурацията е запазена, но не е активирана. Syncthing трябва да рестартира, за да се активира новата конфигурация.",
"The device ID cannot be blank.": "Полето идентификатор на устройство не може да бъде празно.",
@@ -219,14 +212,14 @@
"The rate limit must be a non-negative number (0: no limit)": "Ограничението на скоростта трябва да бъде положително число (0: неограничено)",
"The rescan interval must be a non-negative number of seconds.": "Интервала на сканиране трябва да бъде не отрицателно число в секунди.",
"They are retried automatically and will be synced when the error is resolved.": "Ще бъдат спрени и автоматично синхронизирани, когато грешката бъде оправена.",
"This Device": "Вашето устройство",
"This Device": "Това устройство",
"This can easily give hackers access to read and change any files on your computer.": "Това дава лесен достъп на хакери да разглеждат и променят всякакви файлове на компютъра Ви.",
"This is a major version upgrade.": "Това е нова основна версия.",
"Trash Can File Versioning": "Само на файловете в кошчето",
"Unknown": "Неясно",
"Unshared": "Несподелена",
"Unused": "Неизползван",
"Up to Date": "Синхронизирана",
"Up to Date": "Синхронизирано",
"Updated": "Обновено",
"Upgrade": "Обнови",
"Upgrade To {%version%}": "Обновен до {{version}}",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Comentari quan és usat al principi d'una línia",
"Compression": "Compressió",
"Connection Error": "Error de connexió",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copiat d'un altre lloc",
"Copied from original": "Copiat de l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -75,7 +74,6 @@
"Folder Label": "Folder Label",
"Folder Master": "Carpeta mestra",
"Folder Path": "Camí de carpeta",
"Folder Type": "Folder Type",
"Folders": "Carpetes",
"GUI": "GUI",
"GUI Authentication Password": "Contrasenya d'autenticació GUI",
@@ -98,15 +96,12 @@
"Keep Versions": "Mantenir Versions",
"Largest First": "Més gran primer",
"Last File Received": "Últim fitxer rebut",
"Last Scan": "Last Scan",
"Last seen": "Vist per última vegada",
"Later": "Després",
"Listeners": "Listeners",
"Local Discovery": "Descobriment Local",
"Local State": "Estat local",
"Local State (Total)": "Estat local (Total)",
"Major Upgrade": "Actualització major",
"Master": "Master",
"Maximum Age": "Antiguitat Màxima",
"Metadata Only": "Només metadades",
"Minimum Free Disk Space": "Espai de disc lliure mínim",
@@ -118,7 +113,6 @@
"Newest First": "Més nou primer",
"No": "No",
"No File Versioning": "Sense Versionat de Fitxer",
"Normal": "Normal",
"Notice": "Avís",
"OK": "OK",
"Off": "Desactivar",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Synthing sembla parat, o hi ha algun problema amb la connexió a Internet. Reintentant...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Sembla ser que Syncthing està tinguent problemes per processar la teva petició. Si us plau, refresca la pàgina o reinicia Syncthing si el problema persisteix.",
"The Syncthing admin interface is configured to allow remote access without a password.": "La interfície d'administració de Syncthing està configurada per permetre l'accés remot sense contrasenya.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan públicament disponibles a {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració s'ha guardar però no s'ha activat. S'ha de reiniciar el synthing per activar la nova configuració.",
"The device ID cannot be blank.": "El ID del dispositiu no pot estar en blanc.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Comentar, quant s'utilitza al principi d'una línia",
"Compression": "Compresió",
"Connection Error": "Error de connexió",
"Connection Type": "Tipus de connexió",
"Copied from elsewhere": "Copiat de qualsevol lloc",
"Copied from original": "Copiat de l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 els següents Col·laboradors:",
@@ -75,7 +74,6 @@
"Folder Label": "Etiqueta de la Carpeta",
"Folder Master": "Carpeta principal",
"Folder Path": "Ruta de la carpeta",
"Folder Type": "Tipus de carpeta",
"Folders": "Carpetes",
"GUI": "IGU (Interfície Gràfica d'Usuari)",
"GUI Authentication Password": "Password d'autenticació de l'Interfície Gràfica d'Usuari (GUI)",
@@ -98,15 +96,12 @@
"Keep Versions": "Mantindre versions",
"Largest First": "El més gran primer",
"Last File Received": "Darrer fitxer rebut",
"Last Scan": "Últim escaneig",
"Last seen": "Vist per última vegada",
"Later": "Més tard",
"Listeners": "Escoltants",
"Local Discovery": "Descobriment local",
"Local State": "Estat local",
"Local State (Total)": "Estat Local (Total)",
"Major Upgrade": "Actualització important",
"Master": "Mestre",
"Maximum Age": "Edat màxima",
"Metadata Only": "Sols metadades",
"Minimum Free Disk Space": "Espai minim de disc lliure",
@@ -118,7 +113,6 @@
"Newest First": "El més nou primer",
"No": "No",
"No File Versioning": "Sense versionat de fitxer",
"Normal": "Normal",
"Notice": "Avís",
"OK": "OK",
"Off": "Off",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing pareix apagat o hi ha un problema amb la connexió a Internet. Tornant a intentar...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing pareix que té un problema processant la seua sol·licitud. Per favor, refresque la pàgina o reinicie Syncthing si el problema persistix.",
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfície d'administració de Syncthing està configurat per a permetre l'accés remot sense una contrasenya.",
"The aggregated statistics are publicly available at the URL below.": "Les estadístiques agregades estàn disponibles en la URL que figura a continuació.",
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan disponibles públicament en {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració ha sigut gravada però no activada. Syncthing deu reiniciar per tal d'activar la nova configuració.",
"The device ID cannot be blank.": "L'ID del dispositiu no pot estar buida.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Komentář, pokud použito na začátku řádku",
"Compression": "Komprese",
"Connection Error": "Chyba připojení",
"Connection Type": "Typ připojení",
"Copied from elsewhere": "Zkopírováno odjinud",
"Copied from original": "Zkopírováno z originálu",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následující přispěvatelé:",
@@ -75,7 +74,6 @@
"Folder Label": "Jmenovka adresáře",
"Folder Master": "Master adresář",
"Folder Path": "Cesta k adresáři",
"Folder Type": "Typ adresáře",
"Folders": "Adresáře",
"GUI": "GUI",
"GUI Authentication Password": "Přihlašovací heslo pro GUI",
@@ -98,15 +96,12 @@
"Keep Versions": "Ponechat verze",
"Largest First": "Od největšího",
"Last File Received": "Poslední přijatý soubor",
"Last Scan": "Poslední sken",
"Last seen": "Naposledy spatřen",
"Later": "Později",
"Listeners": "Naslouchající",
"Local Discovery": "Místní oznamování",
"Local State": "Místní status",
"Local State (Total)": "Místní status (Celkem)",
"Major Upgrade": "Důležitá aktualizace",
"Master": "Master",
"Maximum Age": "Maximální časový limit",
"Metadata Only": "Pouze metadata",
"Minimum Free Disk Space": "Minimální velikost volného místa na disku",
@@ -118,7 +113,6 @@
"Newest First": "Od nejnovějšího",
"No": "Ne",
"No File Versioning": "Bez verzování souborů",
"Normal": "Normální",
"Notice": "Oznámení",
"OK": "OK",
"Off": "Vypnuta",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing se zdá být nefunkční, nebo je problém s připojením k Internetu. Opakuji...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing má nejspíše problém s provedením vašeho požadavku. Pokud problém přetrvává, obnovte stránku v prohlížeči nebo restartujte Syncthing.",
"The Syncthing admin interface is configured to allow remote access without a password.": "V nastavení aplikace Syncthing je povoleno vzdálené připojení k administrátorskému rozhraní bez zadání hesla.",
"The aggregated statistics are publicly available at the URL below.": "Souhrnné statistiky jsou veřejně dostupné na níže uvedené URL.",
"The aggregated statistics are publicly available at {%url%}.": "Souhrnné statistiky jsou veřejně dostupné na {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurace byla uložena, ale není aktivována. Pro aktivaci nové konfigurace je třeba restartovat Syncthing.",
"The device ID cannot be blank.": "ID přístroje nemůže být prázdné.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Kommentering som bruges i starten af en linje",
"Compression": "Anvend komprimering",
"Connection Error": "Tilslutnings fejl",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Kopieret fra et andet sted",
"Copied from original": "Kopieret fra originalen",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -75,7 +74,6 @@
"Folder Label": "Folder Label",
"Folder Master": "Mastermappe",
"Folder Path": "Mappesti",
"Folder Type": "Folder Type",
"Folders": "Mapper",
"GUI": "GUI",
"GUI Authentication Password": "GUI-kodeord",
@@ -98,15 +96,12 @@
"Keep Versions": "Behold versioner",
"Largest First": "Største først",
"Last File Received": "Sidste modtaget fil",
"Last Scan": "Last Scan",
"Last seen": "Sidst set",
"Later": "Senere",
"Listeners": "Listeners",
"Local Discovery": "Lokal opslag",
"Local State": "Lokal tilstand",
"Local State (Total)": "Lokal tilstand (total)",
"Major Upgrade": "Ny version",
"Master": "Master",
"Maximum Age": "Maks alder",
"Metadata Only": "Kun metadata",
"Minimum Free Disk Space": "Mindst ledig diskplads",
@@ -118,7 +113,6 @@
"Newest First": "Nyeste først",
"No": "Nej",
"No File Versioning": "Ingen filversion",
"Normal": "Normal",
"Notice": "OBS",
"OK": "OK",
"Off": "Slå fra",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ud til at være stoppet eller oplever problemer med din internetforbindels. Prøver igen...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Det ser ud til at Syncthiing har problemer med at udføre opgaven. Prøv at genopfriske siden eller genstarte Synching hvis problemet vedbliver.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administationsdelen er konfigureret til at blive fjernstyret uden kodeord.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "Samlet statistik er offentligt tilgængelig på {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen er gemt, men ikke aktiveret. Syncthing skal genstarte for at aktivere den nye konfiguration.",
"The device ID cannot be blank.": "Enhedens ID må ikke være tom.",

View File

@@ -21,18 +21,17 @@
"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",
"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 aktivieren",
"Automatic upgrades": "automatische Updates",
"Be careful!": "Vorsicht!",
"Bugs": "Fehler",
"CPU Utilization": "Prozessorauslastung",
"Changelog": "Änderungsprotokoll",
"Clean out after": "Löschen nach",
"Close": "Schließen",
"Command": "Befehl",
"Command": "Kommando",
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
"Compression": "Komprimierung",
"Connection Error": "Verbindungsfehler",
"Connection Type": "Verbindungstyp",
"Copied from elsewhere": "Von anderer Quelle kopiert",
"Copied from original": "Vom Original kopiert",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 der folgenden Unterstützer:",
@@ -56,7 +55,7 @@
"Edit Device": "Gerät bearbeiten",
"Edit Folder": "Verzeichnis bearbeiten",
"Editing": "Bearbeitet",
"Enable NAT traversal": "NAT-Durchdringung aktivieren",
"Enable NAT traversal": "NAT-Traversal aktivieren",
"Enable Relaying": "Weiterleitung aktivieren",
"Enable UPnP": "UPnP aktivieren",
"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.",
@@ -75,7 +74,6 @@
"Folder Label": "Verzeichnisbezeichnung",
"Folder Master": "Master Verzeichnis - schreibgeschützt",
"Folder Path": "Verzeichnispfad",
"Folder Type": "Verzeichnistyp",
"Folders": "Verzeichnisse",
"GUI": "GUI",
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
@@ -98,15 +96,12 @@
"Keep Versions": "Versionen erhalten",
"Largest First": "Größte zuerst",
"Last File Received": "Letzte Änderung",
"Last Scan": "Letzter Scan",
"Last seen": "Zuletzt online",
"Later": "Später",
"Listeners": "Zuhörer",
"Local Discovery": "Lokale Gerätesuche",
"Local State": "Lokaler Status",
"Local State (Total)": "Lokaler Status (Gesamt)",
"Major Upgrade": "Hauptversionsupgrade",
"Master": "Master",
"Maximum Age": "Höchstalter",
"Metadata Only": "Nur Metadaten",
"Minimum Free Disk Space": "Minimal freier Festplattenspeicher",
@@ -118,7 +113,6 @@
"Newest First": "Neueste zuerst",
"No": "Nein",
"No File Versioning": "Keine Dateiversionierung",
"Normal": "Normal",
"Notice": "Hinweis",
"OK": "OK",
"Off": "Aus",
@@ -193,7 +187,6 @@
"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.": "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 Syncthing admin interface is configured to allow remote access without a password.": "Die Syncthing-Oberfläche erlaubt mit den jetzigen Einstellungen einen Zugriff ohne Passwort.",
"The aggregated statistics are publicly available at the URL below.": "Die gesammelten Statistiken sind öffentlich unter der nachfolgenden URL verfügbar.",
"The 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 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.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Σχόλιο, όταν χρησιμοποιείται στην αρχή μιας γραμμής",
"Compression": "Συμπίεση",
"Connection Error": "Σφάλμα σύνδεσης",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Έχει αντιγραφεί από κάπου αλλού",
"Copied from original": "Έχει αντιγραφεί από το πρωτότυπο",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -75,7 +74,6 @@
"Folder Label": "Folder Label",
"Folder Master": "Να μην επιτρέπονται αλλαγές",
"Folder Path": "Μονοπάτι φακέλου",
"Folder Type": "Folder Type",
"Folders": "Φάκελοι",
"GUI": "Γραφικό περιβάλλον",
"GUI Authentication Password": "Κωδικός για την πρόσβαση στη διεπαφή",
@@ -98,15 +96,12 @@
"Keep Versions": "Διατήρηση εκδόσεων",
"Largest First": "Το μεγαλύτερο πρώτα",
"Last File Received": "Πιο πρόσφατο αρχείο",
"Last Scan": "Last Scan",
"Last seen": "Τελευταία φορά συνδεδεμένος",
"Later": "Αργότερα",
"Listeners": "Listeners",
"Local Discovery": "Τοπική ανεύρεση",
"Local State": "Τοπική κατάσταση",
"Local State (Total)": "Τοπική κατάσταση (συνολικά)",
"Major Upgrade": "Σημαντική αναβάθμιση",
"Master": "Master",
"Maximum Age": "Μέγιστη ηλικία",
"Metadata Only": "Μόνο μεταδεδομένα",
"Minimum Free Disk Space": "Ελάχιστος ελεύθερος αποθηκευτικός χώρος",
@@ -118,7 +113,6 @@
"Newest First": "Το νεότερο πρώτα",
"No": "Όχι",
"No File Versioning": "Να μην τηρούνται εκδόσεις",
"Normal": "Normal",
"Notice": "Σημείωση",
"OK": "OK",
"Off": "Απενεργοποιημένο",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Το Syncthing φαίνεται πως είναι απενεργοποιημένο ή υπάρχει πρόβλημα στη σύνδεσή σου στο διαδίκτυο. Προσπαθώ πάλι…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Το Syncthing φαίνεται να αντιμετωπίζει ένα πρόβλημα με την επεξεργασία του αιτήματός σου. Παρακαλούμε, αν το πρόβλημα συνεχίζει, ανανέωσε την σελίδα ή επανεκκίνησε το Syncthing.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Η διεπαφή διαχείρισης του Syncthing είναι ρυθμισμένη να επιτρέπει την πρόσβαση χωρίς κωδικό.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "Τα στατιστικά που έχουν συλλεγεί είναι δημόσια διαθέσιμα στο {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Οι ρυθμίσεις έχουν αποθηκευτεί αλλά δεν έχουν ενεργοποιηθεί. Πρέπει να επανεκκινήσεις το Syncthing για να ισχύσουν οι νέες ρυθμίσεις.",
"The device ID cannot be blank.": "Η ταυτότητα της συσκευής δεν μπορεί να είναι κενή",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression": "Compression",
"Connection Error": "Connection Error",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copied from elsewhere",
"Copied from original": "Copied from original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -75,7 +74,6 @@
"Folder Label": "Folder Label",
"Folder Master": "Folder Master",
"Folder Path": "Folder Path",
"Folder Type": "Folder Type",
"Folders": "Folders",
"GUI": "GUI",
"GUI Authentication Password": "GUI Authentication Password",
@@ -98,15 +96,12 @@
"Keep Versions": "Keep Versions",
"Largest First": "Largest First",
"Last File Received": "Last File Received",
"Last Scan": "Last Scan",
"Last seen": "Last seen",
"Later": "Later",
"Listeners": "Listeners",
"Local Discovery": "Local Discovery",
"Local State": "Local State",
"Local State (Total)": "Local State (Total)",
"Major Upgrade": "Major Upgrade",
"Master": "Master",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
"Minimum Free Disk Space": "Minimum Free Disk Space",
@@ -118,7 +113,6 @@
"Newest First": "Newest First",
"No": "No",
"No File Versioning": "No File Versioning",
"Normal": "Normal",
"Notice": "Notice",
"OK": "OK",
"Off": "Off",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
"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.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "The aggregated statistics are publicly available at {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
"The device ID cannot be blank.": "The device ID cannot be blank.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression": "Compression",
"Connection Error": "Connection Error",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copied from elsewhere",
"Copied from original": "Copied from original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -75,7 +74,6 @@
"Folder Label": "Folder Label",
"Folder Master": "Folder Master",
"Folder Path": "Folder Path",
"Folder Type": "Folder Type",
"Folders": "Folders",
"GUI": "GUI",
"GUI Authentication Password": "GUI Authentication Password",
@@ -98,15 +96,12 @@
"Keep Versions": "Keep Versions",
"Largest First": "Largest First",
"Last File Received": "Last File Received",
"Last Scan": "Last Scan",
"Last seen": "Last seen",
"Later": "Later",
"Listeners": "Listeners",
"Local Discovery": "Local Discovery",
"Local State": "Local State",
"Local State (Total)": "Local State (Total)",
"Major Upgrade": "Major Upgrade",
"Master": "Master",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
"Minimum Free Disk Space": "Minimum Free Disk Space",
@@ -118,7 +113,6 @@
"Newest First": "Newest First",
"No": "No",
"No File Versioning": "No File Versioning",
"Normal": "Normal",
"Notice": "Notice",
"OK": "OK",
"Off": "Off",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
"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.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "The aggregated statistics are publicly available at {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
"The device ID cannot be blank.": "The device ID cannot be blank.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Comentar, cuando se usa al comienzo de una línea",
"Compression": "Compresión",
"Connection Error": "Error de conexión",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copiado de otro sitio",
"Copied from original": "Copiado del original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes Colaboradores:",
@@ -75,7 +74,6 @@
"Folder Label": "Etiqueta de la Carpeta",
"Folder Master": "Carpeta principal",
"Folder Path": "Ruta de la carpeta",
"Folder Type": "Folder Type",
"Folders": "Carpetas",
"GUI": "GUI",
"GUI Authentication Password": "Password de la Interfaz Gráfica de Usuario (GUI)",
@@ -98,15 +96,12 @@
"Keep Versions": "Mantener versiones",
"Largest First": "Más grande primero",
"Last File Received": "Último fichero recibido",
"Last Scan": "Last Scan",
"Last seen": "Visto por última vez",
"Later": "Más tarde",
"Listeners": "Listeners",
"Local Discovery": "Descubrimiento local",
"Local State": "Estado local",
"Local State (Total)": "Estado Local (Total)",
"Major Upgrade": "Actualización importante",
"Master": "Master",
"Maximum Age": "Edad máxima",
"Metadata Only": "Sólo metadatos",
"Minimum Free Disk Space": "Espacio mínimo libre en disco",
@@ -118,7 +113,6 @@
"Newest First": "El más nuevo primero",
"No": "No",
"No File Versioning": "Sin versionado de fichero",
"Normal": "Normal",
"Notice": "Aviso",
"OK": "OK",
"Off": "Desconectar",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece no estar activo o hay un problema con tu conexión de internet. Reintentando...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing tiene problemas para procesar tu solicitud. Por favor, actualiza la página o reinicia Syncthing si el problema persiste.",
"The Syncthing admin interface is configured to allow remote access without a password.": "El panel de administración de Syncthing está configurado para permitir el acceso remoto sin contraseña.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "Las estadísticas agregadas están disponibles públicamente en {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido grabada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
"The device ID cannot be blank.": "La ID del dispositivo no puede estar vacía.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Comentario, cuando es utilizado al inicio de una línea.",
"Compression": "Compresión",
"Connection Error": "Error de conexión",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copiado desde otra parte.",
"Copied from original": "Copiado del original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes contribuidores:",
@@ -75,7 +74,6 @@
"Folder Label": "Folder Label",
"Folder Master": "Repositorio maestro",
"Folder Path": "Ruta del repositorio",
"Folder Type": "Folder Type",
"Folders": "Repositorios",
"GUI": "GUI",
"GUI Authentication Password": "Contraseña de autenticación de la GUI",
@@ -98,15 +96,12 @@
"Keep Versions": "Conservar versiones",
"Largest First": "Más grande primero",
"Last File Received": "Último archivo recibido",
"Last Scan": "Last Scan",
"Last seen": "Visto por ultima vez",
"Later": "Más tarde",
"Listeners": "Listeners",
"Local Discovery": "Búsqueda en red local",
"Local State": "Estado local",
"Local State (Total)": "Estado local (total)",
"Major Upgrade": "Actualización mayor",
"Master": "Master",
"Maximum Age": "Edad máxima",
"Metadata Only": "Sólo metadatos",
"Minimum Free Disk Space": "Espacio mínimo libre en disco",
@@ -118,7 +113,6 @@
"Newest First": "Nuevo primero",
"No": "No",
"No File Versioning": "Sin control de versiones de archivos",
"Normal": "Normal",
"Notice": "Aviso",
"OK": "OK",
"Off": "Apagado",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece estar apagado, o hay un problema con su conexión de Internet. Reintentando...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing parece estar experimentando un problema al procesar su solicitud. Por favor, recargue el navegador o reinicie Syncthing si el problema persiste.",
"The Syncthing admin interface is configured to allow remote access without a password.": "La interfaz administrativa del Syncthing está configurada para permitir acceso remoto sin una contraseña.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "Las estadísticas acumuladas están disponibles públicamente en {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido guardada pero no activada.\nSyncthing debe reiniciarse para activar la nueva configuración.",
"The device ID cannot be blank.": "La ID del dispositivo no puede estar en blanco.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Kommentti, käytettäessä rivin alussa",
"Compression": "Pakkaus",
"Connection Error": "Yhteysvirhe",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Kopioitu muualta",
"Copied from original": "Kopioitu alkuperäisestä lähteestä",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -75,7 +74,6 @@
"Folder Label": "Folder Label",
"Folder Master": "Hallitsijakansio",
"Folder Path": "Kansion polku",
"Folder Type": "Folder Type",
"Folders": "Kansiot",
"GUI": "GUI",
"GUI Authentication Password": "GUI:n salasana",
@@ -98,15 +96,12 @@
"Keep Versions": "Säilytä versiot",
"Largest First": "Suurin ensin",
"Last File Received": "Viimeksi vastaanotettu tiedosto",
"Last Scan": "Last Scan",
"Last seen": "Nähty viimeksi",
"Later": "Myöhemmin",
"Listeners": "Listeners",
"Local Discovery": "Paikallinen etsintä",
"Local State": "Paikallinen tila",
"Local State (Total)": "Paikallinen tila (Yhteensä)",
"Major Upgrade": "Pääversion päivitys.",
"Master": "Master",
"Maximum Age": "Maksimi-ikä",
"Metadata Only": "Vain metadata",
"Minimum Free Disk Space": "Vapaan levytilan vähimmäismäärä",
@@ -118,7 +113,6 @@
"Newest First": "Uusin ensin",
"No": "Ei",
"No File Versioning": "Ei tiedostoversiointia",
"Normal": "Normal",
"Notice": "Huomautus",
"OK": "OK",
"Off": "Pois",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing näyttää olevan alhaalla tai internetyhteydessä on ongelma. Yritetään uudelleen...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ei pysty käsittelemään pyyntöäsi. Ole hyvä ja päivitä sivu tai käynnistä Syncthing uudelleen, jos ongelma jatkuu.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthingin hallintakäyttöliittymä on asetettu sallimaan ulkoiset yhteydet ilman salasanaa.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "Yhdistetyt tilastot ovat julkisesti saatavilla osoitteessa {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Asetukset on tallennettu, mutta niitä ei ole otettu käyttöön. Syncthingin täytyy käynnistyä uudelleen, jotta uudet asetukset saadaan käyttöön.",
"The device ID cannot be blank.": "Laitteen ID ei voi olla tyhjä.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
"Compression": "Compression",
"Connection Error": "Erreur de connexion",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copié d'ailleurs",
"Copied from original": "Copié depuis l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -75,7 +74,6 @@
"Folder Label": "Folder Label",
"Folder Master": "Répertoire maître",
"Folder Path": "Chemin du répertoire",
"Folder Type": "Folder Type",
"Folders": "Dossiers",
"GUI": "GUI",
"GUI Authentication Password": "Mot de passe d'authentification GUI",
@@ -98,15 +96,12 @@
"Keep Versions": "Conserver les versions",
"Largest First": "Les plus volumineux en premier",
"Last File Received": "Dernier fichier reçu",
"Last Scan": "Last Scan",
"Last seen": "Dernière apparition",
"Later": "Plus tard",
"Listeners": "Listeners",
"Local Discovery": "Recherche locale",
"Local State": "État local",
"Local State (Total)": "État local (Total)",
"Major Upgrade": "Mise à jour majeure",
"Master": "Master",
"Maximum Age": "Ancienneté maximum",
"Metadata Only": "Métadonnées uniquement",
"Minimum Free Disk Space": "Espace disque libre minimum",
@@ -118,7 +113,6 @@
"Newest First": "Les plus récents en premier",
"No": "Non",
"No File Versioning": "Pas de version de fichier",
"Normal": "Normal",
"Notice": "Notification",
"OK": "OK",
"Off": "Éteint",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plait, rafraichissez la page ou redémarrer Syncthing si le problème persiste.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
"The device ID cannot be blank.": "L'ID de l'appareil ne peut être vide.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
"Compression": "Compression",
"Connection Error": "Erreur de connexion",
"Connection Type": "Type de connexion",
"Copied from elsewhere": "Copié d'ailleurs",
"Copied from original": "Copié depuis l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016, les contributeurs suivants:",
@@ -56,9 +55,9 @@
"Edit Device": "Éditer la machine",
"Edit Folder": "Éditer le dossier",
"Editing": "Édition",
"Enable NAT traversal": "Activer transfert d'adresses (NAT)",
"Enable NAT traversal": "Activer le transfert NAT",
"Enable Relaying": "Activer le relayage",
"Enable UPnP": "Activer UPnP",
"Enable UPnP": "Activer l'UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://host:port\") séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
"Enter ignore patterns, one per line.": "Entrer les masques de filtrage, un par ligne.",
"Error": "Erreur",
@@ -75,7 +74,6 @@
"Folder Label": "Étiquette du dossier",
"Folder Master": "Dossier maître",
"Folder Path": "Chemin du dossier",
"Folder Type": "Type de répertoire",
"Folders": "Dossiers",
"GUI": "GUI",
"GUI Authentication Password": "Mot de passe d'authentification GUI",
@@ -98,15 +96,12 @@
"Keep Versions": "Conserver les versions",
"Largest First": "Les plus volumineux en premier",
"Last File Received": "Dernier fichier reçu",
"Last Scan": "Dernière analyse",
"Last seen": "Dernière apparition",
"Later": "Plus tard",
"Listeners": "Systèmes en écoute",
"Local Discovery": "Recherche locale",
"Local State": "État local",
"Local State (Total)": "État local (Total)",
"Major Upgrade": "Mise à jour majeure",
"Master": "Maitre",
"Maximum Age": "Ancienneté maximum",
"Metadata Only": "Métadonnées uniquement",
"Minimum Free Disk Space": "Espace disque libre minimum",
@@ -118,7 +113,6 @@
"Newest First": "Les plus récents en premier",
"No": "Non",
"No File Versioning": "Pas de version de fichier",
"Normal": "Normal",
"Notice": "Notification",
"OK": "OK",
"Off": "Éteint",
@@ -158,7 +152,7 @@
"Reused": "Réutilisé",
"Save": "Sauver",
"Scan Time Remaining": "Intervalle entre chaque analyse",
"Scanning": "Analyse en cours",
"Scanning": "En cours d'analyse",
"Select the devices to share this folder with.": "Sélectionner les machines avec qui partager ce dossier.",
"Select the folders to share with this device.": "Sélectionner les dossiers à partager avec cette machine.",
"Settings": "Configuration",
@@ -185,7 +179,7 @@
"Stopped": "Arrêté",
"Support": "Aide",
"Sync Protocol Listen Addresses": "Adresse d'écoute du protocole de synchronisation",
"Syncing": "Synchronisation en cours",
"Syncing": "En cours de synchronisation",
"Syncthing has been shut down.": "Syncthing a été éteint.",
"Syncthing includes the following software or portions thereof:": "Syncthing intègre les logiciels suivants (ou des éléments provenant de ces logiciels) :",
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plait, rafraichissez la page ou redémarrer Syncthing si le problème persiste.",
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interface d'administration de Syncthing est paramétrée pour autoriser les accès à distance sans mot de passe.",
"The aggregated statistics are publicly available at the URL below.": "Les statistiques agrégées sont disponibles publiquement à l'adresse ci-dessous.",
"The aggregated statistics are publicly available at {%url%}.": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
"The device ID cannot be blank.": "L'ID de la machine ne peut être vide.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Kommentaar, wannear as brûkt by it begjin fan in rige",
"Compression": "Kompresje",
"Connection Error": "Ferbiningsflater",
"Connection Type": "Ferbiningstype",
"Copied from elsewhere": "Oernommen fan earne oars",
"Copied from original": "Oernommen fan orizjineel",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 de folgende bydragers:",
@@ -56,7 +55,7 @@
"Edit Device": "Apparaat bewurkje",
"Edit Folder": "Map bewurkje",
"Editing": "Bewurkjen",
"Enable NAT traversal": "NAT-trochkruse ynskeakelje",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Trochjaan tastean",
"Enable UPnP": "UPnP oansette",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Fier troch komma's skieden (\"tcp://ip:port\", \"tcp://host:port\") adressen yn of \"dynamic\" om automatyske ûntdekking fan it adres út te fieren.",
@@ -75,7 +74,6 @@
"Folder Label": "Map-opskrift",
"Folder Master": "Map-master",
"Folder Path": "Map-paad",
"Folder Type": "Maptype",
"Folders": "Mappen",
"GUI": "GUI",
"GUI Authentication Password": "Wachtwurd foar ferifikaasje yn GUI",
@@ -98,15 +96,12 @@
"Keep Versions": "Ferzjes bewarje",
"Largest First": "Grutste earst",
"Last File Received": "Leste triem ûntfongen",
"Last Scan": "Lêst Skent",
"Last seen": "Lêst sjoen",
"Later": "Letter",
"Listeners": "Harkers",
"Local Discovery": "Lokale ûntdekking",
"Local State": "Lokale tastân",
"Local State (Total)": "Lokale tastân (Folledich)",
"Major Upgrade": "Wichtige fernijing",
"Master": "Master",
"Maximum Age": "Maksimale âldens",
"Metadata Only": "Allinnich metadata",
"Minimum Free Disk Space": "Minimale frije skiifromte",
@@ -118,7 +113,6 @@
"Newest First": "Nijste earst",
"No": "Nee",
"No File Versioning": "Gjin triemferzjebehear",
"Normal": "Normaal",
"Notice": "Notysje",
"OK": "Okee",
"Off": "Ut",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "It liket dêrop dat Syncthing op dit stuit net rint, of der is in swierrichheid mei jo ynternetferbining. Wurd no opnij besocht...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "It liket dêrop dat Syncthing swierrichheden ûnderfynt mei it ferwurkjen fan jo fersyk. Graach de stee ferfarskje of Syncthing werstarte as it probleem der bliuwt.",
"The Syncthing admin interface is configured to allow remote access without a password.": "De Syncthing haadbrûker-ynterfaasje is sa ynstelt dat tagong fan ôfstân sûnder wachtwurd tastean is.",
"The aggregated statistics are publicly available at the URL below.": "De fersammele statistiken binnen yn it publyk beskikber fia ûndersteande keppeling.",
"The aggregated statistics are publicly available at {%url%}.": "De fersammele statistiken binnen yn it publyk beskikber op {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De konfiguraasje is bewarre mar noch net aktivearre. Syncthing moat werstarte om de nije konfiguraasje te aktivearren.",
"The device ID cannot be blank.": "It apparaat-ID kin net leech wêze.",

View File

@@ -17,7 +17,7 @@
"Advanced settings": "Haladó beállítások",
"All Data": "Minden adat",
"Allow Anonymous Usage Reporting?": "Engedélyezed a névtelen felhasználási adatok küldését?",
"Alphabetic": "ABC sorrendben",
"Alphabetic": "ABC rendben",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Külső program kezeli a fájl verziókövetést. A fájlt el kell távolítania a szinkronizált mappából.",
"Anonymous Usage Reporting": "Névtelen felhasználási adatok küldése",
"Any devices configured on an introducer device will be added to this device as well.": "A bevezető eszközön beállított minden eszköz hozzá lesz adva ehhez az eszközhöz is.",
@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Megjegyzés, a sor elején használva",
"Compression": "Tömörítés",
"Connection Error": "Kapcsolódási hiba",
"Connection Type": "Kapcsolat típus",
"Copied from elsewhere": "Másolva máshonnan",
"Copied from original": "Másolva az eredetiről",
"Copyright © 2014-2016 the following Contributors:": "Szerzői jog © 2014-2016 az alábbi közreműködők:",
@@ -75,7 +74,6 @@
"Folder Label": "Mappa címke",
"Folder Master": "Központi mappa",
"Folder Path": "Mappa elérési útja",
"Folder Type": "Mappa típus",
"Folders": "Mappák",
"GUI": "Grafikus felület",
"GUI Authentication Password": "Grafikus felület jelszava",
@@ -98,15 +96,12 @@
"Keep Versions": "Megtartott verziók",
"Largest First": "Nagyobb először",
"Last File Received": "Utolsó beérkezett fájl",
"Last Scan": "Utolsó vizsgálat",
"Last seen": "Utoljára látva",
"Later": "Később",
"Listeners": "Kapcsolatok",
"Local Discovery": "Helyi felfedezés",
"Local State": "Helyi állapot",
"Local State (Total)": "Helyi állapot (Teljes)",
"Major Upgrade": "Főverzió frissítés",
"Master": "Központi",
"Maximum Age": "Maximális kor",
"Metadata Only": "Csak metaadatok",
"Minimum Free Disk Space": "Minimális szabad lemezterület",
@@ -118,7 +113,6 @@
"Newest First": "Újabb először",
"No": "Nem",
"No File Versioning": "Nincs fájl verziókövetés",
"Normal": "Normál",
"Notice": "Megjegyzés",
"OK": "Rendben",
"Off": "Kikapcsolva",
@@ -177,7 +171,7 @@
"Shutdown Complete": "Leállítás kész",
"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)",
"Smallest First": "Kisebb először",
"Smallest First": "Kisebb előbb",
"Source Code": "Forráskód",
"Staggered File Versioning": "Többszintű fájl verziókövetés",
"Start Browser": "Böngésző indítása",
@@ -193,7 +187,6 @@
"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 a 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.": "Ú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 Syncthing admin interface is configured to allow remote access without a password.": "A Syncthing adminisztrációs felületének távoli elérése be van kapcsolva jelszó nélkül.",
"The aggregated statistics are publicly available at the URL below.": "Az összesített statisztikák elérhetők az alábbi címen.",
"The 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 device ID cannot be blank.": "Az eszköz azonosító nem lehet üres.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Komentar, digunakan saat awal baris",
"Compression": "Kompresi",
"Connection Error": "Koneksi Galat",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Tersalin dari tempat lain",
"Copied from original": "Tersalin dari asal",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -75,7 +74,6 @@
"Folder Label": "Folder Label",
"Folder Master": "Master Folder",
"Folder Path": "Path Folder",
"Folder Type": "Folder Type",
"Folders": "Folder",
"GUI": "GUI",
"GUI Authentication Password": "Sandi Otentikasi GUI",
@@ -98,15 +96,12 @@
"Keep Versions": "Keep Versions",
"Largest First": "Largest First",
"Last File Received": "Last File Received",
"Last Scan": "Last Scan",
"Last seen": "Last seen",
"Later": "Later",
"Listeners": "Listeners",
"Local Discovery": "Local Discovery",
"Local State": "Local State",
"Local State (Total)": "Local State (Total)",
"Major Upgrade": "Major Upgrade",
"Master": "Master",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
"Minimum Free Disk Space": "Minimum Free Disk Space",
@@ -118,7 +113,6 @@
"Newest First": "Newest First",
"No": "No",
"No File Versioning": "No File Versioning",
"Normal": "Normal",
"Notice": "Notice",
"OK": "OK",
"Off": "Off",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
"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.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "The aggregated statistics are publicly available at {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
"The device ID cannot be blank.": "The device ID cannot be blank.",

View File

@@ -13,7 +13,7 @@
"Address": "Indirizzo",
"Addresses": "Indirizzi",
"Advanced": "Avanzato",
"Advanced Configuration": "Configurazione Avanzata",
"Advanced Configuration": "Configurazione avanzata",
"Advanced settings": "Impostazioni avanzate",
"All Data": "Tutti i Dati",
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Per commentare, va inserito all'inizio di una riga",
"Compression": "Compressione",
"Connection Error": "Errore di Connessione",
"Connection Type": "Tipo di Connessione",
"Copied from elsewhere": "Copiato da qualche altra parte",
"Copied from original": "Copiato dall'originale",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 i seguenti Collaboratori:",
@@ -56,35 +55,34 @@
"Edit Device": "Modifica Dispositivo",
"Edit Folder": "Modifica Cartella",
"Editing": "Modifica di",
"Enable NAT traversal": "Abilita NAT traversal",
"Enable Relaying": "Abilita Reindirizzamento",
"Enable NAT traversal": "Abilita NAT trasversale",
"Enable Relaying": "Abilita relaying",
"Enable UPnP": "Attiva UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserisci indirizzi separati da virgola (\"tcp://ip:porta\", \"tcp://host:porta\") oppure \"dynamic\" per effettuare il rilevamento automatico dell'indirizzo.",
"Enter ignore patterns, one per line.": "Inserisci gli schemi di esclusione, uno per riga.",
"Error": "Errore",
"External File Versioning": "Controllo Versione Esterno",
"Failed Items": "Elementi Errati",
"File Pull Order": "Ordine Prelievo File",
"File Versioning": "Controllo Versione File",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Il software ignora i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
"Failed Items": "Elementi errati",
"File Pull Order": "Ordine di prelievo dei file",
"File Versioning": "Controllo Versione dei File",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Il software evita i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "I file sono spostati nella certella .stversions quando vengono sostituiti o cancellati da Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "I file sostituiti o eliminati da Syncthing vengono datati e spostati in una cartella .stversions.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "I file sono protetti dalle modifiche effettuate negli altri dispositivi, ma le modifiche effettuate in questo dispositivo verranno inviate anche al resto del cluster.",
"Folder": "Cartella",
"Folder ID": "ID Cartella",
"Folder Label": "Etichetta per la Cartella",
"Folder Label": "Etichetta per la cartella",
"Folder Master": "Cartella Principale",
"Folder Path": "Percorso Cartella",
"Folder Type": "Tipo di Cartella",
"Folders": "Cartelle",
"GUI": "Interfaccia Grafica Utente",
"GUI Authentication Password": "Password dell'Interfaccia Grafica",
"GUI": "Interfaccia grafica utente",
"GUI Authentication Password": "Password di Autenticazione dell'Utente",
"GUI Authentication User": "Utente dell'Interfaccia Grafica",
"GUI Listen Addresses": "Indirizzi dell'Interfaccia Grafica",
"Generate": "Genera",
"Global Discovery": "Individuazione Globale",
"Global Discovery Server": "Server di Individuazione Globale",
"Global Discovery Servers": "Server di Individuazione Globale",
"Global Discovery Servers": "Servers di Individuazione Globale",
"Global State": "Stato Globale",
"Help": "Aiuto",
"Home page": "Pagina home",
@@ -92,59 +90,55 @@
"Ignore Patterns": "Schemi Esclusione File",
"Ignore Permissions": "Ignora Permessi",
"Incoming Rate Limit (KiB/s)": "Limite Velocità in Ingresso (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configurazione non corretta potrebbe danneggiare il contenuto delle cartelle e rendere Syncthing inoperativo.",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configurazione incorretta potrebbe danneggiare il contenuto delle cartelle e rendere Syncthing inoperativo.",
"Introducer": "Introduttore",
"Inversion of the given condition (i.e. do not exclude)": "Inversione della condizione indicata (ad es. non escludere)",
"Keep Versions": "Versioni Mantenute",
"Largest First": "Prima il più grande",
"Last File Received": "Ultimo File Ricevuto",
"Last Scan": "Ultima scansione",
"Last seen": "Ultima connessione",
"Later": "Più Tardi",
"Listeners": "In ascolto",
"Local Discovery": "Individuazione Locale",
"Local State": "Stato Locale",
"Local State (Total)": "Stato Locale (Totale)",
"Major Upgrade": "Aggiornamento Principale",
"Master": "Principale",
"Major Upgrade": "Aggiornamento principale",
"Maximum Age": "Durata Massima",
"Metadata Only": "Solo i Metadati",
"Minimum Free Disk Space": "Minimo Spazio Libero su Disco",
"Minimum Free Disk Space": "Minimo spazio libero su disco",
"Move to top of queue": "Posiziona in cima alla coda",
"Multi level wildcard (matches multiple directory levels)": "Metacarattere multi-livello (per corrispondenze in più livelli di cartelle)",
"Multi level wildcard (matches multiple directory levels)": "Metacarattere multi-livello (corrisponde alle cartelle e alle sotto-cartelle)",
"Never": "Mai",
"New Device": "Nuovo Dispositivo",
"New Folder": "Nuova Cartella",
"Newest First": "Prima il più recente",
"No": "No",
"No File Versioning": "Nessun Controllo Versione",
"Normal": "Normale",
"Notice": "Avviso",
"OK": "OK",
"Off": "Disattiva",
"Oldest First": "Prima il Meno Recente",
"Oldest First": "Prima il meno recente",
"Optional descriptive label for the folder. Can be different on each device.": "Etichetta descrittiva facoltativa della cartella. Può essere diversa su ogni dispositivo.",
"Options": "Opzioni",
"Out of Sync": "Non sincronizzato",
"Out of Sync Items": "Elementi Non Sincronizzati",
"Outgoing Rate Limit (KiB/s)": "Limite Velocità in Uscita (KiB/s)",
"Override Changes": "Ignora le 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 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": "Pausa",
"Paused": "In Pausa",
"Paused": "In pausa",
"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 set a GUI Authentication User and Password in the Settings dialog.": "Per favore impostare Utente e Password dell'Interfaccia Grafica nelle Impostazioni.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Per favore impostare nome utente e password dell'interfaccia grafica utente.",
"Please wait": "Attendere prego",
"Preview": "Anteprima",
"Preview Usage Report": "Anteprima Statistiche di Utilizzo",
"Quick guide to supported patterns": "Guida veloce agli schemi supportati",
"RAM Utilization": "Utilizzo RAM",
"Random": "Casuale",
"Relay Servers": "Server di Reindirizzamento",
"Relay Servers": "Servers di relay",
"Relayed via": "Reindirizzato tramite",
"Relays": "Reindirizzamenti",
"Release Notes": "Note di Rilascio",
"Relays": "Servers di reindirizzamento",
"Release Notes": "Note di rilascio",
"Remote Devices": "Dispositivi Remoti",
"Remove": "Rimuovi",
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificatore obbligatorio della cartella. Deve essere lo stesso su tutti i dispositivi del cluster.",
@@ -157,7 +151,7 @@
"Resume": "Riprendi",
"Reused": "Riutilizzato",
"Save": "Salva",
"Scan Time Remaining": "Tempo di Scansione Rimanente",
"Scan Time Remaining": "Tempo di scansione rimanente",
"Scanning": "Scansione in corso",
"Select the devices to share this folder with.": "Seleziona i dispositivi con i quali condividere questa cartella.",
"Select the folders to share with this device.": "Seleziona le cartelle da condividere con questo dispositivo.",
@@ -176,7 +170,7 @@
"Shutdown": "Arresta",
"Shutdown Complete": "Arresto Eseguito",
"Simple File Versioning": "Controllo Versione Semplice",
"Single level wildcard (matches within a directory only)": "Metacarattere di singolo livello (per corrispondenze solo all'interno di una cartella)",
"Single level wildcard (matches within a directory only)": "Metacarattere di singolo livello (corrisponde solo all'interno di una cartella)",
"Smallest First": "Prima il più piccolo",
"Source Code": "Codice Sorgente",
"Staggered File Versioning": "Controllo Versione Cadenzato",
@@ -191,15 +185,14 @@
"Syncthing is restarting.": "Riavvio di Syncthing in corso.",
"Syncthing is upgrading.": "Aggiornamento di Syncthing in corso.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing sembra inattivo, oppure c'è un problema con la tua connessione a Internet. Nuovo tentativo…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Sembra che Syncthing abbia problemi nell'elaborazione della tua richiesta. Aggiorna la pagina o riavvia Syncthing se il problema persiste.",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Sembra che Syncthing non sia in grado di elaborare il tuo comando. Se il problema persiste prova a ricaricare la pagina nel tuo navigatore oppure prova a riavviare Syncthing.",
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfaccia di amministrazione di Syncthing è configurata in modo da permettere l'accesso senza password.",
"The aggregated statistics are publicly available at the URL below.": "Le statistiche aggregate sono disponibili pubblicamente all'URL seguente.",
"The aggregated statistics are publicly available at {%url%}.": "Le statistiche aggregate sono disponibili pubblicamente su {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Syncthing deve essere riavviato per attivare la nuova configurazione.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Devi riavviare Syncthing per attivare la nuova configurazione.",
"The device ID cannot be blank.": "L'ID del dispositivo non può essere vuoto.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID del dispositivo da inserire qui può essere trovato nella finestra \"Azioni> Mostra ID\" sull'altro dispositivo. Gli spazi e i trattini sono opzionali (ignorati).",
"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).": "L'ID del dispositivo da inserire qui può essere trovato nella finestra \"Modifica> Mostra ID\" sull'altro dispositivo. Gli spazi e i trattini sono opzionali (ignorati).",
"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.": "Quotidianamente il software invia le statistiche di utilizzo in forma criptata. Questi dati riguardano i sistemi operativi utilizzati, le dimensioni delle cartelle e le versioni del software. Se i set di dati riportati vengono modificati, verrà mostrata nuovamente questa finestra di dialogo.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Trova l'ID nella finestra di dialogo \"Modifica > Mostra ID\" dell'altro dispositivo, poi inseriscilo qui. Gli spazi e i trattini sono opzionali (ignorati).",
"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).": "Trova l'ID nella finestra di dialogo \"Modifica > Mostra ID\" dell'altro dispositivo, poi inseriscilo qui. Gli spazi e i trattini sono opzionali (ignorati).",
"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.": "Quotidianamente il software invia le statistiche di utilizzo in forma criptata. Questi dati riguardano i sistemi operativi utilizzati, le dimensioni delle cartelle e le versioni del software. Se i dati riportati sono cambiati, verrà mostrata di nuovo questa finestra di dialogo.",
"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.": "L'ID del dispositivo inserito non sembra valido. Dovrebbe essere una stringa di 52 o 56 caratteri costituita da lettere e numeri, con spazi e trattini opzionali.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Il primo parametro della riga di comando è il percorso della cartella e il secondo parametro è il percorso relativo nella cartella.",
"The folder ID cannot be blank.": "L'ID della cartella non può essere vuoto.",
@@ -207,7 +200,7 @@
"The folder ID must be unique.": "L'ID della cartella dev'essere unico.",
"The folder path cannot be blank.": "Il percorso della cartella non può essere vuoto.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Vengono utilizzati i seguenti intervalli temporali: per la prima ora viene mantenuta una versione ogni 30 secondi, per il primo giorno viene mantenuta una versione ogni ora, per i primi 30 giorni viene mantenuta una versione al giorno, successivamente viene mantenuta una versione ogni settimana fino al periodo massimo impostato.",
"The following items could not be synchronized.": "Non è stato possibile sincronizzare i seguenti elementi.",
"The following items could not be synchronized.": "Non è stato possibile sincronizzare i seguenti elementi",
"The maximum age must be a number and cannot be blank.": "La durata massima dev'essere un numero e non può essere vuoto.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La durata massima di una versione (in giorni, imposta a 0 per mantenere le versioni per sempre).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Lo spazio libero minimo su disco deve essere un numero non negativo tra 0 e 100 (inclusi)",
@@ -216,8 +209,8 @@
"The number of old versions to keep, per file.": "Il numero di vecchie versioni da mantenere, per file.",
"The number of versions must be a number and cannot be blank.": "Il numero di versioni dev'essere un numero e non può essere vuoto.",
"The path cannot be blank.": "Il percorso non può essere vuoto.",
"The rate limit must be a non-negative number (0: no limit)": "Il limite di banda deve essere un numero non negativo (0: nessun limite)",
"The rescan interval must be a non-negative number of seconds.": "L'intervallo di scansione deve essere un numero non negativo secondi.",
"The rate limit must be a non-negative number (0: no limit)": "Il limite di banda deve essere un numero non negativo (da 0 a infinito)",
"The rescan interval must be a non-negative number of seconds.": "L'intervallo di scansione deve essere un numero superiore a zero secondi.",
"They are retried automatically and will be synced when the error is resolved.": "Verranno effettuati tentativi in automatico e verranno sincronizzati quando l'errore sarà risolto.",
"This Device": "Questo Dispositivo",
"This can easily give hackers access to read and change any files on your computer.": "Ciò potrebbe facilmente permettere agli hackers accesso alla lettura e modifica di qualunque file del tuo computer.",
@@ -238,7 +231,7 @@
"Versions Path": "Percorso Cartella Versioni",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Le versioni vengono eliminate automaticamente se superano la durata massima o il numero di file permessi in un determinato intervallo temporale.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Attenzione, questo percorso è una sottocartella di una cartella esistente \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando si aggiunge un nuovo dispositivo, tenere presente che il dispositivo deve essere aggiunto anche dall'altra parte.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Anche nel nuovo dispositivo devi aggiungere l'ID di questo, con la stessa procedura.",
"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ì",
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
@@ -247,5 +240,5 @@
"items": "elementi",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vuole condividere la cartella \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderLabel}}\" ({{folder}})."
}

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "行頭で使用された場合、コメント行",
"Compression": "圧縮",
"Connection Error": "接続エラー",
"Connection Type": "接続種別",
"Copied from elsewhere": "別ファイルからコピー済",
"Copied from original": "元ファイルからコピー済",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -75,7 +74,6 @@
"Folder Label": "フォルダー名",
"Folder Master": "フォルダーのマスター",
"Folder Path": "フォルダーパス",
"Folder Type": "フォルダーの種類",
"Folders": "フォルダー",
"GUI": "GUI",
"GUI Authentication Password": "GUI認証パスワード",
@@ -98,27 +96,23 @@
"Keep Versions": "保存するバージョンの数",
"Largest First": "大きい順",
"Last File Received": "最後に受信したファイル",
"Last Scan": "最終スキャン時刻",
"Last seen": "最終接続日時",
"Later": "後で設定",
"Listeners": "待ち受けポート",
"Local Discovery": "LAN内で探索",
"Local State": "ローカル状態",
"Local State (Total)": "ローカル状態 (合計)",
"Major Upgrade": "メジャーアップグレード",
"Master": "マスター",
"Maximum Age": "最大寿命",
"Metadata Only": "メタデータのみ",
"Minimum Free Disk Space": "同期を停止する最小空きディスク容量",
"Move to top of queue": "最優先にする",
"Multi level wildcard (matches multiple directory levels)": "多階層ワイルドカード (複数のディレクトリ階層にマッチします)",
"Never": "記録なし",
"Never": "接続記録なし",
"New Device": "新規デバイス",
"New Folder": "新規フォルダー",
"Newest First": "新しい順",
"No": "いいえ",
"No File Versioning": "バージョン管理をしない",
"Normal": "通常",
"Notice": "通知",
"OK": "OK",
"Off": "オフ",
@@ -193,13 +187,12 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthingが落ちているか、インターネット接続に問題があります。リトライ中です…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "リクエストの処理に問題があるようです。問題が継続する場合、ページを更新するかSyncthingを再起動してください。",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthingの管理画面が、パスワードなしで外部からアクセスできるように設定されています。",
"The aggregated statistics are publicly available at the URL below.": "集計結果は次のURLで公開されています。",
"The aggregated statistics are publicly available at {%url%}.": "集計結果は {{url}} でどなたでもご覧いただけます。",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "設定が保存されましたが、まだ有効になっていません。新しい設定を有効にするにはSyncthingを再起動してください。",
"The device ID cannot be blank.": "デバイスIDは空欄にできません。",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ここに入力するデバイスIDは、接続したい相手側デバイスの [メニュー]→[IDを表示] で確認することができます。スペースとハイフンは入力しなくてもかまいません。",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ここに入力するデバイスIDは、接続したい相手側デバイスの [メニュー]→[IDを表示] で確認することができます。スペースとハイフンは入力しなくてもかまいません。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "利用状況レポートは暗号化されて毎日送信されます。この情報はプラットフォーム、フォルダの大きさ、アプリのバージョンを調査するために使われます。送信するデータセットが変更された場合、このダイアログで再度確認が求められます。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "利用状況レポートは暗号化されて毎日送信されます。この情報はプラットフォーム、フォルダの大きさ、アプリのバージョンを調査するために使われます。レポートのデータが変更された場合、このダイアログがまた表示されます。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "入力されたデバイスIDが正しくありません。デバイスIDは、52文字または56文字のアルファベットと数字からなる文字列です。スペースとハイフンは入力してもしなくてもかまいません。",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "第1コマンドライン引数はフォルダーのパス、第2引数はフォルダー内の相対パスです。",
"The folder ID cannot be blank.": "フォルダーIDは空欄にできません。",

View File

@@ -1,6 +1,6 @@
{
"A device with that ID is already added.": "이 기기 ID는 이미 추가되었습니다.",
"A negative number of days doesn't make sense.": "음수로는 지정할 수 없습니다.",
"A device with that ID is already added.": "A device with that ID is already added.",
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
"A new major version may not be compatible with previous versions.": "새로운 메이저 버전은 이전 버전과 호환되지 않을 수 있습니다.",
"API Key": "API 키",
"About": " 정보",
@@ -8,13 +8,13 @@
"Add": "추가",
"Add Device": "기기 추가",
"Add Folder": "폴더 추가",
"Add Remote Device": "다른 기기 추가",
"Add Remote Device": "Add Remote Device",
"Add new folder?": "새로운 폴더를 추가하시겠습니까?",
"Address": "주소",
"Addresses": "주소",
"Advanced": "고급",
"Advanced Configuration": "고급 설정",
"Advanced settings": "고급 설정",
"Advanced": "Advanced",
"Advanced Configuration": "Advanced Configuration",
"Advanced settings": "Advanced settings",
"All Data": "전체 데이터",
"Allow Anonymous Usage Reporting?": "익명 사용 보고서를 보내시겠습니까?",
"Alphabetic": "알파벳순",
@@ -22,32 +22,31 @@
"Anonymous Usage Reporting": "익명 사용 보고서",
"Any devices configured on an introducer device will be added to this device as well.": "유도 장치에 추가된 기기들은 이 기기에도 동시에 추가됩니다.",
"Automatic upgrades": "자동 업데이트",
"Be careful!": "주의!",
"Be careful!": "Be careful!",
"Bugs": "버그",
"CPU Utilization": "CPU 사용률",
"Changelog": "바뀐 점",
"Clean out after": "삭제 후",
"Clean out after": "Clean out after",
"Close": "닫기",
"Command": "커맨드",
"Comment, when used at the start of a line": "명령행에서 시작을 할수 있어요.",
"Compression": "압축",
"Connection Error": "연결 에러",
"Connection Type": "연결 종류",
"Copied from elsewhere": "다른 곳에서 복사됨",
"Copied from original": "원본에서 복사됨",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
"Danger!": "경고!",
"Danger!": "Danger!",
"Delete": "삭제",
"Deleted": "삭제됨",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "다른 기기 {{device}} ({{address}}) 에서 접속을 요청했습니다. 새 장치를 추가하시겠습니까?",
"Deleted": "Deleted",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
"Device ID": "기기 ID",
"Device Identification": "기기 식별자",
"Device Name": "기기 이름",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "다른 기기 {{device}} ({{address}}) 에서 접속을 요청했습니다. 새 장치를 추가하시겠습니까?",
"Devices": "기기",
"Disconnected": "연결 끊김",
"Discovery": "탐색",
"Discovery": "Discovery",
"Documentation": "문서",
"Download Rate": "다운로드 속도",
"Downloaded": "다운로드됨",
@@ -56,26 +55,25 @@
"Edit Device": "기기 편집",
"Edit Folder": "폴더 편집",
"Editing": "편집",
"Enable NAT traversal": "NAT traversal 활성화",
"Enable Relaying": "Relaying 활성화",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable UPnP": "UPnP 활성화",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "주소 자동 검색을 하기 위해서는 \"ip:port\" 형식의 주소들을 쉼표로 구분해서 입력하거나 \"dynamic\"을 입력하세요.",
"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.": "무시할 패턴을 한 줄에 하나씩 입력하세요.",
"Error": "오류",
"External File Versioning": "외부 파일 버전 관리",
"Failed Items": "실패한 항목",
"Failed Items": "Failed Items",
"File Pull Order": "파일 동기화 순서",
"File Versioning": "파일 버전 관리",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "파일을 동기화할 때 파일 권한이 무시됩니다. FAT 파일 시스템에서 사용하세요.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "파일이 Syncthing에 의해서 교체되거나 삭제되면 .stversions 폴더로 이동됩니다.",
"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.": "파일이 Syncthing에 의해서 교체되거나 삭제되면 .stversions 폴더에 있는 날짜가 바뀐 버전으로 이동됩니다.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "다른 장치가 파일을 편집할 수 없으며 반드시 이 장치의 내용을 기준으로 동기화합니다.",
"Folder": "폴더",
"Folder": "Folder",
"Folder ID": "폴더 ID",
"Folder Label": "폴더 라벨",
"Folder Label": "Folder Label",
"Folder Master": "폴더 소유자",
"Folder Path": "폴더 경로",
"Folder Type": "폴더 유형",
"Folders": "폴더",
"GUI": "GUI",
"GUI Authentication Password": "GUI 인증 비밀번호",
@@ -84,32 +82,29 @@
"Generate": "생성",
"Global Discovery": "글로벌 탐색",
"Global Discovery Server": "글로벌 탐색 서버",
"Global Discovery Servers": "글로벌 탐색 서버",
"Global Discovery Servers": "Global Discovery Servers",
"Global State": "글로벌 서버 상태",
"Help": "도움말",
"Home page": "홈페이지",
"Home page": "Home page",
"Ignore": "무시",
"Ignore Patterns": "패턴 무시",
"Ignore Permissions": "권한 무시",
"Incoming Rate Limit (KiB/s)": "다운로드 속도 제한 (KiB/S)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "잘못된 설정은 폴더의 컨텐츠를 훼손하거나 Syncthing의 오작동을 일으킬 수 있습니다.",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
"Introducer": "유도",
"Inversion of the given condition (i.e. do not exclude)": "주어진 조건의 반대 (전혀 배제하지 않음)",
"Inversion of the given condition (i.e. do not exclude)": "주어진 조건의 반대(전혀 배제하지 않음)",
"Keep Versions": "버전 보관",
"Largest First": "큰 파일 순",
"Last File Received": "마지막으로 받은 파일",
"Last Scan": "마지막 탐색",
"Last seen": "마지막 접속",
"Later": "나중에",
"Listeners": "수신자",
"Local Discovery": "로컬 노드 검색",
"Local State": "로컬 상태",
"Local State (Total)": "로컬 상태 (합계)",
"Local State (Total)": "Local State (Total)",
"Major Upgrade": "메이저 업데이트",
"Master": "마스터",
"Maximum Age": "최대 보존 기간",
"Metadata Only": "메타데이터만",
"Minimum Free Disk Space": "최소 여유 디스크 용량",
"Minimum Free Disk Space": "Minimum Free Disk Space",
"Move to top of queue": "대기열 상단으로 이동",
"Multi level wildcard (matches multiple directory levels)": "다중 레벨 와일드 카드 (여러 단계의 디렉토리와 일치하는 경우)",
"Never": "사용 안 함",
@@ -118,46 +113,45 @@
"Newest First": "새로운 파일순",
"No": "아니오",
"No File Versioning": "파일 버전 관리 안 함",
"Normal": "일반",
"Notice": "공지",
"OK": "확인",
"Off": "꺼짐",
"Oldest First": "오래된 파일순",
"Optional descriptive label for the folder. Can be different on each device.": "폴더 라벨은 편의를 위한 것입니다. 기기마다 다르게 설정할 수 있습니다.",
"Options": "옵션",
"Out of Sync": "동기화 오류",
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
"Options": "Options",
"Out of Sync": "Out of Sync",
"Out of Sync Items": "동기화되지 않은 항목",
"Outgoing Rate Limit (KiB/s)": "업로드 속도 제한 (KiB/s)",
"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 where versions should be stored (leave empty for the default .stversions folder in the folder).": "버전을 보관할 경로 (비워둘 시 기본값 .stversions 폴더로 지정됨)",
"Pause": "일시 중지",
"Paused": "일시 중지됨",
"Pause": "Pause",
"Paused": "Paused",
"Please consult the release notes before performing a major upgrade.": "메이저 업데이트를 하기 전에 먼저 릴리즈 노트를 살펴보세요.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "설정에서 GUI 인증용 User와 암호를 입력해주세요.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
"Please wait": "기다려 주십시오",
"Preview": "미리보기",
"Preview Usage Report": "사용 보고서 미리보기",
"Quick guide to supported patterns": "지원하는 패턴에 대한 빠른 도움말",
"RAM Utilization": "RAM 사용량",
"Random": "무작위",
"Relay Servers": "중계 서버",
"Relayed via": "중계 중인 서버 주소",
"Relays": "중계",
"Relay Servers": "Relay Servers",
"Relayed via": "Relayed via",
"Relays": "Relays",
"Release Notes": "릴리즈 노트",
"Remote Devices": "원격 기기",
"Remove": "삭제",
"Required identifier for the folder. Must be the same on all cluster devices.": "폴더 식별자가 필요합니다. 모든 장치에서 동일해야 합니다.",
"Remote Devices": "Remote Devices",
"Remove": "Remove",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Rescan": "재탐색",
"Rescan All": "전체 재탐색",
"Rescan Interval": "재탐색 간격",
"Restart": "재시작",
"Restart Needed": "재시작 필요함",
"Restarting": "재시작 중",
"Resume": "재개",
"Resume": "Resume",
"Reused": "재개",
"Save": "저장",
"Scan Time Remaining": "탐색 남은 시간",
"Scan Time Remaining": "Scan Time Remaining",
"Scanning": "탐색중",
"Select the devices to share this folder with.": "이 폴더를 공유할 장치를 선택합니다.",
"Select the folders to share with this device.": "이 장치와 공유할 폴더를 선택합니다.",
@@ -170,7 +164,7 @@
"Shared With": "~와 공유",
"Short identifier for the folder. Must be the same on all cluster devices.": "간단한 폴더 식별자입니다. 모든 장치에서 동일해야 합니다.",
"Show ID": "내 기기 ID",
"Show QR": "QR 코드 보기",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "장치에 대한 아이디로 표시됩니다. 옵션에 얻은 기본이름으로 다른장치에 통보합니다.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "아이디가 비어있는 경우 기본 값으로 다른 장치에 업데이트 됩니다.",
"Shutdown": "종료",
@@ -181,7 +175,7 @@
"Source Code": "소스 코드",
"Staggered File Versioning": "타임스탬프 기준 파일 버전 관리",
"Start Browser": "브라우저 열기",
"Statistics": "통계",
"Statistics": "Statistics",
"Stopped": "중지됨",
"Support": "지원",
"Sync Protocol Listen Addresses": "동기화 프로토콜 수신 주소",
@@ -192,12 +186,11 @@
"Syncthing is upgrading.": "Syncthing이 업데이트 중입니다.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing이 중지되었거나 인터넷 연결에 문제가 있는 것 같습니다. 재시도 중입니다...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing에서 요청을 처리하는 중에 문제가 발생했습니다. 계속 문제가 발생하면 페이지를 다시 불러오거나 Syncthing을 재시작해 보세요.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing 관리자 인터페이스가 암호 없이 원격 접속이 허가되도록 설정되었습니다.",
"The aggregated statistics are publicly available at the URL below.": "수집된 통계는 아래 URL에서 공개적으로 볼 수 있습니다.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The aggregated statistics are publicly available at {%url%}.": "수집된 통계는 {{URL}} 에서 공개적으로 볼 수 있습니다.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "설정이 저장되었지만 활성화되지 않았습니다. 설정을 활성화 하려면 Syncthing을 다시 시작하세요.",
"The device ID cannot be blank.": "기기 ID는 비워 둘 수 없습니다.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "여기에 입력한 기기 ID가 다른 장치의 \"동작 - ID 보기\"에 표시됩니다. 공백과 하이픈은 세지 않습니다. 즉 무시됩니다.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "여기에 입력한 기기 ID가 다른 장치의 \"편집 - ID 보기\"에 표시됩니다. 공백과 하이픈은 세지 않습니다. 즉 무시됩니다.",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "암호화된 사용 보고서는 매일 전송됩니다 사용 중인 플랫폼과 폴더 크기, 앱 버전이 포함되어 있습니다. 전송되는 데이터가 변경되면 다시 이 대화 상자가 나타납니다.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "입력한 기기 ID가 올바르지 않습니다. 52/56자의 알파벳과 숫자로 구성되어 있으며, 공백과 하이픈은 포함되지 않습니다.",
@@ -207,27 +200,27 @@
"The folder ID must be unique.": "폴더 ID는 중복될 수 없습니다.",
"The folder path cannot be blank.": "폴더 경로는 비워 둘 수 없습니다.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "다음과 같은 간격이 사용됩니다: 첫 한 시간 동안은 버전이 매 30초마다 유지되며, 첫 하루 동안은 매 시간, 첫 한 달 동안은 매 일마다 유지됩니다. 그리고 최대 날짜까지는 버전이 매 주마다 유지됩니다.",
"The following items could not be synchronized.": "이 항목들은 동기화 할 수 없습니다.",
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The maximum age must be a number and cannot be blank.": "최대 보존 기간은 숫자여야 하며 비워 둘 수 없습니다.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "버전을 유지할 최대 시간을 지정합니다. 일단위이며 버전을 계속 유지하려면 0을 입력하세요,",
"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 to keep files in the trash can. Zero means 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 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 versions must be a number and cannot be blank.": "버전 개수는 숫자여야 하며 비워 둘 수 없습니다.",
"The path cannot be blank.": "경로는 비워 둘 수 없습니다.",
"The rate limit must be a non-negative number (0: no limit)": "대역폭 제한 설정은 반드시 양수로 입력해야 합니다 (0: 무제한)",
"The 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.": "재검색 간격은 초단위이며 양수로 입력해야 합니다.",
"They are retried automatically and will be synced when the error is resolved.": "오류가 해결되면 자동적으로 동기화 됩니다.",
"This Device": "현재 기기",
"This can easily give hackers access to read and change any files on your computer.": "이 설정은 해커가 손쉽게 사용자 컴퓨터의 모든 파일을 읽고 변경할 수 있도록 할 수 있습니다.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This Device": "This Device",
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
"This is a major version upgrade.": "이 업데이트는 메이저 버전입니다.",
"Trash Can File Versioning": "휴지통을 통한 파일 버전 관리",
"Trash Can File Versioning": "Trash Can File Versioning",
"Unknown": "알 수 없음",
"Unshared": "공유되지 않음",
"Unused": "사용되지 않음",
"Up to Date": "최신 데이터",
"Updated": "업데이트 완료",
"Updated": "Updated",
"Upgrade": "업데이트",
"Upgrade To {%version%}": "{{version}} 으로 업데이트",
"Upgrading": "업데이트 중",
@@ -237,15 +230,15 @@
"Version": "버전",
"Versions Path": "버전 저장 경로",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "최대 보존 기간보다 오래되었거나 지정한 개수를 넘긴 버전은 자동으로 삭제됩니다.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "경고, 이 경로는 현재 존재하는 폴더 \"{{otherFolder}}\" 의 하위 폴더 입니다.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "새 장치를 추가할 시 추가한 기기 쪽에서도 이 장치를 추가해야 합니다.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "새 폴더를 추가할 시 폴더 ID는 장치간에 폴더를 묶을 때 사용됩니다. 대소문자를 구분하며 모든 장치에서 같은 ID를 사용해야 합니다.",
"Yes": "예",
"You must keep at least one version.": "최소 한 개의 버전은 유지해야 합니다.",
"days": "",
"days": "days",
"full documentation": "전체 문서",
"items": "항목",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 에서 폴더 \\\"{{folder}}\\\" 를 공유하길 원합니다.",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} 에서 폴더 \"{{folderLabel}}\" ({{folder}})를 공유하길 원합니다.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 에서 폴더 \"{{folderLabel}}\" ({{folder}})를 공유하길 원합니다."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
"Compression": "Kompresija",
"Connection Error": "Susijungimo klaida",
"Connection Type": "Ryšio tipas",
"Copied from elsewhere": "Nukopijuota iš kitur",
"Copied from original": "Nukopijuota iš originalo",
"Copyright © 2014-2016 the following Contributors:": "Autorių teisės © 2014-2016 šių bendraautorių:",
@@ -75,7 +74,6 @@
"Folder Label": "Aplanko etiketė",
"Folder Master": "Aplanko vadovas",
"Folder Path": "Kelias iki aplanko",
"Folder Type": "Aplanko tipas",
"Folders": "Aplankai",
"GUI": "Valdymo skydelis",
"GUI Authentication Password": "Valdymo skydelio slaptažodis",
@@ -98,15 +96,12 @@
"Keep Versions": "Saugojamų versijų kiekis",
"Largest First": "Didžiausi pirmiau",
"Last File Received": "Paskutinis priimtas failas",
"Last Scan": "Paskutinis nuskaitymas",
"Last seen": "Paskutinį kartą matytas",
"Later": "Vėliau",
"Listeners": "Klausytojai",
"Local Discovery": "Vietinis matomumas",
"Local State": "Vietinė būsena",
"Local State (Total)": "Vietinė būsena (Bendrai)",
"Major Upgrade": "Stambus atnaujinimas",
"Master": "Pagrindinis",
"Maximum Age": "Maksimalus amžius",
"Metadata Only": "Metaduomenims",
"Minimum Free Disk Space": "Minimum laisvos vietos diske",
@@ -118,7 +113,6 @@
"Newest First": "Naujausi pirmiau",
"No": "Ne",
"No File Versioning": "Nėra versijų valdymo",
"Normal": "Normalus",
"Notice": "Įspėjimas",
"OK": "Gerai",
"Off": "Netaikoma",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing išjungta arba problemos su Interneto ryšių. Bandoma iš naujo...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Atrodo, kad Syncthing, vykdydamas jūsų užklausą, susidūrė su problemomis. Prašome iš naujo įkelti puslapį, arba jei problema išlieka, iš naujo paleisti Syncthing.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratoriaus sąsaja yra sukonfigūruota taip, kad be slaptažodžio leistų nuotolinę prieigą.",
"The aggregated statistics are publicly available at the URL below.": "Naudojimosi ataskaitą galite peržiūrėti žemiau nurodytu URL adresu.",
"The aggregated statistics are publicly available at {%url%}.": "Naudojimosi ataskaitą galite peržiūrėti adresu: {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Nauji nustatymai išsaugoti, bet neaktyvuoti. Perleiskite Syncthing programą iš naujo norėdami įgalinti naujus nustatymus.",
"The device ID cannot be blank.": "Įrenginio ID negali būti tuščias.",
@@ -219,7 +212,7 @@
"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.",
"They are retried automatically and will be synced when the error is resolved.": "Failus bus automatiškai bandoma parsiųsti dar kartą kai išspręsite klaidas",
"This Device": "Šis įrenginys",
"This Device": "This Device",
"This can easily give hackers access to read and change any files on your computer.": "Tai gali suteikti programišiams lengvą prieigą skaityti ir keisti bet kokius failus jūsų kompiuteryje.",
"This is a major version upgrade.": "Tai yra stambus atnaujinimas.",
"Trash Can File Versioning": "Šiukšliadėžės versijų valdymas",

View File

@@ -8,13 +8,13 @@
"Add": "Legg til",
"Add Device": "Legg til Enhet",
"Add Folder": "Legg til Mappe",
"Add Remote Device": "Legg til ekstern enhet",
"Add Remote Device": "Add Remote Device",
"Add new folder?": "Legg til ny mappe?",
"Address": "Adresse",
"Addresses": "Adresser",
"Advanced": "Avansert",
"Advanced Configuration": "Avanserte Innstillinger",
"Advanced settings": "Avanserte innstillinger ",
"Advanced settings": "Advanced settings",
"All Data": "Alle data",
"Allow Anonymous Usage Reporting?": "Tillat Anonym Innsamling Av Brukerdata?",
"Alphabetic": "Alfabetisk",
@@ -32,15 +32,14 @@
"Comment, when used at the start of a line": "Kommentar, når det blir brukt i starten av en linje.",
"Compression": "Komprimering",
"Connection Error": "Tilkoblingsfeil",
"Connection Type": "Tilkoblingstype",
"Copied from elsewhere": "Kopiert fra et annet sted",
"Copied from original": "Kopiert fra original",
"Copyright © 2014-2016 the following Contributors:": "Opphavsrett © 2014-2016 for følgende bidragsytere:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2015 the following Contributors:": "Opphavsrett © 2015 de følgende bidragsytere:",
"Danger!": "Fare!",
"Delete": "Slett",
"Deleted": "Slettet",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} {{address}}) ønsker å koble til. Legge til ny enhet?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
"Device ID": "Enhets ID",
"Device Identification": "Enhetskjennemerke",
"Device Name": "Navn på Enhet",
@@ -56,7 +55,7 @@
"Edit Device": "Rediger Enhet",
"Edit Folder": "Rediger Mappe",
"Editing": "Redigerer",
"Enable NAT traversal": "Slå på NAT traversering",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Aktiver relésending",
"Enable UPnP": "Aktiver UPnP",
"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 ordet \"dynamic\" for å gjøre automatisk oppslag for adressen.",
@@ -72,10 +71,9 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskyttet mot endringer som er gjort på andre enheter, men endringer som er gjort på denne enheten blir sendt til resten av gruppen.",
"Folder": "Katalog",
"Folder ID": "Mappe ID",
"Folder Label": "Merkelapp for katalog",
"Folder Label": "Folder Label",
"Folder Master": "Styrende Mappe",
"Folder Path": "Mappeplassering",
"Folder Type": "Katalogtype",
"Folders": "Mapper",
"GUI": "grafisk brukergrensesnitt",
"GUI Authentication Password": "Passord for GUI-autenisering",
@@ -97,16 +95,13 @@
"Inversion of the given condition (i.e. do not exclude)": "Invers av den gitte tilstanden (t.d. ikke ekskluder)",
"Keep Versions": "Behold Versjoner",
"Largest First": "Største fil",
"Last File Received": "Siste mottatte fil",
"Last Scan": "Siste gjennomsøking",
"Last File Received": "Sist Mottatte Fil",
"Last seen": "Sist sett",
"Later": "Senere",
"Listeners": "Lyttere",
"Local Discovery": "Lokalt oppslag",
"Local State": "Lokal Tilstand",
"Local State (Total)": "Lokal Tilstand (Total)",
"Major Upgrade": "Hovedoppgradering",
"Master": "Hoved",
"Maximum Age": "Maksimal Levetid",
"Metadata Only": "Kun metadata",
"Minimum Free Disk Space": "Nødvendig ledig diskplass",
@@ -118,12 +113,11 @@
"Newest First": "Den nyeste først",
"No": "Nei",
"No File Versioning": "Ingen Versjonskontroll",
"Normal": "Normal",
"Notice": "Merknader",
"OK": "OK",
"Off": "Av",
"Oldest First": "Den eldste først",
"Optional descriptive label for the folder. Can be different on each device.": "Valgfri merkelapp på katalogen. Denne kan være ulik på forskjellige enheter",
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
"Options": "Valg",
"Out of Sync": "Ikke synkronisert",
"Out of Sync Items": "Ikke Synkroniserte Element",
@@ -145,9 +139,9 @@
"Relayed via": "Relé via",
"Relays": "Reléer",
"Release Notes": "Utgivelsesnotat",
"Remote Devices": "Andre enheter",
"Remote Devices": "Remote Devices",
"Remove": "Fjern",
"Required identifier for the folder. Must be the same on all cluster devices.": "Påkrevd identifikator for katalogen. Denne må være lik på alle enheter i samme klynge.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Rescan": "Gjennomsøk på nytt",
"Rescan All": "Gjennomsøk alt på nytt",
"Rescan Interval": "Intervall for gjennomsøking",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å være nede, eller så er det et problem med nettforbindelsen din. Prøver på ny …",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ser ut til å ha støtt på et problem under behandling av din forespørsel. Vennligst oppfrisk nettleseren eller start Syncthing på nytt dersom problemet vedvarer.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Grensesnittet for administrering av Syncthing er satt til å tillate ekstern tilgang uten et passord.",
"The aggregated statistics are publicly available at the URL below.": "Samlet statistikk er åpent tilgjengelig via URL som er angitt under",
"The aggregated statistics are publicly available at {%url%}.": "Samlet statistikk er åpent tilgjengelig på {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Innstillingene har blitt lagret men ikke aktivert. Syncthing må starte på ny for å aktivere de nye innstillingene.",
"The device ID cannot be blank.": "Enhets-ID kan ikke være tom.",
@@ -219,7 +212,7 @@
"The rate limit must be a non-negative number (0: no limit)": "Hastighetsbegrensningen kan ikke være et negativt tall (0: ingen begrensing)",
"The rescan interval must be a non-negative number of seconds.": "Antall sekund for intervallet kan ikke være negativt.",
"They are retried automatically and will be synced when the error is resolved.": "Disse hentes automatisk og vil synkroniseres når feilen er blitt utbedret.",
"This Device": "Denne enheten",
"This Device": "This Device",
"This can easily give hackers access to read and change any files on your computer.": "Dette kan lett gi hackere tilgang til å lese og endre alle filer på datamaskinen din.",
"This is a major version upgrade.": "Dette er en hovedoppgradering",
"Trash Can File Versioning": "Papirkurv Versjonskontroll",
@@ -237,7 +230,7 @@
"Version": "Versjon",
"Versions Path": "Plassering Av Versjoner",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versjoner blir automatisk slettet når maksimal levetid er nådd eller når antall filer er oversteget.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Advarsel, denne stien er en underkatalog i en eksisterende katalog \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Merk at når en ny enhet blir lagt til må denne også legges til på andre siden.",
"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",
@@ -246,6 +239,6 @@
"full documentation": "all dokumentasjon",
"items": "elementer",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} ønsker å dele katalogen \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} ønsker å dele katalogen \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -32,10 +32,9 @@
"Comment, when used at the start of a line": "Reageer indien gebruikt aan het begin van een lijn.",
"Compression": "Compressie",
"Connection Error": "Verbindingsfout",
"Connection Type": "Soort verbinding",
"Copied from elsewhere": "Gekopieerd vanaf elders",
"Copied from original": "Gekopieerd van het origineel",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 voor de volgende bijdragers:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 voor de volgende contributanten:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 de volgende Bijdragers:",
"Danger!": "Let op!",
"Delete": "Verwijderen",
@@ -75,7 +74,6 @@
"Folder Label": "Map label",
"Folder Master": "Hoofdmap",
"Folder Path": "Maplocatie",
"Folder Type": "Soort map",
"Folders": "Mappen",
"GUI": "GUI",
"GUI Authentication Password": "GUI-wachtwoord",
@@ -98,15 +96,12 @@
"Keep Versions": "Versies behouden",
"Largest First": "Grootste eerst",
"Last File Received": "Laatst ontvangen bestand",
"Last Scan": "Laatste scan",
"Last seen": "Laatst gezien op",
"Later": "Later",
"Listeners": "Luisteraars",
"Local Discovery": "Lokaal zoeken",
"Local State": "Lokale status",
"Local State (Total)": "Lokale status (totaal)",
"Major Upgrade": "Grote update",
"Master": "Master",
"Maximum Age": "Maximum leeftijd",
"Metadata Only": "Alleen metadata",
"Minimum Free Disk Space": "Minimale vrije schijfruimte",
@@ -118,7 +113,6 @@
"Newest First": "Nieuwste eerst",
"No": "Nee",
"No File Versioning": "Geen versiebeheer",
"Normal": "Normaal",
"Notice": "Mededeling",
"OK": "OK",
"Off": "Uit",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing lijkt afgesloten te zijn, of er is een verbindingsprobleem met het internet. Nieuwe poging....",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing lijkt een probleem te ondervinden met het verwerken van je verzoek. Refresh de pagina of herstart Syncthing als de problemen zich blijven voordoen. ",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing's beheerdersinterface is ingesteld om externe toegang zonder wachtwoord toe te staan.",
"The aggregated statistics are publicly available at the URL below.": "De geaggregeerde statistieken zijn publiek beschikbaar op de onderstaande URL.",
"The aggregated statistics are publicly available at {%url%}.": "The verzamelde statistieken zijn publiek beschikbaar op {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De configuratie is opslagen maar nog niet actief. Syncthing moet opnieuw opgestart worden om de nieuwe configuratie te activeren.",
"The device ID cannot be blank.": "Het apparaat-ID mag niet leeg zijn.",

View File

@@ -8,13 +8,13 @@
"Add": "Legg til",
"Add Device": "Legg Til Eining",
"Add Folder": "Legg Til Mappe",
"Add Remote Device": "Lett Til Ekstern Eining",
"Add Remote Device": "Add Remote Device",
"Add new folder?": "Leggja til ny mappe?",
"Address": "Adresse",
"Addresses": "Adresser",
"Advanced": "Avansert",
"Advanced Configuration": "Avansert konfigurasjon",
"Advanced settings": "Avansert innstillingar",
"Advanced settings": "Advanced settings",
"All Data": "Alle data",
"Allow Anonymous Usage Reporting?": "Tillata anonymisert bruksrapportering?",
"Alphabetic": "Alfabetisk",
@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Kommentar, når brukt i starten av linja",
"Compression": "Komprimering",
"Connection Error": "Tilkoplingsfeil",
"Connection Type": "Tilkoplingstype",
"Copied from elsewhere": "Kopiert frå ein annan stad",
"Copied from original": "Kopiert frå originalen",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -40,7 +39,7 @@
"Danger!": "Fare!",
"Delete": "Slett",
"Deleted": "Sletta",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Eininga \"{{name}}\" {{device}} ({{address}}) vil kopla seg til. Vil du leggja ho til?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
"Device ID": "Eining ID",
"Device Identification": "Einingskjennemerke",
"Device Name": "Namn På Eining",
@@ -56,7 +55,7 @@
"Edit Device": "Rediger Eining",
"Edit Folder": "Rediger Mappe",
"Editing": "Redigerer",
"Enable NAT traversal": "Slå på NAT-gjennomgang",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Aktiver Reléer",
"Enable UPnP": "Aktiver UPnP",
"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.",
@@ -72,10 +71,9 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskytta mot endringar gjort på andre einingar, men endringar gjort på denne eininga vert sende til resten av klyngja.",
"Folder": "Mappe",
"Folder ID": "Mappe ID",
"Folder Label": "Merkelapp for Mappe",
"Folder Label": "Folder Label",
"Folder Master": "Styrande Mappe",
"Folder Path": "Mappeplassering",
"Folder Type": "Mappetype",
"Folders": "Mapper",
"GUI": "grafisk brukargrensesnitt",
"GUI Authentication Password": "GUI Passord",
@@ -98,15 +96,12 @@
"Keep Versions": "Behald Versjonar",
"Largest First": "Største fyrst",
"Last File Received": "Siste mottatte fila",
"Last Scan": "Siste Skanning",
"Last seen": "Sist sett",
"Later": "Seinare",
"Listeners": "Lyttarar",
"Local Discovery": "Lokal oppdaging",
"Local State": "Lokal Tilstand",
"Local State (Total)": "Lokal tilstand (total)",
"Major Upgrade": "Hovudoppgradering",
"Master": "Styrar",
"Maximum Age": "Maksimal Levetid",
"Metadata Only": "Berre metadata",
"Minimum Free Disk Space": "Naudsynt ledig diskplass",
@@ -118,7 +113,6 @@
"Newest First": "Nyaste fyrst",
"No": "Nei",
"No File Versioning": "Ingen filutgåvehandtering",
"Normal": "Normal",
"Notice": "Merknad",
"OK": "OK",
"Off": "Av",
@@ -145,7 +139,7 @@
"Relayed via": "Relé via",
"Relays": "Reléer",
"Release Notes": "Utgivingsnotat",
"Remote Devices": "Eksterne Einingar",
"Remote Devices": "Remote Devices",
"Remove": "Fjern",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Rescan": "Skann På Ny",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å vera nede, eller så er det eit problem med nettilkoplinga di. Prøvar på ny …",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ser ut til å ha støtt på eit problem under behandling av din førespurnad. Vær vennleg å oppfrisk nettlesaren eller start Syncthing på nytt om problemet vedvarer.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing sitt administreringsgrensesnitt er sett opp til å tillate ekstern tilgang uten passord.",
"The aggregated statistics are publicly available at the URL below.": "Samla statistikk er opent tilgjengeleg på URL-en nedanfor.",
"The aggregated statistics are publicly available at {%url%}.": "Samla statistikk er opent tilgjengeleg på {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Instillingane har blitt lagra men ikkje aktivert. Syncthing må starta på ny for å aktivera dei nye instillingane.",
"The device ID cannot be blank.": "Eining ID kan ikkje vera tom.",
@@ -219,7 +212,7 @@
"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.",
"They are retried automatically and will be synced when the error is resolved.": "Desse vil bli prøvd på nytt automatisk og vil bli synkronisert når feilen har blitt utbetra.",
"This Device": "Denne Eininga",
"This Device": "This Device",
"This can easily give hackers access to read and change any files on your computer.": "Dette kan lett gje dataekspertar tilgang til å lese og endre vilkårlege filer på denne maskina.",
"This is a major version upgrade.": "Dette er ei hovudoppgradering",
"Trash Can File Versioning": "Papirkorg filutgåvehandtering",
@@ -246,6 +239,6 @@
"full documentation": "all dokumentasjon",
"items": "element",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønskjer å dela mappa \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} ønskjer å dela mappa \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} ønskjer å dela mappa \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Komentarz, jeżeli użyty na początku linii",
"Compression": "Kompresja",
"Connection Error": "Błąd połączenia",
"Connection Type": "Rodzaj połączenia",
"Copied from elsewhere": "Skopiowane z innego miejsca ",
"Copied from original": "Skopiowane z oryginału",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016: ",
@@ -75,7 +74,6 @@
"Folder Label": "Etykieta folderu",
"Folder Master": "Główny folder",
"Folder Path": "Ścieżka folderu",
"Folder Type": "Rodzaj folderu",
"Folders": "Foldery",
"GUI": "GUI",
"GUI Authentication Password": "Hasło",
@@ -98,15 +96,12 @@
"Keep Versions": "Zachowuj wersje",
"Largest First": "Największe na początku",
"Last File Received": "Ostatni otrzymany plik",
"Last Scan": "Czas ostatniego skanu",
"Last seen": "Ostatnio widziany",
"Later": "Później",
"Listeners": "Nasłuchujący",
"Local Discovery": "Lokalne odnajdywanie",
"Local State": "Status lokalny",
"Local State (Total)": "Status lokalny (suma)",
"Major Upgrade": "Ważna aktualizacja",
"Master": "Mistrz",
"Maximum Age": "Maksymalny wiek",
"Metadata Only": "Tylko metadane",
"Minimum Free Disk Space": "Minimum wolnego miejsca na dysku",
@@ -118,7 +113,6 @@
"Newest First": "Najnowsze na początku",
"No": "Nie",
"No File Versioning": "Bez wersjonowania pliku",
"Normal": "Zwykły",
"Notice": "Wskazówka",
"OK": "OK",
"Off": "Wyłącz",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing wydaje się być wyłączony lub jest problem z twoim połączeniem internetowym. Próbuje ponownie...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing nie może przetworzyć twojego zapytania. Proszę przeładuj stronę lub zrestartuj Syncthing, jeśli problem pozostanie.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Interfejs administracyjny Syncthing jest skonfigurowany w sposób pozwalający na zdalny dostęp bez hasła.",
"The aggregated statistics are publicly available at the URL below.": "Zebrane statystyki są dostępne pod poniższym linkiem.",
"The aggregated statistics are publicly available at {%url%}.": "Zebrane statystyki są publicznie dostępne pod adresem {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfiguracja została zapisana lecz nie jest aktywna. Syncthing musi zostać zrestartowany aby aktywować nową konfiguracje.",
"The device ID cannot be blank.": "ID urządzenia nie może być puste.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Comentário, se usado no início de uma linha",
"Compression": "Compressão",
"Connection Error": "Erro de conexão",
"Connection Type": "Tipo da conexão",
"Copied from elsewhere": "Copiado de outro lugar",
"Copied from original": "Copiado do original",
"Copyright © 2014-2016 the following Contributors:": "Direitos reservados © 2014-2016 aos seguintes colaboradores:",
@@ -75,7 +74,6 @@
"Folder Label": "Rótulo da pasta",
"Folder Master": "Pasta mestre",
"Folder Path": "Caminho da pasta",
"Folder Type": "Tipo da pasta",
"Folders": "Pastas",
"GUI": "Interface gráfica",
"GUI Authentication Password": "Senha para acesso à interface",
@@ -98,15 +96,12 @@
"Keep Versions": "Manter versões",
"Largest First": "Maior primeiro",
"Last File Received": "Último arquivo recebido",
"Last Scan": "Última verificação",
"Last seen": "Visto por último em",
"Later": "Depois",
"Listeners": "Escutadores",
"Local Discovery": "Descoberta local",
"Local State": "Estado local",
"Local State (Total)": "Estado local (total)",
"Major Upgrade": "Atualização \"major\"",
"Master": "Mestre",
"Maximum Age": "Idade máxima",
"Metadata Only": "Somente metadados",
"Minimum Free Disk Space": "Espaço livre mínimo no disco",
@@ -118,7 +113,6 @@
"Newest First": "Mais novo primeiro",
"No": "Não",
"No File Versioning": "Sem versionamento de arquivos",
"Normal": "Normal",
"Notice": "Aviso",
"OK": "OK",
"Off": "Desligada",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Parece que o Syncthing está desligado ou há um problema com a sua conexão de internet. Tentando novamente...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Parece que o Syncthing está tendo problemas no processamento da requisição. Por favor, atualize a página ou reinicie o Syncthing caso o problema persista.",
"The Syncthing admin interface is configured to allow remote access without a password.": "A interface de administração do Syncthing está configurada para permitir acesso remoto sem uma senha.",
"The aggregated statistics are publicly available at the URL below.": "As estatísticas agregadas estão disponíveis no endereço abaixo.",
"The aggregated statistics are publicly available at {%url%}.": "As estatísticas agregadas estão disponíveis publicamente em {{url}}.",
"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.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Comentário, quando usado no início de uma linha",
"Compression": "Compressão",
"Connection Error": "Erro de ligação",
"Connection Type": "Tipo de ligação",
"Copied from elsewhere": "Copiado doutro sítio",
"Copied from original": "Copiado do original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 os seguintes contribuidores:",
@@ -47,7 +46,7 @@
"Device {%device%} ({%address%}) wants to connect. Add new device?": "O dispositivo {{device}} ({{address}}) quer conectar-se. Adiciono este novo dispositivo?",
"Devices": "Dispositivos",
"Disconnected": "Desconectado",
"Discovery": "Pesquisa",
"Discovery": "Detecção",
"Documentation": "Documentação",
"Download Rate": "Velocidade de recepção",
"Downloaded": "Recebido",
@@ -75,16 +74,15 @@
"Folder Label": "Etiqueta da pasta",
"Folder Master": "Pasta mestre",
"Folder Path": "Caminho da pasta",
"Folder Type": "Tipo de pasta",
"Folders": "Pastas",
"GUI": "GUI",
"GUI Authentication Password": "Senha da autenticação na interface gráfica",
"GUI Authentication User": "Utilizador da autenticação na interface gráfica",
"GUI Listen Addresses": "Endereço de escuta da interface gráfica",
"Generate": "Gerar",
"Global Discovery": "Pesquisa global",
"Global Discovery Server": "Servidor de pesquisa global",
"Global Discovery Servers": "Servidores de pesquisa global",
"Global Discovery": "Detecção global",
"Global Discovery Server": "Servidor de detecção global",
"Global Discovery Servers": "Servidores de detecção global",
"Global State": "Estado global",
"Help": "Ajuda",
"Home page": "Página do projecto",
@@ -98,15 +96,12 @@
"Keep Versions": "Manter versões",
"Largest First": "Primeiro os maiores",
"Last File Received": "Último ficheiro recebido",
"Last Scan": "Última verificação",
"Last seen": "Última vez que foi verificado",
"Later": "Mais tarde",
"Listeners": "Auscultadores",
"Local Discovery": "Pesquisa local",
"Local Discovery": "Detecção local",
"Local State": "Estado local",
"Local State (Total)": "Estado local (total)",
"Major Upgrade": "Actualização importante",
"Master": "Mestre",
"Maximum Age": "Idade máxima",
"Metadata Only": "Metadados apenas",
"Minimum Free Disk Space": "Espaço livre mínimo no disco",
@@ -118,7 +113,6 @@
"Newest First": "Primeiro os mais recentes",
"No": "Não",
"No File Versioning": "Nenhuma",
"Normal": "Normal",
"Notice": "Avisos",
"OK": "OK",
"Off": "Desligada",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "O Syncthing parece estar em baixo, ou então existe um problema com a sua ligação à Internet. Tentando novamente...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "O Syncthing parece estar com problemas em processar o seu pedido. Tente recarregar a página ou reiniciar o Syncthing, se o problema persistir.",
"The Syncthing admin interface is configured to allow remote access without a password.": "A interface de administração do Syncthing está configurada para permitir o acesso remoto sem pedir senha.",
"The aggregated statistics are publicly available at the URL below.": "As estatísticas agregadas estão publicamente disponíveis no URL abaixo.",
"The aggregated statistics are publicly available at {%url%}.": "As estatísticas agrupadas estão disponíveis publicamente em {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi gravada mas não activada. O Syncthing tem que reiniciar para activar a nova configuração.",
"The device ID cannot be blank.": "O ID do dispositivo não pode estar vazio.",

View File

@@ -16,15 +16,15 @@
"Advanced Configuration": "Дополнительные настройки",
"Advanced settings": "Дополнительные настройки",
"All Data": "Все данные",
"Allow Anonymous Usage Reporting?": "Разрешить анонимный отчет об использовании?",
"Allow Anonymous Usage Reporting?": "Разрешить сбор анонимной статистики использования?",
"Alphabetic": "По алфавиту",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Внешний процесс управляет версиями файлов. Процесс удалит файл из синхронизируемой папки.",
"Anonymous Usage Reporting": "Анонимный отчет об использовании",
"Anonymous Usage Reporting": "Анонимная статистика использования",
"Any devices configured on an introducer device will be added to this device as well.": "Все устройства, подключённые к устройству-рекомендателю, будут добавлены к текущему устройству.",
"Automatic upgrades": "Автообновление",
"Be careful!": "Будьте осторожны!",
"Bugs": "Ошибки",
"CPU Utilization": "Загрузка ЦП",
"CPU Utilization": "Загрузка ЦПУ",
"Changelog": "Журнал изменений",
"Clean out after": "Очистить после",
"Close": "Закрыть",
@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Комментарий, если используется в начале строки",
"Compression": "Сжатие",
"Connection Error": "Ошибка подключения",
"Connection Type": "Тип соединения",
"Copied from elsewhere": "Скопировано из другого места",
"Copied from original": "Скопировано с оригинала",
"Copyright © 2014-2016 the following Contributors:": "Авторские права © 20142016 принадлежат:",
@@ -52,14 +51,14 @@
"Download Rate": "Скорость загрузки",
"Downloaded": "Загружено",
"Downloading": "Загрузка",
"Edit": "Редактировать",
"Edit Device": "Редактирование устройства",
"Edit Folder": "Редактирование папки",
"Edit": "Изменить",
"Edit Device": "Изменить устройство",
"Edit Folder": "Изменить папку",
"Editing": "Редактирование",
"Enable NAT traversal": "Включить NAT traversal",
"Enable Relaying": "Включить релеи",
"Enable UPnP": "Включить UPnP",
"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 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.": "Введите шаблоны игнорирования, по одному на строку.",
"Error": "Ошибка",
"External File Versioning": "Внешний контроль версий файлов",
@@ -75,7 +74,6 @@
"Folder Label": "Ярлык папки",
"Folder Master": "Папка-оригинал",
"Folder Path": "Путь к папке",
"Folder Type": "Тип папки",
"Folders": "Папки",
"GUI": "Интерфейс",
"GUI Authentication Password": "Пароль для доступа к панели управления",
@@ -91,22 +89,19 @@
"Ignore": "Игнорировать",
"Ignore Patterns": "Шаблоны игнорирования",
"Ignore Permissions": "Игнорировать файловые права доступа",
"Incoming Rate Limit (KiB/s)": "Ограничение входящей скорости (КиБ/с)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Неправильные настройки могут повредить содержимое папок и сделать Syncthing неработоспособным.",
"Incoming Rate Limit (KiB/s)": "Ограничение входящего потока (Кбит/сек)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Неправильные настройки могут повредить содержимое папок и сделать Syncthing нерабочим",
"Introducer": "Рекомендатель",
"Inversion of the given condition (i.e. do not exclude)": "Инвертировать текущее условие (например, исключить)",
"Keep Versions": "Количество хранимых версий",
"Largest First": "Сначала большие",
"Last File Received": "Последний полученный файл",
"Last Scan": "Последнее сканирование",
"Last seen": "Был доступен",
"Later": "Позже",
"Listeners": "Прослушиватель",
"Later": "Потом",
"Local Discovery": "Локальное обнаружение",
"Local State": "Локальное состояние",
"Local State (Total)": "Локальное состояние (всего)",
"Local State (Total)": "Локально (всего)",
"Major Upgrade": "Обновление основной версии",
"Master": "Мастер",
"Maximum Age": "Максимальный срок",
"Metadata Only": "Только метаданные",
"Minimum Free Disk Space": "Минимальное свободное место на диске",
@@ -118,7 +113,6 @@
"Newest First": "Сначала новые",
"No": "Нет",
"No File Versioning": "Без управления версиями файлов",
"Normal": "Нормально",
"Notice": "Внимание",
"OK": "ОК",
"Off": "Отключить",
@@ -126,32 +120,32 @@
"Optional descriptive label for the folder. Can be different on each device.": "Необязательное описательное название папки. Может различаться на разных устройствах.",
"Options": "Настройки",
"Out of Sync": "Нет синхронизации",
"Out of Sync Items": "Несинхронизированные элементы",
"Outgoing Rate Limit (KiB/s)": "Ограничение исходящей скорости (КиБ/с)",
"Out of Sync Items": "Не синхронизированные пункты",
"Outgoing Rate Limit (KiB/s)": "Предел скорости отдачи (KiB/s)",
"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 where versions should be stored (leave empty for the default .stversions folder in the folder).": "Путь, где должны храниться версии (оставьте пустым, чтобы использовать папку по умолчанию .stversions внутри папки).",
"Pause": "Пауза",
"Paused": "Приостановлено",
"Paused": "Остановлено",
"Please consult the release notes before performing a major upgrade.": "Перед проведением обновления основной версии ознакомтесь, пожалуйста, с Замечаниями к версии",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Установите имя пользователя и пароль для интерфейса в настройках",
"Please wait": "Пожалуйста, подождите",
"Preview": "Предварительный просмотр",
"Preview Usage Report": "Посмотреть отчёт об использовании",
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
"RAM Utilization": "Использование памяти",
"RAM Utilization": "Использование ОЗУ",
"Random": "Случайно",
"Relay Servers": "Релеи",
"Relayed via": "Релей через",
"Relays": "Релеи",
"Release Notes": "Примечания к выпуску",
"Release Notes": "Замечания к версии",
"Remote Devices": "Удалённые устройства",
"Remove": "Удалить",
"Required identifier for the folder. Must be the same on all cluster devices.": "Обязательный идентификатор папки. Должен быть одним и тем же на всех устройствах кластера.",
"Rescan": "Пересканировать",
"Rescan": "Пересканирование",
"Rescan All": "Пересканировать все",
"Rescan Interval": "Интервал пересканирования",
"Restart": "Перезапустить",
"Restart": "Перезапуск",
"Restart Needed": "Требуется перезапуск",
"Restarting": "Перезапуск",
"Resume": "Возобновить",
@@ -160,7 +154,7 @@
"Scan Time Remaining": "Оставшееся время сканирования",
"Scanning": "Сканирование",
"Select the devices to share this folder with.": "Выберите устройства, для которых будет доступна эта папка.",
"Select the folders to share with this device.": "Выберите папки, которые будут доступны этому устройству.",
"Select the folders to share with this device.": "Выберите папку для предоставления доступа данному устройству",
"Settings": "Настройки",
"Share": "Предоставить доступ",
"Share Folder": "Предоставить доступ к папке",
@@ -174,33 +168,32 @@
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Отображается вместо ID устройства в статусе группы. Будет разослан другим устройствам в качестве имени по умолчанию.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Отображается вместо ID устройства в статусе группы. Если поле не заполнено, то будет установлено имя, передаваемое этим устройством.",
"Shutdown": "Выключить",
"Shutdown Complete": "Выключение",
"Shutdown Complete": "Выключено",
"Simple File Versioning": "Простое управление версиями файлов",
"Single level wildcard (matches within a directory only)": "Одноуровневая маска (поиск совпадений только внутри папки)",
"Smallest First": "Сначала маленькие",
"Source Code": "Исходный код",
"Staggered File Versioning": "Ступенчатое управление версиями файлов",
"Start Browser": "Запускать браузер",
"Start Browser": "Открыть браузер",
"Statistics": "Статистика",
"Stopped": "Остановлено",
"Support": "Поддержка",
"Sync Protocol Listen Addresses": "Адрес протокола синхронизации",
"Syncing": "Синхронизация",
"Syncthing has been shut down.": "Syncthing был выключен.",
"Syncthing has been shut down.": "Syncthing выключен.",
"Syncthing includes the following software or portions thereof:": "Syncthing включает в себя следующее ПО или его части:",
"Syncthing is restarting.": "Перезапуск Syncthing.",
"Syncthing is upgrading.": "Обновление Syncthing.",
"Syncthing is restarting.": "Перезапуск Syncthing",
"Syncthing is upgrading.": "Обновление Syncthing ",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing столкнулся с проблемой при обработке Вашего запроса. Пожалуйста, обновите страницу или перезапустите Syncthing если проблема повторится.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Административный интерфейс Syncthing настроен для предоставления удаленного доступа без пароля.",
"The aggregated statistics are publicly available at the URL below.": "Агрегированные статистические данные общедоступны по ссылке ниже.",
"The aggregated statistics are publicly available at {%url%}.": "Суммарная статистика общедоступна на {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурация была сохранена, но не активирована. Syncthing должен быть перезапущен для применения новой конфигурации.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурация была сохранена но не активирована. Для активации новой конфигурации необходимо рестартовать Syncthing.",
"The device ID cannot be blank.": "ID устройства не может быть пустым.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор устройства можно найти в диалоге «Действия → Показать ID» на другом устройстве. Пробелы и дефисы вводить не обязательно (игнорируются).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор устройства можно найти в диалоге «Редактирование Показать ID» на другом устройстве. Пробелы и дефисы вводить не обязательно (игнорируются).",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор устройства можно найти в диалоге «Действия → Показать ID» на другом устройстве. Пробелы и дефисы вводить не обязательно (они игнорируются).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор устройства, который следует тут ввести, может быть найден в диалоге \"Редактирование > Показать ID\" на другом устройстве. Пробелы и тире не обязательны (игнорируются).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Зашифрованный отчет об использовании отправляется ежедневно. Это используется для отслеживания общих платформ, размеров папок и версий приложения. Если отчетные данные изменятся, вам будет снова показано это диалоговое окно.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введён недопустимый ID устройства. Он должен состоять из букв и цифр, может включать пробелы и дефисы, длина должна быть 52 или 56 символов.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введён недопустимый ID устройства. Он должен состоять из букв и цифр, может включать пробелы и дефисы, длина должна быть от 52 до 56 символов, ",
"The 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.": "ID папки не может быть пустым.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "ID папки должен быть коротким (не более 64 символов), должен состоять только из букв, цифр, точек (.), дефисов (-) или подчёркиваний (_).",
@@ -226,13 +219,13 @@
"Unknown": "Неизвестно",
"Unshared": "Необщедоступно",
"Unused": "Не используется",
"Up to Date": "В актуальном состоянии",
"Up to Date": "Обновлено",
"Updated": "Обновлено",
"Upgrade": "Обновить",
"Upgrade To {%version%}": "Обновить до {{version}}",
"Upgrading": "Обновление",
"Upload Rate": "Скорость отдачи",
"Uptime": "Время работы",
"Uptime": "Аптайм",
"Use HTTPS for GUI": "Использовать HTTPS для панели управления",
"Version": "Версия",
"Versions Path": "Путь к версиям",
@@ -242,10 +235,10 @@
"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": "Да",
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
"days": "дней",
"days": "Дней",
"full documentation": "полная документация",
"items": "элементы",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой «{{folder}}».",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderLabel}}» ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderlabel}}» ({{folder}})."
}

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Kommentar, vid början av en rad.",
"Compression": "Komprimering",
"Connection Error": "Anslutningsproblem",
"Connection Type": "Anslutningstyp",
"Copied from elsewhere": "Kopierat utifrån",
"Copied from original": "Oförändrat",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragande:",
@@ -75,7 +74,6 @@
"Folder Label": "Katalog etikett",
"Folder Master": "Huvudlagring",
"Folder Path": "Sökväg",
"Folder Type": "Katalogtyp",
"Folders": "Kataloger",
"GUI": "GUI",
"GUI Authentication Password": "GUI-lösenord",
@@ -98,15 +96,12 @@
"Keep Versions": "Behåll versioner",
"Largest First": "Störst först",
"Last File Received": "Senast Mottagna Fil",
"Last Scan": "Senast skanning",
"Last seen": "Senast online",
"Later": "Senare",
"Listeners": "Lyssnare",
"Local Discovery": "Lokal uppslagning",
"Local State": "Lokal status",
"Local State (Total)": "Lokal status (Total)",
"Major Upgrade": "Stor uppgradering",
"Master": "Huvud",
"Maximum Age": "Högsta åldersgräns",
"Metadata Only": "Endast metadata",
"Minimum Free Disk Space": "Minimum ledigt diskutrymme",
@@ -118,7 +113,6 @@
"Newest First": "Nyast först",
"No": "Nej",
"No File Versioning": "Ingen versionshantering",
"Normal": "Normal",
"Notice": "Observera",
"OK": "OK",
"Off": "Av",
@@ -147,7 +141,7 @@
"Release Notes": "versionsnyheter",
"Remote Devices": "Fjärrenheter",
"Remove": "Ta bort",
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för katalogen. Måste vara densamma på alla kluster enheter.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för mappen. Måste vara densamma på alla kluster enheter.",
"Rescan": "Uppdatera",
"Rescan All": "Uppdatera alla",
"Rescan Interval": "Uppdateringsintervall",
@@ -157,7 +151,7 @@
"Resume": "Återuppta",
"Reused": "Återanvänt",
"Save": "Spara",
"Scan Time Remaining": "Granska återstående tid",
"Scan Time Remaining": "Skanna Återstående Tid",
"Scanning": "Uppdaterar",
"Select the devices to share this folder with.": "Ange enheterna att dela den här katalogen med.",
"Select the folders to share with this device.": "Välj kataloger att dela med den här enheten.",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller finns det problem med din Internetanslutning. Försöker igen...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratör gränssnittet är konfigurerat för att tillåta fjärrtillträde utan ett lösenord.",
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgängliga på webbadressen nedan.",
"The aggregated statistics are publicly available at {%url%}.": "Sammanställd statistik finns publikt tillgänglig på {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
"The device ID cannot be blank.": "Enhets-ID kan inte vara tomt.",
@@ -246,6 +239,6 @@
"full documentation": "fullständig dokumentation",
"items": "poster",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vill dela katalogen \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela katalogen \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -32,7 +32,6 @@
"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ı",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Başka bir yerden kopyalanmış",
"Copied from original": "Aslından kopyalanmış",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -75,7 +74,6 @@
"Folder Label": "Folder Label",
"Folder Master": "Ana Klasör",
"Folder Path": "Klasör Yolu",
"Folder Type": "Folder Type",
"Folders": "Klasörler",
"GUI": "GUI / Kullanıcı Grafik Arayüzü",
"GUI Authentication Password": "GUI Kimlik Doğrulaması için Kullanıcı Parolası",
@@ -98,15 +96,12 @@
"Keep Versions": "Sürümleri Tut",
"Largest First": "En büyük olan önce",
"Last File Received": "Alınan Son Dosya",
"Last Scan": "Last Scan",
"Last seen": "Son Görülen",
"Later": "Sonra",
"Listeners": "Listeners",
"Local Discovery": "Yerel Discovery",
"Local State": "Yerel Durum",
"Local State (Total)": "Yerel Durum (Toplamı)",
"Major Upgrade": "Birincil Yükseltme",
"Master": "Master",
"Maximum Age": "Azami Süre",
"Metadata Only": "Sadece Üstveri",
"Minimum Free Disk Space": "En Az Boş Disk Alanı",
@@ -118,7 +113,6 @@
"Newest First": "En yeni olan önce",
"No": "Hayır",
"No File Versioning": "Dosya Sürümlendirmesi Yok",
"Normal": "Normal",
"Notice": "Uyarı",
"OK": "Tamam",
"Off": "Kapalı",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing uygulaması çökmüş olabilir, veya internet bağlantınızda bir sorun 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 Syncthing admin interface is configured to allow remote access without a password.": "Syncthing yönetici arayüzü parolasız olarak uzaktan erişime izin verilecek şekilde yapılandırıldı.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"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.": "Yapılandırma kaydedildi ancak etkinleştirilmedi. Etkinleştirmek için Syncthing yeniden başlatılmalı.",
"The device ID cannot be blank.": "Cihaz ID boş olamaz.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Коментар, якщо використовується на початку рядка",
"Compression": "Стиснення",
"Connection Error": "Помилка з’єднання",
"Connection Type": "Тип з*єднання",
"Copied from elsewhere": "Скопійовано з іншого місця",
"Copied from original": "Скопійовано з оригіналу",
"Copyright © 2014-2016 the following Contributors:": "© 2014-2016 Всі права застережено, вклад внесли:",
@@ -47,7 +46,7 @@
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Пристрій {{device}} ({{address}}) намагається під’єднатися. Додати новий пристрій?",
"Devices": "Пристрої",
"Disconnected": "З’єднання відсутнє",
"Discovery": "Сервери координації NAT",
"Discovery": "Сервери обходу NAT",
"Documentation": "Документація",
"Download Rate": "Швидкість завантаження",
"Downloaded": "Завантажено",
@@ -75,7 +74,6 @@
"Folder Label": "Мітка директорії",
"Folder Master": "Вважати за оригінал",
"Folder Path": "Шлях до директорії",
"Folder Type": "Тип директорії",
"Folders": "Директорії",
"GUI": "Графічний інтерфейс",
"GUI Authentication Password": "Пароль для доступу до панелі управління",
@@ -84,7 +82,7 @@
"Generate": "Згенерувати",
"Global Discovery": "Глобальне виявлення (internet)",
"Global Discovery Server": "Сервер для глобального виявлення",
"Global Discovery Servers": "Сервери глобального виявлення \n(координації NAT)",
"Global Discovery Servers": "Сервери глобального виявлення \n(координації обходу NAT)",
"Global State": "Глобальний статус",
"Help": "Допомога",
"Home page": "Домашня сторінка",
@@ -98,15 +96,12 @@
"Keep Versions": "Зберігати версії",
"Largest First": "Спершу найбільші",
"Last File Received": "Останній завантажений файл",
"Last Scan": "Останнє сканування",
"Last seen": "З’являвся останній раз",
"Later": "Пізніше",
"Listeners": "Приймачі (TCP & Relay)",
"Local Discovery": "Локальне виявлення (LAN)",
"Local State": "Локальний статус",
"Local State (Total)": "Локальний статус (загалом)",
"Major Upgrade": "Мажорне оновлення",
"Master": "Головний",
"Maximum Age": "Максимальний вік",
"Metadata Only": "Тільки метадані",
"Minimum Free Disk Space": "Мінімальний вільний простір на диску",
@@ -118,7 +113,6 @@
"Newest First": "Спершу новіші",
"No": "Ні",
"No File Versioning": "Версіонування вимкнено",
"Normal": "Нормальний",
"Notice": "Повідомлення",
"OK": "Гаразд",
"Off": "Вимкнути",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Схоже на те, що Syncthing закритий, або виникла проблема із Інтернет-з’єднанням. Проводиться повторна спроба з’єднання…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Схоже на те, що Syncthing стикнувся з проблемою оброблюючи ваш запит. Будь ласка перезавантажте сторінку в браузері або перезапустіть Syncthing.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Інтерфейс адміністрування Syncthing налаштовано на дозвіл віддаленого доступу без пароля.",
"The aggregated statistics are publicly available at the URL below.": "Зібрана статистика публічно доступна за наступним посиланням.",
"The aggregated statistics are publicly available at {%url%}.": "Зібрана статистика публічно доступна за посиланням {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфігурацію збережено, але не активовано. Необхідно перезапустити Syncthing для того, щоби активувати нову конфігурацію.",
"The device ID cannot be blank.": "ID пристрою не може бути порожнім.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "Bình luận, khi dùng trước đầu dòng",
"Compression": "Nén",
"Connection Error": "Lỗi kết nối",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Đã sao chép từ nơi khác",
"Copied from original": "Đã sao chép từ nguồn",
"Copyright © 2014-2016 the following Contributors:": "Bản quyền © 2014-2016 thuộc về các nhà cộng tác sau:",
@@ -75,7 +74,6 @@
"Folder Label": "Nhãn thư mục",
"Folder Master": "Thư mục Chủ",
"Folder Path": "Đ.dẫn đến th.mục",
"Folder Type": "Folder Type",
"Folders": "Các th.mục",
"GUI": "GUI",
"GUI Authentication Password": "Mật khẩu xác minh GUI",
@@ -98,15 +96,12 @@
"Keep Versions": "Giữ các ph.bản",
"Largest First": "Lớn nhất đầu tiên",
"Last File Received": "T.tin nhận được gần đây",
"Last Scan": "Last Scan",
"Last seen": "Thấy lần cuối",
"Later": "Để sau",
"Listeners": "Listeners",
"Local Discovery": "Dò tìm cục bộ",
"Local State": "Tr.thái cục bộ",
"Local State (Total)": "Tr.thái cục bộ (Tổng)",
"Major Upgrade": "Bản n.cấp q.trọng",
"Master": "Master",
"Maximum Age": "Thời hạn tối đa",
"Metadata Only": "Chỉ siêu dữ liệu",
"Minimum Free Disk Space": "Dung lượng đĩa trống tối thiểu",
@@ -118,7 +113,6 @@
"Newest First": "Mới nhất đầu tiên",
"No": "Không",
"No File Versioning": "Không dùng",
"Normal": "Normal",
"Notice": "Chú ý",
"OK": "OK",
"Off": "Tắt",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Có vẻ như Syncthing đang bị nghẽn hoặc là mạng của bạn có vấn đề. Đang thử lại...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Có vẻ như Syncthing đang gặp phải v.đề khi x.lý yêu cầu của bạn. Xin làm mới trang hoặc kh.động lại Syncthing nếu v.đề vẫn còn tiếp diễn.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Giao diện q.trị của Syncthing được c.hình nhằm cho phép tr.cập từ xa không cần mật khẩu.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "Thống kê tổng hợp được đăng công khai trên {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Cấu hình đã được lưu nhưng chưa được kích hoạt. Syncthing phải khởi động lại để kích hoạt cấu hình mới. ",
"The device ID cannot be blank.": "Không được để trống ID thiết bị.",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "注释,在行首使用",
"Compression": "压缩",
"Connection Error": "连接出错",
"Connection Type": "连接类型",
"Copied from elsewhere": "从其他设备复制",
"Copied from original": "从源复制",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 以下贡献者:",
@@ -75,7 +74,6 @@
"Folder Label": "文件夹标签",
"Folder Master": "主文件夹",
"Folder Path": "文件夹路径",
"Folder Type": "文件夹类型",
"Folders": "文件夹",
"GUI": "图形用户界面",
"GUI Authentication Password": "图形管理界面密码",
@@ -98,15 +96,12 @@
"Keep Versions": "保留历史版本数量",
"Largest First": "大文件优先",
"Last File Received": "最后接收的文件",
"Last Scan": "最后扫描",
"Last seen": "最后可见",
"Later": "稍后",
"Listeners": "侦听程序",
"Local Discovery": "在局域网上寻找设备",
"Local State": "本地状态",
"Local State (Total)": "本地状态汇总",
"Major Upgrade": "重大更新",
"Master": "主要",
"Maximum Age": "历史版本最长保留时间",
"Metadata Only": "仅元数据",
"Minimum Free Disk Space": "最低可用磁盘空间",
@@ -118,7 +113,6 @@
"Newest First": "新文件优先",
"No": "否",
"No File Versioning": "不启用版本控制",
"Normal": "正常",
"Notice": "提示",
"OK": "确定",
"Off": "关闭",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎关闭了,或者您的网络连接存在故障。重试中...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing 在处理您的请求时似乎遇到了问题。如果问题持续,请刷新页面,或重启 Syncthing。",
"The Syncthing admin interface is configured to allow remote access without a password.": "当前配置允许在不使用密码的情况下远程访问 Syncthing 管理界面",
"The aggregated statistics are publicly available at the URL below.": "全局统计数据公布于以下 URL。",
"The aggregated statistics are publicly available at {%url%}.": "全局统计公布于 {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "设置已经保存但是还未生效。Syncthing 需要重启以启用新的设置。",
"The device ID cannot be blank.": "设备标识不能为空",

View File

@@ -32,7 +32,6 @@
"Comment, when used at the start of a line": "註解,當輸入在一行的開頭時",
"Compression": "壓縮",
"Connection Error": "連線錯誤",
"Connection Type": "Connection Type",
"Copied from elsewhere": "從別處複製",
"Copied from original": "從原處複製",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 下列貢獻者:",
@@ -75,7 +74,6 @@
"Folder Label": "資料夾標籤",
"Folder Master": "主資料夾",
"Folder Path": "資料夾路徑",
"Folder Type": "Folder Type",
"Folders": "資料夾",
"GUI": "GUI",
"GUI Authentication Password": "GUI 認證密碼",
@@ -98,15 +96,12 @@
"Keep Versions": "保留歷史版本數",
"Largest First": "最大的優先",
"Last File Received": "最後接收的檔案",
"Last Scan": "Last Scan",
"Last seen": "最後發現時間",
"Later": "稍後",
"Listeners": "Listeners",
"Local Discovery": "本機探索",
"Local State": "本機狀態",
"Local State (Total)": "本機狀態 (總結)",
"Major Upgrade": "重大更新",
"Master": "Master",
"Maximum Age": "最長保留時間",
"Metadata Only": "僅中繼資料",
"Minimum Free Disk Space": "最少閒置磁碟空間",
@@ -118,7 +113,6 @@
"Newest First": "最新的優先",
"No": "否",
"No File Versioning": "無檔案版本控制",
"Normal": "Normal",
"Notice": "注意",
"OK": "確定",
"Off": "關閉",
@@ -193,7 +187,6 @@
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎離線了,或者您的網際網路連線出現問題。正在重試...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The aggregated statistics are publicly available at {%url%}.": "匯總統計資訊公佈於 {{url}}。",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "組態已經儲存但尚未啟用。Syncthing 必須重新啟動以便啟用新的組態。",
"The device ID cannot be blank.": "裝置識別碼不能為空白。",

182
gui/default/index.html Normal file → Executable file
View File

@@ -14,7 +14,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="assets/img/favicon-{{syncthingStatus()}}.png">
<link rel="shortcut icon" href="assets/img/favicon.png">
<link rel="mask-icon" href="assets/img/safari-pinned-tab.svg" color="#0882c8">
<title ng-bind="thisDeviceName() + ' | Syncthing'"></title>
@@ -34,38 +34,31 @@
<div class="container">
<span class="navbar-brand">
<img class="logo hidden-xs" src="assets/img/logo-horizontal.svg" height="32" width="117"/>
<img class="logo hidden visible-xs" src="assets/img/favicon-default.png" height="32"/>
<img class="logo hidden visible-xs" src="assets/img/favicon.png" height="32"/>
</span>
<p class="navbar-text hidden-xs" ng-class="{'hidden-sm':upgradeInfo && upgradeInfo.newer}">{{thisDeviceName()}}</p>
<ul class="nav navbar-nav navbar-right">
<li ng-if="upgradeInfo && upgradeInfo.newer" class="upgrade-newer">
<button type="button" class="btn navbar-btn btn-primary btn-sm" ng-click="upgrade()">
<span class="fa fa-arrow-circle-up"></span>
<span class="hidden-xs" translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
<span class="fa fa-arrow-circle-up"></span>&nbsp;<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
</button>
</li>
<li ng-if="upgradeInfo && upgradeInfo.majorNewer" class="upgrade-newer-major">
<button type="button" class="btn navbar-btn btn-danger btn-sm" data-toggle="modal" data-target="#majorUpgrade">
<span class="fa fa-arrow-circle-up"></span>
<span class="hidden-xs" translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
<button type="button" class="btn navbar-btn btn-danger btn-sm" ng-click="upgradeMajor()">
<span class="fa fa-arrow-circle-up"></span>&nbsp;<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
</button>
</li>
<li class="dropdown" language-select></li>
<li>
<a class="navbar-link" href="https://docs.syncthing.net/intro/gui.html" target="_blank">
<span class="fa fa-question-circle"></span>
<span class="hidden-xs" translate>Help</span>
<a href="https://docs.syncthing.net/intro/gui.html" target="_blank">
<span class="fa fa-question-circle"></span>&nbsp;<span class="hidden-xs" translate>Help</span>
</a>
</li>
<li class="dropdown action-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span class="fa fa-cog"></span>
<span class="hidden-xs" translate>Actions</span>
<span class="caret"></span>
</a>
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="fa fa-cog"></span>&nbsp;<span class="hidden-xs" translate>Actions</span> <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="" ng-click="editSettings()"><span class="fa fa-fw fa-cog"></span>&nbsp;<span translate>Settings</span></a></li>
<li><a href="" data-toggle="modal" data-target="#idqr" ng-click="currentDevice=thisDevice()"><span class="fa fa-fw fa-qrcode"></span>&nbsp;<span translate>Show ID</span></a></li>
<li><a href="" ng-click="idDevice(thisDevice())"><span class="fa fa-fw fa-qrcode"></span>&nbsp;<span translate>Show ID</span></a></li>
<li class="divider"></li>
<li><a href="" ng-click="shutdown()"><span class="fa fa-fw fa-power-off"></span>&nbsp;<span translate>Shutdown</span></a></li>
<li><a href="" ng-click="restart()"><span class="fa fa-fw fa-refresh"></span>&nbsp;<span translate>Restart</span></a></li>
@@ -75,7 +68,7 @@
<span class="fa fa-fw fa-book"></span>&nbsp;<span translate>Help</span>
</a>
</li>
<li><a href="" data-toggle="modal" data-target="#about"><span class="fa fa-fw fa-heart-o"></span>&nbsp;<span translate>About</span></a></li>
<li><a href="" ng-click="about()"><span class="fa fa-fw fa-heart-o"></span>&nbsp;<span translate>About</span></a></li>
<li class="divider"></li>
<li><a href="" ng-click="advanced()"><span class="fa fa-fw fa-cogs"></span>&nbsp;<span translate>Advanced</span></a></li>
</ul>
@@ -91,14 +84,7 @@
<div ng-if="openNoAuth" class="row">
<div class="col-md-12">
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">
<div class="panel-icon">
<span class="fa fa-exclamation-circle"></span>
</div>
<span translate>Danger!</span>
</h3>
</div>
<div class="panel-heading"><h3 class="panel-title"><span class="fa fa-exclamation-circle"></span><span translate>Danger!</span></h3></div>
<div class="panel-body">
<p>
<span translate>The Syncthing admin interface is configured to allow remote access without a password.</span>
@@ -121,14 +107,7 @@
<div ng-if="!configInSync" class="row">
<div class="col-md-12">
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">
<div class="panel-icon">
<span class="fa fa-exclamation-circle"></span>
</div>
<span translate>Restart Needed</span>
</h3>
</div>
<div class="panel-heading"><h3 class="panel-title"><span class="fa fa-exclamation-circle"></span><span translate>Restart Needed</span></h3></div>
<div class="panel-body">
<p translate>The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.</p>
</div>
@@ -149,7 +128,7 @@
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">
<identicon class="panel-icon" data-value="device"></identicon>
<identicon data-value="device"></identicon>
<span translate>New Device</span>
<span class="pull-right">{{ event.time | date:"yyyy-MM-dd HH:mm:ss" }}</span>
</h3>
@@ -184,10 +163,7 @@
<div class="col-md-12">
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">
<div class="panel-icon">
<span class="fa fa-folder"></span>
</div>
<h3 class="panel-title"><span class="fa fa-folder"></span>
<span translate ng-if="!folders[event.data.folder]">New Folder</span>
<span translate ng-if="folders[event.data.folder]">Share Folder</span>
<span class="pull-right">{{ event.time | date:"yyyy-MM-dd HH:mm:ss" }}</span>
@@ -227,14 +203,7 @@
<div ng-if="errorList().length > 0" class="row">
<div class="col-md-12">
<div class="panel panel-warning">
<div class="panel-heading">
<h3 class="panel-title">
<div class="panel-icon">
<span class="fa fa-exclamation-circle"></span>
</div>
<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">
<p ng-repeat="err in errorList()"><small>{{err.when | date:"yyyy-MM-dd HH:mm:ss"}}:</small> {{friendlyDevices(err.message)}}</p>
</div>
@@ -258,14 +227,16 @@
<h3 translate>Folders</h3>
<div class="panel-group" id="folders">
<div class="panel panel-default" ng-repeat="folder in folderList()">
<button class="btn panel-heading" data-toggle="collapse" data-parent="#folders" data-target="#folder-{{$index}}">
<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) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id)}}%"></div>
<h4 class="panel-title">
<div class="panel-icon hidden-xs">
<span class="fa fa-fw" ng-class="[folder.type == 'readonly' ? 'fa-lock' : 'fa-folder']"></span>
</div>
<div class="panel-status pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
<span class="fa hidden-xs fa-fw" ng-class="[folder.type == 'readonly' ? 'fa-lock' : 'fa-folder']"></span>
<a href="#folder-{{$index}}">
<span ng-show="folder.label.length == 0">{{folder.id}}</span>
<span tooltip data-original-title="{{folder.id}}" ng-show="folder.label.length != 0">{{folder.label}}</span>
</a>
<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">&#9724;</span></span>
<span ng-switch-when="unshared"><span class="hidden-xs" translate>Unshared</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="stopped"><span class="hidden-xs" translate>Stopped</span><span class="visible-xs">&#9724;</span></span>
@@ -282,12 +253,9 @@
({{syncPercentage(folder.id)}}%)
</span>
<span ng-switch-when="outofsync"><span class="hidden-xs" translate>Out of Sync</span><span class="visible-xs">&#9724;</span></span>
</div>
<div class="panel-title-text">
<span tooltip data-original-title="{{folder.label.length != 0 ? folder.id : ''}}">{{folder.label.length != 0 ? folder.label : folder.id}}</span>
</div>
</span>
</h4>
</button>
</div>
<div id="folder-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<table class="table table-condensed table-striped">
@@ -384,13 +352,6 @@
<th><span class="fa fa-fw fa-share-alt"></span>&nbsp;<span translate>Shared With</span></th>
<td class="text-right">{{sharesFolder(folder)}}</td>
</tr>
<tr>
<th><span class="fa fa-fw fa-clock-o"></span>&nbsp;<span translate>Last Scan</span></th>
<td translate ng-if="folderStats[folder.id].lastScanDays >= 365" class="text-right">Never</td>
<td ng-if="folderStats[folder.id].lastScanDays < 365" class="text-right">
<span>{{folderStats[folder.id].lastScan | date:'yyyy-MM-dd HH:mm:ss'}}</span>
</td>
</tr>
<tr ng-if="folder.type != 'readonly' && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
<th><span class="fa fa-fw fa-exchange"></span>&nbsp;<span translate>Last File Received</span></th>
<td class="text-right">
@@ -440,12 +401,11 @@
<div class="col-md-6">
<h3 translate>This Device</h3>
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
<button class="btn panel-heading" data-toggle="collapse" data-target="#device-this">
<div class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer">
<h4 class="panel-title">
<identicon class="panel-icon" data-value="deviceCfg.deviceID"></identicon>
<div class="panel-title-text">{{deviceName(deviceCfg)}}</div>
<identicon data-value="deviceCfg.deviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
</h4>
</button>
</div>
<div id="device-this" class="panel-collapse collapse in">
<div class="panel-body">
<table class="table table-condensed table-striped">
@@ -516,10 +476,10 @@
<h3 translate>Remote Devices</h3>
<div class="panel-group" id="devices">
<div class="panel panel-default" ng-repeat="deviceCfg in otherDevices()">
<button class="btn panel-heading" data-toggle="collapse" data-parent="#devices" data-target="#device-{{$index}}">
<div class="panel-heading" data-toggle="collapse" data-parent="#devices" href="#device-{{$index}}" style="cursor: pointer">
<div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.deviceID]._total | number:0}}%"></div>
<h4 class="panel-title">
<identicon class="panel-icon" data-value="deviceCfg.deviceID"></identicon>
<identicon data-value="deviceCfg.deviceID"></identicon>&emsp;<a href="#device-{{$index}}">{{deviceName(deviceCfg)}}</a>
<span ng-switch="deviceStatus(deviceCfg)" class="pull-right text-{{deviceClass(deviceCfg)}}">
<span ng-switch-when="insync"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="syncing">
@@ -529,9 +489,8 @@
<span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">&#9724;</span></span>
</span>
<span>{{deviceName(deviceCfg)}}</span>
</h4>
</button>
</div>
<div id="device-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<table class="table table-condensed table-striped">
@@ -544,20 +503,16 @@
<th><span class="fa fa-fw fa-cloud-upload"></span>&nbsp;<span translate>Upload Rate</span></th>
<td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
</tr>
<tr>
<tr ng-if="connections[deviceCfg.deviceID].connected">
<th><span class="fa fa-fw fa-link"></span>&nbsp<span translate>Address</span></th>
<td ng-if="connections[deviceCfg.deviceID].connected" class="text-right">
<td class="text-right">
<span tooltip data-original-title="{{ connections[deviceCfg.deviceID].type.indexOf('Relay') > -1 ? '' : connections[deviceCfg.deviceID].type }}">
{{deviceAddr(deviceCfg)}}
</span>
</td>
<td ng-if="!connections[deviceCfg.deviceID].connected" class="text-right">
<span ng-repeat="addr in deviceCfg.addresses"><span tooltip data-original-title="{{'Configured' | translate}}">{{addr}}</span><br></span>
<span ng-repeat="addr in discoveryCache[deviceCfg.deviceID].addresses"><span tooltip data-original-title="{{'Discovered' | translate}}">{{addr}}</span><br></span>
</td>
</tr>
<tr ng-if="connections[deviceCfg.deviceID].connected && connections[deviceCfg.deviceID].type.indexOf('Relay') > -1" tooltip data-original-title="Connections via relays might be rate limited by the relay">
<th><span class="fa fa-fw fa-warning text-danger"></span>&nbsp;<span translate>Connection Type</span></th>
<th><span class="fa fa-fw fa-warning text-danger"></span>&nbsp<span translate>Connection Type</span></th>
<td class="text-right">{{connections[deviceCfg.deviceID].type}}</td>
</tr>
<tr ng-if="deviceCfg.compression != 'metadata'">
@@ -582,22 +537,22 @@
</tr>
<tr ng-if="deviceFolders(deviceCfg).length > 0">
<th><span class="fa fa-fw fa-folder"></span>&nbsp;<span translate>Folders</span></th>
<td class="text-right">{{deviceFolders(deviceCfg).map(folderLabel).join(", ")}}</td>
<td class="text-right">{{deviceFolders(deviceCfg).join(", ")}}</td>
</tr>
</tbody>
</table>
</div>
<div class="panel-footer">
<span class="pull-right">
<button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
<span class="fa fa-pencil"></span>&nbsp;<span translate>Edit</span>
</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>&nbsp;<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>&nbsp;<span translate>Resume</span>
</button>
<button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
<span class="fa fa-pencil"></span>&nbsp;<span translate>Edit</span>
</button>
</span>
<div class="clearfix"></div>
</div>
@@ -632,23 +587,23 @@
</div>
</nav>
<ng-include src="'syncthing/core/networkErrorDialogView.html'"></ng-include>
<ng-include src="'syncthing/core/httpErrorDialogView.html'"></ng-include>
<ng-include src="'syncthing/core/restartingDialogView.html'"></ng-include>
<ng-include src="'syncthing/core/upgradingDialogView.html'"></ng-include>
<ng-include src="'syncthing/core/shutdownDialogView.html'"></ng-include>
<ng-include src="'syncthing/device/idqrModalView.html'"></ng-include>
<ng-include src="'syncthing/device/editDeviceModalView.html'"></ng-include>
<ng-include src="'syncthing/folder/editFolderModalView.html'"></ng-include>
<ng-include src="'syncthing/folder/editIgnoresModalView.html'"></ng-include>
<ng-include src="'syncthing/settings/settingsModalView.html'"></ng-include>
<ng-include src="'syncthing/settings/advancedSettingsModalView.html'"></ng-include>
<ng-include src="'syncthing/usagereport/usageReportModalView.html'"></ng-include>
<ng-include src="'syncthing/usagereport/usageReportPreviewModalView.html'"></ng-include>
<ng-include src="'syncthing/transfer/neededFilesModalView.html'"></ng-include>
<ng-include src="'syncthing/transfer/failedFilesModalView.html'"></ng-include>
<ng-include src="'syncthing/core/majorUpgradeModalView.html'"></ng-include>
<ng-include src="'syncthing/core/aboutModalView.html'"></ng-include>
<div network-error-dialog></div>
<div http-error-dialog></div>
<div restarting-dialog></div>
<div upgrading-dialog></div>
<div shutdown-dialog></div>
<div idqr-modal></div>
<div major-upgrade-modal></div>
<div edit-device-modal></div>
<div edit-folder-modal></div>
<div edit-ignores-modal></div>
<div settings-modal></div>
<div advanced-settings-modal></div>
<div usage-report-modal></div>
<div usage-report-preview-modal></div>
<div needed-files-modal></div>
<div failed-files-modal></div>
<div about-modal></div>
<!-- vendor scripts -->
<script src="vendor/jquery/jquery-2.2.2.js"></script>
@@ -661,27 +616,54 @@
<!-- gui application code -->
<script src="syncthing/core/module.js"></script>
<script src="syncthing/core/aboutModalDirective.js"></script>
<script src="syncthing/core/alwaysNumberFilter.js"></script>
<script src="syncthing/core/basenameFilter.js"></script>
<script src="syncthing/core/binaryFilter.js"></script>
<script src="syncthing/core/durationFilter.js"></script>
<script src="syncthing/core/eventService.js"></script>
<script src="syncthing/core/httpErrorDialogDirective.js"></script>
<script src="syncthing/core/identiconDirective.js"></script>
<script src="syncthing/core/languageSelectDirective.js"></script>
<script src="syncthing/core/lastErrorComponentFilter.js"></script>
<script src="syncthing/core/localeService.js"></script>
<script src="syncthing/core/majorUpgradeModalDirective.js"></script>
<script src="syncthing/core/modalDirective.js"></script>
<script src="syncthing/core/naturalFilter.js"></script>
<script src="syncthing/core/networkErrorDialogDirective.js"></script>
<script src="syncthing/core/pathIsSubDirDirective.js"></script>
<script src="syncthing/core/popoverDirective.js"></script>
<script src="syncthing/core/restartingDialogDirective.js"></script>
<script src="syncthing/core/selectOnClickDirective.js"></script>
<script src="syncthing/core/shutdownDialogDirective.js"></script>
<script src="syncthing/core/syncthingController.js"></script>
<script src="syncthing/core/tooltipDirective.js"></script>
<script src="syncthing/core/uniqueFolderDirective.js"></script>
<script src="syncthing/core/upgradingDialogDirective.js"></script>
<script src="syncthing/core/validDeviceidDirective.js"></script>
<script src="syncthing/device/module.js"></script>
<script src="syncthing/device/editDeviceModalDirective.js"></script>
<script src="syncthing/device/idqrModalDirective.js"></script>
<script src="syncthing/folder/module.js"></script>
<script src="syncthing/folder/editFolderModalDirective.js"></script>
<script src="syncthing/folder/editIgnoresModalDirective.js"></script>
<script src="syncthing/settings/module.js"></script>
<script src="syncthing/settings/settingsModalDirective.js"></script>
<script src="syncthing/settings/advancedSettingsModalDirective.js"></script>
<script src="syncthing/transfer/module.js"></script>
<script src="syncthing/transfer/failedFilesModalDirective.js"></script>
<script src="syncthing/transfer/neededFilesModalDirective.js"></script>
<script src="syncthing/usagereport/module.js"></script>
<script src="syncthing/usagereport/usageReportModalDirective.js"></script>
<script src="syncthing/usagereport/usageReportPreviewModalDirective.js"></script>
<script src="assets/lang/valid-langs.js"></script>
<script src="assets/lang/prettyprint.js"></script>
<script src="meta.js"></script>
<script src="syncthing/app.js"></script>
<!-- / gui application code -->

View File

@@ -1,4 +1,4 @@
<div class="modal fade" tabindex="-1" ng-attr-data-backdrop="{{ closeable == 'yes' ? true : 'static' }}" ng-attr-data-keyboard="{{ closeable == 'yes' ? true : false }}">
<div class="modal fade" tabindex="-1" ng-attr-data-backdrop="{{ close ? true : 'static' }}" ng-attr-data-keyboard="{{ Boolean(close) }}">
<!--
// Copyright (C) 2014 The Syncthing Authors.
//
@@ -6,17 +6,21 @@
// 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/.
-->
<div class="modal-dialog {{ large == 'yes' ? 'modal-lg' : '' }}">
<div class="modal-dialog" ng-class="{'modal-lg': large}">
<div class="modal-content">
<div class="modal-header {{status == 'default' ? '' : 'alert alert-'+status }}">
<div class="modal-header alert alert-{{status}}">
<h4 class="modal-title">
<div ng-if="icon" class="panel-icon">
<span class="fa fa-{{icon}}"></span>
</div>
{{heading}}
<span ng-if="icon" class="fa fa-{{icon}}"></span>
{{title}}
</h4>
</div>
<div ng-transclude></div>
<div class="modal-body" ng-transclude>
</div>
<div ng-if="close" class="modal-footer">
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
</button>
</div>
</div>
</div>
</div>

View File

@@ -12,15 +12,42 @@ var syncthing = angular.module('syncthing', [
'angularUtils.directives.dirPagination',
'pascalprecht.translate',
'syncthing.core'
'syncthing.core',
'syncthing.device',
'syncthing.folder',
'syncthing.settings',
'syncthing.transfer',
'syncthing.usagereport'
]);
var urlbase = 'rest';
syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvider) {
var deviceIDShort = metadata.deviceID.substr(0, 5);
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + deviceIDShort;
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIDShort;
$httpProvider.interceptors.push(function xHeadersResponseInterceptor() {
var deviceId = null;
return {
response: function onResponse(response) {
var headers = response.headers();
// angular template cache sends no headers
if(Object.keys(headers).length === 0) {
return response;
}
if (!deviceId) {
deviceId = headers['x-syncthing-id'];
if (deviceId) {
var deviceIdShort = deviceId.substring(0, 5);
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + deviceIdShort;
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIdShort;
}
}
return response;
}
};
});
// language and localisation
@@ -32,6 +59,7 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
LocaleServiceProvider.setAvailableLocales(validLangs);
LocaleServiceProvider.setDefaultLocale('en');
});
// @TODO: extract global level functions into separate service(s)
@@ -49,20 +77,10 @@ function deviceCompare(a, b) {
}
function folderCompare(a, b) {
var labelA = a.id;
if (typeof a.label !== 'undefined' && a.label !== null && a.label.length > 0) {
labelA = a.label;
}
var labelB = b.id;
if (typeof b.label !== 'undefined' && b.label !== null && b.label.length > 0) {
labelB = b.label;
}
if (labelA < labelB) {
if (a.id < b.id) {
return -1;
}
return labelA > labelB;
return a.id > b.id;
}
function folderMap(l) {
@@ -94,6 +112,19 @@ function decimals(val, num) {
return decs;
}
function randomString(len) {
var chars = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-';
return randomStringFromCharset(len, chars);
}
function randomStringFromCharset(len, charset) {
var result = '';
for (var i = 0; i < len; i++) {
result += charset[Math.round(Math.random() * (charset.length - 1))];
}
return result;
}
function isEmptyObject(obj) {
var name;
for (name in obj) {

View File

@@ -0,0 +1,7 @@
angular.module('syncthing.core')
.directive('aboutModal', function () {
return {
restrict: 'A',
templateUrl: 'syncthing/core/aboutModalView.html'
};
});

View File

@@ -1,40 +1,33 @@
<modal id="about" status="info" icon="heart-o" heading="{{'About' | translate}}" large="yes" closeable="yes">
<div class="modal-body">
<h1 class="text-center">
<img alt="Syncthing" src="assets/img/logo-horizontal.svg" style="vertical-align: -16px" height="100" width="366"/>
<br/>
<small>{{versionString()}}</small>
<br/>
<small><i>"{{version.codename}}"</i></small>
</h1>
<hr/>
<modal id="about" status="info" icon="heart-o" title="{{'About' | translate}}" large="yes" close="yes">
<h1 class="text-center">
<img alt="Syncthing" title="Syncthing" src="assets/img/logo-horizontal.svg" style="vertical-align: -16px" height="100" width="366"/>
<br/>
<small>{{versionString()}}</small>
<br/>
<small><i>"{{version.codename}}"</i></small>
</h1>
<hr/>
<p translate>Copyright &copy; 2014-2016 the following Contributors:</p>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
<p translate>Copyright &copy; 2014-2016 the following Contributors:</p>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Ben Schulz, Caleb Callaway, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Andrew Dunham, Antony Male, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Harte, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Lord Landon Agahnim, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
</div>
</div>
<hr/>
</div>
<hr/>
<p translate>Syncthing includes the following software or portions thereof:</p>
<ul class="list-unstyled two-columns">
<li><a href="https://golang.org">The Go Programming Language</a>, Copyright &copy; 2012 The Go Authors.</li>
<li><a href="https://github.com/bkaradzic/go-lz4">bkaradzic/go-lz4</a>, Copyright &copy; 2011-2012 Branimir Karadzic, 2013 Damian Gryski.</li>
<li><a href="https://github.com/kardianos/osext">kardianos/osext</a>, Copyright &copy; 2012 Daniel Theophanes.</li>
<li><a href="https://github.com/golang/snappy">golang/snappy</a>, Copyright &copy; 2011 The Snappy-Go Authors.</li>
<li><a href="https://github.com/juju/ratelimit">juju/ratelimit</a>, Copyright &copy; 2015 Canonical Ltd.</li>
<li><a href="https://github.com/thejerf/suture">thejerf/suture</a>, Copyright &copy; 2014-2015 Barracuda Networks, Inc.</li>
<li><a href="https://github.com/syndtr/goleveldb">syndtr/goleveldb</a>, Copyright &copy; 2012, Suryandaru Triandana</li>
<li><a href="https://github.com/vitrun/qart">vitrun/qart</a>, Copyright &copy; The Go Authors.</li>
<li><a href="https://angularjs.org/">AngularJS</a>, Copyright &copy; 2010-2015 Google, Inc.</li>
<li><a href="http://getbootstrap.com/">Bootstrap</a>, Copyright &copy; 2011-2015 Twitter, Inc.</li>
<li><a href="http://fontawesome.io/">Font Awesome</a>, Copyright &copy; 2015 Dave Gandy</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
</button>
</div>
<p translate>Syncthing includes the following software or portions thereof:</p>
<ul class="list-unstyled two-columns">
<li><a href="https://golang.org">The Go Programming Language</a>, Copyright &copy; 2012 The Go Authors.</li>
<li><a href="https://github.com/bkaradzic/go-lz4">bkaradzic/go-lz4</a>, Copyright &copy; 2011-2012 Branimir Karadzic, 2013 Damian Gryski.</li>
<li><a href="https://github.com/kardianos/osext">kardianos/osext</a>, Copyright &copy; 2012 Daniel Theophanes.</li>
<li><a href="https://github.com/golang/snappy">golang/snappy</a>, Copyright &copy; 2011 The Snappy-Go Authors.</li>
<li><a href="https://github.com/juju/ratelimit">juju/ratelimit</a>, Copyright &copy; 2015 Canonical Ltd.</li>
<li><a href="https://github.com/thejerf/suture">thejerf/suture</a>, Copyright &copy; 2014-2015 Barracuda Networks, Inc.</li>
<li><a href="https://github.com/syndtr/goleveldb">syndtr/goleveldb</a>, Copyright &copy; 2012, Suryandaru Triandana</li>
<li><a href="https://github.com/vitrun/qart">vitrun/qart</a>, Copyright &copy; The Go Authors.</li>
<li><a href="https://angularjs.org/">AngularJS</a>, Copyright &copy; 2010-2015 Google, Inc.</li>
<li><a href="http://getbootstrap.com/">Bootstrap</a>, Copyright &copy; 2011-2015 Twitter, Inc.</li>
<li><a href="http://fontawesome.io/">Font Awesome</a>, Copyright &copy; 2015 Dave Gandy</li>
</ul>
</modal>

View File

@@ -0,0 +1,7 @@
angular.module('syncthing.core')
.directive('httpErrorDialog', function () {
return {
restrict: 'A',
templateUrl: 'syncthing/core/httpErrorDialogView.html'
};
});

View File

@@ -1,7 +1,5 @@
<modal id="httpError" status="danger" icon="exclamation-circle" heading="{{'Connection Error' | translate}}" large="no" closeable="no">
<div class="modal-body">
<p translate>
Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.
</p>
</div>
<modal id="httpError" status="danger" icon="exclamation-circle" title="{{'Connection Error' | translate}}">
<p translate>
Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.
</p>
</modal>

View File

@@ -0,0 +1,7 @@
angular.module('syncthing.core')
.directive('majorUpgradeModal', function () {
return {
restrict: 'A',
templateUrl: 'syncthing/core/majorUpgradeModalView.html'
};
});

View File

@@ -1,20 +1,29 @@
<modal id="majorUpgrade" status="danger" icon="arrow-circle-up" heading="{{'Major Upgrade' | translate}}" large="no" closeable="yes">
<div class="modal-body">
<p>
<span translate>This is a major version upgrade.</span>
<span translate>A new major version may not be compatible with previous versions.</span>
<span translate>Please consult the release notes before performing a major upgrade.</span>
</p>
<p>
<a href="https://github.com/syncthing/syncthing/releases/tag/{{upgradeInfo.latest}}" target="_blank" translate>Release Notes</a>
</p>
<div id="majorUpgrade" class="modal fade" tabindex="-1" data-backdrop="true" data-keyboard="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header alert alert-danger">
<h4 class="modal-title">
<span class="fa fa-arrow-circle-up"></span><span translate>Major Upgrade</span>
</h4>
</div>
<div class="modal-body">
<p>
<span translate>This is a major version upgrade.</span>
<span translate>A new major version may not be compatible with previous versions.</span>
<span translate>Please consult the release notes before performing a major upgrade.</span>
</p>
<p>
<a href="https://github.com/syncthing/syncthing/releases/tag/{{upgradeInfo.latest}}" target="_blank" translate>Release Notes</a>
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" ng-click="upgrade()">
<span class="fa fa-check"></span>&nbsp;<span translate>Upgrade</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary btn-sm" ng-click="upgrade()">
<span class="fa fa-check"></span>&nbsp;<span translate>Upgrade</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
</button>
</div>
</modal>
</div>

View File

@@ -6,70 +6,11 @@ angular.module('syncthing.core')
replace: true,
transclude: true,
scope: {
heading: '@',
title: '@',
status: '@',
icon: '@',
closeable: '@',
close: '@',
large: '@'
},
link: function (scope, element, attrs) {
// before modal show animation
$(element).on('show.bs.modal', function () {
// cycle through open modals, acertain modal with highest z-index
var largestZ = 1040;
$('.modal:visible').each(function (i) {
var thisZ = parseInt($(this).css('zIndex'));
if (thisZ > largestZ) {
largestZ = thisZ;
}
});
// set this modal's z-index to be 10 above the highest z
var aboveLargestZ = largestZ + 10;
$(element).css('zIndex', aboveLargestZ);
// set backdrop z-index. timeout used because element does not exist immediatly
setTimeout(function () {
$('.modal-backdrop:not(:last)').removeClass('in').addClass('out');
$('.modal-backdrop:last').attr('for-modal-id', $(element).attr('id')).css('zIndex', aboveLargestZ - 5);
}, 0);
});
// BEFORE modal hide animation
$(element).on('hide.bs.modal', function () {
// find and unhide the next backdrop down in z order
var sel = false, largestZ = 0;
$('.modal-backdrop').each(function (i) {
var thisZ = parseInt($(this).css('zIndex'));
if (thisZ > largestZ && $(this).attr('for-modal-id') !== $(element).attr('id')) {
largestZ = thisZ;
sel = i;
}
});
if (sel !== false) {
$('.modal-backdrop:eq(' + sel + ')').removeClass('out').addClass('in');
}
});
// AFTER modal hide animation
$(element).on('hidden.bs.modal', function () {
// reset z-index of modal to normal
$(element).css('zIndex', 1040);
// fix scrolling by re-adding .modal-open to body
if ($('.modal:visible').length > 0) {
$('body').addClass('modal-open');
}
});
// inform syncthingContoller that a modal is ready
scope.$parent.modalLoaded();
}
};
});

View File

@@ -0,0 +1,7 @@
angular.module('syncthing.core')
.directive('networkErrorDialog', function () {
return {
restrict: 'A',
templateUrl: 'syncthing/core/networkErrorDialogView.html'
};
});

View File

@@ -1,7 +1,5 @@
<modal id="networkError" status="danger" icon="exclamation-circle" heading="{{'Connection Error' | translate}}" large="no" closeable="no">
<div class="modal-body">
<p translate>
Syncthing seems to be down, or there is a problem with your Internet connection. Retrying&hellip;
</p>
</div>
<modal id="networkError" status="danger" icon="exclamation-circle" title="{{'Connection Error' | translate}}">
<p translate>
Syncthing seems to be down, or there is a problem with your Internet connection. Retrying&hellip;
</p>
</modal>

View File

@@ -0,0 +1,7 @@
angular.module('syncthing.core')
.directive('restartingDialog', function () {
return {
restrict: 'A',
templateUrl: 'syncthing/core/restartingDialogView.html'
};
});

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