Compare commits

..
Author SHA1 Message Date
Jakob Borg 0155b6f841 Update docs & translations
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2976
2016-04-15 11:47:33 +00:00
Jakob Borg f6953624dd lib/model: Test should pass go vet inspections
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2977
2016-04-15 11:41:18 +00:00
AudriusButkeviciusandJakob Borg 1a5f524ae4 lib/model, lib/protocol: Implement temporary indexes (fixes #950)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2252
2016-04-15 10:59:41 +00:00
Jakob BorgandAudrius Butkevicius a4cd4cc253 build: Clean up "go vet" and "go lint" steps
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2972
2016-04-15 07:26:25 +00:00
Audrius ButkeviciusandJakob Borg c49453c519 lib/pmp: Add NAT-PMP support (ref #698)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2968
2016-04-13 18:50:40 +00:00
Jakob Borg 52c7804f32 lib/connections: Silence vet and lint warnings
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2971
2016-04-13 11:50:51 +00:00
Audrius ButkeviciusandJakob Borg 19b4f3bfb4 lib/nat: Add a nat package and service to track mappings on multiple IGDs 2016-04-10 19:36:38 +00:00
Jakob BorgandAudrius Butkevicius f3ac421266 lib/protocol: Comment the bit numbers for flags in IndexMessage 2016-04-10 10:47:30 +00:00
Michael PloujnikovandAudrius Butkevicius 7533a61203 unifySubs: add two trivial test cases 2016-04-10 02:41:28 +00:00
Jakob Borg 6355a7019b gui: Update translations for previous commit 2016-04-09 12:23:13 +00:00
Audrius ButkeviciusandJakob Borg 490464e170 gui: Some browsers force lowercase attributes 2016-04-09 12:20:56 +00:00
Michael PloujnikovandJakob Borg 467d338fe4 lib/model: Scanning unknown items is OK as long as the parent is known (fixes #2915) 2016-04-09 11:25:06 +00:00
Audrius ButkeviciusandJakob Borg 6130578d18 lib/db: Empty slice is not nil (fixes #2872) 2016-04-09 07:46:19 +00:00
Audrius ButkeviciusandJakob Borg 4389bb037d lib/model: Add option for overwriting names on connect (fixes #2912) 2016-04-09 07:43:47 +00:00
Audrius ButkeviciusandJakob Borg 2eb8a9ef56 all: Dead code cleanup 2016-04-09 01:10:31 +00:00
Audrius ButkeviciusandJakob Borg 393798098c cmd/syncthing: Listening on a 0 port is not valid (fixes #2926) 2016-04-09 01:06:55 +00:00
Jakob Borg 668eb7c398 build: Archives should have release name as first file name component 2016-04-08 10:53:29 +00:00
Jakob Borg 0937f85534 gui, man: Update docs & translations 2016-04-08 10:25:40 +00:00
Jakob BorgandAudrius Butkevicius cf64376dca build: go vet is now included in the distribution 2016-04-08 10:19:11 +00:00
Audrius ButkeviciusandJakob Borg 5a98af622d lib/ignore: Implement deletable ignores using (?d) prefix (fixes #1362) 2016-04-07 09:34:07 +00:00
Jakob BorgandAudrius Butkevicius 4f5d0b46f7 build: Parameterize build targets 2016-04-06 22:18:30 +01:00
Lars K.W. GohlkeandJakob Borg 492e92d65d gui: Show Javascript error indicator on dev builds, remove logging of missing translations 2016-04-05 06:36:53 +00:00
Jakob Borg 181939c841 lib/ignore: Correct case insensitive matching on Mac/Windows
There was a bug in that we only did the lowercase folding when the
pattern had an explicit (?i), which is not the case on Windows/Mac
necessarily.
2016-04-05 06:35:51 +00:00
Audrius ButkeviciusandJakob Borg b678b4e048 cmd/syncthing: Skip a calculation if timediff is zero (fixes #2854) 2016-04-05 07:12:17 +02:00
Jakob BorgandAudrius Butkevicius 1934b3a5b6 lib/ignore: Remove pattern for foo/** which is already covered by foo/
Actual speed difference according to benchmarks is hidden in the noise



Also make the "pattern" field for each entry match what is actually

evaluated.
2016-04-04 13:22:25 +01:00
Jakob BorgandAudrius Butkevicius cc1d122352 lib/model: Correctly detect deleted but previously ignored files as deleted 2016-04-04 11:53:55 +01:00
Jakob BorgandAudrius Butkevicius a4f0b85462 lib/config: Disable cacheIgnoredFiles, new default is disabled 2016-04-03 20:18:16 +01:00
Laurent EtiembleandJakob Borg 7b4e1e9055 cmd/syncthing: Fix handler ordering so CORS middleware wraps all the others but the debug one 2016-04-03 13:24:55 +02:00
Jakob BorgandAudrius Butkevicius 4c3cd4c9e3 lib/ignore: Replace lib/fnmatch with github.com/gobwas/glob
Because it's literally ten times faster:



	benchmark                  old ns/op     new ns/op     delta

	BenchmarkMatch-8           13842         1200          -91.33%

	BenchmarkMatchCached-8     139           147           +5.76%



	benchmark                  old allocs     new allocs     delta

	BenchmarkMatch-8           0              0              +0.00%

	BenchmarkMatchCached-8     0              0              +0.00%



	benchmark                  old bytes     new bytes     delta

	BenchmarkMatch-8           12            0             -100.00%

	BenchmarkMatchCached-8     0             0             +0.00%
2016-04-02 20:03:24 +01:00
Lars K.W. GohlkeandJakob Borg 46e913dc23 gui: Improve header and footer layout on small screens 2016-04-02 10:31:16 +02:00
192 changed files with 10017 additions and 1354 deletions
+1
View File
@@ -1,5 +1,6 @@
syncthing
!gui/syncthing
!debian/syncthing
!Godeps/_workspace/src/github.com/syncthing
syncthing.exe
*.tar.gz
+214 -154
View File
@@ -26,6 +26,7 @@ import (
"runtime"
"strconv"
"strings"
"syscall"
"text/template"
"time"
)
@@ -40,6 +41,76 @@ var (
race bool
)
type target struct {
name string
buildPkg string
binaryName string
archiveFiles []archiveFile
debianFiles []archiveFile
}
type archiveFile struct {
src string
dst string
perm os.FileMode
}
var targets = map[string]target{
"all": {
// Only valid for the "build" and "install" commands as it lacks all
// the archive creation stuff.
buildPkg: "./cmd/...",
},
"syncthing": {
// The default target for "build", "install", "tar", "zip", "deb", etc.
name: "syncthing",
buildPkg: "./cmd/syncthing",
binaryName: "syncthing", // .exe will be added automatically for Windows builds
archiveFiles: []archiveFile{
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
{src: "README.md", dst: "README.txt", perm: 0644},
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
// All files from etc/ and extra/ added automatically in init().
},
debianFiles: []archiveFile{
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
{src: "README.md", dst: "deb/usr/share/doc/syncthing/README.txt", perm: 0644},
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing/LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing/AUTHORS.txt", perm: 0644},
{src: "man/syncthing.1", dst: "deb/usr/share/man/man1/syncthing.1", perm: 0644},
{src: "man/syncthing-config.5", dst: "deb/usr/share/man/man5/syncthing-config.5", perm: 0644},
{src: "man/syncthing-stignore.5", dst: "deb/usr/share/man/man5/syncthing-stignore.5", perm: 0644},
{src: "man/syncthing-device-ids.7", dst: "deb/usr/share/man/man7/syncthing-device-ids.7", perm: 0644},
{src: "man/syncthing-event-api.7", dst: "deb/usr/share/man/man7/syncthing-event-api.7", perm: 0644},
{src: "man/syncthing-faq.7", dst: "deb/usr/share/man/man7/syncthing-faq.7", perm: 0644},
{src: "man/syncthing-networking.7", dst: "deb/usr/share/man/man7/syncthing-networking.7", perm: 0644},
{src: "man/syncthing-rest-api.7", dst: "deb/usr/share/man/man7/syncthing-rest-api.7", perm: 0644},
{src: "man/syncthing-security.7", dst: "deb/usr/share/man/man7/syncthing-security.7", perm: 0644},
{src: "man/syncthing-versioning.7", dst: "deb/usr/share/man/man7/syncthing-versioning.7", perm: 0644},
{src: "etc/linux-systemd/system/syncthing@.service", dst: "deb/lib/systemd/system/syncthing@.service", perm: 0644},
{src: "etc/linux-systemd/system/syncthing-resume.service", dst: "deb/lib/systemd/system/syncthing-resume.service", perm: 0644},
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
},
},
}
func init() {
// The "syncthing" target includes a few more files found in the "etc"
// and "extra" dirs.
syncthingPkg := targets["syncthing"]
for _, file := range listFiles("etc") {
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
}
for _, file := range listFiles("extra") {
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
}
for _, file := range listFiles("extra") {
syncthingPkg.debianFiles = append(syncthingPkg.debianFiles, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0644})
}
targets["syncthing"] = syncthingPkg
}
const minGoVersion = 1.3
func main() {
@@ -81,82 +152,92 @@ func main() {
goVersion, _ = checkRequiredGoVersion()
// Invoking build.go with no parameters at all is equivalent to "go run
// build.go install all" as that builds everything (incrementally),
// which is what you want for maximum error checking during development.
if flag.NArg() == 0 {
var tags []string
if noupgrade {
tags = []string{"noupgrade"}
}
install("./cmd/...", tags)
install(targets["all"], tags)
vet("./cmd/syncthing")
vet("./lib/...")
lint("./cmd/syncthing")
vet("cmd", "lib")
lint("./cmd/...")
lint("./lib/...")
return
}
for _, cmd := range flag.Args() {
switch cmd {
case "setup":
setup()
// Otherwise, with any command given but not a target, the target is
// "syncthing". So "go run build.go install" is "go run build.go install
// syncthing" etc.
targetName := "syncthing"
if flag.NArg() > 1 {
targetName = flag.Arg(1)
}
target, ok := targets[targetName]
if !ok {
log.Fatalln("Unknown target", target)
}
case "install":
pkg := "./cmd/..."
var tags []string
if noupgrade {
tags = []string{"noupgrade"}
}
install(pkg, tags)
cmd := flag.Arg(0)
switch cmd {
case "setup":
setup()
case "build":
pkg := "./cmd/syncthing"
var tags []string
if noupgrade {
tags = []string{"noupgrade"}
}
build(pkg, tags)
case "test":
test("./lib/...", "./cmd/...")
case "bench":
bench("./lib/...", "./cmd/...")
case "assets":
rebuildAssets()
case "xdr":
xdr()
case "translate":
translate()
case "transifex":
transifex()
case "tar":
buildTar()
case "zip":
buildZip()
case "deb":
buildDeb()
case "clean":
clean()
case "vet":
vet("./cmd/syncthing")
vet("./lib/...")
case "lint":
lint("./cmd/syncthing")
lint("./lib/...")
default:
log.Fatalf("Unknown command %q", cmd)
case "install":
var tags []string
if noupgrade {
tags = []string{"noupgrade"}
}
install(target, tags)
case "build":
var tags []string
if noupgrade {
tags = []string{"noupgrade"}
}
build(target, tags)
case "test":
test("./lib/...", "./cmd/...")
case "bench":
bench("./lib/...", "./cmd/...")
case "assets":
rebuildAssets()
case "xdr":
xdr()
case "translate":
translate()
case "transifex":
transifex()
case "tar":
buildTar(target)
case "zip":
buildZip(target)
case "deb":
buildDeb(target)
case "clean":
clean()
case "vet":
vet("cmd", "lib")
case "lint":
lint("./cmd/...")
lint("./lib/...")
default:
log.Fatalf("Unknown command %q", cmd)
}
}
@@ -185,7 +266,6 @@ func checkRequiredGoVersion() (float64, bool) {
func setup() {
runPrint("go", "get", "-v", "golang.org/x/tools/cmd/cover")
runPrint("go", "get", "-v", "golang.org/x/tools/cmd/vet")
runPrint("go", "get", "-v", "golang.org/x/net/html")
runPrint("go", "get", "-v", "github.com/FiloSottile/gvt")
runPrint("go", "get", "-v", "github.com/axw/gocov/gocov")
@@ -215,7 +295,7 @@ func bench(pkgs ...string) {
runPrint("go", append([]string{"test", "-run", "NONE", "-bench", "."}, pkgs...)...)
}
func install(pkg string, tags []string) {
func install(target target, tags []string) {
lazyRebuildAssets()
cwd, err := os.Getwd()
@@ -230,22 +310,17 @@ func install(pkg string, tags []string) {
if race {
args = append(args, "-race")
}
args = append(args, pkg)
args = append(args, target.buildPkg)
os.Setenv("GOOS", goos)
os.Setenv("GOARCH", goarch)
runPrint("go", args...)
}
func build(pkg string, tags []string) {
func build(target target, tags []string) {
lazyRebuildAssets()
binary := "syncthing"
if goos == "windows" {
binary += ".exe"
}
rmr(binary)
rmr(target.binaryName)
args := []string{"build", "-i", "-v", "-ldflags", ldflags()}
if len(tags) > 0 {
args = append(args, "-tags", strings.Join(tags, ","))
@@ -253,68 +328,64 @@ func build(pkg string, tags []string) {
if race {
args = append(args, "-race")
}
args = append(args, pkg)
args = append(args, target.buildPkg)
os.Setenv("GOOS", goos)
os.Setenv("GOARCH", goarch)
runPrint("go", args...)
}
func buildTar() {
name := archiveName()
func buildTar(target target) {
name := archiveName(target)
filename := name + ".tar.gz"
var tags []string
if noupgrade {
tags = []string{"noupgrade"}
name += "-noupgrade"
}
build("./cmd/syncthing", tags)
filename := name + ".tar.gz"
files := []archiveFile{
{src: "README.md", dst: name + "/README.txt"},
{src: "LICENSE", dst: name + "/LICENSE.txt"},
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
{src: "syncthing", dst: name + "/syncthing"},
}
for _, file := range listFiles("etc") {
files = append(files, archiveFile{src: file, dst: name + "/" + file})
}
for _, file := range listFiles("extra") {
files = append(files, archiveFile{src: file, dst: name + "/" + filepath.Base(file)})
}
build(target, tags)
if goos == "darwin" {
macosCodesign("syncthing")
macosCodesign(target.binaryName)
}
tarGz(filename, files)
for i := range target.archiveFiles {
target.archiveFiles[i].src = strings.Replace(target.archiveFiles[i].src, "{{binary}}", target.binaryName, 1)
target.archiveFiles[i].dst = strings.Replace(target.archiveFiles[i].dst, "{{binary}}", target.binaryName, 1)
target.archiveFiles[i].dst = name + "/" + target.archiveFiles[i].dst
}
tarGz(filename, target.archiveFiles)
log.Println(filename)
}
func buildZip() {
name := archiveName()
func buildZip(target target) {
target.binaryName += ".exe"
name := archiveName(target)
filename := name + ".zip"
var tags []string
if noupgrade {
tags = []string{"noupgrade"}
name += "-noupgrade"
}
build("./cmd/syncthing", tags)
filename := name + ".zip"
files := []archiveFile{
{src: "README.md", dst: name + "/README.txt"},
{src: "LICENSE", dst: name + "/LICENSE.txt"},
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
{src: "syncthing.exe", dst: name + "/syncthing.exe"},
build(target, tags)
for i := range target.archiveFiles {
target.archiveFiles[i].src = strings.Replace(target.archiveFiles[i].src, "{{binary}}", target.binaryName, 1)
target.archiveFiles[i].dst = strings.Replace(target.archiveFiles[i].dst, "{{binary}}", target.binaryName, 1)
target.archiveFiles[i].dst = name + "/" + target.archiveFiles[i].dst
}
for _, file := range listFiles("extra") {
files = append(files, archiveFile{src: file, dst: name + "/" + filepath.Base(file)})
}
zipFile(filename, files)
zipFile(filename, target.archiveFiles)
log.Println(filename)
}
func buildDeb() {
func buildDeb(target target) {
os.RemoveAll("deb")
// "goarch" here is set to whatever the Debian packages expect. We correct
@@ -328,33 +399,14 @@ func buildDeb() {
goarch = "arm"
}
build("./cmd/syncthing", []string{"noupgrade"})
build(target, []string{"noupgrade"})
files := []archiveFile{
{src: "README.md", dst: "deb/usr/share/doc/syncthing/README.txt", perm: 0644},
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing/LICENSE.txt", perm: 0644},
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing/AUTHORS.txt", perm: 0644},
{src: "syncthing", dst: "deb/usr/bin/syncthing", perm: 0755},
{src: "man/syncthing.1", dst: "deb/usr/share/man/man1/syncthing.1", perm: 0644},
{src: "man/syncthing-config.5", dst: "deb/usr/share/man/man5/syncthing-config.5", perm: 0644},
{src: "man/syncthing-stignore.5", dst: "deb/usr/share/man/man5/syncthing-stignore.5", perm: 0644},
{src: "man/syncthing-device-ids.7", dst: "deb/usr/share/man/man7/syncthing-device-ids.7", perm: 0644},
{src: "man/syncthing-event-api.7", dst: "deb/usr/share/man/man7/syncthing-event-api.7", perm: 0644},
{src: "man/syncthing-faq.7", dst: "deb/usr/share/man/man7/syncthing-faq.7", perm: 0644},
{src: "man/syncthing-networking.7", dst: "deb/usr/share/man/man7/syncthing-networking.7", perm: 0644},
{src: "man/syncthing-rest-api.7", dst: "deb/usr/share/man/man7/syncthing-rest-api.7", perm: 0644},
{src: "man/syncthing-security.7", dst: "deb/usr/share/man/man7/syncthing-security.7", perm: 0644},
{src: "man/syncthing-versioning.7", dst: "deb/usr/share/man/man7/syncthing-versioning.7", perm: 0644},
{src: "etc/linux-systemd/system/syncthing@.service", dst: "deb/lib/systemd/system/syncthing@.service", perm: 0644},
{src: "etc/linux-systemd/system/syncthing-resume.service", dst: "deb/lib/systemd/system/syncthing-resume.service", perm: 0644},
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
for i := range target.debianFiles {
target.debianFiles[i].src = strings.Replace(target.debianFiles[i].src, "{{binary}}", target.binaryName, 1)
target.debianFiles[i].dst = strings.Replace(target.debianFiles[i].dst, "{{binary}}", target.binaryName, 1)
}
for _, file := range listFiles("extra") {
files = append(files, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0644})
}
for _, af := range files {
for _, af := range target.debianFiles {
if err := copyFile(af.src, af.dst, af.perm); err != nil {
log.Fatal(err)
}
@@ -363,11 +415,14 @@ func buildDeb() {
os.MkdirAll("deb/DEBIAN", 0755)
data := map[string]string{
"name": target.name,
"arch": debarch,
"version": version[1:],
"date": time.Now().Format(time.RFC1123),
}
for _, file := range listFiles("debian") {
debTemplateFiles := append(listFiles("debian/common"), listFiles("debian/"+target.name)...)
for _, file := range debTemplateFiles {
tpl, err := template.New(filepath.Base(file)).ParseFiles(file)
if err != nil {
log.Fatal(err)
@@ -634,8 +689,8 @@ func buildArch() string {
return fmt.Sprintf("%s-%s", os, goarch)
}
func archiveName() string {
return fmt.Sprintf("syncthing-%s-%s", buildArch(), version)
func archiveName(target target) string {
return fmt.Sprintf("%s-%s-%s", target.name, buildArch(), version)
}
func run(cmd string, args ...string) []byte {
@@ -681,12 +736,6 @@ func runPipe(file, cmd string, args ...string) {
fd.Close()
}
type archiveFile struct {
src string
dst string
perm os.FileMode
}
func tarGz(out string, files []archiveFile) {
fd, err := os.Create(out)
if err != nil {
@@ -802,24 +851,25 @@ func zipFile(out string, files []archiveFile) {
}
}
func vet(pkg string) {
bs, err := runError("go", "vet", pkg)
if err != nil && err.Error() == "exit status 3" || bytes.Contains(bs, []byte("no such tool \"vet\"")) {
// Go said there is no go vet
log.Println(`- No go vet, no vetting. Try "go get -u golang.org/x/tools/cmd/vet".`)
return
func vet(dirs ...string) {
params := []string{"tool", "vet", "-all"}
params = append(params, dirs...)
bs, err := runError("go", params...)
if len(bs) > 0 {
log.Printf("%s", bs)
}
falseAlarmComposites := regexp.MustCompile("composite literal uses unkeyed fields")
exitStatus := regexp.MustCompile("exit status 1")
for _, line := range bytes.Split(bs, []byte("\n")) {
if falseAlarmComposites.Match(line) || exitStatus.Match(line) {
continue
}
if len(line) > 0 {
log.Printf("%s", line)
if err != nil {
if exitStatus(err) == 3 {
// Exit code 3, the "vet" tool is not installed
return
}
// A genuine error exit from the vet tool.
log.Fatal(err)
}
}
func lint(pkg string) {
@@ -858,3 +908,13 @@ func macosCodesign(file string) {
log.Println("Codesign: successfully signed", file)
}
}
func exitStatus(err error) int {
if err, ok := err.(*exec.ExitError); ok {
if ws, ok := err.ProcessState.Sys().(syscall.WaitStatus); ok {
return ws.ExitStatus()
}
}
return -1
}
-1
View File
@@ -15,7 +15,6 @@ import (
"github.com/syndtr/goleveldb/leveldb"
)
// An IntHeap is a min-heap of ints.
type SizedElement struct {
key string
size int
+13 -9
View File
@@ -32,6 +32,7 @@ import (
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/logger"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay"
@@ -85,7 +86,7 @@ type modelIntf interface {
CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool)
CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool)
ResetFolder(folder string)
Availability(folder, file string) []protocol.DeviceID
Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []model.Availability
GetIgnores(folder string) ([]string, []string, error)
SetIgnores(folder string, content []string) error
PauseDevice(device protocol.DeviceID)
@@ -199,7 +200,10 @@ func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener,
return nil, err
}
listener := &tlsutil.DowngradingListener{rawListener, tlsCfg}
listener := &tlsutil.DowngradingListener{
Listener: rawListener,
TLSConfig: tlsCfg,
}
return listener, nil
}
@@ -301,12 +305,9 @@ func (s *apiService) Serve() {
guiCfg := s.cfg.GUI()
// Add the CORS handling
handler := corsMiddleware(mux)
// Wrap everything in CSRF protection. The /rest prefix should be
// protected, other requests will grant cookies.
handler = csrfMiddleware(s.id.String()[:5], "/rest", guiCfg, handler)
handler := csrfMiddleware(s.id.String()[:5], "/rest", guiCfg, mux)
// Add our version and ID as a header to responses
handler = withDetailsMiddleware(s.id, handler)
@@ -321,6 +322,9 @@ func (s *apiService) Serve() {
handler = redirectToHTTPSMiddleware(handler)
}
// Add the CORS handling
handler = corsMiddleware(handler)
handler = debugMiddleware(handler)
srv := http.Server{
@@ -449,7 +453,7 @@ func corsMiddleware(next http.Handler) http.Handler {
// when the browser initiate a POST request.
//
// As the OPTIONS request is unauthorized, this handler must be the first
// of the chain.
// of the chain (hence added at the end).
//
// See https://www.w3.org/TR/cors/ for details.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -693,7 +697,7 @@ func (s *apiService) getDBFile(w http.ResponseWriter, r *http.Request) {
return
}
av := s.model.Availability(folder, file)
av := s.model.Availability(folder, file, protocol.Vector{}, protocol.BlockInfo{})
sendJSON(w, map[string]interface{}{
"global": jsonFileInfo(gf),
"local": jsonFileInfo(lf),
@@ -934,7 +938,7 @@ func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string][]string{
"ignore": ignores,
"patterns": patterns,
"expanded": patterns,
})
}
+4
View File
@@ -42,6 +42,10 @@ func trackCPUUsage() {
curTime := time.Now().UnixNano()
timeDiff := curTime - prevTime
// This is sometimes 0, no clue why.
if timeDiff == 0 {
continue
}
curUsage := ktime.Nanoseconds() + utime.Nanoseconds()
usageDiff := curUsage - prevUsage
cpuUsageLock.Lock()
+28 -22
View File
@@ -38,15 +38,19 @@ import (
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/logger"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/symlinks"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/upnp"
"github.com/syncthing/syncthing/lib/util"
// Registers NAT service providers
_ "github.com/syncthing/syncthing/lib/pmp"
_ "github.com/syncthing/syncthing/lib/upnp"
"github.com/thejerf/suture"
)
@@ -117,7 +121,6 @@ func init() {
var (
myID protocol.DeviceID
stop = make(chan int)
cert tls.Certificate
lans []*net.IPNet
)
@@ -558,10 +561,6 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
}
}
// We reinitialize the predictable RNG with our device ID, to get a
// sequence that is always the same but unique to this syncthing instance.
util.PredictableRandom.Seed(util.SeedFromBytes(cert.Certificate[0]))
myID = protocol.NewDeviceID(cert.Certificate[0])
l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5]))
@@ -710,23 +709,30 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
mainService.Add(m)
// The default port we announce, possibly modified by setupUPnP next.
// Start NAT service
var natService *nat.Service
var mappings []*nat.Mapping
if opts.NATEnabled {
natService = nat.NewService(myID, cfg)
for _, addrStr := range opts.ListenAddress {
uri, err := url.Parse(addrStr)
if err != nil {
l.Fatalf("Failed to parse listen address %s: %v", addrStr, err)
}
uri, err := url.Parse(opts.ListenAddress[0])
if err != nil {
l.Fatalf("Failed to parse listen address %s: %v", opts.ListenAddress[0], err)
}
if uri.Scheme == "tcp" || uri.Scheme == "tcp4" {
addr, err := net.ResolveTCPAddr(uri.Scheme, uri.Host)
if err != nil {
l.Fatalln("Bad listen address:", err)
}
if addr.Port == 0 {
l.Fatalf("Listen address %s: invalid port", uri)
}
addr, err := net.ResolveTCPAddr("tcp", uri.Host)
if err != nil {
l.Fatalln("Bad listen address:", err)
}
// Start UPnP
var upnpService *upnp.Service
if opts.UPnPEnabled {
upnpService = upnp.NewUPnPService(cfg, addr.Port)
mainService.Add(upnpService)
mappings = append(mappings, natService.NewMapping(nat.TCP, addr.IP, addr.Port))
}
}
mainService.Add(natService)
}
// Start relay management
@@ -744,7 +750,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// Start connection management
connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, upnpService, relayService, bepProtocolName, tlsDefaultCommonName, lans)
connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, mappings, relayService, bepProtocolName, tlsDefaultCommonName, lans)
mainService.Add(connectionService)
if cfg.Options().GlobalAnnEnabled {
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"time"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/stats"
)
@@ -57,7 +58,7 @@ func (m *mockedModel) CurrentGlobalFile(folder string, file string) (protocol.Fi
func (m *mockedModel) ResetFolder(folder string) {
}
func (m *mockedModel) Availability(folder, file string) []protocol.DeviceID {
func (m *mockedModel) Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []model.Availability {
return nil
}
+6 -4
View File
@@ -147,11 +147,13 @@ func (s *verboseService) formatEvent(ev events.Event) string {
data := ev.Data.(map[string]string)
device := data["device"]
return fmt.Sprintf("Device %v was resumed", device)
case events.ExternalPortMappingChanged:
data := ev.Data.(map[string]int)
port := data["port"]
return fmt.Sprintf("External port mapping changed; new port is %d.", port)
data := ev.Data.(map[string]interface{})
protocol := data["protocol"]
local := data["local"]
added := data["added"]
removed := data["removed"]
return fmt.Sprintf("External port mapping changed; protocol: %s, local: %s, added: %s, removed: %s", protocol, local, added, removed)
case events.RelayStateChanged:
data := ev.Data.(map[string][]string)
newRelays := data["new"]
+1 -1
View File
@@ -1,4 +1,4 @@
syncthing ({{.version}}); urgency=medium
{{.name}} ({{.version}}); urgency=medium
* Packaging of {{.version}}.
View File
View File
View File
+12
View File
@@ -0,0 +1,12 @@
.dev-top-bar{
display: none;
background-color: yellow;
}
.dev-error .hasCount{
background-color: red;
}
.dev-warn .hasCount{
background-color: yellow;
}
+57 -53
View File
@@ -163,64 +163,14 @@ table.table-condensed td.no-overflow-ellipse {
display: none;
}
@media (max-width:767px) {
table.table-condensed td {
/* for mobile phones to allow linebreaks in long repro folder/shared with
* columns. */
white-space: normal;
}
.nav>li{
float: left;
}
.navbar-right {
/* to align with panel */
padding-right: 15px;
}
/**
* Menu for select language
*/
@media (min-width:480px) and (max-width:649px) {
*[language-select] > .dropdown-menu {
width: 230px;
}
}
@media (min-width:650px) {
*[language-select] > .dropdown-menu > li {
width: 50%;
float: left;
}
*[language-select] > .dropdown-menu {
width: 440px;
}
}
@media (max-width:479px) {
.dropdown-menu {
padding-top: 55px;
}
nav .dropdown-toggle {
font-size: 14px;
}
.dropdown-toggle {
float: left;
}
.navbar-brand {
padding-left: 0;
padding-top: 16px;
}
.navbar-nav .open .dropdown-menu > li > a {
padding: 12px 15px 12px 25px;
}
}
.panel-body .table-condensed {
margin-bottom: 0;
}
@@ -267,7 +217,6 @@ ul.three-columns li, ul.two-columns li {
}
/** Footer nav on small devices **/
@media (max-width: 1199px) {
/* Stay at the end of the page, with space reserved for the footer
usually taking up two rows. */
@@ -297,4 +246,59 @@ ul.three-columns li, ul.two-columns li {
.navbar-fixed-bottom {
position: relative;
}
.nav>li {
float:right;
}
table.table-condensed td {
/* for mobile phones to allow linebreaks in long repro folder/shared with
* columns. */
white-space: normal;
}
}
@media (min-width:650px) {
*[language-select] > .dropdown-menu > li {
width: 50%;
float: left;
}
*[language-select] > .dropdown-menu {
width: 440px;
}
}
/**
* Menu for select language
*/
@media (min-width:480px) and (max-width:649px) {
*[language-select] > .dropdown-menu {
width: 230px;
}
}
@media (max-width:479px) {
.dropdown-menu {
padding-top: 55px;
}
nav .dropdown-toggle {
font-size: 1em;
}
.dropdown-toggle {
float: left;
}
.logo{
margin:auto;
}
.navbar-nav .open .dropdown-menu > li > a {
padding: 12px 15px 12px 25px;
}
.navbar-fixed-bottom li{
width:100%;
}
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "пълна документация",
"items": "елемента",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} желае да сподели папка \"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "documentació sencera",
"items": "Elements",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartir la carpeta \"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
@@ -238,5 +238,6 @@
"full documentation": "Documentació completa",
"items": "Elements",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartit la carpeta \"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+12 -11
View File
@@ -8,13 +8,13 @@
"Add": "Přidat",
"Add Device": "Přidat přístroj",
"Add Folder": "Přidat adresář",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Přidat vzdálené zařízení",
"Add new folder?": "Přidat nový adresář?",
"Address": "Adresa",
"Addresses": "Adresy",
"Advanced": "Pokročilé",
"Advanced Configuration": "Pokročilá nastavení",
"Advanced settings": "Advanced settings",
"Advanced settings": "Pokročilá nastavení",
"All Data": "Všechna data",
"Allow Anonymous Usage Reporting?": "Povolit anonymní hlášení o používání?",
"Alphabetic": "Abecedně",
@@ -34,12 +34,12 @@
"Connection Error": "Chyba připojení",
"Copied from elsewhere": "Zkopírováno odjinud",
"Copied from original": "Zkopírováno z originálu",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následující přispěvatelé:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 následující přispěvatelé:",
"Danger!": "Pozor!",
"Delete": "Smazat",
"Deleted": "Smazáno",
"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?": "Zařízení \"{{name}}\" ({{device}} na {{address}}) se chce připojit. Přidat nové zařízení?",
"Device ID": "ID přístroje",
"Device Identification": "Identifikace přístroje",
"Device Name": "Jméno přístroje",
@@ -70,7 +70,7 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Soubory jsou chráněny před změnami na ostatních přístrojích, ale změny provedené z tohoto přístroje budou rozeslány na zbytek clusteru.",
"Folder": "Adresář",
"Folder ID": "ID adresáře",
"Folder Label": "Folder Label",
"Folder Label": "Jmenovka adresáře",
"Folder Master": "Master adresář",
"Folder Path": "Cesta k adresáři",
"Folders": "Adresáře",
@@ -116,7 +116,7 @@
"OK": "OK",
"Off": "Vypnuta",
"Oldest First": "Od nejstaršího",
"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.": "Toto zařízení",
"Options": "Nastavení",
"Out of Sync": "Nesesynchronizováno",
"Out of Sync Items": "Nesesynchronizované položky",
@@ -138,9 +138,9 @@
"Relayed via": "Přenášené přes",
"Relays": "Přenašeče",
"Release Notes": "Poznámky k vydání",
"Remote Devices": "Remote Devices",
"Remote Devices": "Vzdálená zařízení",
"Remove": "Odstranit",
"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.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Požadovaný identifikátor adresáře. Musí být stejný na všech zařízeních.",
"Rescan": "Opakovat skenování",
"Rescan All": "Opakovat skenování všech",
"Rescan Interval": "Interval opakování skenování",
@@ -211,7 +211,7 @@
"The rate limit must be a non-negative number (0: no limit)": "Limit rychlosti musí být nezáporné číslo (0: bez limitu)",
"The rescan interval must be a non-negative number of seconds.": "Interval opakování skenování musí být pozitivní číslo.",
"They are retried automatically and will be synced when the error is resolved.": "Nové pokusy o synchronizaci budou probíhat automaticky a položky budou synchronizovány jakmile bude chyba odstraněna.",
"This Device": "This Device",
"This Device": "Toto zařízení",
"This can easily give hackers access to read and change any files on your computer.": "To může útočníkům jednoduše povolit čtení a úpravy souborů na vašem přístroji. ",
"This is a major version upgrade.": "Toto je důležitá aktualizace.",
"Trash Can File Versioning": "Verzování souborů v koši",
@@ -229,7 +229,7 @@
"Version": "Verze",
"Versions Path": "Cesta k verzím",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Verze jsou automaticky smazány, pokud jsou starší než maximální časový limit nebo překročí počet souborů povolených pro interval.",
"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%}\".": "Varování: tato cesta je podadresářem existujícího adresáře \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Při přidávání nového přístroje mějte na paměti, že je ho třeba také zadat na druhé straně.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Při přidávání nového adresáře mějte na paměti, že jeho ID je použito ke svázání adresářů napříč přístoji. Rozlišují se malá a velká písmena a musí přesně souhlasit mezi všemi přístroji.",
"Yes": "Ano",
@@ -238,5 +238,6 @@
"full documentation": "plná dokumentace",
"items": "položky",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce sdílet adresář \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} chce sdílet adresář \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "Fuld dokumentation",
"items": "poster",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker at dele mappen \"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+3 -2
View File
@@ -39,7 +39,7 @@
"Danger!": "Achtung!",
"Delete": "Löschen",
"Deleted": "Gelöscht",
"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?": "Gerät \"{{name}}\" ({{device}} {{address}}) möchte sich verbinden. Gerät hinzufügen?",
"Device ID": "Geräte ID",
"Device Identification": "Geräte Identifikation",
"Device Name": "Gerätename",
@@ -238,5 +238,6 @@
"full documentation": "Komplette Dokumentation",
"items": "Objekte",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} möchte das Verzeichnis \"{{folder}}\" teilen.",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "πλήρης τεκμηρίωση",
"items": "εγγραφές",
"{%device%} wants to share folder \"{%folder%}\".": "Η συσκευή {{device}} θέλει να μοιράσει τον φάκελο «{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "full documentation",
"items": "items",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+3 -1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Editing",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable UPnP": "Enable UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
@@ -238,5 +239,6 @@
"full documentation": "full documentation",
"items": "items",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "Documentación completa",
"items": "Elementos",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "documentación completa",
"items": "ítems",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir repositorio \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} qiuere compartir el repositorio \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} qiuere compartir el repositorio \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "täysi dokumentaatio",
"items": "kohteet",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} haluaa jakaa kansion \"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"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 \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,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}} veut partager le dossier \"{{folderLabel}}\" ({{folder}})."
}
+15 -14
View File
@@ -8,13 +8,13 @@
"Add": "Taheakje",
"Add Device": "Apparaat taheakje",
"Add Folder": "Map taheakje",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Apparaat op Ofstân Taheakje",
"Add new folder?": "Nije map taheakje?",
"Address": "Adres",
"Addresses": "Adressen",
"Advanced": "Avansearre",
"Advanced Configuration": "Avansearre konfiguraasje",
"Advanced settings": "Advanced settings",
"Advanced settings": "Avansearre ynstellings",
"All Data": "Alle data",
"Allow Anonymous Usage Reporting?": "Anonime brûkensrapportaazje tastean?",
"Alphabetic": "Alfabetysk",
@@ -34,12 +34,12 @@
"Connection Error": "Ferbiningsflater",
"Copied from elsewhere": "Oernommen fan earne oars",
"Copied from original": "Oernommen fan orizjineel",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 de folgende bydragers:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 de folgende bydragers:",
"Danger!": "Gefaar!",
"Delete": "Fuortsmite",
"Deleted": "Fuortsmiten",
"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?": "Apparaat \"{{name}}\" {{device}} op ({{address}}) wol ferbining meitsje. Nij apparaat taheakje?",
"Device ID": "Apparaat-ID",
"Device Identification": "Apparaatidentifikaasje",
"Device Name": "Apparaatnamme",
@@ -67,10 +67,10 @@
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Bits foar triemrjochten wurde negearre yn it sykjen foar feroarings. Brûk dit op FAT-triemsystemen.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Triemen wurde ferset nei map .stversions wannear't troch Syncthing ferfangen of fuortsmiten.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Triemen wurde ferset nei in mei datum stimpele ferzjes yn in .stversions map wannear troch Syncthing ferfangen of fuortsmiten.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Triemen binne ymmún foar feroarings makke troch oare apparaten, mar feroarings makke op dit apparaat wurde nei de rest ferstjoerd.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Triemen binne ymmún foar feroarings makke troch oare apparaten, mar feroarings makke op dit apparaat wurde nei de rest fan 'e bondel ferstjoerd.",
"Folder": "Map",
"Folder ID": "Map-ID",
"Folder Label": "Folder Label",
"Folder Label": "Map-opskrift",
"Folder Master": "Map-master",
"Folder Path": "Map-paad",
"Folders": "Mappen",
@@ -116,7 +116,7 @@
"OK": "Okee",
"Off": "Ut",
"Oldest First": "Aldste earst",
"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.": "Opsjoneel beskriuwend opskrift foar de map. Mei op ider apparaat oars wêze.",
"Options": "Opsjes",
"Out of Sync": "Net syngronisearre",
"Out of Sync Items": "Net syngronisearre items",
@@ -138,9 +138,9 @@
"Relayed via": "Trochjûn fia",
"Relays": "Trochjouers",
"Release Notes": "Utjeftenotysjes",
"Remote Devices": "Remote Devices",
"Remote Devices": "Apparaten op Ofstân",
"Remove": "Fuortsmite",
"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.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Ferplicht ID foar de map. Moat op alle bondelapparaten itselde wêze.",
"Rescan": "Sken opnij",
"Rescan All": "Sken alles opnij",
"Rescan Interval": "Wersken ynterval",
@@ -161,11 +161,11 @@
"Share With Devices": "Diele mei apparaten",
"Share this folder?": "Dizze map diele?",
"Shared With": "Dielt mei",
"Short identifier for the folder. Must be the same on all cluster devices.": "Koart opskrift foar de map. Moat op alle apparaten itselde wêze.",
"Short identifier for the folder. Must be the same on all cluster devices.": "Koarte ID foar de map. Moat op alle bondelapparaten itselde wêze.",
"Show ID": "ID sjen litte",
"Show QR": "QR sjen litte",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wurd ynstee fan apparaat-ID sjen litten by de bondeltastân. Wurd nei oare apparaten advertearre as in mooglike standertnamme.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd yn de bondel-tastân sjen litten ynstee fan apparaat-ID. Wannear't leech litten wurd, wurd it fernijt nei de namme die it apparaat útstjoert.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd yn de bondeltastân sjen litten ynstee fan apparaat-ID. Wannear't leech litten wurd, wurd it fernijt nei de namme die it apparaat útstjoert.",
"Shutdown": "Ofslute",
"Shutdown Complete": "Ofsluten klear",
"Simple File Versioning": "Ienfâldich triemferzjebehear",
@@ -211,7 +211,7 @@
"The rate limit must be a non-negative number (0: no limit)": "It fluggenslimyt moat in posityf nûmer wêze (0: gjin limyt)",
"The rescan interval must be a non-negative number of seconds.": "It wersken-ynterfal moat in posityf tal fan sekonden wêze.",
"They are retried automatically and will be synced when the error is resolved.": "Sy wurde automatysk opnij probearre en sille syngronisearre wurde wannear at de flater oplost is.",
"This Device": "This Device",
"This Device": "Dit Apparaat",
"This can easily give hackers access to read and change any files on your computer.": "Dit kin samar ynkringers (hackers) tagong jaan om elke triem op jo kompjûter te besjen en te feroarjen.",
"This is a major version upgrade.": "Dit is in wichtige ferzjefernijing.",
"Trash Can File Versioning": "Jiskefet-triemferzjebehear",
@@ -229,7 +229,7 @@
"Version": "Ferzje",
"Versions Path": "Ferzjes-paad",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Ferzjes wurde automatysk fuortsmiten wannear't se âlder binne dan de maksimale âldens of wannear it tal fan triemen yn in ynterval grutter is dan tastean.",
"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%}\".": "Warskôging, dit paad is in ûnderlizzende triemtafel fan in besteande map \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Hâld by it taheakjen fan in nij apparaat yn de holle dat it apparaat oan de oare kant ek taheakke wurde moat. ",
"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.": "Hâld by it taheakjen fan in nije map yn de holle dat de map-ID brûkt wurd om de mappen tusken apparaten mei-inoar te ferbinen. Se binne haadlettergefoelich en moatte oer alle apparaten eksakt oerienkomme.",
"Yes": "Ja",
@@ -238,5 +238,6 @@
"full documentation": "komplete dokumintaasje",
"items": "items",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wol map \"{{folder}}\" diele.",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wol de map \"{{folderLabel}}\" ({{folder}}) diele.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wol map \"{{folderlabel}}\" ({{folder}}) diele."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "teljes dokumentáció",
"items": "elem",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} meg szeretné osztani a \"{{folder}}\" nevű mappát.",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} meg szeretné osztani \"{{folderLabel}}\" ({{folder}}) mappát."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} meg szeretné osztani \"{{folderLabel}}\" ({{folder}}) mappát.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} szeretné megosztani \"{{folderlabel}}\" ({{folder}}) mappát."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "full documentation",
"items": "items",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+7 -6
View File
@@ -39,7 +39,7 @@
"Danger!": "Pericolo!",
"Delete": "Elimina",
"Deleted": "Cancellato",
"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?": "Il dispositivo \"{{name}}\" ({{device}} - {{address}}) chiede di connettersi. Aggiungere il nuovo dispositivo?",
"Device ID": "ID Dispositivo",
"Device Identification": "Identificazione Dispositivo",
"Device Name": "Nome Dispositivo",
@@ -70,7 +70,7 @@
"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": "Folder Label",
"Folder Label": "Etichetta per la cartella",
"Folder Master": "Cartella Principale",
"Folder Path": "Percorso Cartella",
"Folders": "Cartelle",
@@ -116,7 +116,7 @@
"OK": "OK",
"Off": "Disattiva",
"Oldest First": "Prima il meno recente",
"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.": "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",
@@ -140,7 +140,7 @@
"Release Notes": "Note di rilascio",
"Remote Devices": "Dispositivi Remoti",
"Remove": "Rimuovi",
"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.",
"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.",
"Rescan": "Riscansiona",
"Rescan All": "Riscansiona Tutto",
"Rescan Interval": "Intervallo Scansione",
@@ -229,7 +229,7 @@
"Version": "Versione",
"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%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"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 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ì",
@@ -238,5 +238,6 @@
"full documentation": "documentazione completa",
"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}})."
}
+11 -10
View File
@@ -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": "アルファベット順",
@@ -39,7 +39,7 @@
"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}} ({{address}} の {{device}}) が接続を求めています。新しいデバイスとして追加しますか?",
"Device ID": "デバイスID",
"Device Identification": "デバイス識別情報",
"Device Name": "デバイス名",
@@ -70,7 +70,7 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "ファイルを他のデバイスによる変更から保護します。一方、このデバイスでの変更は他のデバイスに送信されます。",
"Folder": "フォルダー",
"Folder ID": "フォルダーID",
"Folder Label": "Folder Label",
"Folder Label": "フォルダー名",
"Folder Master": "フォルダーのマスター",
"Folder Path": "フォルダーパス",
"Folders": "フォルダー",
@@ -116,7 +116,7 @@
"OK": "OK",
"Off": "オフ",
"Oldest First": "古い順",
"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.": "分かりやすいフォルダーの名前で、設定は任意です。デバイスごとに異なってもかまいません。",
"Options": "オプション",
"Out of Sync": "未同期",
"Out of Sync Items": "同期の必要な項目",
@@ -138,9 +138,9 @@
"Relayed via": "経由している中継サーバーのアドレス",
"Relays": "中継サーバー",
"Release Notes": "リリースノート",
"Remote Devices": "Remote Devices",
"Remote Devices": "他のデバイス",
"Remove": "除去",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Required identifier for the folder. Must be the same on all cluster devices.": "フォルダーの識別子で、必須です。このフォルダーを共有する全てのデバイス上で同一でなくてはなりません。",
"Rescan": "再スキャン",
"Rescan All": "すべて再スキャン",
"Rescan Interval": "再スキャン間隔",
@@ -211,7 +211,7 @@
"The rate limit must be a non-negative number (0: no limit)": "帯域制限の値として負の数は指定できません (0を指定すると無制限になります)。",
"The rescan interval must be a non-negative number of seconds.": "再スキャン間隔として負の数は指定できません。",
"They are retried automatically and will be synced when the error is resolved.": "エラーが解決すると、自動的に再試行され同期されます。",
"This Device": "This Device",
"This Device": "このデバイス",
"This can easily give hackers access to read and change any files on your computer.": "この設定のままでは、あなたのコンピューターにある任意のファイルを、他者が簡単に盗み見たり書き換えたりすることができます。",
"This is a major version upgrade.": "メジャーアップグレードです。",
"Trash Can File Versioning": "ゴミ箱によるバージョン管理",
@@ -229,7 +229,7 @@
"Version": "バージョン",
"Versions Path": "古いバージョンを保存するパス",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "古いバージョンは、最大寿命もしくは期間ごとの最大保存数を超えた場合、自動的に削除されます。",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "警告: 入力されたパスは、設定済みのフォルダー「{{otherFolder}}」のサブディレクトリです。",
"When adding a new device, keep in mind that this device must be added on the other side too.": "新しいデバイスを追加する際は、相手側のデバイスにもこのデバイスを追加する必要があることに留意してください。",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "新しいフォルダーを追加する際、フォルダーIDはデバイス間でフォルダーの対応づけに使われることに注意してください。フォルダーIDは大文字と小文字が区別され、共有するすべてのデバイスの間で完全に一致しなくてはなりません。",
"Yes": "はい",
@@ -238,5 +238,6 @@
"full documentation": "詳細なマニュアル",
"items": "項目",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} がフォルダー \"{{folder}}\" を共有するよう求めています。",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} がフォルダー「{{folderLabel}} ({{folder}}) を共有するよう求めています。",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} がフォルダー「{{folderlabel}}」 ({{folder}}) を共有するよう求めています。"
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "전체 문서",
"items": "항목",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 에서 폴더 \\\"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+17 -16
View File
@@ -8,13 +8,13 @@
"Add": "Pridėti",
"Add Device": "Pridėti įrenginį",
"Add Folder": "Pridėti aplanką",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Pridėti nuotolinį įrenginį",
"Add new folder?": "Pridėti naują aplanką?",
"Address": "Adresas",
"Addresses": "Adresai",
"Advanced": "Pažangus",
"Advanced": "Išplėstiniai",
"Advanced Configuration": "Išplėstinė konfigūracija",
"Advanced settings": "Advanced settings",
"Advanced settings": "Išplėstiniai nustatymai",
"All Data": "Visiems duomenims",
"Allow Anonymous Usage Reporting?": "Siųsti anonimišką vartojimo ataskaitą?",
"Alphabetic": "Abėcėlės tvarka",
@@ -26,20 +26,20 @@
"Bugs": "Klaidos",
"CPU Utilization": "Procesoriaus panaudojimas",
"Changelog": "Pasikeitimai",
"Clean out after": "Išvalyto po",
"Clean out after": "Išvalyti po",
"Close": "Uždaryti",
"Command": "Komanda",
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
"Compression": "Kompresija",
"Connection Error": "Susijungimo klaida",
"Copied from elsewhere": "Nukopijuota iš betkur",
"Copied from elsewhere": "Nukopijuota iš kitur",
"Copied from original": "Nukopijuota iš originalo",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Autorių teisės © 2014-2016 šių bendraautor:",
"Copyright © 2015 the following Contributors:": "Visos teisės saugomos © 2015 šių bendraautorių:",
"Danger!": "Pavojus!",
"Delete": "Ištrinti",
"Deleted": "Ištrinta",
"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?": "Įrenginys \"{{name}}\" ({{device}} {{address}}) nori prisijungti. Pridėti naują įrenginį?",
"Device ID": "Įrenginio ID",
"Device Identification": "Įrenginio identifikacija",
"Device Name": "Įrenginio pavadinimas",
@@ -67,12 +67,12 @@
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Ieškant pakeitimų, į failų leidimų bitus yra nekreipiama dėmesio. Naudoti FAT failų sistemose.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Failai perkeliami į .stversions aplanką kai tampa pakeisti arba ištrinti.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Programai Syncthing pakeičiant ar ištrinant failus, jie yra perkeliami į datomis pažymėtas versijas, aplanke .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.": "Failai apsaugoti nuo pakeitimų atliktų kituose įrenginiuose, bet pakeitimai šiame įrenginyje bus nusiųsti kitiems.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Failai yra apsaugoti nuo kituose įrenginiuose atliktų pakeitimų, bet pakeitimai šiame įrenginyje bus nusiųsti kitiems įrenginiams.",
"Folder": "Aplankas",
"Folder ID": "Aplanko ID",
"Folder Label": "Folder Label",
"Folder Label": "Aplanko etiketė",
"Folder Master": "Aplanko vadovas",
"Folder Path": "Kelias iki apkanko",
"Folder Path": "Kelias iki aplanko",
"Folders": "Aplankai",
"GUI": "Valdymo skydelis",
"GUI Authentication Password": "Valdymo skydelio slaptažodis",
@@ -116,7 +116,7 @@
"OK": "Gerai",
"Off": "Netaikoma",
"Oldest First": "Seniausi pirmiau",
"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.": "Nebūtina aprašomoji aplanko etiketė. Kiekviename įrenginyje gali būti skirtinga.",
"Options": "Parametrai",
"Out of Sync": "Išsisinchronizavę",
"Out of Sync Items": "Nesutikrinta",
@@ -138,9 +138,9 @@
"Relayed via": "Retransliuojama per",
"Relays": "Retransliatoriai",
"Release Notes": "Laidos Informacija",
"Remote Devices": "Remote Devices",
"Remote Devices": "Nuotoliniai įrenginiai",
"Remove": "Pašalinti",
"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.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Reikalaujamas aplanko identifikatorius. Privalo būti toks pats visuose įrenginiuose.",
"Rescan": "Nuskaityti iš naujo",
"Rescan All": "Nuskaityti visus aplankus",
"Rescan Interval": "Pertrauka tarp nuskaitymų",
@@ -210,7 +210,7 @@
"The path cannot be blank.": "Kelias negali būti tuščias.",
"The rate limit must be a non-negative number (0: no limit)": "Srauto maksimalus greitis privalo būti ne neigiamas skaičius (0: nėra apribojimo)",
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
"They are retried automatically and will be synced when the error is resolved.": "Failus bus automatiškai badoma parsiųsti dar kartą kai išspręsite klaidas",
"They are retried automatically and will be synced when the error is resolved.": "Failus bus automatiškai bandoma parsiųsti dar kartą kai išspręsite klaidas",
"This Device": "This Device",
"This can easily give hackers access to read and change any files on your computer.": "Tai gali suteikti programišiams lengvą prieigą skaityti ir keisti bet kokius failus jūsų kompiuteryje.",
"This is a major version upgrade.": "Tai yra stambus atnaujinimas.",
@@ -229,7 +229,7 @@
"Version": "Versija",
"Versions Path": "Kelias iki versijos",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versijos ištrinamos jei senesnės už nustatyta maksimalų amžių arba jei viršytas maksimalus failų skaičius per nustatytą laiko tarpą.",
"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%}\".": "Įspėjimas, šis kelias yra esamo aplanko \"{{otherFolder}}\" pakatalogis.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Pridėdami įrenginį, turėkite omeny, kad šis įrenginys taip pat turi būti pridėtas kitoje pusėje.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Kai įvedate naują aplanką neužmirškite, kad jis bus naudojamas visuose įrenginiuose. Svarbu visur įvesti visiškai tokį pat aplanko vardą neužmirštant apie didžiąsias ir mažąsias raides.",
"Yes": "Taip",
@@ -238,5 +238,6 @@
"full documentation": "pilna dokumentacija",
"items": "įrašai",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} nori dalintis aplanku \"{{folder}}\"",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} nori dalintis aplanku \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} nori dalintis aplanku \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "all dokumentasjon",
"items": "elementer",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} 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}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "volledige documentatie",
"items": "objecten",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wil de map \"{{folder}}\" delen.",
"{%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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "pełna dokumentacja",
"items": "pozycji",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "documentação completa",
"items": "itens",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer compartilhar a pasta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} deseja compartilhar a pasta \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} deseja compartilhar a pasta \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quer compartilhar a pasta \"{{folderlabel}}\" ({{folder}})."
}
+13 -12
View File
@@ -8,13 +8,13 @@
"Add": "Adicionar",
"Add Device": "Adicionar dispositivo",
"Add Folder": "Adicionar pasta",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Adicionar dispositivo remoto",
"Add new folder?": "Adicionar nova pasta?",
"Address": "Endereço",
"Addresses": "Endereços",
"Advanced": "Avançadas",
"Advanced Configuration": "Configuração avançada",
"Advanced settings": "Advanced settings",
"Advanced settings": "Configurações avançadas",
"All Data": "Todos os dados",
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
"Alphabetic": "Alfabética",
@@ -34,12 +34,12 @@
"Connection Error": "Erro de ligação",
"Copied from elsewhere": "Copiado doutro sítio",
"Copied from original": "Copiado do original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 os seguintes contribuidores:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 os seguintes contribuidores:",
"Danger!": "Perigo!",
"Delete": "Eliminar",
"Deleted": "Eliminado",
"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?": "O dispositivo \"{{name}}\" ({{device}} em {{address}}) quer conectar-se. Adiciono este novo dispositivo?",
"Device ID": "ID do dispositivo",
"Device Identification": "Identificação do dispositivo",
"Device Name": "Nome do dispositivo",
@@ -70,7 +70,7 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Os ficheiros estão protegidos contra alterações feitas noutros dispositivos, mas alterações feitas neste dispositivo serão enviadas ao resto do grupo.",
"Folder": "Pasta",
"Folder ID": "ID da pasta",
"Folder Label": "Folder Label",
"Folder Label": "Etiqueta da pasta",
"Folder Master": "Pasta mestre",
"Folder Path": "Caminho da pasta",
"Folders": "Pastas",
@@ -116,7 +116,7 @@
"OK": "OK",
"Off": "Desligada",
"Oldest First": "Primeiro os mais antigos",
"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.": "Etiqueta descritiva opcional para a pasta. Pode ser diferente em cada dispositivo.",
"Options": "Opções",
"Out of Sync": "Fora de sincronia",
"Out of Sync Items": "Itens por sincronizar",
@@ -138,9 +138,9 @@
"Relayed via": "Retransmitido via",
"Relays": "Retransmissores",
"Release Notes": "Notas de lançamento",
"Remote Devices": "Remote Devices",
"Remote Devices": "Dispositivos remotos",
"Remove": "Remover",
"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.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificador obrigatório para a pasta. Tem que ser igual em todos os dispositivos do grupo.",
"Rescan": "Verificar agora",
"Rescan All": "Verificar todas agora",
"Rescan Interval": "Intervalo entre verificações",
@@ -150,7 +150,7 @@
"Resume": "Retomar",
"Reused": "Reutilizado",
"Save": "Gravar",
"Scan Time Remaining": "Tempo restante de rastreio",
"Scan Time Remaining": "Tempo restante da verificação",
"Scanning": "Verificando",
"Select the devices to share this folder with.": "Seleccione os dispositivos com os quais vai partilhar esta pasta.",
"Select the folders to share with this device.": "Seleccione as pastas a partilhar com este dispositivo.",
@@ -211,7 +211,7 @@
"The rate limit must be a non-negative number (0: no limit)": "O limite de velocidade tem que ser um número que não seja negativo (0: sem limite)",
"The rescan interval must be a non-negative number of seconds.": "O intervalo entre verificações tem que ser um valor não negativo de segundos.",
"They are retried automatically and will be synced when the error is resolved.": "Será tentado automaticamente e os itens serão sincronizados assim que o erro seja resolvido.",
"This Device": "This Device",
"This Device": "Este dispositivo",
"This can easily give hackers access to read and change any files on your computer.": "Isso facilmente dará acesso aos piratas informáticos para lerem e modificarem quaisquer ficheiros no seu computador.",
"This is a major version upgrade.": "Esta é uma actualização para uma versão importante.",
"Trash Can File Versioning": "Reciclagem",
@@ -229,7 +229,7 @@
"Version": "Versão",
"Versions Path": "Caminho das versões",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são eliminadas automaticamente se forem mais antigas do que a idade máxima ou excederem o número máximo de ficheiros permitido num intervalo.",
"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%}\".": "Aviso: Este caminho é uma subpasta da pasta \"{{otherFolder}}\" já existente.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando adicionar um novo dispositivo, lembre-se que este dispositivo tem que ser adicionado do outro lado também.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quando adicionar uma nova pasta, lembre-se que o ID da pasta é utilizado para ligar as pastas entre dispositivos. É sensível às diferenças entre maiúsculas e minúsculas e tem que ter uma correspondência perfeita entre todos os dispositivos.",
"Yes": "Sim",
@@ -238,5 +238,6 @@
"full documentation": "documentação completa",
"items": "itens",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer partilhar a pasta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} quer partilhar a pasta \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quer partilhar a pasta \"{{folderlabel}}\" ({{folder}})."
}
+12 -11
View File
@@ -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": "По алфавиту",
@@ -34,12 +34,12 @@
"Connection Error": "Ошибка подключения",
"Copied from elsewhere": "Скопировано из другого места",
"Copied from original": "Скопировано с оригинала",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Авторские права © 20142016 принадлежат:",
"Copyright © 2015 the following Contributors:": "Все права защищены ©, 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": "ID устройства",
"Device Identification": "Идентификация устройства",
"Device Name": "Имя устройства",
@@ -70,7 +70,7 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файлы защищены от изменений сделанных на других устройствах, но изменения сделанные на этом устройстве будут отправлены всему кластеру.",
"Folder": "Папка",
"Folder ID": "ID папки",
"Folder Label": "Folder Label",
"Folder Label": "Ярлык папки",
"Folder Master": "Папка-оригинал",
"Folder Path": "Путь к папке",
"Folders": "Папки",
@@ -116,7 +116,7 @@
"OK": "ОК",
"Off": "Отключить",
"Oldest First": "Сначала старые",
"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.": "Необязательное описательное название папки. Может различаться на разных устройствах.",
"Options": "Настройки",
"Out of Sync": "Нет синхронизации",
"Out of Sync Items": "Не синхронизированные пункты",
@@ -138,9 +138,9 @@
"Relayed via": "Релей через",
"Relays": "Релеи",
"Release Notes": "Замечания к версии",
"Remote Devices": "Remote Devices",
"Remote Devices": "Удалённые устройства",
"Remove": "Удалить",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Обязательный идентификатор папки. Должен быть одним и тем же на всех устройствах кластера.",
"Rescan": "Пересканирование",
"Rescan All": "Пересканировать все",
"Rescan Interval": "Интервал пересканирования",
@@ -211,7 +211,7 @@
"The rate limit must be a non-negative number (0: no limit)": "Скорость должна быть неотрицательным числом (0: нет ограничения)",
"The rescan interval must be a non-negative number of seconds.": "Интервал пересканирования должен быть неотрицательным количеством секунд.",
"They are retried automatically and will be synced when the error is resolved.": "Будут синхронизированы автоматически когда ошибка будет исправлена.",
"This Device": "This Device",
"This Device": "Это устройство",
"This can easily give hackers access to read and change any files on your computer.": "Это может дать доступ хакерам для чтения и изменения любых файлов на вашем компьютере.",
"This is a major version upgrade.": "Это обновление основной версии продукта.",
"Trash Can File Versioning": "Использовать версионность для файлов в Корзине",
@@ -229,7 +229,7 @@
"Version": "Версия",
"Versions Path": "Путь к версиям",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версии удаляются автоматически, если они существуют дольше максимального срока или превышают разрешённое количество файлов за интервал.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Осторожно, этот путь является подкаталогом существующей папки «{{otherFolder}}».",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Когда добавляете устройство, помните о том, что это же устройство должно быть добавлено и другой стороной.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Когда добавляете новую папку, помните, что ID папок используются для того, чтобы связывать папки между всеми устройствами. Они чувствительны к регистру и должны совпадать на всех используемых устройствах.",
"Yes": "Да",
@@ -238,5 +238,6 @@
"full documentation": "полная документация",
"items": "элементы",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderLabel}}» ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+17 -16
View File
@@ -8,13 +8,13 @@
"Add": "Lägg till",
"Add Device": "Lägg till enhet",
"Add Folder": "Lägg till katalog",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Lägg till fjärrenhet",
"Add new folder?": "Lägg till katalog?",
"Address": "Adress",
"Addresses": "Adresser",
"Advanced": "Avancerat",
"Advanced Configuration": "Avancerad konfiguration",
"Advanced settings": "Advanced settings",
"Advanced settings": "Avancerade inställningar",
"All Data": "All data",
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistik?",
"Alphabetic": "Alfabetisk",
@@ -34,12 +34,12 @@
"Connection Error": "Anslutningsproblem",
"Copied from elsewhere": "Kopierat utifrån",
"Copied from original": "Oförändrat",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragande:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 följande medverkande:",
"Danger!": "Fara!",
"Delete": "Radera",
"Deleted": "Borttaget",
"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?": "Enhet \"{{name}}\" ({{device}} {{address}}) vill ansluta. Lägg till ny enhet?",
"Device ID": "Enhets-ID",
"Device Identification": "Enhetsidentifikation",
"Device Name": "Enhetsnamn",
@@ -55,7 +55,7 @@
"Edit Device": "Redigera enhet",
"Edit Folder": "Redigera katalog",
"Editing": "Redigerar",
"Enable Relaying": "Enable Relaying",
"Enable Relaying": "Aktivera reläa",
"Enable UPnP": "Använd 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.",
@@ -70,7 +70,7 @@
"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": "Folder Label",
"Folder Label": "Katalog etikett",
"Folder Master": "Huvudlagring",
"Folder Path": "Sökväg",
"Folders": "Kataloger",
@@ -81,7 +81,7 @@
"Generate": "Skapa",
"Global Discovery": "Global uppslagning",
"Global Discovery Server": "Global uppslagningsserver",
"Global Discovery Servers": "Global Discovery Servers",
"Global Discovery Servers": "Globala uppslagningsservrar",
"Global State": "Global status",
"Help": "Hjälp",
"Home page": "Hemsida",
@@ -116,7 +116,7 @@
"OK": "OK",
"Off": "Av",
"Oldest First": "Äldst först",
"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.": "Valfri beskrivande etikett för katalogen. Kan vara olika på varje enhet.",
"Options": "Alternativ",
"Out of Sync": "Osynkad",
"Out of Sync Items": "Osynkade poster",
@@ -127,20 +127,20 @@
"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.": "Please set a GUI Authentication User and Password in the Settings dialog.",
"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 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",
"Random": "Slumpmässig",
"Relay Servers": "Relay Servers",
"Relay Servers": "Reläservrar",
"Relayed via": "Vidarbefordras via",
"Relays": "Vidarbefordringar",
"Release Notes": "versionsnyheter",
"Remote Devices": "Remote Devices",
"Remote Devices": "Fjärrenheter",
"Remove": "Ta bort",
"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.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för mappen. Måste vara densamma på alla kluster enheter.",
"Rescan": "Uppdatera",
"Rescan All": "Uppdatera alla",
"Rescan Interval": "Uppdateringsintervall",
@@ -211,8 +211,8 @@
"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": "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": "Enheten",
"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",
@@ -229,7 +229,7 @@
"Version": "Version",
"Versions Path": "Katalog 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%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"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.",
"Yes": "Ja",
@@ -238,5 +238,6 @@
"full documentation": "fullständig dokumentation",
"items": "poster",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderlabel}}\" ({{folder}})."
}
+2 -1
View File
@@ -238,5 +238,6 @@
"full documentation": "belgelendirme içeriğinin tümü",
"items": "öğel",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} \"{{folder}}\" klasörünü paylaşmak istiyor.",
"{%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}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+3 -2
View File
@@ -39,7 +39,7 @@
"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": "ID пристрою",
"Device Identification": "Ідентифікатор пристрою",
"Device Name": "Назва пристрою",
@@ -238,5 +238,6 @@
"full documentation": "повна документація",
"items": "елементи",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хоче поділитися директорією \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хоче поділитися директорією \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хоче поділитися директорією \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} хоче поділитися директорією \"{{folderLabel}}\" ({{folder}})."
}
+76 -75
View File
@@ -8,13 +8,13 @@
"Add": "Thêm",
"Add Device": "Thêm thiết bị",
"Add Folder": "Thêm thư mục",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Thêm thiết bị từ xa",
"Add new folder?": "Thêm thư mục mới?",
"Address": "Địa chỉ",
"Addresses": "Các địa chỉ",
"Advanced": "Nâng cao",
"Advanced Configuration": "Cấu hình nâng cao",
"Advanced settings": "Advanced settings",
"Advanced settings": "Cài đặt nâng cao",
"All Data": "Tất cả dữ liệu",
"Allow Anonymous Usage Reporting?": "Cho phép báo cáo sử dụng ẩn danh?",
"Alphabetic": "A-Z",
@@ -24,7 +24,7 @@
"Automatic upgrades": "Cập nhật tự động",
"Be careful!": "Cẩn thận!",
"Bugs": "Lỗi",
"CPU Utilization": "Mức sdụng CPU",
"CPU Utilization": "Mức s.dụng CPU",
"Changelog": "Lịch sử thay đổi",
"Clean out after": "Dọn dẹp sau",
"Close": "Đóng",
@@ -34,12 +34,12 @@
"Connection Error": "Lỗi kết nối",
"Copied from elsewhere": "Đã sao chép từ nơi khác",
"Copied from original": "Đã sao chép từ nguồn",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Bản quyền © 2014-2016 thuộc về các nhà cộng tác sau:",
"Copyright © 2015 the following Contributors:": "Bản quyền © 2015 thuộc về các nhà cộng tác sau:",
"Danger!": "Nguy hiểm!",
"Delete": "Xoá",
"Deleted": "Đã xoá",
"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?": "Thiết bị \"{{name}}\" ({{device}} tại {{address}}) muốn kết nối. Thêm thiết bị mới?",
"Device ID": "ID thiết bị",
"Device Identification": "Danh tính thiết bị",
"Device Name": "Tên thiết bị",
@@ -52,61 +52,61 @@
"Downloaded": "Đã tải xuống",
"Downloading": "Đang tải xuống",
"Edit": "Chỉnh sửa",
"Edit Device": "Chỉnh sửa thiết bị",
"Edit Folder": "Chỉnh sửa thư mục",
"Editing": "Đang chỉnh sửa",
"Enable Relaying": "Bật chế độ chuyển tiếp",
"Edit Device": "Ch.sửa thiết bị",
"Edit Folder": "Ch.sửa thư mục",
"Editing": "Đang ch.sửa",
"Enable Relaying": "Bật chế độ ch.tiếp",
"Enable UPnP": "Bật UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Nhập các địa chỉ ngăn cách bởi dấu phẩy (\"tcp://ip:port\", \"tcp://host:port\") hoặc \"dynamic\" để tiến hành dò tìm địa chỉ tự động.",
"Enter ignore patterns, one per line.": "Nhập các quy luật bỏ qua, từng dòng một.",
"Error": "Lỗi",
"External File Versioning": "Kiểu ngoại vi",
"Failed Items": "Các nội dung bị lỗi",
"Failed Items": "Các n.dung bị lỗi",
"File Pull Order": "Thứ tự pull tập tin",
"File Versioning": "Phiên bản hoá tập tin",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Các phần tử giấy phép tập tin sẽ được bỏ qua khi tìm kiếm thay đổi. Dùng trên hthống tập tin FAT.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Các tập tin sẽ được chuyển tới thư mục .stversions khi bị thay thế hoặc xoá bởi Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Các tập tin sẽ được chuyển tới các phiên bản được đánh dấu ngày tháng trong thư mục .stversions khi bị thay thế hoặc xoá bởi Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Các tập tin sẽ được bảo vệ khỏi những thay đổi trên các thiết bị khác, nhưng những thay đổi trên thiết bị này sẽ được chuyển tới các máy còn lại trong cụm.",
"File Versioning": "Ph.bản hoá tập tin",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Các ph.tử g.phép tập tin sẽ được bỏ qua khi t.kiếm th.đổi. Dùng trên h.thống tập tin FAT.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Các t.tin sẽ được chuyển tới th.mục .stversions khi bị th.thế hoặc xoá bởi Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Các t.tin sẽ được chuyển tới các ph.bản được đ.dấu ng.tháng trong th.mục .stversions khi bị th.thế hoặc xoá bởi Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Các t.tin được b.vệ khỏi những th.đổi trên các th.bị khác, nhưng những th.đổi trên th.bị này sẽ được chuyển tới các máy cụm còn lại.",
"Folder": "Thư mục",
"Folder ID": "ID thư mục",
"Folder Label": "Folder Label",
"Folder Label": "Nhãn thư mục",
"Folder Master": "Thư mục Chủ",
"Folder Path": ường dẫn đến thư mục",
"Folders": "Các thư mục",
"Folder Path": .dẫn đến th.mục",
"Folders": "Các th.mục",
"GUI": "GUI",
"GUI Authentication Password": "Mật khẩu xác minh GUI",
"GUI Authentication User": "Người dùng xác minh GUI",
"GUI Listen Addresses": "Các địa chỉ lắng nghe GUI",
"GUI Listen Addresses": "Các đ.chỉ lắng nghe GUI",
"Generate": "Tạo mới",
"Global Discovery": "Dò tìm toàn cầu",
"Global Discovery Server": "Máy chủ dò tìm toàn cầu",
"Global Discovery Servers": "Máy chủ dò tìm toàn cầu",
"Global State": "Trạng thái toàn cầu",
"Global Discovery Servers": "Các m.chủ dò tìm toàn cầu",
"Global State": "Tr.thái toàn cầu",
"Help": "Trợ giúp",
"Home page": "Trang chủ",
"Ignore": "Bỏ qua",
"Ignore Patterns": "Bỏ qua các quy luật",
"Ignore Permissions": "Bỏ qua các giấy phép",
"Incoming Rate Limit (KiB/s)": "Giới hạn tốc độ đầu vào (KiB/s)",
"Incoming Rate Limit (KiB/s)": "Giới hạn t.độ đầu vào (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Cấu hình không đúng có thể làm mất mát dữ liệu và khiến Syncthing ngừng hoạt động.",
"Introducer": "Thiết bị giới thiệu",
"Inversion of the given condition (i.e. do not exclude)": "Đảo ngược điều kiện cho trước (ví dụ: không được loại trừ)",
"Keep Versions": "Giữ các phiên bản",
"Introducer": "Th.bị giới thiệu",
"Inversion of the given condition (i.e. do not exclude)": "Đảo ngược điều kiện cho trước (VD: không được loại trừ)",
"Keep Versions": "Giữ các ph.bản",
"Largest First": "Lớn nhất đầu tiên",
"Last File Received": "Tập tin nhận được gần đây",
"Last File Received": "T.tin nhận được gần đây",
"Last seen": "Thấy lần cuối",
"Later": "Để sau",
"Local Discovery": "Dò tìm cục bộ",
"Local State": "Trạng thái cục bộ",
"Local State (Total)": "Trạng thái cục bộ (Tổng)",
"Major Upgrade": "Bản nâng cấp quan trọng",
"Local State": "Tr.thái cục bộ",
"Local State (Total)": "Tr.thái cục bộ (Tổng)",
"Major Upgrade": "Bản n.cấp q.trọng",
"Maximum Age": "Thời hạn tối đa",
"Metadata Only": "Chỉ siêu dữ liệu",
"Minimum Free Disk Space": "Dung lượng đĩa trống tối thiểu",
"Move to top of queue": "Chuyển đến đầu hàng chờ",
"Multi level wildcard (matches multiple directory levels)": "Ký tự thay thế đa cấp (phù hợp với đa cấp độ thư mục)",
"Never": "Chưa bao giờ",
"Never": "Chưa từng",
"New Device": "Thiết bị mới",
"New Folder": "Thư mục mới",
"Newest First": "Mới nhất đầu tiên",
@@ -116,39 +116,39 @@
"OK": "OK",
"Off": "Tắt",
"Oldest First": "Cũ nhất đầu tiên",
"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.": "Nhãn mô tả tuỳ chọn cho thư mục. Có thể khác nhau trên từng thiết bị.",
"Options": "Tuỳ chọn",
"Out of Sync": "Mất đồng bộ",
"Out of Sync Items": "Các nội dung mất đồng bộ",
"Outgoing Rate Limit (KiB/s)": "Giới hạn tốc độ đầu ra (KiB/s)",
"Override Changes": "Ghi đè các thay đổi",
"Out of Sync Items": "Các n.dung mất đ.bộ",
"Outgoing Rate Limit (KiB/s)": "Giới hạn t.độ đầu ra (KiB/s)",
"Override Changes": "Ghi đè các th.đổi",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Đường dẫn đến thư mục trên máy cục bộ. Sẽ tạo mới nếu chưa hiện hữu. Dấu ngã (~) có thể được dùng làm lối tắt cho",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Đường dẫn nơi các phiên bản được lưu trữ (nếu để trống, thư mục mặc định sẽ là .stversions).",
"Pause": "Tạm dừng",
"Paused": "Đã tạm dừng",
"Please consult the release notes before performing a major upgrade.": "Hãy xem kỹ lịch sử phát hành trước khi tiến hành bản cập nhật quan trọng.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Hãy thiết lập tên người dùng và mật khẩu xác minh GUI trong hộp thoại Cài đặt.",
"Paused": "Đã t.dừng",
"Please consult the release notes before performing a major upgrade.": "Hãy xem kỹ lịch sử phát hành trước khi tiến hành bản cập nhật q.trọng.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Hãy thiết lập tên ng.dùng và mật khẩu xác minh GUI trong hộp thoại Cài đặt.",
"Please wait": "Xin chờ",
"Preview": "Xem trước",
"Preview Usage Report": "Xem trước báo cáo sdụng",
"Quick guide to supported patterns": "Hướng dẫn sơ lược về các quy luật được hỗ trợ",
"RAM Utilization": "Mức sdụng RAM",
"Preview Usage Report": "Xem trước báo cáo s.dụng",
"Quick guide to supported patterns": "H.dẫn sơ lược về các q.luật được hỗ trợ",
"RAM Utilization": "Mức s.dụng RAM",
"Random": "Ngẫu nhiên",
"Relay Servers": "Máy chủ chuyển tiếp",
"Relayed via": "Chuyển tiếp qua",
"Relays": "Các máy chuyển tiếp",
"Relay Servers": "M.chủ chuyển tiếp",
"Relayed via": "Ch.tiếp qua",
"Relays": "Các máy ch.tiếp",
"Release Notes": "Lịch sử phát hành",
"Remote Devices": "Remote Devices",
"Remote Devices": "Các thiết bị từ xa",
"Remove": "Xoá",
"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.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Tên tắt bắt buộc cho thư mục. Phải trùng khớp trên tất cả thiết bị trong cụm.",
"Rescan": "Quét lại",
"Rescan All": "Quét lại tất cả",
"Rescan Interval": "Khoảng cách thời gian quét lại",
"Restart": "Khởi động lại",
"Restart Needed": "Cần khởi động lại",
"Restarting": "Đang khởi động lại",
"Rescan All": "Quét lại t.cả",
"Rescan Interval": "Kh.cách th.gian quét lại",
"Restart": "Kh.động lại",
"Restart Needed": "Cần kh.động lại",
"Restarting": "Đang kh.động lại",
"Resume": "Tiếp tục",
"Reused": "Đã sdụng lại",
"Reused": "Đã s.dụng lại",
"Save": "Lưu",
"Scan Time Remaining": "Thời gian quét còn lại",
"Scanning": "Đang quét",
@@ -156,15 +156,15 @@
"Select the folders to share with this device.": "Chọn các thư mục để chia sẻ với thiết bị này.",
"Settings": "Cài đặt",
"Share": "Chia sẻ",
"Share Folder": "Chia sẻ thư mục",
"Share Folders With Device": "Chia sẻ các thư mục với thiết bị",
"Share With Devices": "Chia sẻ với các thiết bị",
"Share this folder?": "Chia sẻ thư mục này?",
"Shared With": "Đã chia sẻ với",
"Share Folder": "Chia sẻ th.mục",
"Share Folders With Device": "Chia sẻ các th.mục với th.bị",
"Share With Devices": "Chia sẻ với các th.bị",
"Share this folder?": "Chia sẻ th.mục này?",
"Shared With": "Đã ch.sẻ với",
"Short identifier for the folder. Must be the same on all cluster devices.": "Tên tắt cho thư mục. Phải trùng khớp trên tất cả thiết bị trong cụm.",
"Show ID": "Hiển thị ID",
"Show QR": "Hiển thị QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Hiển thị thay cho ID thiết bị trong trạng thái cụm. Sẽ được giới thiệu đến các thiết bị khác như tên mặc định tuỳ chọn.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Hiển thị thay cho ID th.bị trong trạng thái cụm. Sẽ được giới thiệu đến các th.bị khác như tên mặc định tuỳ chọn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Hiển thị thay cho ID thiết bị trong trạng thái cụm. Nếu để trống sẽ được cập nhật thành tên mà thiết bị giới thiệu.",
"Shutdown": "Tắt",
"Shutdown Complete": "Tắt hoàn tất",
@@ -177,31 +177,31 @@
"Statistics": "Thống kê",
"Stopped": "Đã dừng",
"Support": "Hỗ trợ",
"Sync Protocol Listen Addresses": "Đồng bộ các địa chỉ lắng nghe giao thức",
"Syncing": "Đang đồng bộ",
"Sync Protocol Listen Addresses": "Đồng bộ các đ.chỉ l.nghe giao thức",
"Syncing": "Đang đ.bộ",
"Syncthing has been shut down.": "Đã tắt Syncthing.",
"Syncthing includes the following software or portions thereof:": "Syncthing bao gồm các phần mềm hoặc phần tử của các phần mềm sau:",
"Syncthing is restarting.": "Syncthing đang khởi động lại.",
"Syncthing is upgrading.": "Syncthing đang nâng cấp.",
"Syncthing includes the following software or portions thereof:": "Syncthing bao gồm các ph.mềm hoặc ph.tử của các ph.mềm sau:",
"Syncthing is restarting.": "Syncthing đang kh.động lại.",
"Syncthing is upgrading.": "Syncthing đang n.cấp.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Có vẻ như Syncthing đang bị nghẽn hoặc là mạng của bạn có vấn đề. Đang thử lại...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Có vẻ như Syncthing đang gặp phải vấn đề khi xlý yêu cầu của bạn. Xin làm mới trang hoặc khởi động lại Syncthing nếu vấn đề vẫn còn tiếp diễn.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Giao diện quản trị của Syncthing được cấu hình nhằm cho phép truy cập từ xa không cần mật .",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Có vẻ như Syncthing đang gặp phải v.đề khi x.lý yêu cầu của bạn. Xin làm mới trang hoặc kh.động lại Syncthing nếu v.đề vẫn còn tiếp diễn.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Giao diện q.trị của Syncthing được c.hình nhằm cho phép tr.cập từ xa không cần mật khẩu.",
"The aggregated statistics are publicly available at {%url%}.": "Thống kê tổng hợp được đăng công khai trên {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Cấu hình đã được lưu nhưng chưa được kích hoạt. Syncthing phải khởi động lại để kích hoạt cấu hình mới. ",
"The device ID cannot be blank.": "Không được để trống ID thiết bị.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ID thiết bị cần nhập có thể được tìm thấy trong hộp thoại \"Thao tác > Hiển thị ID\" trên thiết bị kia. Khoảng trắng và gạch ngang là tuỳ chọn (bỏ qua).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ID thiết bị cần nhập có thể được tìm thấy trong hộp thoại \"Chỉnh sửa > Hiển thị ID\" trên thiết bị kia. Khoảng trắng và gạch ngang là tuỳ chọn (bỏ qua).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Báo cáo sdụng đã mã hoá sẽ được gửi đi hằng ngày. Nó được dùng để thu thập sliệu về các hệ điều hành phổ biến, kích cỡ thư mục và phiên bản ứng dụng. Nếu bộ dliệu báo cáo có thay đổi, bạn sẽ được nhắc nhở thông qua hộp thoại này.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ID th.bị cần nhập có thể được tìm thấy trong h.thoại \"Chỉnh sửa > Hiển thị ID\" trên th.bị kia. Khoảng trắng và gạch ngang là t.chọn (bỏ qua).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Báo cáo s.dụng đã mã hoá sẽ được gửi đi hằng ngày. Nó được dùng để t.thập s.liệu về các HĐH phổ biến, kích cỡ th.mục và ph.bản ứng dụng. Nếu bộ d.liệu báo cáo có th.đổi, bạn sẽ được nhắc thông qua h.thoại này.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "ID thiết bị đã nhập không hợp lệ. Nó phải là một chuỗi từ 52 đến 56 ký tự, bao gồm chữ cái và các con số, với khoảng trắng và gạch ngang là tuỳ chọn.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Tham số dòng lệnh đầu tiên là đường dẫn thư mục và tham số thứ hai là đường dẫn tương đối trong thư mục.",
"The folder ID cannot be blank.": "Không được để trống ID thư mục.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "ID thư mục phải là một tên tắt (64 ký tự hoặc ít hơn) chỉ bao gồm chữ cái, các con số và dấu chấm (.), gạch ngang (-) và gạch chân (_).",
"The folder ID must be unique.": "ID thư mục phải là duy nhất.",
"The folder path cannot be blank.": "Không được để trống đường dẫn đến thư mục.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Các khoảng thời gian sau đây sẽ được sdụng: một phiên bản, trong giờ đầu tiên, được giữ lại mỗi 30 giây, trong ngày đầu tiên là mỗi giờ, trong 30 ngày đầu tiên là mỗi ngày, cho đến khi thời hạn tối đa mỗi phiên bản được giữ lại là mỗi tuần. ",
"The folder path cannot be blank.": "Không được để trống đ.dẫn đến th.mục.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Các khoảng th.gian sau đây sẽ được s.dụng: một phiên bản, trong giờ đầu tiên, được giữ lại mỗi 30 giây, trong ngày đầu tiên là mỗi giờ, trong 30 ngày đầu tiên là mỗi ngày, cho đến khi th.hạn tối đa mỗi ph.bản được giữ lại là mỗi tuần. ",
"The following items could not be synchronized.": "Không thể đồng bộ những nội dung sau.",
"The maximum age must be a number and cannot be blank.": "Thời hạn tối đa phải là một con số và không được để trống.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Thời gian tối đa một phiên bản được giữ lại (tính bằng ngày, nhập 0 để giữ tập tin mãi mãi).",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Th.gian t.đa một ph.bản được giữ lại (tính bằng ngày, nhập 0 để giữ tập tin mãi mãi).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Phần trăm dung lượng đĩa trống tối thiểu phải là một số không âm (nằm trong khoảng) từ 0 đến 100.",
"The number of days must be a number and cannot be blank.": "Số ngày phải là một con số và không được để trống.",
"The number of days to keep files in the trash can. Zero means forever.": "Số ngày tập tin được giữ trong thùng rác. 0 nghĩa là mãi mãi.",
@@ -211,8 +211,8 @@
"The rate limit must be a non-negative number (0: no limit)": "Giới hạn tốc độ phải là một số không âm (0: không giới hạn)",
"The rescan interval must be a non-negative number of seconds.": "Khoảng thời gian quét lại phải là một số giây không âm.",
"They are retried automatically and will be synced when the error is resolved.": "Chúng sẽ được tự động thử lại và đồng bộ khi lỗi được khắc phục.",
"This Device": "This Device",
"This can easily give hackers access to read and change any files on your computer.": "Thao tác này có thể khiến tin tặc truy cập để đọc và thay đổi bất kỳ tập tin nào trên máy tính của bạn một cách dễ dàng.",
"This Device": "Thiết bị này",
"This can easily give hackers access to read and change any files on your computer.": "Th.tác này có thể khiến tin tặc dễ dàng tr.cập để đọc và th.đổi bất kỳ t.tin nào trên máy của bạn.",
"This is a major version upgrade.": "Đây là bản nâng cấp quan trọng.",
"Trash Can File Versioning": "Kiểu thùng rác",
"Unknown": "Không biết",
@@ -224,12 +224,12 @@
"Upgrade To {%version%}": "Nâng cấp lên {{version}}",
"Upgrading": "Đang nâng cấp",
"Upload Rate": "Tốc độ tải lên",
"Uptime": "Thời gian hoạt động",
"Uptime": "Th.gian h.động",
"Use HTTPS for GUI": "Sử dụng HTTPS cho GUI",
"Version": "Phiên bản",
"Versions Path": ường dẫn đến các phiên bản",
"Versions Path": .dẫn đến các ph.bản",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Các phiên bản sẽ tự động được xoá nếu chúng cũ hơn thời hạn tối đa hoặc vượt quá số tập tin cho phép trong một khoảng thời gian.",
"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%}\".": "Cảnh báo, đường dẫn này là thư mục con của thư mục hiện hữu \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Khi thêm một thiết bị mới, hãy nhớ rằng thiết bị này cũng phải được thêm vào máy kia.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Khi thêm một thư mục mới, hãy nhớ rằng ID thư mục được dùng để gắn kết thư mục giữa các thiết bị với nhau. Chúng phải chính xác từng chữ, cả viết hoa và thường giữa tất cả thiết bị.",
"Yes": "Phải",
@@ -238,5 +238,6 @@
"full documentation": "tài liệu đầy đủ",
"items": "nội dung",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} muốn chia sẻ thư mục \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} muốn chia sẻ thư mục \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
}
+7 -6
View File
@@ -1,6 +1,6 @@
{
"A device with that ID is already added.": "您已添加过设备",
"A negative number of days doesn't make sense.": "天数不能为负",
"A device with that ID is already added.": "您已添加过相同 ID 的设备",
"A negative number of days doesn't make sense.": "天数为负数没有意义。",
"A new major version may not be compatible with previous versions.": "重大更新可能与之前的版本之间无法兼容",
"API Key": "API Key",
"About": "关于",
@@ -39,7 +39,7 @@
"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}}\" (位于 {{address}} 的 {{device}}) 请求连接。是否添加新设备?",
"Device ID": "设备标识",
"Device Identification": "设备标识",
"Device Name": "设备名",
@@ -57,7 +57,7 @@
"Editing": "正在编辑",
"Enable Relaying": "开启中继",
"Enable UPnP": "开启UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "输入以半角逗号分隔的 \"tcp://IP地址:端口号\", \"tcp://主机:端口号\" 设置可用地址列表,或者输入\"dynamic\"表示自动寻找地址。",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "输入以半角逗号分隔的 (\"tcp://ip:port\", \"tcp://host:port\") 设置可用地址列表,或者输入 \"dynamic\" 表示自动寻找地址。",
"Enter ignore patterns, one per line.": "请输入忽略表达式,每行一条",
"Error": "错误",
"External File Versioning": "外部版本控制",
@@ -88,7 +88,7 @@
"Ignore": "忽略",
"Ignore Patterns": "忽略列表",
"Ignore Permissions": "忽略文件权限",
"Incoming Rate Limit (KiB/s)": "下载速率限制KiB/s",
"Incoming Rate Limit (KiB/s)": "下载速率限制 (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "错误的配置可能损坏您文件夹内的内容,使得 Syncthing 无法工作。",
"Introducer": "介绍人设备",
"Inversion of the given condition (i.e. do not exclude)": "对本条件取反(例如:不要排除某项)",
@@ -238,5 +238,6 @@
"full documentation": "完整文档",
"items": "条目",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想将 “{{folder}}” 文件夹共享给您",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}}想要分享文件夹\"{{folderLabel}}\" ({{folder}}).\n"
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} 想要分享文件夹 \"{{folderLabel}}\" ({{folder}})。",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 想要分享文件夹 \"{{folderLabel}}\" ({{folder}})。"
}
+34 -33
View File
@@ -1,28 +1,28 @@
{
"A device with that ID is already added.": "A device with that ID is already added.",
"A device with that ID is already added.": "該裝置識別碼已被新增。",
"A negative number of days doesn't make sense.": "一個負的天數並不合理。",
"A new major version may not be compatible with previous versions.": "一個新的主要版本可能與以前的版本不相容。",
"A new major version may not be compatible with previous versions.": "新的主要版本可能與以前的版本不相容。",
"API Key": "API 金鑰",
"About": "關於",
"Actions": "操作",
"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 Configuration": "進階配置",
"Advanced settings": "進階設定",
"All Data": "全部資料",
"Allow Anonymous Usage Reporting?": "允許匿名的使用資訊回報",
"Allow Anonymous Usage Reporting?": "允許匿名的使用資訊回報?",
"Alphabetic": "字母順序",
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
"Anonymous Usage Reporting": "匿名的使用資訊回報",
"Any devices configured on an introducer device will be added to this device as well.": "任何在引入者裝置所設置的裝置將會一併新增至此裝置",
"Automatic upgrades": "自動升級",
"Be careful!": "請小心",
"Be careful!": "請小心!",
"Bugs": "程式錯誤",
"CPU Utilization": "CPU 使用",
"Changelog": "更新日誌",
@@ -33,20 +33,20 @@
"Compression": "壓縮",
"Connection Error": "連線錯誤",
"Copied from elsewhere": "從別處複製",
"Copied from original": "從原複製",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copied from original": "從原複製",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 下列貢獻者:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 下列貢獻者:",
"Danger!": "Danger!",
"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": "裝置名稱",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "裝置 {{device}} ({{address}}) 想要連線。要新增裝置嗎",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "裝置 {{device}} ({{address}}) 想要連線。要新增裝置嗎?",
"Devices": "裝置",
"Disconnected": "斷線",
"Discovery": "Discovery",
"Discovery": "探索",
"Documentation": "說明文件",
"Download Rate": "下載速率",
"Downloaded": "已下載",
@@ -55,7 +55,7 @@
"Edit Device": "編輯裝置",
"Edit Folder": "編輯資料夾",
"Editing": "正在編輯",
"Enable Relaying": "Enable Relaying",
"Enable Relaying": "啟用中繼",
"Enable UPnP": "啟用 UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "輸入忽略樣式,每行一種。",
@@ -70,26 +70,26 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "其他裝置做的改變不會影響到此裝置的檔案,但在此裝置上的變化將被發送到叢集中的其他部分。",
"Folder": "資料夾",
"Folder ID": "資料夾識別碼",
"Folder Label": "Folder Label",
"Folder Label": "資料夾標籤",
"Folder Master": "主資料夾",
"Folder Path": "資料夾路徑",
"Folders": "資料夾",
"GUI": "GUI",
"GUI Authentication Password": "GUI 認證密碼",
"GUI Authentication User": "GUI 認證使用者名稱",
"GUI Authentication User": "GUI 使用者認證名稱",
"GUI Listen Addresses": "GUI 監聽位址",
"Generate": "產生",
"Global Discovery": "全域探索",
"Global Discovery Server": "全域探索伺服器",
"Global Discovery Servers": "Global Discovery Servers",
"Global Discovery Servers": "全域探索伺服器",
"Global State": "全域狀態",
"Help": "說明",
"Home page": "首頁",
"Ignore": "忽略",
"Ignore Patterns": "忽略樣式",
"Ignore Permissions": "忽略權限",
"Incoming Rate Limit (KiB/s)": "入速率限制 (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "不正確的設定可能會損壞的資料夾內容,並致 Syncthing 不正運作。",
"Incoming Rate Limit (KiB/s)": "入速率限制 (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "不正確的設定可能會損壞的資料夾內容,並致 Syncthing 不正運作。",
"Introducer": "引入者",
"Inversion of the given condition (i.e. do not exclude)": "反轉給定條件 (即:不要排除)",
"Keep Versions": "保留歷史版本數",
@@ -97,9 +97,9 @@
"Last File Received": "最後接收的檔案",
"Last seen": "最後發現時間",
"Later": "稍後",
"Local Discovery": "本探索",
"Local State": "本狀態",
"Local State (Total)": "本狀態 (總結)",
"Local Discovery": "本探索",
"Local State": "本狀態",
"Local State (Total)": "本狀態 (總結)",
"Major Upgrade": "重大更新",
"Maximum Age": "最長保留時間",
"Metadata Only": "僅中繼資料",
@@ -118,27 +118,27 @@
"Oldest First": "最舊的優先",
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
"Options": "選項",
"Out of Sync": "Out of Sync",
"Out of Sync": "不同步",
"Out of Sync Items": "不同步物件",
"Outgoing Rate Limit (KiB/s)": "連出速率限制 (KiB/s)",
"Override Changes": "置換改變",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "資料夾在本地電腦的路徑。若資料夾不存在則會建立。波浪符號 (~) 可用作下列資料夾的捷徑:",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "資料夾在本的路徑。若資料夾不存在則會建立。波浪符號 (~) 可用作下列資料夾的捷徑:",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "儲存歷史版本的路徑 (若為空,則預設使用資料夾中的 .stversions 資料夾)。",
"Pause": "暫停",
"Paused": "暫停",
"Please consult the release notes before performing a major upgrade.": "執行重大升級前請先參閱版本資訊。",
"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.": "請在設定對話方塊內設置 GUI 使用者認證名稱及密碼。",
"Please wait": "請稍後",
"Preview": "預覽",
"Preview Usage Report": "預覽使用資訊報告",
"Quick guide to supported patterns": "可支援樣式的快速指南",
"RAM Utilization": "記憶體使用",
"Random": "隨機",
"Relay Servers": "Relay Servers",
"Relay Servers": "中繼伺服器",
"Relayed via": "中繼於",
"Relays": "中繼點",
"Release Notes": "版本資訊",
"Remote Devices": "Remote Devices",
"Remote Devices": "遠端裝置",
"Remove": "移除",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Rescan": "重新掃描",
@@ -159,11 +159,11 @@
"Share Folder": "分享資料夾",
"Share Folders With Device": "與裝置共享資料夾",
"Share With Devices": "與這些裝置共享",
"Share this folder?": "分享此資料夾",
"Share this folder?": "分享此資料夾?",
"Shared With": "與誰共享",
"Short identifier for the folder. Must be the same on all cluster devices.": "資料夾的簡短識別字。必須在叢集內所有的裝置上皆相同。",
"Show ID": "顯示識別碼",
"Show QR": "Show QR",
"Show QR": "顯示 QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "代替裝置識別碼顯示在叢集狀態中。這段文字將會廣播到其他的裝置作為一個可選的預設名稱。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "代替裝置識別碼顯示在叢集狀態中。本欄若未填寫則將被更新為此裝置所廣播的名稱。",
"Shutdown": "關閉",
@@ -183,14 +183,14 @@
"Syncthing includes the following software or portions thereof:": "Syncthing 包括以下軟體或其中的一部分:",
"Syncthing is restarting.": "Syncthing 正在重新啟動。",
"Syncthing is upgrading.": "Syncthing 正在進行升級。",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎線了,或者您的網際網路連線出現問題。正在重試...",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎線了,或者您的網際網路連線出現問題。正在重試...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The aggregated statistics are publicly available at {%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.": "裝置識別碼不能為空白。",
"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).": "要輸入在這裡的裝置識別碼可以在其裝置的 \"編輯 > 顯示識別碼\" 對話框找到。空白以及連接符號可不輸入 (省略)",
"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).": "要輸入在這裡的裝置識別碼可以在其裝置的 \"編輯 > 顯示識別碼\" 對話框找到。空白以及連接符號可不輸入 (省略)",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "經過加密的使用資訊報告會每天傳送。報告是用來追蹤常用的平台、資料夾的大小以及應用程式的版本。若傳送的資料集有異動,您會再次看到這個對話框。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "輸入的裝置識別碼似乎無效。它應該為一串包含半形英文字母及數字,並可能會含有空白或連接符號的字串,且長度為 52 或 56 個字元。",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
@@ -211,7 +211,7 @@
"The rate limit must be a non-negative number (0: no limit)": "限制速率必須為非負的數字 (0: 不設限制)",
"The rescan interval must be a non-negative number of seconds.": "重新掃描間隔必須為一個非負數的秒數。",
"They are retried automatically and will be synced when the error is resolved.": "解決間題後,將會自動重試和同步。",
"This Device": "This Device",
"This Device": "本機",
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
"This is a major version upgrade.": "這是一個主要版本更新。",
"Trash Can File Versioning": "Trash Can File Versioning",
@@ -238,5 +238,6 @@
"full documentation": "完整說明文件",
"items": "個項目",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想要分享資料夾 \"{{folder}}\"。",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} 想要分享資料夾 \"{{folderLabel}}\" ({{folder}})",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 想要分享資料夾 \"{{folderlabel}}\" ({{folder}})。"
}
+14 -10
View File
@@ -26,31 +26,36 @@
</head>
<body>
<script src="syncthing/development/logbar.js"></script>
<div ng-if="version.isDevelopmentVersion" ng-include="'syncthing/development/logbar.html'"></div>
<!-- Top bar -->
<nav class="navbar navbar-top navbar-default" role="navigation">
<div class="container">
<span class="navbar-brand"><img class="logo" src="assets/img/logo-horizontal.svg" height="32" width="117"/></span>
<span class="navbar-brand">
<img class="logo hidden-xs" src="assets/img/logo-horizontal.svg" height="32" width="117"/>
<img class="logo hidden visible-xs" src="assets/img/favicon.png" height="32"/>
</span>
<p class="navbar-text hidden-xs" ng-class="{'hidden-sm':upgradeInfo && upgradeInfo.newer}">{{thisDeviceName()}}</p>
<ul class="nav navbar-nav navbar-right">
<li ng-if="upgradeInfo && upgradeInfo.newer">
<li ng-if="upgradeInfo && upgradeInfo.newer" class="upgrade-newer">
<button type="button" class="btn navbar-btn btn-primary btn-sm" ng-click="upgrade()">
<span class="fa fa-arrow-circle-up"></span>&nbsp;<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
</button>
</li>
<li ng-if="upgradeInfo && upgradeInfo.majorNewer">
<li ng-if="upgradeInfo && upgradeInfo.majorNewer" class="upgrade-newer-major">
<button type="button" class="btn navbar-btn btn-danger btn-sm" ng-click="upgradeMajor()">
<span class="fa fa-arrow-circle-up"></span>&nbsp;<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
</button>
</li>
<li class="dropdown" language-select></li>
<li class="hidden-xs">
<li>
<a href="https://docs.syncthing.net/intro/gui.html" target="_blank">
<span class="fa fa-book"></span>&nbsp;<span translate>Help</span>
<span class="fa fa-question-circle"></span>&nbsp;<span class="hidden-xs" translate>Help</span>
</a>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="fa fa-cog"></span>&nbsp;<span translate>Actions</span> <span class="caret"></span></a>
<li class="dropdown action-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="fa fa-cog"></span>&nbsp;<span class="hidden-xs" translate>Actions</span> <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="" ng-click="editSettings()"><span class="fa fa-fw fa-cog"></span>&nbsp;<span translate>Settings</span></a></li>
<li><a href="" ng-click="idDevice(thisDevice())"><span class="fa fa-fw fa-qrcode"></span>&nbsp;<span translate>Show ID</span></a></li>
@@ -169,8 +174,8 @@
<span ng-if="event.data.folderLabel.length == 0" translate translate-value-device="{{ deviceName(findDevice(event.data.device)) }}" translate-value-folder="{{ event.data.folder }}">
{%device%} wants to share folder "{%folder%}".
</span>
<span ng-if="event.data.folderLabel.length != 0" translate translate-value-device="{{ deviceName(findDevice(event.data.device)) }}" translate-value-folder="{{ event.data.folder }}" translate-value-folderLabel="{{ event.data.folderLabel }}">
{%device%} wants to share folder "{%folderLabel%}" ({%folder%}).
<span ng-if="event.data.folderLabel.length != 0" translate translate-value-device="{{ deviceName(findDevice(event.data.device)) }}" translate-value-folder="{{ event.data.folder }}" translate-value-folderlabel="{{ event.data.folderLabel }}">
{%device%} wants to share folder "{%folderlabel%}" ({%folder%}).
</span>
<span translate ng-if="folders[event.data.folder]">Share this folder?</span>
<span translate ng-if="!folders[event.data.folder]">Add new folder?</span>
@@ -599,7 +604,6 @@
<script src="vendor/jquery/jquery-2.2.2.js"></script>
<script src="vendor/angular/angular.js"></script>
<script src="vendor/angular/angular-translate.js"></script>
<script src="vendor/angular/angular-translate-handler-log.js"></script>
<script src="vendor/angular/angular-translate-loader-static-files.js"></script>
<script src="vendor/angular/angular-dirPagination.js"></script>
<script src="vendor/bootstrap/js/bootstrap.js"></script>
-1
View File
@@ -56,7 +56,6 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
prefix: 'assets/lang/lang-',
suffix: '.json'
});
$translateProvider.useMissingTranslationHandlerLog();
LocaleServiceProvider.setAvailableLocales(validLangs);
LocaleServiceProvider.setDefaultLocale('en');
@@ -11,7 +11,7 @@
<p translate>Copyright &copy; 2014-2016 the following Contributors:</p>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Ben Schulz, Caleb Callaway, Lode Hoste, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Andrew Dunham, Antony Male, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Harte, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Lars K.W. Gohlke, Laurent Etiemble, Lord Landon Agahnim, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Ploujnikov, 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, Ben Schulz, Caleb Callaway, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Andrew Dunham, Antony Male, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Harte, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Lars K.W. Gohlke, Laurent Etiemble, Lord Landon Agahnim, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
</div>
</div>
<hr/>
@@ -4,7 +4,7 @@ angular.module('syncthing.core')
return {
restrict: 'EA',
template:
'<a ng-if="visible" href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="true"><span class="fa fa-globe"></span>&nbsp;{{localesNames[currentLocale] || "English"}} <span class="caret"></span></a>'+
'<a ng-if="visible" href="#" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="true"><span class="fa fa-globe"></span><span class="hidden-xs">&nbsp;{{localesNames[currentLocale] || "English"}}</span> <span class="caret"></span></a>'+
'<ul ng-if="visible" class="dropdown-menu">'+
'<li ng-repeat="(i,name) in localesNames" ng-class="{active: i==currentLocale}">'+
'<a href="#" data-ng-click="changeLanguage(i)">{{name}}</a>'+
@@ -97,6 +97,7 @@ angular.module('syncthing.core')
}
$scope.version = data;
$scope.version.isDevelopmentVersion = data.version.indexOf('-')>0;
}).error($scope.emitHTTPError);
$http.get(urlbase + '/svc/report').success(function (data) {
@@ -810,7 +811,7 @@ angular.module('syncthing.core')
};
$scope.deviceName = function (deviceCfg) {
if (typeof deviceCfg === 'undefined') {
if (typeof deviceCfg === 'undefined' || typeof deviceCfg.deviceID === 'undefined') {
return "";
}
if (deviceCfg.name) {
@@ -0,0 +1,13 @@
<div class="dev-top-bar" id="dev-top-bar">
<link href="assets/css/dev.css" rel="stylesheet">
<div class="row">
<div class="col-xs-4"><b>DEV</b></div>
<div id="log" class="col-xs-8">
<span>
JS:
<span class="dev-error fa fa-exclamation-triangle"> errors <span id="log_error">0</span></span>
<span class="dev-warn fa fa-frown-o"> warn <span id="log_warn">0</span></span>
</span>
</div>
</div>
</div>
@@ -0,0 +1,39 @@
'use strict';
function intercept(method, handler) {
var console = window.console;
var original = console[method];
console[method] = function () {
handler(method);
// do sneaky stuff
if (original.apply) {
// Do this for normal browsers
original.apply(console, arguments);
} else {
// Do this for IE
var message = Array.prototype.slice.apply(arguments).join(' ');
original(message);
}
};
}
function handleConsoleCall(type) {
var element = document.querySelector('#log_' + type);
if (element) {
if (!element.classList.contains("hasCount")) {
element.classList.add("hasCount");
}
var devTopBar = document.querySelector('#dev-top-bar');
devTopBar.style.display = 'block';
element.innerHTML = parseInt(element.innerHTML) + 1;
}
}
if (window.console) {
var methods = ['error', 'warn'];
for (var i = 0; i < methods.length; i++) {
intercept(methods[i], handleConsoleCall);
}
}
@@ -40,7 +40,7 @@
<div class="form-group">
<div class="checkbox">
<label>
<input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.upnpEnabled"> <span translate>Enable UPnP</span>
<input id="NATEnabled" type="checkbox" ng-model="tmpOptions.natEnabled"> <span translate>Enable NAT traversal</span>
</label>
</div>
</div>
-1
View File
@@ -3,5 +3,4 @@ The files contained herein are:
- angular 1.5.3
- angular-translate 2.9.0.1
- angular-translate-loader-static-files 2.11.0
- angular-translate-handler-log 2.11.0
- angular-dirPagination 759009c
@@ -1,50 +0,0 @@
/*!
* angular-translate - v2.11.0 - 2016-03-20
*
* Copyright (c) 2016 The angular-translate team, Pascal Precht; Licensed MIT
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module unless amdModuleId is set
define([], function () {
return (factory());
});
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
factory();
}
}(this, function () {
$translateMissingTranslationHandlerLog.$inject = ['$log'];
angular.module('pascalprecht.translate')
/**
* @ngdoc object
* @name pascalprecht.translate.$translateMissingTranslationHandlerLog
* @requires $log
*
* @description
* Uses angular's `$log` service to give a warning when trying to translate a
* translation id which doesn't exist.
*
* @returns {function} Handler function
*/
.factory('$translateMissingTranslationHandlerLog', $translateMissingTranslationHandlerLog);
function $translateMissingTranslationHandlerLog ($log) {
'use strict';
return function (translationId) {
$log.warn('Translation for ' + translationId + ' doesn\'t exist');
};
}
$translateMissingTranslationHandlerLog.displayName = '$translateMissingTranslationHandlerLog';
return 'pascalprecht.translate';
}));
+15 -1
View File
@@ -21,7 +21,7 @@ import (
const (
OldestHandledVersion = 10
CurrentVersion = 12
CurrentVersion = 13
MaxRescanIntervalS = 365 * 24 * 60 * 60
)
@@ -182,6 +182,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
if cfg.Version == 11 {
convertV11V12(cfg)
}
if cfg.Version == 12 {
convertV12V13(cfg)
}
// Build a list of available devices
existingDevices := make(map[protocol.DeviceID]bool)
@@ -235,6 +238,17 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
}
}
func convertV12V13(cfg *Configuration) {
// Not using the ignore cache is the new default. Disable it on existing
// configurations.
cfg.Options.CacheIgnoredFiles = false
cfg.Options.NATEnabled = cfg.Options.DeprecatedUPnPEnabled
cfg.Options.NATLeaseM = cfg.Options.DeprecatedUPnPLeaseM
cfg.Options.NATRenewalM = cfg.Options.DeprecatedUPnPRenewalM
cfg.Options.NATTimeoutS = cfg.Options.DeprecatedUPnPTimeoutS
cfg.Version = 13
}
func convertV11V12(cfg *Configuration) {
// Change listen address schema
for i, addr := range cfg.Options.ListenAddress {
+14 -10
View File
@@ -44,14 +44,14 @@ func TestDefaultValues(t *testing.T) {
RelaysEnabled: true,
RelayReconnectIntervalM: 10,
StartBrowser: true,
UPnPEnabled: true,
UPnPLeaseM: 60,
UPnPRenewalM: 30,
UPnPTimeoutS: 10,
NATEnabled: true,
NATLeaseM: 60,
NATRenewalM: 30,
NATTimeoutS: 10,
RestartOnWakeup: true,
AutoUpgradeIntervalH: 12,
KeepTemporariesH: 24,
CacheIgnoredFiles: true,
CacheIgnoredFiles: false,
ProgressUpdateIntervalS: 5,
SymlinksEnabled: true,
LimitBandwidthInLan: false,
@@ -61,6 +61,8 @@ func TestDefaultValues(t *testing.T) {
URPostInsecurely: false,
ReleasesURL: "https://api.github.com/repos/syncthing/syncthing/releases?per_page=30",
AlwaysLocalNets: []string{},
OverwriteNames: false,
TempIndexMinBlocks: 10,
}
cfg := New(device1)
@@ -173,14 +175,14 @@ func TestOverriddenValues(t *testing.T) {
RelaysEnabled: false,
RelayReconnectIntervalM: 20,
StartBrowser: false,
UPnPEnabled: false,
UPnPLeaseM: 90,
UPnPRenewalM: 15,
UPnPTimeoutS: 15,
NATEnabled: false,
NATLeaseM: 90,
NATRenewalM: 15,
NATTimeoutS: 15,
RestartOnWakeup: false,
AutoUpgradeIntervalH: 24,
KeepTemporariesH: 48,
CacheIgnoredFiles: false,
CacheIgnoredFiles: true,
ProgressUpdateIntervalS: 10,
SymlinksEnabled: false,
LimitBandwidthInLan: true,
@@ -190,6 +192,8 @@ func TestOverriddenValues(t *testing.T) {
URPostInsecurely: true,
ReleasesURL: "https://localhost/releases",
AlwaysLocalNets: []string{},
OverwriteNames: true,
TempIndexMinBlocks: 100,
}
cfg, err := Load("testdata/overridenvalues.xml", device1)
+1
View File
@@ -37,6 +37,7 @@ type FolderConfiguration struct {
PullerPauseS int `xml:"pullerPauseS" json:"pullerPauseS"`
MaxConflicts int `xml:"maxConflicts" json:"maxConflicts"`
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
+12 -5
View File
@@ -20,10 +20,10 @@ type OptionsConfiguration struct {
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"`
UPnPRenewalM int `xml:"upnpRenewalMinutes" json:"upnpRenewalMinutes" default:"30"`
UPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"upnpTimeoutSeconds" default:"10"`
NATEnabled bool `xml:"natEnabled" json:"natEnabled" default:"true"`
NATLeaseM int `xml:"natLeaseMinutes" json:"natLeaseMinutes" default:"60"`
NATRenewalM int `xml:"natRenewalMinutes" json:"natRenewalMinutes" default:"30"`
NATTimeoutS int `xml:"natTimeoutSeconds" json:"natTimeoutSeconds" default:"10"`
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
@@ -32,13 +32,20 @@ type OptionsConfiguration struct {
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12"` // 0 for off
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"true"`
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"false"`
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
SymlinksEnabled bool `xml:"symlinksEnabled" json:"symlinksEnabled" default:"true"`
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
MinHomeDiskFreePct float64 `xml:"minHomeDiskFreePct" json:"minHomeDiskFreePct" default:"1"`
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=30"`
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
OverwriteNames bool `xml:"overwriteNames" json:"overwriteNames" default:"false"`
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
DeprecatedUPnPEnabled bool `xml:"upnpEnabled"`
DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes"`
DeprecatedUPnPRenewalM int `xml:"upnpRenewalMinutes"`
DeprecatedUPnPTimeoutS int `xml:"upnpTimeoutSeconds"`
}
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
+9 -7
View File
@@ -1,6 +1,6 @@
<configuration version="11">
<configuration version="13">
<options>
<listenAddress>:23000</listenAddress>
<listenAddress>tcp://:23000</listenAddress>
<allowDelete>false</allowDelete>
<globalAnnounceServer>udp4://syncthing.nym.se:22026</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
@@ -17,14 +17,14 @@
<relayReconnectIntervalM>20</relayReconnectIntervalM>
<relayWithoutGlobalAnn>true</relayWithoutGlobalAnn>
<startBrowser>false</startBrowser>
<upnpEnabled>false</upnpEnabled>
<upnpLeaseMinutes>90</upnpLeaseMinutes>
<upnpRenewalMinutes>15</upnpRenewalMinutes>
<upnpTimeoutSeconds>15</upnpTimeoutSeconds>
<natEnabled>false</natEnabled>
<natLeaseMinutes>90</natLeaseMinutes>
<natRenewalMinutes>15</natRenewalMinutes>
<natTimeoutSeconds>15</natTimeoutSeconds>
<restartOnWakeup>false</restartOnWakeup>
<autoUpgradeIntervalH>24</autoUpgradeIntervalH>
<keepTemporariesH>48</keepTemporariesH>
<cacheIgnoredFiles>false</cacheIgnoredFiles>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
<progressUpdateIntervalS>10</progressUpdateIntervalS>
<symlinksEnabled>false</symlinksEnabled>
<limitBandwidthInLan>true</limitBandwidthInLan>
@@ -34,5 +34,7 @@
<urInitialDelayS>800</urInitialDelayS>
<urPostInsecurely>true</urPostInsecurely>
<releasesURL>https://localhost/releases</releasesURL>
<overwriteNames>true</overwriteNames>
<tempIndexMinBlocks>100</tempIndexMinBlocks>
</options>
</configuration>
+14
View File
@@ -0,0 +1,14 @@
<configuration version="13">
<folder id="test" path="testdata" ro="true" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
<minDiskFreePct>1</minDiskFreePct>
<maxConflicts>-1</maxConflicts>
</folder>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
<address>tcp://a</address>
</device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
<address>tcp://b</address>
</device>
</configuration>
+27 -14
View File
@@ -19,11 +19,12 @@ import (
"github.com/juju/ratelimit"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/relay/client"
"github.com/syncthing/syncthing/lib/upnp"
"github.com/syncthing/syncthing/lib/util"
"github.com/thejerf/suture"
@@ -56,7 +57,7 @@ type Service struct {
tlsCfg *tls.Config
discoverer discover.Finder
conns chan model.IntermediateConnection
upnpService *upnp.Service
mappings []*nat.Mapping
relayService relay.Service
bepProtocolName string
tlsDefaultCommonName string
@@ -71,7 +72,7 @@ type Service struct {
relaysEnabled bool
}
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, upnpService *upnp.Service,
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, mappings []*nat.Mapping,
relayService relay.Service, bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) *Service {
service := &Service{
Supervisor: suture.NewSimple("connections.Service"),
@@ -80,7 +81,7 @@ func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model
model: mdl,
tlsCfg: tlsCfg,
discoverer: discoverer,
upnpService: upnpService,
mappings: mappings,
relayService: relayService,
conns: make(chan model.IntermediateConnection),
bepProtocolName: bepProtocolName,
@@ -140,6 +141,17 @@ func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model
service.Add(serviceFunc(service.acceptRelayConns))
}
for _, mapping := range mappings {
mapping.OnChanged(func(m *nat.Mapping, added, removed []nat.Address) {
events.Default.Log(events.ExternalPortMappingChanged, map[string]interface{}{
"protocol": m.Protocol(),
"local": m.Address().String(),
"added": added,
"removed": removed,
})
})
}
return service
}
@@ -256,9 +268,9 @@ next:
s.mut.Lock()
s.model.AddConnection(model.Connection{
c,
protoConn,
c.Type,
Conn: c,
Connection: protoConn,
Type: c.Type,
}, hello)
s.connType[remoteID] = c.Type
s.mut.Unlock()
@@ -307,7 +319,8 @@ func (s *Service) connect() {
s.model.Close(deviceID, fmt.Errorf("switching connections"))
}
s.conns <- model.IntermediateConnection{
conn, model.ConnectionTypeDirectDial,
Conn: conn,
Type: model.ConnectionTypeDirectDial,
}
continue nextDevice
}
@@ -338,7 +351,8 @@ func (s *Service) connect() {
if conn := s.connectViaRelay(deviceID, addr); conn != nil {
l.Debugln("Connecting to", deviceID, "via", addr, "succeeded")
s.conns <- model.IntermediateConnection{
conn, model.ConnectionTypeRelayDial,
Conn: conn,
Type: model.ConnectionTypeRelayDial,
}
continue nextDevice
}
@@ -531,11 +545,10 @@ func (s *Service) addresses(includePrivateIPV4 bool) []string {
}
}
// Get an external port mapping from the upnpService, if it has one. If so,
// add it as another unspecified address.
if s.upnpService != nil {
if port := s.upnpService.ExternalPort(); port != 0 {
addrs = append(addrs, fmt.Sprintf("tcp://:%d", port))
// Add addresses provided by the mappings from the NAT service.
for _, mapping := range s.mappings {
for _, addr := range mapping.ExternalAddresses() {
addrs = append(addrs, fmt.Sprintf("tcp://%s", addr))
}
}
+2 -1
View File
@@ -91,7 +91,8 @@ func makeTCPListener(network string) ListenerFactory {
}
conns <- model.IntermediateConnection{
tc, model.ConnectionTypeDirectAccept,
Conn: tc,
Type: model.ConnectionTypeDirectAccept,
}
}
}
+1 -1
View File
@@ -100,7 +100,7 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
var oldFile protocol.FileInfo
var hasOldFile bool
// Remove the device from the current version list
if svl != nil {
if len(svl) != 0 {
err = fl.UnmarshalXDR(svl)
if err != nil {
panic(err)
-79
View File
@@ -1,79 +0,0 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package fnmatch
import (
"path/filepath"
"regexp"
"runtime"
"strings"
)
const (
NoEscape = (1 << iota)
PathName
CaseFold
)
func Convert(pattern string, flags int) (*regexp.Regexp, error) {
any := "."
switch runtime.GOOS {
case "windows":
flags |= NoEscape | CaseFold
pattern = filepath.FromSlash(pattern)
if flags&PathName != 0 {
any = `[^\\]`
}
case "darwin":
flags |= CaseFold
fallthrough
default:
if flags&PathName != 0 {
any = `[^/]`
}
}
if flags&NoEscape != 0 {
pattern = strings.Replace(pattern, `\`, `\\`, -1)
} else {
pattern = strings.Replace(pattern, `\*`, "[:escapedstar:]", -1)
pattern = strings.Replace(pattern, `\?`, "[:escapedques:]", -1)
pattern = strings.Replace(pattern, `\.`, "[:escapeddot:]", -1)
}
// Characters that are special in regexps but not in glob, must be
// escaped.
for _, char := range []string{`.`, `+`, `$`, `^`, `(`, `)`, `|`} {
pattern = strings.Replace(pattern, char, `\`+char, -1)
}
pattern = strings.Replace(pattern, `**`, `[:doublestar:]`, -1)
pattern = strings.Replace(pattern, `*`, any+`*`, -1)
pattern = strings.Replace(pattern, `[:doublestar:]`, `.*`, -1)
pattern = strings.Replace(pattern, `?`, any, -1)
pattern = strings.Replace(pattern, `[:escapedstar:]`, `\*`, -1)
pattern = strings.Replace(pattern, `[:escapedques:]`, `\?`, -1)
pattern = strings.Replace(pattern, `[:escapeddot:]`, `\.`, -1)
pattern = `^` + pattern + `$`
if flags&CaseFold != 0 {
pattern = `(?i)` + pattern
}
return regexp.Compile(pattern)
}
// Match matches the pattern against the string, with the given flags, and
// returns true if the match is successful.
func Match(pattern, s string, flags int) (bool, error) {
exp, err := Convert(pattern, flags)
if err != nil {
return false, err
}
return exp.MatchString(s), nil
}
-98
View File
@@ -1,98 +0,0 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package fnmatch
import (
"path/filepath"
"runtime"
"testing"
)
type testcase struct {
pat string
name string
flags int
match bool
}
var testcases = []testcase{
{"", "", 0, true},
{"*", "", 0, true},
{"*", "foo", 0, true},
{"*", "bar", 0, true},
{"*", "*", 0, true},
{"**", "f", 0, true},
{"**", "foo.txt", 0, true},
{"*.*", "foo.txt", 0, true},
{"foo*.txt", "foobar.txt", 0, true},
{"foo.txt", "foo.txt", 0, true},
{"foo.txt", "bar/foo.txt", 0, false},
{"*/foo.txt", "bar/foo.txt", 0, true},
{"f?o.txt", "foo.txt", 0, true},
{"f?o.txt", "fooo.txt", 0, false},
{"f[ab]o.txt", "foo.txt", 0, false},
{"f[ab]o.txt", "fao.txt", 0, true},
{"f[ab]o.txt", "fbo.txt", 0, true},
{"f[ab]o.txt", "fco.txt", 0, false},
{"f[ab]o.txt", "fabo.txt", 0, false},
{"f[ab]o.txt", "f[ab]o.txt", 0, false},
{"f\\[ab\\]o.txt", "f[ab]o.txt", NoEscape, false},
{"*foo.txt", "bar/foo.txt", 0, true},
{"*foo.txt", "bar/foo.txt", PathName, false},
{"*/foo.txt", "bar/foo.txt", 0, true},
{"*/foo.txt", "bar/foo.txt", PathName, true},
{"*/foo.txt", "bar/baz/foo.txt", 0, true},
{"*/foo.txt", "bar/baz/foo.txt", PathName, false},
{"**/foo.txt", "bar/baz/foo.txt", 0, true},
{"**/foo.txt", "bar/baz/foo.txt", PathName, true},
{"foo.txt", "foo.TXT", CaseFold, true},
// These characters are literals in glob, but not in regexp.
{"hey$hello", "hey$hello", 0, true},
{"hey^hello", "hey^hello", 0, true},
{"hey{hello", "hey{hello", 0, true},
{"hey}hello", "hey}hello", 0, true},
{"hey(hello", "hey(hello", 0, true},
{"hey)hello", "hey)hello", 0, true},
{"hey|hello", "hey|hello", 0, true},
{"hey|hello", "hey|other", 0, false},
}
func TestMatch(t *testing.T) {
switch runtime.GOOS {
case "windows":
testcases = append(testcases, testcase{"foo.txt", "foo.TXT", 0, true})
case "darwin":
testcases = append(testcases, testcase{"foo.txt", "foo.TXT", 0, true})
fallthrough
default:
testcases = append(testcases, testcase{"f\\[ab\\]o.txt", "f[ab]o.txt", 0, true})
testcases = append(testcases, testcase{"foo\\.txt", "foo.txt", 0, true})
testcases = append(testcases, testcase{"foo\\*.txt", "foo*.txt", 0, true})
testcases = append(testcases, testcase{"foo\\.txt", "foo.txt", NoEscape, false})
testcases = append(testcases, testcase{"f\\\\\\[ab\\\\\\]o.txt", "f\\[ab\\]o.txt", 0, true})
}
for _, tc := range testcases {
if m, err := Match(tc.pat, filepath.FromSlash(tc.name), tc.flags); m != tc.match {
if err != nil {
t.Error(err)
} else {
t.Errorf("Match(%q, %q, %d) != %v", tc.pat, tc.name, tc.flags, tc.match)
}
}
}
}
func TestInvalid(t *testing.T) {
if _, err := Match("foo[bar", "...", 0); err == nil {
t.Error("Unexpected nil error")
}
}
+8 -8
View File
@@ -14,7 +14,7 @@ type cache struct {
}
type cacheEntry struct {
value bool
result Result
access time.Time
}
@@ -33,17 +33,17 @@ func (c *cache) clean(d time.Duration) {
}
}
func (c *cache) get(key string) (result, ok bool) {
res, ok := c.entries[key]
func (c *cache) get(key string) (Result, bool) {
entry, ok := c.entries[key]
if ok {
res.access = time.Now()
c.entries[key] = res
entry.access = time.Now()
c.entries[key] = entry
}
return res.value, ok
return entry.result, ok
}
func (c *cache) set(key string, val bool) {
c.entries[key] = cacheEntry{val, time.Now()}
func (c *cache) set(key string, result Result) {
c.entries[key] = cacheEntry{result, time.Now()}
}
func (c *cache) len() int {
+7 -7
View File
@@ -15,22 +15,22 @@ func TestCache(t *testing.T) {
c := newCache(nil)
res, ok := c.get("nonexistent")
if res != false || ok != false {
if res.IsIgnored() || res.IsDeletable() || ok != false {
t.Errorf("res %v, ok %v for nonexistent item", res, ok)
}
// Set and check some items
c.set("true", true)
c.set("false", false)
c.set("true", Result{true, true})
c.set("false", Result{false, false})
res, ok = c.get("true")
if res != true || ok != true {
if !res.IsIgnored() || !res.IsDeletable() || ok != true {
t.Errorf("res %v, ok %v for true item", res, ok)
}
res, ok = c.get("false")
if res != false || ok != true {
if res.IsIgnored() || res.IsDeletable() || ok != true {
t.Errorf("res %v, ok %v for false item", res, ok)
}
@@ -41,12 +41,12 @@ func TestCache(t *testing.T) {
// Same values should exist
res, ok = c.get("true")
if res != true || ok != true {
if !res.IsIgnored() || !res.IsDeletable() || ok != true {
t.Errorf("res %v, ok %v for true item", res, ok)
}
res, ok = c.get("false")
if res != false || ok != true {
if res.IsIgnored() || res.IsDeletable() || ok != true {
t.Errorf("res %v, ok %v for false item", res, ok)
}
+105 -35
View File
@@ -14,24 +14,52 @@ import (
"io"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
"github.com/syncthing/syncthing/lib/fnmatch"
"github.com/gobwas/glob"
"github.com/syncthing/syncthing/lib/sync"
)
var notMatched = Result{
include: false,
deletable: false,
}
type Pattern struct {
match *regexp.Regexp
include bool
pattern string
match glob.Glob
include bool
foldCase bool
deletable bool
}
func (p Pattern) String() string {
if p.include {
return p.match.String()
ret := p.pattern
if !p.include {
ret = "!" + ret
}
return "(?exclude)" + p.match.String()
if p.foldCase {
ret = "(?i)" + ret
}
if p.deletable {
ret = "(?d)" + ret
}
return ret
}
type Result struct {
include bool
deletable bool
}
func (r Result) IsIgnored() bool {
return r.include
}
func (r Result) IsDeletable() bool {
return r.include && r.deletable
}
type Matcher struct {
@@ -93,16 +121,16 @@ func (m *Matcher) Parse(r io.Reader, file string) error {
return err
}
func (m *Matcher) Match(file string) (result bool) {
func (m *Matcher) Match(file string) (result Result) {
if m == nil {
return false
return notMatched
}
m.mut.Lock()
defer m.mut.Unlock()
if len(m.patterns) == 0 {
return false
return notMatched
}
if m.matches != nil {
@@ -119,17 +147,34 @@ func (m *Matcher) Match(file string) (result bool) {
}
// Check all the patterns for a match.
file = filepath.ToSlash(file)
var lowercaseFile string
for _, pattern := range m.patterns {
if pattern.match.MatchString(file) {
return pattern.include
if pattern.foldCase {
if lowercaseFile == "" {
lowercaseFile = strings.ToLower(file)
}
if pattern.match.Match(lowercaseFile) {
return Result{
pattern.include,
pattern.deletable,
}
}
} else {
if pattern.match.Match(file) {
return Result{
pattern.include,
pattern.deletable,
}
}
}
}
// Default to false.
return false
return notMatched
}
// Patterns return a list of the loaded regexp patterns, as strings
// Patterns return a list of the loaded patterns, as they've been parsed
func (m *Matcher) Patterns() []string {
if m == nil {
return nil
@@ -200,38 +245,60 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
var patterns []Pattern
addPattern := func(line string) error {
include := true
if strings.HasPrefix(line, "!") {
line = line[1:]
include = false
pattern := Pattern{
pattern: line,
include: true,
foldCase: runtime.GOOS == "darwin" || runtime.GOOS == "windows",
}
flags := fnmatch.PathName
if strings.HasPrefix(line, "(?i)") {
line = line[4:]
flags |= fnmatch.CaseFold
// Allow prefixes to be specified in any order, but only once.
var seenPrefix [3]bool
for {
if strings.HasPrefix(line, "!") && !seenPrefix[0] {
seenPrefix[0] = true
line = line[1:]
pattern.include = false
} else if strings.HasPrefix(line, "(?i)") && !seenPrefix[1] {
seenPrefix[1] = true
pattern.foldCase = true
line = line[4:]
} else if strings.HasPrefix(line, "(?d)") && !seenPrefix[2] {
seenPrefix[2] = true
pattern.deletable = true
line = line[4:]
} else {
break
}
}
if pattern.foldCase {
line = strings.ToLower(line)
}
var err error
if strings.HasPrefix(line, "/") {
// Pattern is rooted in the current dir only
exp, err := fnmatch.Convert(line[1:], flags)
pattern.match, err = glob.Compile(line[1:])
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
}
patterns = append(patterns, Pattern{exp, include})
patterns = append(patterns, pattern)
} else if strings.HasPrefix(line, "**/") {
// Add the pattern as is, and without **/ so it matches in current dir
exp, err := fnmatch.Convert(line, flags)
pattern.match, err = glob.Compile(line)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
}
patterns = append(patterns, Pattern{exp, include})
patterns = append(patterns, pattern)
exp, err = fnmatch.Convert(line[3:], flags)
line = line[3:]
pattern.pattern = line
pattern.match, err = glob.Compile(line)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
}
patterns = append(patterns, Pattern{exp, include})
patterns = append(patterns, pattern)
} else if strings.HasPrefix(line, "#include ") {
includeRel := line[len("#include "):]
includeFile := filepath.Join(filepath.Dir(currentFile), includeRel)
@@ -243,17 +310,19 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
} else {
// Path name or pattern, add it so it matches files both in
// current directory and subdirs.
exp, err := fnmatch.Convert(line, flags)
pattern.match, err = glob.Compile(line)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
}
patterns = append(patterns, Pattern{exp, include})
patterns = append(patterns, pattern)
exp, err = fnmatch.Convert("**/"+line, flags)
line := "**/" + line
pattern.pattern = line
pattern.match, err = glob.Compile(line)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
}
patterns = append(patterns, Pattern{exp, include})
patterns = append(patterns, pattern)
}
return nil
}
@@ -267,15 +336,16 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
continue
case strings.HasPrefix(line, "//"):
continue
}
line = filepath.ToSlash(line)
switch {
case strings.HasPrefix(line, "#"):
err = addPattern(line)
case strings.HasSuffix(line, "/**"):
err = addPattern(line)
case strings.HasSuffix(line, "/"):
err = addPattern(line)
if err == nil {
err = addPattern(line + "**")
}
default:
err = addPattern(line)
if err == nil {
+141 -15
View File
@@ -8,6 +8,7 @@ package ignore
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
@@ -52,7 +53,7 @@ func TestIgnore(t *testing.T) {
}
for i, tc := range tests {
if r := pats.Match(tc.f); r != tc.r {
if r := pats.Match(tc.f); r.IsIgnored() != tc.r {
t.Errorf("Incorrect ignoreFile() #%d (%s); E: %v, A: %v", i, tc.f, tc.r, r)
}
}
@@ -90,12 +91,90 @@ func TestExcludes(t *testing.T) {
}
for _, tc := range tests {
if r := pats.Match(tc.f); r != tc.r {
if r := pats.Match(tc.f); r.IsIgnored() != tc.r {
t.Errorf("Incorrect match for %s: %v != %v", tc.f, r, tc.r)
}
}
}
func TestFlagOrder(t *testing.T) {
stignore := `
## Ok cases
(?i)(?d)!ign1
(?d)(?i)!ign2
(?i)!(?d)ign3
(?d)!(?i)ign4
!(?i)(?d)ign5
!(?d)(?i)ign6
## Bad cases
!!(?i)(?d)ign7
(?i)(?i)(?d)ign8
(?i)(?d)(?d)!ign9
(?d)(?d)!ign10
`
pats := New(true)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
for i := 1; i < 7; i++ {
pat := fmt.Sprintf("ign%d", i)
if r := pats.Match(pat); r.IsIgnored() || r.IsDeletable() {
t.Errorf("incorrect %s", pat)
}
}
for i := 7; i < 10; i++ {
pat := fmt.Sprintf("ign%d", i)
if r := pats.Match(pat); r.IsDeletable() {
t.Errorf("incorrect %s", pat)
}
}
if r := pats.Match("(?d)!ign10"); !r.IsIgnored() {
t.Errorf("incorrect")
}
}
func TestDeletables(t *testing.T) {
stignore := `
(?d)ign1
(?d)(?i)ign2
(?i)(?d)ign3
!(?d)ign4
!ign5
!(?i)(?d)ign6
ign7
(?i)ign8
`
pats := New(true)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
var tests = []struct {
f string
i bool
d bool
}{
{"ign1", true, true},
{"ign2", true, true},
{"ign3", true, true},
{"ign4", false, false},
{"ign5", false, false},
{"ign6", false, false},
{"ign7", true, false},
{"ign8", true, false},
}
for _, tc := range tests {
if r := pats.Match(tc.f); r.IsIgnored() != tc.i || r.IsDeletable() != tc.d {
t.Errorf("Incorrect match for %s: %v != Result{%t, %t}", tc.f, r, tc.i, tc.d)
}
}
}
func TestBadPatterns(t *testing.T) {
var badPatterns = []string{
"[",
@@ -132,13 +211,13 @@ func TestCaseSensitivity(t *testing.T) {
}
for _, tc := range match {
if !ign.Match(tc) {
if !ign.Match(tc).IsIgnored() {
t.Errorf("Incorrect match for %q: should be matched", tc)
}
}
for _, tc := range dontMatch {
if ign.Match(tc) {
if ign.Match(tc).IsIgnored() {
t.Errorf("Incorrect match for %q: should not be matched", tc)
}
}
@@ -177,10 +256,6 @@ func TestCaching(t *testing.T) {
t.Fatal("Expected empty cache")
}
if len(pats.patterns) != 4 {
t.Fatal("Incorrect number of patterns loaded", len(pats.patterns), "!=", 4)
}
// Cache some outcomes
for _, letter := range []string{"a", "b", "x", "y"} {
@@ -281,7 +356,7 @@ func TestCommentsAndBlankLines(t *testing.T) {
}
}
var result bool
var result Result
func BenchmarkMatch(b *testing.B) {
stignore := `
@@ -385,13 +460,13 @@ func TestCacheReload(t *testing.T) {
// Verify that both are ignored
if !pats.Match("f1") {
if !pats.Match("f1").IsIgnored() {
t.Error("Unexpected non-match for f1")
}
if !pats.Match("f2") {
if !pats.Match("f2").IsIgnored() {
t.Error("Unexpected non-match for f2")
}
if pats.Match("f3") {
if pats.Match("f3").IsIgnored() {
t.Error("Unexpected match for f3")
}
@@ -417,13 +492,13 @@ func TestCacheReload(t *testing.T) {
// Verify that the new patterns are in effect
if !pats.Match("f1") {
if !pats.Match("f1").IsIgnored() {
t.Error("Unexpected non-match for f1")
}
if pats.Match("f2") {
if pats.Match("f2").IsIgnored() {
t.Error("Unexpected match for f2")
}
if !pats.Match("f3") {
if !pats.Match("f3").IsIgnored() {
t.Error("Unexpected non-match for f3")
}
}
@@ -509,3 +584,54 @@ func TestHashOfEmpty(t *testing.T) {
t.Error("there are more than zero patterns")
}
}
func TestWindowsPatterns(t *testing.T) {
// We should accept patterns as both a/b and a\b and match that against
// both kinds of slash as well.
if runtime.GOOS != "windows" {
t.Skip("Windows specific test")
return
}
stignore := `
a/b
c\d
`
pats := New(true)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
tests := []string{`a\b`, `c\d`}
for _, pat := range tests {
if !pats.Match(pat).IsIgnored() {
t.Errorf("Should match %s", pat)
}
}
}
func TestAutomaticCaseInsensitivity(t *testing.T) {
// We should do case insensitive matching by default on some platforms.
if runtime.GOOS != "windows" && runtime.GOOS != "darwin" {
t.Skip("Windows/Mac specific test")
return
}
stignore := `
A/B
c/d
`
pats := New(true)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
tests := []string{`a/B`, `C/d`}
for _, pat := range tests {
if !pats.Match(pat).IsIgnored() {
t.Errorf("Should match %s", pat)
}
}
}
+12 -10
View File
@@ -26,28 +26,30 @@ func newDeviceActivity() *deviceActivity {
}
}
func (m *deviceActivity) leastBusy(availability []protocol.DeviceID) protocol.DeviceID {
func (m *deviceActivity) leastBusy(availability []Availability) (Availability, bool) {
m.mut.Lock()
low := 2<<30 - 1
var selected protocol.DeviceID
for _, device := range availability {
if usage := m.act[device]; usage < low {
found := false
var selected Availability
for _, info := range availability {
if usage := m.act[info.ID]; usage < low {
low = usage
selected = device
selected = info
found = true
}
}
m.mut.Unlock()
return selected
return selected, found
}
func (m *deviceActivity) using(device protocol.DeviceID) {
func (m *deviceActivity) using(availability Availability) {
m.mut.Lock()
m.act[device]++
m.act[availability.ID]++
m.mut.Unlock()
}
func (m *deviceActivity) done(device protocol.DeviceID) {
func (m *deviceActivity) done(availability Availability) {
m.mut.Lock()
m.act[device]--
m.act[availability.ID]--
m.mut.Unlock()
}
+18 -16
View File
@@ -13,46 +13,48 @@ import (
)
func TestDeviceActivity(t *testing.T) {
n0 := protocol.DeviceID([32]byte{1, 2, 3, 4})
n1 := protocol.DeviceID([32]byte{5, 6, 7, 8})
n2 := protocol.DeviceID([32]byte{9, 10, 11, 12})
devices := []protocol.DeviceID{n0, n1, n2}
n0 := Availability{protocol.DeviceID([32]byte{1, 2, 3, 4}), false}
n1 := Availability{protocol.DeviceID([32]byte{5, 6, 7, 8}), true}
n2 := Availability{protocol.DeviceID([32]byte{9, 10, 11, 12}), false}
devices := []Availability{n0, n1, n2}
na := newDeviceActivity()
if lb := na.leastBusy(devices); lb != n0 {
if lb, ok := na.leastBusy(devices); !ok || lb != n0 {
t.Errorf("Least busy device should be n0 (%v) not %v", n0, lb)
}
if lb := na.leastBusy(devices); lb != n0 {
if lb, ok := na.leastBusy(devices); !ok || lb != n0 {
t.Errorf("Least busy device should still be n0 (%v) not %v", n0, lb)
}
na.using(na.leastBusy(devices))
if lb := na.leastBusy(devices); lb != n1 {
lb, _ := na.leastBusy(devices)
na.using(lb)
if lb, ok := na.leastBusy(devices); !ok || lb != n1 {
t.Errorf("Least busy device should be n1 (%v) not %v", n1, lb)
}
na.using(na.leastBusy(devices))
if lb := na.leastBusy(devices); lb != n2 {
lb, _ = na.leastBusy(devices)
na.using(lb)
if lb, ok := na.leastBusy(devices); !ok || lb != n2 {
t.Errorf("Least busy device should be n2 (%v) not %v", n2, lb)
}
na.using(na.leastBusy(devices))
if lb := na.leastBusy(devices); lb != n0 {
lb, _ = na.leastBusy(devices)
na.using(lb)
if lb, ok := na.leastBusy(devices); !ok || lb != n0 {
t.Errorf("Least busy device should be n0 (%v) not %v", n0, lb)
}
na.done(n1)
if lb := na.leastBusy(devices); lb != n1 {
if lb, ok := na.leastBusy(devices); !ok || lb != n1 {
t.Errorf("Least busy device should be n1 (%v) not %v", n1, lb)
}
na.done(n2)
if lb := na.leastBusy(devices); lb != n1 {
if lb, ok := na.leastBusy(devices); !ok || lb != n1 {
t.Errorf("Least busy device should still be n1 (%v) not %v", n1, lb)
}
na.done(n0)
if lb := na.leastBusy(devices); lb != n0 {
if lb, ok := na.leastBusy(devices); !ok || lb != n0 {
t.Errorf("Least busy device should be n0 (%v) not %v", n0, lb)
}
}
+156
View File
@@ -0,0 +1,156 @@
// 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 model
import (
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
)
// deviceFolderFileDownloadState holds current download state of a file that
// a remote device has advertised. blockIndexes represends indexes within
// FileInfo.Blocks that the remote device already has, and version represents
// the version of the file that the remote device is downloading.
type deviceFolderFileDownloadState struct {
blockIndexes []int32
version protocol.Vector
}
// deviceFolderDownloadState holds current download state of all files that
// a remote device is currently downloading in a specific folder.
type deviceFolderDownloadState struct {
mut sync.RWMutex
files map[string]deviceFolderFileDownloadState
numberOfBlocksInProgress int
}
// Has returns wether a block at that specific index, and that specific version of the file
// is currently available on the remote device for pulling from a temporary file.
func (p *deviceFolderDownloadState) Has(file string, version protocol.Vector, index int32) bool {
p.mut.RLock()
defer p.mut.RUnlock()
local, ok := p.files[file]
if !ok || !local.version.Equal(version) {
return false
}
for _, existingIndex := range local.blockIndexes {
if existingIndex == index {
return true
}
}
return false
}
// Update updates internal state of what has been downloaded into the temporary
// files by the remote device for this specific folder.
func (p *deviceFolderDownloadState) Update(updates []protocol.FileDownloadProgressUpdate) {
p.mut.Lock()
defer p.mut.Unlock()
for _, update := range updates {
local, ok := p.files[update.Name]
if update.UpdateType == protocol.UpdateTypeForget && ok && local.version.Equal(update.Version) {
p.numberOfBlocksInProgress -= len(local.blockIndexes)
delete(p.files, update.Name)
} else if update.UpdateType == protocol.UpdateTypeAppend {
if !ok {
local = deviceFolderFileDownloadState{
blockIndexes: update.BlockIndexes,
version: update.Version,
}
} else if !local.version.Equal(update.Version) {
p.numberOfBlocksInProgress -= len(local.blockIndexes)
local.blockIndexes = append(local.blockIndexes[:0], update.BlockIndexes...)
local.version = update.Version
} else {
local.blockIndexes = append(local.blockIndexes, update.BlockIndexes...)
}
p.files[update.Name] = local
p.numberOfBlocksInProgress += len(update.BlockIndexes)
}
}
}
// NumberOfBlocksInProgress returns the number of blocks the device has downloaded
// for a specific folder.
func (p *deviceFolderDownloadState) NumberOfBlocksInProgress() int {
p.mut.RLock()
n := p.numberOfBlocksInProgress
p.mut.RUnlock()
return n
}
// deviceDownloadState represents the state of all in progress downloads
// for all folders of a specific device.
type deviceDownloadState struct {
mut sync.RWMutex
folders map[string]*deviceFolderDownloadState
numberOfBlocksInProgress int
}
// Update updates internal state of what has been downloaded into the temporary
// files by the remote device for this specific folder.
func (t *deviceDownloadState) Update(folder string, updates []protocol.FileDownloadProgressUpdate) {
t.mut.RLock()
f, ok := t.folders[folder]
t.mut.RUnlock()
if !ok {
f = &deviceFolderDownloadState{
mut: sync.NewRWMutex(),
files: make(map[string]deviceFolderFileDownloadState),
}
t.mut.Lock()
t.folders[folder] = f
t.mut.Unlock()
}
f.Update(updates)
}
// Has returns wether block at that specific index, and that specific version of the file
// is currently available on the remote device for pulling from a temporary file.
func (t *deviceDownloadState) Has(folder, file string, version protocol.Vector, index int32) bool {
if t == nil {
return false
}
t.mut.RLock()
f, ok := t.folders[folder]
t.mut.RUnlock()
if !ok {
return false
}
return f.Has(file, version, index)
}
// NumberOfBlocksInProgress returns the number of blocks the device has downloaded
// for all folders.
func (t *deviceDownloadState) NumberOfBlocksInProgress() int {
if t == nil {
return 0
}
n := 0
t.mut.RLock()
for _, folder := range t.folders {
n += folder.NumberOfBlocksInProgress()
}
t.mut.RUnlock()
return n
}
func newDeviceDownloadState() *deviceDownloadState {
return &deviceDownloadState{
mut: sync.NewRWMutex(),
folders: make(map[string]*deviceFolderDownloadState),
}
}
+111
View File
@@ -0,0 +1,111 @@
// Copyright (C) 2016 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 model
import (
"testing"
"github.com/syncthing/syncthing/lib/protocol"
)
func TestDeviceDownloadState(t *testing.T) {
v1 := (protocol.Vector{}).Update(0)
v2 := (protocol.Vector{}).Update(1)
// file 1 version 1 part 1
f1v1p1 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f1", Version: v1, BlockIndexes: []int32{0, 1, 2}}
f1v1p2 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f1", Version: v1, BlockIndexes: []int32{3, 4, 5}}
f1v1del := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeForget, Name: "f1", Version: v1, BlockIndexes: nil}
f1v2p1 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f1", Version: v2, BlockIndexes: []int32{10, 11, 12}}
f1v2p2 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f1", Version: v2, BlockIndexes: []int32{13, 14, 15}}
f1v2del := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeForget, Name: "f1", Version: v2, BlockIndexes: nil}
f2v1p1 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f2", Version: v1, BlockIndexes: []int32{20, 21, 22}}
f2v1p2 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f2", Version: v1, BlockIndexes: []int32{23, 24, 25}}
f2v1del := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeForget, Name: "f2", Version: v1, BlockIndexes: nil}
tests := []struct {
updates []protocol.FileDownloadProgressUpdate
shouldHaveIndexesFrom []protocol.FileDownloadProgressUpdate
shouldNotHaveIndexesFrom []protocol.FileDownloadProgressUpdate
}{
{ //1
[]protocol.FileDownloadProgressUpdate{f1v1p1},
[]protocol.FileDownloadProgressUpdate{f1v1p1},
[]protocol.FileDownloadProgressUpdate{f1v1p2, f1v2p1, f1v2p2},
},
{ //2
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2},
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2},
[]protocol.FileDownloadProgressUpdate{f1v2p1, f1v2p2},
},
{ //3
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v1del},
nil,
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p1, f1v2p2}},
{ //4
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2del},
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2},
[]protocol.FileDownloadProgressUpdate{f1v2p1, f1v2p2},
},
{ //5
// v2 replaces old v1 data
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p1},
[]protocol.FileDownloadProgressUpdate{f1v2p1},
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p2},
},
{ //6
// v1 delete on v2 data does nothing
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p1, f1v1del},
[]protocol.FileDownloadProgressUpdate{f1v2p1},
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p2},
},
{ //7
// v2 replacees v1, v2 gets deleted, and v2 part 2 gets added.
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p1, f1v2del, f1v2p2},
[]protocol.FileDownloadProgressUpdate{f1v2p2},
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p1},
},
// Multiple files in one go
{ //8
[]protocol.FileDownloadProgressUpdate{f1v1p1, f2v1p1},
[]protocol.FileDownloadProgressUpdate{f1v1p1, f2v1p1},
[]protocol.FileDownloadProgressUpdate{f1v1p2, f2v1p2},
},
{ //9
[]protocol.FileDownloadProgressUpdate{f1v1p1, f2v1p1, f2v1del},
[]protocol.FileDownloadProgressUpdate{f1v1p1},
[]protocol.FileDownloadProgressUpdate{f2v1p1, f2v1p1},
},
{ //10
[]protocol.FileDownloadProgressUpdate{f1v1p1, f2v1del, f2v1p1},
[]protocol.FileDownloadProgressUpdate{f1v1p1, f2v1p1},
[]protocol.FileDownloadProgressUpdate{f1v1p2, f2v1p2},
},
}
for i, test := range tests {
s := newDeviceDownloadState()
s.Update("folder", test.updates)
for _, expected := range test.shouldHaveIndexesFrom {
for _, n := range expected.BlockIndexes {
if !s.Has("folder", expected.Name, expected.Version, n) {
t.Error("Test", i+1, "error:", expected.Name, expected.Version, "missing", n)
}
}
}
for _, unexpected := range test.shouldNotHaveIndexesFrom {
for _, n := range unexpected.BlockIndexes {
if s.Has("folder", unexpected.Name, unexpected.Version, n) {
t.Error("Test", i+1, "error:", unexpected.Name, unexpected.Version, "has extra", n)
}
}
}
}
}
+149 -66
View File
@@ -60,6 +60,11 @@ type service interface {
getState() (folderState, time.Time, error)
}
type Availability struct {
ID protocol.DeviceID `json:"id"`
FromTemporary bool `json:"fromTemporary"`
}
type Model struct {
*suture.Supervisor
@@ -87,10 +92,12 @@ type Model struct {
folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef
fmut sync.RWMutex // protects the above
conn map[protocol.DeviceID]Connection
helloMessages map[protocol.DeviceID]protocol.HelloMessage
devicePaused map[protocol.DeviceID]bool
pmut sync.RWMutex // protects the above
conn map[protocol.DeviceID]Connection
helloMessages map[protocol.DeviceID]protocol.HelloMessage
deviceClusterConf map[protocol.DeviceID]protocol.ClusterConfigMessage
devicePaused map[protocol.DeviceID]bool
deviceDownloads map[protocol.DeviceID]*deviceDownloadState
pmut sync.RWMutex // protects the above
}
var (
@@ -129,10 +136,11 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
conn: make(map[protocol.DeviceID]Connection),
helloMessages: make(map[protocol.DeviceID]protocol.HelloMessage),
deviceClusterConf: make(map[protocol.DeviceID]protocol.ClusterConfigMessage),
devicePaused: make(map[protocol.DeviceID]bool),
fmut: sync.NewRWMutex(),
pmut: sync.NewRWMutex(),
deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState),
fmut: sync.NewRWMutex(),
pmut: sync.NewRWMutex(),
}
if cfg.Options().ProgressUpdateIntervalS > -1 {
go m.progressEmitter.Serve()
@@ -209,7 +217,7 @@ func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
}
// check if file is ignored
if ignores.Match(protectedFilePath) {
if ignores.Match(protectedFilePath).IsIgnored() {
continue
}
@@ -304,10 +312,6 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
// ConnectionStats returns a map with connection statistics for each device.
func (m *Model) ConnectionStats() map[string]interface{} {
type remoteAddrer interface {
RemoteAddr() net.Addr
}
m.pmut.RLock()
m.fmut.RLock()
@@ -392,6 +396,11 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
return true
})
// This might might be more than it really is, because some blocks can be of a smaller size.
m.pmut.RLock()
need -= int64(m.deviceDownloads[device].NumberOfBlocksInProgress() * protocol.BlockSize)
m.pmut.RUnlock()
needRatio := float64(need) / float64(tot)
completionPct := 100 * (1 - needRatio)
l.Debugf("%v Completion(%s, %q): %f (%d / %d = %f)", m, device, folder, completionPct, need, tot, needRatio)
@@ -613,6 +622,10 @@ func (m *Model) folderSharedWithUnlocked(folder string, deviceID protocol.Device
func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterConfigMessage) {
// Check the peer device's announced folders against our own. Emits events
// for folders that we don't expect (unknown or not shared).
// Also, collect a list of folders we do share, and if he's interested in
// temporary indexes, subscribe the connection.
tempIndexFolders := make([]string, 0, len(cm.Folders))
m.fmut.Lock()
nextFolder:
@@ -639,9 +652,24 @@ nextFolder:
l.Infof("Unexpected folder ID %q sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.ID, deviceID)
continue
}
if folder.Flags&protocol.FlagFolderDisabledTempIndexes == 0 {
tempIndexFolders = append(tempIndexFolders, folder.ID)
}
}
m.fmut.Unlock()
// This breaks if we send multiple CM messages during the same connection.
if len(tempIndexFolders) > 0 {
m.pmut.RLock()
conn, ok := m.conn[deviceID]
m.pmut.RUnlock()
// In case we've got ClusterConfig, and the connection disappeared
// from infront of our nose.
if ok {
m.progressEmitter.temporaryIndexSubscribe(conn, tempIndexFolders)
}
}
var changed bool
if m.cfg.Devices()[deviceID].Introducer {
@@ -649,9 +677,6 @@ nextFolder:
// and devices and add what we are missing.
for _, folder := range cm.Folders {
// If we don't have this folder yet, skip it. Ideally, we'd
// offer up something in the GUI to create the folder, but for the
// moment we only handle folders that we already have.
if _, ok := m.folderDevices[folder.ID]; !ok {
continue
}
@@ -740,10 +765,13 @@ func (m *Model) Close(device protocol.DeviceID, err error) {
conn, ok := m.conn[device]
if ok {
m.progressEmitter.temporaryIndexUnsubscribe(conn)
closeRawConn(conn)
}
delete(m.conn, device)
delete(m.helloMessages, device)
delete(m.deviceClusterConf, device)
delete(m.deviceDownloads, device)
m.pmut.Unlock()
}
@@ -756,19 +784,20 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
if !m.folderSharedWith(folder, deviceID) {
l.Warnf("Request from %s for file %s in unshared folder %q", deviceID, name, folder)
return protocol.ErrInvalid
return protocol.ErrNoSuchFile
}
if flags != 0 {
// We don't currently support or expect any flags.
return protocol.ErrInvalid
if flags != 0 && flags != protocol.FlagFromTemporary {
// We currently support only no flags, or FromTemporary flag.
return fmt.Errorf("protocol error: unknown flags 0x%x in Request message", flags)
}
if deviceID != protocol.LocalDeviceID {
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d f=%d", m, deviceID, folder, name, offset, len(buf), flags)
}
m.fmut.RLock()
folderPath := m.folderCfgs[folder].Path()
folderCfg := m.folderCfgs[folder]
folderPath := folderCfg.Path()
folderIgnores := m.folderIgnores[folder]
m.fmut.RUnlock()
@@ -800,14 +829,12 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
// cleaned from any possible funny business.
if rn, err := filepath.Rel(folderPath, fn); err != nil {
return err
} else if folderIgnores.Match(rn) {
} else if folderIgnores.Match(rn).IsIgnored() {
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
return protocol.ErrNoSuchFile
}
}
var reader io.ReaderAt
var err error
if info, err := os.Lstat(fn); err == nil && info.Mode()&os.ModeSymlink != 0 {
target, _, err := symlinks.Read(fn)
if err != nil {
@@ -817,28 +844,30 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
}
return protocol.ErrGeneric
}
reader = strings.NewReader(target)
} else {
// Cannot easily cache fd's because we might need to delete the file
// at any moment.
reader, err = os.Open(fn)
if err != nil {
l.Debugln("os.Open:", err)
if os.IsNotExist(err) {
return protocol.ErrNoSuchFile
}
if _, err := strings.NewReader(target).ReadAt(buf, offset); err != nil {
l.Debugln("symlink.Reader.ReadAt", err)
return protocol.ErrGeneric
}
defer reader.(*os.File).Close()
return nil
}
_, err = reader.ReadAt(buf, offset)
if err != nil {
l.Debugln("reader.ReadAt:", err)
// Only check temp files if the flag is set, and if we are set to advertise
// the temp indexes.
if flags&protocol.FlagFromTemporary != 0 && !folderCfg.DisableTempIndexes {
tempFn := filepath.Join(folderPath, defTempNamer.TempName(name))
if err := readOffsetIntoBuf(tempFn, offset, buf); err == nil {
return nil
}
// Fall through to reading from a non-temp file, just incase the temp
// file has finished downloading.
}
err := readOffsetIntoBuf(fn, offset, buf)
if os.IsNotExist(err) {
return protocol.ErrNoSuchFile
} else if err != nil {
return protocol.ErrGeneric
}
return nil
}
@@ -990,6 +1019,7 @@ func (m *Model) AddConnection(conn Connection, hello protocol.HelloMessage) {
panic("add existing device")
}
m.conn[deviceID] = conn
m.deviceDownloads[deviceID] = newDeviceDownloadState()
m.helloMessages[deviceID] = hello
@@ -1011,7 +1041,7 @@ func (m *Model) AddConnection(conn Connection, hello protocol.HelloMessage) {
l.Infof(`Device %s client is "%s %s" named "%s"`, deviceID, hello.ClientName, hello.ClientVersion, hello.DeviceName)
device, ok := m.cfg.Devices()[deviceID]
if ok && device.Name == "" {
if ok && (device.Name == "" || m.cfg.Options().OverwriteNames) {
device.Name = hello.DeviceName
m.cfg.SetDevice(device)
m.cfg.Save()
@@ -1044,6 +1074,24 @@ func (m *Model) PauseDevice(device protocol.DeviceID) {
events.Default.Log(events.DevicePaused, map[string]string{"device": device.String()})
}
func (m *Model) DownloadProgress(device protocol.DeviceID, folder string, updates []protocol.FileDownloadProgressUpdate, flags uint32, options []protocol.Option) {
if !m.folderSharedWith(folder, device) {
return
}
m.fmut.RLock()
cfg, ok := m.folderCfgs[folder]
m.fmut.RUnlock()
if !ok || cfg.ReadOnly || cfg.DisableTempIndexes {
return
}
m.pmut.RLock()
m.deviceDownloads[device].Update(folder, updates)
m.pmut.RUnlock()
}
func (m *Model) ResumeDevice(device protocol.DeviceID) {
m.pmut.Lock()
m.devicePaused[device] = false
@@ -1129,7 +1177,6 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
// time to batch them up a little.
time.Sleep(250 * time.Millisecond)
}
}
func sendIndexTo(initial bool, minLocalVer int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher) (int64, error) {
@@ -1150,7 +1197,7 @@ func sendIndexTo(initial bool, minLocalVer int64, conn protocol.Connection, fold
maxLocalVer = f.LocalVersion
}
if ignores.Match(f.Name) || symlinkInvalid(folder, f) {
if ignores.Match(f.Name).IsIgnored() || symlinkInvalid(folder, f) {
l.Debugln("not sending update for ignored/unsupported symlink", f)
return true
}
@@ -1216,7 +1263,7 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
})
}
func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, flags uint32, options []protocol.Option) ([]byte, error) {
func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
m.pmut.RLock()
nc, ok := m.conn[deviceID]
m.pmut.RUnlock()
@@ -1225,9 +1272,9 @@ func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, o
return nil, fmt.Errorf("requestGlobal: no such device: %s", deviceID)
}
l.Debugf("%v REQ(out): %s: %q / %q o=%d s=%d h=%x f=%x op=%s", m, deviceID, folder, name, offset, size, hash, flags, options)
l.Debugf("%v REQ(out): %s: %q / %q o=%d s=%d h=%x ft=%t op=%s", m, deviceID, folder, name, offset, size, hash, fromTemporary)
return nc.Request(folder, name, offset, size, hash, flags, options)
return nc.Request(folder, name, offset, size, hash, fromTemporary)
}
func (m *Model) AddFolder(cfg config.FolderConfiguration) {
@@ -1433,10 +1480,6 @@ func (m *Model) internalScanFolderSubs(folder string, subs []string) error {
fs.WithPrefixedHaveTruncated(protocol.LocalDeviceID, sub, func(fi db.FileIntf) bool {
f := fi.(db.FileInfoTruncated)
if !f.IsDeleted() {
if f.IsInvalid() {
return true
}
if len(batch) == batchSizeFiles {
if err := m.CheckFolderHealth(folder); err != nil {
iterError = err
@@ -1446,7 +1489,7 @@ func (m *Model) internalScanFolderSubs(folder string, subs []string) error {
batch = batch[:0]
}
if ignores.Match(f.Name) || symlinkInvalid(folder, f) {
if ignores.Match(f.Name).IsIgnored() || symlinkInvalid(folder, f) {
// File has been ignored or an unsupported symlink. Set invalid bit.
l.Debugln("setting invalid bit on ignored", f)
nf := protocol.FileInfo{
@@ -1472,6 +1515,12 @@ func (m *Model) internalScanFolderSubs(folder string, subs []string) error {
Modified: f.Modified,
Version: f.Version.Update(m.shortID),
}
// The deleted file might have been ignored at some
// point, but it currently isn't so we make sure to
// clear the invalid bit.
nf.Flags &^= protocol.FlagInvalid
batch = append(batch, nf)
}
}
@@ -1556,6 +1605,9 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
if folderCfg.IgnoreDelete {
flags |= protocol.FlagFolderIgnoreDelete
}
if folderCfg.DisableTempIndexes {
flags |= protocol.FlagFolderDisabledTempIndexes
}
protocolFolder.Flags = flags
for _, device := range m.folderDevices[folder] {
// DeviceID is a value type, but with an underlying array. Copy it
@@ -1739,7 +1791,7 @@ func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly
return output
}
func (m *Model) Availability(folder, file string) []protocol.DeviceID {
func (m *Model) Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []Availability {
// Acquire this lock first, as the value returned from foldersFiles can
// get heavily modified on Close()
m.pmut.RLock()
@@ -1747,19 +1799,27 @@ func (m *Model) Availability(folder, file string) []protocol.DeviceID {
m.fmut.RLock()
fs, ok := m.folderFiles[folder]
devices := m.folderDevices[folder]
m.fmut.RUnlock()
if !ok {
return nil
}
availableDevices := []protocol.DeviceID{}
var availabilities []Availability
for _, device := range fs.Availability(file) {
_, ok := m.conn[device]
if ok {
availableDevices = append(availableDevices, device)
availabilities = append(availabilities, Availability{ID: device, FromTemporary: false})
}
}
return availableDevices
for _, device := range devices {
if m.deviceDownloads[device].Has(folder, file, version, int32(block.Offset/protocol.BlockSize)) {
availabilities = append(availabilities, Availability{ID: device, FromTemporary: true})
}
}
return availabilities
}
// BringToFront bumps the given files priority in the job queue.
@@ -2089,31 +2149,54 @@ func stringSliceWithout(ss []string, s string) []string {
return ss
}
// unifySubs takes a list of files or directories and trims them down to
// themselves or the closest parent that exists() returns true for, while
// removing duplicates and subdirectories. That is, if we have foo/ in the
// list, we don't also need foo/bar/ because that's already covered.
func unifySubs(dirs []string, exists func(dir string) bool) []string {
var subs []string
func readOffsetIntoBuf(file string, offset int64, buf []byte) error {
fd, err := os.Open(file)
if err != nil {
l.Debugln("readOffsetIntoBuf.Open", file, err)
return err
}
// Trim each item to itself or its closest known parent
defer fd.Close()
_, err = fd.ReadAt(buf, offset)
if err != nil {
l.Debugln("readOffsetIntoBuf.ReadAt", file, err)
}
return err
}
// The exists function is expected to return true for all known paths
// (excluding "" and ".")
func unifySubs(dirs []string, exists func(dir string) bool) []string {
subs := trimUntilParentKnown(dirs, exists)
sort.Strings(subs)
return simplifySortedPaths(subs)
}
func trimUntilParentKnown(dirs []string, exists func(dir string) bool) []string {
var subs []string
for _, sub := range dirs {
for sub != "" && sub != ".stfolder" && sub != ".stignore" {
if exists(sub) {
sub = filepath.Clean(sub)
parent := filepath.Dir(sub)
if parent == "." || exists(parent) {
break
}
sub = filepath.Dir(sub)
sub = parent
if sub == "." || sub == string(filepath.Separator) {
// Shortcut. We are going to scan the full folder, so we can
// just return an empty list of subs at this point.
return nil
}
}
if sub == "" {
return nil
}
subs = append(subs, sub)
}
return subs
}
// Remove any paths that are already covered by their parent
sort.Strings(subs)
func simplifySortedPaths(subs []string) []string {
var cleaned []string
next:
for _, sub := range subs {
+65 -17
View File
@@ -203,9 +203,17 @@ func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
b.ReportAllocs()
}
type downloadProgressMessage struct {
folder string
updates []protocol.FileDownloadProgressUpdate
flags uint32
options []protocol.Option
}
type FakeConnection struct {
id protocol.DeviceID
requestData []byte
id protocol.DeviceID
requestData []byte
downloadProgressMessages []downloadProgressMessage
}
func (FakeConnection) Close() error {
@@ -235,7 +243,7 @@ func (FakeConnection) IndexUpdate(string, []protocol.FileInfo, uint32, []protoco
return nil
}
func (f FakeConnection) Request(folder, name string, offset int64, size int, hash []byte, flags uint32, options []protocol.Option) ([]byte, error) {
func (f FakeConnection) Request(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
return f.requestData, nil
}
@@ -253,6 +261,15 @@ func (FakeConnection) Statistics() protocol.Statistics {
return protocol.Statistics{}
}
func (f *FakeConnection) DownloadProgress(folder string, updates []protocol.FileDownloadProgressUpdate, flags uint32, options []protocol.Option) {
f.downloadProgressMessages = append(f.downloadProgressMessages, downloadProgressMessage{
folder: folder,
updates: updates,
flags: flags,
options: options,
})
}
func BenchmarkRequest(b *testing.B) {
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
@@ -271,7 +288,7 @@ func BenchmarkRequest(b *testing.B) {
}
}
fc := FakeConnection{
fc := &FakeConnection{
id: device1,
requestData: []byte("some data to return"),
}
@@ -284,7 +301,7 @@ func BenchmarkRequest(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
data, err := m.requestGlobal(device1, "default", files[i%n].Name, 0, 32, nil, 0, nil)
data, err := m.requestGlobal(device1, "default", files[i%n].Name, 0, 32, nil, false)
if err != nil {
b.Error(err)
}
@@ -318,7 +335,7 @@ func TestDeviceRename(t *testing.T) {
conn := Connection{
&net.TCPConn{},
FakeConnection{
&FakeConnection{
id: device1,
requestData: []byte("some data to return"),
},
@@ -357,6 +374,19 @@ func TestDeviceRename(t *testing.T) {
if cfgw.Devices()[device1].Name != "tester" {
t.Errorf("Device name not saved in config")
}
m.Close(device1, protocol.ErrTimeout)
opts := cfg.Options()
opts.OverwriteNames = true
cfg.SetOptions(opts)
hello.DeviceName = "tester2"
m.AddConnection(conn, hello)
if cfg.Devices()[device1].Name != "tester2" {
t.Errorf("Device name not overwritten")
}
}
func TestClusterConfig(t *testing.T) {
@@ -1223,49 +1253,67 @@ func TestUnifySubs(t *testing.T) {
out []string // expected output
}{
{
// trailing slashes are cleaned, known paths are just passed on
// 0. trailing slashes are cleaned, known paths are just passed on
[]string{"foo/", "bar//"},
[]string{"foo", "bar"},
[]string{"bar", "foo"}, // the output is sorted
},
{
// "foo/bar" gets trimmed as it's covered by foo
// 1. "foo/bar" gets trimmed as it's covered by foo
[]string{"foo", "bar/", "foo/bar/"},
[]string{"foo", "bar"},
[]string{"bar", "foo"},
},
{
// "bar" gets trimmed to "" as it's unknown,
// "" gets simplified to the empty list
[]string{"foo", "bar", "foo/bar"},
// 2. "" gets simplified to the empty list; ie scan all
[]string{"foo", ""},
[]string{"foo"},
nil,
},
{
// two independent known paths, both are kept
// 3. "foo/bar" is unknown, but it's kept
// because its parent is known
[]string{"foo/bar"},
[]string{"foo"},
[]string{"foo/bar"},
},
{
// 4. two independent known paths, both are kept
// "usr/lib" is not a prefix of "usr/libexec"
[]string{"usr/lib", "usr/libexec"},
[]string{"usr/lib", "usr/libexec"},
[]string{"usr", "usr/lib", "usr/libexec"},
[]string{"usr/lib", "usr/libexec"},
},
{
// "usr/lib" is a prefix of "usr/lib/exec"
// 5. "usr/lib" is a prefix of "usr/lib/exec"
[]string{"usr/lib", "usr/lib/exec"},
[]string{"usr/lib", "usr/libexec"},
[]string{"usr", "usr/lib", "usr/libexec"},
[]string{"usr/lib"},
},
{
// .stignore and .stfolder are special and are passed on
// 6. .stignore and .stfolder are special and are passed on
// verbatim even though they are unknown
[]string{".stfolder", ".stignore"},
[]string{},
[]string{".stfolder", ".stignore"},
},
{
// but the presense of something else unknown forces an actual
// 7. but the presense of something else unknown forces an actual
// scan
[]string{".stfolder", ".stignore", "foo/bar"},
[]string{},
[]string{".stfolder", ".stignore", "foo"},
},
{
// 8. explicit request to scan all
nil,
[]string{"foo"},
nil,
},
{
// 9. empty list of subs
[]string{},
[]string{"foo"},
nil,
},
}
+159 -19
View File
@@ -9,19 +9,22 @@ package model
import (
"fmt"
"path/filepath"
"reflect"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
)
type ProgressEmitter struct {
registry map[string]*sharedPullerState
interval time.Duration
last map[string]map[string]*pullerProgress
mut sync.Mutex
registry map[string]*sharedPullerState
interval time.Duration
minBlocks int
lastUpdate time.Time
sentDownloadStates map[protocol.DeviceID]*sentDownloadState // States representing what we've sent to the other peer via DownloadProgress messages.
connections map[string][]protocol.Connection
mut sync.Mutex
timer *time.Timer
@@ -32,11 +35,12 @@ type ProgressEmitter struct {
// DownloadProgress events every interval.
func NewProgressEmitter(cfg *config.Wrapper) *ProgressEmitter {
t := &ProgressEmitter{
stop: make(chan struct{}),
registry: make(map[string]*sharedPullerState),
last: make(map[string]map[string]*pullerProgress),
timer: time.NewTimer(time.Millisecond),
mut: sync.NewMutex(),
stop: make(chan struct{}),
registry: make(map[string]*sharedPullerState),
timer: time.NewTimer(time.Millisecond),
sentDownloadStates: make(map[protocol.DeviceID]*sentDownloadState),
connections: make(map[string][]protocol.Connection),
mut: sync.NewMutex(),
}
t.CommitConfiguration(config.Configuration{}, cfg.Raw())
@@ -48,6 +52,8 @@ func NewProgressEmitter(cfg *config.Wrapper) *ProgressEmitter {
// Serve starts the progress emitter which starts emitting DownloadProgress
// events as the progress happens.
func (t *ProgressEmitter) Serve() {
var lastUpdate time.Time
var lastCount, newCount int
for {
select {
case <-t.stop:
@@ -56,21 +62,28 @@ func (t *ProgressEmitter) Serve() {
case <-t.timer.C:
t.mut.Lock()
l.Debugln("progress emitter: timer - looking after", len(t.registry))
output := make(map[string]map[string]*pullerProgress)
newLastUpdated := lastUpdate
newCount = len(t.registry)
for _, puller := range t.registry {
if output[puller.folder] == nil {
output[puller.folder] = make(map[string]*pullerProgress)
updated := puller.Updated()
if updated.After(newLastUpdated) {
newLastUpdated = updated
}
output[puller.folder][puller.file.Name] = puller.Progress()
}
if !reflect.DeepEqual(t.last, output) {
events.Default.Log(events.DownloadProgress, output)
t.last = output
l.Debugf("progress emitter: emitting %#v", output)
if !newLastUpdated.Equal(lastUpdate) || newCount != lastCount {
lastUpdate = newLastUpdated
lastCount = newCount
t.sendDownloadProgressEvent()
if len(t.connections) > 0 {
t.sendDownloadProgressMessages()
}
} else {
l.Debugln("progress emitter: nothing new")
}
if len(t.registry) != 0 {
if newCount != 0 {
t.timer.Reset(t.interval)
}
t.mut.Unlock()
@@ -78,6 +91,95 @@ func (t *ProgressEmitter) Serve() {
}
}
func (t *ProgressEmitter) sendDownloadProgressEvent() {
// registry lock already held
output := make(map[string]map[string]*pullerProgress)
for _, puller := range t.registry {
if output[puller.folder] == nil {
output[puller.folder] = make(map[string]*pullerProgress)
}
output[puller.folder][puller.file.Name] = puller.Progress()
}
events.Default.Log(events.DownloadProgress, output)
l.Debugf("progress emitter: emitting %#v", output)
}
func (t *ProgressEmitter) sendDownloadProgressMessages() {
// registry lock already held
sharedFolders := make(map[protocol.DeviceID][]string)
deviceConns := make(map[protocol.DeviceID]protocol.Connection)
subscribers := t.connections
for folder, conns := range subscribers {
for _, conn := range conns {
id := conn.ID()
deviceConns[id] = conn
sharedFolders[id] = append(sharedFolders[id], folder)
state, ok := t.sentDownloadStates[id]
if !ok {
state = &sentDownloadState{
folderStates: make(map[string]*sentFolderDownloadState),
}
t.sentDownloadStates[id] = state
}
var activePullers []*sharedPullerState
for _, puller := range t.registry {
if puller.folder != folder || puller.file.IsSymlink() || puller.file.IsDirectory() || len(puller.file.Blocks) <= t.minBlocks {
continue
}
activePullers = append(activePullers, puller)
}
// For every new puller that hasn't yet been seen, it will send all the blocks the puller has available
// For every existing puller, it will check for new blocks, and send update for the new blocks only
// For every puller that we've seen before but is no longer there, we will send a forget message
updates := state.update(folder, activePullers)
if len(updates) > 0 {
conn.DownloadProgress(folder, updates, 0, nil)
}
}
}
// Clean up sentDownloadStates for devices which we are no longer connected to.
for id := range t.sentDownloadStates {
_, ok := deviceConns[id]
if !ok {
// Null out outstanding entries for device
delete(t.sentDownloadStates, id)
}
}
// If a folder was unshared from some device, tell it that all temp files
// are now gone.
for id, sharedDeviceFolders := range sharedFolders {
state := t.sentDownloadStates[id]
nextFolder:
// For each of the folders that the state is aware of,
// try to match it with a shared folder we've discovered above,
for _, folder := range state.folders() {
for _, existingFolder := range sharedDeviceFolders {
if existingFolder == folder {
continue nextFolder
}
}
// If we fail to find that folder, we tell the state to forget about it
// and return us a list of updates which would clean up the state
// on the remote end.
updates := state.cleanup(folder)
if len(updates) > 0 {
// XXX: Don't send this now, as the only way we've unshared a folder
// is by breaking the connection and reconnecting, hence sending
// forget messages for some random folder currently makes no sense.
// deviceConns[id].DownloadProgress(folder, updates, 0, nil)
}
}
}
}
// VerifyConfiguration implements the config.Committer interface
func (t *ProgressEmitter) VerifyConfiguration(from, to config.Configuration) error {
return nil
@@ -89,6 +191,7 @@ func (t *ProgressEmitter) CommitConfiguration(from, to config.Configuration) boo
defer t.mut.Unlock()
t.interval = time.Duration(to.Options.ProgressUpdateIntervalS) * time.Second
t.minBlocks = to.Options.TempIndexMinBlocks
l.Debugln("progress emitter: updated interval", t.interval)
return true
@@ -115,7 +218,9 @@ func (t *ProgressEmitter) Register(s *sharedPullerState) {
func (t *ProgressEmitter) Deregister(s *sharedPullerState) {
t.mut.Lock()
defer t.mut.Unlock()
l.Debugln("progress emitter: deregistering", s.folder, s.file.Name)
delete(t.registry, filepath.Join(s.folder, s.file.Name))
}
@@ -142,3 +247,38 @@ func (t *ProgressEmitter) lenRegistry() int {
defer t.mut.Unlock()
return len(t.registry)
}
func (t *ProgressEmitter) temporaryIndexSubscribe(conn protocol.Connection, folders []string) {
t.mut.Lock()
for _, folder := range folders {
t.connections[folder] = append(t.connections[folder], conn)
}
t.mut.Unlock()
}
func (t *ProgressEmitter) temporaryIndexUnsubscribe(conn protocol.Connection) {
t.mut.Lock()
left := make(map[string][]protocol.Connection, len(t.connections))
for folder, conns := range t.connections {
connsLeft := connsWithout(conns, conn)
if len(connsLeft) > 0 {
left[folder] = connsLeft
}
}
t.connections = left
t.mut.Unlock()
}
func connsWithout(conns []protocol.Connection, conn protocol.Connection) []protocol.Connection {
if len(conns) == 0 {
return nil
}
newConns := make([]protocol.Connection, 0, len(conns)-1)
for _, existingConn := range conns {
if existingConn != conn {
newConns = append(newConns, existingConn)
}
}
return newConns
}
+353 -8
View File
@@ -7,34 +7,46 @@
package model
import (
"fmt"
"path/filepath"
"runtime"
"testing"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
)
var timeout = 10 * time.Millisecond
var timeout = 100 * time.Millisecond
func caller(skip int) string {
_, file, line, ok := runtime.Caller(skip + 1)
if !ok {
return "unknown"
}
return fmt.Sprintf("%s:%d", filepath.Base(file), line)
}
func expectEvent(w *events.Subscription, t *testing.T, size int) {
event, err := w.Poll(timeout)
if err != nil {
t.Fatal("Unexpected error:", err)
t.Fatal("Unexpected error:", err, "at", caller(1))
}
if event.Type != events.DownloadProgress {
t.Fatal("Unexpected event:", event)
t.Fatal("Unexpected event:", event, "at", caller(1))
}
data := event.Data.(map[string]map[string]*pullerProgress)
if len(data) != size {
t.Fatal("Unexpected event data size:", data)
t.Fatal("Unexpected event data size:", data, "at", caller(1))
}
}
func expectTimeout(w *events.Subscription, t *testing.T) {
_, err := w.Poll(timeout)
if err != events.ErrTimeout {
t.Fatal("Unexpected non-Timeout error:", err)
t.Fatal("Unexpected non-Timeout error:", err, "at", caller(1))
}
}
@@ -52,14 +64,15 @@ func TestProgressEmitter(t *testing.T) {
expectTimeout(w, t)
s := sharedPullerState{
mut: sync.NewMutex(),
updated: time.Now(),
mut: sync.NewRWMutex(),
}
p.Register(&s)
expectEvent(w, t, 1)
expectTimeout(w, t)
s.copyDone()
s.copyDone(protocol.BlockInfo{})
expectEvent(w, t, 1)
expectTimeout(w, t)
@@ -74,7 +87,7 @@ func TestProgressEmitter(t *testing.T) {
expectEvent(w, t, 1)
expectTimeout(w, t)
s.pullDone()
s.pullDone(protocol.BlockInfo{})
expectEvent(w, t, 1)
expectTimeout(w, t)
@@ -85,3 +98,335 @@ func TestProgressEmitter(t *testing.T) {
expectTimeout(w, t)
}
func TestSendDownloadProgressMessages(t *testing.T) {
c := config.Wrap("/tmp/test", config.Configuration{})
c.SetOptions(config.OptionsConfiguration{
ProgressUpdateIntervalS: 0,
TempIndexMinBlocks: 10,
})
fc := &FakeConnection{}
p := NewProgressEmitter(c)
p.temporaryIndexSubscribe(fc, []string{"folder", "folder2"})
expect := func(updateIdx int, state *sharedPullerState, updateType uint32, version protocol.Vector, blocks []int32, remove bool) {
messageIdx := -1
for i, msg := range fc.downloadProgressMessages {
if msg.folder == state.folder {
messageIdx = i
break
}
}
if messageIdx < 0 {
t.Errorf("Message for folder %s does not exist at %s", state.folder, caller(1))
}
msg := fc.downloadProgressMessages[messageIdx]
// Don't know the index (it's random due to iterating maps)
if updateIdx == -1 {
for i, upd := range msg.updates {
if upd.Name == state.file.Name {
updateIdx = i
break
}
}
}
if updateIdx == -1 {
t.Errorf("Could not find update for %s at %s", state.file.Name, caller(1))
}
if updateIdx > len(msg.updates)-1 {
t.Errorf("Update at index %d does not exist at %s", updateIdx, caller(1))
}
update := msg.updates[updateIdx]
if update.UpdateType != updateType {
t.Errorf("Wrong update type at %s", caller(1))
}
if !update.Version.Equal(version) {
t.Errorf("Wrong version at %s", caller(1))
}
if len(update.BlockIndexes) != len(blocks) {
t.Errorf("Wrong indexes. Have %d expect %d at %s", len(update.BlockIndexes), len(blocks), caller(1))
}
for i := range update.BlockIndexes {
if update.BlockIndexes[i] != blocks[i] {
t.Errorf("Index %d incorrect at %s", i, caller(1))
}
}
if remove {
fc.downloadProgressMessages = append(fc.downloadProgressMessages[:messageIdx], fc.downloadProgressMessages[messageIdx+1:]...)
}
}
expectEmpty := func() {
if len(fc.downloadProgressMessages) > 0 {
t.Errorf("Still have something at %s: %#v", caller(1), fc.downloadProgressMessages)
}
}
now := time.Now()
tick := func() time.Time {
now = now.Add(time.Second)
return now
}
if len(fc.downloadProgressMessages) != 0 {
t.Error("Expected no requests")
}
v1 := (protocol.Vector{}).Update(0)
v2 := (protocol.Vector{}).Update(1)
// Requires more than 10 blocks to work.
blocks := make([]protocol.BlockInfo, 11, 11)
state1 := &sharedPullerState{
folder: "folder",
file: protocol.FileInfo{
Name: "state1",
Version: v1,
Blocks: blocks,
},
mut: sync.NewRWMutex(),
availableUpdated: time.Now(),
}
p.registry["1"] = state1
// Has no blocks, hence no message is sent
p.sendDownloadProgressMessages()
expectEmpty()
// Returns update for puller with new extra blocks
state1.available = []int32{1}
p.sendDownloadProgressMessages()
expect(0, state1, protocol.UpdateTypeAppend, v1, []int32{1}, true)
expectEmpty()
// Does nothing if nothing changes
p.sendDownloadProgressMessages()
expectEmpty()
// Does nothing if timestamp updated, but no new blocks (should never happen)
state1.availableUpdated = tick()
p.sendDownloadProgressMessages()
expectEmpty()
// Does not return an update if date blocks change but date does not (should never happen)
state1.available = []int32{1, 2}
p.sendDownloadProgressMessages()
expectEmpty()
// If the date and blocks changes, returns only the diff
state1.availableUpdated = tick()
p.sendDownloadProgressMessages()
expect(0, state1, protocol.UpdateTypeAppend, v1, []int32{2}, true)
expectEmpty()
// Returns forget and update if puller version has changed
state1.file.Version = v2
p.sendDownloadProgressMessages()
expect(0, state1, protocol.UpdateTypeForget, v1, nil, false)
expect(1, state1, protocol.UpdateTypeAppend, v2, []int32{1, 2}, true)
expectEmpty()
// Sends an empty update if new file exists, but does not have any blocks yet. (To indicate that the old blocks are no longer available)
state1.file.Version = v1
state1.available = nil
state1.availableUpdated = tick()
p.sendDownloadProgressMessages()
expect(0, state1, protocol.UpdateTypeForget, v2, nil, false)
expect(1, state1, protocol.UpdateTypeAppend, v1, nil, true)
expectEmpty()
// Updates for multiple files and folders can be combined
state1.available = []int32{1, 2, 3}
state1.availableUpdated = tick()
state2 := &sharedPullerState{
folder: "folder2",
file: protocol.FileInfo{
Name: "state2",
Version: v1,
Blocks: blocks,
},
mut: sync.NewRWMutex(),
available: []int32{1, 2, 3},
availableUpdated: time.Now(),
}
state3 := &sharedPullerState{
folder: "folder",
file: protocol.FileInfo{
Name: "state3",
Version: v1,
Blocks: blocks,
},
mut: sync.NewRWMutex(),
available: []int32{1, 2, 3},
availableUpdated: time.Now(),
}
state4 := &sharedPullerState{
folder: "folder2",
file: protocol.FileInfo{
Name: "state4",
Version: v1,
Blocks: blocks,
},
mut: sync.NewRWMutex(),
available: []int32{1, 2, 3},
availableUpdated: time.Now(),
}
p.registry["2"] = state2
p.registry["3"] = state3
p.registry["4"] = state4
p.sendDownloadProgressMessages()
expect(-1, state1, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, false)
expect(-1, state3, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, true)
expect(-1, state2, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, false)
expect(-1, state4, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, true)
expectEmpty()
// Returns forget if puller no longer exists, as well as updates if it has been updated.
state1.available = []int32{1, 2, 3, 4, 5}
state1.availableUpdated = tick()
state2.available = []int32{1, 2, 3, 4, 5}
state2.availableUpdated = tick()
delete(p.registry, "3")
delete(p.registry, "4")
p.sendDownloadProgressMessages()
expect(-1, state1, protocol.UpdateTypeAppend, v1, []int32{4, 5}, false)
expect(-1, state3, protocol.UpdateTypeForget, v1, nil, true)
expect(-1, state2, protocol.UpdateTypeAppend, v1, []int32{4, 5}, false)
expect(-1, state4, protocol.UpdateTypeForget, v1, nil, true)
expectEmpty()
// Deletions are sent only once (actual bug I found writing the tests)
p.sendDownloadProgressMessages()
p.sendDownloadProgressMessages()
expectEmpty()
// Not sent for "inactive" (symlinks, dirs, or wrong folder) pullers
// Directory
state5 := &sharedPullerState{
folder: "folder",
file: protocol.FileInfo{
Name: "state5",
Version: v1,
Flags: protocol.FlagDirectory,
Blocks: blocks,
},
mut: sync.NewRWMutex(),
available: []int32{1, 2, 3},
availableUpdated: time.Now(),
}
// Symlink
state6 := &sharedPullerState{
folder: "folder",
file: protocol.FileInfo{
Name: "state6",
Version: v1,
Flags: protocol.FlagSymlink,
},
mut: sync.NewRWMutex(),
available: []int32{1, 2, 3},
availableUpdated: time.Now(),
}
// Some other directory
state7 := &sharedPullerState{
folder: "folderXXX",
file: protocol.FileInfo{
Name: "state7",
Version: v1,
Blocks: blocks,
},
mut: sync.NewRWMutex(),
available: []int32{1, 2, 3},
availableUpdated: time.Now(),
}
// Less than 10 blocks
state8 := &sharedPullerState{
folder: "folder",
file: protocol.FileInfo{
Name: "state8",
Version: v1,
Blocks: blocks[:3],
},
mut: sync.NewRWMutex(),
available: []int32{1, 2, 3},
availableUpdated: time.Now(),
}
p.registry["5"] = state5
p.registry["6"] = state6
p.registry["7"] = state7
p.registry["8"] = state8
p.sendDownloadProgressMessages()
expectEmpty()
// Device is no longer subscribed to a particular folder
delete(p.registry, "1") // Clean up first
delete(p.registry, "2") // Clean up first
p.sendDownloadProgressMessages()
expect(-1, state1, protocol.UpdateTypeForget, v1, nil, true)
expect(-1, state2, protocol.UpdateTypeForget, v1, nil, true)
expectEmpty()
p.registry["1"] = state1
p.registry["2"] = state2
p.registry["3"] = state3
p.registry["4"] = state4
p.sendDownloadProgressMessages()
expect(-1, state1, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3, 4, 5}, false)
expect(-1, state3, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, true)
expect(-1, state2, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3, 4, 5}, false)
expect(-1, state4, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, true)
expectEmpty()
p.temporaryIndexUnsubscribe(fc)
p.temporaryIndexSubscribe(fc, []string{"folder"})
p.sendDownloadProgressMessages()
// See progressemitter.go for explanation why this is commented out.
// Search for state.cleanup
//expect(-1, state2, protocol.UpdateTypeForget, v1, nil, false)
//expect(-1, state4, protocol.UpdateTypeForget, v1, nil, true)
expectEmpty()
// Cleanup when device no longer exists
p.temporaryIndexUnsubscribe(fc)
p.sendDownloadProgressMessages()
_, ok := p.sentDownloadStates[fc.ID()]
if ok {
t.Error("Should not be there")
}
}
+57 -36
View File
@@ -470,7 +470,7 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
file := intf.(protocol.FileInfo)
if ignores.Match(file.Name) {
if ignores.Match(file.Name).IsIgnored() {
// This is an ignored file. Skip it, continue iteration.
return true
}
@@ -583,7 +583,7 @@ nextFile:
for i := range dirDeletions {
dir := dirDeletions[len(dirDeletions)-i-1]
l.Debugln("Deleting dir", dir.Name)
p.deleteDir(dir)
p.deleteDir(dir, ignores)
}
// Wait for db updates to complete
@@ -689,7 +689,7 @@ func (p *rwFolder) handleDir(file protocol.FileInfo) {
}
// deleteDir attempts to delete the given directory
func (p *rwFolder) deleteDir(file protocol.FileInfo) {
func (p *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
var err error
events.Default.Log(events.ItemStarted, map[string]string{
"folder": p.folder,
@@ -712,9 +712,9 @@ func (p *rwFolder) deleteDir(file protocol.FileInfo) {
dir, _ := os.Open(realName)
if dir != nil {
files, _ := dir.Readdirnames(-1)
for _, file := range files {
if defTempNamer.IsTemporary(file) {
osutil.InWritableDir(osutil.Remove, filepath.Join(realName, file))
for _, dirFile := range files {
if defTempNamer.IsTemporary(dirFile) || (matcher != nil && matcher.Match(filepath.Join(file.Name, dirFile)).IsDeletable()) {
osutil.InWritableDir(osutil.Remove, filepath.Join(realName, dirFile))
}
}
dir.Close()
@@ -970,9 +970,9 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
scanner.PopulateOffsets(file.Blocks)
reused := 0
var blocks []protocol.BlockInfo
var blocksSize int64
var reused []int32
// Check for an old temporary file which might have some blocks we could
// reuse.
@@ -988,25 +988,27 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
}
// Since the blocks are already there, we don't need to get them.
for _, block := range file.Blocks {
for i, block := range file.Blocks {
_, ok := existingBlocks[block.String()]
if !ok {
blocks = append(blocks, block)
blocksSize += int64(block.Size)
} else {
reused = append(reused, int32(i))
}
}
// The sharedpullerstate will know which flags to use when opening the
// temp file depending if we are reusing any blocks or not.
reused = len(file.Blocks) - len(blocks)
if reused == 0 {
if len(reused) == 0 {
// Otherwise, discard the file ourselves in order for the
// sharedpuller not to panic when it fails to exclusively create a
// file which already exists
osutil.InWritableDir(osutil.Remove, tempName)
}
} else {
blocks = file.Blocks
// Copy the blocks, as we don't want to shuffle them on the FileInfo
blocks = append(blocks, file.Blocks...)
blocksSize = file.Size()
}
@@ -1018,6 +1020,12 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
}
}
// Shuffle the blocks
for i := range blocks {
j := rand.Intn(i + 1)
blocks[i], blocks[j] = blocks[j], blocks[i]
}
events.Default.Log(events.ItemStarted, map[string]string{
"folder": p.folder,
"item": file.Name,
@@ -1026,17 +1034,20 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
})
s := sharedPullerState{
file: file,
folder: p.folder,
tempName: tempName,
realName: realName,
copyTotal: len(blocks),
copyNeeded: len(blocks),
reused: reused,
ignorePerms: p.ignorePermissions(file),
version: curFile.Version,
mut: sync.NewMutex(),
sparse: p.allowSparse,
file: file,
folder: p.folder,
tempName: tempName,
realName: realName,
copyTotal: len(blocks),
copyNeeded: len(blocks),
reused: len(reused),
updated: time.Now(),
available: reused,
availableUpdated: time.Now(),
ignorePerms: p.ignorePermissions(file),
version: curFile.Version,
mut: sync.NewRWMutex(),
sparse: p.allowSparse,
}
l.Debugf("%v need file %s; copy %d, reused %v", p, file.Name, len(blocks), reused)
@@ -1184,7 +1195,7 @@ func (p *rwFolder) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pull
}
pullChan <- ps
} else {
state.copyDone()
state.copyDone(block)
}
}
out <- state.sharedPullerState
@@ -1210,19 +1221,19 @@ func (p *rwFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPul
if p.allowSparse && state.reused == 0 && state.block.IsEmpty() {
// There is no need to request a block of all zeroes. Pretend we
// requested it and handled it correctly.
state.pullDone()
state.pullDone(state.block)
out <- state.sharedPullerState
continue
}
var lastError error
potentialDevices := p.model.Availability(p.folder, state.file.Name)
candidates := p.model.Availability(p.folder, state.file.Name, state.file.Version, state.block)
for {
// Select the least busy device to pull the block from. If we found no
// feasible device at all, fail the block (and in the long run, the
// file).
selected := activity.leastBusy(potentialDevices)
if selected == (protocol.DeviceID{}) {
selected, found := activity.leastBusy(candidates)
if !found {
if lastError != nil {
state.fail("pull", lastError)
} else {
@@ -1231,12 +1242,12 @@ func (p *rwFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPul
break
}
potentialDevices = removeDevice(potentialDevices, selected)
candidates = removeAvailability(candidates, selected)
// Fetch the block, while marking the selected device as in use so that
// leastBusy can select another device when someone else asks.
activity.using(selected)
buf, lastError := p.model.requestGlobal(selected, p.folder, state.file.Name, state.block.Offset, int(state.block.Size), state.block.Hash, 0, nil)
buf, lastError := p.model.requestGlobal(selected.ID, p.folder, state.file.Name, state.block.Offset, int(state.block.Size), state.block.Hash, selected.FromTemporary)
activity.done(selected)
if lastError != nil {
l.Debugln("request:", p.folder, state.file.Name, state.block.Offset, state.block.Size, "returned error:", lastError)
@@ -1256,7 +1267,7 @@ func (p *rwFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPul
if err != nil {
state.fail("save", err)
} else {
state.pullDone()
state.pullDone(state.block)
}
break
}
@@ -1481,14 +1492,24 @@ func (p *rwFolder) inConflict(current, replacement protocol.Vector) bool {
return false
}
func removeDevice(devices []protocol.DeviceID, device protocol.DeviceID) []protocol.DeviceID {
for i := range devices {
if devices[i] == device {
devices[i] = devices[len(devices)-1]
return devices[:len(devices)-1]
func invalidateFolder(cfg *config.Configuration, folderID string, err error) {
for i := range cfg.Folders {
folder := &cfg.Folders[i]
if folder.ID == folderID {
folder.Invalid = err.Error()
return
}
}
return devices
}
func removeAvailability(availabilities []Availability, availability Availability) []Availability {
for i := range availabilities {
if availabilities[i] == availability {
availabilities[i] = availabilities[len(availabilities)-1]
return availabilities[:len(availabilities)-1]
}
}
return availabilities
}
func (p *rwFolder) moveForConflict(name string) error {
+36 -12
View File
@@ -104,9 +104,16 @@ func TestHandleFile(t *testing.T) {
t.Errorf("Unexpected count of copy blocks: %d != 8", len(toCopy.blocks))
}
for i, block := range toCopy.blocks {
if string(block.Hash) != string(blocks[i+1].Hash) {
t.Errorf("Block mismatch: %s != %s", block.String(), blocks[i+1].String())
for _, block := range blocks[1:] {
found := false
for _, toCopyBlock := range toCopy.blocks {
if string(toCopyBlock.Hash) == string(block.Hash) {
found = true
break
}
}
if !found {
t.Errorf("Did not find block %s", block.String())
}
}
}
@@ -138,9 +145,17 @@ func TestHandleFileWithTemp(t *testing.T) {
t.Errorf("Unexpected count of copy blocks: %d != 4", len(toCopy.blocks))
}
for i, eq := range []int{1, 5, 6, 8} {
if string(toCopy.blocks[i].Hash) != string(blocks[eq].Hash) {
t.Errorf("Block mismatch: %s != %s", toCopy.blocks[i].String(), blocks[eq].String())
for _, idx := range []int{1, 5, 6, 8} {
found := false
block := blocks[idx]
for _, toCopyBlock := range toCopy.blocks {
if string(toCopyBlock.Hash) == string(block.Hash) {
found = true
break
}
}
if !found {
t.Errorf("Did not find block %s", block.String())
}
}
}
@@ -187,13 +202,22 @@ func TestCopierFinder(t *testing.T) {
default:
}
// Verify that the right blocks went into the pull list
for i, eq := range []int{1, 5, 6, 8} {
if string(pulls[i].block.Hash) != string(blocks[eq].Hash) {
t.Errorf("Block %d mismatch: %s != %s", eq, pulls[i].block.String(), blocks[eq].String())
// Verify that the right blocks went into the pull list.
// They are pulled in random order.
for _, idx := range []int{1, 5, 6, 8} {
found := false
block := blocks[idx]
for _, pulledBlock := range pulls {
if string(pulledBlock.block.Hash) == string(block.Hash) {
found = true
break
}
}
if string(finish.file.Blocks[eq-1].Hash) != string(blocks[eq].Hash) {
t.Errorf("Block %d mismatch: %s != %s", eq, finish.file.Blocks[eq-1].String(), blocks[eq].String())
if !found {
t.Errorf("Did not find block %s", block.String())
}
if string(finish.file.Blocks[idx-1].Hash) != string(blocks[idx].Hash) {
t.Errorf("Block %d mismatch: %s != %s", idx, finish.file.Blocks[idx-1].String(), blocks[idx].String())
}
}
+184
View File
@@ -0,0 +1,184 @@
// 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 model
import (
"time"
"github.com/syncthing/syncthing/lib/protocol"
)
// sentFolderFileDownloadState represents a state of what we've announced as available
// to some remote device for a specific file.
type sentFolderFileDownloadState struct {
blockIndexes []int32
version protocol.Vector
updated time.Time
}
// sentFolderDownloadState represents a state of what we've announced as available
// to some remote device for a specific folder.
type sentFolderDownloadState struct {
files map[string]*sentFolderFileDownloadState
}
// update takes a set of currently active sharedPullerStates, and returns a list
// of updates which we need to send to the client to become up to date.
func (s *sentFolderDownloadState) update(pullers []*sharedPullerState) []protocol.FileDownloadProgressUpdate {
var name string
var updates []protocol.FileDownloadProgressUpdate
seen := make(map[string]struct{}, len(pullers))
for _, puller := range pullers {
name = puller.file.Name
seen[name] = struct{}{}
pullerBlockIndexes := puller.Available()
pullerVersion := puller.file.Version
pullerBlockIndexesUpdated := puller.AvailableUpdated()
localFile, ok := s.files[name]
// New file we haven't seen before
if !ok {
// Only send an update if the file actually has some blocks.
if len(pullerBlockIndexes) > 0 {
s.files[name] = &sentFolderFileDownloadState{
blockIndexes: pullerBlockIndexes,
updated: pullerBlockIndexesUpdated,
version: pullerVersion,
}
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: name,
Version: pullerVersion,
UpdateType: protocol.UpdateTypeAppend,
BlockIndexes: pullerBlockIndexes,
})
}
continue
}
// Existing file we've already sent an update for.
if pullerBlockIndexesUpdated.Equal(localFile.updated) && pullerVersion.Equal(localFile.version) {
// The file state hasn't changed, go to next.
continue
}
if !pullerVersion.Equal(localFile.version) {
// The version has changed, clean up whatever we had for the old
// file, and advertise the new file.
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: name,
Version: localFile.version,
UpdateType: protocol.UpdateTypeForget,
})
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: name,
Version: pullerVersion,
UpdateType: protocol.UpdateTypeAppend,
BlockIndexes: pullerBlockIndexes,
})
localFile.blockIndexes = pullerBlockIndexes
localFile.updated = pullerBlockIndexesUpdated
localFile.version = pullerVersion
continue
}
// Relies on the fact that sharedPullerState.Available() should always
// append.
newBlocks := pullerBlockIndexes[len(localFile.blockIndexes):]
localFile.blockIndexes = append(localFile.blockIndexes, newBlocks...)
localFile.updated = pullerBlockIndexesUpdated
// If there are new blocks, send the update.
if len(newBlocks) > 0 {
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: name,
Version: localFile.version,
UpdateType: protocol.UpdateTypeAppend,
BlockIndexes: newBlocks,
})
}
}
// For each file that we are tracking, see if there still is a puller for it
// if not, the file completed or errored out.
for name, info := range s.files {
_, ok := seen[name]
if !ok {
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: name,
Version: info.version,
UpdateType: protocol.UpdateTypeForget,
})
delete(s.files, name)
}
}
return updates
}
// destroy removes all stored state, and returns a set of updates we need to
// dispatch to clean up the state on the remote end.
func (s *sentFolderDownloadState) destroy() []protocol.FileDownloadProgressUpdate {
updates := make([]protocol.FileDownloadProgressUpdate, 0, len(s.files))
for name, info := range s.files {
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: name,
Version: info.version,
UpdateType: protocol.UpdateTypeForget,
})
delete(s.files, name)
}
return updates
}
// sentDownloadState represents a state of what we've announced as available
// to some remote device. It is used from within the progress emitter
// which only has one routine, hence is deemed threadsafe.
type sentDownloadState struct {
folderStates map[string]*sentFolderDownloadState
}
// update receives a folder, and a slice of pullers that are currently available
// for the given folder, and according to the state of what we've seen before
// returns a set of updates which we should send to the remote device to make
// it aware of everything that we currently have available.
func (s *sentDownloadState) update(folder string, pullers []*sharedPullerState) []protocol.FileDownloadProgressUpdate {
fs, ok := s.folderStates[folder]
if !ok {
fs = &sentFolderDownloadState{
files: make(map[string]*sentFolderFileDownloadState),
}
s.folderStates[folder] = fs
}
return fs.update(pullers)
}
// folders returns a set of folders this state is currently aware off.
func (s *sentDownloadState) folders() []string {
folders := make([]string, 0, len(s.folderStates))
for key := range s.folderStates {
folders = append(folders, key)
}
return folders
}
// cleanup cleans up all state related to a folder, and returns a set of updates
// which would clean up the state on the remote device.
func (s *sentDownloadState) cleanup(folder string) []protocol.FileDownloadProgressUpdate {
fs, ok := s.folderStates[folder]
if ok {
updates := fs.destroy()
delete(s.folderStates, folder)
return updates
}
return nil
}
+55 -18
View File
@@ -10,6 +10,7 @@ import (
"io"
"os"
"path/filepath"
"time"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/protocol"
@@ -30,15 +31,18 @@ type sharedPullerState struct {
sparse bool
// Mutable, must be locked for access
err error // The first error we hit
fd *os.File // The fd of the temp file
copyTotal int // Total number of copy actions for the whole job
pullTotal int // Total number of pull actions for the whole job
copyOrigin int // Number of blocks copied from the original file
copyNeeded int // Number of copy actions still pending
pullNeeded int // Number of block pulls still pending
closed bool // True if the file has been finalClosed.
mut sync.Mutex // Protects the above
err error // The first error we hit
fd *os.File // The fd of the temp file
copyTotal int // Total number of copy actions for the whole job
pullTotal int // Total number of pull actions for the whole job
copyOrigin int // Number of blocks copied from the original file
copyNeeded int // Number of copy actions still pending
pullNeeded int // Number of block pulls still pending
updated time.Time // Time when any of the counters above were last updated
closed bool // True if the file has been finalClosed.
available []int32 // Indexes of the blocks that are available in the temporary file
availableUpdated time.Time // Time when list of available blocks was last updated
mut sync.RWMutex // Protects the above
}
// A momentary state representing the progress of the puller
@@ -56,7 +60,7 @@ type pullerProgress struct {
// A lockedWriterAt synchronizes WriteAt calls with an external mutex.
// WriteAt() is goroutine safe by itself, but not against for example Close().
type lockedWriterAt struct {
mut *sync.Mutex
mut *sync.RWMutex
wr io.WriterAt
}
@@ -196,15 +200,19 @@ func (s *sharedPullerState) failLocked(context string, err error) {
}
func (s *sharedPullerState) failed() error {
s.mut.Lock()
defer s.mut.Unlock()
s.mut.RLock()
err := s.err
s.mut.RUnlock()
return s.err
return err
}
func (s *sharedPullerState) copyDone() {
func (s *sharedPullerState) copyDone(block protocol.BlockInfo) {
s.mut.Lock()
s.copyNeeded--
s.updated = time.Now()
s.available = append(s.available, int32(block.Offset/protocol.BlockSize))
s.availableUpdated = time.Now()
l.Debugln("sharedPullerState", s.folder, s.file.Name, "copyNeeded ->", s.copyNeeded)
s.mut.Unlock()
}
@@ -212,6 +220,7 @@ func (s *sharedPullerState) copyDone() {
func (s *sharedPullerState) copiedFromOrigin() {
s.mut.Lock()
s.copyOrigin++
s.updated = time.Now()
s.mut.Unlock()
}
@@ -221,13 +230,17 @@ func (s *sharedPullerState) pullStarted() {
s.copyNeeded--
s.pullTotal++
s.pullNeeded++
s.updated = time.Now()
l.Debugln("sharedPullerState", s.folder, s.file.Name, "pullNeeded start ->", s.pullNeeded)
s.mut.Unlock()
}
func (s *sharedPullerState) pullDone() {
func (s *sharedPullerState) pullDone(block protocol.BlockInfo) {
s.mut.Lock()
s.pullNeeded--
s.updated = time.Now()
s.available = append(s.available, int32(block.Offset/protocol.BlockSize))
s.availableUpdated = time.Now()
l.Debugln("sharedPullerState", s.folder, s.file.Name, "pullNeeded done ->", s.pullNeeded)
s.mut.Unlock()
}
@@ -265,10 +278,10 @@ func (s *sharedPullerState) finalClose() (bool, error) {
return true, s.err
}
// Returns the momentarily progress for the puller
// Progress returns the momentarily progress for the puller
func (s *sharedPullerState) Progress() *pullerProgress {
s.mut.Lock()
defer s.mut.Unlock()
s.mut.RLock()
defer s.mut.RUnlock()
total := s.reused + s.copyTotal + s.pullTotal
done := total - s.copyNeeded - s.pullNeeded
return &pullerProgress{
@@ -282,3 +295,27 @@ func (s *sharedPullerState) Progress() *pullerProgress {
BytesDone: db.BlocksToSize(done),
}
}
// Updated returns the time when any of the progress related counters was last updated.
func (s *sharedPullerState) Updated() time.Time {
s.mut.RLock()
t := s.updated
s.mut.RUnlock()
return t
}
// AvailableUpdated returns the time last time list of available blocks was updated
func (s *sharedPullerState) AvailableUpdated() time.Time {
s.mut.RLock()
t := s.availableUpdated
s.mut.RUnlock()
return t
}
// Available returns blocks available in the current temporary file
func (s *sharedPullerState) Available() []int32 {
s.mut.RLock()
blocks := s.available
s.mut.RUnlock()
return blocks
}
+3 -3
View File
@@ -16,7 +16,7 @@ import (
func TestSourceFileOK(t *testing.T) {
s := sharedPullerState{
realName: "testdata/foo",
mut: sync.NewMutex(),
mut: sync.NewRWMutex(),
}
fd, err := s.sourceFile()
@@ -45,7 +45,7 @@ func TestSourceFileOK(t *testing.T) {
func TestSourceFileBad(t *testing.T) {
s := sharedPullerState{
realName: "nonexistent",
mut: sync.NewMutex(),
mut: sync.NewRWMutex(),
}
fd, err := s.sourceFile()
@@ -71,7 +71,7 @@ func TestReadOnlyDir(t *testing.T) {
s := sharedPullerState{
tempName: "testdata/read_only_dir/.temp_name",
mut: sync.NewMutex(),
mut: sync.NewRWMutex(),
}
fd, err := s.tempFile()
+22
View File
@@ -0,0 +1,22 @@
// 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 nat
import (
"os"
"strings"
"github.com/syncthing/syncthing/lib/logger"
)
var (
l = logger.DefaultLogger.NewFacility("nat", "NAT discovery and port mapping")
)
func init() {
l.SetDebug("nat", strings.Contains(os.Getenv("STTRACE"), "nat") || os.Getenv("STTRACE") == "all")
}
+26
View File
@@ -0,0 +1,26 @@
// 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 nat
import (
"net"
"time"
)
type Protocol string
const (
TCP Protocol = "TCP"
UDP = "UDP"
)
type Device interface {
ID() string
GetLocalIPAddress() net.IP
AddPortMapping(protocol Protocol, internalPort, externalPort int, description string, duration time.Duration) (int, error)
GetExternalIPAddress() (net.IP, error)
}
+53
View File
@@ -0,0 +1,53 @@
// 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 nat
import (
"time"
"github.com/syncthing/syncthing/lib/sync"
)
type DiscoverFunc func(renewal, timeout time.Duration) []Device
var providers []DiscoverFunc
func Register(provider DiscoverFunc) {
providers = append(providers, provider)
}
func discoverAll(renewal, timeout time.Duration) map[string]Device {
wg := sync.NewWaitGroup()
wg.Add(len(providers))
c := make(chan Device)
done := make(chan struct{})
for _, discoverFunc := range providers {
go func(f DiscoverFunc) {
for _, dev := range f(renewal, timeout) {
c <- dev
}
wg.Done()
}(discoverFunc)
}
nats := make(map[string]Device)
go func() {
for dev := range c {
nats[dev.ID()] = dev
}
close(done)
}()
wg.Wait()
close(c)
<-done
return nats
}
+298
View File
@@ -0,0 +1,298 @@
// 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 nat
import (
"fmt"
"math/rand"
"net"
stdsync "sync"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
)
// Service runs a loop for discovery of IGDs (Internet Gateway Devices) and
// setup/renewal of a port mapping.
type Service struct {
id protocol.DeviceID
cfg *config.Wrapper
stop chan struct{}
immediate chan chan struct{}
timer *time.Timer
announce *stdsync.Once
mappings []*Mapping
mut sync.RWMutex
}
func NewService(id protocol.DeviceID, cfg *config.Wrapper) *Service {
return &Service{
id: id,
cfg: cfg,
immediate: make(chan chan struct{}),
timer: time.NewTimer(time.Second),
mut: sync.NewRWMutex(),
}
}
func (s *Service) Serve() {
s.timer.Reset(0)
s.stop = make(chan struct{})
s.announce = &stdsync.Once{}
for {
select {
case result := <-s.immediate:
s.process()
close(result)
case <-s.timer.C:
s.process()
case <-s.stop:
s.timer.Stop()
return
}
}
}
func (s *Service) process() {
// toRenew are mappings which are due for renewal
// toUpdate are the remaining mappings, which will only be updated if one of
// the old IGDs has gone away, or a new IGD has appeared, but only if we
// actually need to perform a renewal.
var toRenew, toUpdate []*Mapping
renewIn := time.Duration(s.cfg.Options().NATRenewalM) * time.Minute
if renewIn == 0 {
// We always want to do renewal so lets just pick a nice sane number.
renewIn = 30 * time.Minute
}
s.mut.RLock()
for _, mapping := range s.mappings {
if mapping.expires.Before(time.Now()) {
toRenew = append(toRenew, mapping)
} else {
toUpdate = append(toUpdate, mapping)
mappingRenewIn := mapping.expires.Sub(time.Now())
if mappingRenewIn < renewIn {
renewIn = mappingRenewIn
}
}
}
s.mut.RUnlock()
s.timer.Reset(renewIn)
// Don't do anything, unless we really need to renew
if len(toRenew) == 0 {
return
}
nats := discoverAll(time.Duration(s.cfg.Options().NATRenewalM)*time.Minute, time.Duration(s.cfg.Options().NATTimeoutS)*time.Second)
s.announce.Do(func() {
suffix := "s"
if len(nats) == 1 {
suffix = ""
}
l.Infoln("Detected", len(nats), "NAT device"+suffix)
})
for _, mapping := range toRenew {
s.updateMapping(mapping, nats, true)
}
for _, mapping := range toUpdate {
s.updateMapping(mapping, nats, false)
}
}
func (s *Service) Stop() {
close(s.stop)
}
func (s *Service) NewMapping(protocol Protocol, ip net.IP, port int) *Mapping {
mapping := &Mapping{
protocol: protocol,
address: Address{
IP: ip,
Port: port,
},
extAddresses: make(map[string]Address),
mut: sync.NewRWMutex(),
}
s.mut.Lock()
s.mappings = append(s.mappings, mapping)
s.mut.Unlock()
return mapping
}
// Sync forces the service to recheck all mappings.
func (s *Service) Sync() {
wait := make(chan struct{})
s.immediate <- wait
<-wait
}
// updateMapping compares the addresses of the existing mapping versus the natds
// discovered, and removes any addresses of natds that do not exist, or tries to
// acquire mappings for natds which the mapping was unaware of before.
// Optionally takes renew flag which indicates whether or not we should renew
// mappings with existing natds
func (s *Service) updateMapping(mapping *Mapping, nats map[string]Device, renew bool) {
var added, removed []Address
renewalTime := time.Duration(s.cfg.Options().NATRenewalM) * time.Minute
mapping.expires = time.Now().Add(renewalTime)
newAdded, newRemoved := s.verifyExistingMappings(mapping, nats, renew)
added = append(added, newAdded...)
removed = append(removed, newRemoved...)
newAdded, newRemoved = s.acquireNewMappings(mapping, nats)
added = append(added, newAdded...)
removed = append(removed, newRemoved...)
if len(added) > 0 || len(removed) > 0 {
mapping.notify(added, removed)
}
}
func (s *Service) verifyExistingMappings(mapping *Mapping, nats map[string]Device, renew bool) ([]Address, []Address) {
var added, removed []Address
leaseTime := time.Duration(s.cfg.Options().NATLeaseM) * time.Minute
for id, address := range mapping.addressMap() {
// Delete addresses for NATDevice's that do not exist anymore
nat, ok := nats[id]
if !ok {
mapping.removeAddress(id)
removed = append(removed, address)
continue
} else if renew {
// Only perform renewals on the nat's that have the right local IP
// address
localIP := nat.GetLocalIPAddress()
if !mapping.validGateway(localIP) {
l.Debugf("Skipping %s for %s because of IP mismatch. %s != %s", id, mapping, mapping.address.IP, localIP)
continue
}
l.Debugf("Renewing %s -> %s mapping on %s", mapping, address, id)
addr, err := s.tryNATDevice(nat, mapping.address.Port, address.Port, leaseTime)
if err != nil {
l.Debugf("Failed to renew %s -> mapping on %s", mapping, address, id)
mapping.removeAddress(id)
removed = append(removed, address)
continue
}
l.Debugf("Renewed %s -> %s mapping on %s", mapping, address, id)
if !addr.Equal(address) {
mapping.removeAddress(id)
mapping.setAddress(id, addr)
removed = append(removed, address)
added = append(added, address)
}
}
}
return added, removed
}
func (s *Service) acquireNewMappings(mapping *Mapping, nats map[string]Device) ([]Address, []Address) {
var added, removed []Address
leaseTime := time.Duration(s.cfg.Options().NATLeaseM) * time.Minute
addrMap := mapping.addressMap()
for id, nat := range nats {
if _, ok := addrMap[id]; ok {
continue
}
// Only perform mappings on the nat's that have the right local IP
// address
localIP := nat.GetLocalIPAddress()
if !mapping.validGateway(localIP) {
l.Debugf("Skipping %s for %s because of IP mismatch. %s != %s", id, mapping, mapping.address.IP, localIP)
continue
}
l.Debugf("Acquiring %s mapping on %s", mapping, id)
addr, err := s.tryNATDevice(nat, mapping.address.Port, 0, leaseTime)
if err != nil {
l.Debugf("Failed to acquire %s mapping on %s", mapping, id)
continue
}
l.Debugf("Acquired %s -> %s mapping on %s", mapping, addr, id)
mapping.setAddress(id, addr)
added = append(added, addr)
}
return added, removed
}
// tryNATDevice tries to acquire a port mapping for the given internal address to
// the given external port. If external port is 0, picks a pseudo-random port.
func (s *Service) tryNATDevice(natd Device, intPort, extPort int, leaseTime time.Duration) (Address, error) {
var err error
// Generate a predictable random which is based on device ID + local port
// number so that the ports we'd try to acquire for the mapping would always
// be the same.
predictableRand := rand.New(rand.NewSource(int64(s.id.Short()) + int64(intPort)))
if extPort != 0 {
// First try renewing our existing mapping, if we have one.
name := fmt.Sprintf("syncthing-%d", extPort)
port, err := natd.AddPortMapping(TCP, intPort, extPort, name, leaseTime)
if err == nil {
extPort = port
goto findIP
}
l.Debugln("Error extending lease on", natd.ID(), err)
}
for i := 0; i < 10; i++ {
// Then try up to ten random ports.
extPort = 1024 + predictableRand.Intn(65535-1024)
name := fmt.Sprintf("syncthing-%d", extPort)
port, err := natd.AddPortMapping(TCP, intPort, extPort, name, leaseTime)
if err == nil {
extPort = port
goto findIP
}
l.Debugln("Error getting new lease on", natd.ID(), err)
}
return Address{}, err
findIP:
ip, err := natd.GetExternalIPAddress()
if err != nil {
l.Debugln("Error getting external ip on", natd.ID(), err)
ip = nil
}
return Address{
IP: ip,
Port: extPort,
}, nil
}
+129
View File
@@ -0,0 +1,129 @@
// 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 nat
import (
"fmt"
"net"
"time"
"github.com/syncthing/syncthing/lib/sync"
)
type MappingChangeSubscriber func(*Mapping, []Address, []Address)
type Mapping struct {
protocol Protocol
address Address
extAddresses map[string]Address // NAT ID -> Address
expires time.Time
subscribers []MappingChangeSubscriber
mut sync.RWMutex
}
func (m *Mapping) setAddress(id string, address Address) {
m.mut.Lock()
if existing, ok := m.extAddresses[id]; !ok || !existing.Equal(address) {
l.Infof("New NAT port mapping: external %s address %s to local address %s.", m.protocol, address, m.address)
m.extAddresses[id] = address
}
m.mut.Unlock()
}
func (m *Mapping) removeAddress(id string) {
m.mut.Lock()
addr, ok := m.extAddresses[id]
if ok {
l.Infof("Removing NAT port mapping: external %s address %s, NAT %s is no longer available.", m.protocol, addr, id)
delete(m.extAddresses, id)
}
m.mut.Unlock()
}
func (m *Mapping) notify(added, removed []Address) {
m.mut.RLock()
for _, subscriber := range m.subscribers {
subscriber(m, added, removed)
}
m.mut.RUnlock()
}
func (m *Mapping) addressMap() map[string]Address {
m.mut.RLock()
addrMap := m.extAddresses
m.mut.RUnlock()
return addrMap
}
func (m *Mapping) Protocol() Protocol {
return m.protocol
}
func (m *Mapping) Address() Address {
return m.address
}
func (m *Mapping) ExternalAddresses() []Address {
m.mut.RLock()
addrs := make([]Address, 0, len(m.extAddresses))
for _, addr := range m.extAddresses {
addrs = append(addrs, addr)
}
m.mut.RUnlock()
return addrs
}
func (m *Mapping) OnChanged(subscribed MappingChangeSubscriber) {
m.mut.Lock()
m.subscribers = append(m.subscribers, subscribed)
m.mut.Unlock()
}
func (m *Mapping) String() string {
return fmt.Sprintf("%s %s", m.protocol, m.address)
}
func (m *Mapping) GoString() string {
return m.String()
}
// Checks if the mappings local IP address matches the IP address of the gateway
// For example, if we are explicitly listening on 192.168.0.12, there is no
// point trying to acquire a mapping on a gateway to which the local IP is
// 10.0.0.1. Fallback to true if any of the IPs is not there.
func (m *Mapping) validGateway(ip net.IP) bool {
if m.address.IP == nil || ip == nil || m.address.IP.IsUnspecified() || ip.IsUnspecified() {
return true
}
return m.address.IP.Equal(ip)
}
// Address is essentially net.TCPAddr yet is more general, and has a few helper
// methods which reduce boilerplate code.
type Address struct {
IP net.IP
Port int
}
func (a Address) Equal(b Address) bool {
return a.Port == b.Port && a.IP.Equal(b.IP)
}
func (a Address) String() string {
var ipStr string
if a.IP == nil {
ipStr = net.IPv4zero.String()
} else {
ipStr = a.IP.String()
}
return net.JoinHostPort(ipStr, fmt.Sprintf("%d", a.Port))
}
func (a Address) GoString() string {
return a.String()
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright (C) 2016 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 nat
import (
"net"
"testing"
)
func TestMappingValidGateway(t *testing.T) {
a := net.ParseIP("10.0.0.1")
b := net.ParseIP("192.168.0.1")
tests := []struct {
mappingLocalIP net.IP
gatewayLocalIP net.IP
expected bool
}{
// Any of the IPs is nil or unspecified implies correct
{nil, nil, true},
{net.IPv4zero, net.IPv4zero, true},
{nil, net.IPv4zero, true},
{net.IPv4zero, nil, true},
{a, nil, true},
{b, nil, true},
{a, net.IPv4zero, true},
{b, net.IPv4zero, true},
{nil, a, true},
{nil, b, true},
{net.IPv4zero, a, true},
{net.IPv4zero, b, true},
// IPs are the same implies correct
{a, a, true},
{b, b, true},
// IPs are specified and different, implies incorrect
{a, b, false},
{b, a, false},
}
for _, test := range tests {
m := Mapping{
address: Address{
IP: test.mappingLocalIP,
},
}
result := m.validGateway(test.gatewayLocalIP)
if result != test.expected {
t.Errorf("Incorrect: local %s gateway %s result %t expected %t", test.mappingLocalIP, test.gatewayLocalIP, result, test.expected)
}
}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright (C) 2016 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 pmp
import (
"os"
"strings"
"github.com/syncthing/syncthing/lib/logger"
)
var (
l = logger.DefaultLogger.NewFacility("pmp", "NAT-PMP discovery and port mapping")
)
func init() {
l.SetDebug("pmp", strings.Contains(os.Getenv("STTRACE"), "pmp") || os.Getenv("STTRACE") == "all")
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright (C) 2016 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 pmp
import (
"fmt"
"net"
"strings"
"time"
"github.com/AudriusButkevicius/go-nat-pmp"
"github.com/jackpal/gateway"
"github.com/syncthing/syncthing/lib/nat"
)
func init() {
nat.Register(Discover)
}
func Discover(renewal, timeout time.Duration) []nat.Device {
ip, err := gateway.DiscoverGateway()
if err != nil {
l.Debugln("Failed to discover gateway", err)
return nil
}
l.Debugln("Discovered gateway at", ip)
c := natpmp.NewClient(ip, timeout)
// Try contacting the gateway, if it does not respond, assume it does not
// speak NAT-PMP.
_, err = c.GetExternalAddress()
if err != nil && strings.Contains(err.Error(), "Timed out") {
l.Debugln("Timeout trying to get external address, assume no NAT-PMP available")
return nil
}
var localIP net.IP
// Port comes from the natpmp package
conn, err := net.DialTimeout("udp", net.JoinHostPort(ip.String(), "5351"), timeout)
if err == nil {
conn.Close()
localIPAddress, _, err := net.SplitHostPort(conn.LocalAddr().String())
if err == nil {
localIP = net.ParseIP(localIPAddress)
} else {
l.Debugln("Failed to lookup local IP", err)
}
}
return []nat.Device{&wrapper{
renewal: renewal,
localIP: localIP,
gatewayIP: ip,
client: c,
}}
}
type wrapper struct {
renewal time.Duration
localIP net.IP
gatewayIP net.IP
client *natpmp.Client
}
func (w *wrapper) ID() string {
return fmt.Sprintf("NAT-PMP@%s", w.gatewayIP.String())
}
func (w *wrapper) GetLocalIPAddress() net.IP {
return w.localIP
}
func (w *wrapper) AddPortMapping(protocol nat.Protocol, internalPort, externalPort int, description string, duration time.Duration) (int, error) {
// NAT-PMP says that if duration is 0, the mapping is actually removed
// Swap the zero with the renewal value, which should make the lease for the
// exact amount of time between the calls.
if duration == 0 {
duration = w.renewal
}
result, err := w.client.AddPortMapping(strings.ToLower(string(protocol)), internalPort, externalPort, int(duration/time.Second))
port := 0
if result != nil {
port = int(result.MappedExternalPort)
}
return port, err
}
func (w *wrapper) GetExternalIPAddress() (net.IP, error) {
result, err := w.client.GetExternalAddress()
ip := net.IPv4zero
if result != nil {
ip = net.IPv4(
result.ExternalIPAddress[0],
result.ExternalIPAddress[1],
result.ExternalIPAddress[2],
result.ExternalIPAddress[3],
)
}
return ip, err
}
+4 -25
View File
@@ -2,10 +2,7 @@
package protocol
import (
"io"
"time"
)
import "time"
type TestModel struct {
data []byte
@@ -52,6 +49,9 @@ func (t *TestModel) Close(deviceID DeviceID, err error) {
func (t *TestModel) ClusterConfig(deviceID DeviceID, config ClusterConfigMessage) {
}
func (t *TestModel) DownloadProgress(DeviceID, string, []FileDownloadProgressUpdate, uint32, []Option) {
}
func (t *TestModel) closedError() error {
select {
case <-t.closedCh:
@@ -60,24 +60,3 @@ func (t *TestModel) closedError() error {
return nil // Timeout
}
}
type ErrPipe struct {
io.PipeWriter
written int
max int
err error
closed bool
}
func (e *ErrPipe) Write(data []byte) (int, error) {
if e.closed {
return 0, e.err
}
if e.written+len(data) > e.max {
n, _ := e.PipeWriter.Write(data[:e.max-e.written])
e.PipeWriter.CloseWithError(e.err)
e.closed = true
return n, e.err
}
return e.PipeWriter.Write(data)
}
+14
View File
@@ -138,6 +138,13 @@ type ClusterConfigMessage struct {
Options []Option // max:64
}
type DownloadProgressMessage struct {
Folder string // max:64
Updates []FileDownloadProgressUpdate // max:1000000
Flags uint32
Options []Option // max:64
}
func (o *ClusterConfigMessage) GetOption(key string) string {
for _, option := range o.Options {
if option.Key == key {
@@ -166,6 +173,13 @@ type Device struct {
Options []Option // max:64
}
type FileDownloadProgressUpdate struct {
UpdateType uint32
Name string // max:8192
Version Vector
BlockIndexes []int32 // max:1000000
}
type Option struct {
Key string // max:64
Value string // max:1024
+232
View File
@@ -690,6 +690,135 @@ func (o *ClusterConfigMessage) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
/*
DownloadProgressMessage 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Folder (length + padded data) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Number of Updates |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Zero or more FileDownloadProgressUpdate Structures \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Flags |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Number of Options |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Zero or more Option Structures \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct DownloadProgressMessage {
string Folder<64>;
FileDownloadProgressUpdate Updates<1000000>;
unsigned int Flags;
Option Options<64>;
}
*/
func (o DownloadProgressMessage) XDRSize() int {
return 4 + len(o.Folder) + xdr.Padding(len(o.Folder)) +
4 + xdr.SizeOfSlice(o.Updates) + 4 +
4 + xdr.SizeOfSlice(o.Options)
}
func (o DownloadProgressMessage) MarshalXDR() ([]byte, error) {
buf := make([]byte, o.XDRSize())
m := &xdr.Marshaller{Data: buf}
return buf, o.MarshalXDRInto(m)
}
func (o DownloadProgressMessage) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o DownloadProgressMessage) MarshalXDRInto(m *xdr.Marshaller) error {
if l := len(o.Folder); l > 64 {
return xdr.ElementSizeExceeded("Folder", l, 64)
}
m.MarshalString(o.Folder)
if l := len(o.Updates); l > 1000000 {
return xdr.ElementSizeExceeded("Updates", l, 1000000)
}
m.MarshalUint32(uint32(len(o.Updates)))
for i := range o.Updates {
if err := o.Updates[i].MarshalXDRInto(m); err != nil {
return err
}
}
m.MarshalUint32(o.Flags)
if l := len(o.Options); l > 64 {
return xdr.ElementSizeExceeded("Options", l, 64)
}
m.MarshalUint32(uint32(len(o.Options)))
for i := range o.Options {
if err := o.Options[i].MarshalXDRInto(m); err != nil {
return err
}
}
return m.Error
}
func (o *DownloadProgressMessage) UnmarshalXDR(bs []byte) error {
u := &xdr.Unmarshaller{Data: bs}
return o.UnmarshalXDRFrom(u)
}
func (o *DownloadProgressMessage) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
o.Folder = u.UnmarshalStringMax(64)
_UpdatesSize := int(u.UnmarshalUint32())
if _UpdatesSize < 0 {
return xdr.ElementSizeExceeded("Updates", _UpdatesSize, 1000000)
} else if _UpdatesSize == 0 {
o.Updates = nil
} else {
if _UpdatesSize > 1000000 {
return xdr.ElementSizeExceeded("Updates", _UpdatesSize, 1000000)
}
if _UpdatesSize <= len(o.Updates) {
o.Updates = o.Updates[:_UpdatesSize]
} else {
o.Updates = make([]FileDownloadProgressUpdate, _UpdatesSize)
}
for i := range o.Updates {
(&o.Updates[i]).UnmarshalXDRFrom(u)
}
}
o.Flags = u.UnmarshalUint32()
_OptionsSize := int(u.UnmarshalUint32())
if _OptionsSize < 0 {
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
} else if _OptionsSize == 0 {
o.Options = nil
} else {
if _OptionsSize > 64 {
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
}
if _OptionsSize <= len(o.Options) {
o.Options = o.Options[:_OptionsSize]
} else {
o.Options = make([]Option, _OptionsSize)
}
for i := range o.Options {
(&o.Options[i]).UnmarshalXDRFrom(u)
}
}
return u.Error
}
/*
Folder Structure:
0 1 2 3
@@ -996,6 +1125,109 @@ func (o *Device) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
/*
FileDownloadProgressUpdate 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Update Type |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Name (length + padded data) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Vector Structure \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Number of Block Indexes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
| Block Indexes (n items) |
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct FileDownloadProgressUpdate {
unsigned int UpdateType;
string Name<8192>;
Vector Version;
int BlockIndexes<1000000>;
}
*/
func (o FileDownloadProgressUpdate) XDRSize() int {
return 4 +
4 + len(o.Name) + xdr.Padding(len(o.Name)) +
o.Version.XDRSize() +
4 + len(o.BlockIndexes)*4
}
func (o FileDownloadProgressUpdate) MarshalXDR() ([]byte, error) {
buf := make([]byte, o.XDRSize())
m := &xdr.Marshaller{Data: buf}
return buf, o.MarshalXDRInto(m)
}
func (o FileDownloadProgressUpdate) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o FileDownloadProgressUpdate) MarshalXDRInto(m *xdr.Marshaller) error {
m.MarshalUint32(o.UpdateType)
if l := len(o.Name); l > 8192 {
return xdr.ElementSizeExceeded("Name", l, 8192)
}
m.MarshalString(o.Name)
if err := o.Version.MarshalXDRInto(m); err != nil {
return err
}
if l := len(o.BlockIndexes); l > 1000000 {
return xdr.ElementSizeExceeded("BlockIndexes", l, 1000000)
}
m.MarshalUint32(uint32(len(o.BlockIndexes)))
for i := range o.BlockIndexes {
m.MarshalUint32(uint32(o.BlockIndexes[i]))
}
return m.Error
}
func (o *FileDownloadProgressUpdate) UnmarshalXDR(bs []byte) error {
u := &xdr.Unmarshaller{Data: bs}
return o.UnmarshalXDRFrom(u)
}
func (o *FileDownloadProgressUpdate) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
o.UpdateType = u.UnmarshalUint32()
o.Name = u.UnmarshalStringMax(8192)
(&o.Version).UnmarshalXDRFrom(u)
_BlockIndexesSize := int(u.UnmarshalUint32())
if _BlockIndexesSize < 0 {
return xdr.ElementSizeExceeded("BlockIndexes", _BlockIndexesSize, 1000000)
} else if _BlockIndexesSize == 0 {
o.BlockIndexes = nil
} else {
if _BlockIndexesSize > 1000000 {
return xdr.ElementSizeExceeded("BlockIndexes", _BlockIndexesSize, 1000000)
}
if _BlockIndexesSize <= len(o.BlockIndexes) {
o.BlockIndexes = o.BlockIndexes[:_BlockIndexesSize]
} else {
o.BlockIndexes = make([]int32, _BlockIndexesSize)
}
for i := range o.BlockIndexes {
o.BlockIndexes[i] = int32(u.UnmarshalUint32())
}
}
return u.Error
}
/*
Option Structure:
0 1 2 3
+4 -12
View File
@@ -9,32 +9,24 @@ package protocol
import "golang.org/x/text/unicode/norm"
type nativeModel struct {
next Model
Model
}
func (m nativeModel) Index(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
for i := range files {
files[i].Name = norm.NFD.String(files[i].Name)
}
m.next.Index(deviceID, folder, files, flags, options)
m.Model.Index(deviceID, folder, files, flags, options)
}
func (m nativeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
for i := range files {
files[i].Name = norm.NFD.String(files[i].Name)
}
m.next.IndexUpdate(deviceID, folder, files, flags, options)
m.Model.IndexUpdate(deviceID, folder, files, flags, options)
}
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, flags uint32, options []Option, buf []byte) error {
name = norm.NFD.String(name)
return m.next.Request(deviceID, folder, name, offset, hash, flags, options, buf)
}
func (m nativeModel) ClusterConfig(deviceID DeviceID, config ClusterConfigMessage) {
m.next.ClusterConfig(deviceID, config)
}
func (m nativeModel) Close(deviceID DeviceID, err error) {
m.next.Close(deviceID, err)
return m.Model.Request(deviceID, folder, name, offset, hash, flags, options, buf)
}
+1 -21
View File
@@ -7,25 +7,5 @@ package protocol
// Normal Unixes uses NFC and slashes, which is the wire format.
type nativeModel struct {
next Model
}
func (m nativeModel) Index(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
m.next.Index(deviceID, folder, files, flags, options)
}
func (m nativeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
m.next.IndexUpdate(deviceID, folder, files, flags, options)
}
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, flags uint32, options []Option, buf []byte) error {
return m.next.Request(deviceID, folder, name, offset, hash, flags, options, buf)
}
func (m nativeModel) ClusterConfig(deviceID DeviceID, config ClusterConfigMessage) {
m.next.ClusterConfig(deviceID, config)
}
func (m nativeModel) Close(deviceID DeviceID, err error) {
m.next.Close(deviceID, err)
Model
}
+4 -12
View File
@@ -21,30 +21,22 @@ var disallowedCharacters = string([]rune{
})
type nativeModel struct {
next Model
Model
}
func (m nativeModel) Index(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
fixupFiles(folder, files)
m.next.Index(deviceID, folder, files, flags, options)
m.Model.Index(deviceID, folder, files, flags, options)
}
func (m nativeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
fixupFiles(folder, files)
m.next.IndexUpdate(deviceID, folder, files, flags, options)
m.Model.IndexUpdate(deviceID, folder, files, flags, options)
}
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, flags uint32, options []Option, buf []byte) error {
name = filepath.FromSlash(name)
return m.next.Request(deviceID, folder, name, offset, hash, flags, options, buf)
}
func (m nativeModel) ClusterConfig(deviceID DeviceID, config ClusterConfigMessage) {
m.next.ClusterConfig(deviceID, config)
}
func (m nativeModel) Close(deviceID DeviceID, err error) {
m.next.Close(deviceID, err)
return m.Model.Request(deviceID, folder, name, offset, hash, flags, options, buf)
}
func fixupFiles(folder string, files []FileInfo) {

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