mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-04 11:59:12 -05:00
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b3fa8da60 | ||
|
|
2fae7ccf5c | ||
|
|
f36f48c2cf | ||
|
|
8b726c7e8b | ||
|
|
8eb0687407 | ||
|
|
9b9912ba9e | ||
|
|
22d0ed8225 | ||
|
|
b7a58d2f87 | ||
|
|
219ece22fc | ||
|
|
4874301615 | ||
|
|
1827dda0c6 | ||
|
|
d088b01f75 | ||
|
|
00c5062eab | ||
|
|
84eacde63a | ||
|
|
d98290c17f | ||
|
|
6d94a3be05 | ||
|
|
a2833d18ed | ||
|
|
6f95afdc59 | ||
|
|
aaa75a32a5 | ||
|
|
c300015ac2 | ||
|
|
eb4f5e9faa | ||
|
|
c7aec839ae | ||
|
|
37aa89b19d | ||
|
|
3f94e70488 | ||
|
|
435afa0eea | ||
|
|
fb82a5e086 | ||
|
|
71d98c2f26 | ||
|
|
4a97aa12d6 | ||
|
|
c9e67fb460 | ||
|
|
eb1a234a77 | ||
|
|
394c2b67d6 | ||
|
|
aeb3af7105 | ||
|
|
e5cf99e31c | ||
|
|
8120535a35 | ||
|
|
ba01433381 | ||
|
|
6b99cdb83a | ||
|
|
e7b71f8743 | ||
|
|
03935b2d64 | ||
|
|
2b89f33765 | ||
|
|
9c82a4ca60 | ||
|
|
7eb9b9b1c6 | ||
|
|
69b35b2ede | ||
|
|
e8016abd97 | ||
|
|
e54036be25 | ||
|
|
1dc894087c | ||
|
|
b014967550 | ||
|
|
490962ccdb | ||
|
|
d10e81fb3d | ||
|
|
33a87f54bb | ||
|
|
2cdfa59fbe | ||
|
|
bebe74fa4a | ||
|
|
4098f97735 | ||
|
|
a0b7ac402d | ||
|
|
0ca4482977 | ||
|
|
5cf15db6e4 | ||
|
|
6379d50f07 | ||
|
|
5a2792ae24 | ||
|
|
9b26f3cd84 | ||
|
|
316be5ee34 | ||
|
|
f208e6f0b6 | ||
|
|
7c8c131e1a | ||
|
|
81e71d7275 | ||
|
|
fc6c4b8e16 | ||
|
|
2280566bca | ||
|
|
33173e76a0 | ||
|
|
cc0b9e5088 | ||
|
|
ecc72d7693 | ||
|
|
4ab4aeacb0 | ||
|
|
348d12bd60 | ||
|
|
7b686c1103 | ||
|
|
403583cfbb | ||
|
|
29bff06cd6 | ||
|
|
2b80057ac9 | ||
|
|
c3625e16d7 | ||
|
|
f4642e9e66 | ||
|
|
5bdf4c6143 | ||
|
|
80aaf6a065 | ||
|
|
3025caf932 | ||
|
|
95cfc50fbd | ||
|
|
ded0925155 | ||
|
|
931408037c | ||
|
|
3318651565 | ||
|
|
2ab07f3aac | ||
|
|
1340e54327 | ||
|
|
1b6e4645b1 | ||
|
|
a6a573f5dc | ||
|
|
415415b5b2 | ||
|
|
38e9b92c42 | ||
|
|
345d727936 | ||
|
|
86e8e5199e | ||
|
|
739979a116 | ||
|
|
7cbc81adca | ||
|
|
737d0fa23c |
12
AUTHORS
12
AUTHORS
@@ -1,8 +1,9 @@
|
||||
# This is the official list of Syncthing authors for copyright purposes.
|
||||
|
||||
Aaron Bieber <qbit@deftly.net>
|
||||
Adam Piggott <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
|
||||
Adam Piggott <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
|
||||
Alexander Graf <register-github@alex-graf.de>
|
||||
Anderson Mesquita <andersonvom@gmail.com>
|
||||
Andrew Dunham <andrew@du.nham.ca>
|
||||
Antony Male <antony.male@gmail.com>
|
||||
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com> <frioux@gmail.com>
|
||||
@@ -34,9 +35,10 @@ Felix Unterpaintner <bigbear2nd@gmail.com>
|
||||
Francois-Xavier Gsell <fxgsell@gmail.com>
|
||||
Frank Isemann <frank@isemann.name>
|
||||
Gilli Sigurdsson <gilli@vx.is>
|
||||
Jaakko Hannikainen <jgke@jgke.fi>
|
||||
Jacek Szafarkiewicz <szafar@linux.pl>
|
||||
Jakob Borg <jakob@nym.se>
|
||||
Jake Peterson <jake@acogdev.com>
|
||||
Jakob Borg <jakob@nym.se>
|
||||
James Patterson <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
||||
Jaroslav Malec <dzardacz@gmail.com>
|
||||
Jens Diemer <github.com@jensdiemer.de> <git@jensdiemer.de>
|
||||
@@ -54,6 +56,7 @@ Matt Burke <mburke@amplify.com> <burkemw3@gmail.com>
|
||||
Michael Jephcote <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
||||
Michael Ploujnikov <ploujj@gmail.com>
|
||||
Michael Tilli <pyfisch@gmail.com>
|
||||
Nate Morrison <natemorrison@gmail.com>
|
||||
Pascal Jungblut <github@pascalj.com> <mail@pascal-jungblut.com>
|
||||
Peter Hoeg <peter@speartail.com>
|
||||
Philippe Schommers <philippe@schommers.be>
|
||||
@@ -62,14 +65,15 @@ Piotr Bejda <piotrb10@gmail.com>
|
||||
Ryan Sullivan <kayoticsully@gmail.com>
|
||||
Scott Klupfel <kluppy@going2blue.com>
|
||||
Sergey Mishin <ralder@yandex.ru>
|
||||
Stefan Tatschner <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
|
||||
Stefan Kuntz <stefan.github@gmail.com> <Stefan.github@gmail.com>
|
||||
Stefan Tatschner <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
|
||||
Tim Abell <tim@timwise.co.uk>
|
||||
Tobias Nygren <tnn@nygren.pp.se>
|
||||
Tomas Cerveny <kozec@kozec.com>
|
||||
Tully Robinson <tully@tojr.org>
|
||||
Tyler Brazier <tyler@tylerbrazier.com>
|
||||
Veeti Paananen <veeti.paananen@rojekti.fi>
|
||||
Vil Brekin <vilbrekin@gmail.com>
|
||||
Victor Buinsky <vix_booja@tut.by>
|
||||
Vil Brekin <vilbrekin@gmail.com>
|
||||
William A. Kennington III <william@wkennington.com>
|
||||
Yannic A. <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>
|
||||
|
||||
4
Godeps/Godeps.json
generated
4
Godeps/Godeps.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"ImportPath": "github.com/syncthing/syncthing",
|
||||
"GoVersion": "go1.5.1",
|
||||
"GoVersion": "go1.5.2",
|
||||
"Packages": [
|
||||
"./cmd/..."
|
||||
],
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/kardianos/osext",
|
||||
"Rev": "345163ffe35aa66560a4cd7dddf00f3ae21c9fda"
|
||||
"Rev": "431e263e413efe4446ede50cec4819b26659fbe7"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/rcrowley/go-metrics",
|
||||
|
||||
10
Godeps/_workspace/src/github.com/kardianos/osext/osext.go
generated
vendored
10
Godeps/_workspace/src/github.com/kardianos/osext/osext.go
generated
vendored
@@ -7,12 +7,18 @@ package osext
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
var cx, ce = executableClean()
|
||||
|
||||
func executableClean() (string, error) {
|
||||
p, err := executable()
|
||||
return filepath.Clean(p), err
|
||||
}
|
||||
|
||||
// Executable returns an absolute path that can be used to
|
||||
// re-invoke the current program.
|
||||
// It may not be valid after the current program exits.
|
||||
func Executable() (string, error) {
|
||||
p, err := executable()
|
||||
return filepath.Clean(p), err
|
||||
return cx, ce
|
||||
}
|
||||
|
||||
// Returns same path as Executable, returns just the folder
|
||||
|
||||
4
Godeps/_workspace/src/github.com/kardianos/osext/osext_procfs.go
generated
vendored
4
Godeps/_workspace/src/github.com/kardianos/osext/osext_procfs.go
generated
vendored
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux netbsd solaris dragonfly
|
||||
// +build linux netbsd solaris dragonfly freebsd
|
||||
|
||||
package osext
|
||||
|
||||
@@ -27,6 +27,8 @@ func executable() (string, error) {
|
||||
return execpath, nil
|
||||
case "netbsd":
|
||||
return os.Readlink("/proc/curproc/exe")
|
||||
case "freebsd":
|
||||
return os.Readlink("/proc/curproc/file")
|
||||
case "dragonfly":
|
||||
return os.Readlink("/proc/curproc/file")
|
||||
case "solaris":
|
||||
|
||||
48
Godeps/_workspace/src/github.com/kardianos/osext/osext_sysctl.go
generated
vendored
48
Godeps/_workspace/src/github.com/kardianos/osext/osext_sysctl.go
generated
vendored
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin freebsd openbsd
|
||||
// +build darwin openbsd
|
||||
|
||||
package osext
|
||||
|
||||
@@ -53,30 +53,30 @@ func executable() (string, error) {
|
||||
// NULL terminated arguments.
|
||||
var args []string
|
||||
argv := uintptr(unsafe.Pointer(&buf[0]))
|
||||
Loop:
|
||||
for {
|
||||
argp := *(**[1<<20]byte)(unsafe.Pointer(argv))
|
||||
if argp == nil {
|
||||
break
|
||||
}
|
||||
for i := 0; uintptr(i) < n; i++ {
|
||||
// we don't want the full arguments list
|
||||
if string(argp[i]) == " " {
|
||||
break Loop
|
||||
}
|
||||
if argp[i] != 0 {
|
||||
continue
|
||||
}
|
||||
args = append(args, string(argp[:i]))
|
||||
n -= uintptr(i)
|
||||
break
|
||||
}
|
||||
if n < unsafe.Sizeof(argv) {
|
||||
break
|
||||
}
|
||||
argv += unsafe.Sizeof(argv)
|
||||
n -= unsafe.Sizeof(argv)
|
||||
Loop:
|
||||
for {
|
||||
argp := *(**[1 << 20]byte)(unsafe.Pointer(argv))
|
||||
if argp == nil {
|
||||
break
|
||||
}
|
||||
for i := 0; uintptr(i) < n; i++ {
|
||||
// we don't want the full arguments list
|
||||
if string(argp[i]) == " " {
|
||||
break Loop
|
||||
}
|
||||
if argp[i] != 0 {
|
||||
continue
|
||||
}
|
||||
args = append(args, string(argp[:i]))
|
||||
n -= uintptr(i)
|
||||
break
|
||||
}
|
||||
if n < unsafe.Sizeof(argv) {
|
||||
break
|
||||
}
|
||||
argv += unsafe.Sizeof(argv)
|
||||
n -= unsafe.Sizeof(argv)
|
||||
}
|
||||
execPath = args[0]
|
||||
// There is no canonical way to get an executable path on
|
||||
// OpenBSD, so check PATH in case we are called directly
|
||||
|
||||
26
NICKS
26
NICKS
@@ -1,19 +1,11 @@
|
||||
# This file maps email addresses used in commits to nicks used the changelog.
|
||||
|
||||
AudriusButkevicius <audrius.butkevicius@gmail.com>
|
||||
Cathryne <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
|
||||
KayoticSully <kayoticsully@gmail.com>
|
||||
LordLandon <lordlandon@gmail.com>
|
||||
Moter8 <moter8@gmail.com>
|
||||
Nutomic <me@nutomic.com>
|
||||
Rewt0r <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
||||
Stefan-Code <stefan.github@gmail.com> <Stefan.github@gmail.com>
|
||||
Vilbrekin <vilbrekin@gmail.com>
|
||||
Zillode <zillode@zillode.be>
|
||||
acogdev <jake@acogdev.com>
|
||||
alex2108 <register-github@alex-graf.de>
|
||||
andersonvom <andersonvom@gmail.com>
|
||||
andrew-d <andrew@du.nham.ca>
|
||||
asdil12 <dominik@heidler.eu>
|
||||
AudriusButkevicius <audrius.butkevicius@gmail.com>
|
||||
bencurthoys <ben@bencurthoys.com>
|
||||
bigbear2nd <bigbear2nd@gmail.com>
|
||||
brbecker <brbecker@gmail.com>
|
||||
@@ -24,6 +16,7 @@ buinsky <vix_booja@tut.by>
|
||||
burkemw3 <mburke@amplify.com> <burkemw3@gmail.com>
|
||||
calmh <jakob@nym.se>
|
||||
canton7 <antony.male@gmail.com>
|
||||
Cathryne <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
|
||||
cdata <chris@scriptolo.gy>
|
||||
cdhowie <me@chrishowie.com>
|
||||
ceh <emil@hessman.se>
|
||||
@@ -39,18 +32,24 @@ gillisig <gilli@vx.is>
|
||||
hadogenes <szafar@linux.pl>
|
||||
jarlebring <jarlebring@gmail.com>
|
||||
jedie <github.com@jensdiemer.de> <git@jensdiemer.de>
|
||||
jgke <jgke@jgke.fi>
|
||||
jpjp <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
||||
kamadak <kamada@nanohz.org>
|
||||
KayoticSully <kayoticsully@gmail.com>
|
||||
kilburn <kilburn@la3.org>
|
||||
kluppy <kluppy@going2blue.com>
|
||||
kozec <kozec@kozec.com>
|
||||
krozycki <rozycki.karol@gmail.com>
|
||||
LordLandon <lordlandon@gmail.com>
|
||||
marcindziadus <dziadus.marcin@gmail.com>
|
||||
marclaporte <marc@marclaporte.com>
|
||||
mateon1 <matin1111@wp.pl>
|
||||
mogwa1 <devriesb@gmail.com>
|
||||
moshen <moshen.colin@gmail.com>
|
||||
Moter8 <moter8@gmail.com>
|
||||
mvdan <mvdan@mvdan.cc>
|
||||
nrm21 <natemorrison@gmail.com>
|
||||
Nutomic <me@nutomic.com>
|
||||
pascalj <github@pascalj.com> <mail@pascal-jungblut.com>
|
||||
peterhoeg <peter@speartail.com>
|
||||
philips <brandon@ifup.org>
|
||||
@@ -60,16 +59,21 @@ pluby <phill.luby@newredo.com>
|
||||
pyfisch <pyfisch@gmail.com>
|
||||
qbit <qbit@deftly.net>
|
||||
ralder <ralder@yandex.ru>
|
||||
Rewt0r <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
||||
rumpelsepp <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
|
||||
sciurius <jvromans@squirrel.nl>
|
||||
seehuhn <voss@seehuhn.de>
|
||||
snnd <dw@risu.io>
|
||||
simplypeachy <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
|
||||
snnd <dw@risu.io>
|
||||
Stefan-Code <stefan.github@gmail.com> <Stefan.github@gmail.com>
|
||||
timabell <tim@timwise.co.uk>
|
||||
tnn2 <tnn@nygren.pp.se>
|
||||
tojrobinson <tully@tojr.org>
|
||||
tylerbrazier <tyler@tylerbrazier.com>
|
||||
uok <ueomkail@gmail.com> <uok@users.noreply.github.com>
|
||||
veeti <veeti.paananen@rojekti.fi>
|
||||
Vilbrekin <vilbrekin@gmail.com>
|
||||
wkennington <william@wkennington.com>
|
||||
wsgcsysadmin <e.meitner@willystreet.coo>
|
||||
Zillode <zillode@zillode.be>
|
||||
zukoo <fxgsell@gmail.com>
|
||||
|
||||
3
build.go
3
build.go
@@ -219,7 +219,7 @@ func build(pkg string, tags []string) {
|
||||
}
|
||||
|
||||
rmr(binary)
|
||||
args := []string{"build", "-ldflags", ldflags()}
|
||||
args := []string{"build", "-i", "-v", "-ldflags", ldflags()}
|
||||
if len(tags) > 0 {
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
}
|
||||
@@ -448,7 +448,6 @@ func ldflags() string {
|
||||
fmt.Fprintf(b, " -X main.BuildStamp%c%d", sep, buildStamp())
|
||||
fmt.Fprintf(b, " -X main.BuildUser%c%s", sep, buildUser())
|
||||
fmt.Fprintf(b, " -X main.BuildHost%c%s", sep, buildHost())
|
||||
fmt.Fprintf(b, " -X main.BuildEnv%c%s", sep, buildEnvironment())
|
||||
return b.String()
|
||||
}
|
||||
|
||||
|
||||
33
build.sh
33
build.sh
@@ -74,27 +74,26 @@ case "${1:-default}" in
|
||||
;;
|
||||
|
||||
all)
|
||||
build -goos darwin -goarch amd64 tar
|
||||
platforms=(
|
||||
darwin-amd64 dragonfly-amd64 freebsd-amd64 linux-amd64 netbsd-amd64 openbsd-amd64 solaris-amd64 windows-amd64
|
||||
freebsd-386 linux-386 netbsd-386 openbsd-386 windows-386
|
||||
linux-arm
|
||||
)
|
||||
|
||||
build -goos dragonfly -goarch amd64 tar
|
||||
for plat in "${platforms[@]}"; do
|
||||
echo Building "$plat"
|
||||
|
||||
build -goos freebsd -goarch 386 tar
|
||||
build -goos freebsd -goarch amd64 tar
|
||||
goos="${plat%-*}"
|
||||
goarch="${plat#*-}"
|
||||
dist="tar"
|
||||
|
||||
build -goos linux -goarch 386 tar
|
||||
build -goos linux -goarch amd64 tar
|
||||
build -goos linux -goarch arm tar
|
||||
if [[ $goos == "windows" ]]; then
|
||||
dist="zip"
|
||||
fi
|
||||
|
||||
build -goos netbsd -goarch 386 tar
|
||||
build -goos netbsd -goarch amd64 tar
|
||||
|
||||
build -goos openbsd -goarch 386 tar
|
||||
build -goos openbsd -goarch amd64 tar
|
||||
|
||||
build -goos solaris -goarch amd64 tar
|
||||
|
||||
build -goos windows -goarch 386 zip
|
||||
build -goos windows -goarch amd64 zip
|
||||
build -goos "$goos" -goarch "$goarch" "$dist"
|
||||
echo
|
||||
done
|
||||
;;
|
||||
|
||||
test-cov)
|
||||
|
||||
@@ -15,14 +15,14 @@ import (
|
||||
)
|
||||
|
||||
type addressLister struct {
|
||||
upnpSvc *upnpSvc
|
||||
cfg *config.Wrapper
|
||||
upnpService *upnpService
|
||||
cfg *config.Wrapper
|
||||
}
|
||||
|
||||
func newAddressLister(upnpSvc *upnpSvc, cfg *config.Wrapper) *addressLister {
|
||||
func newAddressLister(upnpService *upnpService, cfg *config.Wrapper) *addressLister {
|
||||
return &addressLister{
|
||||
upnpSvc: upnpSvc,
|
||||
cfg: cfg,
|
||||
upnpService: upnpService,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,10 +73,10 @@ func (e *addressLister) addresses(includePrivateIPV4 bool) []string {
|
||||
}
|
||||
}
|
||||
|
||||
// Get an external port mapping from the upnpSvc, if it has one. If so,
|
||||
// Get an external port mapping from the upnpService, if it has one. If so,
|
||||
// add it as another unspecified address.
|
||||
if e.upnpSvc != nil {
|
||||
if port := e.upnpSvc.ExternalPort(); port != 0 {
|
||||
if e.upnpService != nil {
|
||||
if port := e.upnpService.ExternalPort(); port != 0 {
|
||||
addrs = append(addrs, fmt.Sprintf("tcp://:%d", port))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,17 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
)
|
||||
|
||||
// The auditSvc subscribes to events and writes these in JSON format, one
|
||||
// The auditService subscribes to events and writes these in JSON format, one
|
||||
// event per line, to the specified writer.
|
||||
type auditSvc struct {
|
||||
type auditService struct {
|
||||
w io.Writer // audit destination
|
||||
stop chan struct{} // signals time to stop
|
||||
started chan struct{} // signals startup complete
|
||||
stopped chan struct{} // signals stop complete
|
||||
}
|
||||
|
||||
func newAuditSvc(w io.Writer) *auditSvc {
|
||||
return &auditSvc{
|
||||
func newAuditService(w io.Writer) *auditService {
|
||||
return &auditService{
|
||||
w: w,
|
||||
stop: make(chan struct{}),
|
||||
started: make(chan struct{}),
|
||||
@@ -32,7 +32,7 @@ func newAuditSvc(w io.Writer) *auditSvc {
|
||||
}
|
||||
|
||||
// Serve runs the audit service.
|
||||
func (s *auditSvc) Serve() {
|
||||
func (s *auditService) Serve() {
|
||||
defer close(s.stopped)
|
||||
sub := events.Default.Subscribe(events.AllEvents)
|
||||
defer events.Default.Unsubscribe(sub)
|
||||
@@ -52,18 +52,18 @@ func (s *auditSvc) Serve() {
|
||||
}
|
||||
|
||||
// Stop stops the audit service.
|
||||
func (s *auditSvc) Stop() {
|
||||
func (s *auditService) Stop() {
|
||||
close(s.stop)
|
||||
}
|
||||
|
||||
// WaitForStart returns once the audit service is ready to receive events, or
|
||||
// immediately if it's already running.
|
||||
func (s *auditSvc) WaitForStart() {
|
||||
func (s *auditService) WaitForStart() {
|
||||
<-s.started
|
||||
}
|
||||
|
||||
// WaitForStop returns once the audit service has stopped.
|
||||
// (Needed by the tests.)
|
||||
func (s *auditSvc) WaitForStop() {
|
||||
func (s *auditService) WaitForStop() {
|
||||
<-s.stopped
|
||||
}
|
||||
@@ -17,13 +17,13 @@ import (
|
||||
|
||||
func TestAuditService(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
svc := newAuditSvc(buf)
|
||||
service := newAuditService(buf)
|
||||
|
||||
// Event sent before start, will not be logged
|
||||
events.Default.Log(events.Ping, "the first event")
|
||||
|
||||
go svc.Serve()
|
||||
svc.WaitForStart()
|
||||
go service.Serve()
|
||||
service.WaitForStart()
|
||||
|
||||
// Event that should end up in the audit log
|
||||
events.Default.Log(events.Ping, "the second event")
|
||||
@@ -31,8 +31,8 @@ func TestAuditService(t *testing.T) {
|
||||
// We need to give the events time to arrive, since the channels are buffered etc.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
svc.Stop()
|
||||
svc.WaitForStop()
|
||||
service.Stop()
|
||||
service.WaitForStop()
|
||||
|
||||
// This event should not be logged, since we have stopped.
|
||||
events.Default.Log(events.Ping, "the third event")
|
||||
@@ -48,16 +48,16 @@ var (
|
||||
startTime = time.Now()
|
||||
)
|
||||
|
||||
type apiSvc struct {
|
||||
type apiService struct {
|
||||
id protocol.DeviceID
|
||||
cfg *config.Wrapper
|
||||
assetDir string
|
||||
model *model.Model
|
||||
eventSub *events.BufferedSubscription
|
||||
discoverer *discover.CachingMux
|
||||
relaySvc *relay.Svc
|
||||
relayService *relay.Service
|
||||
listener net.Listener
|
||||
fss *folderSummarySvc
|
||||
fss *folderSummaryService
|
||||
stop chan struct{}
|
||||
systemConfigMut sync.Mutex
|
||||
|
||||
@@ -65,26 +65,26 @@ type apiSvc struct {
|
||||
systemLog *logger.Recorder
|
||||
}
|
||||
|
||||
func newAPISvc(id protocol.DeviceID, cfg *config.Wrapper, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc, errors, systemLog *logger.Recorder) (*apiSvc, error) {
|
||||
svc := &apiSvc{
|
||||
func newAPIService(id protocol.DeviceID, cfg *config.Wrapper, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.CachingMux, relayService *relay.Service, errors, systemLog *logger.Recorder) (*apiService, error) {
|
||||
service := &apiService{
|
||||
id: id,
|
||||
cfg: cfg,
|
||||
assetDir: assetDir,
|
||||
model: m,
|
||||
eventSub: eventSub,
|
||||
discoverer: discoverer,
|
||||
relaySvc: relaySvc,
|
||||
relayService: relayService,
|
||||
systemConfigMut: sync.NewMutex(),
|
||||
guiErrors: errors,
|
||||
systemLog: systemLog,
|
||||
}
|
||||
|
||||
var err error
|
||||
svc.listener, err = svc.getListener(cfg.GUI())
|
||||
return svc, err
|
||||
service.listener, err = service.getListener(cfg.GUI())
|
||||
return service, err
|
||||
}
|
||||
|
||||
func (s *apiSvc) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
|
||||
func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
|
||||
cert, err := tls.LoadX509KeyPair(locations[locHTTPSCertFile], locations[locHTTPSKeyFile])
|
||||
if err != nil {
|
||||
l.Infoln("Loading HTTPS certificate:", err)
|
||||
@@ -130,7 +130,12 @@ func (s *apiSvc) getListener(guiCfg config.GUIConfiguration) (net.Listener, erro
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func (s *apiSvc) Serve() {
|
||||
func sendJSON(w http.ResponseWriter, jsonObject interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(jsonObject)
|
||||
}
|
||||
|
||||
func (s *apiService) Serve() {
|
||||
s.stop = make(chan struct{})
|
||||
|
||||
// The GET handlers
|
||||
@@ -224,7 +229,7 @@ func (s *apiSvc) Serve() {
|
||||
ReadTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
s.fss = newFolderSummarySvc(s.cfg, s.model)
|
||||
s.fss = newFolderSummaryService(s.cfg, s.model)
|
||||
defer s.fss.Stop()
|
||||
s.fss.ServeBackground()
|
||||
|
||||
@@ -242,20 +247,20 @@ func (s *apiSvc) Serve() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiSvc) Stop() {
|
||||
func (s *apiService) Stop() {
|
||||
close(s.stop)
|
||||
s.listener.Close()
|
||||
}
|
||||
|
||||
func (s *apiSvc) String() string {
|
||||
return fmt.Sprintf("apiSvc@%p", s)
|
||||
func (s *apiService) String() string {
|
||||
return fmt.Sprintf("apiService@%p", s)
|
||||
}
|
||||
|
||||
func (s *apiSvc) VerifyConfiguration(from, to config.Configuration) error {
|
||||
func (s *apiService) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *apiSvc) CommitConfiguration(from, to config.Configuration) bool {
|
||||
func (s *apiService) CommitConfiguration(from, to config.Configuration) bool {
|
||||
if to.GUI == from.GUI {
|
||||
return true
|
||||
}
|
||||
@@ -317,7 +322,7 @@ func debugMiddleware(h http.Handler) http.Handler {
|
||||
written = rf.Int()
|
||||
}
|
||||
}
|
||||
l.Debugf("http: %s %q: status %d, %d bytes in %.02f ms", r.Method, r.URL.String(), status, written, ms)
|
||||
httpl.Debugf("http: %s %q: status %d, %d bytes in %.02f ms", r.Method, r.URL.String(), status, written, ms)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -365,16 +370,12 @@ func withDetailsMiddleware(id protocol.DeviceID, h http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiSvc) restPing(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"ping": "pong",
|
||||
})
|
||||
func (s *apiService) restPing(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, map[string]string{"ping": "pong"})
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemVersion(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
func (s *apiService) getSystemVersion(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, map[string]string{
|
||||
"version": Version,
|
||||
"codename": Codename,
|
||||
"longVersion": LongVersion,
|
||||
@@ -383,29 +384,28 @@ func (s *apiSvc) getSystemVersion(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemDebug(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
func (s *apiService) getSystemDebug(w http.ResponseWriter, r *http.Request) {
|
||||
names := l.Facilities()
|
||||
enabled := l.FacilityDebugging()
|
||||
sort.Strings(enabled)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
sendJSON(w, map[string]interface{}{
|
||||
"facilities": names,
|
||||
"enabled": enabled,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemDebug(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postSystemDebug(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
q := r.URL.Query()
|
||||
for _, f := range strings.Split(q.Get("enable"), ",") {
|
||||
if f == "" {
|
||||
if f == "" || l.ShouldDebug(f) {
|
||||
continue
|
||||
}
|
||||
l.SetDebug(f, true)
|
||||
l.Infof("Enabled debug data for %q", f)
|
||||
}
|
||||
for _, f := range strings.Split(q.Get("disable"), ",") {
|
||||
if f == "" {
|
||||
if f == "" || !l.ShouldDebug(f) {
|
||||
continue
|
||||
}
|
||||
l.SetDebug(f, false)
|
||||
@@ -413,7 +413,7 @@ func (s *apiSvc) postSystemDebug(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiSvc) getDBBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getDBBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
prefix := qs.Get("prefix")
|
||||
@@ -424,14 +424,10 @@ func (s *apiSvc) getDBBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
levels = -1
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
tree := s.model.GlobalDirectoryTree(folder, prefix, levels, dirsonly)
|
||||
|
||||
json.NewEncoder(w).Encode(tree)
|
||||
sendJSON(w, s.model.GlobalDirectoryTree(folder, prefix, levels, dirsonly))
|
||||
}
|
||||
|
||||
func (s *apiSvc) getDBCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getDBCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var folder = qs.Get("folder")
|
||||
var deviceStr = qs.Get("device")
|
||||
@@ -442,20 +438,15 @@ func (s *apiSvc) getDBCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
res := map[string]float64{
|
||||
sendJSON(w, map[string]float64{
|
||||
"completion": s.model.Completion(device, folder),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiSvc) getDBStatus(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
res := folderSummary(s.cfg, s.model, folder)
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
sendJSON(w, folderSummary(s.cfg, s.model, folder))
|
||||
}
|
||||
|
||||
func folderSummary(cfg *config.Wrapper, m *model.Model, folder string) map[string]interface{} {
|
||||
@@ -497,13 +488,13 @@ func folderSummary(cfg *config.Wrapper, m *model.Model, folder string) map[strin
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *apiSvc) postDBOverride(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postDBOverride(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var folder = qs.Get("folder")
|
||||
go s.model.Override(folder)
|
||||
}
|
||||
|
||||
func (s *apiSvc) getDBNeed(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
|
||||
folder := qs.Get("folder")
|
||||
@@ -520,38 +511,29 @@ func (s *apiSvc) getDBNeed(w http.ResponseWriter, r *http.Request) {
|
||||
progress, queued, rest, total := s.model.NeedFolderFiles(folder, page, perpage)
|
||||
|
||||
// Convert the struct to a more loose structure, and inject the size.
|
||||
output := map[string]interface{}{
|
||||
sendJSON(w, map[string]interface{}{
|
||||
"progress": s.toNeedSlice(progress),
|
||||
"queued": s.toNeedSlice(queued),
|
||||
"rest": s.toNeedSlice(rest),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"perpage": perpage,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(output)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemConnections(w http.ResponseWriter, r *http.Request) {
|
||||
var res = s.model.ConnectionStats()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
func (s *apiService) getSystemConnections(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, s.model.ConnectionStats())
|
||||
}
|
||||
|
||||
func (s *apiSvc) getDeviceStats(w http.ResponseWriter, r *http.Request) {
|
||||
var res = s.model.DeviceStatistics()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
func (s *apiService) getDeviceStats(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, s.model.DeviceStatistics())
|
||||
}
|
||||
|
||||
func (s *apiSvc) getFolderStats(w http.ResponseWriter, r *http.Request) {
|
||||
var res = s.model.FolderStatistics()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
func (s *apiService) getFolderStats(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, s.model.FolderStatistics())
|
||||
}
|
||||
|
||||
func (s *apiSvc) getDBFile(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getDBFile(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
file := qs.Get("file")
|
||||
@@ -559,26 +541,25 @@ func (s *apiSvc) getDBFile(w http.ResponseWriter, r *http.Request) {
|
||||
lf, _ := s.model.CurrentFolderFile(folder, file)
|
||||
|
||||
av := s.model.Availability(folder, file)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
sendJSON(w, map[string]interface{}{
|
||||
"global": jsonFileInfo(gf),
|
||||
"local": jsonFileInfo(lf),
|
||||
"availability": av,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(s.cfg.Raw())
|
||||
func (s *apiService) getSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, s.cfg.Raw())
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
s.systemConfigMut.Lock()
|
||||
defer s.systemConfigMut.Unlock()
|
||||
|
||||
to, err := config.ReadJSON(r.Body, myID)
|
||||
if err != nil {
|
||||
l.Warnln("decoding posted config:", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -587,7 +568,7 @@ func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(to.GUI.Password), 0)
|
||||
if err != nil {
|
||||
l.Warnln("bcrypting password:", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -614,17 +595,16 @@ func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
s.cfg.Save()
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync})
|
||||
func (s *apiService) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, map[string]bool{"configInSync": configInSync})
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemRestart(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postSystemRestart(w http.ResponseWriter, r *http.Request) {
|
||||
s.flushResponse(`{"ok": "restarting"}`, w)
|
||||
go restart()
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemReset(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postSystemReset(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
|
||||
@@ -650,12 +630,12 @@ func (s *apiSvc) postSystemReset(w http.ResponseWriter, r *http.Request) {
|
||||
go restart()
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemShutdown(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postSystemShutdown(w http.ResponseWriter, r *http.Request) {
|
||||
s.flushResponse(`{"ok": "shutting down"}`, w)
|
||||
go shutdown()
|
||||
}
|
||||
|
||||
func (s *apiSvc) flushResponse(resp string, w http.ResponseWriter) {
|
||||
func (s *apiService) flushResponse(resp string, w http.ResponseWriter) {
|
||||
w.Write([]byte(resp + "\n"))
|
||||
f := w.(http.Flusher)
|
||||
f.Flush()
|
||||
@@ -664,7 +644,7 @@ func (s *apiSvc) flushResponse(resp string, w http.ResponseWriter) {
|
||||
var cpuUsagePercent [10]float64 // The last ten seconds
|
||||
var cpuUsageLock = sync.NewRWMutex()
|
||||
|
||||
func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
@@ -688,12 +668,12 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
res["discoveryMethods"] = discoMethods
|
||||
res["discoveryErrors"] = discoErrors
|
||||
}
|
||||
if s.relaySvc != nil {
|
||||
if s.relayService != nil {
|
||||
res["relaysEnabled"] = true
|
||||
relayClientStatus := make(map[string]bool)
|
||||
relayClientLatency := make(map[string]int)
|
||||
for _, relay := range s.relaySvc.Relays() {
|
||||
latency, ok := s.relaySvc.RelayStatus(relay)
|
||||
for _, relay := range s.relayService.Relays() {
|
||||
latency, ok := s.relayService.RelayStatus(relay)
|
||||
relayClientStatus[relay] = ok
|
||||
relayClientLatency[relay] = int(latency / time.Millisecond)
|
||||
}
|
||||
@@ -711,39 +691,35 @@ func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
res["uptime"] = int(time.Since(startTime).Seconds())
|
||||
res["startTime"] = startTime
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
sendJSON(w, res)
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemError(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string][]logger.Line{
|
||||
func (s *apiService) getSystemError(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, map[string][]logger.Line{
|
||||
"errors": s.guiErrors.Since(time.Time{}),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemError(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postSystemError(w http.ResponseWriter, r *http.Request) {
|
||||
bs, _ := ioutil.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
l.Warnln(string(bs))
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemErrorClear(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postSystemErrorClear(w http.ResponseWriter, r *http.Request) {
|
||||
s.guiErrors.Clear()
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemLog(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getSystemLog(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
since, err := time.Parse(time.RFC3339, q.Get("since"))
|
||||
l.Debugln(err)
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
json.NewEncoder(w).Encode(map[string][]logger.Line{
|
||||
sendJSON(w, map[string][]logger.Line{
|
||||
"messages": s.systemLog.Since(since),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
since, err := time.Parse(time.RFC3339, q.Get("since"))
|
||||
l.Debugln(err)
|
||||
@@ -754,7 +730,7 @@ func (s *apiSvc) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
stats := make(map[string]interface{})
|
||||
metrics.Each(func(name string, intf interface{}) {
|
||||
if m, ok := intf.(*metrics.StandardTimer); ok {
|
||||
@@ -774,8 +750,7 @@ func (s *apiSvc) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(bs)
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
func (s *apiService) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
devices := make(map[string]discover.CacheEntry)
|
||||
|
||||
if s.discoverer != nil {
|
||||
@@ -787,17 +762,15 @@ func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(devices)
|
||||
sendJSON(w, devices)
|
||||
}
|
||||
|
||||
func (s *apiSvc) getReport(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(reportData(s.cfg, s.model))
|
||||
func (s *apiService) getReport(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, reportData(s.cfg, s.model))
|
||||
}
|
||||
|
||||
func (s *apiSvc) getDBIgnores(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
ignores, patterns, err := s.model.GetIgnores(qs.Get("folder"))
|
||||
if err != nil {
|
||||
@@ -805,13 +778,13 @@ func (s *apiSvc) getDBIgnores(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string][]string{
|
||||
sendJSON(w, map[string][]string{
|
||||
"ignore": ignores,
|
||||
"patterns": patterns,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiSvc) postDBIgnores(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postDBIgnores(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
|
||||
var data map[string][]string
|
||||
@@ -832,7 +805,7 @@ func (s *apiSvc) postDBIgnores(w http.ResponseWriter, r *http.Request) {
|
||||
s.getDBIgnores(w, r)
|
||||
}
|
||||
|
||||
func (s *apiSvc) getEvents(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getEvents(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
sinceStr := qs.Get("since")
|
||||
limitStr := qs.Get("limit")
|
||||
@@ -841,8 +814,6 @@ func (s *apiSvc) getEvents(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
s.fss.gotEventRequest()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
// Flush before blocking, to indicate that we've received the request
|
||||
// and that it should not be retried.
|
||||
f := w.(http.Flusher)
|
||||
@@ -853,10 +824,10 @@ func (s *apiSvc) getEvents(w http.ResponseWriter, r *http.Request) {
|
||||
evs = evs[len(evs)-limit:]
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(evs)
|
||||
sendJSON(w, evs)
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
if noUpgrade {
|
||||
http.Error(w, upgrade.ErrUpgradeUnsupported.Error(), 500)
|
||||
return
|
||||
@@ -872,38 +843,36 @@ func (s *apiSvc) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
res["newer"] = upgrade.CompareVersions(rel.Tag, Version) == upgrade.Newer
|
||||
res["majorNewer"] = upgrade.CompareVersions(rel.Tag, Version) == upgrade.MajorNewer
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
sendJSON(w, res)
|
||||
}
|
||||
|
||||
func (s *apiSvc) getDeviceID(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getDeviceID(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
idStr := qs.Get("id")
|
||||
id, err := protocol.DeviceIDFromString(idStr)
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
if err == nil {
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
sendJSON(w, map[string]string{
|
||||
"id": id.String(),
|
||||
})
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
sendJSON(w, map[string]string{
|
||||
"error": err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiSvc) getLang(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getLang(w http.ResponseWriter, r *http.Request) {
|
||||
lang := r.Header.Get("Accept-Language")
|
||||
var langs []string
|
||||
for _, l := range strings.Split(lang, ",") {
|
||||
parts := strings.SplitN(l, ";", 2)
|
||||
langs = append(langs, strings.ToLower(strings.TrimSpace(parts[0])))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(langs)
|
||||
sendJSON(w, langs)
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
rel, err := upgrade.LatestRelease(s.cfg.Options().ReleasesURL, Version)
|
||||
if err != nil {
|
||||
l.Warnln("getting latest release:", err)
|
||||
@@ -925,7 +894,7 @@ func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemPause(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postSystemPause(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var deviceStr = qs.Get("device")
|
||||
|
||||
@@ -938,7 +907,7 @@ func (s *apiSvc) postSystemPause(w http.ResponseWriter, r *http.Request) {
|
||||
s.model.PauseDevice(device)
|
||||
}
|
||||
|
||||
func (s *apiSvc) postSystemResume(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postSystemResume(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var deviceStr = qs.Get("device")
|
||||
|
||||
@@ -951,7 +920,7 @@ func (s *apiSvc) postSystemResume(w http.ResponseWriter, r *http.Request) {
|
||||
s.model.ResumeDevice(device)
|
||||
}
|
||||
|
||||
func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postDBScan(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
if folder != "" {
|
||||
@@ -971,13 +940,13 @@ func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
|
||||
errors := s.model.ScanFolders()
|
||||
if len(errors) > 0 {
|
||||
http.Error(w, "Error scanning folders", 500)
|
||||
json.NewEncoder(w).Encode(errors)
|
||||
sendJSON(w, errors)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiSvc) postDBPrio(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) postDBPrio(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
file := qs.Get("file")
|
||||
@@ -985,7 +954,7 @@ func (s *apiSvc) postDBPrio(w http.ResponseWriter, r *http.Request) {
|
||||
s.getDBNeed(w, r)
|
||||
}
|
||||
|
||||
func (s *apiSvc) getQR(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getQR(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var text = qs.Get("text")
|
||||
code, err := qr.Encode(text, qr.M)
|
||||
@@ -998,7 +967,7 @@ func (s *apiSvc) getQR(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(code.PNG())
|
||||
}
|
||||
|
||||
func (s *apiSvc) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
tot := map[string]float64{}
|
||||
count := map[string]float64{}
|
||||
|
||||
@@ -1019,12 +988,10 @@ func (s *apiSvc) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
comp[device] = int(tot[device] / count[device])
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(comp)
|
||||
sendJSON(w, comp)
|
||||
}
|
||||
|
||||
func (s *apiSvc) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
current := qs.Get("current")
|
||||
search, _ := osutil.ExpandTilde(current)
|
||||
@@ -1043,7 +1010,8 @@ func (s *apiSvc) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
json.NewEncoder(w).Encode(ret)
|
||||
|
||||
sendJSON(w, ret)
|
||||
}
|
||||
|
||||
type embeddedStatic struct {
|
||||
@@ -1129,7 +1097,7 @@ func (s embeddedStatic) mimeTypeForFile(file string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiSvc) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
|
||||
func (s *apiService) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
|
||||
res := make([]jsonDBFileInfo, len(fs))
|
||||
for i, f := range fs {
|
||||
res[i] = jsonDBFileInfo(f)
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
_ "net/http/pprof"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
@@ -49,7 +51,6 @@ import (
|
||||
var (
|
||||
Version = "unknown-dev"
|
||||
Codename = "Beryllium Bedbug"
|
||||
BuildEnv = "default"
|
||||
BuildStamp = "0"
|
||||
BuildDate time.Time
|
||||
BuildHost = "unknown"
|
||||
@@ -108,20 +109,14 @@ func init() {
|
||||
BuildDate = time.Unix(int64(stamp), 0)
|
||||
|
||||
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
|
||||
LongVersion = fmt.Sprintf(`syncthing %s "%s" (%s %s-%s %s) %s@%s %s`, Version, Codename, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildEnv, BuildUser, BuildHost, date)
|
||||
|
||||
if os.Getenv("STTRACE") != "" {
|
||||
logFlags = log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile
|
||||
}
|
||||
LongVersion = fmt.Sprintf(`syncthing %s "%s" (%s %s-%s) %s@%s %s`, Version, Codename, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
}
|
||||
|
||||
var (
|
||||
myID protocol.DeviceID
|
||||
confDir string
|
||||
logFlags = log.Ltime
|
||||
stop = make(chan int)
|
||||
cert tls.Certificate
|
||||
lans []*net.IPNet
|
||||
myID protocol.DeviceID
|
||||
stop = make(chan int)
|
||||
cert tls.Certificate
|
||||
lans []*net.IPNet
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -189,170 +184,188 @@ The following are valid values for the STTRACE variable:
|
||||
%s`
|
||||
)
|
||||
|
||||
// Command line and environment options
|
||||
// Environment options
|
||||
var (
|
||||
noUpgrade = os.Getenv("STNOUPGRADE") != ""
|
||||
innerProcess = os.Getenv("STNORESTART") != "" || os.Getenv("STMONITORED") != ""
|
||||
)
|
||||
|
||||
type RuntimeOptions struct {
|
||||
confDir string
|
||||
reset bool
|
||||
showVersion bool
|
||||
doUpgrade bool
|
||||
doUpgradeCheck bool
|
||||
upgradeTo string
|
||||
noBrowser bool
|
||||
noConsole bool
|
||||
browserOnly bool
|
||||
hideConsole bool
|
||||
logFile string
|
||||
auditEnabled bool
|
||||
verbose bool
|
||||
paused bool
|
||||
noRestart = os.Getenv("STNORESTART") != ""
|
||||
noUpgrade = os.Getenv("STNOUPGRADE") != ""
|
||||
profiler = os.Getenv("STPROFILER")
|
||||
guiAssets = os.Getenv("STGUIASSETS")
|
||||
cpuProfile = os.Getenv("STCPUPROFILE") != ""
|
||||
stRestarting = os.Getenv("STRESTART") != ""
|
||||
innerProcess = os.Getenv("STNORESTART") != "" || os.Getenv("STMONITORED") != ""
|
||||
)
|
||||
guiAddress string
|
||||
guiAPIKey string
|
||||
generateDir string
|
||||
noRestart bool
|
||||
profiler string
|
||||
assetDir string
|
||||
cpuProfile bool
|
||||
stRestarting bool
|
||||
logFlags int
|
||||
}
|
||||
|
||||
func main() {
|
||||
if runtime.GOOS == "windows" {
|
||||
// On Windows, we use a log file by default. Setting the -logfile flag
|
||||
// to "-" disables this behavior.
|
||||
flag.StringVar(&logFile, "logfile", "", "Log file name (use \"-\" for stdout)")
|
||||
|
||||
// We also add an option to hide the console window
|
||||
flag.BoolVar(&noConsole, "no-console", false, "Hide console window")
|
||||
} else {
|
||||
flag.StringVar(&logFile, "logfile", "-", "Log file name (use \"-\" for stdout)")
|
||||
func defaultRuntimeOptions() RuntimeOptions {
|
||||
options := RuntimeOptions{
|
||||
noRestart: os.Getenv("STNORESTART") != "",
|
||||
profiler: os.Getenv("STPROFILER"),
|
||||
assetDir: os.Getenv("STGUIASSETS"),
|
||||
cpuProfile: os.Getenv("STCPUPROFILE") != "",
|
||||
stRestarting: os.Getenv("STRESTART") != "",
|
||||
logFlags: log.Ltime,
|
||||
}
|
||||
|
||||
var guiAddress, guiAPIKey string
|
||||
var generateDir string
|
||||
flag.StringVar(&generateDir, "generate", "", "Generate key and config in specified dir, then exit")
|
||||
flag.StringVar(&guiAddress, "gui-address", guiAddress, "Override GUI address")
|
||||
flag.StringVar(&guiAPIKey, "gui-apikey", guiAPIKey, "Override GUI API key")
|
||||
flag.StringVar(&confDir, "home", "", "Set configuration directory")
|
||||
flag.IntVar(&logFlags, "logflags", logFlags, "Select information in log line prefix")
|
||||
flag.BoolVar(&noBrowser, "no-browser", false, "Do not start browser")
|
||||
flag.BoolVar(&noRestart, "no-restart", noRestart, "Do not restart; just exit")
|
||||
flag.BoolVar(&reset, "reset", false, "Reset the database")
|
||||
flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade")
|
||||
flag.BoolVar(&doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
|
||||
flag.BoolVar(&showVersion, "version", false, "Show version")
|
||||
flag.StringVar(&upgradeTo, "upgrade-to", upgradeTo, "Force upgrade directly from specified URL")
|
||||
flag.BoolVar(&auditEnabled, "audit", false, "Write events to audit file")
|
||||
flag.BoolVar(&verbose, "verbose", false, "Print verbose log output")
|
||||
flag.BoolVar(&paused, "paused", false, "Start with all devices paused")
|
||||
if os.Getenv("STTRACE") != "" {
|
||||
options.logFlags = log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
// On non-Windows, we explicitly default to "-" which means stdout. On
|
||||
// Windows, the blank options.logFile will later be replaced with the
|
||||
// default path, unless the user has manually specified "-" or
|
||||
// something else.
|
||||
options.logFile = "-"
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
func parseCommandLineOptions() RuntimeOptions {
|
||||
options := defaultRuntimeOptions()
|
||||
|
||||
flag.StringVar(&options.generateDir, "generate", "", "Generate key and config in specified dir, then exit")
|
||||
flag.StringVar(&options.guiAddress, "gui-address", options.guiAddress, "Override GUI address (e.g. \"http://192.0.2.42:8443\")")
|
||||
flag.StringVar(&options.guiAPIKey, "gui-apikey", options.guiAPIKey, "Override GUI API key")
|
||||
flag.StringVar(&options.confDir, "home", "", "Set configuration directory")
|
||||
flag.IntVar(&options.logFlags, "logflags", options.logFlags, "Select information in log line prefix (see below)")
|
||||
flag.BoolVar(&options.noBrowser, "no-browser", false, "Do not start browser")
|
||||
flag.BoolVar(&options.browserOnly, "browser-only", false, "Open GUI in browser")
|
||||
flag.BoolVar(&options.noRestart, "no-restart", options.noRestart, "Do not restart; just exit")
|
||||
flag.BoolVar(&options.reset, "reset", false, "Reset the database")
|
||||
flag.BoolVar(&options.doUpgrade, "upgrade", false, "Perform upgrade")
|
||||
flag.BoolVar(&options.doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
|
||||
flag.BoolVar(&options.showVersion, "version", false, "Show version")
|
||||
flag.StringVar(&options.upgradeTo, "upgrade-to", options.upgradeTo, "Force upgrade directly from specified URL")
|
||||
flag.BoolVar(&options.auditEnabled, "audit", false, "Write events to audit file")
|
||||
flag.BoolVar(&options.verbose, "verbose", false, "Print verbose log output")
|
||||
flag.BoolVar(&options.paused, "paused", false, "Start with all devices paused")
|
||||
flag.StringVar(&options.logFile, "logfile", options.logFile, "Log file name (use \"-\" for stdout)")
|
||||
if runtime.GOOS == "windows" {
|
||||
// Allow user to hide the console window
|
||||
flag.BoolVar(&options.hideConsole, "no-console", false, "Hide console window")
|
||||
}
|
||||
|
||||
longUsage := fmt.Sprintf(extraUsage, baseDirs["config"], debugFacilities())
|
||||
flag.Usage = usageFor(flag.CommandLine, usage, longUsage)
|
||||
flag.Parse()
|
||||
|
||||
if guiAddress != "" {
|
||||
return options
|
||||
}
|
||||
|
||||
func main() {
|
||||
options := parseCommandLineOptions()
|
||||
l.SetFlags(options.logFlags)
|
||||
|
||||
if options.guiAddress != "" {
|
||||
// The config picks this up from the environment.
|
||||
os.Setenv("STGUIADDRESS", guiAddress)
|
||||
os.Setenv("STGUIADDRESS", options.guiAddress)
|
||||
}
|
||||
if guiAPIKey != "" {
|
||||
if options.guiAPIKey != "" {
|
||||
// The config picks this up from the environment.
|
||||
os.Setenv("STGUIAPIKEY", guiAPIKey)
|
||||
os.Setenv("STGUIAPIKEY", options.guiAPIKey)
|
||||
}
|
||||
|
||||
if noConsole {
|
||||
if options.hideConsole {
|
||||
osutil.HideConsole()
|
||||
}
|
||||
|
||||
if confDir != "" {
|
||||
if options.confDir != "" {
|
||||
// Not set as default above because the string can be really long.
|
||||
baseDirs["config"] = confDir
|
||||
baseDirs["config"] = options.confDir
|
||||
}
|
||||
|
||||
if err := expandLocations(); err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
|
||||
if guiAssets == "" {
|
||||
guiAssets = locations[locGUIAssets]
|
||||
if options.logFile == "" {
|
||||
// Blank means use the default logfile location. We must set this
|
||||
// *after* expandLocations above.
|
||||
options.logFile = locations[locLogFile]
|
||||
}
|
||||
|
||||
if logFile == "" {
|
||||
// Use the default log file location
|
||||
logFile = locations[locLogFile]
|
||||
if options.assetDir == "" {
|
||||
// The asset dir is blank if STGUIASSETS wasn't set, in which case we
|
||||
// should look for extra assets in the default place.
|
||||
options.assetDir = locations[locGUIAssets]
|
||||
}
|
||||
|
||||
if showVersion {
|
||||
if options.showVersion {
|
||||
fmt.Println(LongVersion)
|
||||
return
|
||||
}
|
||||
|
||||
l.SetFlags(logFlags)
|
||||
|
||||
if generateDir != "" {
|
||||
generate(generateDir)
|
||||
if options.browserOnly {
|
||||
openGUI()
|
||||
return
|
||||
}
|
||||
|
||||
if info, err := os.Stat(baseDirs["config"]); err == nil && !info.IsDir() {
|
||||
l.Fatalln("Config directory", baseDirs["config"], "is not a directory")
|
||||
if options.generateDir != "" {
|
||||
generate(options.generateDir)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure that our home directory exists.
|
||||
ensureDir(baseDirs["config"], 0700)
|
||||
|
||||
if upgradeTo != "" {
|
||||
err := upgrade.ToURL(upgradeTo)
|
||||
if options.upgradeTo != "" {
|
||||
err := upgrade.ToURL(options.upgradeTo)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err) // exits 1
|
||||
}
|
||||
l.Okln("Upgraded from", upgradeTo)
|
||||
l.Okln("Upgraded from", options.upgradeTo)
|
||||
return
|
||||
}
|
||||
|
||||
if doUpgrade || doUpgradeCheck {
|
||||
releasesURL := "https://api.github.com/repos/syncthing/syncthing/releases?per_page=30"
|
||||
if cfg, _, err := loadConfig(locations[locConfigFile]); err == nil {
|
||||
releasesURL = cfg.Options().ReleasesURL
|
||||
}
|
||||
rel, err := upgrade.LatestRelease(releasesURL, Version)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err) // exits 1
|
||||
}
|
||||
|
||||
if upgrade.CompareVersions(rel.Tag, Version) <= 0 {
|
||||
l.Infof("No upgrade available (current %q >= latest %q).", Version, rel.Tag)
|
||||
os.Exit(exitNoUpgradeAvailable)
|
||||
}
|
||||
|
||||
l.Infof("Upgrade available (current %q < latest %q)", Version, rel.Tag)
|
||||
|
||||
if doUpgrade {
|
||||
// Use leveldb database locks to protect against concurrent upgrades
|
||||
_, err = db.Open(locations[locDatabase])
|
||||
if err != nil {
|
||||
l.Infoln("Attempting upgrade through running Syncthing...")
|
||||
err = upgradeViaRest()
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err)
|
||||
}
|
||||
l.Okln("Syncthing upgrading")
|
||||
return
|
||||
}
|
||||
|
||||
err = upgrade.To(rel)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err) // exits 1
|
||||
}
|
||||
l.Okf("Upgraded to %q", rel.Tag)
|
||||
}
|
||||
|
||||
if options.doUpgradeCheck {
|
||||
checkUpgrade()
|
||||
return
|
||||
}
|
||||
|
||||
if reset {
|
||||
if options.doUpgrade {
|
||||
release := checkUpgrade()
|
||||
performUpgrade(release)
|
||||
return
|
||||
}
|
||||
|
||||
if options.reset {
|
||||
resetDB()
|
||||
return
|
||||
}
|
||||
|
||||
if noRestart {
|
||||
syncthingMain()
|
||||
if options.noRestart {
|
||||
syncthingMain(options)
|
||||
} else {
|
||||
monitorMain()
|
||||
monitorMain(options)
|
||||
}
|
||||
}
|
||||
|
||||
func openGUI() {
|
||||
cfg, _ := loadConfig()
|
||||
if cfg.GUI().Enabled {
|
||||
openURL(cfg.GUI().URL())
|
||||
} else {
|
||||
l.Warnln("Browser: GUI is currently disabled")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,17 +374,7 @@ func generate(generateDir string) {
|
||||
if err != nil {
|
||||
l.Fatalln("generate:", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(dir)
|
||||
if err == nil && !info.IsDir() {
|
||||
l.Fatalln(dir, "is not a directory")
|
||||
}
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
err = osutil.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
l.Fatalln("generate:", err)
|
||||
}
|
||||
}
|
||||
ensureDir(dir, 0700)
|
||||
|
||||
certFile, keyFile := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
@@ -428,11 +431,46 @@ func debugFacilities() string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func upgradeViaRest() error {
|
||||
cfg, err := config.Load(locations[locConfigFile], protocol.LocalDeviceID)
|
||||
func checkUpgrade() upgrade.Release {
|
||||
cfg, _ := loadConfig()
|
||||
releasesURL := cfg.Options().ReleasesURL
|
||||
release, err := upgrade.LatestRelease(releasesURL, Version)
|
||||
if err != nil {
|
||||
return err
|
||||
l.Fatalln("Upgrade:", err)
|
||||
}
|
||||
|
||||
if upgrade.CompareVersions(release.Tag, Version) <= 0 {
|
||||
noUpgradeMessage := "No upgrade available (current %q >= latest %q)."
|
||||
l.Infof(noUpgradeMessage, Version, release.Tag)
|
||||
os.Exit(exitNoUpgradeAvailable)
|
||||
}
|
||||
|
||||
l.Infof("Upgrade available (current %q < latest %q)", Version, release.Tag)
|
||||
return release
|
||||
}
|
||||
|
||||
func performUpgrade(release upgrade.Release) {
|
||||
// Use leveldb database locks to protect against concurrent upgrades
|
||||
_, err := db.Open(locations[locDatabase])
|
||||
if err == nil {
|
||||
err = upgrade.To(release)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err)
|
||||
}
|
||||
l.Okf("Upgraded to %q", release.Tag)
|
||||
} else {
|
||||
l.Infoln("Attempting upgrade through running Syncthing...")
|
||||
err = upgradeViaRest()
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err)
|
||||
}
|
||||
l.Okln("Syncthing upgrading")
|
||||
os.Exit(exitUpgrading)
|
||||
}
|
||||
}
|
||||
|
||||
func upgradeViaRest() error {
|
||||
cfg, _ := loadConfig()
|
||||
target := cfg.GUI().URL()
|
||||
r, _ := http.NewRequest("POST", target+"/rest/system/upgrade", nil)
|
||||
r.Header.Set("X-API-Key", cfg.GUI().APIKey())
|
||||
@@ -462,26 +500,28 @@ func upgradeViaRest() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func syncthingMain() {
|
||||
func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
setupSignalHandling()
|
||||
|
||||
// Create a main service manager. We'll add things to this as we go along.
|
||||
// We want any logging it does to go through our log system.
|
||||
mainSvc := suture.New("main", suture.Spec{
|
||||
mainService := suture.New("main", suture.Spec{
|
||||
Log: func(line string) {
|
||||
l.Debugln(line)
|
||||
},
|
||||
})
|
||||
mainSvc.ServeBackground()
|
||||
mainService.ServeBackground()
|
||||
|
||||
// Set a log prefix similar to the ID we will have later on, or early log
|
||||
// lines look ugly.
|
||||
l.SetPrefix("[start] ")
|
||||
|
||||
if auditEnabled {
|
||||
startAuditing(mainSvc)
|
||||
if runtimeOptions.auditEnabled {
|
||||
startAuditing(mainService)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
mainSvc.Add(newVerboseSvc())
|
||||
if runtimeOptions.verbose {
|
||||
mainService.Add(newVerboseService())
|
||||
}
|
||||
|
||||
errors := logger.NewRecorder(l, logger.LevelWarn, maxSystemErrors, 0)
|
||||
@@ -527,44 +567,17 @@ func syncthingMain() {
|
||||
"myID": myID.String(),
|
||||
})
|
||||
|
||||
// Prepare to be able to save configuration
|
||||
|
||||
cfgFile := locations[locConfigFile]
|
||||
|
||||
// Load the configuration file, if it exists.
|
||||
// If it does not, create a template.
|
||||
|
||||
cfg, myName, err := loadConfig(cfgFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
l.Infoln("No config file; starting with empty defaults")
|
||||
myName, _ = os.Hostname()
|
||||
newCfg := defaultConfig(myName)
|
||||
cfg = config.Wrap(cfgFile, newCfg)
|
||||
cfg.Save()
|
||||
l.Infof("Edit %s to taste or use the GUI\n", cfgFile)
|
||||
} else {
|
||||
l.Fatalln("Loading config:", err)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Raw().OriginalVersion != config.CurrentVersion {
|
||||
l.Infoln("Archiving a copy of old config file format")
|
||||
// Archive a copy
|
||||
osutil.Rename(cfgFile, cfgFile+fmt.Sprintf(".v%d", cfg.Raw().OriginalVersion))
|
||||
// Save the new version
|
||||
cfg.Save()
|
||||
}
|
||||
cfg := loadOrCreateConfig()
|
||||
|
||||
if err := checkShortIDs(cfg); err != nil {
|
||||
l.Fatalln("Short device IDs are in conflict. Unlucky!\n Regenerate the device ID of one if the following:\n ", err)
|
||||
l.Fatalln("Short device IDs are in conflict. Unlucky!\n Regenerate the device ID of one of the following:\n ", err)
|
||||
}
|
||||
|
||||
if len(profiler) > 0 {
|
||||
if len(runtimeOptions.profiler) > 0 {
|
||||
go func() {
|
||||
l.Debugln("Starting profiler on", profiler)
|
||||
l.Debugln("Starting profiler on", runtimeOptions.profiler)
|
||||
runtime.SetBlockProfileRate(1)
|
||||
err := http.ListenAndServe(profiler, nil)
|
||||
err := http.ListenAndServe(runtimeOptions.profiler, nil)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
@@ -647,7 +660,7 @@ func syncthingMain() {
|
||||
l.Infoln("Compacting database:", err)
|
||||
}
|
||||
|
||||
m := model.NewModel(cfg, myID, myName, "syncthing", Version, ldb, protectedFiles)
|
||||
m := model.NewModel(cfg, myID, myDeviceName(cfg), "syncthing", Version, ldb, protectedFiles)
|
||||
cfg.Subscribe(m)
|
||||
|
||||
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
|
||||
@@ -659,7 +672,7 @@ func syncthingMain() {
|
||||
m.StartDeadlockDetector(20 * time.Minute)
|
||||
}
|
||||
|
||||
if paused {
|
||||
if runtimeOptions.paused {
|
||||
for device := range cfg.Devices() {
|
||||
m.PauseDevice(device)
|
||||
}
|
||||
@@ -685,7 +698,7 @@ func syncthingMain() {
|
||||
}
|
||||
}
|
||||
|
||||
mainSvc.Add(m)
|
||||
mainService.Add(m)
|
||||
|
||||
// The default port we announce, possibly modified by setupUPnP next.
|
||||
|
||||
@@ -706,33 +719,33 @@ func syncthingMain() {
|
||||
// Start UPnP
|
||||
|
||||
if opts.UPnPEnabled {
|
||||
upnpSvc := newUPnPSvc(cfg, addr.Port)
|
||||
mainSvc.Add(upnpSvc)
|
||||
upnpService := newUPnPService(cfg, addr.Port)
|
||||
mainService.Add(upnpService)
|
||||
|
||||
// The external address tracker needs to know about the UPnP service
|
||||
// so it can check for an external mapped port.
|
||||
addrList = newAddressLister(upnpSvc, cfg)
|
||||
addrList = newAddressLister(upnpService, cfg)
|
||||
} else {
|
||||
addrList = newAddressLister(nil, cfg)
|
||||
}
|
||||
|
||||
// Start relay management
|
||||
|
||||
var relaySvc *relay.Svc
|
||||
var relayService *relay.Service
|
||||
if opts.RelaysEnabled && (opts.GlobalAnnEnabled || opts.RelayWithoutGlobalAnn) {
|
||||
relaySvc = relay.NewSvc(cfg, tlsCfg)
|
||||
mainSvc.Add(relaySvc)
|
||||
relayService = relay.NewService(cfg, tlsCfg)
|
||||
mainService.Add(relayService)
|
||||
}
|
||||
|
||||
// Start discovery
|
||||
|
||||
cachedDiscovery := discover.NewCachingMux()
|
||||
mainSvc.Add(cachedDiscovery)
|
||||
mainService.Add(cachedDiscovery)
|
||||
|
||||
if cfg.Options().GlobalAnnEnabled {
|
||||
for _, srv := range cfg.GlobalDiscoveryServers() {
|
||||
l.Infoln("Using discovery server", srv)
|
||||
gd, err := discover.NewGlobal(srv, cert, addrList, relaySvc)
|
||||
gd, err := discover.NewGlobal(srv, cert, addrList, relayService)
|
||||
if err != nil {
|
||||
l.Warnln("Global discovery:", err)
|
||||
continue
|
||||
@@ -747,14 +760,14 @@ func syncthingMain() {
|
||||
|
||||
if cfg.Options().LocalAnnEnabled {
|
||||
// v4 broadcasts
|
||||
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), addrList, relaySvc)
|
||||
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), addrList, relayService)
|
||||
if err != nil {
|
||||
l.Warnln("IPv4 local discovery:", err)
|
||||
} else {
|
||||
cachedDiscovery.Add(bcd, 0, 0, ipv4LocalDiscoveryPriority)
|
||||
}
|
||||
// v6 multicasts
|
||||
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, addrList, relaySvc)
|
||||
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, addrList, relayService)
|
||||
if err != nil {
|
||||
l.Warnln("IPv6 local discovery:", err)
|
||||
} else {
|
||||
@@ -764,14 +777,14 @@ func syncthingMain() {
|
||||
|
||||
// GUI
|
||||
|
||||
setupGUI(mainSvc, cfg, m, apiSub, cachedDiscovery, relaySvc, errors, systemLog)
|
||||
setupGUI(mainService, cfg, m, apiSub, cachedDiscovery, relayService, errors, systemLog, runtimeOptions)
|
||||
|
||||
// Start connection management
|
||||
|
||||
connectionSvc := connections.NewConnectionSvc(cfg, myID, m, tlsCfg, cachedDiscovery, relaySvc, bepProtocolName, tlsDefaultCommonName, lans)
|
||||
mainSvc.Add(connectionSvc)
|
||||
connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, relayService, bepProtocolName, tlsDefaultCommonName, lans)
|
||||
mainService.Add(connectionService)
|
||||
|
||||
if cpuProfile {
|
||||
if runtimeOptions.cpuProfile {
|
||||
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -829,17 +842,48 @@ func syncthingMain() {
|
||||
|
||||
code := <-stop
|
||||
|
||||
mainSvc.Stop()
|
||||
mainService.Stop()
|
||||
|
||||
l.Okln("Exiting")
|
||||
|
||||
if cpuProfile {
|
||||
if runtimeOptions.cpuProfile {
|
||||
pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func myDeviceName(cfg *config.Wrapper) string {
|
||||
devices := cfg.Devices()
|
||||
myName := devices[myID].Name
|
||||
if myName == "" {
|
||||
myName, _ = os.Hostname()
|
||||
}
|
||||
return myName
|
||||
}
|
||||
|
||||
func setupSignalHandling() {
|
||||
// Exit cleanly with "restarting" code on SIGHUP.
|
||||
|
||||
restartSign := make(chan os.Signal, 1)
|
||||
sigHup := syscall.Signal(1)
|
||||
signal.Notify(restartSign, sigHup)
|
||||
go func() {
|
||||
<-restartSign
|
||||
stop <- exitRestarting
|
||||
}()
|
||||
|
||||
// Exit with "success" code (no restart) on INT/TERM
|
||||
|
||||
stopSign := make(chan os.Signal, 1)
|
||||
sigTerm := syscall.Signal(15)
|
||||
signal.Notify(stopSign, os.Interrupt, sigTerm)
|
||||
go func() {
|
||||
<-stopSign
|
||||
stop <- exitSuccess
|
||||
}()
|
||||
}
|
||||
|
||||
// printHashRate prints the hashing performance in MB/s, formatting it with
|
||||
// appropriate precision for the value, i.e. 182 MB/s, 18 MB/s, 1.8 MB/s, 0.18
|
||||
// MB/s.
|
||||
@@ -856,47 +900,71 @@ func printHashRate() {
|
||||
l.Infof("Single thread hash performance is ~%.*f MB/s", decimals, hashRate)
|
||||
}
|
||||
|
||||
func loadConfig(cfgFile string) (*config.Wrapper, string, error) {
|
||||
info, err := os.Stat(cfgFile)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, "", errors.New("configuration is not a file")
|
||||
}
|
||||
|
||||
func loadConfig() (*config.Wrapper, error) {
|
||||
cfgFile := locations[locConfigFile]
|
||||
cfg, err := config.Load(cfgFile, myID)
|
||||
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
l.Infoln("Error loading config file; using defaults for now")
|
||||
myName, _ := os.Hostname()
|
||||
newCfg := defaultConfig(myName)
|
||||
cfg = config.Wrap(cfgFile, newCfg)
|
||||
}
|
||||
|
||||
myCfg := cfg.Devices()[myID]
|
||||
myName := myCfg.Name
|
||||
if myName == "" {
|
||||
myName, _ = os.Hostname()
|
||||
}
|
||||
|
||||
return cfg, myName, nil
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func startAuditing(mainSvc *suture.Supervisor) {
|
||||
func loadOrCreateConfig() *config.Wrapper {
|
||||
cfg, err := loadConfig()
|
||||
if os.IsNotExist(err) {
|
||||
cfg.Save()
|
||||
l.Infof("Defaults saved. Edit %s to taste or use the GUI\n", cfg.ConfigPath())
|
||||
} else if err != nil {
|
||||
l.Fatalln("Config:", err)
|
||||
}
|
||||
|
||||
if cfg.Raw().OriginalVersion != config.CurrentVersion {
|
||||
err = archiveAndSaveConfig(cfg)
|
||||
if err != nil {
|
||||
l.Fatalln("Config archive:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func archiveAndSaveConfig(cfg *config.Wrapper) error {
|
||||
// To prevent previous config from being cleaned up, quickly touch it too
|
||||
now := time.Now()
|
||||
_ = os.Chtimes(cfg.ConfigPath(), now, now) // May return error on Android etc; no worries
|
||||
|
||||
archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", cfg.Raw().OriginalVersion)
|
||||
l.Infoln("Archiving a copy of old config file format at:", archivePath)
|
||||
if err := osutil.Rename(cfg.ConfigPath(), archivePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cfg.Save()
|
||||
}
|
||||
|
||||
func startAuditing(mainService *suture.Supervisor) {
|
||||
auditFile := timestampedLoc(locAuditLog)
|
||||
fd, err := os.OpenFile(auditFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
l.Fatalln("Audit:", err)
|
||||
}
|
||||
|
||||
auditSvc := newAuditSvc(fd)
|
||||
mainSvc.Add(auditSvc)
|
||||
auditService := newAuditService(fd)
|
||||
mainService.Add(auditService)
|
||||
|
||||
// We wait for the audit service to fully start before we return, to
|
||||
// ensure we capture all events from the start.
|
||||
auditSvc.WaitForStart()
|
||||
auditService.WaitForStart()
|
||||
|
||||
l.Infoln("Audit log in", auditFile)
|
||||
}
|
||||
|
||||
func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.CachingMux, relaySvc *relay.Svc, errors, systemLog *logger.Recorder) {
|
||||
func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.CachingMux, relayService *relay.Service, errors, systemLog *logger.Recorder, runtimeOptions RuntimeOptions) {
|
||||
guiCfg := cfg.GUI()
|
||||
|
||||
if !guiCfg.Enabled {
|
||||
@@ -907,14 +975,14 @@ func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, a
|
||||
l.Warnln("Insecure admin access is enabled.")
|
||||
}
|
||||
|
||||
api, err := newAPISvc(myID, cfg, guiAssets, m, apiSub, discoverer, relaySvc, errors, systemLog)
|
||||
api, err := newAPIService(myID, cfg, runtimeOptions.assetDir, m, apiSub, discoverer, relayService, errors, systemLog)
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot start GUI:", err)
|
||||
}
|
||||
cfg.Subscribe(api)
|
||||
mainSvc.Add(api)
|
||||
mainService.Add(api)
|
||||
|
||||
if cfg.Options().StartBrowser && !noBrowser && !stRestarting {
|
||||
if cfg.Options().StartBrowser && !runtimeOptions.noBrowser && !runtimeOptions.stRestarting {
|
||||
// Can potentially block if the utility we are invoking doesn't
|
||||
// fork, and just execs, hence keep it in it's own routine.
|
||||
go openURL(guiCfg.URL())
|
||||
@@ -971,18 +1039,23 @@ func shutdown() {
|
||||
stop <- exitSuccess
|
||||
}
|
||||
|
||||
func ensureDir(dir string, mode int) {
|
||||
fi, err := os.Stat(dir)
|
||||
if os.IsNotExist(err) {
|
||||
err := osutil.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
} else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
|
||||
err := os.Chmod(dir, os.FileMode(mode))
|
||||
// This can fail on crappy filesystems, nothing we can do about it.
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
func ensureDir(dir string, mode os.FileMode) {
|
||||
err := osutil.MkdirAll(dir, mode)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(dir); err == nil {
|
||||
// Apprently the stat may fail even though the mkdirall passed. If it
|
||||
// does, we'll just assume things are in order and let other things
|
||||
// fail (like loading or creating the config...).
|
||||
currentMode := fi.Mode() & 0777
|
||||
if currentMode != mode {
|
||||
err := os.Chmod(dir, mode)
|
||||
// This can fail on crappy filesystems, nothing we can do about it.
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,14 @@ const (
|
||||
logFileMaxOpenTime = time.Minute
|
||||
)
|
||||
|
||||
func monitorMain() {
|
||||
func monitorMain(runtimeOptions RuntimeOptions) {
|
||||
os.Setenv("STNORESTART", "yes")
|
||||
os.Setenv("STMONITORED", "yes")
|
||||
l.SetPrefix("[monitor] ")
|
||||
|
||||
var dst io.Writer = os.Stdout
|
||||
|
||||
logFile := runtimeOptions.logFile
|
||||
if logFile != "-" {
|
||||
var fileDst io.Writer = newAutoclosedFile(logFile, logFileAutoCloseDelay, logFileMaxOpenTime)
|
||||
|
||||
@@ -62,9 +63,12 @@ func monitorMain() {
|
||||
args := os.Args
|
||||
var restarts [countRestarts]time.Time
|
||||
|
||||
sign := make(chan os.Signal, 1)
|
||||
sigTerm := syscall.Signal(0xf)
|
||||
signal.Notify(sign, os.Interrupt, sigTerm, os.Kill)
|
||||
stopSign := make(chan os.Signal, 1)
|
||||
sigTerm := syscall.Signal(15)
|
||||
signal.Notify(stopSign, os.Interrupt, sigTerm)
|
||||
restartSign := make(chan os.Signal, 1)
|
||||
sigHup := syscall.Signal(1)
|
||||
signal.Notify(restartSign, sigHup)
|
||||
|
||||
for {
|
||||
if t := time.Since(restarts[0]); t < loopThreshold {
|
||||
@@ -124,12 +128,17 @@ func monitorMain() {
|
||||
}()
|
||||
|
||||
select {
|
||||
case s := <-sign:
|
||||
case s := <-stopSign:
|
||||
l.Infof("Signal %d received; exiting", s)
|
||||
cmd.Process.Kill()
|
||||
<-exit
|
||||
return
|
||||
|
||||
case s := <-restartSign:
|
||||
l.Infof("Signal %d received; restarting", s)
|
||||
cmd.Process.Signal(sigHup)
|
||||
err = <-exit
|
||||
|
||||
case err = <-exit:
|
||||
if err == nil {
|
||||
// Successful exit indicates an intentional shutdown
|
||||
|
||||
@@ -16,9 +16,9 @@ import (
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
// The folderSummarySvc adds summary information events (FolderSummary and
|
||||
// The folderSummaryService adds summary information events (FolderSummary and
|
||||
// FolderCompletion) into the event stream at certain intervals.
|
||||
type folderSummarySvc struct {
|
||||
type folderSummaryService struct {
|
||||
*suture.Supervisor
|
||||
|
||||
cfg *config.Wrapper
|
||||
@@ -35,9 +35,9 @@ type folderSummarySvc struct {
|
||||
lastEventReqMut sync.Mutex
|
||||
}
|
||||
|
||||
func newFolderSummarySvc(cfg *config.Wrapper, m *model.Model) *folderSummarySvc {
|
||||
svc := &folderSummarySvc{
|
||||
Supervisor: suture.NewSimple("folderSummarySvc"),
|
||||
func newFolderSummaryService(cfg *config.Wrapper, m *model.Model) *folderSummaryService {
|
||||
service := &folderSummaryService{
|
||||
Supervisor: suture.NewSimple("folderSummaryService"),
|
||||
cfg: cfg,
|
||||
model: m,
|
||||
stop: make(chan struct{}),
|
||||
@@ -47,20 +47,20 @@ func newFolderSummarySvc(cfg *config.Wrapper, m *model.Model) *folderSummarySvc
|
||||
lastEventReqMut: sync.NewMutex(),
|
||||
}
|
||||
|
||||
svc.Add(serviceFunc(svc.listenForUpdates))
|
||||
svc.Add(serviceFunc(svc.calculateSummaries))
|
||||
service.Add(serviceFunc(service.listenForUpdates))
|
||||
service.Add(serviceFunc(service.calculateSummaries))
|
||||
|
||||
return svc
|
||||
return service
|
||||
}
|
||||
|
||||
func (c *folderSummarySvc) Stop() {
|
||||
func (c *folderSummaryService) Stop() {
|
||||
c.Supervisor.Stop()
|
||||
close(c.stop)
|
||||
}
|
||||
|
||||
// listenForUpdates subscribes to the event bus and makes note of folders that
|
||||
// need their data recalculated.
|
||||
func (c *folderSummarySvc) listenForUpdates() {
|
||||
func (c *folderSummaryService) listenForUpdates() {
|
||||
sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged)
|
||||
defer events.Default.Unsubscribe(sub)
|
||||
|
||||
@@ -110,7 +110,7 @@ func (c *folderSummarySvc) listenForUpdates() {
|
||||
|
||||
// calculateSummaries periodically recalculates folder summaries and
|
||||
// completion percentage, and sends the results on the event bus.
|
||||
func (c *folderSummarySvc) calculateSummaries() {
|
||||
func (c *folderSummaryService) calculateSummaries() {
|
||||
const pumpInterval = 2 * time.Second
|
||||
pump := time.NewTimer(pumpInterval)
|
||||
|
||||
@@ -139,7 +139,7 @@ func (c *folderSummarySvc) calculateSummaries() {
|
||||
|
||||
// foldersToHandle returns the list of folders needing a summary update, and
|
||||
// clears the list.
|
||||
func (c *folderSummarySvc) foldersToHandle() []string {
|
||||
func (c *folderSummaryService) foldersToHandle() []string {
|
||||
// We only recalculate summaries if someone is listening to events
|
||||
// (a request to /rest/events has been made within the last
|
||||
// pingEventInterval).
|
||||
@@ -162,7 +162,7 @@ func (c *folderSummarySvc) foldersToHandle() []string {
|
||||
}
|
||||
|
||||
// sendSummary send the summary events for a single folder
|
||||
func (c *folderSummarySvc) sendSummary(folder string) {
|
||||
func (c *folderSummaryService) sendSummary(folder string) {
|
||||
// The folder summary contains how many bytes, files etc
|
||||
// are in the folder and how in sync we are.
|
||||
data := folderSummary(c.cfg, c.model, folder)
|
||||
@@ -192,7 +192,7 @@ func (c *folderSummarySvc) sendSummary(folder string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *folderSummarySvc) gotEventRequest() {
|
||||
func (c *folderSummaryService) gotEventRequest() {
|
||||
c.lastEventReqMut.Lock()
|
||||
c.lastEventReq = time.Now()
|
||||
c.lastEventReqMut.Unlock()
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
// The UPnP service runs a loop for discovery of IGDs (Internet Gateway
|
||||
// Devices) and setup/renewal of a port mapping.
|
||||
type upnpSvc struct {
|
||||
type upnpService struct {
|
||||
cfg *config.Wrapper
|
||||
localPort int
|
||||
extPort int
|
||||
@@ -26,15 +26,15 @@ type upnpSvc struct {
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func newUPnPSvc(cfg *config.Wrapper, localPort int) *upnpSvc {
|
||||
return &upnpSvc{
|
||||
func newUPnPService(cfg *config.Wrapper, localPort int) *upnpService {
|
||||
return &upnpService{
|
||||
cfg: cfg,
|
||||
localPort: localPort,
|
||||
extPortMut: sync.NewMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *upnpSvc) Serve() {
|
||||
func (s *upnpService) Serve() {
|
||||
foundIGD := true
|
||||
s.stop = make(chan struct{})
|
||||
|
||||
@@ -72,18 +72,18 @@ func (s *upnpSvc) Serve() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *upnpSvc) Stop() {
|
||||
func (s *upnpService) Stop() {
|
||||
close(s.stop)
|
||||
}
|
||||
|
||||
func (s *upnpSvc) ExternalPort() int {
|
||||
func (s *upnpService) ExternalPort() int {
|
||||
s.extPortMut.Lock()
|
||||
port := s.extPort
|
||||
s.extPortMut.Unlock()
|
||||
return port
|
||||
}
|
||||
|
||||
func (s *upnpSvc) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
|
||||
func (s *upnpService) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
|
||||
// Lets try all the IGDs we found and use the first one that works.
|
||||
// TODO: Use all of them, and sort out the resulting mess to the
|
||||
// discovery announcement code...
|
||||
@@ -105,7 +105,7 @@ func (s *upnpSvc) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *upnpSvc) tryIGD(igd upnp.IGD, suggestedPort int) (int, error) {
|
||||
func (s *upnpService) tryIGD(igd upnp.IGD, suggestedPort int) (int, error) {
|
||||
var err error
|
||||
leaseTime := s.cfg.Options().UPnPLeaseM * 60
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
@@ -61,9 +60,9 @@ func (m *usageReportingManager) VerifyConfiguration(from, to config.Configuratio
|
||||
func (m *usageReportingManager) CommitConfiguration(from, to config.Configuration) bool {
|
||||
if to.Options.URAccepted >= usageReportVersion && m.sup == nil {
|
||||
// Usage reporting was turned on; lets start it.
|
||||
svc := newUsageReportingService(m.cfg, m.model)
|
||||
service := newUsageReportingService(m.cfg, m.model)
|
||||
m.sup = suture.NewSimple("usageReporting")
|
||||
m.sup.Add(svc)
|
||||
m.sup.Add(service)
|
||||
m.sup.ServeBackground()
|
||||
} else if to.Options.URAccepted < usageReportVersion && m.sup != nil {
|
||||
// Usage reporting was turned off
|
||||
@@ -79,7 +78,7 @@ func (m *usageReportingManager) String() string {
|
||||
}
|
||||
|
||||
// reportData returns the data to be sent in a usage report. It's used in
|
||||
// various places, so not part of the usageReportingSvc object.
|
||||
// various places, so not part of the usageReportingManager object.
|
||||
func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
|
||||
res := make(map[string]interface{})
|
||||
res["urVersion"] = usageReportVersion
|
||||
@@ -184,8 +183,6 @@ func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
|
||||
for _, addr := range cfg.Options().GlobalAnnServers {
|
||||
if addr == "default" || addr == "default-v4" || addr == "default-v6" {
|
||||
defaultAnnounceServersDNS++
|
||||
} else if stringIn(addr, config.DefaultDiscoveryServersIP) {
|
||||
defaultAnnounceServersIP++
|
||||
} else {
|
||||
otherAnnounceServers++
|
||||
}
|
||||
@@ -221,15 +218,6 @@ func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
|
||||
return res
|
||||
}
|
||||
|
||||
func stringIn(needle string, haystack []string) bool {
|
||||
for _, s := range haystack {
|
||||
if needle == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type usageReportingService struct {
|
||||
cfg *config.Wrapper
|
||||
model *model.Model
|
||||
@@ -249,19 +237,14 @@ func (s *usageReportingService) sendUsageReport() error {
|
||||
var b bytes.Buffer
|
||||
json.NewEncoder(&b).Encode(d)
|
||||
|
||||
transp := &http.Transport{}
|
||||
client := &http.Client{Transport: transp}
|
||||
if BuildEnv == "android" {
|
||||
// This works around the lack of DNS resolution on Android... :(
|
||||
transp.Dial = func(network, addr string) (net.Conn, error) {
|
||||
return dialer.Dial(network, "194.126.249.13:443")
|
||||
}
|
||||
}
|
||||
|
||||
if s.cfg.Options().URPostInsecurely {
|
||||
transp.TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Dial: dialer.Dial,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: s.cfg.Options().URPostInsecurely,
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := client.Post(s.cfg.Options().URURL, "application/json", &b)
|
||||
return err
|
||||
|
||||
@@ -15,25 +15,31 @@ import (
|
||||
|
||||
// The verbose logging service subscribes to events and prints these in
|
||||
// verbose format to the console using INFO level.
|
||||
type verboseSvc struct {
|
||||
type verboseService struct {
|
||||
stop chan struct{} // signals time to stop
|
||||
started chan struct{} // signals startup complete
|
||||
}
|
||||
|
||||
func newVerboseSvc() *verboseSvc {
|
||||
return &verboseSvc{
|
||||
func newVerboseService() *verboseService {
|
||||
return &verboseService{
|
||||
stop: make(chan struct{}),
|
||||
started: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Serve runs the verbose logging service.
|
||||
func (s *verboseSvc) Serve() {
|
||||
func (s *verboseService) Serve() {
|
||||
sub := events.Default.Subscribe(events.AllEvents)
|
||||
defer events.Default.Unsubscribe(sub)
|
||||
|
||||
// We're ready to start processing events.
|
||||
close(s.started)
|
||||
select {
|
||||
case <-s.started:
|
||||
// The started channel has already been closed; do nothing.
|
||||
default:
|
||||
// This is the first time around. Indicate that we're ready to start
|
||||
// processing events.
|
||||
close(s.started)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -49,17 +55,17 @@ func (s *verboseSvc) Serve() {
|
||||
}
|
||||
|
||||
// Stop stops the verbose logging service.
|
||||
func (s *verboseSvc) Stop() {
|
||||
func (s *verboseService) Stop() {
|
||||
close(s.stop)
|
||||
}
|
||||
|
||||
// WaitForStart returns once the verbose logging service is ready to receive
|
||||
// events, or immediately if it's already running.
|
||||
func (s *verboseSvc) WaitForStart() {
|
||||
func (s *verboseService) WaitForStart() {
|
||||
<-s.started
|
||||
}
|
||||
|
||||
func (s *verboseSvc) formatEvent(ev events.Event) string {
|
||||
func (s *verboseService) formatEvent(ev events.Event) string {
|
||||
switch ev.Type {
|
||||
case events.Ping, events.DownloadProgress, events.LocalIndexUpdated:
|
||||
// Skip
|
||||
@@ -96,7 +102,7 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
|
||||
return fmt.Sprintf("Rejected unshared folder %q from device %v", data["folder"], data["device"])
|
||||
|
||||
case events.ItemStarted:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
data := ev.Data.(map[string]string)
|
||||
return fmt.Sprintf("Started syncing %q / %q (%v %v)", data["folder"], data["item"], data["action"], data["type"])
|
||||
case events.ItemFinished:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
@@ -6,10 +6,9 @@ Wants=syncthing-inotify@.service
|
||||
|
||||
[Service]
|
||||
User=%i
|
||||
Environment=STNORESTART=yes
|
||||
ExecStart=/usr/bin/syncthing -no-browser -logflags=0
|
||||
ExecStart=/usr/bin/syncthing -no-browser -no-restart -logflags=0
|
||||
Restart=on-failure
|
||||
SuccessExitStatus=2 3 4
|
||||
SuccessExitStatus=3 4
|
||||
RestartForceExitStatus=3 4
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -5,10 +5,9 @@ After=network.target
|
||||
Wants=syncthing-inotify.service
|
||||
|
||||
[Service]
|
||||
Environment=STNORESTART=yes
|
||||
ExecStart=/usr/bin/syncthing -no-browser -logflags=0
|
||||
ExecStart=/usr/bin/syncthing -no-browser -no-restart -logflags=0
|
||||
Restart=on-failure
|
||||
SuccessExitStatus=2 3 4
|
||||
SuccessExitStatus=3 4
|
||||
RestartForceExitStatus=3 4
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -245,7 +245,7 @@ ul.three-columns li, ul.two-columns li {
|
||||
|
||||
/** Footer nav on small devices **/
|
||||
|
||||
@media (max-width: 991px) {
|
||||
@media (max-width: 1199px) {
|
||||
body {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
BIN
gui/assets/img/safari-pinned-tab.svg
Normal file
BIN
gui/assets/img/safari-pinned-tab.svg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
|
||||
"A negative number of days doesn't make sense.": "Un nombre negatiu de dies no té sentit.",
|
||||
"A new major version may not be compatible with previous versions.": "Una nova versió major pot ser incompatible amb versions anteriors.",
|
||||
"API Key": "Clau API",
|
||||
"About": "Sobre",
|
||||
@@ -10,8 +10,8 @@
|
||||
"Add new folder?": "Afegir nova carpeta?",
|
||||
"Address": "Adreça",
|
||||
"Addresses": "Adreces",
|
||||
"Advanced": "Advanced",
|
||||
"Advanced Configuration": "Advanced Configuration",
|
||||
"Advanced": "Avançat",
|
||||
"Advanced Configuration": "Configuració Avançada",
|
||||
"All Data": "Totes les dades",
|
||||
"Allow Anonymous Usage Reporting?": "Permetre l'enviament anònim d'informes d'ús?",
|
||||
"Alphabetic": "Alfabètic",
|
||||
@@ -19,11 +19,11 @@
|
||||
"Anonymous Usage Reporting": "Informe anònim d'ús",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Qualsevol dispositiu configurat en un dispositiu introductor també s'afegirà a aquest dispositiu.",
|
||||
"Automatic upgrades": "Actualitzacions automàtiques",
|
||||
"Be careful!": "Be careful!",
|
||||
"Be careful!": "Ves amb compte!",
|
||||
"Bugs": "Bugs",
|
||||
"CPU Utilization": "Utilització del CPU",
|
||||
"Changelog": "Historial de canvis",
|
||||
"Clean out after": "Clean out after",
|
||||
"Clean out after": "Netejar després",
|
||||
"Close": "Tancar",
|
||||
"Command": "Comando",
|
||||
"Comment, when used at the start of a line": "Comentari quan és usat al principi d'una línia",
|
||||
@@ -32,16 +32,16 @@
|
||||
"Copied from elsewhere": "Copiat d'un altre lloc",
|
||||
"Copied from original": "Copiat de l'original",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 els següents col·laboradors:",
|
||||
"Danger!": "Danger!",
|
||||
"Danger!": "Perill!",
|
||||
"Delete": "Esborrar",
|
||||
"Deleted": "Deleted",
|
||||
"Deleted": "Esborrat",
|
||||
"Device ID": "ID del dispositiu",
|
||||
"Device Identification": "Identificació del dispositiu",
|
||||
"Device Name": "Nom del dispositiu",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositiu {{device}} ({{address}}) vol conectar-se. Afegir nou dispositiu?",
|
||||
"Devices": "Dispositius",
|
||||
"Disconnected": "Desconnectat",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "Descobriment",
|
||||
"Documentation": "Documentació",
|
||||
"Download Rate": "Tasca de descarrega",
|
||||
"Downloaded": "Descarregat",
|
||||
@@ -51,18 +51,18 @@
|
||||
"Edit Folder": "Modificar carpeta",
|
||||
"Editing": "Modificant",
|
||||
"Enable UPnP": "Habilitat UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introdueix adreces separades per comes (\"tcp://ip:port\", \"tcp://host:port\") o \"dinàmic\" per realitzar descobriments automàtics de l'adreça.",
|
||||
"Enter ignore patterns, one per line.": "Introduex patrons a ignorar, un per línia.",
|
||||
"Error": "Error",
|
||||
"External File Versioning": "Versionat de fitxers extern",
|
||||
"Failed Items": "Failed Items",
|
||||
"Failed Items": "Elements fallats",
|
||||
"File Pull Order": "Ordre d'agafar fitxers",
|
||||
"File Versioning": "Versionat de Fitxers",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Els bits de permisos dels fitxers son ignorats quan es cerquen canvis. Utilitzar en sistemes de fitxers FAT.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Els fitxers són moguts a la carpeta .stversions quan són reemplaçats o esborrats per Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Els fitxers es mouen amb l'estampat de la data a la carpeta .stversions quan son substituïts o esborrats per syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Els fitxers estan protegits de canvis fets per altres dispositius, però els canvis fets en aquest dispositiu seran enviats a la resta del cluster.",
|
||||
"Folder": "Folder",
|
||||
"Folder": "Carpeta",
|
||||
"Folder ID": "ID de carpeta",
|
||||
"Folder Master": "Carpeta mestre",
|
||||
"Folder Path": "Camí de carpeta",
|
||||
@@ -76,12 +76,12 @@
|
||||
"Global Discovery Server": "Servidor de Descobriment Global",
|
||||
"Global State": "Estat global",
|
||||
"Help": "Ajuda",
|
||||
"Home page": "Home page",
|
||||
"Home page": "Pàgina d'inici",
|
||||
"Ignore": "Ignorar",
|
||||
"Ignore Patterns": "Patrons d'ignoració",
|
||||
"Ignore Permissions": "Ignora Permisos",
|
||||
"Incoming Rate Limit (KiB/s)": "Tasca Límit d'Entrada (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configuració incorrecta pot malmetre els continguts de la teva carpeta i que Syncthing esdevingui inoperatiu.",
|
||||
"Introducer": "Introductor",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversió del patrò introduït",
|
||||
"Keep Versions": "Mantenir Versions",
|
||||
@@ -91,11 +91,11 @@
|
||||
"Later": "Després",
|
||||
"Local Discovery": "Descobriment Local",
|
||||
"Local State": "Estat local",
|
||||
"Local State (Total)": "Local State (Total)",
|
||||
"Local State (Total)": "Estat local (Total)",
|
||||
"Major Upgrade": "Actualització major",
|
||||
"Maximum Age": "Antiguitat Màxima",
|
||||
"Metadata Only": "Només metadades",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "Espai de disc lliure mínim",
|
||||
"Move to top of queue": "Moure al primer de la cua",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Caràcter comodí de nivell múltiple (aparella en carpetes de nivells múltiples)",
|
||||
"Never": "Mai",
|
||||
@@ -108,37 +108,37 @@
|
||||
"OK": "OK",
|
||||
"Off": "Desactivar",
|
||||
"Oldest First": "Més antic primer",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Out of Sync",
|
||||
"Options": "Opcions",
|
||||
"Out of Sync": "Fora de sincronia",
|
||||
"Out of Sync Items": "Arxius encara no sincronitzats",
|
||||
"Outgoing Rate Limit (KiB/s)": "Tasca Límit de Sortida (KiB/s)",
|
||||
"Override Changes": "Sobreescriure Canvis",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta de la carpeta a l'equip local. Si no existeix serà creada. El caràcter (~) es pot fer servir com a drecera de",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta on les versions s'haurien de guardar (deixa-ho buit per fer servir el directori .stversions per defecte a la carpeta)",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Pause": "Pausa",
|
||||
"Paused": "Pausat",
|
||||
"Please consult the release notes before performing a major upgrade.": "Si us plau consulta les notes de llançament abans de realitzar una actualització major.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Si us plau, estableix un usuari i contrasenya al GUI a través del quadre de diàleg de configuració.",
|
||||
"Please wait": "Si-us-plau espera",
|
||||
"Preview": "Vista prèvia",
|
||||
"Preview Usage Report": "Vista Prèvia de l'Informe d'Ús",
|
||||
"Quick guide to supported patterns": "Guia ràpida per als possibles patrons",
|
||||
"RAM Utilization": "Utilització de la RAM",
|
||||
"Random": "Aleatori",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Relayed via": "Retransmés a través",
|
||||
"Relays": "Repetidors",
|
||||
"Release Notes": "Notes de llançament",
|
||||
"Remove": "Remove",
|
||||
"Remove": "Esborrar",
|
||||
"Rescan": "Re-escanejar",
|
||||
"Rescan All": "Re-escanejar tot",
|
||||
"Rescan Interval": "Interval de re-escaneig",
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "És Necessari Reiniciar",
|
||||
"Restarting": "Reiniciant",
|
||||
"Resume": "Resume",
|
||||
"Resume": "Reprendre",
|
||||
"Reused": "Reutilitzat",
|
||||
"Save": "Guardar",
|
||||
"Scan Time Remaining": "Scan Time Remaining",
|
||||
"Scan Time Remaining": "Temps d'escanejat restant",
|
||||
"Scanning": "Escanejant",
|
||||
"Select the devices to share this folder with.": "Selecciona els dispositius en els quals compartir aquesta carpeta.",
|
||||
"Select the folders to share with this device.": "Selecciona la carpeta per a compartir en aquest dispositiu.",
|
||||
@@ -161,7 +161,7 @@
|
||||
"Source Code": "Codi Font",
|
||||
"Staggered File Versioning": "Versionat de Fitxers Esglaonat",
|
||||
"Start Browser": "Arrancar Navegador",
|
||||
"Statistics": "Statistics",
|
||||
"Statistics": "Estadístiques",
|
||||
"Stopped": "Aturat",
|
||||
"Support": "Suport",
|
||||
"Sync Protocol Listen Addresses": "Adreça d'escolta del Protocol Sync",
|
||||
@@ -172,7 +172,7 @@
|
||||
"Syncthing is upgrading.": "Actualitzant syncthing.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Synthing sembla parat, o hi ha algun problema amb la connexió a Internet. Reintentant...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Sembla ser que Syncthing està tinguent problemes per processar la teva petició. Si us plau, refresca la pàgina o reinicia Syncthing si el problema persisteix.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "La interfície d'administració de Syncthing està configurada per permetre l'accés remot sense contrasenya.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan públicament disponibles a {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració s'ha guardar però no s'ha activat. S'ha de reiniciar el synthing per activar la nova configuració.",
|
||||
"The device ID cannot be blank.": "El ID del dispositiu no pot estar en blanc.",
|
||||
@@ -185,26 +185,26 @@
|
||||
"The folder ID must be unique.": "El ID de la carpeta ha de ser únic.",
|
||||
"The folder path cannot be blank.": "El camí a la carpeta no pot estar en blanc.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Es fan servir els següents intervals: per la primera hora es manté una versió cada 30 segons, pel primer dia es manté una versió cada hora, pel primer cada 30 dies es manté una versió cada dia, fins el màxim d'antiguitat es manté una versió cada setmana.",
|
||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
||||
"The following items could not be synchronized.": "Els següents elements no es poden sincronitzar.",
|
||||
"The maximum age must be a number and cannot be blank.": "La màxima antiguitat ha de ser un número i no pot estar en blanc.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Temps màxim en mantenir una versió (en dies, si es deixa en 0 es mantenen les versions per sempre).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "El percentatge d'espai de disc lliure mínim ha de ser un nombre positiu entre 0 i 100 (inclosos).",
|
||||
"The number of days must be a number and cannot be blank.": "El nombre de dies ha de ser un número i no pot estar en blanc.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "El nombre de dies per guardar els fitxers a la paperera. Zero significa per sempre.",
|
||||
"The number of old versions to keep, per file.": "El nombre de versions antigues que es mantenen per fitxer.",
|
||||
"The number of versions must be a number and cannot be blank.": "El nombre de versions ha de ser un número i no es pot deixar en blanc.",
|
||||
"The path cannot be blank.": "El camí no pot estar en blanc.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "El límit de velocitat ha de ser un nombre positiu (0: sense límit)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "El interval de re-escaneig ha der ser un nombre positiu de segons.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Són reintentats automàticament i seran sincronitzats quan l'error estigui resolt.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Això pot donar facilment accés a hackers per llegir i canviar qualsevol fitxer del teu ordinador.",
|
||||
"This is a major version upgrade.": "Aquesta és una actualització de versió major.",
|
||||
"Trash Can File Versioning": "Trash Can File Versioning",
|
||||
"Trash Can File Versioning": "Paperera de versionat de fitxers",
|
||||
"Unknown": "Desconegut",
|
||||
"Unshared": "No compartit",
|
||||
"Unused": "No usat",
|
||||
"Up to Date": "Actualitzat",
|
||||
"Updated": "Updated",
|
||||
"Updated": "Actualitzat",
|
||||
"Upgrade": "Actualització",
|
||||
"Upgrade To {%version%}": "Actualitzar a {{version}}",
|
||||
"Upgrading": "Actualitzant",
|
||||
@@ -218,7 +218,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quan s'afegeix una nova carpeta recorda que el ID d'aquesta s'utilitza per lligar repositoris entre els dispositius. Es distingeix entre majúscules i minúscules i ha de ser exactament iguals entre tots els dispositius.",
|
||||
"Yes": "Si",
|
||||
"You must keep at least one version.": "Has de mantenir com a mínim una versió.",
|
||||
"days": "days",
|
||||
"days": "dies",
|
||||
"full documentation": "documentació sencera",
|
||||
"items": "Elements",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartir la carpeta \"{{folder}}\"."
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"Add new folder?": "Afegir nova carpeta?",
|
||||
"Address": "Direcció",
|
||||
"Addresses": "Direccions",
|
||||
"Advanced": "Advanced",
|
||||
"Advanced Configuration": "Advanced Configuration",
|
||||
"Advanced": "Avançat",
|
||||
"Advanced Configuration": "Configuració avançada",
|
||||
"All Data": "Totes les dades",
|
||||
"Allow Anonymous Usage Reporting?": "Permetre informes d'ús anònim?",
|
||||
"Alphabetic": "Alfabètic",
|
||||
@@ -19,7 +19,7 @@
|
||||
"Anonymous Usage Reporting": "Informe d'ús anònim",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Tots els dispositius configurats en un dispositiu presentador seràn afegits també a aquest dispositiu.",
|
||||
"Automatic upgrades": "Actualitzacions automàtiques",
|
||||
"Be careful!": "Be careful!",
|
||||
"Be careful!": "Tin precaució!",
|
||||
"Bugs": "Errors (Bugs)",
|
||||
"CPU Utilization": "Utilització de la CPU",
|
||||
"Changelog": "Registre de canvis",
|
||||
@@ -32,7 +32,7 @@
|
||||
"Copied from elsewhere": "Copiat de qualsevol lloc",
|
||||
"Copied from original": "Copiat de l'original",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 els següents Col·laboradors:",
|
||||
"Danger!": "Danger!",
|
||||
"Danger!": "Perill!",
|
||||
"Delete": "Esborrar",
|
||||
"Deleted": "Esborrat",
|
||||
"Device ID": "ID del dispositiu",
|
||||
@@ -41,7 +41,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositiu {{device}} ({{address}}) vol connectar-se. Afegir nou dispositiu?",
|
||||
"Devices": "Dispositius",
|
||||
"Disconnected": "Desconnectat",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "Descobriment",
|
||||
"Documentation": "Documentació",
|
||||
"Download Rate": "Velocitat de descàrrega",
|
||||
"Downloaded": "Descarregat",
|
||||
@@ -51,23 +51,23 @@
|
||||
"Edit Folder": "Editar carpeta",
|
||||
"Editing": "Editant",
|
||||
"Enable UPnP": "Activar UPnp",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introdueix adreces separades per coma (\"tcp://ip:port\", \"tcp://host:port\") o \"dynamic\" per a realitzar el descobriment automàtic de l'adreça.",
|
||||
"Enter ignore patterns, one per line.": "Introduïr patrons a ignorar, un per línia.",
|
||||
"Error": "Error",
|
||||
"External File Versioning": "Versionat extern de fitxers",
|
||||
"Failed Items": "Failed Items",
|
||||
"Failed Items": "Objectes fallits",
|
||||
"File Pull Order": "Ordre de fitxers del pull",
|
||||
"File Versioning": "Versionat de fitxer",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Els bits de permís del fitxer són ignorats quant es busquen els canvis. Utilitzar en sistemes de fitxers FAT.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Els arxius es menejen a la carpeta .stversions quant són substituïts o esborrats per Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Els fitxers són canviats a versions amb indicació de data en una carpeta \".stversions\" quant són reemplaçats o esborrats per Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Els fitxers són protegits dels canvis fets en altres dispositius, però els canvis fets en aquest dispositiu seràn enviats a la resta del grup (cluster).",
|
||||
"Folder": "Folder",
|
||||
"Folder": "Carpeta",
|
||||
"Folder ID": "ID de carpeta",
|
||||
"Folder Master": "Carpeta principal",
|
||||
"Folder Path": "Ruta de la carpeta",
|
||||
"Folders": "Carpetes",
|
||||
"GUI": "GUI",
|
||||
"GUI": "IGU (Interfície Gràfica d'Usuari)",
|
||||
"GUI Authentication Password": "Password d'autenticació de l'Interfície Gràfica d'Usuari (GUI)",
|
||||
"GUI Authentication User": "Autenticació de l'usuari de l'Interfície Gràfica d'Usuari (GUI)",
|
||||
"GUI Listen Addresses": "Direcció d'escolta de l'Interfície Gràfica d'Usuari (GUI)",
|
||||
@@ -76,12 +76,12 @@
|
||||
"Global Discovery Server": "Servidor de descobriment global",
|
||||
"Global State": "Estat global",
|
||||
"Help": "Ajuda",
|
||||
"Home page": "Home page",
|
||||
"Home page": "Pàgina inicial",
|
||||
"Ignore": "Ignorar",
|
||||
"Ignore Patterns": "Patrons a ignorar",
|
||||
"Ignore Permissions": "Permisos a ignorar",
|
||||
"Incoming Rate Limit (KiB/s)": "Límit de descàrrega (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "La configuración incorrecta pot danyar el contingut de la teua carpeta i deixar Syncthing inoperatiu.",
|
||||
"Introducer": "Presentador",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversió de la condició donada (per exemple no excloure)",
|
||||
"Keep Versions": "Mantindre versions",
|
||||
@@ -95,7 +95,7 @@
|
||||
"Major Upgrade": "Actualització important",
|
||||
"Maximum Age": "Edat màxima",
|
||||
"Metadata Only": "Sols metadades",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "Espai minim de disc lliure",
|
||||
"Move to top of queue": "Moure al principi de la cua",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Comodí multinivell (coincideix amb múltiples nivells de directoris)",
|
||||
"Never": "Mai",
|
||||
@@ -108,37 +108,37 @@
|
||||
"OK": "OK",
|
||||
"Off": "Off",
|
||||
"Oldest First": "El més vell primer",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Out of Sync",
|
||||
"Options": "Opcions",
|
||||
"Out of Sync": "Sense sincronització",
|
||||
"Out of Sync Items": "Dispositius sense sincronitzar",
|
||||
"Outgoing Rate Limit (KiB/s)": "Límit de pujada (KiB/s)",
|
||||
"Override Changes": "Sobreescriure els canvis",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta a la carpeta local en l'ordinador. Es crearà si no existeix. El caràcter tilde (~) es pot utilitzar com a drecera",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta on les versions deurien estar emmagatzemades (deixar buit per a la carpeta .stversions en la carpeta).",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Pause": "Pausa",
|
||||
"Paused": "Pausat",
|
||||
"Please consult the release notes before performing a major upgrade.": "Per favor, consultar les notes de la versió abans de fer una actualització important.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Per favor, estableix un usuari i password per a l'Interfície Gràfica d'Usuari en el menú d'Adjustos.",
|
||||
"Please wait": "Per favor, espere",
|
||||
"Preview": "Vista prèvia",
|
||||
"Preview Usage Report": "Informe d'ús de vista prèvia",
|
||||
"Quick guide to supported patterns": "Guía ràpida de patrons suportats",
|
||||
"RAM Utilization": "Utilització de la RAM",
|
||||
"Random": "Aleatori",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Relayed via": "Transmitit via",
|
||||
"Relays": "Transmissions",
|
||||
"Release Notes": "Notes de la versió",
|
||||
"Remove": "Remove",
|
||||
"Remove": "Eliminar",
|
||||
"Rescan": "Tornar a buscar",
|
||||
"Rescan All": "Tornar a buscar tot",
|
||||
"Rescan Interval": "Interval de nova busca",
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "Reinici necesari",
|
||||
"Restarting": "Reiniciant",
|
||||
"Resume": "Resume",
|
||||
"Resume": "Continuar",
|
||||
"Reused": "Reutilitzat",
|
||||
"Save": "Gravar",
|
||||
"Scan Time Remaining": "Scan Time Remaining",
|
||||
"Scan Time Remaining": "Temps d'escaneig restant",
|
||||
"Scanning": "Rastrejant",
|
||||
"Select the devices to share this folder with.": "Selecciona els dispositius amb els que compartir aquesta carpeta.",
|
||||
"Select the folders to share with this device.": "Selecciona les carpetes per a compartir amb aquest dispositiu.",
|
||||
@@ -161,7 +161,7 @@
|
||||
"Source Code": "Codi font",
|
||||
"Staggered File Versioning": "Versionat de fitxers escalonat",
|
||||
"Start Browser": "Iniciar navegador",
|
||||
"Statistics": "Statistics",
|
||||
"Statistics": "Estadístiques",
|
||||
"Stopped": "Parat",
|
||||
"Support": "Suport",
|
||||
"Sync Protocol Listen Addresses": "Direccions d'escolta del protocol de sincronització",
|
||||
@@ -172,7 +172,7 @@
|
||||
"Syncthing is upgrading.": "Syncthing està actualitzant-se.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing pareix apagat o hi ha un problema amb la connexió a Internet. Tornant a intentar...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing pareix que té un problema processant la seua sol·licitud. Per favor, refresque la pàgina o reinicie Syncthing si el problema persistix.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfície d'administració de Syncthing està configurat per a permetre l'accés remot sense una contrasenya.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan disponibles públicament en {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració ha sigut gravada però no activada. Syncthing deu reiniciar per tal d'activar la nova configuració.",
|
||||
"The device ID cannot be blank.": "L'ID del dispositiu no pot estar buida.",
|
||||
@@ -185,19 +185,19 @@
|
||||
"The folder ID must be unique.": "L'ID de la carpeta deu ser única.",
|
||||
"The folder path cannot be blank.": "La ruta de la carpeta no pot estar buida.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "S'utilitzen els següents intervals: per a la primera hora es guarda una versió cada 30 segons, per al primer dia es guarda una versió cada hora, per als primers 30 dies es guarda una versió diaria, fins l'edat màxima es guarda una versió cada setmana.",
|
||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
||||
"The following items could not be synchronized.": "Els següents objectes no s'han pogut sincronitzar.",
|
||||
"The maximum age must be a number and cannot be blank.": "L'edat màxima deu ser un nombre i no pot estar buida.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El temps màxim per a guardar una versió (en dies, ficar 0 per a guardar les versions per a sempre).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "El porcentatge d'espai mínim lliure en el disc deu ser un nombre no negatiu entre 0 i 100 (ambdós inclosos).",
|
||||
"The number of days must be a number and cannot be blank.": "El nombre de dies deu ser un nombre i no pot estar en blanc.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "El nombre de dies per a mantindre els arxius a la paperera. Cero vol dir \"per a sempre\".",
|
||||
"The number of old versions to keep, per file.": "El nombre de versions antigues per a guardar, per cada fitxer.",
|
||||
"The number of versions must be a number and cannot be blank.": "El nombre de versions deu ser un nombre i no pot estar buit.",
|
||||
"The path cannot be blank.": "La ruta no pot estar buida.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "El llímit del ritme deu ser un nombre no negatiu (0: sense llímit)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "L'interval de reescaneig deu ser un nombre positiu de segons.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Es reintenta automàticament i es sincronitzaràn quant el resolga l'error.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Açò pot donar accés fàcilment als hackers per a llegir i canviar qualsevol fitxer al teu ordinador.",
|
||||
"This is a major version upgrade.": "Aquesta és una actualització important de la versió.",
|
||||
"Trash Can File Versioning": "Versionat d'arxius de la paperera",
|
||||
"Unknown": "Desconegut",
|
||||
@@ -218,7 +218,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quant s'afig una nova carpeta, hi ha que tindre en compte que l'ID de la carpeta s'utilitza per a juntar les carpetes entre dispositius. Són sensibles a les majúscules i deuen coincidir exactament entre tots els dispositius.",
|
||||
"Yes": "Sí",
|
||||
"You must keep at least one version.": "Es deu mantindre al menys una versió.",
|
||||
"days": "days",
|
||||
"days": "dies",
|
||||
"full documentation": "Documentació completa",
|
||||
"items": "Elements",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartit la carpeta \"{{folder}}\"."
|
||||
|
||||
225
gui/assets/lang/lang-da.json
Normal file
225
gui/assets/lang/lang-da.json
Normal file
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"A negative number of days doesn't make sense.": "Et negativt antal dage giver ikke mening.",
|
||||
"A new major version may not be compatible with previous versions.": "En ny versionsudgivelse er måske ikke kompatibel med tidligere versioner.",
|
||||
"API Key": "API-nøgle",
|
||||
"About": "Om",
|
||||
"Actions": "Handlinger.",
|
||||
"Add": "Tilføj",
|
||||
"Add Device": "Tilføj enhed",
|
||||
"Add Folder": "Tilføj mappe",
|
||||
"Add new folder?": "Tilføj ny mappe",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresser",
|
||||
"Advanced": "Avanceret",
|
||||
"Advanced Configuration": "Avanceret konfiguration",
|
||||
"All Data": "Alt data",
|
||||
"Allow Anonymous Usage Reporting?": "Tillad anonym brugerstatistik?",
|
||||
"Alphabetic": "Alfabetisk",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": " ",
|
||||
"Anonymous Usage Reporting": "Anonym brugerstatistik",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Alle enheder som er konfigueret som en introducerende enhed, vil også blive tilføjet til denne enhed.",
|
||||
"Automatic upgrades": "Automatisk opdatering",
|
||||
"Be careful!": "Vær forsigtig!",
|
||||
"Bugs": "Fejl",
|
||||
"CPU Utilization": "CPU-forbrug",
|
||||
"Changelog": "Udgivelsesnoter",
|
||||
"Clean out after": "Rens efter",
|
||||
"Close": "Luk",
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentering som bruges i starten af en linje",
|
||||
"Compression": "Anvend komprimering",
|
||||
"Connection Error": "Tilslutnings fejl",
|
||||
"Copied from elsewhere": "Kopieret fra et andet sted",
|
||||
"Copied from original": "Kopieret fra originalen",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 alle bidragsydere:",
|
||||
"Danger!": "Fare!",
|
||||
"Delete": "Slet",
|
||||
"Deleted": "Slettet",
|
||||
"Device ID": "Enheds-ID",
|
||||
"Device Identification": "Enhedsidentifikation",
|
||||
"Device Name": "Enhedsnavn",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enheden {{device}} ({{address}}) ønsker at forbinde. Tilføj denne enhed?",
|
||||
"Devices": "Enheder",
|
||||
"Disconnected": "Ikke tilsluttet",
|
||||
"Discovery": "Opslag",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Downloadhastighed",
|
||||
"Downloaded": "Downloadet",
|
||||
"Downloading": "Downloader",
|
||||
"Edit": "Rediger",
|
||||
"Edit Device": "Rediger enhed",
|
||||
"Edit Folder": "Rediger mappe",
|
||||
"Editing": "Redigerer",
|
||||
"Enable UPnP": "Anvend UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv kommaseparerede adresser (\"tcp://ip:port\", \"tcp://host:port\") eller \"dynamic\" for at benytte automatisk opdagelse af adressen.",
|
||||
"Enter ignore patterns, one per line.": "Vælg ignorer maske, én per linje.",
|
||||
"Error": "Fejl",
|
||||
"External File Versioning": "Ekstern fil-versionskontrol",
|
||||
"Failed Items": "Mislykkede filer",
|
||||
"File Pull Order": "Filhentnings rækkefølge",
|
||||
"File Versioning": "Filversionering",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Filtilladelses bits ignoreres når der søges efter ændringer. Bruges på FAT filsystemer. ",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer flyttes til .stversions mappen når de erstattes eller slettes af Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttes til data-stemplede versioner i en .stversions mappe når de erstattes eller slettes af Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskyttet fra ændringer foretaget på andre enheder, men ændringerne på denne enhed vil blive sendt til alle andre tilknyttede enheder.",
|
||||
"Folder": "Mappe",
|
||||
"Folder ID": "Mappe-ID",
|
||||
"Folder Master": "Mastermappe",
|
||||
"Folder Path": "Mappesti",
|
||||
"Folders": "Mapper",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "GUI-kodeord",
|
||||
"GUI Authentication User": "GUI-brugernavn",
|
||||
"GUI Listen Addresses": "GUI-lytteadresse",
|
||||
"Generate": "Opret",
|
||||
"Global Discovery": "Globalt opslag",
|
||||
"Global Discovery Server": "Global opslagsserver",
|
||||
"Global State": "Global tilstand",
|
||||
"Help": "Hjælp",
|
||||
"Home page": "Hjem",
|
||||
"Ignore": "Ignorer",
|
||||
"Ignore Patterns": "Ignoreringsmaske",
|
||||
"Ignore Permissions": "Ignorér filrettigheder",
|
||||
"Incoming Rate Limit (KiB/s)": "Indgående hastighedsbegrænsning (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Ukorrekt opsætning kan skade dine data og gøre Syncthing ude af stand til at fungere.",
|
||||
"Introducer": "Introducerende enhed",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Det omvendte (dvs. undlad ikke)",
|
||||
"Keep Versions": "Behold versioner",
|
||||
"Largest First": "Største først",
|
||||
"Last File Received": "Sidste modtaget fil",
|
||||
"Last seen": "Sidst set",
|
||||
"Later": "Senere",
|
||||
"Local Discovery": "Lokal opslag",
|
||||
"Local State": "Lokal tilstand",
|
||||
"Local State (Total)": "Lokal tilstand (total)",
|
||||
"Major Upgrade": "Ny version",
|
||||
"Maximum Age": "Maks alder",
|
||||
"Metadata Only": "Kun metadata",
|
||||
"Minimum Free Disk Space": "Mindst ledig diskplads",
|
||||
"Move to top of queue": "Flyt til toppen af køen",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Flerniveau wildcard (matcher flere biblioteksniveauer)",
|
||||
"Never": "Aldrig",
|
||||
"New Device": "Ny enhed",
|
||||
"New Folder": "Ny mappe",
|
||||
"Newest First": "Nyeste først",
|
||||
"No": "Nej",
|
||||
"No File Versioning": "Ingen filversion",
|
||||
"Notice": "OBS",
|
||||
"OK": "OK",
|
||||
"Off": "Slå fra",
|
||||
"Oldest First": "Ældste først",
|
||||
"Options": "Indstillinger",
|
||||
"Out of Sync": "Ikke synkroniseret",
|
||||
"Out of Sync Items": "Endnu ikke synkroniserede filer",
|
||||
"Outgoing Rate Limit (KiB/s)": "Udgående hastighedsbegrænsning (KiB/s)",
|
||||
"Override Changes": "Overskriv ændringer",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sti til den lokale mappe. Vil blive oprettet hvis den ikke findes. Tilde tegnet (~) kan bruges som en forkortelse for",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sti hvor versioner skal gemmes (efterlad tom for default .stversions mappe)",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Pauset",
|
||||
"Please consult the release notes before performing a major upgrade.": "Tjek venligst udgivelsesnoterne før opgradering til en ny version.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Sæt vensligt en GUI bruger og kodeord i opsætningsdialogen.",
|
||||
"Please wait": "Vent venligst",
|
||||
"Preview": "Forhåndsvisning",
|
||||
"Preview Usage Report": "Forhåndsvisning af forbrugsrapport",
|
||||
"Quick guide to supported patterns": "Hurtig guide til supporteret mønstre",
|
||||
"RAM Utilization": "RAM-forbrug",
|
||||
"Random": "Tilfældig",
|
||||
"Relayed via": "Passeret gennem",
|
||||
"Relays": "Passager",
|
||||
"Release Notes": "Udgivelsesnoter",
|
||||
"Remove": "Fjern",
|
||||
"Rescan": "Skan igen",
|
||||
"Rescan All": "Skan alt igen",
|
||||
"Rescan Interval": "Genskannings interval",
|
||||
"Restart": "Genstart",
|
||||
"Restart Needed": "Programmet kræver genstart",
|
||||
"Restarting": "Genstarter",
|
||||
"Resume": "Genoptag",
|
||||
"Reused": "Genbrugt",
|
||||
"Save": "Gem",
|
||||
"Scan Time Remaining": "Tid tilbage af skanningen",
|
||||
"Scanning": "Opdaterer",
|
||||
"Select the devices to share this folder with.": "Vælg hvilke enheder du vil dele denne mappe med",
|
||||
"Select the folders to share with this device.": "Vælg hvilke mapper du vil dele med denne enhed.",
|
||||
"Settings": "Indstillinger",
|
||||
"Share": "Del",
|
||||
"Share Folder": "Delt mappe",
|
||||
"Share Folders With Device": "Del mappe med enhed",
|
||||
"Share With Devices": "Del med enhed",
|
||||
"Share this folder?": "Del denne mappe",
|
||||
"Shared With": "Delt med",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort navn for mappen. Skal være det samme på alle tilknyttede enheder.",
|
||||
"Show ID": "Vis ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vist istedet for Enheds ID i klynge status. Vil blive vist på andre enheder som valgfrit standard navn.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist istedet for Enheds ID i klynge status. Vil blive opdateret til det navn som enheden viser, hvis det ikke er udfyldt.",
|
||||
"Shutdown": "Luk ned",
|
||||
"Shutdown Complete": "Nedlukning fuldført",
|
||||
"Simple File Versioning": "Simpel fil versioner",
|
||||
"Single level wildcard (matches within a directory only)": "Enkeltnivau wildcard (matcher kun inden for en mapp)",
|
||||
"Smallest First": "Mindste først",
|
||||
"Source Code": "Kildekode",
|
||||
"Staggered File Versioning": "Forskudte filversioner",
|
||||
"Start Browser": "Start browser",
|
||||
"Statistics": "Statistikker",
|
||||
"Stopped": "Stoppet",
|
||||
"Support": "Support",
|
||||
"Sync Protocol Listen Addresses": "Lytteadresser for indgående forbindelser",
|
||||
"Syncing": "Synkroniserer",
|
||||
"Syncthing has been shut down.": "Syncthing er blevet lukket ned.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing indeholder følgende software eller dele heraf:",
|
||||
"Syncthing is restarting.": "Syncthing genstarter",
|
||||
"Syncthing is upgrading.": "Syncthing opgraderer",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ud til at være stoppet eller oplever problemer med din internetforbindels. Prøver igen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Det ser ud til at Syncthiing har problemer med at udføre opgaven. Prøv at genopfriske siden eller genstarte Synching hvis problemet vedbliver.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administationsdelen er konfigureret til at blive fjernstyret uden kodeord.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Samlet statistik er offentligt tilgængelig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen er gemt, men ikke aktiveret. Syncthing skal genstarte for at aktivere den nye konfiguration.",
|
||||
"The device ID cannot be blank.": "Enhedens ID må ikke være tom.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enheds ID som som skal bruges her, kan du finde i \"Rediger > Vis ID\"-dialogen på den anden enhed. Mellemrum og bindestreg er valgfri (ignoreres).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterede forbrugsrapport sendes dagligt. Den benyttes til at spore anvendte platforme, mappestørrelser og versioner. Hvis det typen af opsamlet data ændres på et senere tidspunkt, vil du blive spurgt om tilladelse igen.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det indtastede node ID ser ikke gyldigt ud. Det skal være en 52 eller 56 tegn streng, bestående af tal og bogstaver, eventuelt med mellemrum og bindestreger.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den første kommandolinjeparameter er mappestien og den anden parameter er den relative sti til mappen.",
|
||||
"The folder ID cannot be blank.": "Mappe-ID må ikke være tom",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Mappe-ID skal være en kort identificierende streng (64 karakterer eller mindre) bestående af bogstav-, tal-, punktum- (.), bindestreg- (-) og understregskarakteren (_).",
|
||||
"The folder ID must be unique.": "Mappe-ID skal være unik",
|
||||
"The folder path cannot be blank.": "Mappestien må ikke være tom",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De følgende intervaller er brugt: i den første time, er en version gemt hvert 30. sekund, for første da er en version gemt hver time, for de første 30 dage er en version gemt hver dag og indtil den maksimale periode er en version hver uge gemt.",
|
||||
"The following items could not be synchronized.": "Følgende filer kunne ikke synkroniseres.",
|
||||
"The maximum age must be a number and cannot be blank.": "Maks alder skal være et nummer og må ikke være tom",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maks tid gamle udgaver skal gemmes (i dage, sæt lig med 0 for at beholde gamle versioner for altid)",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Procentsatsen på den mindste ledige diskplads må ikke være negative og skal være mellem 0 og 100 (inklusiv).",
|
||||
"The number of days must be a number and cannot be blank.": "Antallet af dage skal være et nummer og feltet må ikke være tomt",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Antal dage filer gemmes i skraldespanden. Nul betyder for evigt.",
|
||||
"The number of old versions to keep, per file.": "Antallet af gamle versioner som gemmes, per fil.",
|
||||
"The number of versions must be a number and cannot be blank.": "Antallet af versioner skal være et tal, og kan ikke være blankt.",
|
||||
"The path cannot be blank.": "Stien må ikke være tom",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Ratebegrænsningen må ikke være negative tal (0: ingen begrænsning)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Genskanningsintervallet skal være et ikke-negativt antal sekunder",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "De prøves igen automatisk og vil blive synkroniseret når fejlen er løst.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Dette gør det nemt for hackere at få adgang til at læse og ændre filer på din computer.",
|
||||
"This is a major version upgrade.": "Dette er en ny version",
|
||||
"Trash Can File Versioning": "Skraldespand fil versioner",
|
||||
"Unknown": "Ukendt",
|
||||
"Unshared": "Ikke delt",
|
||||
"Unused": "Ubrugt",
|
||||
"Up to Date": "Fuldt opdateret",
|
||||
"Updated": "Opdateret",
|
||||
"Upgrade": "Upgradér",
|
||||
"Upgrade To {%version%}": "Opgradér til {{version}}",
|
||||
"Upgrading": "Opgradere",
|
||||
"Upload Rate": "Uploadhastighed",
|
||||
"Uptime": "Oppetid",
|
||||
"Use HTTPS for GUI": "Anvend HTTPS til GUI adgang",
|
||||
"Version": "Version",
|
||||
"Versions Path": "Versions-sti",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner slettes automatisk, hvis de er ældre end den satte maksimum alder eller overstiger det tilladte antal filer i et interval.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Når der tilføjes en ny enhed, vær da opmærksom på, at denne enhed også skal tilføjes på den anden side.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Når der tilføjes en ny enhed, vær da opmærksom på at samme ID bruges til at forbinde mapperne på de forskellige enheder. Der er forskel på store og små bogstaver, og ID skal være fuldstændig identisk på alle enheder.",
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du skal beholde mindst én version.",
|
||||
"days": "dage",
|
||||
"full documentation": "Fuld dokumentation",
|
||||
"items": "poster",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker at dele mappen \"{{folder}}\". "
|
||||
}
|
||||
@@ -68,9 +68,9 @@
|
||||
"Folder Path": "Map-paad",
|
||||
"Folders": "Mappen",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "GUI-wachtwurd foar ferifikaasje",
|
||||
"GUI Authentication User": "GUI-brûker foar ferifikaasje",
|
||||
"GUI Listen Addresses": "GUI-harkadres",
|
||||
"GUI Authentication Password": "Wachtwurd foar ferifikaasje yn GUI",
|
||||
"GUI Authentication User": "Brûkers-ID foar ferifikaasje yn GUI",
|
||||
"GUI Listen Addresses": "Harkadres foar GUI",
|
||||
"Generate": "Generearje",
|
||||
"Global Discovery": "Wrâldwide ûntdekking",
|
||||
"Global Discovery Server": "Wrâldwide ûntdekkingstsjinner",
|
||||
@@ -152,7 +152,7 @@
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Koart opskrift foar de map. Moat op alle apparaten itselde wêze.",
|
||||
"Show ID": "ID sjen litte",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wurd ynstee fan apparaat-ID sjen litten by de bondeltastân. Wurd nei oare apparaten advertearre as in mooglike standertnamme.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd ynstee fan apparaat-ID sjen litten by de bondeltastân. As it leech litten wurd, wurd it fernijt nei de namme die it apparaat advertearret.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd yn de bondel-tastân sjen litten ynstee fan apparaat-ID. Wannear't leech litten wurd, wurd it fernijt nei de namme die it apparaat útstjoert.",
|
||||
"Shutdown": "Ofslute",
|
||||
"Shutdown Complete": "Ofsluten klear",
|
||||
"Simple File Versioning": "Ienfâldich triemferzjebehear",
|
||||
@@ -160,7 +160,7 @@
|
||||
"Smallest First": "Lytste earst",
|
||||
"Source Code": "Boarnekoade",
|
||||
"Staggered File Versioning": "Sprieden triemferzjebehear",
|
||||
"Start Browser": "Browser starte",
|
||||
"Start Browser": "Browser iepenje wannear't Syncthing start",
|
||||
"Statistics": "Statistiken",
|
||||
"Stopped": "Stoppe",
|
||||
"Support": "Understeuning",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"Copied from elsewhere": "Kopiert fra et annet sted",
|
||||
"Copied from original": "Kopiert fra original",
|
||||
"Copyright © 2015 the following Contributors:": "Opphavsrett © 2015 de følgende bidragsytere:",
|
||||
"Danger!": "Danger!",
|
||||
"Danger!": "Fare!",
|
||||
"Delete": "Slett",
|
||||
"Deleted": "Slettet",
|
||||
"Device ID": "Enhets ID",
|
||||
@@ -148,12 +148,12 @@
|
||||
"Share Folders With Device": "Del Mapper Med Enhet",
|
||||
"Share With Devices": "Del Med Enheter",
|
||||
"Share this folder?": "Dele denne mappen?",
|
||||
"Shared With": "Delt Med",
|
||||
"Shared With": "Delt med",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort kjennemerke på mappen. Må være det samme på alle enheter i en gruppe.",
|
||||
"Show ID": "Vis ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vis i stedet for Eining ID i gruppestatus. Vil bli kringkastet til andre enheter som et valgfritt standardnavn.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist i stedet for Mappe-ID i gruppestatus. Vil bli oppdatert til navnet enheten kringkaster dersom tomt.",
|
||||
"Shutdown": "Slå Av",
|
||||
"Shutdown": "Avslutt",
|
||||
"Shutdown Complete": "Avslutning fullført",
|
||||
"Simple File Versioning": "Enkel Versjonskontroll",
|
||||
"Single level wildcard (matches within a directory only)": "Enkeltnivåsøk (søker kun i en mappe)",
|
||||
@@ -172,7 +172,7 @@
|
||||
"Syncthing is upgrading.": "Syncthing oppgraderer.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å være nede, eller så er det et problem med nettforbindelsen din. Prøver på ny …",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ser ut til å ha støtt på et problem under behandling av din forespørsel. Vennligst oppfrisk nettleseren eller start Syncthing på nytt dersom problemet vedvarer.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Grensesnittet for administrering av Syncthing er satt til å tillate ekstern tilgang uten et passord.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Samlet statistikk er åpent tilgjengelig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Innstillingene har blitt lagret men ikke aktivert. Syncthing må starte på ny for å aktivere de nye innstillingene.",
|
||||
"The device ID cannot be blank.": "Enhets-ID kan ikke være tom.",
|
||||
@@ -197,7 +197,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Hastighetsbegrensningen kan ikke være et negativt tall (0: ingen begrensing)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Antall sekund i skanneintervallet kan ikke være negativt.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Disse hentes automatisk og vil synkroniseres når feilen er blitt utbedret.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Dette kan lett gi hackere tilgang til å lese og endre alle filer på datamaskinen din.",
|
||||
"This is a major version upgrade.": "Dette er en hovedoppgradering",
|
||||
"Trash Can File Versioning": "Papirkurv Versjonskontroll",
|
||||
"Unknown": "Ukjent",
|
||||
@@ -220,6 +220,6 @@
|
||||
"You must keep at least one version.": "Du må beholde minst én versjon",
|
||||
"days": "dager",
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "element",
|
||||
"items": "elementer",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\"."
|
||||
}
|
||||
@@ -138,7 +138,7 @@
|
||||
"Resume": "Возобновить",
|
||||
"Reused": "Повторно использовано",
|
||||
"Save": "Сохранить",
|
||||
"Scan Time Remaining": "Scan Time Remaining",
|
||||
"Scan Time Remaining": "Оставшееся время сканирования",
|
||||
"Scanning": "Сканирование",
|
||||
"Select the devices to share this folder with.": "Выберите устройства, для которых будет доступна эта папка.",
|
||||
"Select the folders to share with this device.": "Выберите папку для предоставления доступа данному устройству",
|
||||
|
||||
225
gui/assets/lang/lang-vi.json
Normal file
225
gui/assets/lang/lang-vi.json
Normal file
@@ -0,0 +1,225 @@
|
||||
{
|
||||
"A negative number of days doesn't make sense.": "Số ngày không thể âm.",
|
||||
"A new major version may not be compatible with previous versions.": "Phiên bản quan trọng mới có thể sẽ không tương thích với các bản cũ.",
|
||||
"API Key": "Khoá API",
|
||||
"About": "Thông tin về",
|
||||
"Actions": "Hành động",
|
||||
"Add": "Thêm",
|
||||
"Add Device": "Thêm thiết bị",
|
||||
"Add Folder": "Thêm thư mục",
|
||||
"Add new folder?": "Thêm thư mục mới?",
|
||||
"Address": "Địa chỉ",
|
||||
"Addresses": "Các địa chỉ",
|
||||
"Advanced": "Nâng cao",
|
||||
"Advanced Configuration": "Cấu hình nâng cao",
|
||||
"All Data": "Tất cả dữ liệu",
|
||||
"Allow Anonymous Usage Reporting?": "Cho phép báo cáo sử dụng ẩn danh?",
|
||||
"Alphabetic": "A-Z",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Một lệnh ngoại vi xử lý việc phiên bản hoá. Nó phải loại bỏ tập tin này khỏi thư mục đã đồng bộ.",
|
||||
"Anonymous Usage Reporting": "Báo cáo sử dụng ẩn danh",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Bất kỳ thiết bị nào liên kết với thiết bị giới thiệu cũng sẽ được thêm vào.",
|
||||
"Automatic upgrades": "Cập nhật tự động",
|
||||
"Be careful!": "Cẩn thận!",
|
||||
"Bugs": "Lỗi",
|
||||
"CPU Utilization": "Mức sử dụng CPU",
|
||||
"Changelog": "Lược sử thay đổi",
|
||||
"Clean out after": "Dọn dẹp sau",
|
||||
"Close": "Đóng",
|
||||
"Command": "Lệnh",
|
||||
"Comment, when used at the start of a line": "Bình luận, khi dùng trước đầu dòng",
|
||||
"Compression": "Nén",
|
||||
"Connection Error": "Lỗi kết nối",
|
||||
"Copied from elsewhere": "Đã sao chép từ nơi khác",
|
||||
"Copied from original": "Đã sao chép từ nguồn",
|
||||
"Copyright © 2015 the following Contributors:": "Bản quyền © 2015 theo các nhà cộng tác sau:",
|
||||
"Danger!": "Nguy hiểm!",
|
||||
"Delete": "Xoá",
|
||||
"Deleted": "Đã xoá",
|
||||
"Device ID": "ID thiết bị",
|
||||
"Device Identification": "Danh tính thiết bị",
|
||||
"Device Name": "Tên thiết bị",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Thiết bị {{thiết bị}} ({{địa chỉ}}) muốn kết nối. Thêm thiết bị mới?",
|
||||
"Devices": "Các thiết bị",
|
||||
"Disconnected": "Đã ngắt kết nối",
|
||||
"Discovery": "Dò tìm",
|
||||
"Documentation": "Tài liệu",
|
||||
"Download Rate": "Tốc độ tải xuống",
|
||||
"Downloaded": "Đã tải xuống",
|
||||
"Downloading": "Đang tải xuống",
|
||||
"Edit": "Chỉnh sửa",
|
||||
"Edit Device": "Chỉnh sửa thiết bị",
|
||||
"Edit Folder": "Chỉnh sửa thư mục",
|
||||
"Editing": "Đang chỉnh sửa",
|
||||
"Enable UPnP": "Bật UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Nhập dấu phẩy ngăn cách các địa chỉ (\"tcp://ip:port\", \"tcp://host:port\") hoặc \"dynamic\" để tiến hành dò tìm địa chỉ tự động.",
|
||||
"Enter ignore patterns, one per line.": "Nhập quy luật bỏ qua, từng dòng một.",
|
||||
"Error": "Lỗi",
|
||||
"External File Versioning": "Phiên bản hoá tập tin ngoại vi",
|
||||
"Failed Items": "Các nội dung bị lỗi",
|
||||
"File Pull Order": "Thứ tự pull tập tin",
|
||||
"File Versioning": "Phiên bản hoá tập tin",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Các phần tử giấy phép tập tin sẽ được bỏ qua khi tìm kiếm thay đổi. Dùng trên hệ thống tập tin FAT.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Các tập tin sẽ được chuyển tới thư mục .stversions khi bị thay thế hoặc xoá bởi Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Các tập tin sẽ được chuyển tới các phiên bản được đánh dấu ngày tháng trong thư mục .stversions khi bị thay thế hoặc xoá bởi Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Các tập tin sẽ được bảo vệ khỏi những thay đổi trên các thiết bị khác, nhưng những thay đổi trên thiết bị này sẽ được chuyển tới những máy còn lại trong cụm.",
|
||||
"Folder": "Thư mục",
|
||||
"Folder ID": "ID thư mục",
|
||||
"Folder Master": "Thư mục chính",
|
||||
"Folder Path": "Đường dẫn thư mục",
|
||||
"Folders": "Các thư mục",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "Mật khẩu xác minh GUI",
|
||||
"GUI Authentication User": "Người dùng xác minh GUI",
|
||||
"GUI Listen Addresses": "Các địa chỉ lắng nghe GUI",
|
||||
"Generate": "Tạo mới",
|
||||
"Global Discovery": "Dò tìm toàn cầu",
|
||||
"Global Discovery Server": "Máy chủ dò tìm toàn cầu",
|
||||
"Global State": "Hiện trạng toàn cầu",
|
||||
"Help": "Trợ giúp",
|
||||
"Home page": "Trang chủ",
|
||||
"Ignore": "Bỏ qua",
|
||||
"Ignore Patterns": "Bỏ qua các quy luật",
|
||||
"Ignore Permissions": "Bỏ qua các giấy phép ",
|
||||
"Incoming Rate Limit (KiB/s)": "Giới hạn tốc độ đầu vào (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Cấu hình không đúng có thể gây mất mát nội dung và khiến Syncthing dừng hoạt động.",
|
||||
"Introducer": "Thiết bị giới thiệu",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Đảo ngược điều kiện cho trước (ví dụ: không được loại trừ)",
|
||||
"Keep Versions": "Giữ các phiên bản",
|
||||
"Largest First": "Lớn nhất trước tiên",
|
||||
"Last File Received": "Tập tin nhận cuối cùng",
|
||||
"Last seen": "Thấy lần cuối",
|
||||
"Later": "Để sau",
|
||||
"Local Discovery": "Dò tìm cục bộ",
|
||||
"Local State": "Trạng thái cục bộ",
|
||||
"Local State (Total)": "Trạng thái cục bộ (Tổng)",
|
||||
"Major Upgrade": "Bản nâng cấp quan trọng",
|
||||
"Maximum Age": "Thời hạn tối đa",
|
||||
"Metadata Only": "Chỉ siêu dữ liệu",
|
||||
"Minimum Free Disk Space": "Dung lượng ổ đĩa trống tối thiểu",
|
||||
"Move to top of queue": "Chuyển đến đầu hàng chờ",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Ký tự thay thế đa cấp (phù hợp với đa cấp độ thư mục)",
|
||||
"Never": "Không bao giờ",
|
||||
"New Device": "Thiết bị mới",
|
||||
"New Folder": "Thư mục mới",
|
||||
"Newest First": "Mới nhất đầu tiên",
|
||||
"No": "Không",
|
||||
"No File Versioning": "Không phiên bản hoá tập tin",
|
||||
"Notice": "Chú ý",
|
||||
"OK": "OK",
|
||||
"Off": "Tắt",
|
||||
"Oldest First": "Cũ nhất đầu tiên",
|
||||
"Options": "Tuỳ chọn",
|
||||
"Out of Sync": "Mất đồng bộ",
|
||||
"Out of Sync Items": "Các nội dung mất đồng bộ",
|
||||
"Outgoing Rate Limit (KiB/s)": "Giới hạn tốc độ đầu ra (KiB/s)",
|
||||
"Override Changes": "Ghi đè các thay đổi",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Đường dẫn đến thư mục trên máy tính cục bộ. Sẽ tạo mới nếu chưa có sẵn. Dấu ngã (~) có thể được dùng làm lối tắt",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Đường dẫn nơi các phiên bản được lưu trữ (để trống cho thư mục mặc định .stversions).",
|
||||
"Pause": "Tạm dừng",
|
||||
"Paused": "Đã tạm dừng",
|
||||
"Please consult the release notes before performing a major upgrade.": "Hãy xem kỹ lược sử phát hành trước khi tiến hành bản cập nhật quan trọng.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Hãy thiết lập tên người dùng và mật khẩu xác minh GUI trong hộp thoại Cài đặt.",
|
||||
"Please wait": "Xin chờ",
|
||||
"Preview": "Xem trước",
|
||||
"Preview Usage Report": "Xem trước báo cáo sử dụng",
|
||||
"Quick guide to supported patterns": "Hướng dẫn sơ lược về các quy luật được hỗ trợ",
|
||||
"RAM Utilization": "Mức sử dụng RAM",
|
||||
"Random": "Ngẫu nhiên",
|
||||
"Relayed via": "Chuyển tiếp qua",
|
||||
"Relays": "Các máy chuyển tiếp",
|
||||
"Release Notes": "Lược sử phát hành",
|
||||
"Remove": "Loại bỏ",
|
||||
"Rescan": "Quét lại",
|
||||
"Rescan All": "Quét lại tất cả",
|
||||
"Rescan Interval": "Khoảng cách thời gian quét lại",
|
||||
"Restart": "Khởi động lại",
|
||||
"Restart Needed": "Cần khởi động lại",
|
||||
"Restarting": "Đang khởi động lại",
|
||||
"Resume": "Tiếp tục",
|
||||
"Reused": "Đã dùng lại",
|
||||
"Save": "Sao lưu",
|
||||
"Scan Time Remaining": "Thời gian quét còn lại",
|
||||
"Scanning": "Đang quét",
|
||||
"Select the devices to share this folder with.": "Chọn các thiết bị để chia sẻ thư mục này.",
|
||||
"Select the folders to share with this device.": "Chọn các thư mục để chia sẻ với thiết bị này.",
|
||||
"Settings": "Cài đặt",
|
||||
"Share": "Chia sẻ",
|
||||
"Share Folder": "Chia sẻ thư mục",
|
||||
"Share Folders With Device": "Chia sẻ các thư mục với thiết bị",
|
||||
"Share With Devices": "Chia sẻ với các thiết bị",
|
||||
"Share this folder?": "Chia sẻ thư mục này?",
|
||||
"Shared With": "Đã chia sẻ với",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Tên tắt cho thư mục. Phải trùng khớp trên tất cả thiết bị trong cụm.",
|
||||
"Show ID": "Hiển thị ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Hiển thị thay cho ID thiết bị trong trạng thái cụm. Sẽ được giới thiệu đến các thiết bị khác như tên mặc định tuỳ chọn.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Hiển thị thay cho ID thiết bị trong trạng thái cụm. Nếu để trống sẽ được cập nhật thành tên mà thiết bị giới thiệu.",
|
||||
"Shutdown": "Đóng",
|
||||
"Shutdown Complete": "Hoàn tất đóng",
|
||||
"Simple File Versioning": "Phiên bản hoá tập tin dạng đơn giản",
|
||||
"Single level wildcard (matches within a directory only)": "Ký tự thay thế đơn cấp (phù hợp với chỉ một thư mục)",
|
||||
"Smallest First": "Nhỏ nhất đầu tiên",
|
||||
"Source Code": "Mã nguồn",
|
||||
"Staggered File Versioning": "Phiên bản hoá tập tin theo thời gian",
|
||||
"Start Browser": "Mở trình duyệt",
|
||||
"Statistics": "Thống kê",
|
||||
"Stopped": "Đã dừng",
|
||||
"Support": "Hỗ trợ",
|
||||
"Sync Protocol Listen Addresses": "Đồng bộ các địa chỉ lắng nghe giao thức",
|
||||
"Syncing": "Đang đồng bộ",
|
||||
"Syncthing has been shut down.": "Đã đóng Syncthing.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing bao gồm phần mềm hoặc các phần tử của phần mềm sau:",
|
||||
"Syncthing is restarting.": "Syncthing đang khởi động lại.",
|
||||
"Syncthing is upgrading.": "Syncthing đang nâng cấp.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Có vẻ như Syncthing đang bị nghẽn hoặc là mạng của bạn có vấn đề. Đang thử lại...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Có vẻ như Syncthing đang gặp phải vấn đề khi xử lý yêu cầu của bạn. Xin làm mới trang hoặc khởi động lại Syncthing nếu vấn đề vẫn còn tiếp diễn.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Giao diện quản trị của Syncthing được cấu hình nhằm cho phép truy cập từ xa không cần mật mã.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Thống kê tổng hợp được đăng công khai trên {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Cấu hình đã được lưu nhưng chưa được kích hoạt. Syncthing phải khởi động lại để kích hoạt cấu hình mới. ",
|
||||
"The device ID cannot be blank.": "ID thiết bị không thể trống.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ID thiết bị cần nhập có thể được tìm thấy trong hộp thoại \"Chỉnh sửa > Hiển thị ID\" trên thiết bị kia. Khoảng trắng và gạch ngang là tuỳ chọn (bỏ qua).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Báo cáo sử dụng đã mã khoá sẽ được gửi đi hằng ngày. Nó được dùng để thu thập số liệu về các hệ điều hành phổ biến, kích cỡ thư mục và phiên bản ứng dụng. Nếu bộ dữ liệu báo cáo có thay đổi, bạn sẽ được nhắc nhở thông qua hộp thoại này.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "ID thiết bị đã nhập không hợp lệ. Nó phải là một chuỗi từ 52 đến 56 ký tự, bao gồm chữ cái và các con số, với khoảng trắng và gạch ngang là tuỳ chọn.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Tham số dòng lệnh đầu tiên là đường dẫn thư mục và tham số thứ hai là đường dẫn tương đối trong thư mục.",
|
||||
"The folder ID cannot be blank.": "ID thư mục không thể trống.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "ID thư mục phải là một tên tắt (64 ký tự hoặc ít hơn) chỉ bao gồm chữ cái, các con số và dấu chấm (.), gạch ngang (-) và gạch chân (_).",
|
||||
"The folder ID must be unique.": "ID thư mục phải là duy nhất.",
|
||||
"The folder path cannot be blank.": "Đường dẫn thư mục không thể trống.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Các khoảng thời gian sau đây được sử dụng: một phiên bản, trong giờ đầu tiên, được giữ lại mỗi 30 giây, trong ngày đầu tiên là mỗi giờ, trong 30 ngày đầu tiên là mỗi ngày, cho đến khi thời hạn tối đa mỗi phiên bản được giữ lại là mỗi tuần. ",
|
||||
"The following items could not be synchronized.": "Những nội dung sau không thể đồng bộ được.",
|
||||
"The maximum age must be a number and cannot be blank.": "Thời hạn tối đa phải là một con số và không thể để trống.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Thời gian tối đa phiên bản được giữ lại (tính bằng ngày, từ 0 đến mãi mãi).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Phần trăm dung lượng ổ đĩa còn trống tối thiểu phải là một số không âm (trong khoảng) từ 0 đến 100.",
|
||||
"The number of days must be a number and cannot be blank.": "Số ngày phải là một con số và không thể để trống.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Số ngày giữ tập tin trong thùng rác. Số 0 nghĩa là mãi mãi.",
|
||||
"The number of old versions to keep, per file.": "Số phiên bản cũ cần giữ, với mỗi tập tin.",
|
||||
"The number of versions must be a number and cannot be blank.": "Số phiên bản phải là một con số và không thể để trống.",
|
||||
"The path cannot be blank.": "Đường dẫn không thể trống.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Giới hạn tốc độ phải là một số không âm (0: không giới hạn)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Khoảng thời gian quét lại phải là một số giây không âm. ",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Chúng sẽ được tự động thử lại và đồng bộ khi lỗi được khắc phục.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Thao tác này có thể khiến tin tặc truy cập để đọc và thay đổi bất kỳ tập tin nào trên máy tính của bạn một cách dễ dàng.",
|
||||
"This is a major version upgrade.": "Đây là bản nâng cấp quan trọng.",
|
||||
"Trash Can File Versioning": "Phiên bản hoá tập tin trong thùng rác",
|
||||
"Unknown": "Không biết",
|
||||
"Unshared": "Chưa chia sẻ",
|
||||
"Unused": "Chưa sử dụng",
|
||||
"Up to Date": "Đã cập nhật",
|
||||
"Updated": "Đã cập nhật",
|
||||
"Upgrade": "Nâng cấp",
|
||||
"Upgrade To {%version%}": "Nâng cấp lên {{phiên bản}}",
|
||||
"Upgrading": "Đang nâng cấp",
|
||||
"Upload Rate": "Tốc độ tải lên",
|
||||
"Uptime": "Thời gian hoạt động",
|
||||
"Use HTTPS for GUI": "Sử dụng HTTPS cho GUI",
|
||||
"Version": "Phiên bản",
|
||||
"Versions Path": "Đường dẫn các phiên bản",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Các phiên bản sẽ tự động được xoá nếu chúng cũ hơn thời hạn tối đa hoặc vượt quá số tập tin cho phép trong một khoảng thời gian.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Khi thêm một thiết bị mới, hãy nhớ rằng thiết bị này cũng được thêm vào máy khác.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Khi thêm một thư mục mới, hãy nhớ rằng ID thư mục được dùng để gắn kết thư mục giữa các thiết bị với nhau. Chúng phải chính xác từng chữ, cả viết hoa và thường giữa tất cả thiết bị.",
|
||||
"Yes": "Có",
|
||||
"You must keep at least one version.": "Bạn phải giữ ít nhất một phiên bản.",
|
||||
"days": "ngày",
|
||||
"full documentation": "tài liệu đầy đủ",
|
||||
"items": "các nội dung",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{thiết bị}} muốn chia sẻ thư mục \"{{thư mục}}\"."
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"A negative number of days doesn't make sense.": "天数不能为负。",
|
||||
"A negative number of days doesn't make sense.": "天数不能为负",
|
||||
"A new major version may not be compatible with previous versions.": "重大更新可能与之前的版本之间无法兼容",
|
||||
"API Key": "API Key",
|
||||
"About": "关于",
|
||||
@@ -20,7 +20,7 @@
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "在介绍人设备上被添加的其它设备,也将会被添加到本机。",
|
||||
"Automatic upgrades": "自动升级",
|
||||
"Be careful!": "小心!",
|
||||
"Bugs": "Bug汇报",
|
||||
"Bugs": "问题回报",
|
||||
"CPU Utilization": "CPU使用率",
|
||||
"Changelog": "更新日志",
|
||||
"Clean out after": "在该时间后清除:",
|
||||
@@ -68,12 +68,12 @@
|
||||
"Folder Path": "文件夹路径",
|
||||
"Folders": "文件夹",
|
||||
"GUI": "图形界面",
|
||||
"GUI Authentication Password": "登陆web管理页面的密码",
|
||||
"GUI Authentication User": "登陆web管理页面的用户名",
|
||||
"GUI Listen Addresses": "web管理页面监听地址",
|
||||
"GUI Authentication Password": "图形管理界面密码",
|
||||
"GUI Authentication User": "图形管理界面用户名",
|
||||
"GUI Listen Addresses": "图形管理界面监听地址",
|
||||
"Generate": "生成",
|
||||
"Global Discovery": "在互联网上寻找设备\n",
|
||||
"Global Discovery Server": "用以在互联网上寻找设备的Announce服务器地址",
|
||||
"Global Discovery Server": "全球发现服务器",
|
||||
"Global State": "全局状态",
|
||||
"Help": "帮助",
|
||||
"Home page": "主页",
|
||||
@@ -138,7 +138,7 @@
|
||||
"Resume": "恢复",
|
||||
"Reused": "复用",
|
||||
"Save": "保存",
|
||||
"Scan Time Remaining": "Scan Time Remaining",
|
||||
"Scan Time Remaining": "扫描剩余时间",
|
||||
"Scanning": "扫描中",
|
||||
"Select the devices to share this folder with.": "选择将本文件夹共享给哪些设备",
|
||||
"Select the folders to share with this device.": "选择与该设备共享的文件夹。",
|
||||
@@ -172,7 +172,7 @@
|
||||
"Syncthing is upgrading.": "Syncthing 正在升级",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎关闭了,或者您的网络连接存在故障。重试中...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing 在处理您的请求时似乎遇到了问题。如果问题持续,请刷新页面,或重启 Syncthing。",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "当前配置允许在不使用密码的情况下远程访问 Syncthing 管理界面",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "全局统计公布于 {{url}}",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "设置已经保存,但是还未生效。Syncthing 需要重启以启用新的设置。",
|
||||
"The device ID cannot be blank.": "设备标识不能为空",
|
||||
@@ -184,7 +184,7 @@
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "文件夹标识不得长于 64 字符,且仅能包含字母、数字、半角句号(.)、横线(-)和下划线(_)。",
|
||||
"The folder ID must be unique.": "文件夹标识不得重复",
|
||||
"The folder path cannot be blank.": "文件夹路径不能为空",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "保留的历史版本会满足以下条件:最近一小时内的历史版本,更新间隔小于30秒的仅保留一份。最近一天内的历史版本,更新间隔小于1小时的仅保留一份。最近一个月内的历史版本,更新间隔小于1天的仅保留一份。距离现在超过一个月且小于最长保留时间的,更新间隔小于1周的仅保留一份。",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "保留的历史版本会遵循以下条件:最近一小时内的历史版本,更新间隔小于三十秒的仅保留一份。最近一天内的历史版本,更新间隔小于一小时的仅保留一份。最近一个月内的历史版本,更新间隔小于一天的仅保留一份。距离现在超过一个月且小于最长保留时间的,更新间隔小于一周的仅保留一份。",
|
||||
"The following items could not be synchronized.": "下列项目无法被同步。",
|
||||
"The maximum age must be a number and cannot be blank.": "最长保留时间必须为数字,且不能为空。",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "历史版本保留的最长天数,0为永久保存",
|
||||
@@ -197,8 +197,8 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "传输速度限制为非负整数(0 表示不限制)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "扫描间隔单位为秒,且不能为负数。",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "系统将会自动重试,当错误被解决时,它们将会被同步。",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
|
||||
"This is a major version upgrade.": "这是一个重大版本更新。",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "这会让骇客能够轻而易举地访问及修改您的文件",
|
||||
"This is a major version upgrade.": "这是一个重大版本更新",
|
||||
"Trash Can File Versioning": "回收站式版本控制",
|
||||
"Unknown": "未知",
|
||||
"Unshared": "未共享",
|
||||
@@ -210,14 +210,14 @@
|
||||
"Upgrading": "升级中",
|
||||
"Upload Rate": "上传速度",
|
||||
"Uptime": "已启动",
|
||||
"Use HTTPS for GUI": "使用HTTPS连接web管理页面",
|
||||
"Use HTTPS for GUI": "使用加密连接到图形管理页面",
|
||||
"Version": "版本",
|
||||
"Versions Path": "历史版本路径",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "超过最长保留时间,或者不满足下列条件的历史版本,将会被删除。",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "若您在本机添加新设备,记住您也必须在这个设备上添加本机。",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "若你添加了新文件夹,记住文件夹标识是用以在不同设备间建立联系的。在不同设备间拥有相同标识的文件夹将会被同步。且文件夹标识大小写敏感。",
|
||||
"Yes": "是",
|
||||
"You must keep at least one version.": "您必须保留至少一个版本。",
|
||||
"You must keep at least one version.": "您必须保留至少一个版本",
|
||||
"days": "天",
|
||||
"full documentation": "完整文档",
|
||||
"items": "条目",
|
||||
|
||||
@@ -1 +1 @@
|
||||
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian (Romania)","ru":"Russian","sv":"Swedish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
|
||||
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian (Romania)","ru":"Russian","sv":"Swedish","uk":"Ukrainian","vi":"Vietnamese","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
var validLangs = ["bg","ca","ca@valencia","cs","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","uk","zh-CN","zh-TW"]
|
||||
var validLangs = ["bg","ca","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","uk","vi","zh-CN","zh-TW"]
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<link rel="shortcut icon" href="assets/img/favicon.png">
|
||||
<link rel="mask-icon" href="assets/img/safari-pinned-tab.svg" color="#0882c8">
|
||||
|
||||
<title ng-bind="thisDeviceName() + ' | Syncthing'"></title>
|
||||
<link href="vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
@@ -121,21 +122,22 @@
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<identicon data-value="device"></identicon> <span translate>New Device</span>
|
||||
<identicon data-value="device"></identicon>
|
||||
<span translate>New Device</span>
|
||||
<span class="pull-right">{{ event.time | date:"yyyy-MM-dd HH:mm:ss" }}</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
<small>{{ event.time | date:"yyyy-MM-dd HH:mm:ss" }}:</small>
|
||||
<span translate translate-value-device="{{ device }}" translate-value-address="{{ event.data.address }}">
|
||||
Device {%device%} ({%address%}) wants to connect. Add new device?
|
||||
<span>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="panel-footer clearfix">
|
||||
<div class="pull-right">
|
||||
<button type="button" class="btn btn-sm btn-success" ng-click="addNewDeviceID(device)">
|
||||
<span class="fa fa-check"></span> <span translate>Add</span>
|
||||
<button type="button" class="btn btn-sm btn-success" ng-click="addDevice(device)">
|
||||
<span class="fa fa-plus"></span> <span translate>Add Device</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger" ng-click="ignoreRejectedDevice(device)">
|
||||
<span class="fa fa-times"></span> <span translate>Ignore</span>
|
||||
@@ -158,11 +160,11 @@
|
||||
<h3 class="panel-title"><span class="fa fa-folder"></span>
|
||||
<span translate ng-if="!folders[event.data.folder]">New Folder</span>
|
||||
<span translate ng-if="folders[event.data.folder]">Share Folder</span>
|
||||
<span class="pull-right">{{ event.time | date:"yyyy-MM-dd HH:mm:ss" }}</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
<small>{{ event.time | date:"yyyy-MM-dd HH:mm:ss" }}:</small>
|
||||
<span translate translate-value-device="{{ deviceName(findDevice(event.data.device)) }}" translate-value-folder="{{ event.data.folder }}">
|
||||
{%device%} wants to share folder "{%folder%}".
|
||||
</span>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<li class="auto-generated">Aaron Bieber</li>
|
||||
<li class="auto-generated">Adam Piggott</li>
|
||||
<li class="auto-generated">Alexander Graf</li>
|
||||
<li class="auto-generated">Anderson Mesquita</li>
|
||||
<li class="auto-generated">Andrew Dunham</li>
|
||||
<li class="auto-generated">Antony Male</li>
|
||||
<li class="auto-generated">Arthur Axel fREW Schmidt</li>
|
||||
@@ -46,6 +47,7 @@
|
||||
<li class="auto-generated">Francois-Xavier Gsell</li>
|
||||
<li class="auto-generated">Frank Isemann</li>
|
||||
<li class="auto-generated">Gilli Sigurdsson</li>
|
||||
<li class="auto-generated">Jaakko Hannikainen</li>
|
||||
<li class="auto-generated">Jacek Szafarkiewicz</li>
|
||||
<li class="auto-generated">Jake Peterson</li>
|
||||
<li class="auto-generated">Jakob Borg</li>
|
||||
@@ -66,6 +68,7 @@
|
||||
<li class="auto-generated">Michael Jephcote</li>
|
||||
<li class="auto-generated">Michael Ploujnikov</li>
|
||||
<li class="auto-generated">Michael Tilli</li>
|
||||
<li class="auto-generated">Nate Morrison</li>
|
||||
<li class="auto-generated">Pascal Jungblut</li>
|
||||
<li class="auto-generated">Peter Hoeg</li>
|
||||
<li class="auto-generated">Philippe Schommers</li>
|
||||
@@ -84,6 +87,7 @@
|
||||
<li class="auto-generated">Veeti Paananen</li>
|
||||
<li class="auto-generated">Victor Buinsky</li>
|
||||
<li class="auto-generated">Vil Brekin</li>
|
||||
<li class="auto-generated">William A. Kennington III</li>
|
||||
<li class="auto-generated">Yannic A.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -970,13 +970,14 @@ angular.module('syncthing.core')
|
||||
$('#idqr').modal('show');
|
||||
};
|
||||
|
||||
$scope.addDevice = function () {
|
||||
$http.get(urlbase + '/system/discovery')
|
||||
$scope.addDevice = function (deviceID) {
|
||||
return $http.get(urlbase + '/system/discovery')
|
||||
.success(function (registry) {
|
||||
$scope.discovery = registry;
|
||||
})
|
||||
.then(function () {
|
||||
$scope.currentDevice = {
|
||||
deviceID: deviceID,
|
||||
_addressesStr: 'dynamic',
|
||||
compression: 'metadata',
|
||||
introducer: false,
|
||||
@@ -1016,18 +1017,7 @@ angular.module('syncthing.core')
|
||||
$scope.saveDevice = function () {
|
||||
$('#editDevice').modal('hide');
|
||||
$scope.saveDeviceConfig($scope.currentDevice);
|
||||
};
|
||||
|
||||
$scope.addNewDeviceID = function (device) {
|
||||
var deviceCfg = {
|
||||
deviceID: device,
|
||||
_addressesStr: 'dynamic',
|
||||
compression: 'metadata',
|
||||
introducer: false,
|
||||
selectedFolders: {}
|
||||
};
|
||||
$scope.saveDeviceConfig(deviceCfg);
|
||||
$scope.dismissDeviceRejection(device);
|
||||
$scope.dismissDeviceRejection($scope.currentDevice.deviceID);
|
||||
};
|
||||
|
||||
$scope.saveDeviceConfig = function (deviceCfg) {
|
||||
|
||||
@@ -48,13 +48,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label ng-if="upgradeInfo">
|
||||
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.autoUpgradeEnabled"> <span translate>Automatic upgrades</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
@@ -62,7 +55,15 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label ng-if="upgradeInfo">
|
||||
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.autoUpgradeEnabled"> <span translate>Automatic upgrades</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="form-group">
|
||||
<label translate for="GlobalAnnServersStr">Global Discovery Server</label>
|
||||
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr">
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -48,17 +48,6 @@ var (
|
||||
// DefaultDiscoveryServers should be substituted when the configuration
|
||||
// contains <globalAnnounceServer>default</globalAnnounceServer>.
|
||||
DefaultDiscoveryServers = append(DefaultDiscoveryServersV4, DefaultDiscoveryServersV6...)
|
||||
|
||||
// DefaultDiscoveryServersIP is used by the usage reporting.
|
||||
// XXX: Detect Android, and use this is we still don't have working DNS?
|
||||
DefaultDiscoveryServersIP = []string{
|
||||
"https://194.126.249.5/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA",
|
||||
"https://45.55.230.38/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS",
|
||||
"https://128.199.95.124/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ",
|
||||
"https://[2001:470:28:4d6::5]/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA",
|
||||
"https://[2604:a880:800:10::182:a001]/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS",
|
||||
"https://[2400:6180:0:d0::d9:d001]/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ",
|
||||
}
|
||||
)
|
||||
|
||||
func New(myID protocol.DeviceID) Configuration {
|
||||
|
||||
@@ -44,7 +44,7 @@ func (c GUIConfiguration) Address() string {
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) UseTLS() bool {
|
||||
if override := os.Getenv("STGUIADDRESS"); override != "" {
|
||||
if override := os.Getenv("STGUIADDRESS"); override != "" && strings.HasPrefix(override, "http") {
|
||||
return strings.HasPrefix(override, "https:")
|
||||
}
|
||||
return c.RawUseTLS
|
||||
|
||||
@@ -42,9 +42,9 @@ type Model interface {
|
||||
IsPaused(remoteID protocol.DeviceID) bool
|
||||
}
|
||||
|
||||
// The connection service listens on TLS and dials configured unconnected
|
||||
// The connection connectionService listens on TLS and dials configured unconnected
|
||||
// devices. Successful connections are handed to the model.
|
||||
type connectionSvc struct {
|
||||
type connectionService struct {
|
||||
*suture.Supervisor
|
||||
cfg *config.Wrapper
|
||||
myID protocol.DeviceID
|
||||
@@ -52,7 +52,7 @@ type connectionSvc struct {
|
||||
tlsCfg *tls.Config
|
||||
discoverer discover.Finder
|
||||
conns chan model.IntermediateConnection
|
||||
relaySvc *relay.Svc
|
||||
relayService *relay.Service
|
||||
bepProtocolName string
|
||||
tlsDefaultCommonName string
|
||||
lans []*net.IPNet
|
||||
@@ -66,16 +66,16 @@ type connectionSvc struct {
|
||||
relaysEnabled bool
|
||||
}
|
||||
|
||||
func NewConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, relaySvc *relay.Svc,
|
||||
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, relayService *relay.Service,
|
||||
bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) suture.Service {
|
||||
svc := &connectionSvc{
|
||||
Supervisor: suture.NewSimple("connectionSvc"),
|
||||
service := &connectionService{
|
||||
Supervisor: suture.NewSimple("connectionService"),
|
||||
cfg: cfg,
|
||||
myID: myID,
|
||||
model: mdl,
|
||||
tlsCfg: tlsCfg,
|
||||
discoverer: discoverer,
|
||||
relaySvc: relaySvc,
|
||||
relayService: relayService,
|
||||
conns: make(chan model.IntermediateConnection),
|
||||
bepProtocolName: bepProtocolName,
|
||||
tlsDefaultCommonName: tlsDefaultCommonName,
|
||||
@@ -85,20 +85,20 @@ func NewConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tl
|
||||
relaysEnabled: cfg.Options().RelaysEnabled,
|
||||
lastRelayCheck: make(map[protocol.DeviceID]time.Time),
|
||||
}
|
||||
cfg.Subscribe(svc)
|
||||
cfg.Subscribe(service)
|
||||
|
||||
if svc.cfg.Options().MaxSendKbps > 0 {
|
||||
svc.writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*svc.cfg.Options().MaxSendKbps), int64(5*1000*svc.cfg.Options().MaxSendKbps))
|
||||
if service.cfg.Options().MaxSendKbps > 0 {
|
||||
service.writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*service.cfg.Options().MaxSendKbps), int64(5*1000*service.cfg.Options().MaxSendKbps))
|
||||
}
|
||||
if svc.cfg.Options().MaxRecvKbps > 0 {
|
||||
svc.readRateLimit = ratelimit.NewBucketWithRate(float64(1000*svc.cfg.Options().MaxRecvKbps), int64(5*1000*svc.cfg.Options().MaxRecvKbps))
|
||||
if service.cfg.Options().MaxRecvKbps > 0 {
|
||||
service.readRateLimit = ratelimit.NewBucketWithRate(float64(1000*service.cfg.Options().MaxRecvKbps), int64(5*1000*service.cfg.Options().MaxRecvKbps))
|
||||
}
|
||||
|
||||
// There are several moving parts here; one routine per listening address
|
||||
// to handle incoming connections, one routine to periodically attempt
|
||||
// outgoing connections, one routine to the the common handling
|
||||
// regardless of whether the connection was incoming or outgoing.
|
||||
// Furthermore, a relay service which handles incoming requests to connect
|
||||
// Furthermore, a relay connectionService which handles incoming requests to connect
|
||||
// via the relays.
|
||||
//
|
||||
// TODO: Clean shutdown, and/or handling config changes on the fly. We
|
||||
@@ -106,8 +106,8 @@ func NewConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tl
|
||||
// not new listen addresses and we don't support disconnecting devices
|
||||
// that are removed and so on...
|
||||
|
||||
svc.Add(serviceFunc(svc.connect))
|
||||
for _, addr := range svc.cfg.Options().ListenAddress {
|
||||
service.Add(serviceFunc(service.connect))
|
||||
for _, addr := range service.cfg.Options().ListenAddress {
|
||||
uri, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
l.Infoln("Failed to parse listen address:", addr, err)
|
||||
@@ -122,20 +122,20 @@ func NewConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tl
|
||||
|
||||
l.Debugln("listening on", uri)
|
||||
|
||||
svc.Add(serviceFunc(func() {
|
||||
listener(uri, svc.tlsCfg, svc.conns)
|
||||
service.Add(serviceFunc(func() {
|
||||
listener(uri, service.tlsCfg, service.conns)
|
||||
}))
|
||||
}
|
||||
svc.Add(serviceFunc(svc.handle))
|
||||
service.Add(serviceFunc(service.handle))
|
||||
|
||||
if svc.relaySvc != nil {
|
||||
svc.Add(serviceFunc(svc.acceptRelayConns))
|
||||
if service.relayService != nil {
|
||||
service.Add(serviceFunc(service.acceptRelayConns))
|
||||
}
|
||||
|
||||
return svc
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *connectionSvc) handle() {
|
||||
func (s *connectionService) handle() {
|
||||
next:
|
||||
for c := range s.conns {
|
||||
cs := c.Conn.ConnectionState()
|
||||
@@ -257,15 +257,18 @@ next:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionSvc) connect() {
|
||||
func (s *connectionService) connect() {
|
||||
delay := time.Second
|
||||
for {
|
||||
l.Debugln("Reconnect loop")
|
||||
nextDevice:
|
||||
for deviceID, deviceCfg := range s.cfg.Devices() {
|
||||
if deviceID == s.myID {
|
||||
continue
|
||||
}
|
||||
|
||||
l.Debugln("Reconnect loop for", deviceID)
|
||||
|
||||
if s.model.IsPaused(deviceID) {
|
||||
continue
|
||||
}
|
||||
@@ -277,6 +280,7 @@ func (s *connectionSvc) connect() {
|
||||
relaysEnabled := s.relaysEnabled
|
||||
s.mut.RUnlock()
|
||||
if connected && ok && ct.IsDirect() {
|
||||
l.Debugln("Already connected to", deviceID, "via", ct.String())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -284,6 +288,7 @@ func (s *connectionSvc) connect() {
|
||||
|
||||
for _, addr := range addrs {
|
||||
if conn := s.connectDirect(deviceID, addr); conn != nil {
|
||||
l.Debugln("Connecting to", deviceID, "via", addr, "succeeded")
|
||||
if connected {
|
||||
s.model.Close(deviceID, fmt.Errorf("switching connections"))
|
||||
}
|
||||
@@ -292,6 +297,7 @@ func (s *connectionSvc) connect() {
|
||||
}
|
||||
continue nextDevice
|
||||
}
|
||||
l.Debugln("Connecting to", deviceID, "via", addr, "failed")
|
||||
}
|
||||
|
||||
// Only connect via relays if not already connected
|
||||
@@ -300,6 +306,7 @@ func (s *connectionSvc) connect() {
|
||||
// wait up to RelayReconnectIntervalM to connect again.
|
||||
// Also, do not try relays if we are explicitly told not to.
|
||||
if connected || len(relays) == 0 || !relaysEnabled {
|
||||
l.Debugln("Not connecting via relay", connected, len(relays) == 0, !relaysEnabled)
|
||||
continue nextDevice
|
||||
}
|
||||
|
||||
@@ -307,19 +314,21 @@ func (s *connectionSvc) connect() {
|
||||
if last, ok := s.lastRelayCheck[deviceID]; ok && time.Since(last) < reconIntv {
|
||||
l.Debugln("Skipping connecting via relay to", deviceID, "last checked at", last)
|
||||
continue nextDevice
|
||||
} else {
|
||||
l.Debugln("Trying relay connections to", deviceID, relays)
|
||||
}
|
||||
|
||||
l.Debugln("Trying relay connections to", deviceID, relays)
|
||||
|
||||
s.lastRelayCheck[deviceID] = time.Now()
|
||||
|
||||
for _, addr := range relays {
|
||||
if conn := s.connectViaRelay(deviceID, addr); conn != nil {
|
||||
l.Debugln("Connecting to", deviceID, "via", addr, "succeeded")
|
||||
s.conns <- model.IntermediateConnection{
|
||||
conn, model.ConnectionTypeRelayDial,
|
||||
}
|
||||
continue nextDevice
|
||||
}
|
||||
l.Debugln("Connecting to", deviceID, "via", addr, "failed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +340,7 @@ func (s *connectionSvc) connect() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionSvc) resolveAddresses(deviceID protocol.DeviceID, inAddrs []string) (addrs []string, relays []discover.Relay) {
|
||||
func (s *connectionService) resolveAddresses(deviceID protocol.DeviceID, inAddrs []string) (addrs []string, relays []discover.Relay) {
|
||||
for _, addr := range inAddrs {
|
||||
if addr == "dynamic" {
|
||||
if s.discoverer != nil {
|
||||
@@ -347,7 +356,7 @@ func (s *connectionSvc) resolveAddresses(deviceID protocol.DeviceID, inAddrs []s
|
||||
return
|
||||
}
|
||||
|
||||
func (s *connectionSvc) connectDirect(deviceID protocol.DeviceID, addr string) *tls.Conn {
|
||||
func (s *connectionService) connectDirect(deviceID protocol.DeviceID, addr string) *tls.Conn {
|
||||
uri, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
l.Infoln("Failed to parse connection url:", addr, err)
|
||||
@@ -370,7 +379,7 @@ func (s *connectionSvc) connectDirect(deviceID protocol.DeviceID, addr string) *
|
||||
return conn
|
||||
}
|
||||
|
||||
func (s *connectionSvc) connectViaRelay(deviceID protocol.DeviceID, addr discover.Relay) *tls.Conn {
|
||||
func (s *connectionService) connectViaRelay(deviceID protocol.DeviceID, addr discover.Relay) *tls.Conn {
|
||||
uri, err := url.Parse(addr.URL)
|
||||
if err != nil {
|
||||
l.Infoln("Failed to parse relay connection url:", addr, err)
|
||||
@@ -409,9 +418,9 @@ func (s *connectionSvc) connectViaRelay(deviceID protocol.DeviceID, addr discove
|
||||
return tc
|
||||
}
|
||||
|
||||
func (s *connectionSvc) acceptRelayConns() {
|
||||
func (s *connectionService) acceptRelayConns() {
|
||||
for {
|
||||
conn := s.relaySvc.Accept()
|
||||
conn := s.relayService.Accept()
|
||||
s.conns <- model.IntermediateConnection{
|
||||
Conn: conn,
|
||||
Type: model.ConnectionTypeRelayAccept,
|
||||
@@ -419,7 +428,7 @@ func (s *connectionSvc) acceptRelayConns() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
|
||||
func (s *connectionService) shouldLimit(addr net.Addr) bool {
|
||||
if s.cfg.Options().LimitBandwidthInLan {
|
||||
return true
|
||||
}
|
||||
@@ -436,11 +445,11 @@ func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
|
||||
return !tcpaddr.IP.IsLoopback()
|
||||
}
|
||||
|
||||
func (s *connectionSvc) VerifyConfiguration(from, to config.Configuration) error {
|
||||
func (s *connectionService) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *connectionSvc) CommitConfiguration(from, to config.Configuration) bool {
|
||||
func (s *connectionService) CommitConfiguration(from, to config.Configuration) bool {
|
||||
s.mut.Lock()
|
||||
s.relaysEnabled = to.Options.RelaysEnabled
|
||||
s.mut.Unlock()
|
||||
|
||||
@@ -65,8 +65,8 @@ func (m *CachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration, p
|
||||
m.caches = append(m.caches, newCache())
|
||||
m.mut.Unlock()
|
||||
|
||||
if svc, ok := finder.(suture.Service); ok {
|
||||
m.Supervisor.Add(svc)
|
||||
if service, ok := finder.(suture.Service); ok {
|
||||
m.Supervisor.Add(service)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1128,10 +1128,16 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
|
||||
m.fmut.RUnlock()
|
||||
files.Update(protocol.LocalDeviceID, fs)
|
||||
|
||||
filenames := make([]string, len(fs))
|
||||
for i, file := range fs {
|
||||
filenames[i] = file.Name
|
||||
}
|
||||
|
||||
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
|
||||
"folder": folder,
|
||||
"items": len(fs),
|
||||
"version": files.LocalVersion(protocol.LocalDeviceID),
|
||||
"folder": folder,
|
||||
"items": len(fs),
|
||||
"filenames": filenames,
|
||||
"version": files.LocalVersion(protocol.LocalDeviceID),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1753,8 +1759,11 @@ func (m *Model) CheckFolderHealth(id string) error {
|
||||
// Check for free space, if it isn't a master folder. We aren't
|
||||
// going to change the contents of master folders, so we don't
|
||||
// care about the amount of free space there.
|
||||
if free, errDfp := osutil.DiskFreePercentage(folder.Path()); errDfp == nil && free < folder.MinDiskFreePct {
|
||||
err = errors.New("insufficient free space")
|
||||
diskFreeP, errDfp := osutil.DiskFreePercentage(folder.Path())
|
||||
if errDfp == nil && diskFreeP < folder.MinDiskFreePct {
|
||||
diskFreeBytes, _ := osutil.DiskFreeBytes(folder.Path())
|
||||
str := fmt.Sprintf("insufficient free space (%d MiB, %.2f%%)", diskFreeBytes/1024/1024, diskFreeP)
|
||||
err = errors.New(str)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -79,19 +79,20 @@ type rwFolder struct {
|
||||
progressEmitter *ProgressEmitter
|
||||
virtualMtimeRepo *db.VirtualMtimeRepo
|
||||
|
||||
folder string
|
||||
dir string
|
||||
scanIntv time.Duration
|
||||
versioner versioner.Versioner
|
||||
ignorePerms bool
|
||||
copiers int
|
||||
pullers int
|
||||
shortID uint64
|
||||
order config.PullOrder
|
||||
maxConflicts int
|
||||
sleep time.Duration
|
||||
pause time.Duration
|
||||
allowSparse bool
|
||||
folder string
|
||||
dir string
|
||||
scanIntv time.Duration
|
||||
versioner versioner.Versioner
|
||||
ignorePerms bool
|
||||
copiers int
|
||||
pullers int
|
||||
shortID uint64
|
||||
order config.PullOrder
|
||||
maxConflicts int
|
||||
sleep time.Duration
|
||||
pause time.Duration
|
||||
allowSparse bool
|
||||
checkFreeSpace bool
|
||||
|
||||
stop chan struct{}
|
||||
queue *jobQueue
|
||||
@@ -117,16 +118,17 @@ func newRWFolder(m *Model, shortID uint64, cfg config.FolderConfiguration) *rwFo
|
||||
progressEmitter: m.progressEmitter,
|
||||
virtualMtimeRepo: db.NewVirtualMtimeRepo(m.db, cfg.ID),
|
||||
|
||||
folder: cfg.ID,
|
||||
dir: cfg.Path(),
|
||||
scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second,
|
||||
ignorePerms: cfg.IgnorePerms,
|
||||
copiers: cfg.Copiers,
|
||||
pullers: cfg.Pullers,
|
||||
shortID: shortID,
|
||||
order: cfg.Order,
|
||||
maxConflicts: cfg.MaxConflicts,
|
||||
allowSparse: !cfg.DisableSparseFiles,
|
||||
folder: cfg.ID,
|
||||
dir: cfg.Path(),
|
||||
scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second,
|
||||
ignorePerms: cfg.IgnorePerms,
|
||||
copiers: cfg.Copiers,
|
||||
pullers: cfg.Pullers,
|
||||
shortID: shortID,
|
||||
order: cfg.Order,
|
||||
maxConflicts: cfg.MaxConflicts,
|
||||
allowSparse: !cfg.DisableSparseFiles,
|
||||
checkFreeSpace: cfg.MinDiskFreePct != 0,
|
||||
|
||||
stop: make(chan struct{}),
|
||||
queue: newJobQueue(),
|
||||
@@ -967,23 +969,11 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
}
|
||||
}
|
||||
|
||||
if free, err := osutil.DiskFreeBytes(p.dir); err == nil && free < file.Size() {
|
||||
l.Warnf(`Folder "%s": insufficient disk space in %s for %s: have %.2f MiB, need %.2f MiB`, p.folder, p.dir, file.Name, float64(free)/1024/1024, float64(file.Size())/1024/1024)
|
||||
p.newError(file.Name, errors.New("insufficient space"))
|
||||
return
|
||||
}
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
scanner.PopulateOffsets(file.Blocks)
|
||||
|
||||
reused := 0
|
||||
var blocks []protocol.BlockInfo
|
||||
var blocksSize int64
|
||||
|
||||
// Check for an old temporary file which might have some blocks we could
|
||||
// reuse.
|
||||
@@ -1003,6 +993,7 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
_, ok := existingBlocks[block.String()]
|
||||
if !ok {
|
||||
blocks = append(blocks, block)
|
||||
blocksSize += int64(block.Size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1017,8 +1008,24 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
}
|
||||
} else {
|
||||
blocks = file.Blocks
|
||||
blocksSize = file.Size()
|
||||
}
|
||||
|
||||
if p.checkFreeSpace {
|
||||
if free, err := osutil.DiskFreeBytes(p.dir); err == nil && free < blocksSize {
|
||||
l.Warnf(`Folder "%s": insufficient disk space in %s for %s: have %.2f MiB, need %.2f MiB`, p.folder, p.dir, file.Name, float64(free)/1024/1024, float64(blocksSize)/1024/1024)
|
||||
p.newError(file.Name, errors.New("insufficient space"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
s := sharedPullerState{
|
||||
file: file,
|
||||
folder: p.folder,
|
||||
|
||||
@@ -42,6 +42,40 @@ var blocks = []protocol.BlockInfo{
|
||||
|
||||
var folders = []string{"default"}
|
||||
|
||||
func setUpFile(filename string, blockNumbers []int) protocol.FileInfo {
|
||||
// Create existing file
|
||||
existingBlocks := make([]protocol.BlockInfo, len(blockNumbers))
|
||||
for i := range blockNumbers {
|
||||
existingBlocks[i] = blocks[blockNumbers[i]]
|
||||
}
|
||||
|
||||
return protocol.FileInfo{
|
||||
Name: filename,
|
||||
Flags: 0,
|
||||
Modified: 0,
|
||||
Blocks: existingBlocks,
|
||||
}
|
||||
}
|
||||
|
||||
func setUpModel(file protocol.FileInfo) *Model {
|
||||
db := db.OpenMemory()
|
||||
model := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
model.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
model.updateLocals("default", []protocol.FileInfo{file})
|
||||
return model
|
||||
}
|
||||
|
||||
func setUpRwFolder(model *Model) rwFolder {
|
||||
return rwFolder{
|
||||
folder: "default",
|
||||
dir: "testdata",
|
||||
model: model,
|
||||
errors: make(map[string]string),
|
||||
errorsMut: sync.NewMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
// Layout of the files: (indexes from the above array)
|
||||
// 12345678 - Required file
|
||||
// 02005008 - Existing file (currently in the index)
|
||||
@@ -52,35 +86,13 @@ func TestHandleFile(t *testing.T) {
|
||||
// Copy: 2, 5, 8
|
||||
// Pull: 1, 3, 4, 6, 7
|
||||
|
||||
// Create existing file
|
||||
existingFile := protocol.FileInfo{
|
||||
Name: "filex",
|
||||
Flags: 0,
|
||||
Modified: 0,
|
||||
Blocks: []protocol.BlockInfo{
|
||||
blocks[0], blocks[2], blocks[0], blocks[0],
|
||||
blocks[5], blocks[0], blocks[0], blocks[8],
|
||||
},
|
||||
}
|
||||
|
||||
// Create target file
|
||||
existingBlocks := []int{0, 2, 0, 0, 5, 0, 0, 8}
|
||||
existingFile := setUpFile("filex", existingBlocks)
|
||||
requiredFile := existingFile
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
m.updateLocals("default", []protocol.FileInfo{existingFile})
|
||||
|
||||
p := rwFolder{
|
||||
folder: "default",
|
||||
dir: "testdata",
|
||||
model: m,
|
||||
errors: make(map[string]string),
|
||||
errorsMut: sync.NewMutex(),
|
||||
}
|
||||
|
||||
m := setUpModel(existingFile)
|
||||
p := setUpRwFolder(m)
|
||||
copyChan := make(chan copyBlocksState, 1)
|
||||
|
||||
p.handleFile(requiredFile, copyChan, nil)
|
||||
@@ -108,35 +120,13 @@ func TestHandleFileWithTemp(t *testing.T) {
|
||||
// Copy: 5, 8
|
||||
// Pull: 1, 6
|
||||
|
||||
// Create existing file
|
||||
existingFile := protocol.FileInfo{
|
||||
Name: "file",
|
||||
Flags: 0,
|
||||
Modified: 0,
|
||||
Blocks: []protocol.BlockInfo{
|
||||
blocks[0], blocks[2], blocks[0], blocks[0],
|
||||
blocks[5], blocks[0], blocks[0], blocks[8],
|
||||
},
|
||||
}
|
||||
|
||||
// Create target file
|
||||
existingBlocks := []int{0, 2, 0, 0, 5, 0, 0, 8}
|
||||
existingFile := setUpFile("file", existingBlocks)
|
||||
requiredFile := existingFile
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
m.updateLocals("default", []protocol.FileInfo{existingFile})
|
||||
|
||||
p := rwFolder{
|
||||
folder: "default",
|
||||
dir: "testdata",
|
||||
model: m,
|
||||
errors: make(map[string]string),
|
||||
errorsMut: sync.NewMutex(),
|
||||
}
|
||||
|
||||
m := setUpModel(existingFile)
|
||||
p := setUpRwFolder(m)
|
||||
copyChan := make(chan copyBlocksState, 1)
|
||||
|
||||
p.handleFile(requiredFile, copyChan, nil)
|
||||
@@ -169,47 +159,14 @@ func TestCopierFinder(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Create existing file
|
||||
existingFile := protocol.FileInfo{
|
||||
Name: defTempNamer.TempName("file"),
|
||||
Flags: 0,
|
||||
Modified: 0,
|
||||
Blocks: []protocol.BlockInfo{
|
||||
blocks[0], blocks[2], blocks[3], blocks[4],
|
||||
blocks[0], blocks[0], blocks[7], blocks[0],
|
||||
},
|
||||
}
|
||||
|
||||
// Create target file
|
||||
existingBlocks := []int{0, 2, 3, 4, 0, 0, 7, 0}
|
||||
existingFile := setUpFile(defTempNamer.TempName("file"), existingBlocks)
|
||||
requiredFile := existingFile
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
requiredFile.Name = "file2"
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
m.updateLocals("default", []protocol.FileInfo{existingFile})
|
||||
|
||||
iterFn := func(folder, file string, index int32) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Verify that the blocks we say exist on file, really exist in the db.
|
||||
for _, idx := range []int{2, 3, 4, 7} {
|
||||
if m.finder.Iterate(folders, blocks[idx].Hash, iterFn) == false {
|
||||
t.Error("Didn't find block")
|
||||
}
|
||||
}
|
||||
|
||||
p := rwFolder{
|
||||
folder: "default",
|
||||
dir: "testdata",
|
||||
model: m,
|
||||
errors: make(map[string]string),
|
||||
errorsMut: sync.NewMutex(),
|
||||
}
|
||||
|
||||
m := setUpModel(existingFile)
|
||||
p := setUpRwFolder(m)
|
||||
copyChan := make(chan copyBlocksState)
|
||||
pullChan := make(chan pullBlockState, 4)
|
||||
finisherChan := make(chan *sharedPullerState, 1)
|
||||
@@ -262,24 +219,9 @@ func TestCopierCleanup(t *testing.T) {
|
||||
return true
|
||||
}
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
// Create a file
|
||||
file := protocol.FileInfo{
|
||||
Name: "test",
|
||||
Flags: 0,
|
||||
Modified: 0,
|
||||
Blocks: []protocol.BlockInfo{blocks[0]},
|
||||
}
|
||||
|
||||
// Add file to index
|
||||
m.updateLocals("default", []protocol.FileInfo{file})
|
||||
|
||||
if !m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
|
||||
t.Error("Expected block not found")
|
||||
}
|
||||
file := setUpFile("test", []int{0})
|
||||
m := setUpModel(file)
|
||||
|
||||
file.Blocks = []protocol.BlockInfo{blocks[1]}
|
||||
file.Version = file.Version.Update(protocol.LocalDeviceID.Short())
|
||||
@@ -311,19 +253,10 @@ func TestCopierCleanup(t *testing.T) {
|
||||
// Make sure that the copier routine hashes the content when asked, and pulls
|
||||
// if it fails to find the block.
|
||||
func TestLastResortPulling(t *testing.T) {
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
// Add a file to index (with the incorrect block representation, as content
|
||||
// doesn't actually match the block list)
|
||||
file := protocol.FileInfo{
|
||||
Name: "empty",
|
||||
Flags: 0,
|
||||
Modified: 0,
|
||||
Blocks: []protocol.BlockInfo{blocks[0]},
|
||||
}
|
||||
m.updateLocals("default", []protocol.FileInfo{file})
|
||||
file := setUpFile("empty", []int{0})
|
||||
m := setUpModel(file)
|
||||
|
||||
// Pretend that we are handling a new file of the same content but
|
||||
// with a different name (causing to copy that particular block)
|
||||
@@ -333,18 +266,7 @@ func TestLastResortPulling(t *testing.T) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check that that particular block is there
|
||||
if !m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
|
||||
t.Error("Expected block not found")
|
||||
}
|
||||
|
||||
p := rwFolder{
|
||||
folder: "default",
|
||||
dir: "testdata",
|
||||
model: m,
|
||||
errors: make(map[string]string),
|
||||
errorsMut: sync.NewMutex(),
|
||||
}
|
||||
p := setUpRwFolder(m)
|
||||
|
||||
copyChan := make(chan copyBlocksState)
|
||||
pullChan := make(chan pullBlockState, 1)
|
||||
@@ -374,15 +296,7 @@ func TestLastResortPulling(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
file := protocol.FileInfo{
|
||||
Name: "filex",
|
||||
Flags: 0,
|
||||
Modified: 0,
|
||||
Blocks: []protocol.BlockInfo{
|
||||
blocks[0], blocks[2], blocks[0], blocks[0],
|
||||
blocks[5], blocks[0], blocks[0], blocks[8],
|
||||
},
|
||||
}
|
||||
file := setUpFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
|
||||
defer os.Remove("testdata/" + defTempNamer.TempName("filex"))
|
||||
|
||||
db := db.OpenMemory()
|
||||
@@ -467,15 +381,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
file := protocol.FileInfo{
|
||||
Name: "filex",
|
||||
Flags: 0,
|
||||
Modified: 0,
|
||||
Blocks: []protocol.BlockInfo{
|
||||
blocks[0], blocks[2], blocks[0], blocks[0],
|
||||
blocks[5], blocks[0], blocks[0], blocks[8],
|
||||
},
|
||||
}
|
||||
file := setUpFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
|
||||
defer os.Remove("testdata/" + defTempNamer.TempName("filex"))
|
||||
|
||||
db := db.OpenMemory()
|
||||
|
||||
@@ -88,7 +88,6 @@ func (c *staticClient) Serve() {
|
||||
|
||||
l.Debugln(c, "joined", c.conn.RemoteAddr(), "via", c.conn.LocalAddr())
|
||||
|
||||
defer c.cleanup()
|
||||
c.mut.Lock()
|
||||
c.connected = true
|
||||
c.mut.Unlock()
|
||||
@@ -110,10 +109,10 @@ func (c *staticClient) Serve() {
|
||||
case protocol.Ping:
|
||||
if err := protocol.WriteMessage(c.conn, protocol.Pong{}); err != nil {
|
||||
l.Infoln("Relay write:", err)
|
||||
return
|
||||
|
||||
c.disconnect()
|
||||
} else {
|
||||
l.Debugln(c, "sent pong")
|
||||
}
|
||||
l.Debugln(c, "sent pong")
|
||||
|
||||
case protocol.SessionInvitation:
|
||||
ip := net.IP(msg.Address)
|
||||
@@ -124,24 +123,38 @@ func (c *staticClient) Serve() {
|
||||
|
||||
case protocol.RelayFull:
|
||||
l.Infoln("Disconnected from relay due to it becoming full.")
|
||||
return
|
||||
c.disconnect()
|
||||
|
||||
default:
|
||||
l.Infoln("Relay: protocol error: unexpected message %v", msg)
|
||||
return
|
||||
c.disconnect()
|
||||
}
|
||||
|
||||
case <-c.stop:
|
||||
l.Debugln(c, "stopping")
|
||||
return
|
||||
c.disconnect()
|
||||
|
||||
// We always exit via this branch of the select, to make sure the
|
||||
// the reader routine exits.
|
||||
case err := <-errors:
|
||||
l.Infoln("Relay received:", err)
|
||||
close(errors)
|
||||
close(messages)
|
||||
c.mut.Lock()
|
||||
if c.connected {
|
||||
c.conn.Close()
|
||||
c.connected = false
|
||||
l.Infoln("Relay received:", err)
|
||||
}
|
||||
if c.closeInvitationsOnFinish {
|
||||
close(c.invitations)
|
||||
c.invitations = make(chan protocol.SessionInvitation)
|
||||
}
|
||||
c.mut.Unlock()
|
||||
return
|
||||
|
||||
case <-timeout.C:
|
||||
l.Debugln(c, "timed out")
|
||||
return
|
||||
c.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,9 +213,6 @@ func (c *staticClient) connect() error {
|
||||
c.mut.Unlock()
|
||||
|
||||
conn := tls.Client(tcpConn, c.config)
|
||||
if err = conn.Handshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.SetDeadline(time.Now().Add(c.connectTimeout)); err != nil {
|
||||
conn.Close()
|
||||
@@ -218,13 +228,9 @@ func (c *staticClient) connect() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *staticClient) cleanup() {
|
||||
l.Debugln(c, "cleaning up")
|
||||
func (c *staticClient) disconnect() {
|
||||
l.Debugln(c, "disconnecting")
|
||||
c.mut.Lock()
|
||||
if c.closeInvitationsOnFinish {
|
||||
close(c.invitations)
|
||||
c.invitations = make(chan protocol.SessionInvitation)
|
||||
}
|
||||
c.connected = false
|
||||
c.mut.Unlock()
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const (
|
||||
eventBroadcasterCheckInterval = 10 * time.Second
|
||||
)
|
||||
|
||||
type Svc struct {
|
||||
type Service struct {
|
||||
*suture.Supervisor
|
||||
cfg *config.Wrapper
|
||||
tlsCfg *tls.Config
|
||||
@@ -38,11 +38,11 @@ type Svc struct {
|
||||
conns chan *tls.Conn
|
||||
}
|
||||
|
||||
func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config) *Svc {
|
||||
func NewService(cfg *config.Wrapper, tlsCfg *tls.Config) *Service {
|
||||
conns := make(chan *tls.Conn)
|
||||
|
||||
svc := &Svc{
|
||||
Supervisor: suture.New("Svc", suture.Spec{
|
||||
service := &Service{
|
||||
Supervisor: suture.New("Service", suture.Spec{
|
||||
Log: func(log string) {
|
||||
l.Debugln(log)
|
||||
},
|
||||
@@ -61,28 +61,28 @@ func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config) *Svc {
|
||||
}
|
||||
|
||||
rcfg := cfg.Raw()
|
||||
svc.CommitConfiguration(rcfg, rcfg)
|
||||
cfg.Subscribe(svc)
|
||||
service.CommitConfiguration(rcfg, rcfg)
|
||||
cfg.Subscribe(service)
|
||||
|
||||
receiver := &invitationReceiver{
|
||||
tlsCfg: tlsCfg,
|
||||
conns: conns,
|
||||
invitations: svc.invitations,
|
||||
invitations: service.invitations,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
eventBc := &eventBroadcaster{
|
||||
svc: svc,
|
||||
stop: make(chan struct{}),
|
||||
Service: service,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
svc.Add(receiver)
|
||||
svc.Add(eventBc)
|
||||
service.Add(receiver)
|
||||
service.Add(eventBc)
|
||||
|
||||
return svc
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Svc) VerifyConfiguration(from, to config.Configuration) error {
|
||||
func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
|
||||
for _, addr := range to.Options.RelayServers {
|
||||
_, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
@@ -92,7 +92,7 @@ func (s *Svc) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
|
||||
func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
|
||||
existing := make(map[string]*url.URL, len(to.Options.RelayServers))
|
||||
|
||||
for _, addr := range to.Options.RelayServers {
|
||||
@@ -112,7 +112,7 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
|
||||
l.Debugln("Connecting to relay", uri)
|
||||
c, err := client.NewClient(uri, s.tlsCfg.Certificates, s.invitations, 10*time.Second)
|
||||
if err != nil {
|
||||
l.Debugln("Failed to connect to relay", uri, err)
|
||||
l.Infoln("Failed to connect to relay", uri, err)
|
||||
continue
|
||||
}
|
||||
s.tokens[key] = s.Add(c)
|
||||
@@ -142,7 +142,7 @@ type Status struct {
|
||||
}
|
||||
|
||||
// Relays return the list of relays that currently have an OK status.
|
||||
func (s *Svc) Relays() []string {
|
||||
func (s *Service) Relays() []string {
|
||||
if s == nil {
|
||||
// A nil client does not have a status, really. Yet we may be called
|
||||
// this way, for raisins...
|
||||
@@ -162,7 +162,7 @@ func (s *Svc) Relays() []string {
|
||||
}
|
||||
|
||||
// RelayStatus returns the latency and OK status for a given relay.
|
||||
func (s *Svc) RelayStatus(uri string) (time.Duration, bool) {
|
||||
func (s *Service) RelayStatus(uri string) (time.Duration, bool) {
|
||||
if s == nil {
|
||||
// A nil client does not have a status, really. Yet we may be called
|
||||
// this way, for raisins...
|
||||
@@ -182,7 +182,7 @@ func (s *Svc) RelayStatus(uri string) (time.Duration, bool) {
|
||||
}
|
||||
|
||||
// Accept returns a new *tls.Conn. The connection is already handshaken.
|
||||
func (s *Svc) Accept() *tls.Conn {
|
||||
func (s *Service) Accept() *tls.Conn {
|
||||
return <-s.conns
|
||||
}
|
||||
|
||||
@@ -234,8 +234,8 @@ func (r *invitationReceiver) Stop() {
|
||||
// no way to get the event feed directly from the relay lib. This may be
|
||||
// something to revisit later, possibly.
|
||||
type eventBroadcaster struct {
|
||||
svc *Svc
|
||||
stop chan struct{}
|
||||
Service *Service
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func (e *eventBroadcaster) Serve() {
|
||||
@@ -247,7 +247,7 @@ func (e *eventBroadcaster) Serve() {
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
curOKRelays := e.svc.Relays()
|
||||
curOKRelays := e.Service.Relays()
|
||||
|
||||
changed := len(curOKRelays) != len(prevOKRelays)
|
||||
if !changed {
|
||||
|
||||
@@ -58,13 +58,9 @@ func (n *IGD) URL() *url.URL {
|
||||
|
||||
// An IGDService is a specific service provided by an IGD.
|
||||
type IGDService struct {
|
||||
serviceID string
|
||||
serviceURL string
|
||||
serviceURN string
|
||||
}
|
||||
|
||||
func (s *IGDService) ID() string {
|
||||
return s.serviceID
|
||||
ID string
|
||||
URL string
|
||||
URN string
|
||||
}
|
||||
|
||||
type Protocol string
|
||||
@@ -75,9 +71,9 @@ const (
|
||||
)
|
||||
|
||||
type upnpService struct {
|
||||
ServiceID string `xml:"serviceId"`
|
||||
ServiceType string `xml:"serviceType"`
|
||||
ControlURL string `xml:"controlURL"`
|
||||
ID string `xml:"serviceId"`
|
||||
Type string `xml:"serviceType"`
|
||||
ControlURL string `xml:"controlURL"`
|
||||
}
|
||||
|
||||
type upnpDevice struct {
|
||||
@@ -132,8 +128,8 @@ nextResult:
|
||||
if existingResult.uuid == result.uuid {
|
||||
if shouldDebug() {
|
||||
l.Debugf("Skipping duplicate result %s with services:", result.uuid)
|
||||
for _, svc := range result.services {
|
||||
l.Debugf("* [%s] %s", svc.serviceID, svc.serviceURL)
|
||||
for _, service := range result.services {
|
||||
l.Debugf("* [%s] %s", service.ID, service.URL)
|
||||
}
|
||||
}
|
||||
continue nextResult
|
||||
@@ -143,8 +139,8 @@ nextResult:
|
||||
results = append(results, result)
|
||||
if shouldDebug() {
|
||||
l.Debugf("UPnP discovery result %s with services:", result.uuid)
|
||||
for _, svc := range result.services {
|
||||
l.Debugf("* [%s] %s", svc.serviceID, svc.serviceURL)
|
||||
for _, service := range result.services {
|
||||
l.Debugf("* [%s] %s", service.ID, service.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,9 +313,9 @@ func getChildDevices(d upnpDevice, deviceType string) []upnpDevice {
|
||||
|
||||
func getChildServices(d upnpDevice, serviceType string) []upnpService {
|
||||
var result []upnpService
|
||||
for _, svc := range d.Services {
|
||||
if svc.ServiceType == serviceType {
|
||||
result = append(result, svc)
|
||||
for _, service := range d.Services {
|
||||
if service.Type == serviceType {
|
||||
result = append(result, service)
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -352,7 +348,7 @@ func getServiceDescriptions(rootURL string, device upnpDevice) ([]IGDService, er
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getIGDServices(rootURL string, device upnpDevice, wanDeviceURN string, wanConnectionURN string, serviceURNs []string) []IGDService {
|
||||
func getIGDServices(rootURL string, device upnpDevice, wanDeviceURN string, wanConnectionURN string, URNs []string) []IGDService {
|
||||
var result []IGDService
|
||||
|
||||
devices := getChildDevices(device, wanDeviceURN)
|
||||
@@ -370,21 +366,21 @@ func getIGDServices(rootURL string, device upnpDevice, wanDeviceURN string, wanC
|
||||
}
|
||||
|
||||
for _, connection := range connections {
|
||||
for _, serviceURN := range serviceURNs {
|
||||
services := getChildServices(connection, serviceURN)
|
||||
for _, URN := range URNs {
|
||||
services := getChildServices(connection, URN)
|
||||
|
||||
l.Debugln(rootURL, "- no services of type", serviceURN, " found on connection.")
|
||||
l.Debugln(rootURL, "- no services of type", URN, " found on connection.")
|
||||
|
||||
for _, service := range services {
|
||||
if len(service.ControlURL) == 0 {
|
||||
l.Infoln(rootURL+"- malformed", service.ServiceType, "description: no control URL.")
|
||||
l.Infoln(rootURL+"- malformed", service.Type, "description: no control URL.")
|
||||
} else {
|
||||
u, _ := url.Parse(rootURL)
|
||||
replaceRawPath(u, service.ControlURL)
|
||||
|
||||
l.Debugln(rootURL, "- found", service.ServiceType, "with URL", u)
|
||||
l.Debugln(rootURL, "- found", service.Type, "with URL", u)
|
||||
|
||||
service := IGDService{serviceID: service.ServiceID, serviceURL: u.String(), serviceURN: service.ServiceType}
|
||||
service := IGDService{ID: service.ID, URL: u.String(), URN: service.Type}
|
||||
|
||||
result = append(result, service)
|
||||
}
|
||||
@@ -525,15 +521,14 @@ func (s *IGDService) AddPortMapping(localIPAddress string, protocol Protocol, ex
|
||||
<NewPortMappingDescription>%s</NewPortMappingDescription>
|
||||
<NewLeaseDuration>%d</NewLeaseDuration>
|
||||
</u:AddPortMapping>`
|
||||
body := fmt.Sprintf(tpl, s.serviceURN, externalPort, protocol, internalPort, localIPAddress, description, timeout)
|
||||
body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, timeout)
|
||||
|
||||
response, err := soapRequest(s.serviceURL, s.serviceURN, "AddPortMapping", body)
|
||||
response, err := soapRequest(s.URL, s.URN, "AddPortMapping", body)
|
||||
if err != nil && timeout > 0 {
|
||||
// Try to repair error code 725 - OnlyPermanentLeasesSupported
|
||||
envelope := &soapErrorResponse{}
|
||||
err = xml.Unmarshal(response, envelope)
|
||||
if err != nil {
|
||||
return err
|
||||
if unmarshalErr := xml.Unmarshal(response, envelope); unmarshalErr != nil {
|
||||
return unmarshalErr
|
||||
}
|
||||
if envelope.ErrorCode == 725 {
|
||||
return s.AddPortMapping(localIPAddress, protocol, externalPort, internalPort, description, 0)
|
||||
@@ -550,9 +545,9 @@ func (s *IGDService) DeletePortMapping(protocol Protocol, externalPort int) erro
|
||||
<NewExternalPort>%d</NewExternalPort>
|
||||
<NewProtocol>%s</NewProtocol>
|
||||
</u:DeletePortMapping>`
|
||||
body := fmt.Sprintf(tpl, s.serviceURN, externalPort, protocol)
|
||||
body := fmt.Sprintf(tpl, s.URN, externalPort, protocol)
|
||||
|
||||
_, err := soapRequest(s.serviceURL, s.serviceURN, "DeletePortMapping", body)
|
||||
_, err := soapRequest(s.URL, s.URN, "DeletePortMapping", body)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -567,9 +562,9 @@ func (s *IGDService) DeletePortMapping(protocol Protocol, externalPort int) erro
|
||||
func (s *IGDService) GetExternalIPAddress() (net.IP, error) {
|
||||
tpl := `<u:GetExternalIPAddress xmlns:u="%s" />`
|
||||
|
||||
body := fmt.Sprintf(tpl, s.serviceURN)
|
||||
body := fmt.Sprintf(tpl, s.URN)
|
||||
|
||||
response, err := soapRequest(s.serviceURL, s.serviceURN, "GetExternalIPAddress", body)
|
||||
response, err := soapRequest(s.URL, s.URN, "GetExternalIPAddress", body)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -85,16 +85,18 @@ func (v Simple) Archive(filePath string) error {
|
||||
}
|
||||
|
||||
// Glob according to the new file~timestamp.ext pattern.
|
||||
newVersions, err := osutil.Glob(filepath.Join(dir, taggedFilename(file, TimeGlob)))
|
||||
pattern := filepath.Join(dir, taggedFilename(file, TimeGlob))
|
||||
newVersions, err := osutil.Glob(pattern)
|
||||
if err != nil {
|
||||
l.Warnln("globbing:", err)
|
||||
l.Warnln("globbing:", err, "for", pattern)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Also according to the old file.ext~timestamp pattern.
|
||||
oldVersions, err := osutil.Glob(filepath.Join(dir, file+"~"+TimeGlob))
|
||||
pattern = filepath.Join(dir, file+"~"+TimeGlob)
|
||||
oldVersions, err := osutil.Glob(pattern)
|
||||
if err != nil {
|
||||
l.Warnln("globbing:", err)
|
||||
l.Warnln("globbing:", err, "for", pattern)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -258,16 +258,18 @@ func (v Staggered) Archive(filePath string) error {
|
||||
}
|
||||
|
||||
// Glob according to the new file~timestamp.ext pattern.
|
||||
newVersions, err := osutil.Glob(filepath.Join(dir, taggedFilename(file, TimeGlob)))
|
||||
pattern := filepath.Join(dir, taggedFilename(file, TimeGlob))
|
||||
newVersions, err := osutil.Glob(pattern)
|
||||
if err != nil {
|
||||
l.Warnln("globbing:", err)
|
||||
l.Warnln("globbing:", err, "for", pattern)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Also according to the old file.ext~timestamp pattern.
|
||||
oldVersions, err := osutil.Glob(filepath.Join(dir, file+"~"+TimeGlob))
|
||||
pattern = filepath.Join(dir, file+"~"+TimeGlob)
|
||||
oldVersions, err := osutil.Glob(pattern)
|
||||
if err != nil {
|
||||
l.Warnln("globbing:", err)
|
||||
l.Warnln("globbing:", err, "for", pattern)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ pages=(
|
||||
syncthing-bep.7
|
||||
syncthing-localdisco.7
|
||||
syncthing-globaldisco.7
|
||||
syncthing-relay.7
|
||||
)
|
||||
|
||||
for page in "${pages[@]}" ; do
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-BEP" "7" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-bep \- Block Exchange Protocol v1
|
||||
.
|
||||
@@ -1003,8 +1003,8 @@ directions.
|
||||
.UNINDENT
|
||||
.SS Read Only
|
||||
.sp
|
||||
In read only mode, a device does not synchronize the local folder to the
|
||||
cluster, but publishes changes to its local folder contents as usual.
|
||||
In read only mode, a device does not apply any updates from the cluster,
|
||||
but publishes changes of its local folder to the cluster as usual.
|
||||
The local folder can be seen as a "master copy" that is never affected
|
||||
by the actions of other cluster devices.
|
||||
.INDENT 0.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-EVENT-API" "7" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-FAQ" "7" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v3
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-NETWORKING" "7" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.
|
||||
|
||||
699
man/syncthing-relay.7
Normal file
699
man/syncthing-relay.7
Normal file
@@ -0,0 +1,699 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-RELAY" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-relay \- Relay Protocol v1
|
||||
.
|
||||
.nr rst2man-indent-level 0
|
||||
.
|
||||
.de1 rstReportMargin
|
||||
\\$1 \\n[an-margin]
|
||||
level \\n[rst2man-indent-level]
|
||||
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
-
|
||||
\\n[rst2man-indent0]
|
||||
\\n[rst2man-indent1]
|
||||
\\n[rst2man-indent2]
|
||||
..
|
||||
.de1 INDENT
|
||||
.\" .rstReportMargin pre:
|
||||
. RS \\$1
|
||||
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
|
||||
. nr rst2man-indent-level +1
|
||||
.\" .rstReportMargin post:
|
||||
..
|
||||
.de UNINDENT
|
||||
. RE
|
||||
.\" indent \\n[an-margin]
|
||||
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.nr rst2man-indent-level -1
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.SH WHAT IS A RELAY?
|
||||
.sp
|
||||
Relay is a service which relays data between two \fIdevices\fP which are not able to
|
||||
connect to each other directly otherwise. This is usually due to both devices
|
||||
being behind a NAT and neither side being able to open a port which would
|
||||
be directly accessible from the internet.
|
||||
.sp
|
||||
A relay was designed to relay BEP protocol, hence the reliance on device ID\(aqs
|
||||
in the protocol spec, but at the same time it is general enough that could be
|
||||
reused by other protocols or applications, as the data transferred between two
|
||||
devices which use a relay is completely obscure and does not affect the
|
||||
relaying.
|
||||
.SH OPERATION MODES
|
||||
.sp
|
||||
Relay listens on a single TCP socket, but has two different connection modes,
|
||||
where a connection mode is a predefined set of messages which the relay and
|
||||
the device are expecting to exchange.
|
||||
.sp
|
||||
The first mode is the \fIprotocol\fP mode which allows a client to interact
|
||||
with the relay, for example join the relay, or request to connect to a device,
|
||||
given it is available on the relay. Similarly to BEP, protocol mode requires
|
||||
the device to connect via TLS using a strong suite of ciphers (same as BEP),
|
||||
which allows the relay to verify and derive the identity (Device ID) of the
|
||||
device.
|
||||
.sp
|
||||
The second mode is the \fIsession\fP mode which after a few initial messages
|
||||
connects two devices directly to each other via the relay, and is a plain\-text
|
||||
protocol, which for every byte written by one device, sends the same set of
|
||||
bytes to the other device and vica versa.
|
||||
.SH IDENTIFYING THE CONNECTION MODE
|
||||
.sp
|
||||
Because both connection modes operate over the same single socket, a method
|
||||
of detecting the connection mode is required.
|
||||
.sp
|
||||
When a new client connects to the relay, the relay checks the first byte
|
||||
that the client has sent, and if that matches 0x16, that implies to us that
|
||||
the connection is a protocol mode connection, due to 0x16 being the first byte
|
||||
in the TLS handshake, and only protocol mode connections use TLS.
|
||||
.sp
|
||||
If the first byte is not 0x16, then we assume that the connection is a session
|
||||
mode connection.
|
||||
.SH PROTOCOL MODE
|
||||
.sp
|
||||
Protocol mode uses TLS and protocol name defined by the TLS header should be
|
||||
\fIbep\-relay\fP\&.
|
||||
.sp
|
||||
Protocol mode has two submodes:
|
||||
1. Permanent protocol submode \- Joining the relay, and waiting for messages from
|
||||
the relay asking to connect to some device which is interested in having a
|
||||
session with you.
|
||||
2. Temporary protocol submode \- Only used to request a session with a device
|
||||
which is connected to the relay using the permanent protocol submode.
|
||||
.SS Permanent protocol submode
|
||||
.sp
|
||||
A permanent protocol submode begins with the client sending a JoinRelayRequest
|
||||
message, which the relay responds to with either a ResponseSuccess or
|
||||
ResponseAlreadyConnected message if a client with the same device ID already
|
||||
exists.
|
||||
.sp
|
||||
After the client has joined, no more messages are exchanged apart from
|
||||
Ping/Pong messages for general connection keep alive checking.
|
||||
.sp
|
||||
From this point onwards, the client stand\-by\(aqs and waits for SessionInvitation
|
||||
messages from the relay, which implies that some other device is trying to
|
||||
connect with you. SessionInvitation message contains the unique session key
|
||||
which then can be used to establish a connection in session mode.
|
||||
.sp
|
||||
If the client fails to send a JoinRelayRequest message within the first ping
|
||||
interval, the connection is terminated.
|
||||
If the client fails to send a message (even if its a ping message) every minute
|
||||
(by default), the connection is terminated.
|
||||
.SS Temporary protocol submode
|
||||
.sp
|
||||
A temporary protocol submode begins with ConnectRequest message, to which the
|
||||
relay responds with either ResponseNotFound if the device the client it is after
|
||||
is not available, or with a SessionInvitation, which contains the unique session
|
||||
key which then can be used to establish a connection in session mode.
|
||||
.sp
|
||||
The connection is terminated immediately after that.
|
||||
.SS Example Exchange
|
||||
.sp
|
||||
Client A \- Permanent protocol submode
|
||||
Client B \- Temporary protocol submode
|
||||
.TS
|
||||
center;
|
||||
|l|l|l|l|.
|
||||
_
|
||||
T{
|
||||
#
|
||||
T} T{
|
||||
Client (A)
|
||||
T} T{
|
||||
Relay
|
||||
T} T{
|
||||
Client (B)
|
||||
T}
|
||||
_
|
||||
T{
|
||||
1
|
||||
T} T{
|
||||
JoinRelayRequest\->
|
||||
T} T{
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
2
|
||||
T} T{
|
||||
T} T{
|
||||
<\-ResponseSuccess
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
3
|
||||
T} T{
|
||||
Ping\->
|
||||
T} T{
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
4
|
||||
T} T{
|
||||
T} T{
|
||||
<\-Pong
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
5
|
||||
T} T{
|
||||
T} T{
|
||||
T} T{
|
||||
<\-ConnectRequest(A)
|
||||
T}
|
||||
_
|
||||
T{
|
||||
6
|
||||
T} T{
|
||||
T} T{
|
||||
SessionInvitation(A)\->
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
7
|
||||
T} T{
|
||||
T} T{
|
||||
<\-SessionInvitation(B)
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
8
|
||||
T} T{
|
||||
T} T{
|
||||
T} T{
|
||||
(Disconnects)
|
||||
T}
|
||||
_
|
||||
T{
|
||||
9
|
||||
T} T{
|
||||
Ping\->
|
||||
T} T{
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
10
|
||||
T} T{
|
||||
T} T{
|
||||
<\-Pong
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
11
|
||||
T} T{
|
||||
Ping\->
|
||||
T} T{
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
12
|
||||
T} T{
|
||||
T} T{
|
||||
<\-Pong
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
.TE
|
||||
.SH SESSION MODE
|
||||
.sp
|
||||
The first and only message the client sends in the session mode is the
|
||||
SessionInvitation message which contains the session key identifying which
|
||||
session you are trying to join. The relay responds with one of the following
|
||||
Response messages:
|
||||
.INDENT 0.0
|
||||
.IP 1. 3
|
||||
ResponseNotFound \- Session key is invalid
|
||||
.IP 2. 3
|
||||
ResponseAlreadyConnected \- Session is full (both sides already connected)
|
||||
.IP 3. 3
|
||||
ResponseSuccess \- You have successfully joined the session
|
||||
.UNINDENT
|
||||
.sp
|
||||
After the successful response, all the bytes written and received will be
|
||||
relayed between the two devices in the session directly.
|
||||
.SS Example Exchange
|
||||
.sp
|
||||
Client A \- Permanent protocol mode
|
||||
Client B \- Temporary protocol mode
|
||||
.TS
|
||||
center;
|
||||
|l|l|l|l|.
|
||||
_
|
||||
T{
|
||||
#
|
||||
T} T{
|
||||
Client (A)
|
||||
T} T{
|
||||
Relay
|
||||
T} T{
|
||||
Client (B)
|
||||
T}
|
||||
_
|
||||
T{
|
||||
1
|
||||
T} T{
|
||||
SessionInvitation(A)\->
|
||||
T} T{
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
2
|
||||
T} T{
|
||||
T} T{
|
||||
<\-ResponseSuccess
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
3
|
||||
T} T{
|
||||
Data\->
|
||||
T} T{
|
||||
(Buffers data)
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
4
|
||||
T} T{
|
||||
Data\->
|
||||
T} T{
|
||||
(Buffers data)
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
5
|
||||
T} T{
|
||||
T} T{
|
||||
T} T{
|
||||
<\-SessionInvitation(B)
|
||||
T}
|
||||
_
|
||||
T{
|
||||
6
|
||||
T} T{
|
||||
T} T{
|
||||
ResponseSuccess\->
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
7
|
||||
T} T{
|
||||
T} T{
|
||||
Relays data \->
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
8
|
||||
T} T{
|
||||
T} T{
|
||||
Relays data \->
|
||||
T} T{
|
||||
T}
|
||||
_
|
||||
T{
|
||||
9
|
||||
T} T{
|
||||
T} T{
|
||||
<\-Relays data
|
||||
T} T{
|
||||
<\-Data
|
||||
T}
|
||||
_
|
||||
.TE
|
||||
.SH MESSAGES
|
||||
.sp
|
||||
All messages are preceeded by a header message. Header message contains the
|
||||
magic value 0x9E79BC40, message type integer, and message length.
|
||||
.sp
|
||||
\fBWARNING:\fP
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
Some messages have no content, apart from the implied header which allows
|
||||
us to identify what type of message it is.
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS Header structure
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Magic |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Message Type |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Message Length |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
|
||||
struct Header {
|
||||
unsigned int Magic;
|
||||
int MessageType;
|
||||
int MessageLength;
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS Ping message (Type = 0)
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
|
||||
struct Ping {
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS Pong message (Type = 1)
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
|
||||
struct Pong {
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS JoinRelayRequest message (Type = 2)
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
|
||||
struct JoinRelayRequest {
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS JoinSessionRequest message (Type = 3)
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of Key |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Key (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
|
||||
struct JoinSessionRequest {
|
||||
opaque Key<32>;
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B : Key
|
||||
This is a unique random session key generated by the relay server. It is
|
||||
used to identify which session you are trying to connect to.
|
||||
.UNINDENT
|
||||
.SS Response message (Type = 4)
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Code |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of Message |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Message (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
|
||||
struct Response {
|
||||
int Code;
|
||||
string Message<>;
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B : Code
|
||||
An integer representing the status code.
|
||||
.TP
|
||||
.B : Message
|
||||
Message associated with the code.
|
||||
.UNINDENT
|
||||
.SS ConnectRequest message (Type = 5)
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of ID |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e ID (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
|
||||
struct ConnectRequest {
|
||||
opaque ID<32>;
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B : ID
|
||||
Device ID to which the client would like to connect.
|
||||
.UNINDENT
|
||||
.SS SessionInvitation message (Type = 6)
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of From |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e From (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of Key |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Key (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of Address |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Address (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| 0x0000 | Port |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Server Socket (V=0 or 1) |V|
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
|
||||
struct SessionInvitation {
|
||||
opaque From<32>;
|
||||
opaque Key<32>;
|
||||
opaque Address<32>;
|
||||
unsigned int Port;
|
||||
bool ServerSocket;
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B : From
|
||||
Device ID identifying who you will be connecting with.
|
||||
.TP
|
||||
.B : Key
|
||||
A unique random session key generated by the relay server. It is used to
|
||||
identify which session you are trying to connect to.
|
||||
.TP
|
||||
.B : Address
|
||||
An optional IP address on which the relay server is expecting you to
|
||||
connect, in order to start a connection in session mode.
|
||||
Empty/all zero IP should be replaced with the relay\(aqs public IP address that
|
||||
was used when establishing the protocol mode connection.
|
||||
.TP
|
||||
.B : Port
|
||||
An optional port on which the relay server is expecting you to connect,
|
||||
in order to start a connection in session mode.
|
||||
.TP
|
||||
.B : Server Socket
|
||||
Because both sides connecting to the relay use the client side of the socket,
|
||||
and some protocols behave differently depending if the connection starts on
|
||||
the server side or the client side, this boolean indicates which side of the
|
||||
connection this client should assume it\(aqs getting. The value is inverted in
|
||||
the invitation which is sent to the other device, so that there is always
|
||||
one client socket, and one server socket.
|
||||
.UNINDENT
|
||||
.SH HOW SYNCTHING USES RELAYS, AND GENERAL SECURITY
|
||||
.sp
|
||||
In the case of Syncthing and BEP, when two devices connect via relay, they
|
||||
start their standard TLS connection encapsulated within the relay\(aqs plain\-text
|
||||
session connection, effectively upgrading the plain\-text connection to a TLS
|
||||
connection.
|
||||
.sp
|
||||
Even though the relay could be used for man\-in\-the\-middle attack, using TLS
|
||||
at the application/BEP level ensures that all the traffic is safely encrypted,
|
||||
and is completely meaningless to the relay. Furthermore, the secure suite of
|
||||
ciphers used by BEP provides forward secrecy, meaning that even if the relay
|
||||
did capture all the traffic, and even if the attacker did get their hands on the
|
||||
device keys, they would still not be able to recover/decrypt any traffic which
|
||||
was transported via the relay.
|
||||
.sp
|
||||
After establishing a relay session, syncthing looks at the SessionInvitation
|
||||
message, and depending which side it has received, wraps the raw socket in
|
||||
either a TLS client socket or a TLS server socket depending on the ServerSocket
|
||||
boolean value in the SessionInvitation, and starts the TLS handshake.
|
||||
.sp
|
||||
From that point onwards it functions exactly the same way as if syncthing was
|
||||
establishing a direct connection with the other device over the internet,
|
||||
performing device ID validation, and full TLS encryption, and provides the same
|
||||
security properties as it would provide when connecting over the internet.
|
||||
.SH EXAMPLES OF STRONG CIPHER SUITES
|
||||
.TS
|
||||
center;
|
||||
|l|l|l|.
|
||||
_
|
||||
T{
|
||||
ID
|
||||
T} T{
|
||||
Name
|
||||
T} T{
|
||||
Description
|
||||
T}
|
||||
_
|
||||
T{
|
||||
0x009F
|
||||
T} T{
|
||||
DHE\-RSA\-AES256\-GCM\-SHA384
|
||||
T} T{
|
||||
TLSv1.2 DH RSA AESGCM(256) AEAD
|
||||
T}
|
||||
_
|
||||
T{
|
||||
0x006B
|
||||
T} T{
|
||||
DHE\-RSA\-AES256\-SHA256
|
||||
T} T{
|
||||
TLSv1.2 DH RSA AES(256) SHA256
|
||||
T}
|
||||
_
|
||||
T{
|
||||
0xC030
|
||||
T} T{
|
||||
ECDHE\-RSA\-AES256\-GCM\-SHA384
|
||||
T} T{
|
||||
TLSv1.2 ECDH RSA AESGCM(256) AEAD
|
||||
T}
|
||||
_
|
||||
T{
|
||||
0xC028
|
||||
T} T{
|
||||
ECDHE\-RSA\-AES256\-SHA384
|
||||
T} T{
|
||||
TLSv1.2 ECDH RSA AES(256) SHA384
|
||||
T}
|
||||
_
|
||||
T{
|
||||
0x009E
|
||||
T} T{
|
||||
DHE\-RSA\-AES128\-GCM\-SHA256
|
||||
T} T{
|
||||
TLSv1.2 DH RSA AESGCM(128) AEAD
|
||||
T}
|
||||
_
|
||||
T{
|
||||
0x0067
|
||||
T} T{
|
||||
DHE\-RSA\-AES128\-SHA256
|
||||
T} T{
|
||||
TLSv1.2 DH RSA AES(128) SHA256
|
||||
T}
|
||||
_
|
||||
T{
|
||||
0xC02F
|
||||
T} T{
|
||||
ECDHE\-RSA\-AES128\-GCM\-SHA256
|
||||
T} T{
|
||||
TLSv1.2 ECDH RSA AESGCM(128) AEAD
|
||||
T}
|
||||
_
|
||||
T{
|
||||
0xC027
|
||||
T} T{
|
||||
ECDHE\-RSA\-AES128\-SHA256
|
||||
T} T{
|
||||
TLSv1.2 ECDH RSA AES(128) SHA256
|
||||
T}
|
||||
_
|
||||
.TE
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2015, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-REST-API" "7" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-SECURITY" "7" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.
|
||||
@@ -115,7 +115,7 @@ certificates are sent in clear text (as in HTTPS etc), meaning that the
|
||||
certificate Common Name (by default \fBsyncthing\fP) is visible.
|
||||
.sp
|
||||
An eavesdropper can deduce that this is a Syncthing connection and calculate the
|
||||
device ID:s involved based on the hashes of the sent certificates.
|
||||
device IDs involved based on the hashes of the sent certificates.
|
||||
.sp
|
||||
Likewise, if the sync port (default 22000) is accessible from the internet, a
|
||||
port scanner may discover it, attempt a TLS negotiation and thus obtain the
|
||||
@@ -129,7 +129,7 @@ web GUI defaults to being reachable from the \fBlocal host only\fP\&.
|
||||
.sp
|
||||
Parties doing surveillance on your network (whether that be corporate IT, the
|
||||
NSA or someone else) will be able to see that you use Syncthing, and your device
|
||||
ID\(aqs \fI\%are OK to share anyway\fP <\fBhttp://docs.syncthing.net/users/faq.html#should-i-keep-my-device-ids-secret\fP>,
|
||||
IDs \fI\%are OK to share anyway\fP <\fBhttp://docs.syncthing.net/users/faq.html#should-i-keep-my-device-ids-secret\fP>,
|
||||
but the actual transmitted data is protected as well as we can. Knowing your
|
||||
device ID can expose your IP address, using global discovery.
|
||||
.SH PROTECTING YOUR SYNCTHING KEYS AND IDENTITY
|
||||
@@ -141,8 +141,6 @@ synced files. Here are some general principles to protect your files:
|
||||
.IP 1. 3
|
||||
If a device of yours is lost, make sure to revoke its access from your other
|
||||
devices.
|
||||
.UNINDENT
|
||||
.INDENT 0.0
|
||||
.IP 2. 3
|
||||
If you\(aqre syncing confidential data on an encrypted disk to guard against
|
||||
device theft, put the Syncthing config folder on the same encrypted disk to
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-STIGNORE" "5" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-stignore \- Prevent files from being synchronized to other nodes
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "TODO" "7" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "TODO" "7" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
Todo \- Keep automatic backups of deleted files by other nodes
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING" "1" "December 01, 2015" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING" "1" "December 21, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing \- Syncthing
|
||||
.
|
||||
|
||||
Reference in New Issue
Block a user