Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a506e72b | ||
|
|
25a7b0a6f8 | ||
|
|
7aaa1dd8a3 | ||
|
|
2a6f164923 | ||
|
|
0f28626bb4 | ||
|
|
6ed22d0885 | ||
|
|
6715b91a6c | ||
|
|
694da60659 | ||
|
|
47fa4b0a2c | ||
|
|
8ab6b60778 | ||
|
|
e1a4f81e50 | ||
|
|
7b7e35d339 | ||
|
|
3176629410 | ||
|
|
e3ccc45d19 | ||
|
|
beec9e834e | ||
|
|
f6f0486ff9 | ||
|
|
518f446d31 | ||
|
|
fbbd510088 | ||
|
|
e440d30028 | ||
|
|
44d30c83bf | ||
|
|
7ff7b55732 | ||
|
|
44346b3a5a | ||
|
|
23a538d61a | ||
|
|
dcb5026f33 | ||
|
|
778ff9daa9 | ||
|
|
ce9dc809bc | ||
|
|
59370588dd | ||
|
|
7d434aa9c4 | ||
|
|
59ce7c0424 | ||
|
|
9a0e5a7c18 | ||
|
|
8d0019595f | ||
|
|
6ff74cfcab | ||
|
|
aa50ef4069 | ||
|
|
fa0101bd60 | ||
|
|
21f5b16e47 | ||
|
|
223a835f33 | ||
|
|
223e14b0d0 | ||
|
|
a58f69be04 | ||
|
|
e194eb1f69 | ||
|
|
672824641b | ||
|
|
6d357211b2 | ||
|
|
8e39e2889d | ||
|
|
a9ee4bb9f1 | ||
|
|
80fd6c2400 | ||
|
|
3cbe7d40d1 | ||
|
|
af0bc95de5 | ||
|
|
4bf3e7485b | ||
|
|
b701de60ce | ||
|
|
7ef2743964 | ||
|
|
a165838cbd | ||
|
|
3c77b8388c | ||
|
|
9d16f4545d | ||
|
|
d57e6808cc | ||
|
|
b71cc8a580 | ||
|
|
ac3b03881a | ||
|
|
b0d03d1f1c | ||
|
|
a2dcffcca2 | ||
|
|
9323f0faf8 | ||
|
|
f343c8ba36 | ||
|
|
502bee9a09 | ||
|
|
379e2119a8 | ||
|
|
89a29946f9 | ||
|
|
20a94fafa7 | ||
|
|
99ddf1e4ab | ||
|
|
fb778218f5 | ||
|
|
55fc3cb2c5 | ||
|
|
b779e22205 | ||
|
|
e9063c639a | ||
|
|
8d6dedc15b | ||
|
|
1bc4c1a8ac | ||
|
|
1a35c440e8 | ||
|
|
2c6c84ac61 | ||
|
|
bd666daf82 | ||
|
|
ca3831c4f5 | ||
|
|
bbe0d34f43 | ||
|
|
dd364c962f | ||
|
|
50068b0b0f | ||
|
|
175769b53e | ||
|
|
07722dc33d | ||
|
|
f39f816a98 | ||
|
|
845f31b98f | ||
|
|
89b6c32cee | ||
|
|
6ee36fe361 | ||
|
|
b61d7c2428 | ||
|
|
bcc5d7c00f | ||
|
|
925f60d9c3 | ||
|
|
8b3f5fda07 | ||
|
|
ac17b2c584 | ||
|
|
c67c861dc6 | ||
|
|
09ba9e6259 | ||
|
|
0e167f5c24 | ||
|
|
c885903ff2 | ||
|
|
a91a836224 | ||
|
|
8450ab8dab | ||
|
|
168889d999 | ||
|
|
e1339628d9 | ||
|
|
425f61cf34 | ||
|
|
7d9df5abc6 | ||
|
|
118cba4d9b | ||
|
|
3cacb48f3c | ||
|
|
6965812d79 | ||
|
|
78fb7fe9f9 | ||
|
|
d7c8075862 |
4
.gitignore
vendored
@@ -1,7 +1,7 @@
|
||||
/syncthing
|
||||
/discosrv
|
||||
/stdiscosrv
|
||||
syncthing.exe
|
||||
discosrv.exe
|
||||
stdiscosrv.exe
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.asc
|
||||
|
||||
1
AUTHORS
@@ -27,6 +27,7 @@ Brian R. Becker (brbecker) <brbecker@gmail.com>
|
||||
Caleb Callaway (cqcallaw) <enlightened.despot@gmail.com>
|
||||
Carsten Hagemann (Moter8) <moter8@gmail.com>
|
||||
Cathryne Linenweaver (Cathryne) <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
|
||||
Cedric Staniewski (xduugu) <cedric@gmx.ca>
|
||||
Chris Howie (cdhowie) <me@chrishowie.com>
|
||||
Chris Joel (cdata) <chris@scriptolo.gy>
|
||||
Colin Kennedy (moshen) <moshen.colin@gmail.com>
|
||||
|
||||
1
NICKS
@@ -110,5 +110,6 @@ wkennington <william@wkennington.com>
|
||||
WSGCSysadmin <e.meitner@willystreet.coop>
|
||||
wweich <wweich@users.noreply.github.com>
|
||||
wweich <wweich@gmx.de>
|
||||
xduugu <cedric@gmx.ca>
|
||||
Zillode <zillode@zillode.be>
|
||||
zukoo <fxgsell@gmail.com>
|
||||
|
||||
@@ -27,6 +27,11 @@ There are a few examples for keeping Syncthing running in the background
|
||||
on your system in [the etc directory][3]. There are also several [GUI
|
||||
implementations][11] for Windows, Mac and Linux.
|
||||
|
||||
## Vote on features/bugs
|
||||
|
||||
We'd like to encourage you to [vote][12] on issues that matter to you.
|
||||
This helps the team understand what are the biggest pain points for our users, and could potentially influence what is being worked on next.
|
||||
|
||||
## Getting in Touch
|
||||
|
||||
The first and best point of contact is the [Forum][8]. There is also an IRC
|
||||
@@ -66,3 +71,4 @@ All code is licensed under the [MPLv2 License][7].
|
||||
[9]: https://kiwiirc.com/client/irc.freenode.net/#syncthing
|
||||
[10]: https://github.com/syncthing/syncthing/issues
|
||||
[11]: http://docs.syncthing.net/users/contrib.html#gui-wrappers
|
||||
[12]: https://www.bountysource.com/teams/syncthing/issues
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.2 KiB |
120
build.go
@@ -96,38 +96,55 @@ var targets = map[string]target{
|
||||
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
|
||||
},
|
||||
},
|
||||
"discosrv": {
|
||||
name: "discosrv",
|
||||
buildPkg: "./cmd/discosrv",
|
||||
binaryName: "discosrv", // .exe will be added automatically for Windows builds
|
||||
"stdiscosrv": {
|
||||
name: "stdiscosrv",
|
||||
buildPkg: "./cmd/stdiscosrv",
|
||||
binaryName: "stdiscosrv", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "cmd/discosrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/discosrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "cmd/stdiscosrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/stdiscosrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
debianFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||
{src: "cmd/discosrv/README.md", dst: "deb/usr/share/doc/discosrv/README.txt", perm: 0644},
|
||||
{src: "cmd/discosrv/LICENSE", dst: "deb/usr/share/doc/discosrv/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/discosrv/AUTHORS.txt", perm: 0644},
|
||||
{src: "cmd/stdiscosrv/README.md", dst: "deb/usr/share/doc/stdiscosrv/README.txt", perm: 0644},
|
||||
{src: "cmd/stdiscosrv/LICENSE", dst: "deb/usr/share/doc/stdiscosrv/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/stdiscosrv/AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
tags: []string{"purego"},
|
||||
},
|
||||
"relaysrv": {
|
||||
name: "relaysrv",
|
||||
buildPkg: "./cmd/relaysrv",
|
||||
binaryName: "relaysrv", // .exe will be added automatically for Windows builds
|
||||
"strelaysrv": {
|
||||
name: "strelaysrv",
|
||||
buildPkg: "./cmd/strelaysrv",
|
||||
binaryName: "strelaysrv", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "cmd/relaysrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/relaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "cmd/strelaysrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/strelaysrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
debianFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||
{src: "cmd/relaysrv/README.md", dst: "deb/usr/share/doc/relaysrv/README.txt", perm: 0644},
|
||||
{src: "cmd/relaysrv/LICENSE", dst: "deb/usr/share/doc/relaysrv/LICENSE.txt", perm: 0644},
|
||||
{src: "cmd/strelaysrv/README.md", dst: "deb/usr/share/doc/strelaysrv/README.txt", perm: 0644},
|
||||
{src: "cmd/strelaysrv/LICENSE", dst: "deb/usr/share/doc/strelaysrv/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/strelaysrv/AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
},
|
||||
"strelaypoolsrv": {
|
||||
name: "strelaypoolsrv",
|
||||
buildPkg: "./cmd/strelaypoolsrv",
|
||||
binaryName: "strelaypoolsrv", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "cmd/strelaypoolsrv/README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "cmd/strelaypoolsrv/LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
debianFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||
{src: "cmd/strelaypoolsrv/README.md", dst: "deb/usr/share/doc/relaysrv/README.txt", perm: 0644},
|
||||
{src: "cmd/strelaypoolsrv/LICENSE", dst: "deb/usr/share/doc/relaysrv/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/relaysrv/AUTHORS.txt", perm: 0644},
|
||||
},
|
||||
},
|
||||
@@ -238,8 +255,8 @@ func runCommand(cmd string, target target) {
|
||||
case "assets":
|
||||
rebuildAssets()
|
||||
|
||||
case "xdr":
|
||||
xdr()
|
||||
case "proto":
|
||||
proto()
|
||||
|
||||
case "translate":
|
||||
translate()
|
||||
@@ -271,9 +288,12 @@ func runCommand(cmd string, target target) {
|
||||
case "metalint":
|
||||
if isGometalinterInstalled() {
|
||||
dirs := []string{".", "./cmd/...", "./lib/..."}
|
||||
gometalinter("deadcode", dirs, "test/util.go")
|
||||
gometalinter("structcheck", dirs)
|
||||
gometalinter("varcheck", dirs)
|
||||
ok := gometalinter("deadcode", dirs, "test/util.go")
|
||||
ok = gometalinter("structcheck", dirs) && ok
|
||||
ok = gometalinter("varcheck", dirs) && ok
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -543,16 +563,17 @@ func listFiles(dir string) []string {
|
||||
|
||||
func rebuildAssets() {
|
||||
runPipe("lib/auto/gui.files.go", "go", "run", "script/genassets.go", "gui")
|
||||
runPipe("cmd/strelaypoolsrv/auto/gui.go", "go", "run", "script/genassets.go", "cmd/strelaypoolsrv/gui")
|
||||
}
|
||||
|
||||
func lazyRebuildAssets() {
|
||||
if shouldRebuildAssets() {
|
||||
if shouldRebuildAssets("lib/auto/gui.files.go", "gui") || shouldRebuildAssets("cmd/strelaypoolsrv/auto/gui.go", "cmd/strelaypoolsrv/auto/gui") {
|
||||
rebuildAssets()
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRebuildAssets() bool {
|
||||
info, err := os.Stat("lib/auto/gui.files.go")
|
||||
func shouldRebuildAssets(target, srcdir string) bool {
|
||||
info, err := os.Stat(target)
|
||||
if err != nil {
|
||||
// If the file doesn't exist, we must rebuild it
|
||||
return true
|
||||
@@ -562,7 +583,7 @@ func shouldRebuildAssets() bool {
|
||||
// so we should rebuild it.
|
||||
currentBuild := info.ModTime()
|
||||
assetsAreNewer := false
|
||||
filepath.Walk("gui", func(path string, info os.FileInfo, err error) error {
|
||||
filepath.Walk(srcdir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -576,8 +597,8 @@ func shouldRebuildAssets() bool {
|
||||
return assetsAreNewer
|
||||
}
|
||||
|
||||
func xdr() {
|
||||
runPrint("go", "generate", "./lib/discover", "./lib/db", "./lib/protocol", "./lib/relay/protocol")
|
||||
func proto() {
|
||||
runPrint("go", "generate", "./lib/...")
|
||||
}
|
||||
|
||||
func translate() {
|
||||
@@ -717,10 +738,18 @@ func getBranchSuffix() string {
|
||||
}
|
||||
|
||||
func buildStamp() int64 {
|
||||
// If SOURCE_DATE_EPOCH is set, use that.
|
||||
if s, _ := strconv.ParseInt(os.Getenv("SOURCE_DATE_EPOCH"), 10, 64); s > 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
// Try to get the timestamp of the latest commit.
|
||||
bs, err := runError("git", "show", "-s", "--format=%ct")
|
||||
if err != nil {
|
||||
// Fall back to "now".
|
||||
return time.Now().Unix()
|
||||
}
|
||||
|
||||
s, _ := strconv.ParseInt(string(bs), 10, 64)
|
||||
return s
|
||||
}
|
||||
@@ -879,7 +908,7 @@ func zipFile(out string, files []archiveFile) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fh.Name = f.dst
|
||||
fh.Name = filepath.ToSlash(f.dst)
|
||||
fh.Method = zip.Deflate
|
||||
|
||||
if strings.HasSuffix(f.dst, ".txt") {
|
||||
@@ -948,13 +977,17 @@ func lint(pkg string) {
|
||||
}
|
||||
|
||||
analCommentPolicy := regexp.MustCompile(`exported (function|method|const|type|var) [^\s]+ should have comment`)
|
||||
for _, line := range bytes.Split(bs, []byte("\n")) {
|
||||
if analCommentPolicy.Match(line) {
|
||||
for _, line := range strings.Split(string(bs), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if len(line) > 0 {
|
||||
log.Printf("%s", line)
|
||||
if analCommentPolicy.MatchString(line) {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, ".pb.go:") {
|
||||
continue
|
||||
}
|
||||
log.Println(line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -995,7 +1028,7 @@ func isGometalinterInstalled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func gometalinter(linter string, dirs []string, excludes ...string) {
|
||||
func gometalinter(linter string, dirs []string, excludes ...string) bool {
|
||||
params := []string{"--disable-all"}
|
||||
params = append(params, fmt.Sprintf("--deadline=%ds", 60))
|
||||
params = append(params, "--enable="+linter)
|
||||
@@ -1008,12 +1041,19 @@ func gometalinter(linter string, dirs []string, excludes ...string) {
|
||||
params = append(params, dir)
|
||||
}
|
||||
|
||||
bs, err := runError("gometalinter", params...)
|
||||
bs, _ := runError("gometalinter", params...)
|
||||
|
||||
if len(bs) > 0 {
|
||||
log.Printf("%s", bs)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
nerr := 0
|
||||
for _, line := range strings.Split(string(bs), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(line, ".pb.go:") {
|
||||
continue
|
||||
}
|
||||
log.Println(line)
|
||||
nerr++
|
||||
}
|
||||
|
||||
return nerr == 0
|
||||
}
|
||||
|
||||
49
build.sh
@@ -131,55 +131,6 @@ case "${1:-default}" in
|
||||
go2xunit -output tests.xml -fail < tests.out
|
||||
;;
|
||||
|
||||
docker-all)
|
||||
img=${DOCKERIMG:-syncthing/build:latest}
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
-e "STTRACE=$STTRACE" \
|
||||
"$img" \
|
||||
sh -c './build.sh clean \
|
||||
&& ./build.sh test-cov \
|
||||
&& ./build.sh bench \
|
||||
&& ./build.sh all'
|
||||
;;
|
||||
|
||||
docker-test)
|
||||
img=${DOCKERIMG:-syncthing/build:latest}
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
-e "STTRACE=$STTRACE" \
|
||||
"$img" \
|
||||
sh -euxc './build.sh clean \
|
||||
&& go run build.go -race \
|
||||
&& export GOPATH=$(pwd)/Godeps/_workspace:$GOPATH \
|
||||
&& cd test \
|
||||
&& go test -tags integration -v -timeout 90m -short \
|
||||
&& git clean -fxd .'
|
||||
;;
|
||||
|
||||
docker-lint)
|
||||
img=${DOCKERIMG:-syncthing/build:latest}
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
-e "STTRACE=$STTRACE" \
|
||||
"$img" \
|
||||
sh -euxc 'go run build.go lint'
|
||||
;;
|
||||
|
||||
|
||||
docker-vet)
|
||||
img=${DOCKERIMG:-syncthing/build:latest}
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
-e "STTRACE=$STTRACE" \
|
||||
"$img" \
|
||||
sh -euxc 'go run build.go vet'
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown build command $1"
|
||||
;;
|
||||
|
||||
@@ -9,6 +9,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"log"
|
||||
"strings"
|
||||
@@ -66,24 +67,25 @@ func recv(bc beacon.Interface) {
|
||||
seen := make(map[string]bool)
|
||||
for {
|
||||
data, src := bc.Recv()
|
||||
var ann discover.Announce
|
||||
ann.UnmarshalXDR(data)
|
||||
if m := binary.BigEndian.Uint32(data); m != discover.Magic {
|
||||
log.Printf("Incorrect magic %x in announcement from %v", m, src)
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.Equal(ann.This.ID, myID) {
|
||||
var ann discover.Announce
|
||||
ann.Unmarshal(data[4:])
|
||||
|
||||
if bytes.Equal(ann.ID, myID) {
|
||||
// This is one of our own fake packets, don't print it.
|
||||
continue
|
||||
}
|
||||
|
||||
// Print announcement details for the first packet from a given
|
||||
// device ID and source address, or if -all was given.
|
||||
key := string(ann.This.ID) + src.String()
|
||||
key := string(ann.ID) + src.String()
|
||||
if all || !seen[key] {
|
||||
log.Printf("Announcement from %v\n", src)
|
||||
log.Printf(" %v at %s\n", protocol.DeviceIDFromBytes(ann.This.ID), strings.Join(addrStrs(ann.This), ", "))
|
||||
|
||||
for _, dev := range ann.Extra {
|
||||
log.Printf(" %v at %s\n", protocol.DeviceIDFromBytes(dev.ID), strings.Join(addrStrs(dev), ", "))
|
||||
}
|
||||
log.Printf(" %v at %s\n", protocol.DeviceIDFromBytes(ann.ID), strings.Join(ann.Addresses, ", "))
|
||||
seen[key] = true
|
||||
}
|
||||
}
|
||||
@@ -92,15 +94,10 @@ func recv(bc beacon.Interface) {
|
||||
// sends fake discovery announcements once every second
|
||||
func send(bc beacon.Interface) {
|
||||
ann := discover.Announce{
|
||||
Magic: discover.AnnouncementMagic,
|
||||
This: discover.Device{
|
||||
ID: myID,
|
||||
Addresses: []discover.Address{
|
||||
{URL: "tcp://fake.example.com:12345"},
|
||||
},
|
||||
},
|
||||
ID: myID,
|
||||
Addresses: []string{"tcp://fake.example.com:12345"},
|
||||
}
|
||||
bs, _ := ann.MarshalXDR()
|
||||
bs, _ := ann.Marshal()
|
||||
|
||||
for {
|
||||
bc.Send(bs)
|
||||
@@ -108,15 +105,6 @@ func send(bc beacon.Interface) {
|
||||
}
|
||||
}
|
||||
|
||||
// returns the list of address URLs
|
||||
func addrStrs(dev discover.Device) []string {
|
||||
ss := make([]string, len(dev.Addresses))
|
||||
for i, addr := range dev.Addresses {
|
||||
ss[i] = addr.URL
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
// returns a random but recognizable device ID
|
||||
func randomDeviceID() []byte {
|
||||
var id [32]byte
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
discosrv
|
||||
========
|
||||
stdiscosrv
|
||||
==========
|
||||
|
||||
[](http://build.syncthing.net/job/discosrv/lastBuild/)
|
||||
[](http://build.syncthing.net/job/stdiscosrv/lastBuild/)
|
||||
|
||||
This is the global discovery server for the `syncthing` project.
|
||||
|
||||
To get it, run `go get github.com/syncthing/discosrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/discosrv/lastSuccessfulBuild/artifact/)
|
||||
To get it, run `go get github.com/syncthing/stdiscosrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/stdiscosrv/lastSuccessfulBuild/artifact/)
|
||||
from the build server.
|
||||
|
||||
Usage
|
||||
@@ -19,15 +19,15 @@ By default it will use in-memory `ql` backend. If you wish to persist the
|
||||
information on disk between restarts in `ql`, specify a file DSN:
|
||||
|
||||
```bash
|
||||
$ discosrv -db-dsn="file:///var/run/discosrv.db"
|
||||
$ stdiscosrv -db-dsn="file:///var/run/stdiscosrv.db"
|
||||
```
|
||||
|
||||
For `postgres`, you will need to create a database and a user with permissions
|
||||
to create tables in it, then start the discosrv as follows:
|
||||
to create tables in it, then start the stdiscosrv as follows:
|
||||
|
||||
```bash
|
||||
$ export DISCOSRV_DB_DSN="postgres://user:password@localhost/databasename"
|
||||
$ discosrv -db-backend="postgres"
|
||||
$ export STDISCOSRV_DB_DSN="postgres://user:password@localhost/databasename"
|
||||
$ stdiscosrv -db-backend="postgres"
|
||||
```
|
||||
|
||||
You can pass the DSN as command line option, but the value what you pass in will
|
||||
@@ -37,4 +37,4 @@ to other users.
|
||||
In all cases, the appropriate tables and indexes will be created at first
|
||||
startup. If it doesn't exit with an error, you're fine.
|
||||
|
||||
See `discosrv -help` for other options.
|
||||
See `stdiscosrv -help` for other options.
|
||||
@@ -38,7 +38,7 @@ func init() {
|
||||
BuildDate = time.Unix(int64(stamp), 0)
|
||||
|
||||
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
|
||||
LongVersion = fmt.Sprintf(`discosrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
LongVersion = fmt.Sprintf(`stdiscosrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -48,7 +48,7 @@ var (
|
||||
globalStats stats
|
||||
statsFile string
|
||||
backend = "ql"
|
||||
dsn = getEnvDefault("DISCOSRV_DB_DSN", "memory://discosrv")
|
||||
dsn = getEnvDefault("STDISCOSRV_DB_DSN", "memory://stdiscosrv")
|
||||
certFile = "cert.pem"
|
||||
keyFile = "key.pem"
|
||||
debug = false
|
||||
@@ -28,7 +28,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row := db.QueryRow(`SELECT 'DevicesDeviceIDIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX DevicesDeviceIDIndex ON Devices (DeviceID)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -36,7 +36,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'DevicesSeenIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX DevicesSeenIndex ON Devices (Seen)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -53,7 +53,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'AddressesDeviceIDSeenIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDSeenIndex ON Addresses (DeviceID, Seen)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -61,7 +61,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'AddressesDeviceIDAddressIndex'::regclass`)
|
||||
if err := row.Scan(nil); err != nil {
|
||||
if err = row.Scan(nil); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDAddressIndex ON Addresses (DeviceID, Address)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -330,6 +330,16 @@ func (s *querysrv) handleAnnounce(ctx context.Context, remote net.IP, deviceID p
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if host == "" || ip.IsUnspecified() {
|
||||
// Do not use IPv6 remote address if requested scheme is tcp4
|
||||
if uri.Scheme == "tcp4" && remote.To4() == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Do not use IPv4 remote address if requested scheme is tcp6
|
||||
if uri.Scheme == "tcp6" && remote.To4() != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
host = remote.String()
|
||||
}
|
||||
|
||||
@@ -13,50 +13,61 @@ import (
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
func dump(ldb *leveldb.DB) {
|
||||
func dump(ldb *db.Instance) {
|
||||
it := ldb.NewIterator(nil, nil)
|
||||
var dev protocol.DeviceID
|
||||
for it.Next() {
|
||||
key := it.Key()
|
||||
switch key[0] {
|
||||
case db.KeyTypeDevice:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
devBytes := key[1+64 : 1+64+32]
|
||||
name := nulString(key[1+64+32:])
|
||||
copy(dev[:], devBytes)
|
||||
fmt.Printf("[device] F:%q N:%q D:%v\n", folder, name, dev)
|
||||
folder := binary.BigEndian.Uint32(key[1:])
|
||||
device := binary.BigEndian.Uint32(key[1+4:])
|
||||
name := nulString(key[1+4+4:])
|
||||
fmt.Printf("[device] F:%d D:%d N:%q", folder, device, name)
|
||||
|
||||
var f protocol.FileInfo
|
||||
err := f.UnmarshalXDR(it.Value())
|
||||
err := f.Unmarshal(it.Value())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf(" N:%q\n F:%#o\n M:%d\n V:%v\n S:%d\n B:%d\n", f.Name, f.Flags, f.Modified, f.Version, f.Size(), len(f.Blocks))
|
||||
fmt.Printf(" V:%v\n", f)
|
||||
|
||||
case db.KeyTypeGlobal:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
name := nulString(key[1+64:])
|
||||
folder := binary.BigEndian.Uint32(key[1:])
|
||||
name := nulString(key[1+4:])
|
||||
var flv db.VersionList
|
||||
flv.UnmarshalXDR(it.Value())
|
||||
fmt.Printf("[global] F:%q N:%q V: %s\n", folder, name, flv)
|
||||
flv.Unmarshal(it.Value())
|
||||
fmt.Printf("[global] F:%d N:%q V:%s\n", folder, name, flv)
|
||||
|
||||
case db.KeyTypeBlock:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
hash := key[1+64 : 1+64+32]
|
||||
name := nulString(key[1+64+32:])
|
||||
fmt.Printf("[block] F:%q H:%x N:%q I:%d\n", folder, hash, name, binary.BigEndian.Uint32(it.Value()))
|
||||
folder := binary.BigEndian.Uint32(key[1:])
|
||||
hash := key[1+4 : 1+4+32]
|
||||
name := nulString(key[1+4+32:])
|
||||
fmt.Printf("[block] F:%d H:%x N:%q I:%d\n", folder, hash, name, binary.BigEndian.Uint32(it.Value()))
|
||||
|
||||
case db.KeyTypeDeviceStatistic:
|
||||
fmt.Printf("[dstat]\n %x\n %x\n", it.Key(), it.Value())
|
||||
fmt.Printf("[dstat] K:%x V:%x\n", it.Key(), it.Value())
|
||||
|
||||
case db.KeyTypeFolderStatistic:
|
||||
fmt.Printf("[fstat]\n %x\n %x\n", it.Key(), it.Value())
|
||||
fmt.Printf("[fstat] K:%x V:%x\n", it.Key(), it.Value())
|
||||
|
||||
case db.KeyTypeVirtualMtime:
|
||||
fmt.Printf("[mtime]\n %x\n %x\n", it.Key(), it.Value())
|
||||
fmt.Printf("[mtime] K:%x V:%x\n", it.Key(), it.Value())
|
||||
|
||||
case db.KeyTypeFolderIdx:
|
||||
key := binary.BigEndian.Uint32(it.Key()[1:])
|
||||
fmt.Printf("[folderidx] K:%d V:%q\n", key, it.Value())
|
||||
|
||||
case db.KeyTypeDeviceIdx:
|
||||
key := binary.BigEndian.Uint32(it.Key()[1:])
|
||||
val := it.Value()
|
||||
if len(val) == 0 {
|
||||
fmt.Printf("[deviceidx] K:%d V:<nil>\n", key)
|
||||
} else {
|
||||
dev := protocol.DeviceIDFromBytes(val)
|
||||
fmt.Printf("[deviceidx] K:%d V:%s\n", key, dev)
|
||||
}
|
||||
|
||||
default:
|
||||
fmt.Printf("[???]\n %x\n %x\n", it.Key(), it.Value())
|
||||
|
||||
@@ -8,11 +8,10 @@ package main
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type SizedElement struct {
|
||||
@@ -38,33 +37,31 @@ func (h *ElementHeap) Pop() interface{} {
|
||||
return x
|
||||
}
|
||||
|
||||
func dumpsize(ldb *leveldb.DB) {
|
||||
func dumpsize(ldb *db.Instance) {
|
||||
h := &ElementHeap{}
|
||||
heap.Init(h)
|
||||
|
||||
it := ldb.NewIterator(nil, nil)
|
||||
var dev protocol.DeviceID
|
||||
var ele SizedElement
|
||||
for it.Next() {
|
||||
key := it.Key()
|
||||
switch key[0] {
|
||||
case db.KeyTypeDevice:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
devBytes := key[1+64 : 1+64+32]
|
||||
name := nulString(key[1+64+32:])
|
||||
copy(dev[:], devBytes)
|
||||
ele.key = fmt.Sprintf("DEVICE:%s:%s:%s", dev, folder, name)
|
||||
folder := binary.BigEndian.Uint32(key[1:])
|
||||
device := binary.BigEndian.Uint32(key[1+4:])
|
||||
name := nulString(key[1+4+4:])
|
||||
ele.key = fmt.Sprintf("DEVICE:%d:%d:%s", folder, device, name)
|
||||
|
||||
case db.KeyTypeGlobal:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
name := nulString(key[1+64:])
|
||||
ele.key = fmt.Sprintf("GLOBAL:%s:%s", folder, name)
|
||||
folder := binary.BigEndian.Uint32(key[1:])
|
||||
name := nulString(key[1+4:])
|
||||
ele.key = fmt.Sprintf("GLOBAL:%d:%s", folder, name)
|
||||
|
||||
case db.KeyTypeBlock:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
hash := key[1+64 : 1+64+32]
|
||||
name := nulString(key[1+64+32:])
|
||||
ele.key = fmt.Sprintf("BLOCK:%s:%x:%s", folder, hash, name)
|
||||
folder := binary.BigEndian.Uint32(key[1:])
|
||||
hash := key[1+4 : 1+4+32]
|
||||
name := nulString(key[1+4+32:])
|
||||
ele.key = fmt.Sprintf("BLOCK:%d:%x:%s", folder, hash, name)
|
||||
|
||||
case db.KeyTypeDeviceStatistic:
|
||||
ele.key = fmt.Sprintf("DEVICESTATS:%s", key[1:])
|
||||
@@ -75,6 +72,14 @@ func dumpsize(ldb *leveldb.DB) {
|
||||
case db.KeyTypeVirtualMtime:
|
||||
ele.key = fmt.Sprintf("MTIME:%s", key[1:])
|
||||
|
||||
case db.KeyTypeFolderIdx:
|
||||
id := binary.BigEndian.Uint32(key[1:])
|
||||
ele.key = fmt.Sprintf("FOLDERIDX:%d", id)
|
||||
|
||||
case db.KeyTypeDeviceIdx:
|
||||
id := binary.BigEndian.Uint32(key[1:])
|
||||
ele.key = fmt.Sprintf("DEVICEIDX:%d", id)
|
||||
|
||||
default:
|
||||
ele.key = fmt.Sprintf("UNKNOWN:%x", key)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -28,16 +27,12 @@ func main() {
|
||||
|
||||
path := flag.Arg(0)
|
||||
if path == "" {
|
||||
path = filepath.Join(defaultConfigDir(), "index-v0.11.0.db")
|
||||
path = filepath.Join(defaultConfigDir(), "index-v0.14.0.db")
|
||||
}
|
||||
|
||||
fmt.Println("Path:", path)
|
||||
|
||||
ldb, err := leveldb.OpenFile(path, &opt.Options{
|
||||
ErrorIfMissing: true,
|
||||
Strict: opt.StrictAll,
|
||||
OpenFilesCacheCapacity: 100,
|
||||
})
|
||||
ldb, err := db.Open(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
15
cmd/strelaypoolsrv/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# relaypoolsrv
|
||||
|
||||
[](http://build.syncthing.net/job/relaypoolsrv/lastBuild/)
|
||||
|
||||
This is the relay pool server for the `syncthing` project, which allows community hosted [relaysrv](https://github.com/syncthing/relaysrv)'s to join the public pool.
|
||||
|
||||
Servers that join the pool are then advertised to users of `syncthing` as potential connection points for those who are unable to connect directly due to NAT or firewall issues.
|
||||
|
||||
There is very little reason why you'd want to run this yourself, as `relaypoolsrv` is just used for announcement and lookup of public relay servers. If you are looking to setup a private or a public relay, please check the documentation for [relaysrv](https://github.com/syncthing/relaysrv), which also explains how to join the default public pool.
|
||||
|
||||
If you still want to run it, you can run `go get github.com/syncthing/relaypoolsrv` download it or download the
|
||||
[latest build](http://build.syncthing.net/job/relaypoolsrv/lastSuccessfulBuild/artifact/)
|
||||
from the build server.
|
||||
|
||||
See `relaypoolsrv -help` for configuration options.
|
||||
1
cmd/strelaypoolsrv/auto/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
gui.go
|
||||
395
cmd/strelaypoolsrv/gui/index.html
Normal file
@@ -0,0 +1,395 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" ng-app="syncthing" ng-controller="relayDataController">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
|
||||
<title>Relay stats</title>
|
||||
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.6.1/css/font-awesome.min.css">
|
||||
|
||||
<style>
|
||||
#map {
|
||||
height: 600px;
|
||||
}
|
||||
.ng-cloak {
|
||||
display: none;
|
||||
}
|
||||
table {
|
||||
font-size: 11px !important;
|
||||
width: 100%;
|
||||
border: 1px;
|
||||
|
||||
}
|
||||
td {
|
||||
padding: 0px !important;
|
||||
}
|
||||
tfoot td {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="ng-cloak">
|
||||
<div class="container">
|
||||
<h1>Relay Pool Data</h2>
|
||||
<div ng-if="relays === undefined" class="text-center">
|
||||
<img src="//cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif"/>
|
||||
<p>Please wait while we gather data</p>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-show="relays !== undefined" class="ng-hide">
|
||||
<p>
|
||||
Currently {{ relays.length }} relays online ({{ totals.goMaxProcs }} cores in total).
|
||||
</p>
|
||||
</div>
|
||||
<div id="map"></div> <!-- Can't hide the map, otherwise it freaks out -->
|
||||
<p>The circle size represents how much bytes the relay transfered relative to other relays</p>
|
||||
</div>
|
||||
<div>
|
||||
<table class="table table-striped table-condensed table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2">Address</td>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.numActiveSessions || -1'; sortReverse = !sortReverse">
|
||||
Sessions
|
||||
<span ng-show="sortType == 'status.numActiveSessions || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.numActiveSessions || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.numConnections || -1'; sortReverse = !sortReverse">
|
||||
Connections
|
||||
<span ng-show="sortType == 'status.numConnections || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.numConnections || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.bytesProxied || -1'; sortReverse = !sortReverse">
|
||||
Data relayed
|
||||
<span ng-show="sortType == 'status.bytesProxied || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.bytesProxied || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th colspan="6" class="text-center">Transfer rate in the last period</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.uptimeSeconds || -1'; sortReverse = !sortReverse">
|
||||
Uptime hours
|
||||
<span ng-show="sortType == 'status.uptimeSeconds || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.uptimeSeconds || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th rowspan="2">
|
||||
<a ng-click="sortType = 'status.options[\'provided-by\'] || \'\''; sortReverse = !sortReverse">
|
||||
Provided by
|
||||
<span ng-show="sortType == 'status.options[\'provided-by\'] || \'\'' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.options[\'provided-by\'] || \'\'' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[0] || -1'; sortReverse = !sortReverse">
|
||||
10s
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[0] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[0] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[1] || -1'; sortReverse = !sortReverse">
|
||||
1m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[1] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[1] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[2] || -1'; sortReverse = !sortReverse">
|
||||
5m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[2] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[2] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[3] || -1'; sortReverse = !sortReverse">
|
||||
15m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[3] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[3] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[4] || -1'; sortReverse = !sortReverse">
|
||||
30m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[4] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[4] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="sortType = 'status.kbps10s1m5m15m30m60m[5] || -1'; sortReverse = !sortReverse">
|
||||
60m
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[5] || -1' && !sortReverse" class="fa fa-caret-down"></span>
|
||||
<span ng-show="sortType == 'status.kbps10s1m5m15m30m60m[5] || -1' && sortReverse" class="fa fa-caret-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="relay in relays | orderBy:sortType:sortReverse ">
|
||||
<td>{{ relay.address }}</td>
|
||||
<td ng-if="relay.status === undefined" colspan="11" class="text-center">Looking up...</td>
|
||||
<td ng-if-start="relay.status !== undefined">{{ relay.status.numActiveSessions }}</td>
|
||||
<td>{{ relay.status.numConnections }}</td>
|
||||
<td>{{ relay.status.bytesProxied | bytes }}</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s</td>
|
||||
<td>{{ relay.status.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s</td>
|
||||
<td ng-if="relay.status.uptimeSeconds != undefined">{{ relay.status.uptimeSeconds/60/60 | number:0 }}</td>
|
||||
<td ng-if="relay.status.uptimeSeconds == undefined"></td>
|
||||
<td title="{{ relay.status.options['provided-by'] || '' }}" ng-if-end>
|
||||
{{ relay.status.options['provided-by'] || '' | limitTo:50 }}
|
||||
<span ng-if="(relay.status.options['provided-by'] || '').length > 50">…
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>Totals</td>
|
||||
<td>{{ totals.numActiveSessions }}</td>
|
||||
<td>{{ totals.numConnections }}</td>
|
||||
<td>{{ totals.bytesProxied | bytes }}</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s</td>
|
||||
<td>{{ totals.uptimeSeconds/60/60 | number:0 }} hours</td>
|
||||
<td>{{ relays.length }} relays</td>
|
||||
</tr>
|
||||
</tfoor>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<p>
|
||||
This product includes GeoLite2 data created by MaxMind, available from
|
||||
<a href="http://www.maxmind.com">http://www.maxmind.com</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="//code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.min.js"></script>
|
||||
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
||||
<script src="//maps.googleapis.com/maps/api/js"></script>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
angular.module('syncthing', [
|
||||
])
|
||||
.config(function($httpProvider) {
|
||||
$httpProvider.defaults.timeout = 5000;
|
||||
})
|
||||
.filter('bytes', function() {
|
||||
return function(bytes, precision) {
|
||||
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-';
|
||||
if (typeof precision === 'undefined') precision = 1;
|
||||
|
||||
var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
|
||||
number = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
var value = (bytes / Math.pow(1000, Math.floor(number)));
|
||||
if (!isFinite(value)) {
|
||||
value = 0;
|
||||
precision = 0;
|
||||
}
|
||||
if (!isFinite(number)) {
|
||||
units = 'bytes';
|
||||
} else {
|
||||
units = units[number];
|
||||
}
|
||||
return value.toFixed(precision) + ' ' + units;
|
||||
}
|
||||
})
|
||||
.controller('relayDataController', ['$scope', '$rootScope', '$http', '$q', '$compile', '$timeout', function($scope, $rootScope, $http, $q, $compile, $timeout) {
|
||||
$scope.totals = {
|
||||
bytesProxied: 0,
|
||||
goMaxProcs: 0,
|
||||
kbps10s1m5m15m30m60m: [0, 0, 0, 0, 0, 0],
|
||||
numActiveSessions: 0,
|
||||
numConnections: 0,
|
||||
numPendingSessionKeys: 0,
|
||||
numProxies: 0,
|
||||
uptimeSeconds: 0,
|
||||
};
|
||||
$scope.map = new google.maps.Map(document.getElementById('map'), {
|
||||
zoom: 1,
|
||||
mapTypeId: google.maps.MapTypeId.ROADMAP
|
||||
});
|
||||
$scope.mapBounds = new google.maps.LatLngBounds();
|
||||
$scope.tooltipTemplate = $('#infoTemplate').html();
|
||||
$scope.usedLocations = {};
|
||||
$scope.sortType = 'status.numActiveSessions || -1';
|
||||
$scope.sortReverse = true;
|
||||
|
||||
$http.get("/endpoint").then(function(response) {
|
||||
$scope.relays = response.data.relays;
|
||||
var promises = [];
|
||||
angular.forEach($scope.relays, function(relay) {
|
||||
|
||||
relay.uri = constructURI(relay.url);
|
||||
relay.address = relay.url.split('/')[2];
|
||||
|
||||
addMarkerToMap(relay);
|
||||
|
||||
promises.push(getRelayStatus(relay));
|
||||
});
|
||||
|
||||
// Can only add circles once we know the totals for transfers, which means
|
||||
// we need to resolve all statuses.
|
||||
$q.all(promises).then(function() {
|
||||
angular.forEach($scope.relays, function(relay) {
|
||||
if (relay.status) {
|
||||
addCircleToMap(relay);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.map.fitBounds($scope.mapBounds);
|
||||
if ($scope.relays.length == 1) {
|
||||
$scope.map.setZoom(13);
|
||||
}
|
||||
});
|
||||
|
||||
function addMarkerToMap(relay) {
|
||||
var loc = relay.location.latitude + "," + relay.location.longitude;
|
||||
|
||||
// Deal with overlapping markers
|
||||
while (loc in $scope.usedLocations) {
|
||||
var locParts = loc.split(',');
|
||||
locParts = [parseFloat(locParts[0]), parseFloat(locParts[1])];
|
||||
locParts[Math.round(Math.random())] += 0.5 * (Math.random() >= 0.5 ? 1 : -1);
|
||||
loc = locParts.join(',');
|
||||
}
|
||||
|
||||
$scope.usedLocations[loc] = true;
|
||||
|
||||
var locParts = loc.split(',');
|
||||
|
||||
relay.marker = new google.maps.Marker({
|
||||
map: $scope.map,
|
||||
position: new google.maps.LatLng(locParts[0], locParts[1]),
|
||||
title: relay.url,
|
||||
});
|
||||
|
||||
var scope = $rootScope.$new(true);
|
||||
scope.relay = relay;
|
||||
|
||||
relay.marker.info = new google.maps.InfoWindow({
|
||||
content: $compile($scope.tooltipTemplate)(scope)[0],
|
||||
});
|
||||
|
||||
relay.marker.addListener('mouseover', function() {
|
||||
relay.marker.info.open($scope.map, relay.marker);
|
||||
});
|
||||
|
||||
relay.marker.addListener('mouseout', function() {
|
||||
relay.marker.info.close();
|
||||
});
|
||||
|
||||
$scope.mapBounds.extend(relay.marker.position);
|
||||
}
|
||||
|
||||
function addCircleToMap(relay) {
|
||||
relay.marker.circle = new google.maps.Circle({
|
||||
strokeColor: '#FF0000',
|
||||
strokeOpacity: 0.8,
|
||||
strokeWeight: 2,
|
||||
fillColor: '#FF0000',
|
||||
fillOpacity: 0.35,
|
||||
map: $scope.map,
|
||||
center: relay.marker.position,
|
||||
radius: ((relay.status.bytesProxied * 100) / $scope.totals.bytesProxied) * 10000
|
||||
});
|
||||
}
|
||||
|
||||
function getRelayStatus(relay) {
|
||||
// Normal timeout doesn't deal with relays which accept the TCP connection
|
||||
// but don't respond (some firewalls do that), so deal with it this way.
|
||||
var timeoutRequest = $q.defer();
|
||||
var resolveStatus = $q.defer();
|
||||
|
||||
$http.get("http://" + relay.uri.hostname + (relay.uri.args.statusAddr || ":22070") + "/status", { timeout: timeoutRequest.promise }).then(function (response) {
|
||||
relay.status = response.data;
|
||||
resolveStatus.resolve();
|
||||
angular.forEach($scope.totals, function(value, key) {
|
||||
if (typeof $scope.totals[key] == 'number') {
|
||||
$scope.totals[key] += response.data[key];
|
||||
} else if (typeof $scope.totals[key] == 'object' && $scope.totals[key] instanceof Array) {
|
||||
angular.forEach($scope.totals[key], function(value, index) {
|
||||
$scope.totals[key][index] += response.data[key][index];
|
||||
});
|
||||
}
|
||||
});
|
||||
}, function() {
|
||||
relay.status = null;
|
||||
resolveStatus.resolve();
|
||||
});
|
||||
|
||||
$timeout(function() {
|
||||
timeoutRequest.resolve();
|
||||
}, 5000);
|
||||
|
||||
return resolveStatus.promise;
|
||||
}
|
||||
|
||||
function constructURI(url) {
|
||||
var uri = document.createElement('a');
|
||||
|
||||
// HAX, otherwise doesn't work
|
||||
uri.href = url.replace('relay://', 'http://');
|
||||
|
||||
// Convert query string to object
|
||||
uri.args = {};
|
||||
angular.forEach(uri.search.replace(/^\?/, '').split('&'), function(query) {
|
||||
var split = query.split('=');
|
||||
uri.args[split[0]] = split[1];
|
||||
});
|
||||
|
||||
return uri;
|
||||
}
|
||||
}]);
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="infoTemplate">
|
||||
<div>
|
||||
<p><b>{{ relay.uri.hostname }}</b> <span ng-if="relay.status.options['provided-by']">provided by <u>{{ relay.status.options['provided-by'] }}</u></span></p>
|
||||
<div ng-if="relay.status">
|
||||
<span ng-if="relay.status.startTime">Start time: {{ relay.status.startTime | date:"medium" }}</br></span>
|
||||
<span ng-if="relay.status.bytesProxied != undefined">Proxied: {{ relay.status.bytesProxied | bytes }}</br></span>
|
||||
<span ng-if="relay.status.numActiveSessions != undefined">Sessions: {{ relay.status.numActiveSessions }}</br></span>
|
||||
<span ng-if="relay.status.numConnections != undefined">Clients: {{ relay.status.numConnections }}</br></span>
|
||||
<span ng-if="relay.status.options.pools">Pools: {{ relay.status.options.pools.join(', ') }}</br></span>
|
||||
<span ng-if="relay.status.options['global-rate'] != undefined">
|
||||
<span ng-if="relay.status.options['global-rate'] > 0">Global rate limit: {{ relay.status.options['global-rate'] | bytes }}/s</span>
|
||||
<span ng-if="relay.status.options['global-rate'] == 0">Global rate limit: unlimited</span>
|
||||
</br>
|
||||
</span>
|
||||
<span ng-if="relay.status.options['per-session-rate'] != undefined">
|
||||
<span ng-if="relay.status.options['per-session-rate'] > 0">Session rate limit: {{ relay.status.options['per-session-rate'] | bytes }}/s</span>
|
||||
<span ng-if="relay.status.options['per-session-rate'] == 0">Session rate limit: unlimited</span>
|
||||
</br>
|
||||
</span>
|
||||
</div>
|
||||
<div ng-if="!relay.status">
|
||||
Data unavailable.
|
||||
<div>
|
||||
</div>
|
||||
</script>
|
||||
</html>
|
||||
536
cmd/strelaypoolsrv/main.go
Normal file
@@ -0,0 +1,536 @@
|
||||
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||
|
||||
//go:generate go run genassets.go gui auto/gui.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/groupcache/lru"
|
||||
"github.com/juju/ratelimit"
|
||||
|
||||
"github.com/oschwald/geoip2-golang"
|
||||
|
||||
"github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto"
|
||||
"github.com/syncthing/syncthing/lib/relay/client"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
)
|
||||
|
||||
type location struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
}
|
||||
|
||||
type relay struct {
|
||||
URL string `json:"url"`
|
||||
Location location `json:"location"`
|
||||
uri *url.URL
|
||||
}
|
||||
|
||||
func (r relay) String() string {
|
||||
return r.URL
|
||||
}
|
||||
|
||||
type request struct {
|
||||
relay relay
|
||||
uri *url.URL
|
||||
result chan result
|
||||
}
|
||||
|
||||
type result struct {
|
||||
err error
|
||||
eviction time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
testCert tls.Certificate
|
||||
listen = ":80"
|
||||
dir string
|
||||
evictionTime = time.Hour
|
||||
debug bool
|
||||
getLRUSize = 10 << 10
|
||||
getLimitBurst int64 = 10
|
||||
getLimitAvg = 1
|
||||
postLRUSize = 1 << 10
|
||||
postLimitBurst int64 = 2
|
||||
postLimitAvg = 1
|
||||
getLimit time.Duration
|
||||
postLimit time.Duration
|
||||
permRelaysFile string
|
||||
ipHeader string
|
||||
geoipPath string
|
||||
|
||||
getMut = sync.NewRWMutex()
|
||||
getLRUCache *lru.Cache
|
||||
|
||||
postMut = sync.NewRWMutex()
|
||||
postLRUCache *lru.Cache
|
||||
|
||||
requests = make(chan request, 10)
|
||||
|
||||
mut = sync.NewRWMutex()
|
||||
knownRelays = make([]relay, 0)
|
||||
permanentRelays = make([]relay, 0)
|
||||
evictionTimers = make(map[string]*time.Timer)
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&listen, "listen", listen, "Listen address")
|
||||
flag.StringVar(&dir, "keys", dir, "Directory where http-cert.pem and http-key.pem is stored for TLS listening")
|
||||
flag.BoolVar(&debug, "debug", debug, "Enable debug output")
|
||||
flag.DurationVar(&evictionTime, "eviction", evictionTime, "After how long the relay is evicted")
|
||||
flag.IntVar(&getLRUSize, "get-limit-cache", getLRUSize, "Get request limiter cache size")
|
||||
flag.IntVar(&getLimitAvg, "get-limit-avg", 2, "Allowed average get request rate, per 10 s")
|
||||
flag.Int64Var(&getLimitBurst, "get-limit-burst", getLimitBurst, "Allowed burst get requests")
|
||||
flag.IntVar(&postLRUSize, "post-limit-cache", postLRUSize, "Post request limiter cache size")
|
||||
flag.IntVar(&postLimitAvg, "post-limit-avg", 2, "Allowed average post request rate, per minute")
|
||||
flag.Int64Var(&postLimitBurst, "post-limit-burst", postLimitBurst, "Allowed burst post requests")
|
||||
flag.StringVar(&permRelaysFile, "perm-relays", "", "Path to list of permanent relays")
|
||||
flag.StringVar(&ipHeader, "ip-header", "", "Name of header which holds clients ip:port. Only meaningful when running behind a reverse proxy.")
|
||||
flag.StringVar(&geoipPath, "geoip", "GeoLite2-City.mmdb", "Path to GeoLite2-City database")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
getLimit = 10 * time.Second / time.Duration(getLimitAvg)
|
||||
postLimit = time.Minute / time.Duration(postLimitAvg)
|
||||
|
||||
getLRUCache = lru.New(getLRUSize)
|
||||
postLRUCache = lru.New(postLRUSize)
|
||||
|
||||
var listener net.Listener
|
||||
var err error
|
||||
|
||||
if permRelaysFile != "" {
|
||||
loadPermanentRelays(permRelaysFile)
|
||||
}
|
||||
|
||||
testCert = createTestCertificate()
|
||||
|
||||
go requestProcessor()
|
||||
|
||||
if dir != "" {
|
||||
if debug {
|
||||
log.Println("Starting TLS listener on", listen)
|
||||
}
|
||||
certFile, keyFile := filepath.Join(dir, "http-cert.pem"), filepath.Join(dir, "http-key.pem")
|
||||
var cert tls.Certificate
|
||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to load HTTP X509 key pair:", err)
|
||||
}
|
||||
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS10, // No SSLv3
|
||||
CipherSuites: []uint16{
|
||||
// No RC4
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
},
|
||||
}
|
||||
|
||||
listener, err = tls.Listen("tcp", listen, tlsCfg)
|
||||
} else {
|
||||
if debug {
|
||||
log.Println("Starting plain listener on", listen)
|
||||
}
|
||||
listener, err = net.Listen("tcp", listen)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln("listen:", err)
|
||||
}
|
||||
|
||||
handler := http.NewServeMux()
|
||||
handler.HandleFunc("/", handleAssets)
|
||||
handler.HandleFunc("/endpoint", handleRequest)
|
||||
|
||||
srv := http.Server{
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
err = srv.Serve(listener)
|
||||
if err != nil {
|
||||
log.Fatalln("serve:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleAssets(w http.ResponseWriter, r *http.Request) {
|
||||
assets := auto.Assets()
|
||||
path := r.URL.Path[1:]
|
||||
if path == "" {
|
||||
path = "index.html"
|
||||
}
|
||||
|
||||
bs, ok := assets[path]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
mtype := mimeTypeForFile(path)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
} else {
|
||||
// ungzip if browser not send gzip accepted header
|
||||
var gr *gzip.Reader
|
||||
gr, _ = gzip.NewReader(bytes.NewReader(bs))
|
||||
bs, _ = ioutil.ReadAll(gr)
|
||||
gr.Close()
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
||||
|
||||
w.Write(bs)
|
||||
}
|
||||
|
||||
func mimeTypeForFile(file string) string {
|
||||
// We use a built in table of the common types since the system
|
||||
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||
// to the system.
|
||||
ext := filepath.Ext(file)
|
||||
switch ext {
|
||||
case ".htm", ".html":
|
||||
return "text/html"
|
||||
case ".css":
|
||||
return "text/css"
|
||||
case ".js":
|
||||
return "application/javascript"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".ttf":
|
||||
return "application/x-font-ttf"
|
||||
case ".woff":
|
||||
return "application/x-font-woff"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
default:
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
if ipHeader != "" {
|
||||
r.RemoteAddr = r.Header.Get(ipHeader)
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
if limit(r.RemoteAddr, getLRUCache, getMut, getLimit, int64(getLimitBurst)) {
|
||||
w.WriteHeader(429)
|
||||
return
|
||||
}
|
||||
handleGetRequest(w, r)
|
||||
case "POST":
|
||||
if limit(r.RemoteAddr, postLRUCache, postMut, postLimit, int64(postLimitBurst)) {
|
||||
w.WriteHeader(429)
|
||||
return
|
||||
}
|
||||
handlePostRequest(w, r)
|
||||
default:
|
||||
if debug {
|
||||
log.Println("Unhandled HTTP method", r.Method)
|
||||
}
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetRequest(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
mut.RLock()
|
||||
relays := append(permanentRelays, knownRelays...)
|
||||
mut.RUnlock()
|
||||
|
||||
// Shuffle
|
||||
for i := range relays {
|
||||
j := rand.Intn(i + 1)
|
||||
relays[i], relays[j] = relays[j], relays[i]
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string][]relay{
|
||||
"relays": relays,
|
||||
})
|
||||
}
|
||||
|
||||
func handlePostRequest(w http.ResponseWriter, r *http.Request) {
|
||||
var newRelay relay
|
||||
err := json.NewDecoder(r.Body).Decode(&newRelay)
|
||||
r.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to parse payload")
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
uri, err := url.Parse(newRelay.URL)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to parse URI", newRelay.URL)
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(uri.Host)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to split URI", newRelay.URL)
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the IP address of the client
|
||||
rhost, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Failed to split remote address", r.RemoteAddr)
|
||||
}
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// The client did not provide an IP address, use the IP address of the client.
|
||||
if host == "" {
|
||||
uri.Host = net.JoinHostPort(rhost, port)
|
||||
newRelay.URL = uri.String()
|
||||
} else if host != rhost {
|
||||
if debug {
|
||||
log.Println("IP address advertised does not match client IP address", r.RemoteAddr, uri)
|
||||
}
|
||||
http.Error(w, "IP address does not match client IP", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
newRelay.uri = uri
|
||||
newRelay.Location = getLocation(uri.Host)
|
||||
|
||||
for _, current := range permanentRelays {
|
||||
if current.uri.Host == newRelay.uri.Host {
|
||||
if debug {
|
||||
log.Println("Asked to add a relay", newRelay, "which exists in permanent list")
|
||||
}
|
||||
http.Error(w, "Invalid request", 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reschan := make(chan result)
|
||||
|
||||
select {
|
||||
case requests <- request{newRelay, uri, reschan}:
|
||||
result := <-reschan
|
||||
if result.err != nil {
|
||||
http.Error(w, result.err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]time.Duration{
|
||||
"evictionIn": result.eviction,
|
||||
})
|
||||
|
||||
default:
|
||||
if debug {
|
||||
log.Println("Dropping request")
|
||||
}
|
||||
w.WriteHeader(429)
|
||||
}
|
||||
}
|
||||
|
||||
func requestProcessor() {
|
||||
for request := range requests {
|
||||
if debug {
|
||||
log.Println("Request for", request.relay)
|
||||
}
|
||||
if !client.TestRelay(request.uri, []tls.Certificate{testCert}, time.Second, 2*time.Second, 3) {
|
||||
if debug {
|
||||
log.Println("Test for relay", request.relay, "failed")
|
||||
}
|
||||
request.result <- result{fmt.Errorf("test failed"), 0}
|
||||
continue
|
||||
}
|
||||
|
||||
mut.Lock()
|
||||
timer, ok := evictionTimers[request.relay.uri.Host]
|
||||
if ok {
|
||||
if debug {
|
||||
log.Println("Stopping existing timer for", request.relay)
|
||||
}
|
||||
timer.Stop()
|
||||
}
|
||||
|
||||
for i, current := range knownRelays {
|
||||
if current.uri.Host == request.relay.uri.Host {
|
||||
if debug {
|
||||
log.Println("Relay", request.relay, "already exists")
|
||||
}
|
||||
|
||||
// Evict the old entry anyway, as configuration might have changed.
|
||||
last := len(knownRelays) - 1
|
||||
knownRelays[i] = knownRelays[last]
|
||||
knownRelays = knownRelays[:last]
|
||||
|
||||
goto found
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Println("Adding new relay", request.relay)
|
||||
}
|
||||
|
||||
found:
|
||||
|
||||
knownRelays = append(knownRelays, request.relay)
|
||||
|
||||
evictionTimers[request.relay.uri.Host] = time.AfterFunc(evictionTime, evict(request.relay))
|
||||
mut.Unlock()
|
||||
request.result <- result{nil, evictionTime}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func evict(relay relay) func() {
|
||||
return func() {
|
||||
mut.Lock()
|
||||
defer mut.Unlock()
|
||||
if debug {
|
||||
log.Println("Evicting", relay)
|
||||
}
|
||||
for i, current := range knownRelays {
|
||||
if current.uri.Host == relay.uri.Host {
|
||||
if debug {
|
||||
log.Println("Evicted", relay)
|
||||
}
|
||||
last := len(knownRelays) - 1
|
||||
knownRelays[i] = knownRelays[last]
|
||||
knownRelays = knownRelays[:last]
|
||||
}
|
||||
}
|
||||
delete(evictionTimers, relay.uri.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func limit(addr string, cache *lru.Cache, lock sync.RWMutex, rate time.Duration, burst int64) bool {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
lock.RLock()
|
||||
bkt, ok := cache.Get(host)
|
||||
lock.RUnlock()
|
||||
if ok {
|
||||
bkt := bkt.(*ratelimit.Bucket)
|
||||
if bkt.TakeAvailable(1) != 1 {
|
||||
// Rate limit
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
lock.Lock()
|
||||
cache.Add(host, ratelimit.NewBucket(rate, burst))
|
||||
lock.Unlock()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loadPermanentRelays(file string) {
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(content), "\n") {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
uri, err := url.Parse(line)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Println("Skipping permanent relay", line, "due to parse error", err)
|
||||
}
|
||||
continue
|
||||
|
||||
}
|
||||
|
||||
permanentRelays = append(permanentRelays, relay{
|
||||
URL: line,
|
||||
Location: getLocation(uri.Host),
|
||||
uri: uri,
|
||||
})
|
||||
if debug {
|
||||
log.Println("Adding permanent relay", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createTestCertificate() tls.Certificate {
|
||||
tmpDir, err := ioutil.TempDir("", "relaypoolsrv")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
certFile, keyFile := filepath.Join(tmpDir, "cert.pem"), filepath.Join(tmpDir, "key.pem")
|
||||
cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 3072)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create test X509 key pair:", err)
|
||||
}
|
||||
|
||||
return cert
|
||||
}
|
||||
|
||||
func getLocation(host string) location {
|
||||
db, err := geoip2.Open(geoipPath)
|
||||
if err != nil {
|
||||
return location{}
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", host)
|
||||
if err != nil {
|
||||
return location{}
|
||||
}
|
||||
|
||||
city, err := db.City(addr.IP)
|
||||
if err != nil {
|
||||
return location{}
|
||||
}
|
||||
|
||||
return location{
|
||||
Latitude: city.Location.Latitude,
|
||||
Longitude: city.Location.Longitude,
|
||||
}
|
||||
}
|
||||
22
cmd/strelaysrv/LICENSE
Normal file
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 The Syncthing Project
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
relaysrv
|
||||
========
|
||||
strelaysrv
|
||||
==========
|
||||
|
||||
[](http://build.syncthing.net/job/relaysrv/lastBuild/)
|
||||
[](http://build.syncthing.net/job/strelaysrv/lastBuild/)
|
||||
|
||||
This is the relay server for the `syncthing` project.
|
||||
|
||||
To get it, run `go get github.com/syncthing/relaysrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/relaysrv/lastSuccessfulBuild/artifact/)
|
||||
To get it, run `go get github.com/syncthing/strelaysrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/strelaysrv/lastSuccessfulBuild/artifact/)
|
||||
from the build server.
|
||||
|
||||
:exclamation:Warnings:exclamation: - Read or regret
|
||||
@@ -16,13 +16,13 @@ By default, all relay servers will join the default public relay pool, which mea
|
||||
|
||||
If you wish to disable this behaviour, please specify `-pools=""` argument.
|
||||
|
||||
Please note that `relaysrv` is only usable by `syncthing` **version v0.12 and onwards**.
|
||||
Please note that `strelaysrv` is only usable by `syncthing` **version v0.12 and onwards**.
|
||||
|
||||
To run `relaysrv` you need to have port 22067 available to the internet, which means you might need to allow it through your firewall if you **have a public IP, or setup a port-forwarding** (22067 to 22067) if you are behind a router.
|
||||
To run `strelaysrv` you need to have port 22067 available to the internet, which means you might need to allow it through your firewall if you **have a public IP, or setup a port-forwarding** (22067 to 22067) if you are behind a router.
|
||||
|
||||
Furthermore, **by default relaysrv will also expose a /status HTTP endpoint on port 22070**, which is used by the pool servers to peek at metrics of the relaysrv, such as what are the current transfer rates, how many clients are connected, etc, etc. If you wish this information to be available, similarlly you might want to allow it through your firewall, or port-forward it (22070 to 22070) on your NAT device.
|
||||
Furthermore, **by default strelaysrv will also expose a /status HTTP endpoint on port 22070**, which is used by the pool servers to peek at metrics of the strelaysrv, such as what are the current transfer rates, how many clients are connected, etc, etc. If you wish this information to be available, similarlly you might want to allow it through your firewall, or port-forward it (22070 to 22070) on your NAT device.
|
||||
|
||||
This is **not mandatory** for the relaysrv to function, and is used only to gather metrics and present them in the overview page of the pool server, displaying stats about the specific relay.
|
||||
This is **not mandatory** for the strelaysrv to function, and is used only to gather metrics and present them in the overview page of the pool server, displaying stats about the specific relay.
|
||||
|
||||
At the point of writing the endpoint output looks as follows:
|
||||
|
||||
@@ -62,31 +62,31 @@ At the point of writing the endpoint output looks as follows:
|
||||
}
|
||||
```
|
||||
|
||||
If you wish to disable the /status endpoint, provide `-status-srv=""` as one of the arguments when starting the relaysrv.
|
||||
If you wish to disable the /status endpoint, provide `-status-srv=""` as one of the arguments when starting the strelaysrv.
|
||||
|
||||
Running for public use
|
||||
----
|
||||
Make sure you have a public IP with port 22067 open, or make sure you have port-forwarding (22067 to 22067) if you are behind a router.
|
||||
|
||||
Run the `relaysrv` with no arguments (or `-debug` if you want more output), and that should be enough for the server to join the public relay pool.
|
||||
Run the `strelaysrv` with no arguments (or `-debug` if you want more output), and that should be enough for the server to join the public relay pool.
|
||||
You should see a message saying:
|
||||
```
|
||||
2015/09/21 22:45:46 pool.go:60: Joined https://relays.syncthing.net/endpoint rejoining in 48m0s
|
||||
```
|
||||
|
||||
See `relaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
See `strelaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
|
||||
Running for private use
|
||||
-----
|
||||
|
||||
Once you've started the `relaysrv`, it will generate a key pair and print an URI:
|
||||
Once you've started the `strelaysrv`, it will generate a key pair and print an URI:
|
||||
```bash
|
||||
relay://:22067/?id=EZQOIDM-6DDD4ZI-DJ65NSM-4OQWRAT-EIKSMJO-OZ552BO-WQZEGYY-STS5RQM&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070
|
||||
```
|
||||
|
||||
This URI contains partial address of the relay server, as well as it's options which in the future may be taken into account when choosing the best suitable relay out of multiple available.
|
||||
|
||||
Because `-listen` option was not used, the `relaysrv` does not know it's external IP, therefore you should replace the host part of the URI with your public IP address on which the `relaysrv` will be available:
|
||||
Because `-listen` option was not used, the `strelaysrv` does not know it's external IP, therefore you should replace the host part of the URI with your public IP address on which the `strelaysrv` will be available:
|
||||
|
||||
```bash
|
||||
relay://123.123.123.123:22067/?id=EZQOIDM-6DDD4ZI-DJ65NSM-4OQWRAT-EIKSMJO-OZ552BO-WQZEGYY-STS5RQM&pingInterval=1m0s&networkTimeout=2m0s&sessionLimitBps=0&globalLimitBps=0&statusAddr=:22070
|
||||
@@ -100,7 +100,7 @@ relay://123.123.123.123:22067
|
||||
|
||||
This URI can then be used in `syncthing` as one of the relay servers.
|
||||
|
||||
See `relaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
See `strelaysrv -help` for other options, such as rate limits, timeout intervals, etc.
|
||||
|
||||
Other items available in this repo
|
||||
----
|
||||
@@ -3,10 +3,10 @@ Description=Syncthing relay server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=syncthing-relaysrv
|
||||
Group=syncthing-relaysrv
|
||||
ExecStart=/usr/bin/relaysrv
|
||||
WorkingDirectory=/var/lib/syncthing-relaysrv
|
||||
User=strelaysrv
|
||||
Group=strelaysrv
|
||||
ExecStart=/usr/bin/strelaysrv
|
||||
WorkingDirectory=/var/lib/strelaysrv
|
||||
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
@@ -42,7 +42,7 @@ func init() {
|
||||
BuildDate = time.Unix(int64(stamp), 0)
|
||||
|
||||
date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST")
|
||||
LongVersion = fmt.Sprintf(`relaysrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
LongVersion = fmt.Sprintf(`strelaysrv %s (%s %s-%s) %s@%s %s`, Version, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -88,7 +88,7 @@ func main() {
|
||||
flag.StringVar(&statusAddr, "status-srv", ":22070", "Listen address for status service (blank to disable)")
|
||||
flag.StringVar(&poolAddrs, "pools", defaultPoolAddrs, "Comma separated list of relay pool addresses to join")
|
||||
flag.StringVar(&providedBy, "provided-by", "", "An optional description about who provides the relay")
|
||||
flag.StringVar(&extAddress, "ext-address", "", "An optional address to advertising as being available on.\n\tAllows listening on an unprivileged port with port forwarding from e.g. 443, and be connected to on port 443.")
|
||||
flag.StringVar(&extAddress, "ext-address", "", "An optional address to advertise as being available on.\n\tAllows listening on an unprivileged port with port forwarding from e.g. 443, and be connected to on port 443.")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
@@ -111,7 +111,7 @@ func main() {
|
||||
|
||||
go monitorLimits()
|
||||
} else if err != nil && runtime.GOOS != "windows" {
|
||||
log.Println("Assuming no connection limit, due to error retrievign rlimits:", err)
|
||||
log.Println("Assuming no connection limit, due to error retrieving rlimits:", err)
|
||||
}
|
||||
|
||||
sessionAddress = addr.IP[:]
|
||||
@@ -121,7 +121,7 @@ func main() {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "relaysrv", 3072)
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv", 3072)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to generate X509 key pair:", err)
|
||||
}
|
||||
@@ -41,8 +41,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
configInSync = true
|
||||
startTime = time.Now()
|
||||
startTime = time.Now()
|
||||
)
|
||||
|
||||
type apiService struct {
|
||||
@@ -86,7 +85,7 @@ type modelIntf interface {
|
||||
DelayScan(folder string, next time.Duration)
|
||||
ScanFolder(folder string) error
|
||||
ScanFolders() map[string]error
|
||||
ScanFolderSubs(folder string, subs []string) error
|
||||
ScanFolderSubdirs(folder string, subs []string) error
|
||||
BringToFront(folder, file string)
|
||||
ConnectedTo(deviceID protocol.DeviceID) bool
|
||||
GlobalSize(folder string) (nfiles, deleted int, bytes int64)
|
||||
@@ -100,12 +99,13 @@ type configIntf interface {
|
||||
GUI() config.GUIConfiguration
|
||||
Raw() config.Configuration
|
||||
Options() config.OptionsConfiguration
|
||||
Replace(cfg config.Configuration) config.CommitResponse
|
||||
Replace(cfg config.Configuration) error
|
||||
Subscribe(c config.Committer)
|
||||
Folders() map[string]config.FolderConfiguration
|
||||
Devices() map[protocol.DeviceID]config.DeviceConfiguration
|
||||
Save() error
|
||||
ListenAddresses() []string
|
||||
RequiresRestart() bool
|
||||
}
|
||||
|
||||
type connectionsIntf interface {
|
||||
@@ -577,7 +577,7 @@ func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
|
||||
func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interface{} {
|
||||
var res = make(map[string]interface{})
|
||||
|
||||
res["invalid"] = cfg.Folders()[folder].Invalid
|
||||
res["invalid"] = "" // Deprecated, retains external API for now
|
||||
|
||||
globalFiles, globalDeleted, globalBytes := m.GlobalSize(folder)
|
||||
res["globalFiles"], res["globalDeleted"], res["globalBytes"] = globalFiles, globalDeleted, globalBytes
|
||||
@@ -690,7 +690,7 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
to, err := config.ReadJSON(r.Body, myID)
|
||||
r.Body.Close()
|
||||
if err != nil {
|
||||
l.Warnln("decoding posted config:", err)
|
||||
l.Warnln("Decoding posted config:", err)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -722,13 +722,19 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Activate and save
|
||||
|
||||
resp := s.cfg.Replace(to)
|
||||
configInSync = !resp.RequiresRestart
|
||||
s.cfg.Save()
|
||||
if err := s.cfg.Replace(to); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.cfg.Save(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiService) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, map[string]bool{"configInSync": configInSync})
|
||||
sendJSON(w, map[string]bool{"configInSync": !s.cfg.RequiresRestart()})
|
||||
}
|
||||
|
||||
func (s *apiService) postSystemRestart(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1071,7 +1077,7 @@ func (s *apiService) postDBScan(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
subs := qs["sub"]
|
||||
err = s.model.ScanFolderSubs(folder, subs)
|
||||
err = s.model.ScanFolderSubdirs(folder, subs)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
@@ -1134,9 +1140,9 @@ func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
current := qs.Get("current")
|
||||
if current == "" && runtime.GOOS == "windows" {
|
||||
if drives, err := osutil.GetDriveLetters(); err == nil {
|
||||
sendJSON(w, drives)
|
||||
if current == "" {
|
||||
if roots, err := osutil.GetFilesystemRoots(); err == nil {
|
||||
sendJSON(w, roots)
|
||||
} else {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
@@ -1173,13 +1179,17 @@ type jsonFileInfo protocol.FileInfo
|
||||
|
||||
func (f jsonFileInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"name": f.Name,
|
||||
"size": protocol.FileInfo(f).Size(),
|
||||
"flags": fmt.Sprintf("%#o", f.Flags),
|
||||
"modified": time.Unix(f.Modified, 0),
|
||||
"localVersion": f.LocalVersion,
|
||||
"numBlocks": len(f.Blocks),
|
||||
"version": jsonVersionVector(f.Version),
|
||||
"name": f.Name,
|
||||
"type": f.Type,
|
||||
"size": f.Size,
|
||||
"permissions": fmt.Sprintf("%#o", f.Permissions),
|
||||
"deleted": f.Deleted,
|
||||
"invalid": f.Invalid,
|
||||
"noPermissions": f.NoPermissions,
|
||||
"modified": time.Unix(f.Modified, 0),
|
||||
"localVersion": f.LocalVersion,
|
||||
"numBlocks": len(f.Blocks),
|
||||
"version": jsonVersionVector(f.Version),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1187,20 +1197,23 @@ type jsonDBFileInfo db.FileInfoTruncated
|
||||
|
||||
func (f jsonDBFileInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"name": f.Name,
|
||||
"size": db.FileInfoTruncated(f).Size(),
|
||||
"flags": fmt.Sprintf("%#o", f.Flags),
|
||||
"modified": time.Unix(f.Modified, 0),
|
||||
"localVersion": f.LocalVersion,
|
||||
"version": jsonVersionVector(f.Version),
|
||||
"name": f.Name,
|
||||
"type": f.Type,
|
||||
"size": f.Size,
|
||||
"permissions": fmt.Sprintf("%#o", f.Permissions),
|
||||
"deleted": f.Deleted,
|
||||
"invalid": f.Invalid,
|
||||
"noPermissions": f.NoPermissions,
|
||||
"modified": time.Unix(f.Modified, 0),
|
||||
"localVersion": f.LocalVersion,
|
||||
})
|
||||
}
|
||||
|
||||
type jsonVersionVector protocol.Vector
|
||||
|
||||
func (v jsonVersionVector) MarshalJSON() ([]byte, error) {
|
||||
res := make([]string, len(v))
|
||||
for i, c := range v {
|
||||
res := make([]string, len(v.Counters))
|
||||
for i, c := range v.Counters {
|
||||
res[i] = fmt.Sprintf("%v:%d", c.ID, c.Value)
|
||||
}
|
||||
return json.Marshal(res)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -613,3 +614,55 @@ func TestRandomString(t *testing.T) {
|
||||
t.Errorf("Expected 27 random characters, got %q of length %d", res["random"], len(res["random"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPostOK(t *testing.T) {
|
||||
cfg := bytes.NewBuffer([]byte(`{
|
||||
"version": 15,
|
||||
"folders": [
|
||||
{"id": "foo"}
|
||||
]
|
||||
}`))
|
||||
|
||||
resp, err := testConfigPost(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Error("Expected 200 OK, not", resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPostDupFolder(t *testing.T) {
|
||||
cfg := bytes.NewBuffer([]byte(`{
|
||||
"version": 15,
|
||||
"folders": [
|
||||
{"id": "foo"},
|
||||
{"id": "foo"}
|
||||
]
|
||||
}`))
|
||||
|
||||
resp, err := testConfigPost(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Error("Expected 400 Bad Request, not", resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func testConfigPost(data io.Reader) (*http.Response, error) {
|
||||
const testAPIKey = "foobarbaz"
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.APIKey = testAPIKey
|
||||
baseURL, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cli := &http.Client{
|
||||
Timeout: time.Second,
|
||||
}
|
||||
|
||||
req, _ := http.NewRequest("POST", baseURL+"/rest/system/config", data)
|
||||
req.Header.Set("X-API-Key", testAPIKey)
|
||||
return cli.Do(req)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ var locations = map[locationEnum]string{
|
||||
locKeyFile: "${config}/key.pem",
|
||||
locHTTPSCertFile: "${config}/https-cert.pem",
|
||||
locHTTPSKeyFile: "${config}/https-key.pem",
|
||||
locDatabase: "${config}/index-v0.13.0.db",
|
||||
locDatabase: "${config}/index-v0.14.0.db",
|
||||
locLogFile: "${config}/syncthing.log", // -logfile on Windows
|
||||
locCsrfTokens: "${config}/csrftokens.txt",
|
||||
locPanicLog: "${config}/panic-${timestamp}.log",
|
||||
|
||||
@@ -51,7 +51,7 @@ import (
|
||||
|
||||
var (
|
||||
Version = "unknown-dev"
|
||||
Codename = "Copper Cockroach"
|
||||
Codename = "Dysprosium Dragonfly"
|
||||
BuildStamp = "0"
|
||||
BuildDate time.Time
|
||||
BuildHost = "unknown"
|
||||
@@ -539,8 +539,9 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
errors := logger.NewRecorder(l, logger.LevelWarn, maxSystemErrors, 0)
|
||||
systemLog := logger.NewRecorder(l, logger.LevelDebug, maxSystemLog, initialSystemLog)
|
||||
|
||||
// Event subscription for the API; must start early to catch the early events. The LocalDiskUpdated
|
||||
// event might overwhelm the event reciever in some situations so we will not subscribe to it here.
|
||||
// Event subscription for the API; must start early to catch the early
|
||||
// events. The LocalChangeDetected event might overwhelm the event
|
||||
// receiver in some situations so we will not subscribe to it here.
|
||||
apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents&^events.LocalChangeDetected), 1000)
|
||||
|
||||
if len(os.Getenv("GOMAXPROCS")) == 0 {
|
||||
@@ -681,17 +682,9 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear out old indexes for other devices. Otherwise we'll start up and
|
||||
// start needing a bunch of files which are nowhere to be found. This
|
||||
// needs to be changed when we correctly do persistent indexes.
|
||||
// Add and start folders
|
||||
for _, folderCfg := range cfg.Folders() {
|
||||
m.AddFolder(folderCfg)
|
||||
for _, device := range folderCfg.DeviceIDs() {
|
||||
if device == myID {
|
||||
continue
|
||||
}
|
||||
m.Index(device, folderCfg.ID, nil, 0, nil)
|
||||
}
|
||||
m.StartFolder(folderCfg.ID)
|
||||
}
|
||||
|
||||
@@ -863,7 +856,6 @@ func loadConfig() (*config.Wrapper, error) {
|
||||
cfg, err := config.Load(cfgFile, myID)
|
||||
|
||||
if err != nil {
|
||||
l.Infoln("Error loading config file; using defaults for now")
|
||||
myName, _ := os.Hostname()
|
||||
newCfg := defaultConfig(myName)
|
||||
cfg = config.Wrap(cfgFile, newCfg)
|
||||
|
||||
@@ -7,151 +7,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func TestFolderErrors(t *testing.T) {
|
||||
// This test intentionally avoids starting the folders. If they are
|
||||
// started, they will perform an initial scan, which will create missing
|
||||
// folder markers and race with the stuff we do in the test.
|
||||
|
||||
fcfg := config.FolderConfiguration{
|
||||
ID: "folder",
|
||||
RawPath: "testdata/testfolder",
|
||||
}
|
||||
cfg := config.Wrap("/tmp/test", config.Configuration{
|
||||
Folders: []config.FolderConfiguration{fcfg},
|
||||
})
|
||||
|
||||
for _, file := range []string{".stfolder", "testfolder/.stfolder", "testfolder"} {
|
||||
if err := os.Remove("testdata/" + file); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
// Case 1 - new folder, directory and marker created
|
||||
|
||||
m := model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err != nil {
|
||||
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
|
||||
}
|
||||
|
||||
s, err := os.Stat("testdata/testfolder")
|
||||
if err != nil || !s.IsDir() {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = os.Stat("testdata/testfolder/.stfolder")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := os.Remove("testdata/testfolder/.stfolder"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Remove("testdata/testfolder/"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Case 2 - new folder, marker created
|
||||
|
||||
fcfg.RawPath = "testdata/"
|
||||
cfg = config.Wrap("/tmp/test", config.Configuration{
|
||||
Folders: []config.FolderConfiguration{fcfg},
|
||||
})
|
||||
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err != nil {
|
||||
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
|
||||
}
|
||||
|
||||
_, err = os.Stat("testdata/.stfolder")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := os.Remove("testdata/.stfolder"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Case 3 - Folder marker missing
|
||||
|
||||
set := db.NewFileSet("folder", ldb)
|
||||
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
|
||||
{Name: "dummyfile"},
|
||||
})
|
||||
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder marker missing" {
|
||||
t.Error("Incorrect error: Folder marker missing !=", m.CheckFolderHealth("folder"))
|
||||
}
|
||||
|
||||
// Case 3.1 - recover after folder marker missing
|
||||
|
||||
if err = fcfg.CreateMarker(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err != nil {
|
||||
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
|
||||
}
|
||||
|
||||
// Case 4 - Folder path missing
|
||||
|
||||
if err := os.Remove("testdata/testfolder/.stfolder"); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Remove("testdata/testfolder"); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fcfg.RawPath = "testdata/testfolder"
|
||||
cfg = config.Wrap("testdata/subfolder", config.Configuration{
|
||||
Folders: []config.FolderConfiguration{fcfg},
|
||||
})
|
||||
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder path missing" {
|
||||
t.Error("Incorrect error: Folder path missing !=", m.CheckFolderHealth("folder"))
|
||||
}
|
||||
|
||||
// Case 4.1 - recover after folder path missing
|
||||
|
||||
if err := os.Mkdir("testdata/testfolder", 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder marker missing" {
|
||||
t.Error("Incorrect error: Folder marker missing !=", m.CheckFolderHealth("folder"))
|
||||
}
|
||||
|
||||
// Case 4.2 - recover after missing marker
|
||||
|
||||
if err = fcfg.CreateMarker(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err != nil {
|
||||
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortIDCheck(t *testing.T) {
|
||||
cfg := config.Wrap("/tmp/test", config.Configuration{
|
||||
Devices: []config.DeviceConfiguration{
|
||||
|
||||
@@ -31,8 +31,8 @@ func (c *mockedConfig) Options() config.OptionsConfiguration {
|
||||
return config.OptionsConfiguration{}
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Replace(cfg config.Configuration) config.CommitResponse {
|
||||
return config.CommitResponse{}
|
||||
func (c *mockedConfig) Replace(cfg config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Subscribe(cm config.Committer) {}
|
||||
@@ -48,3 +48,7 @@ func (c *mockedConfig) Devices() map[protocol.DeviceID]config.DeviceConfiguratio
|
||||
func (c *mockedConfig) Save() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) RequiresRestart() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func (m *mockedModel) ScanFolders() map[string]error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) ScanFolderSubs(folder string, subs []string) error {
|
||||
func (m *mockedModel) ScanFolderSubdirs(folder string, subs []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
@@ -59,7 +60,7 @@ func (c *folderSummaryService) Stop() {
|
||||
// listenForUpdates subscribes to the event bus and makes note of folders that
|
||||
// need their data recalculated.
|
||||
func (c *folderSummaryService) listenForUpdates() {
|
||||
sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged | events.RemoteDownloadProgress)
|
||||
sub := events.Default.Subscribe(events.LocalIndexUpdated | events.RemoteIndexUpdated | events.StateChanged | events.RemoteDownloadProgress | events.DeviceConnected)
|
||||
defer events.Default.Unsubscribe(sub)
|
||||
|
||||
for {
|
||||
@@ -67,8 +68,31 @@ func (c *folderSummaryService) listenForUpdates() {
|
||||
|
||||
select {
|
||||
case ev := <-sub.C():
|
||||
// Whenever the local or remote index is updated for a given
|
||||
// folder we make a note of it.
|
||||
if ev.Type == events.DeviceConnected {
|
||||
// When a device connects we schedule a refresh of all
|
||||
// folders shared with that device.
|
||||
|
||||
data := ev.Data.(map[string]string)
|
||||
deviceID, _ := protocol.DeviceIDFromString(data["id"])
|
||||
|
||||
c.foldersMut.Lock()
|
||||
nextFolder:
|
||||
for _, folder := range c.cfg.Folders() {
|
||||
for _, dev := range folder.Devices {
|
||||
if dev.DeviceID == deviceID {
|
||||
c.folders[folder.ID] = struct{}{}
|
||||
continue nextFolder
|
||||
}
|
||||
}
|
||||
}
|
||||
c.foldersMut.Unlock()
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// The other events all have a "folder" attribute that they
|
||||
// affect. Whenever the local or remote index is updated for a
|
||||
// given folder we make a note of it.
|
||||
|
||||
data := ev.Data.(map[string]interface{})
|
||||
folder := data["folder"].(string)
|
||||
|
||||
@@ -156,10 +156,26 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
|
||||
|
||||
/* modal dialogs */
|
||||
.modal-header {
|
||||
border-color: #222 !important;
|
||||
border-bottom-color: #222 !important;
|
||||
}
|
||||
|
||||
.modal-header:not(.alert) {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
color: #222 !important;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
color: #222 !important;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
color: #222 !important;
|
||||
background-color: #d62c1a !important;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-color: #666 !important;
|
||||
border-width: 2px !important;
|
||||
@@ -171,14 +187,6 @@ li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
|
||||
background-color: #111 !important;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #c29d0b !important;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #d62c1a !important;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
color: #aaa !important;
|
||||
}
|
||||
@@ -230,4 +238,8 @@ code.ng-binding{
|
||||
|
||||
.progress-bar-danger {
|
||||
background-color: #d62c1a !important;
|
||||
}
|
||||
}
|
||||
|
||||
.progress .frontal {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ ul+h5 {
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding-left: 20px !important;
|
||||
/*padding-left: 20px !important;*/
|
||||
}
|
||||
|
||||
.table td.small-data {
|
||||
@@ -191,6 +191,7 @@ button.panel-heading {
|
||||
border-top-width: 0;
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.panel-heading .panel-title-text {
|
||||
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -32,7 +32,7 @@
|
||||
"Comment, when used at the start of a line": "Comentar, quant s'utilitza al principi d'una línia",
|
||||
"Compression": "Compresió",
|
||||
"Connection Error": "Error de connexió",
|
||||
"Connection Type": "Connection Type",
|
||||
"Connection Type": "Tipus de connexió",
|
||||
"Copied from elsewhere": "Copiat de qualsevol lloc",
|
||||
"Copied from original": "Copiat de l'original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 els següents Col·laboradors:",
|
||||
@@ -75,7 +75,7 @@
|
||||
"Folder Label": "Etiqueta de la Carpeta",
|
||||
"Folder Master": "Carpeta principal",
|
||||
"Folder Path": "Ruta de la carpeta",
|
||||
"Folder Type": "Folder Type",
|
||||
"Folder Type": "Tipus de carpeta",
|
||||
"Folders": "Carpetes",
|
||||
"GUI": "IGU (Interfície Gràfica d'Usuari)",
|
||||
"GUI Authentication Password": "Password d'autenticació de l'Interfície Gràfica d'Usuari (GUI)",
|
||||
@@ -98,15 +98,15 @@
|
||||
"Keep Versions": "Mantindre versions",
|
||||
"Largest First": "El més gran primer",
|
||||
"Last File Received": "Darrer fitxer rebut",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "Últim escaneig",
|
||||
"Last seen": "Vist per última vegada",
|
||||
"Later": "Més tard",
|
||||
"Listeners": "Listeners",
|
||||
"Listeners": "Escoltants",
|
||||
"Local Discovery": "Descobriment local",
|
||||
"Local State": "Estat local",
|
||||
"Local State (Total)": "Estat Local (Total)",
|
||||
"Major Upgrade": "Actualització important",
|
||||
"Master": "Master",
|
||||
"Master": "Mestre",
|
||||
"Maximum Age": "Edat màxima",
|
||||
"Metadata Only": "Sols metadades",
|
||||
"Minimum Free Disk Space": "Espai minim de disc lliure",
|
||||
@@ -193,7 +193,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing pareix apagat o hi ha un problema amb la connexió a Internet. Tornant a intentar...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing pareix que té un problema processant la seua sol·licitud. Per favor, refresque la pàgina o reinicie Syncthing si el problema persistix.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfície d'administració de Syncthing està configurat per a permetre l'accés remot sense una contrasenya.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Les estadístiques agregades estàn disponibles en la URL que figura a continuació.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan disponibles públicament en {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració ha sigut gravada però no activada. Syncthing deu reiniciar per tal d'activar la nova configuració.",
|
||||
"The device ID cannot be blank.": "L'ID del dispositiu no pot estar buida.",
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Προσθήκη",
|
||||
"Add Device": "Προσθήκη συσκευής",
|
||||
"Add Folder": "Προσθήκη φακέλου",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add Remote Device": "Προσθήκη Απομακρυσμένης Συσκευής",
|
||||
"Add new folder?": "Προσθήκη νέου φακέλου;",
|
||||
"Address": "Διεύθυνση",
|
||||
"Addresses": "Διευθύνσεις",
|
||||
"Advanced": "Προχωρημένες",
|
||||
"Advanced Configuration": "Προχωρημένες ρυθμίσεις",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"Advanced settings": "Προχωρημένες ρυθμίσεις",
|
||||
"All Data": "Όλα τα δεδομένα",
|
||||
"Allow Anonymous Usage Reporting?": "Να επιτρέπεται η αποστολή ανώνυμων στοιχείων χρήσης;",
|
||||
"Alphabetic": "Αλφαβητικά",
|
||||
@@ -32,15 +32,15 @@
|
||||
"Comment, when used at the start of a line": "Σχόλιο, όταν χρησιμοποιείται στην αρχή μιας γραμμής",
|
||||
"Compression": "Συμπίεση",
|
||||
"Connection Error": "Σφάλμα σύνδεσης",
|
||||
"Connection Type": "Connection Type",
|
||||
"Connection Type": "Τύπος Σύνδεσης",
|
||||
"Copied from elsewhere": "Έχει αντιγραφεί από κάπου αλλού",
|
||||
"Copied from original": "Έχει αντιγραφεί από το πρωτότυπο",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 οι παρακάτω Συνεισφέροντες:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 από τους παρακάτω συνεισφορείς:",
|
||||
"Danger!": "Προσοχή!",
|
||||
"Delete": "Διαγραφή",
|
||||
"Deleted": "Διαγραμμένα",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Η συσκευή \"{{name}}\" ({{device}} στη {{address}}) επιθυμεί να συνδεθεί. Προσθήκη της νέας συσκευής?",
|
||||
"Device ID": "Ταυτότητα συσκευής",
|
||||
"Device Identification": "Ταυτότητα συσκευής",
|
||||
"Device Name": "Όνομα συσκευής",
|
||||
@@ -98,7 +98,7 @@
|
||||
"Keep Versions": "Διατήρηση εκδόσεων",
|
||||
"Largest First": "Το μεγαλύτερο πρώτα",
|
||||
"Last File Received": "Πιο πρόσφατο αρχείο",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "Τελευταία Σάρωση",
|
||||
"Last seen": "Τελευταία φορά συνδεδεμένος",
|
||||
"Later": "Αργότερα",
|
||||
"Listeners": "Listeners",
|
||||
@@ -193,7 +193,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Το Syncthing φαίνεται πως είναι απενεργοποιημένο ή υπάρχει πρόβλημα στη σύνδεσή σου στο διαδίκτυο. Προσπαθώ πάλι…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Το Syncthing φαίνεται να αντιμετωπίζει ένα πρόβλημα με την επεξεργασία του αιτήματός σου. Παρακαλούμε, αν το πρόβλημα συνεχίζει, ανανέωσε την σελίδα ή επανεκκίνησε το Syncthing.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Η διεπαφή διαχείρισης του Syncthing είναι ρυθμισμένη να επιτρέπει την πρόσβαση χωρίς κωδικό.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Τα στατιστικά που έχουν συλλεγεί είναι δημόσια διαθέσιμα στη παρακάτω διεύθυνση.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Τα στατιστικά που έχουν συλλεγεί είναι δημόσια διαθέσιμα στο {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Οι ρυθμίσεις έχουν αποθηκευτεί αλλά δεν έχουν ενεργοποιηθεί. Πρέπει να επανεκκινήσεις το Syncthing για να ισχύσουν οι νέες ρυθμίσεις.",
|
||||
"The device ID cannot be blank.": "Η ταυτότητα της συσκευής δεν μπορεί να είναι κενή",
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"A device with that ID is already added.": "A device with that ID is already added.",
|
||||
"A device with that ID is already added.": "La machine portant cette ID est déjà présente.",
|
||||
"A negative number of days doesn't make sense.": "Un nombre négatif de jours n'a pas de sens.",
|
||||
"A new major version may not be compatible with previous versions.": "Une nouvelle version majeure peut présenter des incompatibilités avec les versions antérieures.",
|
||||
"API Key": "Clé API",
|
||||
"About": "À propos",
|
||||
"Actions": "Actions",
|
||||
"Add": "Ajouter",
|
||||
"Add Device": "Ajouter un périphérique",
|
||||
"Add Folder": "Ajouter un répertoire",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "Ajouter un nouveau dossier ?",
|
||||
"Add Device": "Ajouter une machine",
|
||||
"Add Folder": "Ajouter un partage",
|
||||
"Add Remote Device": "Ajouter une machine",
|
||||
"Add new folder?": "Ajouter un nouveau partage ?",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresses",
|
||||
"Advanced": "Avancé",
|
||||
"Advanced Configuration": "Configuration avancée",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"Advanced settings": "Réglages experts",
|
||||
"All Data": "Toutes les données",
|
||||
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation ?",
|
||||
"Alphabetic": "Alphabétique",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Une commande externe gère les versions de fichiers. Elle supprime les fichiers dans le dossier synchronisé.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Une commande externe gère les versions de fichiers. Elle supprime les fichiers dans le répertoire synchronisé.",
|
||||
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Toute machine ajoutée depuis une machine introductrice sera aussi ajoutée sur cette machine.",
|
||||
"Automatic upgrades": "Mises à jour automatiques",
|
||||
@@ -32,32 +32,32 @@
|
||||
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
|
||||
"Compression": "Compression",
|
||||
"Connection Error": "Erreur de connexion",
|
||||
"Connection Type": "Connection Type",
|
||||
"Connection Type": "Type de connexion",
|
||||
"Copied from elsewhere": "Copié d'ailleurs",
|
||||
"Copied from original": "Copié depuis l'original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Les contributeurs suivants, Copyright © 2014-2016 :",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 Les contributeurs suivants:",
|
||||
"Danger!": "Danger!",
|
||||
"Danger!": "Attention !",
|
||||
"Delete": "Supprimer",
|
||||
"Deleted": "Supprimé",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "ID du périphérique",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "La machine \"{{name}}\" ({{device}} à l'IP {{address}}) souhaite se connecter. L'acceptez-vous ?",
|
||||
"Device ID": "ID de la machine",
|
||||
"Device Identification": "Identification de l'appareil",
|
||||
"Device Name": "Nom du périphérique",
|
||||
"Device Name": "Nom de la machine",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "L'appareil {{device}} ({{address}}) veut se connecter. Voulez-vous ajouter cette appareil ?",
|
||||
"Devices": "Appareil",
|
||||
"Disconnected": "Déconnecté",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "Découverte",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Débit de réception",
|
||||
"Downloaded": "Téléchargé",
|
||||
"Downloading": "En cours de téléchargement",
|
||||
"Edit": "Éditer",
|
||||
"Edit Device": "Éditer le périphérique",
|
||||
"Edit Folder": "Éditer le répertoire",
|
||||
"Edit": "Modifier",
|
||||
"Edit Device": "Modifier la machine",
|
||||
"Edit Folder": "Modifier le partage",
|
||||
"Editing": "Édition",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable NAT traversal": "Activer transfert d'adresses NAT",
|
||||
"Enable Relaying": "Activer le relayage",
|
||||
"Enable UPnP": "Activer l'UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://host:port\") séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
|
||||
"Enter ignore patterns, one per line.": "Entrer les masques de filtrage, un par ligne.",
|
||||
@@ -67,16 +67,16 @@
|
||||
"File Pull Order": "Ordre de récupération de fichier",
|
||||
"File Versioning": "Versions de fichier",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Les bits de permission de fichier sont ignorés lors de la recherche de changements. Utilisé sur les systèmes de fichiers FAT.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Les fichiers sont déplacés vers le dossier .stversions quand ils sont remplacés ou effacés par Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Les fichiers sont déplacés, avec horodatage, dans le dossier .stversions quand ils sont remplacés ou supprimés par Syncthing.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Les fichiers sont déplacés vers le répertoire .stversions quand ils sont remplacés ou effacés par Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Les fichiers sont déplacés, avec horodatage, dans le répertoire .stversions quand ils sont remplacés ou supprimés par 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.": "Les fichiers sont protégés des changements réalisés sur les autres appareils, mais les changements réalisés sur cet appareil seront transférés aux autres appareils.",
|
||||
"Folder": "Dossier",
|
||||
"Folder ID": "ID du répertoire",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Répertoire maître",
|
||||
"Folder Path": "Chemin du répertoire",
|
||||
"Folder Type": "Folder Type",
|
||||
"Folders": "Dossiers",
|
||||
"Folder": "Partage",
|
||||
"Folder ID": "ID du partage",
|
||||
"Folder Label": "Étiquette du partage",
|
||||
"Folder Master": "Partage maître",
|
||||
"Folder Path": "Chemin racine du partage",
|
||||
"Folder Type": "Type de partage",
|
||||
"Folders": "Partages",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "Mot de passe d'authentification GUI",
|
||||
"GUI Authentication User": "Utilisateur autorisé GUI",
|
||||
@@ -84,29 +84,29 @@
|
||||
"Generate": "Générer",
|
||||
"Global Discovery": "Recherche globale",
|
||||
"Global Discovery Server": "Serveur global de recherche",
|
||||
"Global Discovery Servers": "Global Discovery Servers",
|
||||
"Global Discovery Servers": "Serveurs de découverte globale",
|
||||
"Global State": "État global",
|
||||
"Help": "Aide",
|
||||
"Home page": "Page d'accueil",
|
||||
"Ignore": "Ignorer",
|
||||
"Ignore Patterns": "Modèles à éviter",
|
||||
"Ignore Patterns": "Masques d'exclusion",
|
||||
"Ignore Permissions": "Ignorer les permissions",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite du débit entrant (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Une configuration incorrecte peut créer des dommages dans vos dossiers et mettre hors-service Syncthing",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Une configuration incorrecte peut créer des dommages dans vos répertoires et mettre Syncthing hors-service.",
|
||||
"Introducer": "Initiateur",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inverser la condition donnée (i.e. ne pas exclure)",
|
||||
"Keep Versions": "Conserver les versions",
|
||||
"Largest First": "Les plus volumineux en premier",
|
||||
"Last File Received": "Dernier fichier reçu",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "Dernière analyse",
|
||||
"Last seen": "Dernière apparition",
|
||||
"Later": "Plus tard",
|
||||
"Listeners": "Listeners",
|
||||
"Listeners": "Systèmes à l'écoute",
|
||||
"Local Discovery": "Recherche locale",
|
||||
"Local State": "État local",
|
||||
"Local State (Total)": "État local (Total)",
|
||||
"Major Upgrade": "Mise à jour majeure",
|
||||
"Master": "Master",
|
||||
"Master": "Maître",
|
||||
"Maximum Age": "Ancienneté maximum",
|
||||
"Metadata Only": "Métadonnées uniquement",
|
||||
"Minimum Free Disk Space": "Espace disque libre minimum",
|
||||
@@ -114,7 +114,7 @@
|
||||
"Multi level wildcard (matches multiple directory levels)": "Astérisque à plusieurs niveaux (correspond aux répertoires et sous-répertoires)",
|
||||
"Never": "Jamais",
|
||||
"New Device": "Nouvel appareil",
|
||||
"New Folder": "Nouveau dossier",
|
||||
"New Folder": "Nouveau partage",
|
||||
"Newest First": "Les plus récents en premier",
|
||||
"No": "Non",
|
||||
"No File Versioning": "Pas de version de fichier",
|
||||
@@ -123,60 +123,60 @@
|
||||
"OK": "OK",
|
||||
"Off": "Éteint",
|
||||
"Oldest First": "Les plus anciens en premier",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Étiquette conviviale pour le partage. Elle peut être différente sur chaque machine.",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Désynchronisé",
|
||||
"Out of Sync Items": "Objets non synchronisés",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite du débit sortant (KiB/s)",
|
||||
"Override Changes": "Écraser les changements",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Le chemin du dossier sur l'ordinateur local sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Le chemin du répertoire sur l'ordinateur local sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
|
||||
"Pause": "Pause",
|
||||
"Paused": "En pause",
|
||||
"Please consult the release notes before performing a major upgrade.": "Veuillez consulter les notes de version avant de réaliser une mise à jour majeure.",
|
||||
"Please 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.": "Veuillez définir un nom d'utilisateur et un mot de passe dans les réglages.",
|
||||
"Please wait": "Merci de patienter",
|
||||
"Preview": "Aperçu",
|
||||
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
|
||||
"Quick guide to supported patterns": "Guide rapide des masques supportés",
|
||||
"RAM Utilization": "Utilisation de la RAM",
|
||||
"Random": "Aléatoire",
|
||||
"Relay Servers": "Relay Servers",
|
||||
"Relay Servers": "Serveurs relais",
|
||||
"Relayed via": "Relayée par",
|
||||
"Relays": "Relais",
|
||||
"Release Notes": "Notes de version",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remote Devices": "Machines distantes",
|
||||
"Remove": "Enlever",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Rescanner",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Identifiant obligatoire du partage. Il doit être identique chez chaque participant.",
|
||||
"Rescan": "Réanalyser",
|
||||
"Rescan All": "Réanalyser tout",
|
||||
"Rescan Interval": "Intervalle de scan",
|
||||
"Rescan Interval": "Intervalle d'analyse",
|
||||
"Restart": "Redémarrer",
|
||||
"Restart Needed": "Redémarrage nécessaire",
|
||||
"Restarting": "Redémarrage",
|
||||
"Resume": "Résumer",
|
||||
"Reused": "Réutilisé",
|
||||
"Save": "Sauver",
|
||||
"Scan Time Remaining": "Scan Time Remaining",
|
||||
"Scanning": "En cours de scan",
|
||||
"Select the devices to share this folder with.": "Sélectionner les appareils avec qui partager ce dossier.",
|
||||
"Select the folders to share with this device.": "Sélectionner les dossiers à partager avec cet appareil.",
|
||||
"Scan Time Remaining": "Temps d'analyse restant",
|
||||
"Scanning": "Analyse en cours",
|
||||
"Select the devices to share this folder with.": "Sélectionner les machines invitées à ce partage.",
|
||||
"Select the folders to share with this device.": "Sélectionner les partages auxquels cette machine participe.",
|
||||
"Settings": "Configuration",
|
||||
"Share": "Partager",
|
||||
"Share Folder": "Partager le dossier",
|
||||
"Share Folders With Device": "Partager des dossiers avec des appareils",
|
||||
"Share Folder": "Partager",
|
||||
"Share Folders With Device": "Partages avec cette machine",
|
||||
"Share With Devices": "Partage avec des appareils",
|
||||
"Share this folder?": "Voulez-vous partager ce dossier ?",
|
||||
"Share this folder?": "Acceptez-vous ce partage ?",
|
||||
"Shared With": "Partagé avec",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du dossier. Il doit être le même sur l'ensemble des appareils du groupe.",
|
||||
"Show ID": "Montrer l'ID",
|
||||
"Show QR": "Show QR",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du partage. Il sera le même sur l'ensemble des machines du groupe.",
|
||||
"Show ID": "Afficher mon ID",
|
||||
"Show QR": "Afficher l'image QR",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de l'appareil dans le groupe. Sera proposé aux autres appareils comme nom optionnel par défaut.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de l'appareil dans le groupe. Si laissé vide, il sera mis à jour par le nom proposé par l'appareil distant.",
|
||||
"Shutdown": "Éteindre",
|
||||
"Shutdown Complete": "Extinction terminée",
|
||||
"Shutdown": "Arrêter",
|
||||
"Shutdown Complete": "Arrêté !",
|
||||
"Simple File Versioning": "Suivi simple des versions de fichier",
|
||||
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à l’intérieur du dossier)",
|
||||
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à l’intérieur du répertoire)",
|
||||
"Smallest First": "Les plus petits en premier",
|
||||
"Source Code": "Code source",
|
||||
"Staggered File Versioning": "Versions échelonnées de fichier",
|
||||
@@ -192,20 +192,20 @@
|
||||
"Syncthing is upgrading.": "Syncthing est cours de mise à jour.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plait, rafraichissez la page ou redémarrer Syncthing si le problème persiste.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interface d'administration de Syncthing est configuré pour accepter l'accès distant sans mot de passe !",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Les statistiques aggrégées sont publiquement disponibles à l'adresse ci-dessous.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
|
||||
"The device ID cannot be blank.": "L'ID de l'appareil ne peut être vide.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de l'appareil à entrer peut être trouvé dans le menu \"Éditer > Montrer l'ID\" des autres appareils. Les espaces et les tirets sont optionnels (ils seront ignorés).",
|
||||
"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.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des dossiers et les versions de l'application. Si les données rapportées sont modifiées cette boite de dialogue vous redemandera votre confirmation.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de machine à saisir ici se trouve dans le menu \"Actions > Afficher mon ID\" de la machine distante. Les tirets et espaces sont optionnels (et ignorés).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de machine à entrer se trouve dans le menu \"Éditer > Montrer l'ID\" de la machine distante. Les espaces et les tirets sont optionnels (ils seront ignorés).",
|
||||
"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.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des partages et les versions de l'application. Si les données rapportées sont modifiées cette boite de dialogue vous redemandera votre confirmation.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID de l'appareil inséré ne semble pas être valide. Il devrait ressembler à une chaîne de 52 ou 56 caractères comprenant des lettres, des chiffres et potentiellement des espaces et des traits d'union.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Le premier paramètre de ligne de commande est le chemin du dossier, et le second est le chemin relatif dans le dossier.",
|
||||
"The folder ID cannot be blank.": "L'identifiant (ID) du dossier ne peut être vide.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "L'ID du dossier doit être un identifiant court (64 caractères ou moins) comprenant uniquement des lettres, chiffre, points (.), traits d'union (-) et tirets bas (_).",
|
||||
"The folder ID must be unique.": "L'ID du répertoire doit être unique.",
|
||||
"The folder path cannot be blank.": "Le chemin du répertoire ne peut pas être vide.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Le premier paramètre de ligne de commande est le chemin du répertoire partagé, et le second est le chemin relatif dans le répertoire.",
|
||||
"The folder ID cannot be blank.": "L'identifiant du partage ne peut être vide.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "L'ID du partage doit être un identifiant court (64 caractères ou moins) comprenant uniquement des lettres, chiffre, points (.), traits d'union (-) et tirets bas (_).",
|
||||
"The folder ID must be unique.": "L'ID du partage doit être unique.",
|
||||
"The folder path cannot be blank.": "Le chemin vers le répertoire ne peut pas être vide.",
|
||||
"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.": "Les intervalles suivant sont utilisés: la première heure une version est conservée chaque 30 secondes, le premier jour une version est conservée chaque heure, les premiers 30 jours une version est conservée chaque jour, jusqu'à la limite d'âge maximum une version est conservée chaque semaine.",
|
||||
"The following items could not be synchronized.": "Les éléments suivants ne peuvent pas être synchronisés.",
|
||||
"The maximum age must be a number and cannot be blank.": "L'âge maximum doit être un nombre et ne peut être vide.",
|
||||
@@ -219,8 +219,8 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "La limite de débit ne doit pas être négative (0: Aucune limite)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "L'intervalle d'analyse ne doit pas être un nombre négatif de secondes.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Ils seront réessayés automatiquement et synchronisés quand l'erreur sera résolue.",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
|
||||
"This Device": "Cette machine",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Ceci peut aisément permettre à un intrus de lire et modifier n'importe quel fichier de votre ordinateur. ",
|
||||
"This is a major version upgrade.": "Ceci est une mise à jour majeure.",
|
||||
"Trash Can File Versioning": "Gestion des versions de fichier style poubelle.",
|
||||
"Unknown": "Inconnu",
|
||||
@@ -237,15 +237,15 @@
|
||||
"Version": "Version",
|
||||
"Versions Path": "Emplacement des versions",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions seront supprimées automatiquement, si elles dépassent la durée maximum de conservation, ou si leur nombre est supérieur à la valeur autorisée dans l'intervalle.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Attention, ce chemin est un sous-répertoire du partage existant \"{{otherFolder}}\". Ceci peut causer des problèmes tels que duplications de fichiers ou suppressions intempestives sur les autres machines.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Lorsqu'un appareil est ajouté, gardez à l'esprit que cet appareil doit aussi être ajouté de l'autre coté.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que son ID est utilisé pour lier les répertoires à travers les appareils. Les ID sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lorsqu'un nouveau partage est ajouté, gardez à l'esprit que son ID est utilisée pour lier les répertoires à travers les machines. L'ID est sensible à la casse et sera forcément la même sur toutes les machines participant à ce partage.",
|
||||
"Yes": "Oui",
|
||||
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
||||
"days": "Jours",
|
||||
"full documentation": "documentation complète",
|
||||
"items": "éléments",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folderLabel}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -40,7 +40,7 @@
|
||||
"Danger!": "Attention !",
|
||||
"Delete": "Supprimer",
|
||||
"Deleted": "Supprimé",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "La machine \"{{name}}\" ({{device}} sur {{address}}) veut se connecter. Ajouter cette nouvelle machine ?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "La machine \"{{name}}\" ({{device}} à {{address}}) veut se connecter. L'acceptez-vous ?",
|
||||
"Device ID": "ID de la machine",
|
||||
"Device Identification": "Identifiant de la machine",
|
||||
"Device Name": "Nom de la machine",
|
||||
@@ -52,13 +52,13 @@
|
||||
"Download Rate": "Vitesse de réception",
|
||||
"Downloaded": "Téléchargé",
|
||||
"Downloading": "En cours de téléchargement",
|
||||
"Edit": "Éditer",
|
||||
"Edit Device": "Éditer la machine",
|
||||
"Edit Folder": "Éditer le dossier",
|
||||
"Editing": "Édition",
|
||||
"Enable NAT traversal": "Activer le transfert NAT",
|
||||
"Enable Relaying": "Activer le relayage",
|
||||
"Enable UPnP": "Activer l'UPnP",
|
||||
"Edit": "Modifier",
|
||||
"Edit Device": "Modifier la machine",
|
||||
"Edit Folder": "Modifier le dossier",
|
||||
"Editing": "Modifications",
|
||||
"Enable NAT traversal": "Activer la translation d'adresses (NAT)",
|
||||
"Enable Relaying": "Relayage possible",
|
||||
"Enable UPnP": "Activer UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://host:port\") séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
|
||||
"Enter ignore patterns, one per line.": "Entrer les masques de filtrage, un par ligne.",
|
||||
"Error": "Erreur",
|
||||
@@ -72,10 +72,10 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Les fichiers sont protégés des changements réalisés sur les autres machines, mais les changements réalisés sur celle-ci seront transférés aux autres machines.",
|
||||
"Folder": "Dossier",
|
||||
"Folder ID": "ID du dossier",
|
||||
"Folder Label": "Étiquette du dossier",
|
||||
"Folder Label": "Étiquette du partage",
|
||||
"Folder Master": "Dossier maître",
|
||||
"Folder Path": "Chemin du dossier",
|
||||
"Folder Type": "Type de répertoire",
|
||||
"Folder Type": "Type de partage",
|
||||
"Folders": "Dossiers",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "Mot de passe d'authentification GUI",
|
||||
@@ -89,7 +89,7 @@
|
||||
"Help": "Aide",
|
||||
"Home page": "Page d'accueil",
|
||||
"Ignore": "Ignorer",
|
||||
"Ignore Patterns": "Modèles à éviter",
|
||||
"Ignore Patterns": "Masques d'exclusion",
|
||||
"Ignore Permissions": "Ignorer les permissions",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite du débit de réception (Ko/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Une configuration incorrecte peut créer des dommages dans vos dossiers et mettre hors-service Syncthing",
|
||||
@@ -106,7 +106,7 @@
|
||||
"Local State": "État local",
|
||||
"Local State (Total)": "État local (Total)",
|
||||
"Major Upgrade": "Mise à jour majeure",
|
||||
"Master": "Maitre",
|
||||
"Master": "Maître",
|
||||
"Maximum Age": "Ancienneté maximum",
|
||||
"Metadata Only": "Métadonnées uniquement",
|
||||
"Minimum Free Disk Space": "Espace disque libre minimum",
|
||||
@@ -123,7 +123,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Éteint",
|
||||
"Oldest First": "Les plus anciens en premier",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Étiquette optionnelle pour le dossier. Peut être différente pour chaque machine.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Étiquette optionnelle pour le partage. Peut être différente sur chaque machine.",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Désynchronisé",
|
||||
"Out of Sync Items": "Fichiers non synchronisés",
|
||||
@@ -134,7 +134,7 @@
|
||||
"Pause": "Pause",
|
||||
"Paused": "En pause",
|
||||
"Please consult the release notes before performing a major upgrade.": "Veuillez consulter les notes de version avant de réaliser une mise à jour majeure.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "SVP, mettez un nom d'utilisateur et un mot de passe dans la fenêtre de paramétrage.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Veuillez définir un nom d'utilisateur et un mot de passe dans la fenêtre de Configuration.",
|
||||
"Please wait": "Merci de patienter",
|
||||
"Preview": "Aperçu",
|
||||
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
|
||||
@@ -147,7 +147,7 @@
|
||||
"Release Notes": "Notes de version",
|
||||
"Remote Devices": "Machines distantes",
|
||||
"Remove": "Enlever",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Identifiant pour le dossier. Doit être le même sur l'ensemble des machines du cluster.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Identifiant du partage. Doit être le même sur l'ensemble des machines concernées.",
|
||||
"Rescan": "Réanalyse",
|
||||
"Rescan All": "Réanalyser tout",
|
||||
"Rescan Interval": "Intervalle d'analyse",
|
||||
@@ -157,7 +157,7 @@
|
||||
"Resume": "Résumer",
|
||||
"Reused": "Réutilisé",
|
||||
"Save": "Sauver",
|
||||
"Scan Time Remaining": "Intervalle entre chaque analyse",
|
||||
"Scan Time Remaining": "Temps d'analyse restant",
|
||||
"Scanning": "Analyse en cours",
|
||||
"Select the devices to share this folder with.": "Sélectionner les machines avec qui partager ce dossier.",
|
||||
"Select the folders to share with this device.": "Sélectionner les dossiers à partager avec cette machine.",
|
||||
@@ -169,12 +169,12 @@
|
||||
"Share this folder?": "Voulez-vous partager ce dossier ?",
|
||||
"Shared With": "Partagé avec",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du dossier. Il doit être le même sur l'ensemble des machines du groupe.",
|
||||
"Show ID": "Afficher l'ID",
|
||||
"Show ID": "Afficher mon ID",
|
||||
"Show QR": "Afficher le QR",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de la machine dans le groupe. Sera proposé aux autres machines comme nom optionnel par défaut.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de la machine dans le groupe. Si laissé vide, il sera mis à jour par le nom proposé par la machine distante.",
|
||||
"Shutdown": "Éteindre",
|
||||
"Shutdown Complete": "Extinction terminée",
|
||||
"Shutdown": "Arrêter",
|
||||
"Shutdown Complete": "Arrêté !",
|
||||
"Simple File Versioning": "Suivi simple des versions de fichier",
|
||||
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à l’intérieur du dossier)",
|
||||
"Smallest First": "Les plus petits en premier",
|
||||
@@ -192,13 +192,13 @@
|
||||
"Syncthing is upgrading.": "Syncthing est cours de mise à jour.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plait, rafraichissez la page ou redémarrer Syncthing si le problème persiste.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interface d'administration de Syncthing est paramétrée pour autoriser les accès à distance sans mot de passe.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interface d'administration de Syncthing est paramétrée pour autoriser les accès à distance sans mot de passe !!!",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Les statistiques agrégées sont disponibles publiquement à l'adresse ci-dessous.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
|
||||
"The device ID cannot be blank.": "L'ID de la machine ne peut être vide.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de la machine à indiquer ici se trouve dans \"Actions > Afficher ID\" sur l'autre machine. Espaces et tirets sont optionnels (ignorés).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de la machine à entrer peut être trouvé dans le menu \"Éditer > Montrer l'ID\" des autres machines. Les espaces et les tirets sont optionnels (ils seront ignorés).",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de machine à saisir se trouve dans le menu \"Actions > Afficher mon ID\" de la machine distante. Espaces et tirets sont optionnels (ignorés).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de machine à saisir se trouve dans le menu \"Modifications > Afficher mon ID\" de la machine distante. Les espaces et les tirets sont optionnels (ils seront ignorés).",
|
||||
"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.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des dossiers et les versions de l'application. Si les données rapportées sont modifiées cette boite de dialogue vous redemandera votre confirmation.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID de la machine inséré ne semble pas être valide. Il devrait ressembler à une chaîne de 52 ou 56 caractères comprenant des lettres, des chiffres et potentiellement des espaces et des traits d'union.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Le premier paramètre de ligne de commande est le chemin du dossier, et le second est le chemin relatif dans le dossier.",
|
||||
@@ -220,7 +220,7 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "L'intervalle d'analyse ne doit pas être un nombre négatif de secondes.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Ils seront réessayés automatiquement et synchronisés quand l'erreur sera résolue.",
|
||||
"This Device": "Cette machine",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Cela permet facilement aux pirates de lire et modifier n'importe quel fichier de votre machine.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Ceci peut aisément permettre à un intrus de lire et modifier n'importe quel fichier de votre ordinateur.",
|
||||
"This is a major version upgrade.": "Ceci est une mise à jour majeure.",
|
||||
"Trash Can File Versioning": "Gestion des versions de fichier style poubelle.",
|
||||
"Unknown": "Inconnu",
|
||||
@@ -237,7 +237,7 @@
|
||||
"Version": "Version",
|
||||
"Versions Path": "Emplacement des versions",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions seront supprimées automatiquement, si elles dépassent la durée maximum de conservation, ou si leur nombre est supérieur à la valeur autorisée dans l'intervalle.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Attention, ce chemin est un sous-répertoire du dossier existant \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "[b]Attention, ce chemin est un sous-répertoire du partage existant \"{{otherFolder}}\". Ceci peut causer des problèmes tels que duplications de fichiers ou suppressions intempestives sur les autres machines.[/b]",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Lorsqu'une machine est ajoutée, gardez à l'esprit que cette machine doit aussi être ajoutée de l'autre coté.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lorsqu'un nouveau dossier est ajouté, gardez à l'esprit que son ID est utilisé pour lier les dossiers à travers les machines. Les ID sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
|
||||
"Yes": "Oui",
|
||||
@@ -246,6 +246,6 @@
|
||||
"full documentation": "documentation complète",
|
||||
"items": "fichiers",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} veut partager le dossier \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} veut partager le dossier \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -33,8 +33,8 @@
|
||||
"Compression": "Tömörítés",
|
||||
"Connection Error": "Kapcsolódási hiba",
|
||||
"Connection Type": "Kapcsolat típus",
|
||||
"Copied from elsewhere": "Másolva máshonnan",
|
||||
"Copied from original": "Másolva az eredetiről",
|
||||
"Copied from elsewhere": "Máshonnan másolva",
|
||||
"Copied from original": "Eredetiről másolva",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Szerzői jog © 2014-2016 az alábbi közreműködők:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 az alábbi közreműködők:",
|
||||
"Danger!": "Veszély!",
|
||||
@@ -210,7 +210,7 @@
|
||||
"The following items could not be synchronized.": "A következő elemek nem szinkronizálhatóak.",
|
||||
"The maximum age must be a number and cannot be blank.": "A maximális kornak számnak kell lenni és nem lehet üres",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "A verziók megtartásának maximális ideje (napokban, ha 0-t adsz meg örökre megmaradnak).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "A minimális szabad terület százalékos. nem-negatív érték 0 és 100 között",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "A minimális szabad terület százalékos, nem-negatív értéke 0 és 100 között",
|
||||
"The number of days must be a number and cannot be blank.": "A napok száma szám kell legyen és nem lehet üres.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "A napok száma ameddig a fájlok meg lesznek tartva a szemetesben. A 0 azt jelenti örökre.",
|
||||
"The number of old versions to keep, per file.": "A megtartott régi verziók száma, fájlonként.",
|
||||
@@ -220,7 +220,7 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Az átnézési intervallum nullánál nagyobb másodperc érték kell legyen",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "A hiba javítása után automatikusan újra megpróbálja a szinkronizálást.",
|
||||
"This Device": "Ez az eszköz",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Így a hekkerek könnyedén hozzáférhetnek a számítógépen található fájlokhoz. ",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Így a hekkerek könnyedén hozzáférést szerezhetnek a gépeden tárolt fájlok olvasásához és módosításához.",
|
||||
"This is a major version upgrade.": "Ez egy főverzió frissítés.",
|
||||
"Trash Can File Versioning": "Szemetes fájl verziókövetés",
|
||||
"Unknown": "Ismeretlen",
|
||||
@@ -241,7 +241,7 @@
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Amikor új eszközt adsz hozzá, tartsd észben, hogy a másik oldalon ezt az eszközt is hozzá kell adni.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Amikor új mappát adsz hozzá, tartsd észben, hogy a mappa azonosító arra való hogy összekösd a mappákat az eszközeiden. Az azonosító kisbetű-nagybetű érzékeny és pontosan egyeznie kell az eszközökön.",
|
||||
"Yes": "Igen",
|
||||
"You must keep at least one version.": "Legalább egy verziót meg kell tartanod",
|
||||
"You must keep at least one version.": "Legalább egy verziót meg kell tartanod.",
|
||||
"days": "nap",
|
||||
"full documentation": "teljes dokumentáció",
|
||||
"items": "elem",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"Address": "Indirizzo",
|
||||
"Addresses": "Indirizzi",
|
||||
"Advanced": "Avanzato",
|
||||
"Advanced Configuration": "Configurazione avanzata",
|
||||
"Advanced Configuration": "Configurazione Avanzata",
|
||||
"Advanced settings": "Impostazioni avanzate",
|
||||
"All Data": "Tutti i Dati",
|
||||
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
|
||||
@@ -56,35 +56,35 @@
|
||||
"Edit Device": "Modifica Dispositivo",
|
||||
"Edit Folder": "Modifica Cartella",
|
||||
"Editing": "Modifica di",
|
||||
"Enable NAT traversal": "Abilita NAT trasversale",
|
||||
"Enable Relaying": "Abilita relaying",
|
||||
"Enable NAT traversal": "Abilita NAT traversal",
|
||||
"Enable Relaying": "Abilita Reindirizzamento",
|
||||
"Enable UPnP": "Attiva UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserisci indirizzi separati da virgola (\"tcp://ip:porta\", \"tcp://host:porta\") oppure \"dynamic\" per effettuare il rilevamento automatico dell'indirizzo.",
|
||||
"Enter ignore patterns, one per line.": "Inserisci gli schemi di esclusione, uno per riga.",
|
||||
"Error": "Errore",
|
||||
"External File Versioning": "Controllo Versione Esterno",
|
||||
"Failed Items": "Elementi errati",
|
||||
"File Pull Order": "Ordine di prelievo dei file",
|
||||
"File Versioning": "Controllo Versione dei File",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Il software evita i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
|
||||
"Failed Items": "Elementi Errati",
|
||||
"File Pull Order": "Ordine Prelievo File",
|
||||
"File Versioning": "Controllo Versione File",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Il software ignora i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "I file sono spostati nella certella .stversions quando vengono sostituiti o cancellati da Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "I file sostituiti o eliminati da Syncthing vengono datati e spostati in una cartella .stversions.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "I file sono protetti dalle modifiche effettuate negli altri dispositivi, ma le modifiche effettuate in questo dispositivo verranno inviate anche al resto del cluster.",
|
||||
"Folder": "Cartella",
|
||||
"Folder ID": "ID Cartella",
|
||||
"Folder Label": "Etichetta per la cartella",
|
||||
"Folder Label": "Etichetta per la Cartella",
|
||||
"Folder Master": "Cartella Principale",
|
||||
"Folder Path": "Percorso Cartella",
|
||||
"Folder Type": "Tipo di Cartella",
|
||||
"Folders": "Cartelle",
|
||||
"GUI": "Interfaccia grafica utente",
|
||||
"GUI Authentication Password": "Password di Autenticazione dell'Utente",
|
||||
"GUI": "Interfaccia Grafica Utente",
|
||||
"GUI Authentication Password": "Password dell'Interfaccia Grafica",
|
||||
"GUI Authentication User": "Utente dell'Interfaccia Grafica",
|
||||
"GUI Listen Addresses": "Indirizzi dell'Interfaccia Grafica",
|
||||
"Generate": "Genera",
|
||||
"Global Discovery": "Individuazione Globale",
|
||||
"Global Discovery Server": "Server di Individuazione Globale",
|
||||
"Global Discovery Servers": "Servers di Individuazione Globale",
|
||||
"Global Discovery Servers": "Server di Individuazione Globale",
|
||||
"Global State": "Stato Globale",
|
||||
"Help": "Aiuto",
|
||||
"Home page": "Pagina home",
|
||||
@@ -92,7 +92,7 @@
|
||||
"Ignore Patterns": "Schemi Esclusione File",
|
||||
"Ignore Permissions": "Ignora Permessi",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite Velocità in Ingresso (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configurazione incorretta potrebbe danneggiare il contenuto delle cartelle e rendere Syncthing inoperativo.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configurazione non corretta potrebbe danneggiare il contenuto delle cartelle e rendere Syncthing inoperativo.",
|
||||
"Introducer": "Introduttore",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversione della condizione indicata (ad es. non escludere)",
|
||||
"Keep Versions": "Versioni Mantenute",
|
||||
@@ -105,13 +105,13 @@
|
||||
"Local Discovery": "Individuazione Locale",
|
||||
"Local State": "Stato Locale",
|
||||
"Local State (Total)": "Stato Locale (Totale)",
|
||||
"Major Upgrade": "Aggiornamento principale",
|
||||
"Major Upgrade": "Aggiornamento Principale",
|
||||
"Master": "Principale",
|
||||
"Maximum Age": "Durata Massima",
|
||||
"Metadata Only": "Solo i Metadati",
|
||||
"Minimum Free Disk Space": "Minimo spazio libero su disco",
|
||||
"Minimum Free Disk Space": "Minimo Spazio Libero su Disco",
|
||||
"Move to top of queue": "Posiziona in cima alla coda",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Metacarattere multi-livello (corrisponde alle cartelle e alle sotto-cartelle)",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Metacarattere multi-livello (per corrispondenze in più livelli di cartelle)",
|
||||
"Never": "Mai",
|
||||
"New Device": "Nuovo Dispositivo",
|
||||
"New Folder": "Nuova Cartella",
|
||||
@@ -122,29 +122,29 @@
|
||||
"Notice": "Avviso",
|
||||
"OK": "OK",
|
||||
"Off": "Disattiva",
|
||||
"Oldest First": "Prima il meno recente",
|
||||
"Oldest First": "Prima il Meno Recente",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Etichetta descrittiva facoltativa della cartella. Può essere diversa su ogni dispositivo.",
|
||||
"Options": "Opzioni",
|
||||
"Out of Sync": "Non sincronizzato",
|
||||
"Out of Sync Items": "Elementi Non Sincronizzati",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite Velocità in Uscita (KiB/s)",
|
||||
"Override Changes": "Ignora Modifiche",
|
||||
"Override Changes": "Ignora le Modifiche",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Percorso della cartella nel computer locale. Verrà creata se non esiste già. Il carattere tilde (~) può essere utilizzato come scorciatoia per",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Percorso di salvataggio delle versioni (lasciare vuoto per utilizzare la cartella predefinita .stversions in questa cartella).",
|
||||
"Pause": "Pausa",
|
||||
"Paused": "In pausa",
|
||||
"Paused": "In Pausa",
|
||||
"Please consult the release notes before performing a major upgrade.": "Si prega di consultare le note di rilascio prima di eseguire un aggiornamento principale.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Per favore impostare nome utente e password dell'interfaccia grafica utente.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Per favore impostare Utente e Password dell'Interfaccia Grafica nelle Impostazioni.",
|
||||
"Please wait": "Attendere prego",
|
||||
"Preview": "Anteprima",
|
||||
"Preview Usage Report": "Anteprima Statistiche di Utilizzo",
|
||||
"Quick guide to supported patterns": "Guida veloce agli schemi supportati",
|
||||
"RAM Utilization": "Utilizzo RAM",
|
||||
"Random": "Casuale",
|
||||
"Relay Servers": "Servers di relay",
|
||||
"Relay Servers": "Server di Reindirizzamento",
|
||||
"Relayed via": "Reindirizzato tramite",
|
||||
"Relays": "Servers di reindirizzamento",
|
||||
"Release Notes": "Note di rilascio",
|
||||
"Relays": "Reindirizzamenti",
|
||||
"Release Notes": "Note di Rilascio",
|
||||
"Remote Devices": "Dispositivi Remoti",
|
||||
"Remove": "Rimuovi",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificatore obbligatorio della cartella. Deve essere lo stesso su tutti i dispositivi del cluster.",
|
||||
@@ -157,7 +157,7 @@
|
||||
"Resume": "Riprendi",
|
||||
"Reused": "Riutilizzato",
|
||||
"Save": "Salva",
|
||||
"Scan Time Remaining": "Tempo di scansione rimanente",
|
||||
"Scan Time Remaining": "Tempo di Scansione Rimanente",
|
||||
"Scanning": "Scansione in corso",
|
||||
"Select the devices to share this folder with.": "Seleziona i dispositivi con i quali condividere questa cartella.",
|
||||
"Select the folders to share with this device.": "Seleziona le cartelle da condividere con questo dispositivo.",
|
||||
@@ -176,7 +176,7 @@
|
||||
"Shutdown": "Arresta",
|
||||
"Shutdown Complete": "Arresto Eseguito",
|
||||
"Simple File Versioning": "Controllo Versione Semplice",
|
||||
"Single level wildcard (matches within a directory only)": "Metacarattere di singolo livello (corrisponde solo all'interno di una cartella)",
|
||||
"Single level wildcard (matches within a directory only)": "Metacarattere di singolo livello (per corrispondenze solo all'interno di una cartella)",
|
||||
"Smallest First": "Prima il più piccolo",
|
||||
"Source Code": "Codice Sorgente",
|
||||
"Staggered File Versioning": "Controllo Versione Cadenzato",
|
||||
@@ -191,15 +191,15 @@
|
||||
"Syncthing is restarting.": "Riavvio di Syncthing in corso.",
|
||||
"Syncthing is upgrading.": "Aggiornamento di Syncthing in corso.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing sembra inattivo, oppure c'è un problema con la tua connessione a Internet. Nuovo tentativo…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Sembra che Syncthing non sia in grado di elaborare il tuo comando. Se il problema persiste prova a ricaricare la pagina nel tuo navigatore oppure prova a riavviare Syncthing.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Sembra che Syncthing abbia problemi nell'elaborazione della tua richiesta. Aggiorna la pagina o riavvia Syncthing se il problema persiste.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfaccia di amministrazione di Syncthing è configurata in modo da permettere l'accesso senza password.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Le statistiche aggregate sono disponibili pubblicamente all'URL seguente.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Le statistiche aggregate sono disponibili pubblicamente su {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Devi riavviare Syncthing per attivare la nuova configurazione.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Syncthing deve essere riavviato per attivare la nuova configurazione.",
|
||||
"The device ID cannot be blank.": "L'ID del dispositivo non può essere vuoto.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Trova l'ID nella finestra di dialogo \"Modifica > Mostra ID\" dell'altro dispositivo, poi inseriscilo qui. Gli spazi e i trattini sono opzionali (ignorati).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Trova l'ID nella finestra di dialogo \"Modifica > Mostra ID\" dell'altro dispositivo, poi inseriscilo qui. Gli spazi e i trattini sono opzionali (ignorati).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Quotidianamente il software invia le statistiche di utilizzo in forma criptata. Questi dati riguardano i sistemi operativi utilizzati, le dimensioni delle cartelle e le versioni del software. Se i dati riportati sono cambiati, verrà mostrata di nuovo questa finestra di dialogo.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID del dispositivo da inserire qui può essere trovato nella finestra \"Azioni> Mostra ID\" sull'altro dispositivo. Gli spazi e i trattini sono opzionali (ignorati).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID del dispositivo da inserire qui può essere trovato nella finestra \"Modifica> Mostra ID\" sull'altro dispositivo. Gli spazi e i trattini sono opzionali (ignorati).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Quotidianamente il software invia le statistiche di utilizzo in forma criptata. Questi dati riguardano i sistemi operativi utilizzati, le dimensioni delle cartelle e le versioni del software. Se i set di dati riportati vengono modificati, verrà mostrata nuovamente questa finestra di dialogo.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID del dispositivo inserito non sembra valido. Dovrebbe essere una stringa di 52 o 56 caratteri costituita da lettere e numeri, con spazi e trattini opzionali.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Il primo parametro della riga di comando è il percorso della cartella e il secondo parametro è il percorso relativo nella cartella.",
|
||||
"The folder ID cannot be blank.": "L'ID della cartella non può essere vuoto.",
|
||||
@@ -207,7 +207,7 @@
|
||||
"The folder ID must be unique.": "L'ID della cartella dev'essere unico.",
|
||||
"The folder path cannot be blank.": "Il percorso della cartella non può essere vuoto.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Vengono utilizzati i seguenti intervalli temporali: per la prima ora viene mantenuta una versione ogni 30 secondi, per il primo giorno viene mantenuta una versione ogni ora, per i primi 30 giorni viene mantenuta una versione al giorno, successivamente viene mantenuta una versione ogni settimana fino al periodo massimo impostato.",
|
||||
"The following items could not be synchronized.": "Non è stato possibile sincronizzare i seguenti elementi",
|
||||
"The following items could not be synchronized.": "Non è stato possibile sincronizzare i seguenti elementi.",
|
||||
"The maximum age must be a number and cannot be blank.": "La durata massima dev'essere un numero e non può essere vuoto.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La durata massima di una versione (in giorni, imposta a 0 per mantenere le versioni per sempre).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Lo spazio libero minimo su disco deve essere un numero non negativo tra 0 e 100 (inclusi)",
|
||||
@@ -216,8 +216,8 @@
|
||||
"The number of old versions to keep, per file.": "Il numero di vecchie versioni da mantenere, per file.",
|
||||
"The number of versions must be a number and cannot be blank.": "Il numero di versioni dev'essere un numero e non può essere vuoto.",
|
||||
"The path cannot be blank.": "Il percorso non può essere vuoto.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Il limite di banda deve essere un numero non negativo (da 0 a infinito)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "L'intervallo di scansione deve essere un numero superiore a zero secondi.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Il limite di banda deve essere un numero non negativo (0: nessun limite)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "L'intervallo di scansione deve essere un numero non negativo secondi.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Verranno effettuati tentativi in automatico e verranno sincronizzati quando l'errore sarà risolto.",
|
||||
"This Device": "Questo Dispositivo",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Ciò potrebbe facilmente permettere agli hackers accesso alla lettura e modifica di qualunque file del tuo computer.",
|
||||
@@ -238,7 +238,7 @@
|
||||
"Versions Path": "Percorso Cartella Versioni",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Le versioni vengono eliminate automaticamente se superano la durata massima o il numero di file permessi in un determinato intervallo temporale.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Attenzione, questo percorso è una sottocartella di una cartella esistente \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Anche nel nuovo dispositivo devi aggiungere l'ID di questo, con la stessa procedura.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando si aggiunge un nuovo dispositivo, tenere presente che il dispositivo deve essere aggiunto anche dall'altra parte.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quando aggiungi una nuova cartella, ricordati che gli ID vengono utilizzati per collegare le cartelle nei dispositivi. Distinguono maiuscole e minuscole e devono corrispondere esattamente su tutti i dispositivi.",
|
||||
"Yes": "Sì",
|
||||
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
|
||||
@@ -247,5 +247,5 @@
|
||||
"items": "elementi",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vuole condividere la cartella \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"A negative number of days doesn't make sense.": "음수로는 지정할 수 없습니다.",
|
||||
"A new major version may not be compatible with previous versions.": "새로운 메이저 버전은 이전 버전과 호환되지 않을 수 있습니다.",
|
||||
"API Key": "API 키",
|
||||
"About": " 정보",
|
||||
"About": "정보",
|
||||
"Actions": "동작",
|
||||
"Add": "추가",
|
||||
"Add Device": "기기 추가",
|
||||
@@ -98,7 +98,7 @@
|
||||
"Keep Versions": "버전 보관",
|
||||
"Largest First": "큰 파일 순",
|
||||
"Last File Received": "마지막으로 받은 파일",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "마지막 탐색",
|
||||
"Last seen": "마지막 접속",
|
||||
"Later": "나중에",
|
||||
"Listeners": "수신자",
|
||||
@@ -193,7 +193,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing이 중지되었거나 인터넷 연결에 문제가 있는 것 같습니다. 재시도 중입니다...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing에서 요청을 처리하는 중에 문제가 발생했습니다. 계속 문제가 발생하면 페이지를 다시 불러오거나 Syncthing을 재시작해 보세요.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing 관리자 인터페이스가 암호 없이 원격 접속이 허가되도록 설정되었습니다.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "수집된 통계는 아래 URL에서 공개적으로 볼 수 있습니다.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "수집된 통계는 {{URL}} 에서 공개적으로 볼 수 있습니다.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "설정이 저장되었지만 활성화되지 않았습니다. 설정을 활성화 하려면 Syncthing을 다시 시작하세요.",
|
||||
"The device ID cannot be blank.": "기기 ID는 비워 둘 수 없습니다.",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"Connection Type": "Soort verbinding",
|
||||
"Copied from elsewhere": "Gekopieerd vanaf elders",
|
||||
"Copied from original": "Gekopieerd van het origineel",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 voor de volgende contributanten:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 voor de volgende bijdragers:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 de volgende Bijdragers:",
|
||||
"Danger!": "Let op!",
|
||||
"Delete": "Verwijderen",
|
||||
@@ -98,7 +98,7 @@
|
||||
"Keep Versions": "Versies behouden",
|
||||
"Largest First": "Grootste eerst",
|
||||
"Last File Received": "Laatst ontvangen bestand",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "Laatste scan",
|
||||
"Last seen": "Laatst gezien op",
|
||||
"Later": "Later",
|
||||
"Listeners": "Luisteraars",
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Legg til",
|
||||
"Add Device": "Legg Til Eining",
|
||||
"Add Folder": "Legg Til Mappe",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add Remote Device": "Lett Til Ekstern Eining",
|
||||
"Add new folder?": "Leggja til ny mappe?",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresser",
|
||||
"Advanced": "Avansert",
|
||||
"Advanced Configuration": "Avansert konfigurasjon",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"Advanced settings": "Avansert innstillingar",
|
||||
"All Data": "Alle data",
|
||||
"Allow Anonymous Usage Reporting?": "Tillata anonymisert bruksrapportering?",
|
||||
"Alphabetic": "Alfabetisk",
|
||||
@@ -32,7 +32,7 @@
|
||||
"Comment, when used at the start of a line": "Kommentar, når brukt i starten av linja",
|
||||
"Compression": "Komprimering",
|
||||
"Connection Error": "Tilkoplingsfeil",
|
||||
"Connection Type": "Connection Type",
|
||||
"Connection Type": "Tilkoplingstype",
|
||||
"Copied from elsewhere": "Kopiert frå ein annan stad",
|
||||
"Copied from original": "Kopiert frå originalen",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
@@ -40,7 +40,7 @@
|
||||
"Danger!": "Fare!",
|
||||
"Delete": "Slett",
|
||||
"Deleted": "Sletta",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Eininga \"{{name}}\" {{device}} ({{address}}) vil kopla seg til. Vil du leggja ho til?",
|
||||
"Device ID": "Eining ID",
|
||||
"Device Identification": "Einingskjennemerke",
|
||||
"Device Name": "Namn På Eining",
|
||||
@@ -56,7 +56,7 @@
|
||||
"Edit Device": "Rediger Eining",
|
||||
"Edit Folder": "Rediger Mappe",
|
||||
"Editing": "Redigerer",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable NAT traversal": "Slå på NAT-gjennomgang",
|
||||
"Enable Relaying": "Aktiver Reléer",
|
||||
"Enable UPnP": "Aktiver UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Skriv inn adresser med komma mellom kvar adresse (\"tcp://ip:port\", \"tcp://host:port\"), eller \"dynamic\" for å automatisk søkja opp adressa.",
|
||||
@@ -72,10 +72,10 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskytta mot endringar gjort på andre einingar, men endringar gjort på denne eininga vert sende til resten av klyngja.",
|
||||
"Folder": "Mappe",
|
||||
"Folder ID": "Mappe ID",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Label": "Merkelapp for Mappe",
|
||||
"Folder Master": "Styrande Mappe",
|
||||
"Folder Path": "Mappeplassering",
|
||||
"Folder Type": "Folder Type",
|
||||
"Folder Type": "Mappetype",
|
||||
"Folders": "Mapper",
|
||||
"GUI": "grafisk brukargrensesnitt",
|
||||
"GUI Authentication Password": "GUI Passord",
|
||||
@@ -98,15 +98,15 @@
|
||||
"Keep Versions": "Behald Versjonar",
|
||||
"Largest First": "Største fyrst",
|
||||
"Last File Received": "Siste mottatte fila",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "Siste Skanning",
|
||||
"Last seen": "Sist sett",
|
||||
"Later": "Seinare",
|
||||
"Listeners": "Listeners",
|
||||
"Listeners": "Lyttarar",
|
||||
"Local Discovery": "Lokal oppdaging",
|
||||
"Local State": "Lokal Tilstand",
|
||||
"Local State (Total)": "Lokal tilstand (total)",
|
||||
"Major Upgrade": "Hovudoppgradering",
|
||||
"Master": "Master",
|
||||
"Master": "Styrar",
|
||||
"Maximum Age": "Maksimal Levetid",
|
||||
"Metadata Only": "Berre metadata",
|
||||
"Minimum Free Disk Space": "Naudsynt ledig diskplass",
|
||||
@@ -145,7 +145,7 @@
|
||||
"Relayed via": "Relé via",
|
||||
"Relays": "Reléer",
|
||||
"Release Notes": "Utgivingsnotat",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remote Devices": "Eksterne Einingar",
|
||||
"Remove": "Fjern",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Skann På Ny",
|
||||
@@ -193,7 +193,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å vera nede, eller så er det eit problem med nettilkoplinga di. Prøvar på ny …",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ser ut til å ha støtt på eit problem under behandling av din førespurnad. Vær vennleg å oppfrisk nettlesaren eller start Syncthing på nytt om problemet vedvarer.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing sitt administreringsgrensesnitt er sett opp til å tillate ekstern tilgang uten passord.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Samla statistikk er opent tilgjengeleg på URL-en nedanfor.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Samla statistikk er opent tilgjengeleg på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Instillingane har blitt lagra men ikkje aktivert. Syncthing må starta på ny for å aktivera dei nye instillingane.",
|
||||
"The device ID cannot be blank.": "Eining ID kan ikkje vera tom.",
|
||||
@@ -219,7 +219,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Hastigheitsgrensa må ver eit positivt tall (0: ingen grensa)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Talet på sekund i skanneintervallet kan ikkje vera negativt.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Desse vil bli prøvd på nytt automatisk og vil bli synkronisert når feilen har blitt utbetra.",
|
||||
"This Device": "This Device",
|
||||
"This Device": "Denne Eininga",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Dette kan lett gje dataekspertar tilgang til å lese og endre vilkårlege filer på denne maskina.",
|
||||
"This is a major version upgrade.": "Dette er ei hovudoppgradering",
|
||||
"Trash Can File Versioning": "Papirkorg filutgåvehandtering",
|
||||
@@ -246,6 +246,6 @@
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "element",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønskjer å dela mappa \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} ønskjer å dela mappa \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} ønskjer å dela mappa \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -98,7 +98,7 @@
|
||||
"Keep Versions": "Zachowuj wersje",
|
||||
"Largest First": "Największe na początku",
|
||||
"Last File Received": "Ostatni otrzymany plik",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "Czas ostatniego skanu",
|
||||
"Last seen": "Ostatnio widziany",
|
||||
"Later": "Później",
|
||||
"Listeners": "Nasłuchujący",
|
||||
@@ -193,7 +193,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing wydaje się być wyłączony lub jest problem z twoim połączeniem internetowym. Próbuje ponownie...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing nie może przetworzyć twojego zapytania. Proszę przeładuj stronę lub zrestartuj Syncthing, jeśli problem pozostanie.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Interfejs administracyjny Syncthing jest skonfigurowany w sposób pozwalający na zdalny dostęp bez hasła.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Zebrane statystyki są dostępne pod poniższym linkiem.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Zebrane statystyki są publicznie dostępne pod adresem {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfiguracja została zapisana lecz nie jest aktywna. Syncthing musi zostać zrestartowany aby aktywować nową konfiguracje.",
|
||||
"The device ID cannot be blank.": "ID urządzenia nie może być puste.",
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"Enable Relaying": "Habilitar retransmissão",
|
||||
"Enable UPnP": "Habilitar UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Insira endereços (\"tcp://ip:porta\", \"tcp://host:porta\") separados por vírgula ou \"dynamic\" para executar a descoberta automática do endereço.",
|
||||
"Enter ignore patterns, one per line.": "Insira os padrões de exclusão, um por linha.",
|
||||
"Enter ignore patterns, one per line.": "Insira os filtros, um por linha.",
|
||||
"Error": "Erro",
|
||||
"External File Versioning": "Versionamento externo de arquivo",
|
||||
"Failed Items": "Itens com falha",
|
||||
@@ -89,7 +89,7 @@
|
||||
"Help": "Ajuda",
|
||||
"Home page": "Página inicial",
|
||||
"Ignore": "Ignorar",
|
||||
"Ignore Patterns": "Padrões de exclusão",
|
||||
"Ignore Patterns": "Filtros",
|
||||
"Ignore Permissions": "Ignorar permissões",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite de velocidade de recepção (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "A configuração incorreta poderá causar danos aos seus dados e tornar o Syncthing inoperante.",
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"Keep Versions": "Количество хранимых версий",
|
||||
"Largest First": "Сначала большие",
|
||||
"Last File Received": "Последний полученный файл",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "Последнее сканирование",
|
||||
"Last seen": "Был доступен",
|
||||
"Later": "Позже",
|
||||
"Listeners": "Прослушиватель",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"A device with that ID is already added.": "En enhet med det ID är redan tillagt.",
|
||||
"A negative number of days doesn't make sense.": "Negativt antal dagar är inte troligt.",
|
||||
"A device with that ID is already added.": "En enhet med det ID:t är redan tillagt.",
|
||||
"A negative number of days doesn't make sense.": "Ett negativt antal dagar är inte rimligt.",
|
||||
"A new major version may not be compatible with previous versions.": "En ny huvudversion kan eventuellt vara inkompatibel med tidigare versioner.",
|
||||
"API Key": "API-nyckel",
|
||||
"About": "Om",
|
||||
@@ -16,100 +16,100 @@
|
||||
"Advanced Configuration": "Avancerad konfiguration",
|
||||
"Advanced settings": "Avancerade inställningar",
|
||||
"All Data": "All data",
|
||||
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistik?",
|
||||
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistiksrapportering?",
|
||||
"Alphabetic": "Alfabetisk",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ett externt kommando sköter versionshanteringen. Det måste ta bort filen från den synkroniserade mappen.",
|
||||
"Anonymous Usage Reporting": "Anonym användarstatistik",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ett externt kommando sköter versionshanteringen. Den behöver ta bort filen från den synkroniserade mappen.",
|
||||
"Anonymous Usage Reporting": "Anonym användarstatistik rapportering",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Enheter konfigurerade på en introduktörsenhet kommer också att läggas till den här enheten.",
|
||||
"Automatic upgrades": "Automatisk uppgradering",
|
||||
"Automatic upgrades": "Automatiska uppgraderingar",
|
||||
"Be careful!": "Var aktsam!",
|
||||
"Bugs": "Buggar",
|
||||
"CPU Utilization": "CPU-användning",
|
||||
"Changelog": "Changelog",
|
||||
"Changelog": "Ändringslogg",
|
||||
"Clean out after": "Rensa efteråt",
|
||||
"Close": "Stäng",
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, vid början av en rad.",
|
||||
"Comment, when used at the start of a line": "Kommentar, vid användning i början av en rad.",
|
||||
"Compression": "Komprimering",
|
||||
"Connection Error": "Anslutningsproblem",
|
||||
"Connection Type": "Anslutningstyp",
|
||||
"Copied from elsewhere": "Kopierat utifrån",
|
||||
"Copied from original": "Oförändrat",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragande:",
|
||||
"Copied from original": "Kopierat från original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragare:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 följande medverkande:",
|
||||
"Danger!": "Fara!",
|
||||
"Delete": "Radera",
|
||||
"Delete": "Ta bort",
|
||||
"Deleted": "Borttaget",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
"Device ID": "Enhets-ID",
|
||||
"Device ID": "Enhets ID",
|
||||
"Device Identification": "Enhetsidentifikation",
|
||||
"Device Name": "Enhetsnamn",
|
||||
"Device Name": "Enhetens namn",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enheten {{device}} ({{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
"Devices": "Enheter",
|
||||
"Disconnected": "Ej ansluten",
|
||||
"Discovery": "Uppslagning",
|
||||
"Disconnected": "Frånkopplad",
|
||||
"Discovery": "Upptäckt",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Nedladdningshastighet",
|
||||
"Downloaded": "Nerladdat",
|
||||
"Downloading": "Laddar ner",
|
||||
"Downloaded": "Hämtat",
|
||||
"Downloading": "Hämtar",
|
||||
"Edit": "Redigera",
|
||||
"Edit Device": "Redigera enhet",
|
||||
"Edit Folder": "Redigera katalog",
|
||||
"Editing": "Redigerar",
|
||||
"Enable NAT traversal": "Aktivera NAT traversering",
|
||||
"Enable Relaying": "Aktivera reläa",
|
||||
"Enable UPnP": "Använd UPnP",
|
||||
"Enable UPnP": "Aktivera UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\")-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
|
||||
"Enter ignore patterns, one per line.": "Ange filmönster, ett per rad.",
|
||||
"Enter ignore patterns, one per line.": "Ange ignorera mönster, en per rad.",
|
||||
"Error": "Fel",
|
||||
"External File Versioning": "Extern versionshantering",
|
||||
"Failed Items": "Misslyckade filer",
|
||||
"File Pull Order": "Hämtningsprioritering av filer",
|
||||
"File Versioning": "Versionshantering",
|
||||
"External File Versioning": "Extern filversionshantering",
|
||||
"Failed Items": "Misslyckade objekt",
|
||||
"File Pull Order": "Filhämtningsprioritering",
|
||||
"File Versioning": "Filversionshantering",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Filrättigheter ignoreras vid sökning efter förändringar. Används på FAT-filsystem.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till katalogen .stversions om de ersätts eller raderas av Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till datummärkta versioner i en .stversions-mapp när de ersatts eller raderats av 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 skyddas från ändringar gjorda på andra enheter, men ändringar som görs på den här noden skickas till de andra klustermedlemmarna.",
|
||||
"Folder": "Katalog",
|
||||
"Folder ID": "Katalog-ID",
|
||||
"Folder Label": "Katalog etikett",
|
||||
"Folder Master": "Huvudlagring",
|
||||
"Folder Path": "Sökväg",
|
||||
"Folder Label": "Katalog-etikett",
|
||||
"Folder Master": "Huvudkatalog",
|
||||
"Folder Path": "Katalog-sökväg",
|
||||
"Folder Type": "Katalogtyp",
|
||||
"Folders": "Kataloger",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "GUI-lösenord",
|
||||
"GUI Authentication User": "GUI-användare",
|
||||
"GUI Listen Addresses": "GUI-adress",
|
||||
"Generate": "Skapa",
|
||||
"Global Discovery": "Global uppslagning",
|
||||
"Global Discovery Server": "Global uppslagningsserver",
|
||||
"Global Discovery Servers": "Globala uppslagningsservrar",
|
||||
"GUI": "Grafiskt gränssnitt",
|
||||
"GUI Authentication Password": "Lösenord för GUI",
|
||||
"GUI Authentication User": "Användare för GUI",
|
||||
"GUI Listen Addresses": "Lyssningsadresser för GUI",
|
||||
"Generate": "Generera",
|
||||
"Global Discovery": "Global upptäckt",
|
||||
"Global Discovery Server": "Global upptäcktsserver",
|
||||
"Global Discovery Servers": "Globala upptäcktsservrar",
|
||||
"Global State": "Global status",
|
||||
"Help": "Hjälp",
|
||||
"Home page": "Hemsida",
|
||||
"Ignore": "Ignorera",
|
||||
"Ignore Patterns": "Ignorerade filmönster",
|
||||
"Ignore Permissions": "Ignorera filrättigheter",
|
||||
"Incoming Rate Limit (KiB/s)": "Max nedladdningshastighet (KiB/s)",
|
||||
"Ignore Patterns": "Ignorera mönster",
|
||||
"Ignore Permissions": "Ignorera rättigheter",
|
||||
"Incoming Rate Limit (KiB/s)": "Nedladdningshastighetsgräns (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Inkorrekt konfiguration kan skada innehållet i katalogen and få Syncthing att sluta fungera.",
|
||||
"Introducer": "introduktör",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Vänder på villkoret, d.v.s. exkluderar inte.",
|
||||
"Keep Versions": "Behåll versioner",
|
||||
"Largest First": "Störst först",
|
||||
"Last File Received": "Senast Mottagna Fil",
|
||||
"Last Scan": "Senast skanning",
|
||||
"Last seen": "Senast online",
|
||||
"Largest First": "Största först",
|
||||
"Last File Received": "Senaste fil mottagen",
|
||||
"Last Scan": "Senaste skanning",
|
||||
"Last seen": "Senast sedd",
|
||||
"Later": "Senare",
|
||||
"Listeners": "Lyssnare",
|
||||
"Local Discovery": "Lokal uppslagning",
|
||||
"Local Discovery": "Lokal upptäckt",
|
||||
"Local State": "Lokal status",
|
||||
"Local State (Total)": "Lokal status (Total)",
|
||||
"Local State (Total)": "Lokal status (totalt)",
|
||||
"Major Upgrade": "Stor uppgradering",
|
||||
"Master": "Huvud",
|
||||
"Maximum Age": "Högsta åldersgräns",
|
||||
"Maximum Age": "Maximum ålder",
|
||||
"Metadata Only": "Endast metadata",
|
||||
"Minimum Free Disk Space": "Minimum ledigt diskutrymme",
|
||||
"Minimum Free Disk Space": "Minsta ledigt diskutrymme",
|
||||
"Move to top of queue": "Flytta till överst i kön",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Jokertecken som representerar noll eller fler godtyckliga tecken, även över kataloggränser.",
|
||||
"Never": "Aldrig",
|
||||
@@ -117,7 +117,7 @@
|
||||
"New Folder": "Ny katalog",
|
||||
"Newest First": "Nyast först",
|
||||
"No": "Nej",
|
||||
"No File Versioning": "Ingen versionshantering",
|
||||
"No File Versioning": "Ingen filversionshantering",
|
||||
"Normal": "Normal",
|
||||
"Notice": "Observera",
|
||||
"OK": "OK",
|
||||
@@ -125,40 +125,40 @@
|
||||
"Oldest First": "Äldst först",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Valfri beskrivande etikett för katalogen. Kan vara olika på varje enhet.",
|
||||
"Options": "Alternativ",
|
||||
"Out of Sync": "Osynkad",
|
||||
"Out of Sync Items": "Osynkade poster",
|
||||
"Outgoing Rate Limit (KiB/s)": "Max uppladdningshastighet (KiB/s)",
|
||||
"Override Changes": "Skriv över ändringar",
|
||||
"Out of Sync": "Osynkroniserad",
|
||||
"Out of Sync Items": "Osynkroniserade objekt",
|
||||
"Outgoing Rate Limit (KiB/s)": "Uppladdningshastighetsgräns (KiB/s)",
|
||||
"Override Changes": "Åsidosätt förändringar",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sökväg där versioner sparas (lämna tomt för att använda .stversions i den ordinarie katalogen).",
|
||||
"Pause": "Paus",
|
||||
"Paused": "Pausad",
|
||||
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiskt användarautentisering och lösenord i dialogrutan Inställningar.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiska gränssnittets användarautentisering och lösenord i inställningsdialogrutan.",
|
||||
"Please wait": "Var god vänta",
|
||||
"Preview": "Förhandsgranska",
|
||||
"Preview Usage Report": "Förhandsgranska statistik",
|
||||
"Quick guide to supported patterns": "Snabb guide till filmönster som stöds",
|
||||
"RAM Utilization": "Minnesanvändning",
|
||||
"Quick guide to supported patterns": "Snabb handledning till mönster som stöds",
|
||||
"RAM Utilization": "RAM-användning",
|
||||
"Random": "Slumpmässig",
|
||||
"Relay Servers": "Reläservrar",
|
||||
"Relayed via": "Vidarbefordras via",
|
||||
"Relays": "Vidarbefordringar",
|
||||
"Release Notes": "versionsnyheter",
|
||||
"Relays": "Reläservrar",
|
||||
"Release Notes": "Versionsanteckningar",
|
||||
"Remote Devices": "Fjärrenheter",
|
||||
"Remove": "Ta bort",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för katalogen. Måste vara densamma på alla kluster enheter.",
|
||||
"Rescan": "Uppdatera",
|
||||
"Rescan All": "Uppdatera alla",
|
||||
"Rescan Interval": "Uppdateringsintervall",
|
||||
"Rescan": "Skanna om",
|
||||
"Rescan All": "Skanna om alla",
|
||||
"Rescan Interval": "Omskanningsintervall",
|
||||
"Restart": "Starta om",
|
||||
"Restart Needed": "Omstart behövs",
|
||||
"Restarting": "Startar om",
|
||||
"Resume": "Återuppta",
|
||||
"Reused": "Återanvänt",
|
||||
"Reused": "Återanvänd",
|
||||
"Save": "Spara",
|
||||
"Scan Time Remaining": "Granska återstående tid",
|
||||
"Scanning": "Uppdaterar",
|
||||
"Scan Time Remaining": "Återstående skanningstid",
|
||||
"Scanning": "Skannar",
|
||||
"Select the devices to share this folder with.": "Ange enheterna att dela den här katalogen med.",
|
||||
"Select the folders to share with this device.": "Välj kataloger att dela med den här enheten.",
|
||||
"Settings": "Inställningar",
|
||||
@@ -166,67 +166,67 @@
|
||||
"Share Folder": "Dela katalog",
|
||||
"Share Folders With Device": "Dela kataloger med enhet",
|
||||
"Share With Devices": "Dela med enheter",
|
||||
"Share this folder?": "Dela den här katalogen?",
|
||||
"Share this folder?": "Dela denna katalog?",
|
||||
"Shared With": "Delad med",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort identifieringssträng för katalogen. Måste vara samma på alla enheter i klustret.",
|
||||
"Show ID": "Visa ID",
|
||||
"Show QR": "Visa QR",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhets-ID. Skickas till andra enheter som namn på denna enhet.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhets-ID. Sätts till namnet på den andra enheten vid första anslutning om det lämnas tomt.",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhetens ID. Skickas till andra enheter som namn på denna enhet.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhetens ID. Sätts till namnet på den andra enheten vid första anslutning om det lämnas tomt.",
|
||||
"Shutdown": "Stäng av",
|
||||
"Shutdown Complete": "Avstängning klar",
|
||||
"Simple File Versioning": "Enkel versionshantering",
|
||||
"Simple File Versioning": "Enkel filversionshantering",
|
||||
"Single level wildcard (matches within a directory only)": "Jokertecken som representerar noll eller fler godtyckliga tecken i ett filnamn.",
|
||||
"Smallest First": "Minst först",
|
||||
"Source Code": "Källkod",
|
||||
"Staggered File Versioning": "Versionshantering i intervall",
|
||||
"Start Browser": "Starta browser",
|
||||
"Staggered File Versioning": "Filversionshantering i intervall",
|
||||
"Start Browser": "Starta webbläsare",
|
||||
"Statistics": "Statistik",
|
||||
"Stopped": "Stoppad",
|
||||
"Support": "Support",
|
||||
"Sync Protocol Listen Addresses": "Address för inkommande anslutningar",
|
||||
"Sync Protocol Listen Addresses": "Synkroniseringsprotokollets lyssningsadresser",
|
||||
"Syncing": "Synkroniserar",
|
||||
"Syncthing has been shut down.": "Syncthing har stängts ner.",
|
||||
"Syncthing has been shut down.": "Syncthing har stängts.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller följande mjukvarupaket eller delar av dem:",
|
||||
"Syncthing is restarting.": "Syncthing startar om.",
|
||||
"Syncthing is upgrading.": "Syncthing uppgraderas.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller finns det problem med din Internetanslutning. Försöker igen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem med behandlingen av din begäran. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratör gränssnittet är konfigurerat för att tillåta fjärrtillträde utan ett lösenord.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgängliga på webbadressen nedan.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Sammanställd statistik finns publikt tillgänglig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
|
||||
"The device ID cannot be blank.": "Enhets-ID kan inte vara tomt.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets-ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"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).": "Enhets-ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"The device ID cannot be blank.": "Enhetens ID kan inte vara tomt.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhetens ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"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).": "Enhetens ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"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 krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, katalogstorlekar och versioner. Om datan som rapporteras ändras så kommer du att bli tillfrågad 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 inmatade enhets-ID:t verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
|
||||
"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 inmatade enhetens ID verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den första kommandoparametern är sökvägen till mappen och den andra parametern är den relativa sökvägen i mappen.",
|
||||
"The folder ID cannot be blank.": "Ange ett enhets-ID.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Katalog-ID:t måste vara en kort sträng (64 tecken eller mindre), bestående av endast bokstäver, siffror, punkt (.), bindestreck (-) och understreck (_).",
|
||||
"The folder ID must be unique.": "Katalog-ID:t måste vara unikt.",
|
||||
"The folder path cannot be blank.": "Ange en sökväg.",
|
||||
"The folder ID cannot be blank.": "Katalogens ID får inte vara tomt.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Katalogens ID måste vara en kort sträng (64 tecken eller mindre), bestående av endast bokstäver, siffror, punkt (.), bindestreck (-) och understreck (_).",
|
||||
"The folder ID must be unique.": "Katalogens ID måste vara unikt.",
|
||||
"The folder path cannot be blank.": "Katalogsökvägen får inte vara 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öljande intervallen används: varje 30 sekunder under den första timmen; varje timme under den första dagen; varje dag för de första 30 dagarna; varje vecka tills den maximala åldersgränsen uppnås.",
|
||||
"The following items could not be synchronized.": "Följande filer kunde inte synkroniseras.",
|
||||
"The following items could not be synchronized.": "Följande objekt kunde inte synkroniseras.",
|
||||
"The maximum age must be a number and cannot be blank.": "Åldersgränsen måste vara ett tal och kan inte lämnas tomt.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tiden att behålla en version (i dagar, sätt till 0 för att behålla versioner för evigt).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Minimum ledigt diskutrymme i procent måste vara en icke negativ siffra mellan 0 och 100 (inklusive).",
|
||||
"The number of days must be a number and cannot be blank.": "Antalet dagar måste vara en siffra och får inte vara tomt.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Antal dagar som filer ligger kvar i papperskorgen. Noll betyder för alltid.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Antalet dagar som filer ligger kvar i papperskorgen. Noll betyder för alltid.",
|
||||
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas, per fil.",
|
||||
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett nummer och kan inte lämnas tomt.",
|
||||
"The path cannot be blank.": "Ange en sökväg",
|
||||
"The path cannot be blank.": "Sökvägen kan inte vara tom.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Frekvensgränsen måste vara ett icke-negativt tal (0: ingen gräns)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Förnyelseintervallet måste vara ett positivt antal sekunder",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "De omprövas automatiskt och kommer att synkroniseras när felet är löst.",
|
||||
"This Device": "Enheten",
|
||||
"This Device": "Denna enhet",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Detta kan lätt ge hackare tillgång till att läsa och ändra några filer på datorn.",
|
||||
"This is a major version upgrade.": "Det här är en stor uppgradering.",
|
||||
"Trash Can File Versioning": "Versionshantering på filer i papperskorgen",
|
||||
"Unknown": "Okänt",
|
||||
"Trash Can File Versioning": "Papperskorgs filversionshantering",
|
||||
"Unknown": "Okänd",
|
||||
"Unshared": "Inte delad",
|
||||
"Unused": "Oanvänd",
|
||||
"Up to Date": "Helt uppdaterad",
|
||||
"Up to Date": "Uppdaterad",
|
||||
"Updated": "Uppdaterad",
|
||||
"Upgrade": "Uppgradering",
|
||||
"Upgrade To {%version%}": "Uppgradera till {{version}}",
|
||||
@@ -235,16 +235,16 @@
|
||||
"Uptime": "Tid sedan start",
|
||||
"Use HTTPS for GUI": "Använd HTTPS för GUI",
|
||||
"Version": "Version",
|
||||
"Versions Path": "Katalog för versioner",
|
||||
"Versions Path": "Sökväg för versioner",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i sitt interval.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en underkatalog till en befintlig katalog \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att den här enheten måste läggas till på den andra enheten också.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny katalog, tänk på att katalog-ID:t knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny katalog, tänk på att katalogens ID knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
|
||||
"days": "dagar",
|
||||
"full documentation": "fullständig dokumentation",
|
||||
"items": "poster",
|
||||
"items": "objekt",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vill dela katalogen \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela katalogen \"{{folderlabel}}\" ({{folder}})."
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
"Keep Versions": "Зберігати версії",
|
||||
"Largest First": "Спершу найбільші",
|
||||
"Last File Received": "Останній завантажений файл",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last Scan": "Останнє сканування",
|
||||
"Last seen": "З’являвся останній раз",
|
||||
"Later": "Пізніше",
|
||||
"Listeners": "Приймачі (TCP & Relay)",
|
||||
|
||||
@@ -544,13 +544,17 @@
|
||||
<th><span class="fa fa-fw fa-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
||||
<td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr ng-if="connections[deviceCfg.deviceID].connected">
|
||||
<tr>
|
||||
<th><span class="fa fa-fw fa-link"></span> <span translate>Address</span></th>
|
||||
<td class="text-right">
|
||||
<td ng-if="connections[deviceCfg.deviceID].connected" class="text-right">
|
||||
<span tooltip data-original-title="{{ connections[deviceCfg.deviceID].type.indexOf('Relay') > -1 ? '' : connections[deviceCfg.deviceID].type }}">
|
||||
{{deviceAddr(deviceCfg)}}
|
||||
</span>
|
||||
</td>
|
||||
<td ng-if="!connections[deviceCfg.deviceID].connected" class="text-right">
|
||||
<span ng-repeat="addr in deviceCfg.addresses"><span tooltip data-original-title="{{'Configured' | translate}}">{{addr}}</span><br></span>
|
||||
<span ng-repeat="addr in discoveryCache[deviceCfg.deviceID].addresses"><span tooltip data-original-title="{{'Discovered' | translate}}">{{addr}}</span><br></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="connections[deviceCfg.deviceID].connected && connections[deviceCfg.deviceID].type.indexOf('Relay') > -1" tooltip data-original-title="Connections via relays might be rate limited by the relay">
|
||||
<th><span class="fa fa-fw fa-warning text-danger"></span> <span translate>Connection Type</span></th>
|
||||
|
||||
@@ -49,10 +49,20 @@ function deviceCompare(a, b) {
|
||||
}
|
||||
|
||||
function folderCompare(a, b) {
|
||||
if (a.id < b.id) {
|
||||
var labelA = a.id;
|
||||
if (typeof a.label !== 'undefined' && a.label !== null && a.label.length > 0) {
|
||||
labelA = a.label;
|
||||
}
|
||||
|
||||
var labelB = b.id;
|
||||
if (typeof b.label !== 'undefined' && b.label !== null && b.label.length > 0) {
|
||||
labelB = b.label;
|
||||
}
|
||||
|
||||
if (labelA < labelB) {
|
||||
return -1;
|
||||
}
|
||||
return a.id > b.id;
|
||||
return labelA > labelB;
|
||||
}
|
||||
|
||||
function folderMap(l) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<p translate>Copyright © 2014-2016 the following Contributors:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
|
||||
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
@@ -67,6 +67,9 @@ angular.module('syncthing.core')
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// inform syncthingContoller that a modal is ready
|
||||
scope.$parent.modalLoaded();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ angular.module('syncthing.core')
|
||||
$scope.myID = '';
|
||||
$scope.devices = [];
|
||||
$scope.deviceRejections = {};
|
||||
$scope.discoveryCache = {};
|
||||
$scope.folderRejections = {};
|
||||
$scope.protocolChanged = false;
|
||||
$scope.reportData = {};
|
||||
@@ -85,6 +86,7 @@ angular.module('syncthing.core')
|
||||
console.log('UIOnline');
|
||||
|
||||
refreshSystem();
|
||||
refreshDiscoveryCache();
|
||||
refreshConfig();
|
||||
refreshConnectionStats();
|
||||
refreshDeviceStats();
|
||||
@@ -221,9 +223,7 @@ angular.module('syncthing.core')
|
||||
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
|
||||
} else {
|
||||
if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
|
||||
setTimeout(function () {
|
||||
$('#ur').modal();
|
||||
}, 2500);
|
||||
$('#ur').modal();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,6 +420,22 @@ angular.module('syncthing.core')
|
||||
}).error($scope.emitHTTPError);
|
||||
}
|
||||
|
||||
function refreshDiscoveryCache() {
|
||||
$http.get(urlbase + '/system/discovery').success(function (data) {
|
||||
for (var device in data) {
|
||||
for (var i = 0; i < data[device].addresses.length; i++) {
|
||||
// Relay addresses are URLs with
|
||||
// .../?foo=barlongstuff that we strip away here. We
|
||||
// remove the final slash as well for symmetry with
|
||||
// tcp://192.0.2.42:1234 type addresses.
|
||||
data[device].addresses[i] = data[device].addresses[i].replace(/\/\?.*/, '');
|
||||
}
|
||||
}
|
||||
$scope.discoveryCache = data;
|
||||
console.log("refreshDiscoveryCache", data);
|
||||
}).error($scope.emitHTTPError);
|
||||
}
|
||||
|
||||
function recalcLocalStateTotal () {
|
||||
$scope.localStateTotal = {
|
||||
bytes: 0,
|
||||
@@ -611,6 +627,7 @@ angular.module('syncthing.core')
|
||||
|
||||
$scope.refresh = function () {
|
||||
refreshSystem();
|
||||
refreshDiscoveryCache();
|
||||
refreshConnectionStats();
|
||||
refreshErrors();
|
||||
};
|
||||
@@ -1301,7 +1318,7 @@ angular.module('syncthing.core')
|
||||
$scope.editingExisting = false;
|
||||
$scope.folderEditor.$setPristine();
|
||||
$http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
|
||||
$scope.currentFolder.id = data.random.substr(0, 5) + '-' + data.random.substr(5, 5);
|
||||
$scope.currentFolder.id = (data.random.substr(0, 5) + '-' + data.random.substr(5, 5)).toLowerCase();
|
||||
$('#editFolder').modal();
|
||||
});
|
||||
};
|
||||
@@ -1436,7 +1453,7 @@ angular.module('syncthing.core')
|
||||
}
|
||||
}
|
||||
|
||||
folders.sort();
|
||||
folders.sort(folderCompare);
|
||||
return folders;
|
||||
};
|
||||
|
||||
@@ -1623,6 +1640,14 @@ angular.module('syncthing.core')
|
||||
});
|
||||
};
|
||||
|
||||
// pseudo main. called on all definitions assigned
|
||||
initController();
|
||||
$scope.modalLoaded = function () {
|
||||
|
||||
// once all modal elements have been processed
|
||||
if ($('modal').length === 0) {
|
||||
|
||||
// pseudo main. called on all definitions assigned
|
||||
initController();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -15,33 +15,42 @@
|
||||
<table class="table table-striped table-condensed">
|
||||
|
||||
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededTotal" pagination-id="needed">
|
||||
|
||||
<!-- Icon -->
|
||||
<td class="small-data"><span class="fa fa-fw fa-{{needIcons[f.action]}}"></span> {{needActions[f.action]}}</td>
|
||||
<td class="small-data col-xs-2">
|
||||
<span class="fa fa-fw fa-{{needIcons[f.action]}}"></span> {{needActions[f.action]}}
|
||||
</td>
|
||||
|
||||
<!-- Name -->
|
||||
<td ng-if="f.type != 'queued'" tooltip data-original-title="{{f.name}}">{{f.name | basename}}</td>
|
||||
<td ng-if="f.type == 'queued'">
|
||||
<a href="" ng-click="bumpFile(neededFolder, f.name)" tooltip data-original-title="{{'Move to top of queue' | translate}}">
|
||||
<span class="fa fa-eject"></span>
|
||||
</a>
|
||||
<span tooltip data-original-title="{{f.name}}"> {{f.name | basename}}</span>
|
||||
<td class="small-data col-xs-6">
|
||||
<span ng-if="f.type != 'queued'">
|
||||
<span tooltip data-original-title="{{f.name}}">{{f.name | basename}}</span>
|
||||
</span>
|
||||
<span ng-if="f.type == 'queued'">
|
||||
<a href="" ng-click="bumpFile(neededFolder, f.name)" tooltip data-original-title="{{'Move to top of queue' | translate}}">
|
||||
<span class="fa fa-eject"></span>
|
||||
</a>
|
||||
<span tooltip data-original-title="{{f.name}}"> {{f.name | basename}}</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Size/Progress -->
|
||||
<td ng-if="f.type == 'progress' && f.action == 'sync' && progress[neededFolder] && progress[neededFolder][f.name]">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.name].reused}}%"></div>
|
||||
<div class="progress-bar" style="width: {{progress[neededFolder][f.name].copiedFromOrigin}}%"></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.name].copiedFromElsewhere}}%"></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.name].pulled}}%"></div>
|
||||
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: {{progress[neededFolder][f.name].pulling}}%"></div>
|
||||
<span class="show frontal">
|
||||
{{progress[neededFolder][f.name].bytesDone | binary}}B / {{progress[neededFolder][f.name].bytesTotal | binary}}B
|
||||
</span>
|
||||
<td class="col-xs-4">
|
||||
<div ng-if="f.type == 'progress' && f.action == 'sync' && progress[neededFolder] && progress[neededFolder][f.name]">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.name].reused}}%"></div>
|
||||
<div class="progress-bar" style="width: {{progress[neededFolder][f.name].copiedFromOrigin}}%"></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.name].copiedFromElsewhere}}%"></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.name].pulled}}%"></div>
|
||||
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: {{progress[neededFolder][f.name].pulling}}%"></div>
|
||||
<span class="show frontal">
|
||||
{{progress[neededFolder][f.name].bytesDone | binary}}B / {{progress[neededFolder][f.name].bytesTotal | binary}}B
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="f.type != 'progress' || f.action != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.name]" class="text-right small-data">
|
||||
<span ng-if="f.size > 0">{{f.size | binary}}B</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right small-data" ng-if="f.type != 'progress' || f.action != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.name]">
|
||||
<span ng-if="f.size > 0">{{f.size | binary}}B</span>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
<p translate>
|
||||
The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.
|
||||
</p>
|
||||
<p translate translate-value-url="<a href="https://data.syncthing.net" target="_blank">https://data.syncthing.net</a>">
|
||||
The aggregated statistics are publicly available at {%url%}.
|
||||
</p>
|
||||
<p translate>The aggregated statistics are publicly available at the URL below.</p>
|
||||
<p><a href="https://data.syncthing.net/" target="_blank">https://data.syncthing.net/</a></p>
|
||||
<form>
|
||||
<textarea class="form-control" rows="20">{{reportData | json}}</textarea>
|
||||
</form>
|
||||
|
||||
@@ -11,12 +11,18 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type requiresRestart struct{}
|
||||
type requiresRestart struct {
|
||||
committed chan struct{}
|
||||
}
|
||||
|
||||
func (requiresRestart) VerifyConfiguration(_, _ Configuration) error {
|
||||
return nil
|
||||
}
|
||||
func (requiresRestart) CommitConfiguration(_, _ Configuration) bool {
|
||||
func (c requiresRestart) CommitConfiguration(_, _ Configuration) bool {
|
||||
select {
|
||||
case c.committed <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (requiresRestart) String() string {
|
||||
@@ -28,7 +34,7 @@ type validationError struct{}
|
||||
func (validationError) VerifyConfiguration(_, _ Configuration) error {
|
||||
return errors.New("some error")
|
||||
}
|
||||
func (validationError) CommitConfiguration(_, _ Configuration) bool {
|
||||
func (c validationError) CommitConfiguration(_, _ Configuration) bool {
|
||||
return true
|
||||
}
|
||||
func (validationError) String() string {
|
||||
@@ -44,11 +50,11 @@ func TestReplaceCommit(t *testing.T) {
|
||||
// Replace config. We should get back a clean response and the config
|
||||
// should change.
|
||||
|
||||
resp := w.Replace(Configuration{Version: 1})
|
||||
if resp.ValidationError != nil {
|
||||
t.Fatal("Should not have a validation error")
|
||||
err := w.Replace(Configuration{Version: 1})
|
||||
if err != nil {
|
||||
t.Fatal("Should not have a validation error:", err)
|
||||
}
|
||||
if resp.RequiresRestart {
|
||||
if w.RequiresRestart() {
|
||||
t.Fatal("Should not require restart")
|
||||
}
|
||||
if w.Raw().Version != 1 {
|
||||
@@ -58,13 +64,16 @@ func TestReplaceCommit(t *testing.T) {
|
||||
// Now with a subscriber requiring restart. We should get a clean response
|
||||
// but with the restart flag set, and the config should change.
|
||||
|
||||
w.Subscribe(requiresRestart{})
|
||||
sub0 := requiresRestart{committed: make(chan struct{}, 1)}
|
||||
w.Subscribe(sub0)
|
||||
|
||||
resp = w.Replace(Configuration{Version: 2})
|
||||
if resp.ValidationError != nil {
|
||||
t.Fatal("Should not have a validation error")
|
||||
err = w.Replace(Configuration{Version: 2})
|
||||
if err != nil {
|
||||
t.Fatal("Should not have a validation error:", err)
|
||||
}
|
||||
if !resp.RequiresRestart {
|
||||
|
||||
<-sub0.committed
|
||||
if !w.RequiresRestart() {
|
||||
t.Fatal("Should require restart")
|
||||
}
|
||||
if w.Raw().Version != 2 {
|
||||
@@ -76,12 +85,12 @@ func TestReplaceCommit(t *testing.T) {
|
||||
|
||||
w.Subscribe(validationError{})
|
||||
|
||||
resp = w.Replace(Configuration{Version: 3})
|
||||
if resp.ValidationError == nil {
|
||||
err = w.Replace(Configuration{Version: 3})
|
||||
if err == nil {
|
||||
t.Fatal("Should have a validation error")
|
||||
}
|
||||
if resp.RequiresRestart {
|
||||
t.Fatal("Should not require restart")
|
||||
if !w.RequiresRestart() {
|
||||
t.Fatal("Should still require restart")
|
||||
}
|
||||
if w.Raw().Version != 2 {
|
||||
t.Fatal("Config should not have changed")
|
||||
|
||||
@@ -10,6 +10,7 @@ package config
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
@@ -81,11 +82,15 @@ func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
|
||||
util.SetDefaults(&cfg.Options)
|
||||
util.SetDefaults(&cfg.GUI)
|
||||
|
||||
err := xml.NewDecoder(r).Decode(&cfg)
|
||||
if err := xml.NewDecoder(r).Decode(&cfg); err != nil {
|
||||
return Configuration{}, err
|
||||
}
|
||||
cfg.OriginalVersion = cfg.Version
|
||||
|
||||
cfg.prepare(myID)
|
||||
return cfg, err
|
||||
if err := cfg.prepare(myID); err != nil {
|
||||
return Configuration{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
|
||||
@@ -97,14 +102,18 @@ func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
|
||||
|
||||
bs, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
return Configuration{}, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(bs, &cfg)
|
||||
if err := json.Unmarshal(bs, &cfg); err != nil {
|
||||
return Configuration{}, err
|
||||
}
|
||||
cfg.OriginalVersion = cfg.Version
|
||||
|
||||
cfg.prepare(myID)
|
||||
return cfg, err
|
||||
if err := cfg.prepare(myID); err != nil {
|
||||
return Configuration{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
@@ -154,7 +163,7 @@ func (cfg *Configuration) WriteXML(w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
func (cfg *Configuration) prepare(myID protocol.DeviceID) error {
|
||||
util.FillNilSlices(&cfg.Options)
|
||||
|
||||
// Initialize any empty slices
|
||||
@@ -168,19 +177,19 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
cfg.Options.AlwaysLocalNets = []string{}
|
||||
}
|
||||
|
||||
// Check for missing, bad or duplicate folder ID:s
|
||||
var seenFolders = map[string]*FolderConfiguration{}
|
||||
// Prepare folders and check for duplicates. Duplicates are bad and
|
||||
// dangerous, can't currently be resolved in the GUI, and shouldn't
|
||||
// happen when configured by the GUI. We return with an error in that
|
||||
// situation.
|
||||
seenFolders := make(map[string]struct{})
|
||||
for i := range cfg.Folders {
|
||||
folder := &cfg.Folders[i]
|
||||
folder.prepare()
|
||||
|
||||
if seen, ok := seenFolders[folder.ID]; ok {
|
||||
l.Warnf("Multiple folders with ID %q; disabling", folder.ID)
|
||||
seen.Invalid = "duplicate folder ID"
|
||||
folder.Invalid = "duplicate folder ID"
|
||||
} else {
|
||||
seenFolders[folder.ID] = folder
|
||||
if _, ok := seenFolders[folder.ID]; ok {
|
||||
return fmt.Errorf("duplicate folder ID %q in configuration", folder.ID)
|
||||
}
|
||||
seenFolders[folder.ID] = struct{}{}
|
||||
}
|
||||
|
||||
cfg.Options.ListenAddresses = util.UniqueStrings(cfg.Options.ListenAddresses)
|
||||
@@ -257,6 +266,8 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
if cfg.GUI.APIKey == "" {
|
||||
cfg.GUI.APIKey = rand.String(32)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertV14V15(cfg *Configuration) {
|
||||
|
||||
@@ -549,6 +549,9 @@ func TestPullOrder(t *testing.T) {
|
||||
t.Logf("%s", buf.Bytes())
|
||||
|
||||
cfg, err = ReadXML(buf, device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wrapper = Wrap("testdata/pullorder.xml", cfg)
|
||||
folders = wrapper.Folders()
|
||||
|
||||
@@ -595,22 +598,14 @@ func TestGUIConfigURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDuplicateDevicesFolders(t *testing.T) {
|
||||
wrapper, err := Load("testdata/duplicates.xml", device1)
|
||||
func TestDuplicateDevices(t *testing.T) {
|
||||
// Duplicate devices should be removed
|
||||
|
||||
wrapper, err := Load("testdata/dupdevices.xml", device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// All folders are loaded, but the duplicate ones are disabled.
|
||||
if l := len(wrapper.Raw().Folders); l != 3 {
|
||||
t.Errorf("Incorrect number of folders, %d != 3", l)
|
||||
}
|
||||
for i, f := range wrapper.Raw().Folders {
|
||||
if f.ID == "f1" && f.Invalid == "" {
|
||||
t.Errorf("Folder %d (%q) is not set invalid", i, f.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if l := len(wrapper.Raw().Devices); l != 3 {
|
||||
t.Errorf("Incorrect number of devices, %d != 3", l)
|
||||
}
|
||||
@@ -621,6 +616,30 @@ func TestRemoveDuplicateDevicesFolders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateFolders(t *testing.T) {
|
||||
// Duplicate folders are a loading error
|
||||
|
||||
_, err := Load("testdata/dupfolders.xml", device1)
|
||||
if err == nil || !strings.HasPrefix(err.Error(), "duplicate folder ID") {
|
||||
t.Fatal(`Expected error to mention "duplicate folder ID":`, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyFolderPaths(t *testing.T) {
|
||||
// Empty folder paths are allowed at the loading stage, and should not
|
||||
// get messed up by the prepare steps (e.g., become the current dir or
|
||||
// get a slash added so that it becomes the root directory or similar).
|
||||
|
||||
wrapper, err := Load("testdata/nopath.xml", device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
folder := wrapper.Folders()["f1"]
|
||||
if folder.Path() != "" {
|
||||
t.Errorf("Expected %q to be empty", folder.Path())
|
||||
}
|
||||
}
|
||||
|
||||
func TestV14ListenAddressesMigration(t *testing.T) {
|
||||
tcs := [][3][]string{
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ type FolderConfiguration struct {
|
||||
DisableSparseFiles bool `xml:"disableSparseFiles" json:"disableSparseFiles"`
|
||||
DisableTempIndexes bool `xml:"disableTempIndexes" json:"disableTempIndexes"`
|
||||
|
||||
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
|
||||
cachedPath string
|
||||
|
||||
DeprecatedReadOnly bool `xml:"ro,attr,omitempty" json:"-"`
|
||||
@@ -70,7 +69,7 @@ func (f FolderConfiguration) Path() string {
|
||||
// This is intentionally not a pointer method, because things like
|
||||
// cfg.Folders["default"].Path() should be valid.
|
||||
|
||||
if f.cachedPath == "" {
|
||||
if f.cachedPath == "" && f.RawPath != "" {
|
||||
l.Infoln("bug: uncached path call (should only happen in tests)")
|
||||
return f.cleanedPath()
|
||||
}
|
||||
@@ -108,31 +107,24 @@ func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) prepare() {
|
||||
if len(f.RawPath) == 0 {
|
||||
f.Invalid = "no directory configured"
|
||||
return
|
||||
}
|
||||
if f.RawPath != "" {
|
||||
// The reason it's done like this:
|
||||
// C: -> C:\ -> C:\ (issue that this is trying to fix)
|
||||
// C:\somedir -> C:\somedir\ -> C:\somedir
|
||||
// C:\somedir\ -> C:\somedir\\ -> C:\somedir
|
||||
// This way in the tests, we get away without OS specific separators
|
||||
// in the test configs.
|
||||
f.RawPath = filepath.Dir(f.RawPath + string(filepath.Separator))
|
||||
|
||||
// The reason it's done like this:
|
||||
// C: -> C:\ -> C:\ (issue that this is trying to fix)
|
||||
// C:\somedir -> C:\somedir\ -> C:\somedir
|
||||
// C:\somedir\ -> C:\somedir\\ -> C:\somedir
|
||||
// This way in the tests, we get away without OS specific separators
|
||||
// in the test configs.
|
||||
f.RawPath = filepath.Dir(f.RawPath + string(filepath.Separator))
|
||||
|
||||
// If we're not on Windows, we want the path to end with a slash to
|
||||
// penetrate symlinks. On Windows, paths must not end with a slash.
|
||||
if runtime.GOOS != "windows" && f.RawPath[len(f.RawPath)-1] != filepath.Separator {
|
||||
f.RawPath = f.RawPath + string(filepath.Separator)
|
||||
// If we're not on Windows, we want the path to end with a slash to
|
||||
// penetrate symlinks. On Windows, paths must not end with a slash.
|
||||
if runtime.GOOS != "windows" && f.RawPath[len(f.RawPath)-1] != filepath.Separator {
|
||||
f.RawPath = f.RawPath + string(filepath.Separator)
|
||||
}
|
||||
}
|
||||
|
||||
f.cachedPath = f.cleanedPath()
|
||||
|
||||
if f.ID == "" {
|
||||
f.ID = "default"
|
||||
}
|
||||
|
||||
if f.RescanIntervalS > MaxRescanIntervalS {
|
||||
f.RescanIntervalS = MaxRescanIntervalS
|
||||
} else if f.RescanIntervalS < 0 {
|
||||
@@ -145,6 +137,10 @@ func (f *FolderConfiguration) prepare() {
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) cleanedPath() string {
|
||||
if f.RawPath == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
cleaned := f.RawPath
|
||||
|
||||
// Attempt tilde expansion; leave unchanged in case of error
|
||||
@@ -168,6 +164,12 @@ func (f *FolderConfiguration) cleanedPath() string {
|
||||
return `\\?\` + cleaned
|
||||
}
|
||||
|
||||
// If we're not on Windows, we want the path to end with a slash to
|
||||
// penetrate symlinks. On Windows, paths must not end with a slash.
|
||||
if runtime.GOOS != "windows" && cleaned[len(cleaned)-1] != filepath.Separator {
|
||||
cleaned = cleaned + string(filepath.Separator)
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,6 @@
|
||||
<!-- duplicate, will be removed -->
|
||||
<address>192.0.2.5</address>
|
||||
</device>
|
||||
<folder id="f1" directory="testdata/">
|
||||
<!-- duplicate, will be disabled -->
|
||||
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
|
||||
<device id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA"></device>
|
||||
</folder>
|
||||
<folder id="f1" directory="testdata/">
|
||||
<!-- duplicate, will be disabled -->
|
||||
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
|
||||
</folder>
|
||||
<folder id="f2" directory="testdata/">
|
||||
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
|
||||
<device id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA"></device>
|
||||
6
lib/config/testdata/dupfolders.xml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<configuration version="15">
|
||||
<folder id="f1" directory="testdata/">
|
||||
</folder>
|
||||
<folder id="f1" directory="testdata/">
|
||||
</folder>
|
||||
</configuration>
|
||||
4
lib/config/testdata/nopath.xml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
<configuration version="15">
|
||||
<folder id="f1">
|
||||
</folder>
|
||||
</configuration>
|
||||
@@ -8,6 +8,7 @@ package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
@@ -41,11 +42,6 @@ type Committer interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
type CommitResponse struct {
|
||||
ValidationError error
|
||||
RequiresRestart bool
|
||||
}
|
||||
|
||||
// A wrapper around a Configuration that manages loads, saves and published
|
||||
// notifications of changes to registered Handlers
|
||||
|
||||
@@ -58,6 +54,8 @@ type Wrapper struct {
|
||||
replaces chan Configuration
|
||||
subs []Committer
|
||||
mut sync.Mutex
|
||||
|
||||
requiresRestart uint32 // an atomic bool
|
||||
}
|
||||
|
||||
// Wrap wraps an existing Configuration structure and ties it to a file on
|
||||
@@ -128,32 +126,21 @@ func (w *Wrapper) Raw() Configuration {
|
||||
}
|
||||
|
||||
// Replace swaps the current configuration object for the given one.
|
||||
func (w *Wrapper) Replace(cfg Configuration) CommitResponse {
|
||||
func (w *Wrapper) Replace(cfg Configuration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
return w.replaceLocked(cfg)
|
||||
}
|
||||
|
||||
func (w *Wrapper) replaceLocked(to Configuration) CommitResponse {
|
||||
func (w *Wrapper) replaceLocked(to Configuration) error {
|
||||
from := w.cfg
|
||||
|
||||
for _, sub := range w.subs {
|
||||
l.Debugln(sub, "verifying configuration")
|
||||
if err := sub.VerifyConfiguration(from, to); err != nil {
|
||||
l.Debugln(sub, "rejected config:", err)
|
||||
return CommitResponse{
|
||||
ValidationError: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allOk := true
|
||||
for _, sub := range w.subs {
|
||||
l.Debugln(sub, "committing configuration")
|
||||
ok := sub.CommitConfiguration(from, to)
|
||||
if !ok {
|
||||
l.Debugln(sub, "requires restart")
|
||||
allOk = false
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,8 +148,22 @@ func (w *Wrapper) replaceLocked(to Configuration) CommitResponse {
|
||||
w.deviceMap = nil
|
||||
w.folderMap = nil
|
||||
|
||||
return CommitResponse{
|
||||
RequiresRestart: !allOk,
|
||||
w.notifyListeners(from, to)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Wrapper) notifyListeners(from, to Configuration) {
|
||||
for _, sub := range w.subs {
|
||||
go w.notifyListener(sub, from, to)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Wrapper) notifyListener(sub Committer, from, to Configuration) {
|
||||
l.Debugln(sub, "committing configuration")
|
||||
if !sub.CommitConfiguration(from, to) {
|
||||
l.Debugln(sub, "requires restart")
|
||||
w.setRequiresRestart()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +183,7 @@ func (w *Wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
||||
|
||||
// SetDevice adds a new device to the configuration, or overwrites an existing
|
||||
// device with the same ID.
|
||||
func (w *Wrapper) SetDevice(dev DeviceConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetDevice(dev DeviceConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -218,7 +219,7 @@ func (w *Wrapper) Folders() map[string]FolderConfiguration {
|
||||
|
||||
// SetFolder adds a new folder to the configuration, or overwrites an existing
|
||||
// folder with the same ID.
|
||||
func (w *Wrapper) SetFolder(fld FolderConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetFolder(fld FolderConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -246,7 +247,7 @@ func (w *Wrapper) Options() OptionsConfiguration {
|
||||
}
|
||||
|
||||
// SetOptions replaces the current options configuration object.
|
||||
func (w *Wrapper) SetOptions(opts OptionsConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetOptions(opts OptionsConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
@@ -262,7 +263,7 @@ func (w *Wrapper) GUI() GUIConfiguration {
|
||||
}
|
||||
|
||||
// SetGUI replaces the current GUI configuration object.
|
||||
func (w *Wrapper) SetGUI(gui GUIConfiguration) CommitResponse {
|
||||
func (w *Wrapper) SetGUI(gui GUIConfiguration) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
newCfg := w.cfg.Copy()
|
||||
@@ -332,3 +333,11 @@ func (w *Wrapper) ListenAddresses() []string {
|
||||
}
|
||||
return util.UniqueStrings(addresses)
|
||||
}
|
||||
|
||||
func (w *Wrapper) RequiresRestart() bool {
|
||||
return atomic.LoadUint32(&w.requiresRestart) != 0
|
||||
}
|
||||
|
||||
func (w *Wrapper) setRequiresRestart() {
|
||||
atomic.StoreUint32(&w.requiresRestart, 1)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (d *relayDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConn
|
||||
tc = tls.Client(conn, d.tlsCfg)
|
||||
}
|
||||
|
||||
err = tc.Handshake()
|
||||
err = tlsTimedHandshake(tc)
|
||||
if err != nil {
|
||||
tc.Close()
|
||||
return IntermediateConnection{}, err
|
||||
|
||||
@@ -85,7 +85,7 @@ func (t *relayListener) Serve() {
|
||||
tc = tls.Client(conn, t.tlsCfg)
|
||||
}
|
||||
|
||||
err = tc.Handshake()
|
||||
err = tlsTimedHandshake(tc)
|
||||
if err != nil {
|
||||
tc.Close()
|
||||
l.Infoln("TLS handshake (BEP/relay):", err)
|
||||
|
||||
@@ -36,7 +36,10 @@ var (
|
||||
listeners = make(map[string]listenerFactory, 0)
|
||||
)
|
||||
|
||||
const perDeviceWarningRate = 1.0 / (15 * 60) // Once per 15 minutes
|
||||
const (
|
||||
perDeviceWarningRate = 1.0 / (15 * 60) // Once per 15 minutes
|
||||
tlsHandshakeTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// Service listens and dials all configured unconnected devices, via supported
|
||||
// dialers. Successful connections are handed to the model.
|
||||
@@ -159,8 +162,17 @@ next:
|
||||
if err != nil {
|
||||
if protocol.IsVersionMismatch(err) {
|
||||
// The error will be a relatively user friendly description
|
||||
// of what's wrong with the version compatibility
|
||||
msg := fmt.Sprintf("Connecting to %s (%s): %s", remoteID, c.RemoteAddr(), err)
|
||||
// of what's wrong with the version compatibility. By
|
||||
// default identify the other side by device ID and IP.
|
||||
remote := fmt.Sprintf("%v (%v)", remoteID, c.RemoteAddr())
|
||||
if hello.DeviceName != "" {
|
||||
// If the name was set in the hello return, use that to
|
||||
// give the user more info about which device is the
|
||||
// affected one. It probably says more than the remote
|
||||
// IP.
|
||||
remote = fmt.Sprintf("%q (%s %s, %v)", hello.DeviceName, hello.ClientName, hello.ClientVersion, remoteID)
|
||||
}
|
||||
msg := fmt.Sprintf("Connecting to %s: %s", remote, err)
|
||||
warningFor(remoteID, msg)
|
||||
} else {
|
||||
// It's something else - connection reset or whatever
|
||||
@@ -347,7 +359,7 @@ func (s *Service) connect() {
|
||||
}
|
||||
|
||||
if connected && dialerFactory.Priority() >= ct.Priority {
|
||||
l.Debugf("Not dialing using %s as priorty is less than current connection (%d >= %d)", dialerFactory, dialerFactory.Priority(), ct.Priority)
|
||||
l.Debugf("Not dialing using %s as priority is less than current connection (%d >= %d)", dialerFactory, dialerFactory.Priority(), ct.Priority)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -607,3 +619,9 @@ func warningFor(dev protocol.DeviceID, msg string) {
|
||||
l.Warnln(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func tlsTimedHandshake(tc *tls.Conn) error {
|
||||
tc.SetDeadline(time.Now().Add(tlsHandshakeTimeout))
|
||||
defer tc.SetDeadline(time.Time{})
|
||||
return tc.Handshake()
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ type Model interface {
|
||||
ConnectedTo(remoteID protocol.DeviceID) bool
|
||||
IsPaused(remoteID protocol.DeviceID) bool
|
||||
OnHello(protocol.DeviceID, net.Addr, protocol.HelloResult)
|
||||
GetHello(protocol.DeviceID) protocol.Version13HelloMessage
|
||||
GetHello(protocol.DeviceID) protocol.HelloIntf
|
||||
}
|
||||
|
||||
// serviceFunc wraps a function to create a suture.Service without stop
|
||||
|
||||
@@ -40,7 +40,7 @@ func (d *tcpDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConnec
|
||||
}
|
||||
|
||||
tc := tls.Client(conn, d.tlsCfg)
|
||||
err = tc.Handshake()
|
||||
err = tlsTimedHandshake(tc)
|
||||
if err != nil {
|
||||
tc.Close()
|
||||
return IntermediateConnection{}, err
|
||||
|
||||
@@ -108,7 +108,7 @@ func (t *tcpListener) Serve() {
|
||||
}
|
||||
|
||||
tc := tls.Server(conn, t.tlsCfg)
|
||||
err = tc.Handshake()
|
||||
err = tlsTimedHandshake(tc)
|
||||
if err != nil {
|
||||
l.Infoln("TLS handshake (BEP/tcp):", err)
|
||||
tc.Close()
|
||||
|
||||
@@ -73,7 +73,7 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
||||
|
||||
m := NewBlockMap(db, db.folderIdx.ID([]byte("folder1")))
|
||||
|
||||
f3.Flags |= protocol.FlagDirectory
|
||||
f3.Type = protocol.FileInfoTypeDirectory
|
||||
|
||||
err := m.Add([]protocol.FileInfo{f1, f2, f3})
|
||||
if err != nil {
|
||||
@@ -99,9 +99,11 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
||||
return true
|
||||
})
|
||||
|
||||
f3.Flags = f1.Flags
|
||||
f1.Flags |= protocol.FlagDeleted
|
||||
f2.Flags |= protocol.FlagInvalid
|
||||
f3.Permissions = f1.Permissions
|
||||
f3.Deleted = f1.Deleted
|
||||
f3.Invalid = f1.Invalid
|
||||
f1.Deleted = true
|
||||
f2.Invalid = true
|
||||
|
||||
// Should remove
|
||||
err = m.Update([]protocol.FileInfo{f1, f2, f3})
|
||||
@@ -145,9 +147,15 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
||||
t.Fatal("db not empty")
|
||||
}
|
||||
|
||||
f1.Flags = 0
|
||||
f2.Flags = 0
|
||||
f3.Flags = 0
|
||||
f1.Deleted = false
|
||||
f1.Invalid = false
|
||||
f1.Permissions = 0
|
||||
f2.Deleted = false
|
||||
f2.Invalid = false
|
||||
f2.Permissions = 0
|
||||
f3.Deleted = false
|
||||
f3.Invalid = false
|
||||
f3.Permissions = 0
|
||||
}
|
||||
|
||||
func TestBlockFinderLookup(t *testing.T) {
|
||||
@@ -187,7 +195,7 @@ func TestBlockFinderLookup(t *testing.T) {
|
||||
t.Fatal("Incorrect count", counter)
|
||||
}
|
||||
|
||||
f1.Flags |= protocol.FlagDeleted
|
||||
f1.Deleted = true
|
||||
|
||||
err = m1.Update([]protocol.FileInfo{f1})
|
||||
if err != nil {
|
||||
@@ -212,7 +220,7 @@ func TestBlockFinderLookup(t *testing.T) {
|
||||
t.Fatal("Incorrect count")
|
||||
}
|
||||
|
||||
f1.Flags = 0
|
||||
f1.Deleted = false
|
||||
}
|
||||
|
||||
func TestBlockFinderFix(t *testing.T) {
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
//go:generate -command genxdr go run ../../vendor/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||
//go:generate genxdr -o leveldb_xdr.go leveldb.go
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
@@ -14,27 +11,10 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
clockTick int64
|
||||
clockMut = sync.NewMutex()
|
||||
)
|
||||
|
||||
func clock(v int64) int64 {
|
||||
clockMut.Lock()
|
||||
defer clockMut.Unlock()
|
||||
if v > clockTick {
|
||||
clockTick = v + 1
|
||||
} else {
|
||||
clockTick++
|
||||
}
|
||||
return clockTick
|
||||
}
|
||||
|
||||
const (
|
||||
KeyTypeDevice = iota
|
||||
KeyTypeGlobal
|
||||
@@ -44,27 +24,19 @@ const (
|
||||
KeyTypeVirtualMtime
|
||||
KeyTypeFolderIdx
|
||||
KeyTypeDeviceIdx
|
||||
KeyTypeIndexID
|
||||
)
|
||||
|
||||
type fileVersion struct {
|
||||
version protocol.Vector
|
||||
device []byte
|
||||
}
|
||||
|
||||
type VersionList struct {
|
||||
versions []fileVersion
|
||||
}
|
||||
|
||||
func (l VersionList) String() string {
|
||||
var b bytes.Buffer
|
||||
var id protocol.DeviceID
|
||||
b.WriteString("{")
|
||||
for i, v := range l.versions {
|
||||
for i, v := range l.Versions {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
copy(id[:], v.device)
|
||||
fmt.Fprintf(&b, "{%d, %v}", v.version, id)
|
||||
copy(id[:], v.Device)
|
||||
fmt.Fprintf(&b, "{%d, %v}", v.Version, id)
|
||||
}
|
||||
b.WriteString("}")
|
||||
return b.String()
|
||||
@@ -101,7 +73,7 @@ func getFile(db dbReader, key []byte) (protocol.FileInfo, bool) {
|
||||
}
|
||||
|
||||
var f protocol.FileInfo
|
||||
err = f.UnmarshalXDR(bs)
|
||||
err = f.Unmarshal(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
// convertKeyFormat converts from the v0.12 to the v0.13 database format, to
|
||||
// avoid having to do rescan. The change is in the key format for folder
|
||||
// labels, so we basically just iterate over the database rewriting keys as
|
||||
// necessary.
|
||||
func convertKeyFormat(from, to *leveldb.DB) error {
|
||||
l.Infoln("Converting database key format")
|
||||
blocks, files, globals, unchanged := 0, 0, 0, 0
|
||||
|
||||
dbi := newDBInstance(to)
|
||||
i := from.NewIterator(nil, nil)
|
||||
for i.Next() {
|
||||
key := i.Key()
|
||||
switch key[0] {
|
||||
case KeyTypeBlock:
|
||||
folder, file := oldFromBlockKey(key)
|
||||
folderIdx := dbi.folderIdx.ID([]byte(folder))
|
||||
hash := key[1+64:]
|
||||
newKey := blockKeyInto(nil, hash, folderIdx, file)
|
||||
if err := to.Put(newKey, i.Value(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
blocks++
|
||||
|
||||
case KeyTypeDevice:
|
||||
newKey := dbi.deviceKey(oldDeviceKeyFolder(key), oldDeviceKeyDevice(key), oldDeviceKeyName(key))
|
||||
if err := to.Put(newKey, i.Value(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
files++
|
||||
|
||||
case KeyTypeGlobal:
|
||||
newKey := dbi.globalKey(oldGlobalKeyFolder(key), oldGlobalKeyName(key))
|
||||
if err := to.Put(newKey, i.Value(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
globals++
|
||||
|
||||
case KeyTypeVirtualMtime:
|
||||
// Cannot be converted, we drop it instead :(
|
||||
|
||||
default:
|
||||
if err := to.Put(key, i.Value(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
unchanged++
|
||||
}
|
||||
}
|
||||
|
||||
l.Infof("Converted %d blocks, %d files, %d globals (%d unchanged).", blocks, files, globals, unchanged)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func oldDeviceKeyFolder(key []byte) []byte {
|
||||
folder := key[1 : 1+64]
|
||||
izero := bytes.IndexByte(folder, 0)
|
||||
if izero < 0 {
|
||||
return folder
|
||||
}
|
||||
return folder[:izero]
|
||||
}
|
||||
|
||||
func oldDeviceKeyDevice(key []byte) []byte {
|
||||
return key[1+64 : 1+64+32]
|
||||
}
|
||||
|
||||
func oldDeviceKeyName(key []byte) []byte {
|
||||
return key[1+64+32:]
|
||||
}
|
||||
|
||||
func oldGlobalKeyName(key []byte) []byte {
|
||||
return key[1+64:]
|
||||
}
|
||||
|
||||
func oldGlobalKeyFolder(key []byte) []byte {
|
||||
folder := key[1 : 1+64]
|
||||
izero := bytes.IndexByte(folder, 0)
|
||||
if izero < 0 {
|
||||
return folder
|
||||
}
|
||||
return folder[:izero]
|
||||
}
|
||||
|
||||
func oldFromBlockKey(data []byte) (string, string) {
|
||||
if len(data) < 1+64+32+1 {
|
||||
panic("Incorrect key length")
|
||||
}
|
||||
if data[0] != KeyTypeBlock {
|
||||
panic("Incorrect key type")
|
||||
}
|
||||
|
||||
file := string(data[1+64+32:])
|
||||
|
||||
slice := data[1 : 1+64]
|
||||
izero := bytes.IndexByte(slice, 0)
|
||||
if izero > -1 {
|
||||
return string(slice[:izero]), file
|
||||
}
|
||||
return string(slice), file
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
func TestLabelConversion(t *testing.T) {
|
||||
os.RemoveAll("testdata/oldformat.db")
|
||||
defer os.RemoveAll("testdata/oldformat.db")
|
||||
os.RemoveAll("testdata/newformat.db")
|
||||
defer os.RemoveAll("testdata/newformat.db")
|
||||
|
||||
if err := unzip("testdata/oldformat.db.zip", "testdata"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
odb, err := leveldb.OpenFile("testdata/oldformat.db", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ldb, err := leveldb.OpenFile("testdata/newformat.db", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err = convertKeyFormat(odb, ldb); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb.Close()
|
||||
odb.Close()
|
||||
|
||||
inst, err := Open("testdata/newformat.db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fs := NewFileSet("default", inst)
|
||||
files, deleted, _ := fs.GlobalSize()
|
||||
if files+deleted != 953 {
|
||||
// Expected number of global entries determined by
|
||||
// ../../bin/stindex testdata/oldformat.db/ | grep global | grep -c default
|
||||
t.Errorf("Conversion error, global list differs (%d != 953)", files+deleted)
|
||||
}
|
||||
|
||||
files, deleted, _ = fs.LocalSize()
|
||||
if files+deleted != 953 {
|
||||
t.Errorf("Conversion error, device list differs (%d != 953)", files+deleted)
|
||||
}
|
||||
|
||||
f := NewBlockFinder(inst)
|
||||
// [block] F:"default" H:1c25dea9003cc16216e2a22900be1ec1cc5aaf270442904e2f9812c314e929d8 N:"f/f2/f25f1b3e6e029231b933531b2138796d" I:3
|
||||
h := []byte{0x1c, 0x25, 0xde, 0xa9, 0x00, 0x3c, 0xc1, 0x62, 0x16, 0xe2, 0xa2, 0x29, 0x00, 0xbe, 0x1e, 0xc1, 0xcc, 0x5a, 0xaf, 0x27, 0x04, 0x42, 0x90, 0x4e, 0x2f, 0x98, 0x12, 0xc3, 0x14, 0xe9, 0x29, 0xd8}
|
||||
found := 0
|
||||
f.Iterate([]string{"default"}, h, func(folder, file string, idx int32) bool {
|
||||
if folder == "default" && file == filepath.FromSlash("f/f2/f25f1b3e6e029231b933531b2138796d") && idx == 3 {
|
||||
found++
|
||||
}
|
||||
return true
|
||||
})
|
||||
if found != 1 {
|
||||
t.Errorf("Found %d blocks instead of expected 1", found)
|
||||
}
|
||||
|
||||
inst.Close()
|
||||
}
|
||||
|
||||
func unzip(src, dest string) error {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := r.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
os.MkdirAll(dest, 0755)
|
||||
|
||||
// Closure to address file descriptors issue with all the deferred .Close() methods
|
||||
extractAndWriteFile := func(f *zip.File) error {
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := rc.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
path := filepath.Join(dest, f.Name)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(path, f.Mode())
|
||||
} else {
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = io.Copy(f, rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, f := range r.File {
|
||||
err := extractAndWriteFile(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -10,11 +10,10 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
@@ -25,9 +24,10 @@ import (
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type deletionHandler func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator) int64
|
||||
type deletionHandler func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator)
|
||||
|
||||
type Instance struct {
|
||||
committed int64 // this must be the first attribute in the struct to ensure 64 bit alignment on 32 bit plaforms
|
||||
*leveldb.DB
|
||||
folderIdx *smallIndex
|
||||
deviceIdx *smallIndex
|
||||
@@ -46,16 +46,6 @@ func Open(file string) (*Instance, error) {
|
||||
WriteBuffer: 4 << 20,
|
||||
}
|
||||
|
||||
if _, err := os.Stat(file); os.IsNotExist(err) {
|
||||
// The file we are looking to open does not exist. This may be the
|
||||
// first launch so we should look for an old version and try to
|
||||
// convert it.
|
||||
if err := checkConvertDatabase(file); err != nil {
|
||||
l.Infoln("Converting old database:", err)
|
||||
l.Infoln("Will rescan from scratch.")
|
||||
}
|
||||
}
|
||||
|
||||
db, err := leveldb.OpenFile(file, opts)
|
||||
if leveldbIsCorrupted(err) {
|
||||
db, err = leveldb.RecoverFile(file, opts)
|
||||
@@ -91,7 +81,12 @@ func newDBInstance(db *leveldb.DB) *Instance {
|
||||
return i
|
||||
}
|
||||
|
||||
func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker, deleteFn deletionHandler) int64 {
|
||||
// Committed returns the number of items committed to the database since startup
|
||||
func (db *Instance) Committed() int64 {
|
||||
return atomic.LoadInt64(&db.committed)
|
||||
}
|
||||
|
||||
func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker, deleteFn deletionHandler) {
|
||||
sort.Sort(fileList(fs)) // sort list on name, same as in the database
|
||||
|
||||
t := db.newReadWriteTransaction()
|
||||
@@ -102,7 +97,6 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
|
||||
|
||||
moreDb := dbi.Next()
|
||||
fsi := 0
|
||||
var maxLocalVer int64
|
||||
|
||||
isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
|
||||
for {
|
||||
@@ -129,9 +123,7 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
|
||||
case moreFs && (!moreDb || cmp == -1):
|
||||
l.Debugln("generic replace; missing - insert")
|
||||
// Database is missing this file. Insert it.
|
||||
if lv := t.insertFile(folder, device, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
t.insertFile(folder, device, fs[fsi])
|
||||
if isLocalDevice {
|
||||
localSize.addFile(fs[fsi])
|
||||
}
|
||||
@@ -144,16 +136,14 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
|
||||
|
||||
case moreFs && moreDb && cmp == 0:
|
||||
// File exists on both sides - compare versions. We might get an
|
||||
// update with the same version and different flags if a device has
|
||||
// marked a file as invalid, so handle that too.
|
||||
// update with the same version if a device has marked a file as
|
||||
// invalid, so handle that too.
|
||||
l.Debugln("generic replace; exists - compare")
|
||||
var ef FileInfoTruncated
|
||||
ef.UnmarshalXDR(dbi.Value())
|
||||
if !fs[fsi].Version.Equal(ef.Version) || fs[fsi].Flags != ef.Flags {
|
||||
ef.Unmarshal(dbi.Value())
|
||||
if !fs[fsi].Version.Equal(ef.Version) || fs[fsi].Invalid != ef.Invalid {
|
||||
l.Debugln("generic replace; differs - insert")
|
||||
if lv := t.insertFile(folder, device, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
t.insertFile(folder, device, fs[fsi])
|
||||
if isLocalDevice {
|
||||
localSize.removeFile(ef)
|
||||
localSize.addFile(fs[fsi])
|
||||
@@ -172,9 +162,7 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
|
||||
|
||||
case moreDb && (!moreFs || cmp == 1):
|
||||
l.Debugln("generic replace; exists - remove")
|
||||
if lv := deleteFn(t, folder, device, oldName, dbi); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
deleteFn(t, folder, device, oldName, dbi)
|
||||
moreDb = dbi.Next()
|
||||
}
|
||||
|
||||
@@ -182,26 +170,21 @@ func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo
|
||||
// growing too large and thus allocating unnecessarily much memory.
|
||||
t.checkFlush()
|
||||
}
|
||||
|
||||
return maxLocalVer
|
||||
}
|
||||
|
||||
func (db *Instance) replace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) int64 {
|
||||
// TODO: Return the remaining maxLocalVer?
|
||||
return db.genericReplace(folder, device, fs, localSize, globalSize, func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator) int64 {
|
||||
func (db *Instance) replace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) {
|
||||
db.genericReplace(folder, device, fs, localSize, globalSize, func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator) {
|
||||
// Database has a file that we are missing. Remove it.
|
||||
l.Debugf("delete; folder=%q device=%v name=%q", folder, protocol.DeviceIDFromBytes(device), name)
|
||||
t.removeFromGlobal(folder, device, name, globalSize)
|
||||
t.Delete(dbi.Key())
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) int64 {
|
||||
func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
var maxLocalVer int64
|
||||
var fk []byte
|
||||
isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
|
||||
for _, f := range fs {
|
||||
@@ -213,9 +196,7 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, l
|
||||
localSize.addFile(f)
|
||||
}
|
||||
|
||||
if lv := t.insertFile(folder, device, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
t.insertFile(folder, device, f)
|
||||
if f.IsInvalid() {
|
||||
t.removeFromGlobal(folder, device, name, globalSize)
|
||||
} else {
|
||||
@@ -225,21 +206,18 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, l
|
||||
}
|
||||
|
||||
var ef FileInfoTruncated
|
||||
err = ef.UnmarshalXDR(bs)
|
||||
err = ef.Unmarshal(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Flags might change without the version being bumped when we set the
|
||||
// invalid flag on an existing file.
|
||||
if !ef.Version.Equal(f.Version) || ef.Flags != f.Flags {
|
||||
// The Invalid flag might change without the version being bumped.
|
||||
if !ef.Version.Equal(f.Version) || ef.Invalid != f.Invalid {
|
||||
if isLocalDevice {
|
||||
localSize.removeFile(ef)
|
||||
localSize.addFile(f)
|
||||
}
|
||||
|
||||
if lv := t.insertFile(folder, device, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
t.insertFile(folder, device, f)
|
||||
if f.IsInvalid() {
|
||||
t.removeFromGlobal(folder, device, name, globalSize)
|
||||
} else {
|
||||
@@ -251,8 +229,6 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, l
|
||||
// growing too large and thus allocating unnecessarily much memory.
|
||||
t.checkFlush()
|
||||
}
|
||||
|
||||
return maxLocalVer
|
||||
}
|
||||
|
||||
func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) {
|
||||
@@ -301,7 +277,7 @@ func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte,
|
||||
// struct, which in turn references the buffer it was unmarshalled
|
||||
// from. dbi.Value() just returns an internal slice that it reuses, so
|
||||
// we need to copy it.
|
||||
err := f.UnmarshalXDR(append([]byte{}, dbi.Value()...))
|
||||
err := f.Unmarshal(append([]byte{}, dbi.Value()...))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -340,16 +316,16 @@ func (db *Instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, boo
|
||||
}
|
||||
|
||||
var vl VersionList
|
||||
err = vl.UnmarshalXDR(bs)
|
||||
err = vl.Unmarshal(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(vl.versions) == 0 {
|
||||
if len(vl.Versions) == 0 {
|
||||
l.Debugln(k)
|
||||
panic("no versions?")
|
||||
}
|
||||
|
||||
k = db.deviceKey(folder, vl.versions[0].device, file)
|
||||
k = db.deviceKey(folder, vl.Versions[0].Device, file)
|
||||
bs, err = t.Get(k, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -377,11 +353,11 @@ func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
var vl VersionList
|
||||
err := vl.UnmarshalXDR(dbi.Value())
|
||||
err := vl.Unmarshal(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(vl.versions) == 0 {
|
||||
if len(vl.Versions) == 0 {
|
||||
l.Debugln(dbi.Key())
|
||||
panic("no versions?")
|
||||
}
|
||||
@@ -391,13 +367,13 @@ func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator
|
||||
return
|
||||
}
|
||||
|
||||
fk = db.deviceKeyInto(fk[:cap(fk)], folder, vl.versions[0].device, name)
|
||||
fk = db.deviceKeyInto(fk[:cap(fk)], folder, vl.Versions[0].Device, name)
|
||||
bs, err := t.Get(fk, nil)
|
||||
if err != nil {
|
||||
l.Debugf("folder: %q (%x)", folder, folder)
|
||||
l.Debugf("key: %q (%x)", dbi.Key(), dbi.Key())
|
||||
l.Debugf("vl: %v", vl)
|
||||
l.Debugf("vl.versions[0].device: %x", vl.versions[0].device)
|
||||
l.Debugf("vl.Versions[0].Device: %x", vl.Versions[0].Device)
|
||||
l.Debugf("name: %q (%x)", name, name)
|
||||
l.Debugf("fk: %q", fk)
|
||||
l.Debugf("fk: %x %x %x",
|
||||
@@ -429,17 +405,17 @@ func (db *Instance) availability(folder, file []byte) []protocol.DeviceID {
|
||||
}
|
||||
|
||||
var vl VersionList
|
||||
err = vl.UnmarshalXDR(bs)
|
||||
err = vl.Unmarshal(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var devices []protocol.DeviceID
|
||||
for _, v := range vl.versions {
|
||||
if !v.version.Equal(vl.versions[0].version) {
|
||||
for _, v := range vl.Versions {
|
||||
if !v.Version.Equal(vl.Versions[0].Version) {
|
||||
break
|
||||
}
|
||||
n := protocol.DeviceIDFromBytes(v.device)
|
||||
n := protocol.DeviceIDFromBytes(v.Device)
|
||||
devices = append(devices, n)
|
||||
}
|
||||
|
||||
@@ -457,11 +433,11 @@ func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator)
|
||||
nextFile:
|
||||
for dbi.Next() {
|
||||
var vl VersionList
|
||||
err := vl.UnmarshalXDR(dbi.Value())
|
||||
err := vl.Unmarshal(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(vl.versions) == 0 {
|
||||
if len(vl.Versions) == 0 {
|
||||
l.Debugln(dbi.Key())
|
||||
panic("no versions?")
|
||||
}
|
||||
@@ -469,29 +445,29 @@ nextFile:
|
||||
have := false // If we have the file, any version
|
||||
need := false // If we have a lower version of the file
|
||||
var haveVersion protocol.Vector
|
||||
for _, v := range vl.versions {
|
||||
if bytes.Equal(v.device, device) {
|
||||
for _, v := range vl.Versions {
|
||||
if bytes.Equal(v.Device, device) {
|
||||
have = true
|
||||
haveVersion = v.version
|
||||
haveVersion = v.Version
|
||||
// XXX: This marks Concurrent (i.e. conflicting) changes as
|
||||
// needs. Maybe we should do that, but it needs special
|
||||
// handling in the puller.
|
||||
need = !v.version.GreaterEqual(vl.versions[0].version)
|
||||
need = !v.Version.GreaterEqual(vl.Versions[0].Version)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if need || !have {
|
||||
name := db.globalKeyName(dbi.Key())
|
||||
needVersion := vl.versions[0].version
|
||||
needVersion := vl.Versions[0].Version
|
||||
|
||||
nextVersion:
|
||||
for i := range vl.versions {
|
||||
if !vl.versions[i].version.Equal(needVersion) {
|
||||
for i := range vl.Versions {
|
||||
if !vl.Versions[i].Version.Equal(needVersion) {
|
||||
// We haven't found a valid copy of the file with the needed version.
|
||||
continue nextFile
|
||||
}
|
||||
fk = db.deviceKeyInto(fk[:cap(fk)], folder, vl.versions[i].device, name)
|
||||
fk = db.deviceKeyInto(fk[:cap(fk)], folder, vl.Versions[i].Device, name)
|
||||
bs, err := t.Get(fk, nil)
|
||||
if err != nil {
|
||||
var id protocol.DeviceID
|
||||
@@ -521,7 +497,7 @@ nextFile:
|
||||
continue nextFile
|
||||
}
|
||||
|
||||
l.Debugf("need folder=%q device=%v name=%q need=%v have=%v haveV=%d globalV=%d", folder, protocol.DeviceIDFromBytes(device), name, need, have, haveVersion, vl.versions[0].version)
|
||||
l.Debugf("need folder=%q device=%v name=%q need=%v have=%v haveV=%d globalV=%d", folder, protocol.DeviceIDFromBytes(device), name, need, have, haveVersion, vl.Versions[0].Version)
|
||||
|
||||
if cont := fn(gf); !cont {
|
||||
return
|
||||
@@ -594,7 +570,7 @@ func (db *Instance) checkGlobals(folder []byte, globalSize *sizeTracker) {
|
||||
for dbi.Next() {
|
||||
gk := dbi.Key()
|
||||
var vl VersionList
|
||||
err := vl.UnmarshalXDR(dbi.Value())
|
||||
err := vl.Unmarshal(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -606,8 +582,8 @@ func (db *Instance) checkGlobals(folder []byte, globalSize *sizeTracker) {
|
||||
|
||||
name := db.globalKeyName(gk)
|
||||
var newVL VersionList
|
||||
for i, version := range vl.versions {
|
||||
fk = db.deviceKeyInto(fk[:cap(fk)], folder, version.device, name)
|
||||
for i, version := range vl.Versions {
|
||||
fk = db.deviceKeyInto(fk[:cap(fk)], folder, version.Device, name)
|
||||
|
||||
_, err := t.Get(fk, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
@@ -616,10 +592,10 @@ func (db *Instance) checkGlobals(folder []byte, globalSize *sizeTracker) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
newVL.versions = append(newVL.versions, version)
|
||||
newVL.Versions = append(newVL.Versions, version)
|
||||
|
||||
if i == 0 {
|
||||
fi, ok := t.getFile(folder, version.device, name)
|
||||
fi, ok := t.getFile(folder, version.Device, name)
|
||||
if !ok {
|
||||
panic("nonexistent global master file")
|
||||
}
|
||||
@@ -627,8 +603,8 @@ func (db *Instance) checkGlobals(folder []byte, globalSize *sizeTracker) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(newVL.versions) != len(vl.versions) {
|
||||
t.Put(dbi.Key(), newVL.MustMarshalXDR())
|
||||
if len(newVL.Versions) != len(vl.Versions) {
|
||||
t.Put(dbi.Key(), mustMarshal(&newVL))
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
@@ -705,15 +681,46 @@ func (db *Instance) globalKeyFolder(key []byte) []byte {
|
||||
return folder
|
||||
}
|
||||
|
||||
func (db *Instance) getIndexID(device, folder []byte) protocol.IndexID {
|
||||
key := db.indexIDKey(device, folder)
|
||||
cur, err := db.Get(key, nil)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var id protocol.IndexID
|
||||
if err := id.Unmarshal(cur); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (db *Instance) setIndexID(device, folder []byte, id protocol.IndexID) {
|
||||
key := db.indexIDKey(device, folder)
|
||||
bs, _ := id.Marshal() // marshalling can't fail
|
||||
if err := db.Put(key, bs, nil); err != nil {
|
||||
panic("storing index ID: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) indexIDKey(device, folder []byte) []byte {
|
||||
k := make([]byte, keyPrefixLen+keyDeviceLen+keyFolderLen)
|
||||
k[0] = KeyTypeIndexID
|
||||
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.deviceIdx.ID(device))
|
||||
binary.BigEndian.PutUint32(k[keyPrefixLen+keyDeviceLen:], db.folderIdx.ID(folder))
|
||||
return k
|
||||
}
|
||||
|
||||
func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
|
||||
if truncate {
|
||||
var tf FileInfoTruncated
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
err := tf.Unmarshal(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
var tf protocol.FileInfo
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
err := tf.Unmarshal(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
@@ -733,50 +740,6 @@ func leveldbIsCorrupted(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// checkConvertDatabase tries to convert an existing old (v0.11) database to
|
||||
// new (v0.13) format.
|
||||
func checkConvertDatabase(dbFile string) error {
|
||||
oldLoc := filepath.Join(filepath.Dir(dbFile), "index-v0.11.0.db")
|
||||
if _, err := os.Stat(oldLoc); os.IsNotExist(err) {
|
||||
// The old database file does not exist; that's ok, continue as if
|
||||
// everything succeeded.
|
||||
return nil
|
||||
} else if err != nil {
|
||||
// Any other error is weird.
|
||||
return err
|
||||
}
|
||||
|
||||
// There exists a database in the old format. We run a one time
|
||||
// conversion from old to new.
|
||||
|
||||
fromDb, err := leveldb.OpenFile(oldLoc, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
toDb, err := leveldb.OpenFile(dbFile, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = convertKeyFormat(fromDb, toDb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = toDb.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We've done this one, we don't want to do it again (if the user runs
|
||||
// -reset or so). We don't care too much about errors any more at this stage.
|
||||
fromDb.Close()
|
||||
osutil.Rename(oldLoc, oldLoc+".converted")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// A smallIndex is an in memory bidirectional []byte to uint32 map. It gives
|
||||
// fast lookups in both directions and persists to the database. Don't use for
|
||||
// storing more items than fit comfortably in RAM.
|
||||
|
||||
@@ -8,6 +8,7 @@ package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
@@ -55,33 +56,30 @@ func (db *Instance) newReadWriteTransaction() readWriteTransaction {
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) close() {
|
||||
if err := t.db.Write(t.Batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.flush()
|
||||
t.readOnlyTransaction.close()
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) checkFlush() {
|
||||
if t.Batch.Len() > batchFlushSize {
|
||||
if err := t.db.Write(t.Batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.flush()
|
||||
t.Batch.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) insertFile(folder, device []byte, file protocol.FileInfo) int64 {
|
||||
l.Debugf("insert; folder=%q device=%v %v", folder, protocol.DeviceIDFromBytes(device), file)
|
||||
|
||||
if file.LocalVersion == 0 {
|
||||
file.LocalVersion = clock(0)
|
||||
func (t readWriteTransaction) flush() {
|
||||
if err := t.db.Write(t.Batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
atomic.AddInt64(&t.db.committed, int64(t.Batch.Len()))
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) insertFile(folder, device []byte, file protocol.FileInfo) {
|
||||
l.Debugf("insert; folder=%q device=%v %v", folder, protocol.DeviceIDFromBytes(device), file)
|
||||
|
||||
name := []byte(file.Name)
|
||||
nk := t.db.deviceKey(folder, device, name)
|
||||
t.Put(nk, file.MustMarshalXDR())
|
||||
|
||||
return file.LocalVersion
|
||||
t.Put(nk, mustMarshal(&file))
|
||||
}
|
||||
|
||||
// updateGlobal adds this device+version to the version list for the given
|
||||
@@ -101,14 +99,14 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
|
||||
var hasOldFile bool
|
||||
// Remove the device from the current version list
|
||||
if len(svl) != 0 {
|
||||
err = fl.UnmarshalXDR(svl)
|
||||
err = fl.Unmarshal(svl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := range fl.versions {
|
||||
if bytes.Equal(fl.versions[i].device, device) {
|
||||
if fl.versions[i].version.Equal(file.Version) {
|
||||
for i := range fl.Versions {
|
||||
if bytes.Equal(fl.Versions[i].Device, device) {
|
||||
if fl.Versions[i].Version.Equal(file.Version) {
|
||||
// No need to do anything
|
||||
return false
|
||||
}
|
||||
@@ -116,29 +114,29 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
|
||||
if i == 0 {
|
||||
// Keep the current newest file around so we can subtract it from
|
||||
// the globalSize if we replace it.
|
||||
oldFile, hasOldFile = t.getFile(folder, fl.versions[0].device, name)
|
||||
oldFile, hasOldFile = t.getFile(folder, fl.Versions[0].Device, name)
|
||||
}
|
||||
|
||||
fl.versions = append(fl.versions[:i], fl.versions[i+1:]...)
|
||||
fl.Versions = append(fl.Versions[:i], fl.Versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nv := fileVersion{
|
||||
device: device,
|
||||
version: file.Version,
|
||||
nv := FileVersion{
|
||||
Device: device,
|
||||
Version: file.Version,
|
||||
}
|
||||
|
||||
insertedAt := -1
|
||||
// Find a position in the list to insert this file. The file at the front
|
||||
// of the list is the newer, the "global".
|
||||
for i := range fl.versions {
|
||||
switch fl.versions[i].version.Compare(file.Version) {
|
||||
for i := range fl.Versions {
|
||||
switch fl.Versions[i].Version.Compare(file.Version) {
|
||||
case protocol.Equal, protocol.Lesser:
|
||||
// The version at this point in the list is equal to or lesser
|
||||
// ("older") than us. We insert ourselves in front of it.
|
||||
fl.versions = insertVersion(fl.versions, i, nv)
|
||||
fl.Versions = insertVersion(fl.Versions, i, nv)
|
||||
insertedAt = i
|
||||
goto done
|
||||
|
||||
@@ -149,12 +147,12 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
|
||||
// "Greater" in the condition above is just based on the device
|
||||
// IDs in the version vector, which is not the only thing we use
|
||||
// to determine the winner.)
|
||||
of, ok := t.getFile(folder, fl.versions[i].device, name)
|
||||
of, ok := t.getFile(folder, fl.Versions[i].Device, name)
|
||||
if !ok {
|
||||
panic("file referenced in version list does not exist")
|
||||
}
|
||||
if file.WinsConflict(of) {
|
||||
fl.versions = insertVersion(fl.versions, i, nv)
|
||||
fl.Versions = insertVersion(fl.Versions, i, nv)
|
||||
insertedAt = i
|
||||
goto done
|
||||
}
|
||||
@@ -162,8 +160,8 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
|
||||
}
|
||||
|
||||
// We didn't find a position for an insert above, so append to the end.
|
||||
fl.versions = append(fl.versions, nv)
|
||||
insertedAt = len(fl.versions) - 1
|
||||
fl.Versions = append(fl.Versions, nv)
|
||||
insertedAt = len(fl.Versions) - 1
|
||||
|
||||
done:
|
||||
if insertedAt == 0 {
|
||||
@@ -174,9 +172,9 @@ done:
|
||||
if hasOldFile {
|
||||
// We have the old file that was removed at the head of the list.
|
||||
globalSize.removeFile(oldFile)
|
||||
} else if len(fl.versions) > 1 {
|
||||
} else if len(fl.Versions) > 1 {
|
||||
// The previous newest version is now at index 1, grab it from there.
|
||||
oldFile, ok := t.getFile(folder, fl.versions[1].device, name)
|
||||
oldFile, ok := t.getFile(folder, fl.Versions[1].Device, name)
|
||||
if !ok {
|
||||
panic("file referenced in version list does not exist")
|
||||
}
|
||||
@@ -186,7 +184,7 @@ done:
|
||||
}
|
||||
|
||||
l.Debugf("new global after update: %v", fl)
|
||||
t.Put(gk, fl.MustMarshalXDR())
|
||||
t.Put(gk, mustMarshal(&fl))
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -206,14 +204,14 @@ func (t readWriteTransaction) removeFromGlobal(folder, device, file []byte, glob
|
||||
}
|
||||
|
||||
var fl VersionList
|
||||
err = fl.UnmarshalXDR(svl)
|
||||
err = fl.Unmarshal(svl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
removed := false
|
||||
for i := range fl.versions {
|
||||
if bytes.Equal(fl.versions[i].device, device) {
|
||||
for i := range fl.Versions {
|
||||
if bytes.Equal(fl.Versions[i].Device, device) {
|
||||
if i == 0 && globalSize != nil {
|
||||
f, ok := t.getFile(folder, device, file)
|
||||
if !ok {
|
||||
@@ -222,18 +220,18 @@ func (t readWriteTransaction) removeFromGlobal(folder, device, file []byte, glob
|
||||
globalSize.removeFile(f)
|
||||
removed = true
|
||||
}
|
||||
fl.versions = append(fl.versions[:i], fl.versions[i+1:]...)
|
||||
fl.Versions = append(fl.Versions[:i], fl.Versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(fl.versions) == 0 {
|
||||
if len(fl.Versions) == 0 {
|
||||
t.Delete(gk)
|
||||
} else {
|
||||
l.Debugf("new global after remove: %v", fl)
|
||||
t.Put(gk, fl.MustMarshalXDR())
|
||||
t.Put(gk, mustMarshal(&fl))
|
||||
if removed {
|
||||
f, ok := t.getFile(folder, fl.versions[0].device, file)
|
||||
f, ok := t.getFile(folder, fl.Versions[0].Device, file)
|
||||
if !ok {
|
||||
panic("new global is nonexistent file")
|
||||
}
|
||||
@@ -242,9 +240,21 @@ func (t readWriteTransaction) removeFromGlobal(folder, device, file []byte, glob
|
||||
}
|
||||
}
|
||||
|
||||
func insertVersion(vl []fileVersion, i int, v fileVersion) []fileVersion {
|
||||
t := append(vl, fileVersion{})
|
||||
func insertVersion(vl []FileVersion, i int, v FileVersion) []FileVersion {
|
||||
t := append(vl, FileVersion{})
|
||||
copy(t[i+1:], t[i:])
|
||||
t[i] = v
|
||||
return t
|
||||
}
|
||||
|
||||
type marshaller interface {
|
||||
Marshal() ([]byte, error)
|
||||
}
|
||||
|
||||
func mustMarshal(f marshaller) []byte {
|
||||
bs, err := f.Marshal()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
// ************************************************************
|
||||
// This file is automatically generated by genxdr. Do not edit.
|
||||
// ************************************************************
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/calmh/xdr"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
fileVersion Structure:
|
||||
|
||||
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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Vector Structure \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ device (length + padded data) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct fileVersion {
|
||||
Vector version;
|
||||
opaque device<>;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (o fileVersion) XDRSize() int {
|
||||
return o.version.XDRSize() +
|
||||
4 + len(o.device) + xdr.Padding(len(o.device))
|
||||
}
|
||||
|
||||
func (o fileVersion) MarshalXDR() ([]byte, error) {
|
||||
buf := make([]byte, o.XDRSize())
|
||||
m := &xdr.Marshaller{Data: buf}
|
||||
return buf, o.MarshalXDRInto(m)
|
||||
}
|
||||
|
||||
func (o fileVersion) MustMarshalXDR() []byte {
|
||||
bs, err := o.MarshalXDR()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (o fileVersion) MarshalXDRInto(m *xdr.Marshaller) error {
|
||||
if err := o.version.MarshalXDRInto(m); err != nil {
|
||||
return err
|
||||
}
|
||||
m.MarshalBytes(o.device)
|
||||
return m.Error
|
||||
}
|
||||
|
||||
func (o *fileVersion) UnmarshalXDR(bs []byte) error {
|
||||
u := &xdr.Unmarshaller{Data: bs}
|
||||
return o.UnmarshalXDRFrom(u)
|
||||
}
|
||||
func (o *fileVersion) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
|
||||
(&o.version).UnmarshalXDRFrom(u)
|
||||
o.device = u.UnmarshalBytes()
|
||||
return u.Error
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
VersionList Structure:
|
||||
|
||||
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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Number of versions |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Zero or more fileVersion Structures \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct VersionList {
|
||||
fileVersion versions<>;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (o VersionList) XDRSize() int {
|
||||
return 4 + xdr.SizeOfSlice(o.versions)
|
||||
}
|
||||
|
||||
func (o VersionList) MarshalXDR() ([]byte, error) {
|
||||
buf := make([]byte, o.XDRSize())
|
||||
m := &xdr.Marshaller{Data: buf}
|
||||
return buf, o.MarshalXDRInto(m)
|
||||
}
|
||||
|
||||
func (o VersionList) MustMarshalXDR() []byte {
|
||||
bs, err := o.MarshalXDR()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (o VersionList) MarshalXDRInto(m *xdr.Marshaller) error {
|
||||
m.MarshalUint32(uint32(len(o.versions)))
|
||||
for i := range o.versions {
|
||||
if err := o.versions[i].MarshalXDRInto(m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return m.Error
|
||||
}
|
||||
|
||||
func (o *VersionList) UnmarshalXDR(bs []byte) error {
|
||||
u := &xdr.Unmarshaller{Data: bs}
|
||||
return o.UnmarshalXDRFrom(u)
|
||||
}
|
||||
func (o *VersionList) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
|
||||
_versionsSize := int(u.UnmarshalUint32())
|
||||
if _versionsSize < 0 {
|
||||
return xdr.ElementSizeExceeded("versions", _versionsSize, 0)
|
||||
} else if _versionsSize == 0 {
|
||||
o.versions = nil
|
||||
} else {
|
||||
if _versionsSize <= len(o.versions) {
|
||||
o.versions = o.versions[:_versionsSize]
|
||||
} else {
|
||||
o.versions = make([]fileVersion, _versionsSize)
|
||||
}
|
||||
for i := range o.versions {
|
||||
(&o.versions[i]).UnmarshalXDRFrom(u)
|
||||
}
|
||||
}
|
||||
return u.Error
|
||||
}
|
||||
117
lib/db/set.go
@@ -14,6 +14,7 @@ package db
|
||||
|
||||
import (
|
||||
stdsync "sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
@@ -21,19 +22,22 @@ import (
|
||||
)
|
||||
|
||||
type FileSet struct {
|
||||
localVersion map[protocol.DeviceID]int64
|
||||
mutex sync.Mutex
|
||||
localVersion int64 // Our local version counter
|
||||
folder string
|
||||
db *Instance
|
||||
blockmap *BlockMap
|
||||
localSize sizeTracker
|
||||
globalSize sizeTracker
|
||||
|
||||
remoteLocalVersion map[protocol.DeviceID]int64 // Highest seen local versions for other devices
|
||||
updateMutex sync.Mutex // protects remoteLocalVersion and database updates
|
||||
}
|
||||
|
||||
// FileIntf is the set of methods implemented by both protocol.FileInfo and
|
||||
// protocol.FileInfoTruncated.
|
||||
// FileInfoTruncated.
|
||||
type FileIntf interface {
|
||||
Size() int64
|
||||
FileSize() int64
|
||||
FileName() string
|
||||
IsDeleted() bool
|
||||
IsInvalid() bool
|
||||
IsDirectory() bool
|
||||
@@ -42,7 +46,7 @@ type FileIntf interface {
|
||||
}
|
||||
|
||||
// The Iterator is called with either a protocol.FileInfo or a
|
||||
// protocol.FileInfoTruncated (depending on the method) and returns true to
|
||||
// FileInfoTruncated (depending on the method) and returns true to
|
||||
// continue iteration, false to stop.
|
||||
type Iterator func(f FileIntf) bool
|
||||
|
||||
@@ -64,7 +68,7 @@ func (s *sizeTracker) addFile(f FileIntf) {
|
||||
} else {
|
||||
s.files++
|
||||
}
|
||||
s.bytes += f.Size()
|
||||
s.bytes += f.FileSize()
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
@@ -79,7 +83,7 @@ func (s *sizeTracker) removeFile(f FileIntf) {
|
||||
} else {
|
||||
s.files--
|
||||
}
|
||||
s.bytes -= f.Size()
|
||||
s.bytes -= f.FileSize()
|
||||
if s.deleted < 0 || s.files < 0 {
|
||||
panic("bug: removed more than added")
|
||||
}
|
||||
@@ -94,11 +98,11 @@ func (s *sizeTracker) Size() (files, deleted int, bytes int64) {
|
||||
|
||||
func NewFileSet(folder string, db *Instance) *FileSet {
|
||||
var s = FileSet{
|
||||
localVersion: make(map[protocol.DeviceID]int64),
|
||||
folder: folder,
|
||||
db: db,
|
||||
blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
|
||||
mutex: sync.NewMutex(),
|
||||
remoteLocalVersion: make(map[protocol.DeviceID]int64),
|
||||
folder: folder,
|
||||
db: db,
|
||||
blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
|
||||
updateMutex: sync.NewMutex(),
|
||||
}
|
||||
|
||||
s.db.checkGlobals([]byte(folder), &s.globalSize)
|
||||
@@ -106,16 +110,17 @@ func NewFileSet(folder string, db *Instance) *FileSet {
|
||||
var deviceID protocol.DeviceID
|
||||
s.db.withAllFolderTruncated([]byte(folder), func(device []byte, f FileInfoTruncated) bool {
|
||||
copy(deviceID[:], device)
|
||||
if f.LocalVersion > s.localVersion[deviceID] {
|
||||
s.localVersion[deviceID] = f.LocalVersion
|
||||
}
|
||||
if deviceID == protocol.LocalDeviceID {
|
||||
if f.LocalVersion > s.localVersion {
|
||||
s.localVersion = f.LocalVersion
|
||||
}
|
||||
s.localSize.addFile(f)
|
||||
} else if f.LocalVersion > s.remoteLocalVersion[deviceID] {
|
||||
s.remoteLocalVersion[deviceID] = f.LocalVersion
|
||||
}
|
||||
return true
|
||||
})
|
||||
l.Debugf("loaded localVersion for %q: %#v", folder, s.localVersion)
|
||||
clock(s.localVersion[protocol.LocalDeviceID])
|
||||
|
||||
return &s
|
||||
}
|
||||
@@ -123,13 +128,25 @@ func NewFileSet(folder string, db *Instance) *FileSet {
|
||||
func (s *FileSet) Replace(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
l.Debugf("%s Replace(%v, [%d])", s.folder, device, len(fs))
|
||||
normalizeFilenames(fs)
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
s.localVersion[device] = s.db.replace([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
|
||||
if len(fs) == 0 {
|
||||
// Reset the local version if all files were removed.
|
||||
s.localVersion[device] = 0
|
||||
|
||||
s.updateMutex.Lock()
|
||||
defer s.updateMutex.Unlock()
|
||||
|
||||
if device == protocol.LocalDeviceID {
|
||||
if len(fs) == 0 {
|
||||
s.localVersion = 0
|
||||
} else {
|
||||
// Always overwrite LocalVersion on updated files to ensure
|
||||
// correct ordering. The caller is supposed to leave it set to
|
||||
// zero anyhow.
|
||||
for i := range fs {
|
||||
fs[i].LocalVersion = atomic.AddInt64(&s.localVersion, 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.remoteLocalVersion[device] = maxLocalVersion(fs)
|
||||
}
|
||||
s.db.replace([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
|
||||
if device == protocol.LocalDeviceID {
|
||||
s.blockmap.Drop()
|
||||
s.blockmap.Add(fs)
|
||||
@@ -139,12 +156,15 @@ func (s *FileSet) Replace(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
l.Debugf("%s Update(%v, [%d])", s.folder, device, len(fs))
|
||||
normalizeFilenames(fs)
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.updateMutex.Lock()
|
||||
defer s.updateMutex.Unlock()
|
||||
|
||||
if device == protocol.LocalDeviceID {
|
||||
discards := make([]protocol.FileInfo, 0, len(fs))
|
||||
updates := make([]protocol.FileInfo, 0, len(fs))
|
||||
for _, newFile := range fs {
|
||||
for i, newFile := range fs {
|
||||
fs[i].LocalVersion = atomic.AddInt64(&s.localVersion, 1)
|
||||
existingFile, ok := s.db.getFile([]byte(s.folder), device[:], []byte(newFile.Name))
|
||||
if !ok || !existingFile.Version.Equal(newFile.Version) {
|
||||
discards = append(discards, existingFile)
|
||||
@@ -153,10 +173,10 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
}
|
||||
s.blockmap.Discard(discards)
|
||||
s.blockmap.Update(updates)
|
||||
} else {
|
||||
s.remoteLocalVersion[device] = maxLocalVersion(fs)
|
||||
}
|
||||
if lv := s.db.updateFiles([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize); lv > s.localVersion[device] {
|
||||
s.localVersion[device] = lv
|
||||
}
|
||||
s.db.updateFiles([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
|
||||
}
|
||||
|
||||
func (s *FileSet) WithNeed(device protocol.DeviceID, fn Iterator) {
|
||||
@@ -229,9 +249,13 @@ func (s *FileSet) Availability(file string) []protocol.DeviceID {
|
||||
}
|
||||
|
||||
func (s *FileSet) LocalVersion(device protocol.DeviceID) int64 {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
return s.localVersion[device]
|
||||
if device == protocol.LocalDeviceID {
|
||||
return atomic.LoadInt64(&s.localVersion)
|
||||
}
|
||||
|
||||
s.updateMutex.Lock()
|
||||
defer s.updateMutex.Unlock()
|
||||
return s.remoteLocalVersion[device]
|
||||
}
|
||||
|
||||
func (s *FileSet) LocalSize() (files, deleted int, bytes int64) {
|
||||
@@ -242,6 +266,37 @@ func (s *FileSet) GlobalSize() (files, deleted int, bytes int64) {
|
||||
return s.globalSize.Size()
|
||||
}
|
||||
|
||||
func (s *FileSet) IndexID(device protocol.DeviceID) protocol.IndexID {
|
||||
id := s.db.getIndexID(device[:], []byte(s.folder))
|
||||
if id == 0 && device == protocol.LocalDeviceID {
|
||||
// No index ID set yet. We create one now.
|
||||
id = protocol.NewIndexID()
|
||||
s.db.setIndexID(device[:], []byte(s.folder), id)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
|
||||
if device == protocol.LocalDeviceID {
|
||||
panic("do not explicitly set index ID for local device")
|
||||
}
|
||||
s.db.setIndexID(device[:], []byte(s.folder), id)
|
||||
}
|
||||
|
||||
// maxLocalVersion returns the highest of the LocalVersion numbers found in
|
||||
// the given slice of FileInfos. This should really be the LocalVersion of
|
||||
// the last item, but Syncthing v0.14.0 and other implementations may not
|
||||
// implement update sorting....
|
||||
func maxLocalVersion(fs []protocol.FileInfo) int64 {
|
||||
var max int64
|
||||
for _, f := range fs {
|
||||
if f.LocalVersion > max {
|
||||
max = f.LocalVersion
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
// DropFolder clears out all information related to the given folder from the
|
||||
// database.
|
||||
func DropFolder(db *Instance, folder string) {
|
||||
|
||||