Compare commits

...
41 Commits
Author SHA1 Message Date
Audrius Butkevicius 6c0a973ac3 Merge pull request #2480 from calmh/shortdblabels
Change database folder label format
2016-01-10 01:33:56 +00:00
Jakob Borg 3ca46c29c3 Merge pull request #2680 from calmh/xunit
Add XUnit compatible test results
2016-01-10 01:00:07 +01:00
Jakob Borg 837fde70ae Add XUnit compatible test output 2016-01-10 00:50:43 +01:00
Jakob Borg 1e52cc474f Merge pull request #2677 from nrm21/default_sync
Added STNODEFAULTFOLDER envvar to skip default folder creation
2016-01-09 13:24:53 +01:00
Nate Morrison 76807006be Added STNODEFAULTFOLDER envvar to skip default folder creation on new
install.
2016-01-08 20:11:06 -05:00
Audrius Butkevicius 0d35fe0f21 Merge pull request #2676 from calmh/fix2667
More fine grained locking in discovery cache (fixes #2667)
2016-01-09 00:58:14 +00:00
Jakob Borg 370b0fc5da More fine grained locking in discovery cache (fixes #2667)
We only need to protect the integrity of the "finders" and "caches"
slices, and for that we only need an RLock except while actually
appending to them. The actual finders and caches are concurrency safe on
their own.
2016-01-09 00:56:03 +01:00
Audrius Butkevicius 25b3c09f6a Merge pull request #2670 from calmh/noresolve
Don't unnecessary resolve destination address
2016-01-07 13:27:31 +00:00
Jakob Borg 576c365753 Don't resolve destination address until we need to (fixes #2671) 2016-01-07 12:32:10 +01:00
Jakob Borg bc2ed60b92 Translations for relaying stuff 2016-01-07 10:28:21 +01:00
Jakob Borg a4385100c4 HTML attribute typo 2016-01-05 11:19:27 +01:00
Audrius Butkevicius 4c01709cdf Merge pull request #2664 from calmh/fix2433
Add relaying to main settings dialog (fixes #2433)
2016-01-05 09:25:11 +00:00
Jakob Borg 1df924f4f8 Add relaying to main settings dialog (fixes #2433) 2016-01-05 10:08:56 +01:00
Audrius Butkevicius 400bfe9251 Merge pull request #2656 from calmh/csrf
Don't allow in use CSRF tokens to expire (fixes #1008)
2016-01-03 21:24:37 +00:00
Jakob Borg 6e1d364d60 Don't allow in use CSRF tokens to expire (fixes #1008) 2016-01-03 22:03:02 +01:00
Audrius Butkevicius 504ad86648 Merge pull request #2655 from calmh/fix2605
Don't conflict copy conflict copies (fixes #2605)
2016-01-03 20:44:05 +00:00
Jakob Borg 096b2d73cd Don't conflict copy conflict copies (fixes #2605) 2016-01-03 21:16:31 +01:00
Audrius Butkevicius 5754d31d0f Merge pull request #2653 from calmh/fix2604
Change default max conflicts to 10 (fixes #2604)
2016-01-03 20:13:51 +00:00
Jakob Borg ed3ed1f90a Change default max conflicts to 10 (fixes #2604) 2016-01-03 21:10:25 +01:00
Jakob Borg adb1227b2e Update kardianos/osext (fixes #2650) 2016-01-03 19:59:56 +01:00
Jakob Borg ac190b2e39 Change DB label format (index folders, devices) 2016-01-03 19:32:40 +01:00
Jakob Borg 18ae87962d Clarify points of contact 2016-01-03 13:07:22 +01:00
Jakob Borg 4673862981 Update docs & translations 2016-01-03 09:56:33 +01:00
Jakob Borg 212d7257e9 Pretty print the new architectures 2016-01-01 21:19:15 +01:00
Audrius Butkevicius 9df8d0848a Merge pull request #2641 from calmh/archs
Also build linux-arm64, linux-ppc64, linux-ppc64le
2016-01-01 20:08:08 +00:00
Jakob Borg ded7abb1f6 Also build linux-arm64, linux-ppc64, linux-ppc64le 2016-01-01 20:56:31 +01:00
Audrius Butkevicius 44d5a61cfe Merge pull request #2639 from calmh/minihashalgo
Detect nonstandard hash algo and stop folder (ref #2314)
2016-01-01 19:42:08 +00:00
Jakob Borg 0db80710aa Detect nonstandard hash algo and stop folder (ref #2314) 2016-01-01 20:14:31 +01:00
Jakob Borg 7c47eff112 Update lang-en.json for translations 2016-01-01 20:13:44 +01:00
Jakob Borg d81849ab3b Merge pull request #2631 from AudriusButkevicius/igwarn
Don't warn about failed ignores if folder unhealthy (fixes #2630)
2016-01-01 16:48:31 +01:00
Audrius Butkevicius dbb3f80995 Merge pull request #2193 from AudriusButkevicius/lans
Remove windows specialisation from osutil.GetLans (fixes #2192)
2016-01-01 15:01:23 +00:00
Audrius Butkevicius 80b9a3e00b Don't warn about failed ignores if folder unhealthy (fixes #2630) 2016-01-01 12:59:13 +00:00
Jakob Borg be5e5d837b Merge branch 'pr/2625'
* pr/2625:
  Show device ID QR code from edit dialog (fixes #1494)
2016-01-01 11:24:49 +01:00
Kevin AllenandJakob Borg ffc4a60bc6 Show device ID QR code from edit dialog (fixes #1494) 2016-01-01 11:24:09 +01:00
Audrius Butkevicius 1edfa4474f Merge pull request #2632 from calmh/fix2627
Ensure loaded config is free of duplicate devices (fixes #2627)
2015-12-31 08:37:52 +00:00
Jakob Borg 18e70f4e79 Ensure loaded config is free of duplicate devices (fixes #2627) 2015-12-31 09:17:17 +01:00
Jakob Borg 62a81cfdd1 Update lang-en.json and assets 2015-12-30 08:54:09 +01:00
Jakob Borg 6365a026c1 Merge branch 'pr/2628'
* pr/2628:
  Disallow adding duplicate device ID in GUI
2015-12-30 08:53:29 +01:00
Jakob Borg 7c89193398 Add ironmig 2015-12-30 08:53:19 +01:00
Kevin AllenandJakob Borg 4a6f1718b8 Disallow adding duplicate device ID in GUI
Adds check in valid device id to check for uniqueness
2015-12-30 08:52:11 +01:00
Audrius Butkevicius cec87be4e3 Remove windows specialisation from osutil.GetLans (fixes #2192) 2015-12-20 18:10:02 +00:00
97 changed files with 1415 additions and 401 deletions
+1
View File
@@ -46,6 +46,7 @@ Jochen Voss <voss@seehuhn.de>
Johan Vromans <jvromans@squirrel.nl>
Karol Różycki <rozycki.karol@gmail.com>
Ken'ichi Kamada <kamada@nanohz.org>
Kevin Allen <kma1660@gmail.com>
Lode Hoste <zillode@zillode.be>
Lord Landon Agahnim <lordlandon@gmail.com>
Marc Laporte <marc@marclaporte.com> <marc@laporte.name>
+1 -1
View File
@@ -31,7 +31,7 @@
},
{
"ImportPath": "github.com/kardianos/osext",
"Rev": "431e263e413efe4446ede50cec4819b26659fbe7"
"Rev": "29ae4ffbc9a6fe9fb2bc5029050ce6996ea1d3bc"
},
{
"ImportPath": "github.com/rcrowley/go-metrics",
+1 -3
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux netbsd solaris dragonfly freebsd
// +build linux netbsd solaris dragonfly
package osext
@@ -27,8 +27,6 @@ func executable() (string, error) {
return execpath, nil
case "netbsd":
return os.Readlink("/proc/curproc/exe")
case "freebsd":
return os.Readlink("/proc/curproc/file")
case "dragonfly":
return os.Readlink("/proc/curproc/file")
case "solaris":
+1 -1
View File
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin openbsd
// +build darwin freebsd openbsd
package osext
+1
View File
@@ -30,6 +30,7 @@ frioux <frew@afoolishmanifesto.com> <frioux@gmail.com>
fti7 <frank@isemann.name>
gillisig <gilli@vx.is>
hadogenes <szafar@linux.pl>
ironmig <kma1660@gmail.com>
jarlebring <jarlebring@gmail.com>
jedie <github.com@jensdiemer.de> <git@jensdiemer.de>
jgke <jgke@jgke.fi>
+10 -3
View File
@@ -27,8 +27,12 @@ Take a look at the [getting started guide][2].
There are a few examples for keeping Syncthing running in the background
on your system in [the etc directory][3].
There is an IRC channel, `#syncthing` on [Freenode][4], for talking directly
to developers and users.
## Getting in Touch
The first and best point of contact is the [Forum][8]. There is also an IRC
channel, `#syncthing` on [Freenode][4] (with a [web client][9]), for talking
directly to developers and users. If you've found something that is clearly a
bug, feel free to report it in the [GitHub issue tracker][10].
## Building
@@ -51,7 +55,10 @@ All code is licensed under the [MPLv2 License][7].
[1]: http://docs.syncthing.net/specs/bep-v1.html
[2]: http://docs.syncthing.net/intro/getting-started.html
[3]: https://github.com/syncthing/syncthing/blob/master/etc
[4]: https://webchat.freenode.net/
[4]: http://www.freenode.net/irc_servers.shtml
[5]: http://docs.syncthing.net/dev/building.html
[6]: http://docs.syncthing.net/
[7]: https://github.com/syncthing/syncthing/blob/master/LICENSE
[8]: https://forum.syncthing.net/
[9]: https://kiwiirc.com/client/irc.freenode.net/#syncthing
[10]: https://github.com/syncthing/syncthing/issues
+2 -1
View File
@@ -64,7 +64,7 @@ func main() {
flag.Parse()
switch goarch {
case "386", "amd64", "arm":
case "386", "amd64", "arm", "arm64", "ppc64", "ppc64le":
break
default:
log.Printf("Unknown goarch %q; proceed with caution!", goarch)
@@ -182,6 +182,7 @@ func setup() {
runPrint("go", "get", "-v", "github.com/tools/godep")
runPrint("go", "get", "-v", "github.com/axw/gocov/gocov")
runPrint("go", "get", "-v", "github.com/AlekSi/gocov-xml")
runPrint("go", "get", "-v", "bitbucket.org/tebeka/go2xunit")
}
func test(pkg string) {
+10 -1
View File
@@ -77,7 +77,7 @@ case "${1:-default}" in
platforms=(
darwin-amd64 dragonfly-amd64 freebsd-amd64 linux-amd64 netbsd-amd64 openbsd-amd64 solaris-amd64 windows-amd64
freebsd-386 linux-386 netbsd-386 openbsd-386 windows-386
linux-arm
linux-arm linux-arm64 linux-ppc64 linux-ppc64le
)
for plat in "${platforms[@]}"; do
@@ -125,6 +125,15 @@ case "${1:-default}" in
fi
;;
test-xunit)
ulimit -t 600 &>/dev/null || true
ulimit -d 512000 &>/dev/null || true
ulimit -m 512000 &>/dev/null || true
(GOPATH="$(pwd)/Godeps/_workspace:$GOPATH" go test -v -race ./lib/... ./cmd/... || true) > tests.out
go2xunit -output tests.xml -fail < tests.out
;;
docker-all)
img=${DOCKERIMG:-syncthing/build:latest}
docker run --rm -h syncthing-builder -u $(id -u) -t \
+26 -5
View File
@@ -17,9 +17,16 @@ import (
"github.com/syncthing/syncthing/lib/sync"
)
// csrfTokens is a list of valid tokens. It is sorted so that the most
// recently used token is first in the list. New tokens are added to the front
// of the list (as it is the most recently used at that time). The list is
// pruned to a maximum of maxCsrfTokens, throwing away the least recently used
// tokens.
var csrfTokens []string
var csrfMut = sync.NewMutex()
const maxCsrfTokens = 25
// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
// the request with 403. For / and /index.html, set a new CSRF cookie if none
// is currently set.
@@ -36,6 +43,7 @@ func csrfMiddleware(unique, prefix, apiKey string, next http.Handler) http.Handl
if !strings.HasPrefix(r.URL.Path, prefix) {
cookie, err := r.Cookie("CSRF-Token-" + unique)
if err != nil || !validCsrfToken(cookie.Value) {
httpl.Debugln("new CSRF cookie in response to request for", r.URL)
cookie = &http.Cookie{
Name: "CSRF-Token-" + unique,
Value: newCsrfToken(),
@@ -47,7 +55,13 @@ func csrfMiddleware(unique, prefix, apiKey string, next http.Handler) http.Handl
}
if r.Method == "GET" {
// Allow GET requests unconditionally
// Allow GET requests unconditionally, but if we got the CSRF
// token cookie do the verification anyway so we keep the
// csrfTokens list sorted by recent usage. We don't care about the
// outcome of the validity check.
if cookie, err := r.Cookie("CSRF-Token-" + unique); err == nil {
validCsrfToken(cookie.Value)
}
next.ServeHTTP(w, r)
return
}
@@ -66,8 +80,15 @@ func csrfMiddleware(unique, prefix, apiKey string, next http.Handler) http.Handl
func validCsrfToken(token string) bool {
csrfMut.Lock()
defer csrfMut.Unlock()
for _, t := range csrfTokens {
for i, t := range csrfTokens {
if t == token {
if i > 0 {
// Move this token to the head of the list. Copy the tokens at
// the front one step to the right and then replace the token
// at the head.
copy(csrfTokens[1:], csrfTokens[:i+1])
csrfTokens[0] = token
}
return true
}
}
@@ -78,9 +99,9 @@ func newCsrfToken() string {
token := randomString(32)
csrfMut.Lock()
csrfTokens = append(csrfTokens, token)
if len(csrfTokens) > 10 {
csrfTokens = csrfTokens[len(csrfTokens)-10:]
csrfTokens = append([]string{token}, csrfTokens...)
if len(csrfTokens) > maxCsrfTokens {
csrfTokens = csrfTokens[:maxCsrfTokens]
}
defer csrfMut.Unlock()
+42
View File
@@ -0,0 +1,42 @@
// 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 main
import "testing"
func TestCSRFToken(t *testing.T) {
t1 := newCsrfToken()
t2 := newCsrfToken()
t3 := newCsrfToken()
if !validCsrfToken(t3) {
t.Fatal("t3 should be valid")
}
for i := 0; i < 250; i++ {
if i%5 == 0 {
// t1 and t2 should remain valid by virtue of us checking them now
// and then.
if !validCsrfToken(t1) {
t.Fatal("t1 should be valid at iteration", i)
}
if !validCsrfToken(t2) {
t.Fatal("t2 should be valid at iteration", i)
}
}
// The newly generated token is always valid
t4 := newCsrfToken()
if !validCsrfToken(t4) {
t.Fatal("t4 should be valid at iteration", i)
}
}
if validCsrfToken(t3) {
t.Fatal("t3 should have expired by now")
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ var locations = map[locationEnum]string{
locKeyFile: "${config}/key.pem",
locHTTPSCertFile: "${config}/https-cert.pem",
locHTTPSKeyFile: "${config}/https-key.pem",
locDatabase: "${config}/index-v0.11.0.db",
locDatabase: "${config}/index-v0.13.0.db",
locLogFile: "${config}/syncthing.log", // -logfile on Windows
locCsrfTokens: "${config}/csrftokens.txt",
locPanicLog: "${config}/panic-${timestamp}.log",
+51 -34
View File
@@ -146,34 +146,38 @@ Development Settings
The following environment variables modify syncthing's behavior in ways that
are mostly useful for developers. Use with care.
STGUIASSETS Directory to load GUI assets from. Overrides compiled in
assets.
STNODEFAULTFOLDER Don't create a default folder when starting for the first
time. This variable will be ignored anytime after the first
run.
STTRACE A comma separated string of facilities to trace. The valid
facility strings listed below.
STGUIASSETS Directory to load GUI assets from. Overrides compiled in
assets.
STPROFILER Set to a listen address such as "127.0.0.1:9090" to start the
profiler with HTTP access.
STTRACE A comma separated string of facilities to trace. The valid
facility strings listed below.
STCPUPROFILE Write a CPU profile to cpu-$pid.pprof on exit.
STPROFILER Set to a listen address such as "127.0.0.1:9090" to start the
profiler with HTTP access.
STHEAPPROFILE Write heap profiles to heap-$pid-$timestamp.pprof each time
heap usage increases.
STCPUPROFILE Write a CPU profile to cpu-$pid.pprof on exit.
STBLOCKPROFILE Write block profiles to block-$pid-$timestamp.pprof every 20
seconds.
STHEAPPROFILE Write heap profiles to heap-$pid-$timestamp.pprof each time
heap usage increases.
STPERFSTATS Write running performance statistics to perf-$pid.csv. Not
supported on Windows.
STBLOCKPROFILE Write block profiles to block-$pid-$timestamp.pprof every 20
seconds.
STNOUPGRADE Disable automatic upgrades.
STPERFSTATS Write running performance statistics to perf-$pid.csv. Not
supported on Windows.
GOMAXPROCS Set the maximum number of CPU cores to use. Defaults to all
available CPU cores.
STNOUPGRADE Disable automatic upgrades.
GOGC Percentage of heap growth at which to trigger GC. Default is
100. Lower numbers keep peak memory usage down, at the price
of CPU usage (ie. performance).
GOMAXPROCS Set the maximum number of CPU cores to use. Defaults to all
available CPU cores.
GOGC Percentage of heap growth at which to trigger GC. Default is
100. Lower numbers keep peak memory usage down, at the price
of CPU usage (ie. performance).
Debugging Facilities
@@ -186,8 +190,9 @@ The following are valid values for the STTRACE variable:
// Environment options
var (
noUpgrade = os.Getenv("STNOUPGRADE") != ""
innerProcess = os.Getenv("STNORESTART") != "" || os.Getenv("STMONITORED") != ""
noUpgrade = os.Getenv("STNOUPGRADE") != ""
innerProcess = os.Getenv("STNORESTART") != "" || os.Getenv("STMONITORED") != ""
noDefaultFolder = os.Getenv("STNODEFAULTFOLDER") != ""
)
type RuntimeOptions struct {
@@ -633,6 +638,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
dbFile := locations[locDatabase]
ldb, err := db.Open(dbFile)
if err != nil {
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
}
@@ -990,18 +996,28 @@ func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Mode
}
func defaultConfig(myName string) config.Configuration {
defaultFolder := config.NewFolderConfiguration("default", locations[locDefFolder])
defaultFolder.RescanIntervalS = 60
defaultFolder.MinDiskFreePct = 1
defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}}
defaultFolder.AutoNormalize = true
defaultFolder.MaxConflicts = -1
var defaultFolder config.FolderConfiguration
if !noDefaultFolder {
l.Infoln("Default folder created and/or linked to new config")
defaultFolder = config.NewFolderConfiguration("default", locations[locDefFolder])
defaultFolder.RescanIntervalS = 60
defaultFolder.MinDiskFreePct = 1
defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}}
defaultFolder.AutoNormalize = true
defaultFolder.MaxConflicts = -1
} else {
l.Infoln("We will skip creation of a default folder on first start since the proper envvar is set")
}
thisDevice := config.NewDeviceConfiguration(myID, myName)
thisDevice.Addresses = []string{"dynamic"}
newCfg := config.New(myID)
newCfg.Folders = []config.FolderConfiguration{defaultFolder}
if !noDefaultFolder {
newCfg.Folders = []config.FolderConfiguration{defaultFolder}
}
newCfg.Devices = []config.DeviceConfiguration{thisDevice}
port, err := getFreePort("127.0.0.1", 8384)
@@ -1153,12 +1169,13 @@ func autoUpgrade(cfg *config.Wrapper) {
// suitable time after they have gone out of fashion.
func cleanConfigDirectory() {
patterns := map[string]time.Duration{
"panic-*.log": 7 * 24 * time.Hour, // keep panic logs for a week
"audit-*.log": 7 * 24 * time.Hour, // keep audit logs for a week
"index": 14 * 24 * time.Hour, // keep old index format for two weeks
"config.xml.v*": 30 * 24 * time.Hour, // old config versions for a month
"*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
"panic-*.log": 7 * 24 * time.Hour, // keep panic logs for a week
"audit-*.log": 7 * 24 * time.Hour, // keep audit logs for a week
"index": 14 * 24 * time.Hour, // keep old index format for two weeks
"index*.converted": 14 * 24 * time.Hour, // keep old converted indexes for two weeks
"config.xml.v*": 30 * 24 * time.Hour, // old config versions for a month
"*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
}
for pat, dur := range patterns {
+2
View File
@@ -1,4 +1,5 @@
{
"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 Ключ",
@@ -151,6 +152,7 @@
"Shared With": "Споделена с",
"Short identifier for the folder. Must be the same on all cluster devices.": "Идентификаторът на папката трябва да бъде еднакъв на всички устройства.",
"Show ID": "Покажи идентификатора",
"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": "Спри програмата",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Un nombre negatiu de dies no té sentit.",
"A new major version may not be compatible with previous versions.": "Una nova versió major pot ser incompatible amb versions anteriors.",
"API Key": "Clau API",
@@ -151,6 +152,7 @@
"Shared With": "Compartir Amb",
"Short identifier for the folder. Must be the same on all cluster devices.": "Breva identificació per a la carpeta. Té que ser la mateixa per tots els dispositius del cluster.",
"Show ID": "Mostrar ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrat en comptes del ID del Node en l'estat del cluster. Serà advertit als altres dispositius com un nom opcional per defecte.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrat en comptes del ID del Node en l'estat del cluster. S'actualitzarà al nom del dispositiu si es deixa buit.",
"Shutdown": "Apagar",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Un nombre negatiu de dies no té sentit.",
"A new major version may not be compatible with previous versions.": "Una nova versión amb canvis importants pot no ser compatible amb versions prèvies.",
"API Key": "Clau API",
@@ -151,6 +152,7 @@
"Shared With": "Compartit amb",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador curt per a la carpeta. Deu ser el mateix en tots els dispositius del grup (cluster).",
"Show ID": "Mostrar ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrat en lloc de l'ID del dispositiu en l'estat del grup (cluster). S'anunciarà als altres dispositius com el nom opcional per defecte.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrat en lloc de l'ID del dispositiu en l'estat del grup (cluster). S'actualitzarà al nom que el dispositiu anuncia si es deixa buit.",
"Shutdown": "Apagar",
+2
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "Přístroj s tímto ID je již přídán.",
"A negative number of days doesn't make sense.": "Záporný počet dní nedává smysl.",
"A new major version may not be compatible with previous versions.": "Nová důležitá verze nemusí být kompatibilní s předchozími verzemi.",
"API Key": "API klíč",
@@ -151,6 +152,7 @@
"Shared With": "Sdíleno s",
"Short identifier for the folder. Must be the same on all cluster devices.": "Krátký identifikátor tohoto adresáře. Musí být stejný na všech přístrojích v clusteru.",
"Show ID": "Zobrazit ID",
"Show QR": "Zobrazit QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Zobrazeno místo ID přístroje na náhledu stavu clusteru. Bude odesíláno ostatním přístrojům jako výchozí jméno přístroje.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Zobrazeno místo ID přístroje na náhledu stavu clusteru. Pokud nebude vyplněno, bude nastaveno na jméno, které přístroj odesílá.",
"Shutdown": "Vypnout",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Et negativt antal dage giver ikke mening.",
"A new major version may not be compatible with previous versions.": "En ny versionsudgivelse er måske ikke kompatibel med tidligere versioner.",
"API Key": "API-nøgle",
@@ -151,6 +152,7 @@
"Shared With": "Delt med",
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort navn for mappen. Skal være det samme på alle tilknyttede enheder.",
"Show ID": "Vis ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vist istedet for Enheds ID i klynge status. Vil blive vist på andre enheder som valgfrit standard navn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist istedet for Enheds ID i klynge status. Vil blive opdateret til det navn som enheden viser, hvis det ikke er udfyldt.",
"Shutdown": "Luk ned",
+2
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "Ein Gerät mit dieser ID existiert bereits.",
"A negative number of days doesn't make sense.": "Eine negative Anzahl von Tagen ergibt keinen Sinn.",
"A new major version may not be compatible with previous versions.": "Die neue Hauptversion ist evtl. nicht mit vorherigen Versionen kompatibel.",
"API Key": "API-Schlüssel",
@@ -151,6 +152,7 @@
"Shared With": "Geteilt mit",
"Short identifier for the folder. Must be the same on all cluster devices.": "Kurze ID für das Verzeichnis. Muss auf allen Verbunds-Geräten gleich sein.",
"Show ID": "ID anzeigen",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstatt der Geräte ID angezeigt. Wird als optionaler Gerätename an die anderen Clients im Cluster weitergegeben.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird auf diesem Gerät als Gerätename angezeigt und an die anderen Geräte im Geräte-Verbund weitergegeben. Wenn kein Gerätename anegegeben wird, wird der Name des entfernten Gerätes genommen.",
"Shutdown": "Herunterfahren",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Μια νέα σημαντική έκδοση μπορεί να μην είναι συμβατή με τις προηγούμενες εκδόσεις.",
"API Key": "Κλειδί API",
@@ -151,6 +152,7 @@
"Shared With": "Διαμοιράζεται με",
"Short identifier for the folder. Must be the same on all cluster devices.": "Σύντομη ταυτότητα για το φάκελο. Θα πρέπει να είναι η ίδια σε όλη τη συστάδα συσκευών με τις οποίες διαμοιράζεται ο φάκελος αυτός.",
"Show ID": "Εμφάνιση ταυτότητας",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Θα φαίνεται αντί για την ταυτότητα της συσκευής στην προβολή της κατάστασης ολόκληρης της συστάδας. Θα γνωστοποιείται σαν το προαιρετικό όνομα της συσκευής.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Θα φαίνεται αντί για την ταυτότητα της συσκευής στην προβολή της κατάστασης ολόκληρης της συστάδας. Θα ενημερώνεται αυτόματα αν αλλάξει το όνομα της συσκευής.",
"Shutdown": "Απενεργοποίηση",
+2
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "A device with that ID is already added.",
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
"A new major version may not be compatible with previous versions.": "A new major version may not be compatible with previous versions.",
"API Key": "API Key",
@@ -151,6 +152,7 @@
"Shared With": "Shared With",
"Short identifier for the folder. Must be the same on all cluster devices.": "Short identifier for the folder. Must be the same on all cluster devices.",
"Show ID": "Show ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be 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.": "Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.",
"Shutdown": "Shutdown",
+5
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "A device with that ID is already added.",
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
"A new major version may not be compatible with previous versions.": "A new major version may not be compatible with previous versions.",
"API Key": "API Key",
@@ -50,6 +51,7 @@
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Editing",
"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.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
@@ -74,6 +76,7 @@
"Generate": "Generate",
"Global Discovery": "Global Discovery",
"Global Discovery Server": "Global Discovery Server",
"Global Discovery Servers": "Global Discovery Servers",
"Global State": "Global State",
"Help": "Help",
"Home page": "Home page",
@@ -125,6 +128,7 @@
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "RAM Utilization",
"Random": "Random",
"Relay Servers": "Relay Servers",
"Relayed via": "Relayed via",
"Relays": "Relays",
"Release Notes": "Release Notes",
@@ -151,6 +155,7 @@
"Shared With": "Shared With",
"Short identifier for the folder. Must be the same on all cluster devices.": "Short identifier for the folder. Must be the same on all cluster devices.",
"Show ID": "Show ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be 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.": "Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.",
"Shutdown": "Shutdown",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Un número negativo de días no tiene sentido.",
"A new major version may not be compatible with previous versions.": "Una nueva versión con cambios importantes puede no ser compatible con versiones anteriores.",
"API Key": "Clave del API",
@@ -151,6 +152,7 @@
"Shared With": "Compartir con",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador corto para la carpeta. Debe ser el mismo en todos los dispositivos del grupo (cluster).",
"Show ID": "Mostrar ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Se muestra en lugar del ID del dispositivo en el estado del grupo (cluster). Se notificará a los otros dispositivos como nombre opcional por defecto.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Se muestra en lugar del ID del dispositivo en el estado del grupo (cluster). Se actualizará al nombre que el dispositivo anuncia si se deja vacío.",
"Shutdown": "Apagar",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Un número negativo no tiene sentido",
"A new major version may not be compatible with previous versions.": "Una versión mayor nueva puede ser incompatible con versiones anteriores.",
"API Key": "Clave API",
@@ -151,6 +152,7 @@
"Shared With": "Compartido con",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador corto para el repositorio. Debe ser el mismo en todos los dispositivos del grupo.",
"Show ID": "Mostrar ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrado en lugar de la ID del dispositivo en el estado del grupo. Será sugerido a otros dispositivos como nombre predeterminado opcional.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrado en lugar de la ID del dispositivo en el estado del grupo. Si se deja en blanco, será usado el nombre sugerido por el dispositivo.",
"Shutdown": "Apagar",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Negatiivinen määrä päiviä ei ole järjellinen.",
"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-avain",
@@ -151,6 +152,7 @@
"Shared With": "Jaettu seuraavien kanssa",
"Short identifier for the folder. Must be the same on all cluster devices.": "Lyhyt tunniste kansiolle. Tämän tulee olla sama kaikilla ryhmän laitteilla.",
"Show ID": "Näytä ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Näytetään ryhmän tiedoissa laitteen ID:n sijaan. Ilmoitetaan muille laitteille vaihtoehtoisena oletusnimenä.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Näytetään ryhmän tiedoissa laitteen ID:n sijaan. Tyhjä nimi päivitetään laitteen ilmoittamaksi nimeksi.",
"Shutdown": "Sammuta",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Un nombre négatif de jours n'a pas de sens.",
"A new major version may not be compatible with previous versions.": "Une nouvelle version majeure peut présenter des incompatibilités avec les versions antérieures.",
"API Key": "Clé API",
@@ -151,6 +152,7 @@
"Shared With": "Partagé avec",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du dossier. Il doit être le même sur l'ensemble des appareils du groupe.",
"Show ID": "Montrer l'ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de l'appareil dans le groupe. Sera proposé aux autres appareils comme nom optionnel par défaut.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de l'appareil dans le groupe. Si laissé vide, il sera mis à jour par le nom proposé par l'appareil distant.",
"Shutdown": "Éteindre",
+2
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "Une machine avec cet ID a déjà été ajoutée.",
"A negative number of days doesn't make sense.": "Un nombre négatif de jours n'a pas de sens.",
"A new major version may not be compatible with previous versions.": "Une nouvelle version majeure peut présenter des incompatibilités avec les versions antérieures.",
"API Key": "Clé API",
@@ -151,6 +152,7 @@
"Shared With": "Partagé avec",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du dossier. Il doit être le même sur l'ensemble des appareils du groupe.",
"Show ID": "Montrer l'ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de l'appareil dans le groupe. Sera proposé aux autres appareils comme nom optionnel par défaut.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de l'appareil dans le groupe. Si laissé vide, il sera mis à jour par le nom proposé par l'appareil distant.",
"Shutdown": "Éteindre",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "In negatyf tal dagen soe healwiis wêze.",
"A new major version may not be compatible with previous versions.": "In nije, wichtige ferzje koe wol ris net te kombinearjen wêze mei foargeande ferzjes.",
"API Key": "API-kaai",
@@ -151,6 +152,7 @@
"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.",
"Show ID": "ID sjen litte",
"Show QR": "Show QR",
"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.",
"Shutdown": "Ofslute",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Negatív számú nap nincs értelmezve.",
"A new major version may not be compatible with previous versions.": "Az új főverzió nem kompatibilis az előző főverzióval.",
"API Key": "API kulcs",
@@ -151,6 +152,7 @@
"Shared With": "Megosztva ezekkel:",
"Short identifier for the folder. Must be the same on all cluster devices.": "Rövid azonosító. Minden megosztott eszközön azonosnak kell lennie.",
"Show ID": "Azonosító mutatása",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Az eszköz azonosító helyett jelenik meg. A többi eszközön alapértelmezett névként használható. ",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Az eszköz azonosító helyett jelenik meg. Üresen hagyva az eszköz saját neve lesz használva ",
"Shutdown": "Leállítás",
+2
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "Un dispositivo con questo ID è già stato aggiunto.",
"A negative number of days doesn't make sense.": "Un numero di giorni negativo non ha alcun senso.",
"A new major version may not be compatible with previous versions.": "Una nuova versione principale potrebbe non essere compatibile con le versioni precedenti.",
"API Key": "Chiave API",
@@ -151,6 +152,7 @@
"Shared With": "Condiviso Con",
"Short identifier for the folder. Must be the same on all cluster devices.": "Breve identificatore della cartella. Deve essere lo stesso su tutti i dispositivi del cluster.",
"Show ID": "Mostra ID",
"Show QR": "Mostra QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visibile al posto dell'ID Dispositivo nello stato del cluster. Negli altri dispositivi verrà presentato come nome predefinito opzionale.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visibile al posto dell'ID Dispositivo nello stato del cluster. Se viene lasciato vuoto, verrà utilizzato il nome proposto dal dispositivo.",
"Shutdown": "Arresta",
+3 -1
View File
@@ -1,4 +1,5 @@
{
"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キー",
@@ -41,7 +42,7 @@
"Device {%device%} ({%address%}) wants to connect. Add new device?": "デバイス {{device}} ({{address}}) が接続を求めています。新しいデバイスとして追加しますか?",
"Devices": "デバイス",
"Disconnected": "切断中",
"Discovery": "Discovery",
"Discovery": "ディスカバリーサーバー",
"Documentation": "マニュアル",
"Download Rate": "ダウンロード速度",
"Downloaded": "ダウンロード済",
@@ -151,6 +152,7 @@
"Shared With": "共有相手",
"Short identifier for the folder. Must be the same on all cluster devices.": "このフォルダーの短い識別子。このフォルダーを共有する全てのデバイス上で同じである必要があります。",
"Show ID": "IDを表示",
"Show QR": "QRコードを表示",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "クラスターステータスでデバイスIDの代わりに表示されます。他のデバイスに対してもデフォルトの名前として通知されます。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "クラスターステータスでデバイスIDの代わりに表示されます。空欄の場合デバイスが名乗る名前に更新されます。",
"Shutdown": "シャットダウン",
+2
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "A device with that ID is already added.",
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
"A new major version may not be compatible with previous versions.": "새로운 메이저 버전은 이전 버전과 호환되지 않을 수 있습니다.",
"API Key": "API 키",
@@ -151,6 +152,7 @@
"Shared With": "~와 공유",
"Short identifier for the folder. Must be the same on all cluster devices.": "간단한 폴더 식별자입니다. 모든 장치에서 동일해야 합니다.",
"Show ID": "내 기기 ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "장치에 대한 아이디로 표시됩니다. 옵션에 얻은 기본이름으로 다른장치에 통보합니다.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "아이디가 비어있는 경우 기본 값으로 다른 장치에 업데이트 됩니다.",
"Shutdown": "종료",
+2
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "Įrenginys su tokiu ID jau yra pridėtas.",
"A negative number of days doesn't make sense.": "Privalo būti teigiamas skaičius.",
"A new major version may not be compatible with previous versions.": "Nauja versija gali būti nesuderinama su senomis versijomis.",
"API Key": "API raktas",
@@ -151,6 +152,7 @@
"Shared With": "Dalinamasi su",
"Short identifier for the folder. Must be the same on all cluster devices.": "Trumpas aplanko identifikatorius. Privalo būti toks pat visuose įrenginiuose.",
"Show ID": "Rodyti ID",
"Show QR": "Rodyti QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Grupės būsenoje rodomas vietoje įrenginio vardo. Kiti įrenginiai matys kaip pasirinktinį vardą.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Grupės būsenoje rodomas vietoje įrenginio vardo. Bus atnaujintas į įrenginio vardą jei nieko neįrašysite.",
"Shutdown": "Išjungti",
+11 -9
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "En enhet med samme ID er allerede lagt til.",
"A negative number of days doesn't make sense.": "Et negativt antall dager gir ikke mening.",
"A new major version may not be compatible with previous versions.": "En ny hovedversjon er ikke nødvendigvis kompatibel med eldre versjoner.",
"API Key": "API-nøkkel",
@@ -68,8 +69,8 @@
"Folder Path": "Mappeplassering",
"Folders": "Mapper",
"GUI": "grafisk brukergrensesnitt",
"GUI Authentication Password": "GUI Passord",
"GUI Authentication User": "GUI Bruker",
"GUI Authentication Password": "Passord for GUI-autenisering",
"GUI Authentication User": "Bruker for GUI-autenisering",
"GUI Listen Addresses": "GUI Lytteadresse",
"Generate": "Generer",
"Global Discovery": "Global Søking",
@@ -118,7 +119,7 @@
"Pause": "Oppholde",
"Paused": "Oppholdt",
"Please consult the release notes before performing a major upgrade.": "Se \"release notes\" før en hovedoppgradering utføres.",
"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.": "Vennligst angi bruker og passord for GUI-autentisering i innstillingsvinduet.",
"Please wait": "Vennligst vent",
"Preview": "Forhåndsvisning",
"Preview Usage Report": "Forhåndsvisning Av Datainnsamling",
@@ -129,17 +130,17 @@
"Relays": "Reléer",
"Release Notes": "Utgivelsesnotat",
"Remove": "Fjern",
"Rescan": "Skann på nytt",
"Rescan All": "Skann alt på nytt",
"Rescan Interval": "Skanneintervall",
"Rescan": "Gjennomsøk på nytt",
"Rescan All": "Gjennomsøk alt på nytt",
"Rescan Interval": "Intervall for gjennomsøking",
"Restart": "Omstart",
"Restart Needed": "Omstart Kreves",
"Restarting": "Starter På Ny",
"Resume": "Gjenoppta",
"Reused": "Gjenbrukt",
"Save": "Lagre",
"Scan Time Remaining": "Scan Time Remaining",
"Scanning": "Skanner",
"Scan Time Remaining": "Gjenstående tid for gjennomsøking",
"Scanning": "Gjennomsøker",
"Select the devices to share this folder with.": "Velg enhetene du vil dele denne mappen med.",
"Select the folders to share with this device.": "Velg hvilke mapper som skal deles med denne enheten.",
"Settings": "Innstillinger",
@@ -151,6 +152,7 @@
"Shared With": "Delt med",
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort kjennemerke på mappen. Må være det samme på alle enheter i en gruppe.",
"Show ID": "Vis ID",
"Show QR": "Vis QR kode",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vis i stedet for Eining ID i gruppestatus. Vil bli kringkastet til andre enheter som et valgfritt standardnavn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist i stedet for Mappe-ID i gruppestatus. Vil bli oppdatert til navnet enheten kringkaster dersom tomt.",
"Shutdown": "Avslutt",
@@ -195,7 +197,7 @@
"The number of versions must be a number and cannot be blank.": "Antall versjoner må være et tall og kan ikke være tomt.",
"The path cannot be blank.": "Plasseringen kan ikke være tom.",
"The rate limit must be a non-negative number (0: no limit)": "Hastighetsbegrensningen kan ikke være et negativt tall (0: ingen begrensing)",
"The rescan interval must be a non-negative number of seconds.": "Antall sekund i skanneintervallet kan ikke være negativt.",
"The rescan interval must be a non-negative number of seconds.": "Antall sekund for intervallet kan ikke være negativt.",
"They are retried automatically and will be synced when the error is resolved.": "Disse hentes automatisk og vil synkroniseres når feilen er blitt utbedret.",
"This can easily give hackers access to read and change any files on your computer.": "Dette kan lett gi hackere tilgang til å lese og endre alle filer på datamaskinen din.",
"This is a major version upgrade.": "Dette er en hovedoppgradering",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Een negatief aantal dagen is niet logisch.",
"A new major version may not be compatible with previous versions.": "Een nieuwe major version is misschien niet compatibel met eerdere versies.",
"API Key": "API-sleutel",
@@ -151,6 +152,7 @@
"Shared With": "Gedeeld met",
"Short identifier for the folder. Must be the same on all cluster devices.": "Korte aanduiding voor deze map. Moet dezelfde zijn op alle apparaten in het cluster.",
"Show ID": "Toon ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wordt i.p.v. het apparaat-ID getoond in de clusterstatus. Deze naam wordt aan andere apparaten voorgesteld als optionele standaard naam.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wordt in plaats van het apparaat-ID in de cluster status getoond. Zal geupdate worden naar de naam die het apparaat uitzend. ",
"Shutdown": "Sluit af",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Eit negativt tal dagar har ikkje meining.",
"A new major version may not be compatible with previous versions.": "Ein ny hovudversjon er ikkje nødvendigvis kompatibel med eldre versjonar. ",
"API Key": "API-nøkkel",
@@ -151,6 +152,7 @@
"Shared With": "Delt Med",
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort kjennemerke på mappa. Må vera det same på alle einingane i klyngja.",
"Show ID": "Vis ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vist i staden for einings-ID-en i klyngjestatusen. Vil verta kringkasta til dei andre einingane som eit valfritt standardnamn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist i staden for mappe-ID-en i klyngjestatuses. Vil verta oppdatert til namnet eininga kringkastar dersom tomt.",
"Shutdown": "Slå Av",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Ujemna ilość dni nie ma sensu.",
"A new major version may not be compatible with previous versions.": "Nowa wersja może być niekompatybilna z poprzednimi wersjami.",
"API Key": "Klucz API",
@@ -151,6 +152,7 @@
"Shared With": "Współdzielony z",
"Short identifier for the folder. Must be the same on all cluster devices.": "Krótki identyfikator folderu. Musi być taki sam na wszystkich urządzeniach.",
"Show ID": "Pokaż ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Pokazane w statusie zamiast ID urządzenia.Zostanie wysłane do innych urządzeń jako opcjonalna domyślna nazwa.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Pokazane w statusie zamiast ID urządzenia. Zostanie zaktualizowane do nazwy urządzenia jeżeli pozostanie puste.",
"Shutdown": "Wyłącz",
+2
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "Um dispositivo com este ID já foi adicionado.",
"A negative number of days doesn't make sense.": "Um número negativo de dias não faz sentido.",
"A new major version may not be compatible with previous versions.": "Uma nova versão principal pode não ser compatível com versões anteriores.",
"API Key": "Chave da API",
@@ -151,6 +152,7 @@
"Shared With": "Compartilhada com",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador curto para a pasta. Deve ser igual em todos os dispositivos.",
"Show ID": "Mostrar ID",
"Show QR": "Exibir QR Code",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrado no lugar do ID do dispositivo no indicador de estado do grupo. Será divulgado aos outros dispositivos como um nome opcional e pré-definido.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrado no lugar do ID do dispositivo no indicador de estado do grupo. Será atualizado para o nome que o dispositivo divulga, caso seja deixado em branco.",
"Shutdown": "Desligar",
+3 -1
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "Já tinha sido adicionado um dispositivo com esse ID.",
"A negative number of days doesn't make sense.": "Um número negativo de dias não faz sentido.",
"A new major version may not be compatible with previous versions.": "Uma nova versão principal pode não ser compatível com versões anteriores.",
"API Key": "Chave da API",
@@ -118,7 +119,7 @@
"Pause": "Pausar",
"Paused": "Em pausa",
"Please consult the release notes before performing a major upgrade.": "Consulte as notas de lançamento antes de fazer uma actualização importante.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Por favor, defina um utilizador e senha de autenticação na interface gráfica, nas configurações.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Por favor, defina um utilizador e senha de autenticação para a interface gráfica, nas configurações.",
"Please wait": "Aguarde",
"Preview": "Previsão",
"Preview Usage Report": "Pré-visualizar relatório de utilização",
@@ -151,6 +152,7 @@
"Shared With": "Partilhada com",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador curto para a pasta. Tem que ser igual em todos os dispositivos do grupo.",
"Show ID": "Mostrar ID",
"Show QR": "Mostrar QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Apresentado ao invés do ID do dispositivo no indicador de estado do grupo. Será divulgado aos outros dispositivos como um nome pré-definido opcional.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Apresentado ao invés do ID do dispositivo no indicador de estado do grupo. Será actualizado para o nome que o dispositivo divulga, se for deixado em branco.",
"Shutdown": "Desligar",
+2
View File
@@ -1,4 +1,5 @@
{
"A device with that ID is already added.": "A device with that ID is already added.",
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
"A new major version may not be compatible with previous versions.": "A new major version may not be compatible with previous versions.",
"API Key": "Cheie API",
@@ -151,6 +152,7 @@
"Shared With": "Împarte Cu",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificator scurt al fișierului. Trebuie să fie același pe toate mașinile. ",
"Show ID": "Arată ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vizibil în locul ID-ului dispozitivului într-un grup. Va fi sugerat celorlalte dispozitive ca nume opţional. ",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vizibil în locul ID-ului dispozitivului într-un grup. Va fi înlocuit de numele sugerat de dispozitiv daca nu este completat. ",
"Shutdown": "Opreşte",
+2
View File
@@ -1,4 +1,5 @@
{
"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",
@@ -151,6 +152,7 @@
"Shared With": "Доступ предоставлен",
"Short identifier for the folder. Must be the same on all cluster devices.": "Короткий идентификатор папки. Должен быть одинаковым на всех устройствах кластера.",
"Show ID": "Показать ID",
"Show QR": "Показать QR-код",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Отображается вместо ID устройства в статусе группы. Будет разослан другим устройствам в качестве имени по умолчанию.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Отображается вместо ID устройства в статусе группы. Если поле не заполнено, то будет установлено имя, передаваемое этим устройством.",
"Shutdown": "Выключить",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Negativt antal dagar är inte troligt.",
"A new major version may not be compatible with previous versions.": "En ny huvudversion kan eventuellt vara inkompatibel med tidigare versioner.",
"API Key": "API-nyckel",
@@ -151,6 +152,7 @@
"Shared With": "Delad med",
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort identifieringssträng för katalogen. Måste vara samma på alla enheter i klustret.",
"Show ID": "Visa ID",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhets-ID. Skickas till andra enheter som namn på denna enhet.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhets-ID. Sätts till namnet på den andra enheten vid första anslutning om det lämnas tomt.",
"Shutdown": "Stäng av",
+227
View File
@@ -0,0 +1,227 @@
{
"A device with that ID is already added.": "Bu ID'yi taşıyan cihaz zaten eklendi.",
"A negative number of days doesn't make sense.": "Eksi gün sayısı mantıklı bir ifade değil.",
"A new major version may not be compatible with previous versions.": "Yeni birincil sürümler önceki sürümlerle uyumlu olmayabilir.",
"API Key": "API Anahtarı",
"About": "Hakkında",
"Actions": "Eylemler",
"Add": "Ekle",
"Add Device": "Cihaz Ekle",
"Add Folder": "Klasör Ekle",
"Add new folder?": "Yeni klasör ekle?",
"Address": "Adres",
"Addresses": "Adresler",
"Advanced": "Gelişmiş Düzey",
"Advanced Configuration": "Gelişmiş Yapılandırma",
"All Data": "Bütün Veriler",
"Allow Anonymous Usage Reporting?": "Anonim kullanımın raporlanmasına izin veriyor musun ?",
"Alphabetic": "Alfabetik",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Sürümlendirme işlemini harici bir komut yürütüyor. Dosyayı eşzamanlama klasöründen kaldırmak zorunda.",
"Anonymous Usage Reporting": "Anonim Kullanım Raporlama",
"Any devices configured on an introducer device will be added to this device as well.": "Tanıtıcı bir cihazda yapılandırılan cihazlar bu cihaza da eklenecektir.",
"Automatic upgrades": "Otomatik güncellemeler",
"Be careful!": "Dikkatli Ol!",
"Bugs": "Hatalar",
"CPU Utilization": "İşlemci Kullanımı",
"Changelog": "Değişim Günlüğü",
"Clean out after": "Clean out after",
"Close": "Kapat",
"Command": "Komut",
"Comment, when used at the start of a line": "Satır başında kullanıldığında açıklama özelliği taşır",
"Compression": "Sıkıştırma",
"Connection Error": "Bağlantı hatası",
"Copied from elsewhere": "Başka bir yerden kopyalanmış",
"Copied from original": "Aslından kopyalanmış",
"Copyright © 2015 the following Contributors:": "Telif Hakkı © 2015 katkıda bulunanlar:",
"Danger!": "Tehlike!",
"Delete": "Sil",
"Deleted": "Silindi",
"Device ID": "Cihaz ID",
"Device Identification": "Cihaz Kimliği",
"Device Name": "Cihaz Adı",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Cihaz {{device}} ({{address}}) bağlanmak istiyor. Yeni cihaz ekle?",
"Devices": "Cihazlar",
"Disconnected": "Bağlantı Kesik",
"Discovery": "Discovery",
"Documentation": "Belgeleme",
"Download Rate": "İndirme Hızı",
"Downloaded": "İndirilmiş",
"Downloading": "İndiriliyor",
"Edit": "Düzenle",
"Edit Device": "Cihaz Düzenle",
"Edit Folder": "Klasör Düzenle",
"Editing": "Düzenleniyor",
"Enable UPnP": "UPnP Etkinleştir",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Adreslerin keşfinin otomatik olarak gerçekleştirilmesi için ya adresleri virgülle ayırarak (\"tcp://ip:port\", \"tcp://host:port\") girin, ya da \"dynamic\" kelimesini girin.",
"Enter ignore patterns, one per line.": "Yoksayılacak/ihmal edilecek kalıp dizilerini her satıra bir tane olacak şekilde girin.",
"Error": "Hata",
"External File Versioning": "Harici Dosya Sürümlendirme",
"Failed Items": "Başarısız olunan Öğeler",
"File Pull Order": "File Pull Order",
"File Versioning": "Dosya Sürümlendirme",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Değişimleri yoklarken dosya izin bilgilerini ihmal et. FAT dosya sistemlerinde kullanın.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Dosyalar Syncthing tarafından yeri değiştirildiğinde ya da silindiğinde .stversions klasörüne taşınır.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dosyalar Syncthing tarafından yeri değiştirildiğinde ya da silindiğinde, tarih damgalı sürümleri .stversions klasörüne taşınır.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosyalar diğer cihazlarda yapılan değişikliklerden korunur, ancak bu cihazdaki değişiklikler kümedeki diğer cihazlara gönderilir.",
"Folder": "Klasör",
"Folder ID": "Klasör ID",
"Folder Master": "Ana Klasör",
"Folder Path": "Klasör Yolu",
"Folders": "Klasörler",
"GUI": "GUI / Kullanıcı Grafik Arayüzü",
"GUI Authentication Password": "GUI Kimlik Doğrulaması için Kullanıcı Parolası",
"GUI Authentication User": "GUI Kimlik Doğrulaması için Kullanıcı Adı",
"GUI Listen Addresses": "GUI Dinleme/Bağlantı Adresleri",
"Generate": "Oluştur",
"Global Discovery": "Küresel Discovery",
"Global Discovery Server": "Küresel Discovery Sunucusu",
"Global State": "Küresel Durum",
"Help": "Yardım",
"Home page": "Ana Sayfa",
"Ignore": "Yoksay",
"Ignore Patterns": "Kalıpları Yoksay",
"Ignore Permissions": "İzinleri yoksay",
"Incoming Rate Limit (KiB/s)": "İndirme Oranı Limiti (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Yanlış yapılandırma klasör içeriğine zarar verebilir ve Syncthing'i çalışamaz hale getirebilir.",
"Introducer": "Tanıtıcı",
"Inversion of the given condition (i.e. do not exclude)": "Verilen koşulun ters çevirilmesi (örneğin: yok sayma)",
"Keep Versions": "Sürümleri Tut",
"Largest First": "En büyük olan önce",
"Last File Received": "Alınan Son Dosya",
"Last seen": "Son Görülen",
"Later": "Sonra",
"Local Discovery": "Yerel Discovery",
"Local State": "Yerel Durum",
"Local State (Total)": "Yerel Durum (Toplamı)",
"Major Upgrade": "Birincil Yükseltme",
"Maximum Age": "Azami Süre",
"Metadata Only": "Sadece Üstveri",
"Minimum Free Disk Space": "Minimum Boş Disk Alanı",
"Move to top of queue": "Kuyruğun başına taşı",
"Multi level wildcard (matches multiple directory levels)": "Çoklu düzey wildcard (çok sayıda dizin düzeyinde eşleşme)",
"Never": "Asla",
"New Device": "Yeni Cihaz",
"New Folder": "Yeni Klasör",
"Newest First": "En yeni olan önce",
"No": "Hayır",
"No File Versioning": "Dosya Sürümlendirmesi Yok",
"Notice": "Uyarı",
"OK": "Tamam",
"Off": "Kapalı",
"Oldest First": "En eski olan önce",
"Options": "Seçenekler",
"Out of Sync": "Eşzamanlama Dışı",
"Out of Sync Items": "Eşzamanlama dışında kalan Öğeler",
"Outgoing Rate Limit (KiB/s)": "Yükleme hız sınırı (KB/sn)",
"Override Changes": "Değişiklikleri Geçersiz kıl",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Yerel bilgisayardaki klasöre ulaşım yolu. Klasör yoksa yaratılacak. Tilde (~) karakterinin kısayol olarak kullanılabileceği yol",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sürümlerin saklanması gereken klasör yolu (öntanımlı .stversions klasörü için boş bırakın)",
"Pause": "Duraklat",
"Paused": "Duraklatıldı",
"Please consult the release notes before performing a major upgrade.": "Birincil bir yükseltme gerçekleştirmeden önce lütfen sürüm notlarını tetkik edin.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "GUI üzerinden Kimlik Doğrulaması yapmak için Ayarlar penceresinde Kullanıcı ve Parola tanımlayın lütfen.",
"Please wait": "Lütfen Bekleyin",
"Preview": "Önizleme",
"Preview Usage Report": "Kullanım raporunu gözden geçir",
"Quick guide to supported patterns": "Desteklenen kalıplar için hızlı rehber",
"RAM Utilization": "RAM Kullanımı",
"Random": "Rastgele",
"Relayed via": "Relayed via",
"Relays": "Relays",
"Release Notes": "Sürüm Notları",
"Remove": "Kaldır",
"Rescan": "Tekrar Tara",
"Rescan All": "Tümünü Tekrar Tara",
"Rescan Interval": "Tarama Aralığı",
"Restart": "Yeniden Başlat",
"Restart Needed": "Yeniden başlatma gereklidir",
"Restarting": "Yeniden başlatılıyor",
"Resume": "Devam Et/Sürdür",
"Reused": "Yeniden Kullanılan",
"Save": "Kaydet",
"Scan Time Remaining": "Kalan Tarama Zamanı",
"Scanning": "Taranıyor",
"Select the devices to share this folder with.": "Bu klasörü paylaşacağın cihazları seç.",
"Select the folders to share with this device.": "Bu cihazla paylaşılacak klasörleri seç.",
"Settings": "Ayarlar",
"Share": "Paylaş",
"Share Folder": "Paylaşım Klasörü",
"Share Folders With Device": "Klasörü Cihazla Paylaş",
"Share With Devices": "Cihazlar İle Paylaş",
"Share this folder?": "Bu klasörü paylaşmak istiyor musun?",
"Shared With": "Paylaşılan düğümler",
"Short identifier for the folder. Must be the same on all cluster devices.": "Klasörü için kısa tanımlayıcı. Tüm küme cihazlarda aynı olmalıdır.",
"Show ID": "ID Göster",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Küme durumunda Cihaz ID yerine bunu göster. Varsayılan isim isteğe bağlı olarak diğer cihazlara ilan edilecektir.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Küme durumunda Cihaz ID yerine bunu göster. Eğer düğüm ismi boş bırakılırsa düğüm ismi güncellenip ilan edilecektir.",
"Shutdown": "Kapat",
"Shutdown Complete": "Kapatma İşlemi Tamamlandı",
"Simple File Versioning": "Basit Dosya Sürümlendirme",
"Single level wildcard (matches within a directory only)": "Tekli düzey wildcard (sadece bir dizin içinde eşleşme)",
"Smallest First": "En küçük olan önce",
"Source Code": "Kaynak Kodu",
"Staggered File Versioning": "Aşamalı Dosya Sürümlendirme",
"Start Browser": "Tarayıcıyı Başlat",
"Statistics": "İstatistikler",
"Stopped": "Durduruldu",
"Support": "Destek",
"Sync Protocol Listen Addresses": "Sync Protokol Dinleme Adresleri",
"Syncing": "Eşzamanlama gerçekleştiriliyor",
"Syncthing has been shut down.": "Syncthing kapatıldı",
"Syncthing includes the following software or portions thereof:": "Syncthing aşağıdaki yazılımları veya bunların bölümlerini içermektedir:",
"Syncthing is restarting.": "Syncthing yeniden başlatılıyor.",
"Syncthing is upgrading.": "Syncthing yükseltiliyor.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing uygulaması çökmüş olabilir, veya internet bağlantınızda bir sorun var. Tekrar deniyor....",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing isteminizi işleme alırken bir sorunla karşılaştı. Lütfen sayfanızı yenileyin veya sorun devam ediyorsa Syncthing'i yeniden başlatın.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing yönetici arayüzü parolasız olarak uzaktan erişime izin verilecek şekilde yapılandırıldı.",
"The aggregated statistics are publicly available at {%url%}.": "Toplanan halka açık istatistiklere ulaşabileceğiniz adres {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Yapılandırma kaydedildi ancak etkinleştirilmedi. Etkinleştirmek için Syncthing yeniden başlatılmalı.",
"The device ID cannot be blank.": "Cihaz ID boş olamaz.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Buraya girilecek cihaz ID'si diğer düğümde \"Düzenle > ID Göster\" menüsünden bulunabilir. Boşluk ve kısa çizginin olup olmaması önemli değildir. (İhmal edilir)",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Şifrelenmiş kullanım bilgisi günlük olarak gönderilir. Platform, klasör büyüklüğü ve uygulama sürümü hakkında bilgi toplanır. Toplanan bilgi çeşidi değişecek olursa, sizden tekrar onay istenecek.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Girilen cihaz ID'si geçerli gibi gözükmüyor. 52 ya da 56 karakter uzunluğunda, harf ve rakamlardan oluşmalı. Boşlukların ve kısa çizgilerin olup olmaması önemli değildir.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "İlk komut satırı parametresi klasör yoludur; ikinci parametre ise klasördeki göreceli yoldur. ",
"The folder ID cannot be blank.": "Klasör ID boş olamaz.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Klasör ID uzun olmamalı (64 karakter ya da daha az). Sadece harf, rakam, nokta (.), kısa çizgi (-) ve alt çizgi (_) kullanabilirsiniz.",
"The folder ID must be unique.": "Klasör ID benzersiz olmalıdır.",
"The folder path cannot be blank.": "Klasör dizini boş bırakılamaz.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Kullanılan zaman aralıkları: ilk bir saat zarfında her 30 saniyede bir, ilk gün zarfında saatte bir, ilk 30 gün zarfında her gün, azami süreye kadar geçen zamanda ise her hafta yeni bir sürüm değeri oluşturulur/tutulur.",
"The following items could not be synchronized.": "Aşağıdaki öğelerin eşzamanlama işlemi gerçekleştirilemedi.",
"The maximum age must be a number and cannot be blank.": "Azami süre tanımı boş bırakılmamalı ve bir sayı olarak tanımlanmalıdır.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Bir sürümün tutulması için belirlenen azami süre (sürümleri sürekli olarak tutabilmek için 0 değeri atayın)",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Minimum boş disk alanı yüzde olarak 0 ve 100 (dahil) arasında kalan pozitif bir sayıyla tanımlanmalıdır.",
"The number of days must be a number and cannot be blank.": "Gün sayısı sayı olarak tanımlanmalıdır ve boş bırakılamaz.",
"The number of days to keep files in the trash can. Zero means forever.": "Dosyaları çöp kutusunda tutma süresini tanımlayan gün sayısı. Sıfır devamlı/sürekli anlamına gelir.",
"The number of old versions to keep, per file.": "Dosya başına saklanacak/tutulacak eski sürüm sayısı.",
"The number of versions must be a number and cannot be blank.": "Sürümlerin sayısı sayı olmalı ve boş bırakılamaz.",
"The path cannot be blank.": "Dizin yolu boş bırakılamaz.",
"The rate limit must be a non-negative number (0: no limit)": "Hız sınırı pozitif bir sayı olmalıdır. (0: sınırsız)",
"The rescan interval must be a non-negative number of seconds.": "Tarama zaman aralığı, saniye cinsinden negatif olmayan bir sayı olmalıdır.",
"They are retried automatically and will be synced when the error is resolved.": "Otomatik olarak yeniden deneniyor; hata giderildiğinde eşzamanlama gerçekleştirilecek.",
"This can easily give hackers access to read and change any files on your computer.": "Hacker'ların bilgisayarındaki dosyaları okuma ve değiştirme yetkisine kolayca erişebilmelerini sağlayabilir.",
"This is a major version upgrade.": "Birincil sürüm yükseltmesidir.",
"Trash Can File Versioning": "Çöp Kutusu Dosya Sürümleme",
"Unknown": "Bilinmiyor",
"Unshared": "Paylaşılmayan",
"Unused": "Kullanılmayan",
"Up to Date": "Güncel",
"Updated": "Güncellendi",
"Upgrade": "Sürüm Yükseltme",
"Upgrade To {%version%}": "{{version}} sürümüne yükselt",
"Upgrading": "Yükseltiliyor",
"Upload Rate": "Yükleme hızı",
"Uptime": "Etkin Hizmet Süresi",
"Use HTTPS for GUI": "GUI için HTTPS kullan",
"Version": "Sürüm",
"Versions Path": "Sürüm Dizin Yolu",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Sürümler, tanımlı azami süre veya belirlenen zaman aralığı için izin verilen dosya sayısı aşılmışsa kendiliğinden silinir.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Yeni bir cihaz eklendiğinde, bu cihazın karşı tarafa da eklenmesi gerektiğini unutmayın.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Yeni bir klasör eklendiğinde, Klasör ID'nin klasörleri cihazlar arasında bağlantılandırmak için kullanıldığını unutmayın. Klasör ID'ler büyük - küçük harf duyarlıdır ve bütün cihazlarda tamı tamına eşleşmelidir.",
"Yes": "Evet",
"You must keep at least one version.": "En az bir sürümü tutmalısınız.",
"days": "günler",
"full documentation": "belgelendirme içeriğinin tümü",
"items": "öğeler",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} \"{{folder}}\" klasörünü paylaşmak istiyor."
}
+2
View File
@@ -1,4 +1,5 @@
{
"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 ключ",
@@ -151,6 +152,7 @@
"Shared With": "Доступно для",
"Short identifier for the folder. Must be the same on all cluster devices.": "Короткий ідентифікатор для директорії. Має бути однаковим на усіх пристроях кластеру.",
"Show ID": "Показати ID",
"Show QR": "Показати QR-код",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Показується замість ID пристрою в статусі кластера. Буде розголошено іншим вузлам як опціональне типове ім’я.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Показується замість ID пристрою в статусі кластера. Буде оновлено ім’ям, яке розголошене пристроєм, якщо залишити порожнім.",
"Shutdown": "Вимкнути",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "Số ngày không thể âm.",
"A new major version may not be compatible with previous versions.": "Phiên bản quan trọng mới có thể sẽ không tương thích với các bản cũ.",
"API Key": "Khoá API",
@@ -151,6 +152,7 @@
"Shared With": "Đã chia sẻ với",
"Short identifier for the folder. Must be the same on all cluster devices.": "Tên tắt cho thư mục. Phải trùng khớp trên tất cả thiết bị trong cụm.",
"Show ID": "Hiển thị ID",
"Show QR": "Show 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 updated to the name the device advertises if left empty.": "Hiển thị thay cho ID thiết bị trong trạng thái cụm. Nếu để trống sẽ được cập nhật thành tên mà thiết bị giới thiệu.",
"Shutdown": "Đóng",
+3 -1
View File
@@ -1,4 +1,5 @@
{
"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.": "重大更新可能与之前的版本之间无法兼容",
"API Key": "API Key",
@@ -150,7 +151,8 @@
"Share this folder?": "是否共享该文件夹?",
"Shared With": "共享给",
"Short identifier for the folder. Must be the same on all cluster devices.": "文件夹的别名。必须在所有设备上保持一致。",
"Show ID": "显示设备标",
"Show ID": "显示设备标",
"Show QR": "显示二维码",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "在设备丛中,显示该名称,而不是设备标识。亦会作为一个可选的默认名称被发送到其他设备。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "在设备丛中,将会显示本名称,而不是设备标识。如果设置为空,则会使用目标设备提供的默认名称。",
"Shutdown": "关闭 Syncthing",
+2
View File
@@ -1,4 +1,5 @@
{
"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.": "一個新的主要版本可能與以前的版本並不相容。",
"API Key": "API 金鑰",
@@ -151,6 +152,7 @@
"Shared With": "與誰共享",
"Short identifier for the folder. Must be the same on all cluster devices.": "資料夾的簡短識別字。必須在叢集內所有的裝置上皆相同。",
"Show ID": "顯示識別碼",
"Show QR": "Show QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "代替裝置識別碼顯示在叢集狀態中。這段文字將會廣播到其他的裝置作為一個可選的預設名稱。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "代替裝置識別碼顯示在叢集狀態中。本欄若未填寫則將被更新為此裝置所廣播的名稱。",
"Shutdown": "關閉",
+1 -1
View File
@@ -1 +1 @@
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian (Romania)","ru":"Russian","sv":"Swedish","uk":"Ukrainian","vi":"Vietnamese","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian (Romania)","ru":"Russian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","vi":"Vietnamese","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
+1 -1
View File
@@ -1 +1 @@
var validLangs = ["bg","ca","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","uk","vi","zh-CN","zh-TW"]
var validLangs = ["bg","ca","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","tr","uk","vi","zh-CN","zh-TW"]
+1 -1
View File
@@ -52,7 +52,7 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="fa fa-cog"></span>&nbsp;<span 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()"><span class="fa fa-fw fa-qrcode"></span>&nbsp;<span translate>Show ID</span></a></li>
<li><a href="" ng-click="idDevice(thisDevice())"><span class="fa fa-fw fa-qrcode"></span>&nbsp;<span translate>Show ID</span></a></li>
<li class="divider"></li>
<li><a href="" ng-click="shutdown()"><span class="fa fa-fw fa-power-off"></span>&nbsp;<span translate>Shutdown</span></a></li>
<li><a href="" ng-click="restart()"><span class="fa fa-fw fa-refresh"></span>&nbsp;<span translate>Restart</span></a></li>
+1
View File
@@ -58,6 +58,7 @@
<li class="auto-generated">Johan Vromans</li>
<li class="auto-generated">Karol Różycki</li>
<li class="auto-generated">Ken'ichi Kamada</li>
<li class="auto-generated">Kevin Allen</li>
<li class="auto-generated">Lode Hoste</li>
<li class="auto-generated">Lord Landon Agahnim</li>
<li class="auto-generated">Marc Laporte</li>
+10 -4
View File
@@ -353,6 +353,7 @@ angular.module('syncthing.core')
$scope.config = config;
$scope.config.options._listenAddressStr = $scope.config.options.listenAddress.join(', ');
$scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
$scope.config.options._relayServersStr = $scope.config.options.relayServers.join(', ');
$scope.devices = $scope.config.devices;
$scope.devices.forEach(function (deviceCfg) {
@@ -890,7 +891,7 @@ angular.module('syncthing.core')
$scope.config.options = angular.copy($scope.tmpOptions);
$scope.config.gui = angular.copy($scope.tmpGUI);
['listenAddress', 'globalAnnounceServers'].forEach(function (key) {
['listenAddress', 'globalAnnounceServers', 'relayServers'].forEach(function (key) {
$scope.config.options[key] = $scope.config.options["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
return x.trim();
});
@@ -966,7 +967,9 @@ angular.module('syncthing.core')
$('#editDevice').modal();
};
$scope.idDevice = function () {
$scope.idDevice = function (deviceCfg) {
$scope.currentDevice = deviceCfg;
$('#editDevice').modal('hide');
$('#idqr').modal('show');
};
@@ -1198,7 +1201,7 @@ angular.module('syncthing.core')
};
$scope.currentFolder.rescanIntervalS = 60;
$scope.currentFolder.minDiskFreePct = 1;
$scope.currentFolder.maxConflicts = -1;
$scope.currentFolder.maxConflicts = 10;
$scope.currentFolder.order = "random";
$scope.currentFolder.fileVersioningSelector = "none";
$scope.currentFolder.trashcanClean = 0;
@@ -1220,7 +1223,7 @@ angular.module('syncthing.core')
selectedDevices: {},
rescanIntervalS: 60,
minDiskFreePct: 1,
maxConflicts: -1,
maxConflicts: 10,
order: "random",
fileVersioningSelector: "none",
trashcanClean: 0,
@@ -1511,6 +1514,9 @@ angular.module('syncthing.core')
'386': '32 bit',
'amd64': '64 bit',
'arm': 'ARM',
'arm64': 'AArch64',
'ppc64': 'PowerPC',
'ppc64le': 'PowerPC (LE)',
}[$scope.version.arch] || $scope.version.arch;
return $scope.version.version + ', ' + os + ' (' + arch + ')';
@@ -15,6 +15,15 @@ angular.module('syncthing.core')
ctrl.$setValidity('validDeviceid', true);
}
});
//Prevents user from adding a duplicate ID
var matches = scope.devices.filter(function (n) {
return n.deviceID == viewValue;
}).length;
if (matches > 0) {
ctrl.$setValidity('unique', false);
} else {
ctrl.$setValidity('unique', true);
}
}
return viewValue;
});
@@ -21,6 +21,7 @@
<span translate ng-show="!editingExisting && (deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine)">When adding a new device, keep in mind that this device must be added on the other side too.</span>
<span translate ng-if="deviceEditor.deviceID.$error.required && deviceEditor.deviceID.$dirty">The device ID cannot be blank.</span>
<span translate ng-if="deviceEditor.deviceID.$error.validDeviceid && deviceEditor.deviceID.$dirty">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.</span>
<span translate ng-if="deviceEditor.deviceID.$error.unique && deviceEditor.deviceID.$dirty">A device with that ID is already added.</span>
</p>
</div>
<div class="form-group">
@@ -75,6 +76,9 @@
<button type="button" class="btn btn-primary btn-sm" ng-click="saveDevice()" ng-disabled="deviceEditor.$invalid">
<span class="fa fa-check"></span>&nbsp;<span translate>Save</span>
</button>
<button type="button" class="btn btn-default btn-sm" ng-click="idDevice(currentDevice)" ng-if="editingExisting || deviceEditor.deviceID.$valid">
<span class="fa fa-qrcode"></span>&nbsp;<span translate>Show QR</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
</button>
+3 -3
View File
@@ -1,4 +1,4 @@
<modal id="idqr" status="info" icon="qrcode" title="{{'Device Identification' | translate}} - {{deviceName(thisDevice())}}" large="yes" close="yes">
<div class="well well-sm text-monospace text-center" select-on-click>{{myID}}</div>
<img ng-if="myID" class="center-block img-thumbnail" ng-src="qr/?text={{myID}}"/>
<modal id="idqr" status="info" icon="qrcode" title="{{'Device Identification' | translate}} - {{deviceName(currentDevice)}}" large="yes" close="yes">
<div class="well well-sm text-monospace text-center" select-on-click>{{currentDevice.deviceID}}</div>
<img ng-if="currentDevice.deviceID" class="center-block img-thumbnail" ng-src="qr/?text={{currentDevice.deviceID}}"/>
</modal>
+48 -26
View File
@@ -13,10 +13,12 @@
<label translate for="DeviceName">Device Name</label>
<input id="DeviceName" class="form-control" type="text" ng-model="tmpOptions.deviceName">
</div>
<div class="form-group">
<label translate for="ListenAddressStr">Sync Protocol Listen Addresses</label>
<input id="ListenAddressStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressStr">
</div>
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}">
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
<input id="MaxRecvKbps" name="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.maxRecvKbps" min="0">
@@ -24,6 +26,7 @@
<span translate ng-if="settingsEditor.MaxRecvKbps.$error.min && settingsEditor.MaxRecvKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
</p>
</div>
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxSendKbps.$invalid && settingsEditor.MaxSendKbps.$dirty}">
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
<input id="MaxSendKbps" name="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.maxSendKbps" min="0">
@@ -31,41 +34,53 @@
<span translate ng-if="settingsEditor.MaxSendKbps.$error.min && settingsEditor.MaxSendKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
</p>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.upnpEnabled"> <span translate>Enable UPnP</span>
</label>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.upnpEnabled"> <span translate>Enable UPnP</span>
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.globalAnnounceEnabled"> <span translate>Global Discovery</span>
</label>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.localAnnounceEnabled"> <span translate>Local Discovery</span>
</label>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.localAnnounceEnabled"> <span translate>Local Discovery</span>
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label ng-if="upgradeInfo">
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.autoUpgradeEnabled"> <span translate>Automatic upgrades</span>
</label>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="RelaysEnabled" type="checkbox" ng-model="tmpOptions.relaysEnabled"> <span translate>Enable Relaying</span>
</label>
</div>
</div>
<div class="clearfix"></div>
<div class="form-group">
<label translate for="GlobalAnnServersStr">Global Discovery Server</label>
<label translate for="RelayServersStr">Relay Servers</label>
<input ng-disabled="!tmpOptions.relaysEnabled" id="RelayServersStr" class="form-control" type="text" ng-model="tmpOptions._relayServersStr">
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.globalAnnounceEnabled"> <span translate>Global Discovery</span>
</label>
</div>
</div>
<div class="clearfix"></div>
<div class="form-group">
<label translate for="GlobalAnnServersStr">Global Discovery Servers</label>
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr">
</div>
</div>
@@ -104,6 +119,13 @@
</label>
</div>
</div>
<div class="form-group" ng-if="upgradeInfo">
<div class="checkbox">
<label>
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.autoUpgradeEnabled"> <span translate>Automatic upgrades</span>
</label>
</div>
</div>
<hr />
+45 -44
View File
File diff suppressed because one or more lines are too long
+23 -2
View File
@@ -199,6 +199,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
existingDevices[myID] = true
}
// Ensure that the device list is free from duplicates
cfg.Devices = ensureNoDuplicateDevices(cfg.Devices)
sort.Sort(DeviceConfigurationList(cfg.Devices))
// Ensure that any loose devices are not present in the wrong places
// Ensure that there are no duplicate devices
@@ -206,7 +209,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
for i := range cfg.Folders {
cfg.Folders[i].Devices = ensureDevicePresent(cfg.Folders[i].Devices, myID)
cfg.Folders[i].Devices = ensureExistingDevices(cfg.Folders[i].Devices, existingDevices)
cfg.Folders[i].Devices = ensureNoDuplicates(cfg.Folders[i].Devices)
cfg.Folders[i].Devices = ensureNoDuplicateFolderDevices(cfg.Folders[i].Devices)
sort.Sort(FolderDeviceConfigurationList(cfg.Folders[i].Devices))
}
@@ -408,7 +411,25 @@ loop:
return devices[0:count]
}
func ensureNoDuplicates(devices []FolderDeviceConfiguration) []FolderDeviceConfiguration {
func ensureNoDuplicateFolderDevices(devices []FolderDeviceConfiguration) []FolderDeviceConfiguration {
count := len(devices)
i := 0
seenDevices := make(map[protocol.DeviceID]bool)
loop:
for i < count {
id := devices[i].DeviceID
if _, ok := seenDevices[id]; ok {
devices[i] = devices[count-1]
count--
continue loop
}
seenDevices[id] = true
i++
}
return devices[0:count]
}
func ensureNoDuplicateDevices(devices []DeviceConfiguration) []DeviceConfiguration {
count := len(devices)
i := 0
seenDevices := make(map[protocol.DeviceID]bool)
+26
View File
@@ -592,3 +592,29 @@ func TestGUIConfigURL(t *testing.T) {
}
}
}
func TestRemoveDuplicateDevicesFolders(t *testing.T) {
wrapper, err := Load("testdata/duplicates.xml", device1)
if err != nil {
t.Fatal(err)
}
// All folders are loaded, but the duplicate ones are disabled.
if l := len(wrapper.Raw().Folders); l != 3 {
t.Errorf("Incorrect number of folders, %d != 3", l)
}
for i, f := range wrapper.Raw().Folders {
if f.ID == "f1" && f.Invalid == "" {
t.Errorf("Folder %d (%q) is not set invalid", i, f.ID)
}
}
if l := len(wrapper.Raw().Devices); l != 3 {
t.Errorf("Incorrect number of devices, %d != 3", l)
}
f := wrapper.Folders()["f2"]
if l := len(f.Devices); l != 2 {
t.Errorf("Incorrect number of folder devices, %d != 2", l)
}
}
+33
View File
@@ -0,0 +1,33 @@
<configuration version="12">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR">
<address>192.0.2.1</address>
<address>192.0.2.2</address>
</device>
<device id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
<address>192.0.2.3:6070</address>
<address>[2001:db8::42]:4242</address>
</device>
<device id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
<address>[2001:db8::44]:4444</address>
<address>192.0.2.4:6090</address>
</device>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR">
<!-- duplicate, will be removed -->
<address>192.0.2.5</address>
</device>
<folder id="f1" directory="testdata/">
<!-- duplicate, will be disabled -->
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA"></device>
</folder>
<folder id="f1" directory="testdata/">
<!-- duplicate, will be disabled -->
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
</folder>
<folder id="f2" directory="testdata/">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA"></device>
<!-- duplicate device, will be removed -->
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
</folder>
</configuration>
+4 -7
View File
@@ -23,6 +23,7 @@ func init() {
}
func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) {
// Check that there is a port number in uri.Host, otherwise add one.
host, port, err := net.SplitHostPort(uri.Host)
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
// addr is on the form "1.2.3.4"
@@ -32,13 +33,9 @@ func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) {
uri.Host = net.JoinHostPort(host, "22000")
}
raddr, err := net.ResolveTCPAddr("tcp", uri.Host)
if err != nil {
l.Debugln(err)
return nil, err
}
conn, err := dialer.Dial(raddr.Network(), raddr.String())
// Don't try to resolve the address before dialing. The dialer may be a
// proxy, and we should let the proxy do the resolving in that case.
conn, err := dialer.Dial("tcp", uri.Host)
if err != nil {
l.Debugln(err)
return nil, err
+1
View File
@@ -1 +1,2 @@
!*.zip
testdata/*.db
+20 -33
View File
@@ -4,16 +4,9 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// Package db provides a set type to track local/remote files with newness
// checks. We must do a certain amount of normalization in here. We will get
// fed paths with either native or wire-format separators and encodings
// depending on who calls us. We transform paths to wire-format (NFC and
// slashes) on the way to the database, and transform to native format
// (varying separator and encoding) on the way back out.
package db
import (
"bytes"
"encoding/binary"
"fmt"
@@ -30,10 +23,10 @@ const maxBatchSize = 256 << 10
type BlockMap struct {
db *Instance
folder string
folder uint32
}
func NewBlockMap(db *Instance, folder string) *BlockMap {
func NewBlockMap(db *Instance, folder uint32) *BlockMap {
return &BlockMap{
db: db,
folder: folder,
@@ -123,7 +116,7 @@ func (m *BlockMap) Discard(files []protocol.FileInfo) error {
// Drop block map, removing all entries related to this block map from the db.
func (m *BlockMap) Drop() error {
batch := new(leveldb.Batch)
iter := m.db.NewIterator(util.BytesPrefix(m.blockKeyInto(nil, nil, "")[:1+64]), nil)
iter := m.db.NewIterator(util.BytesPrefix(m.blockKeyInto(nil, nil, "")[:keyPrefixLen+keyFolderLen]), nil)
defer iter.Release()
for iter.Next() {
if batch.Len() > maxBatchSize {
@@ -173,12 +166,13 @@ func (f *BlockFinder) String() string {
func (f *BlockFinder) Iterate(folders []string, hash []byte, iterFn func(string, string, int32) bool) bool {
var key []byte
for _, folder := range folders {
key = blockKeyInto(key, hash, folder, "")
folderID := f.db.folderIdx.ID([]byte(folder))
key = blockKeyInto(key, hash, folderID, "")
iter := f.db.NewIterator(util.BytesPrefix(key), nil)
defer iter.Release()
for iter.Next() && iter.Error() == nil {
folder, file := fromBlockKey(iter.Key())
file := blockKeyName(iter.Key())
index := int32(binary.BigEndian.Uint32(iter.Value()))
if iterFn(folder, osutil.NativeFilename(file), index) {
return true
@@ -194,48 +188,41 @@ func (f *BlockFinder) Fix(folder, file string, index int32, oldHash, newHash []b
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, uint32(index))
folderID := f.db.folderIdx.ID([]byte(folder))
batch := new(leveldb.Batch)
batch.Delete(blockKeyInto(nil, oldHash, folder, file))
batch.Put(blockKeyInto(nil, newHash, folder, file), buf)
batch.Delete(blockKeyInto(nil, oldHash, folderID, file))
batch.Put(blockKeyInto(nil, newHash, folderID, file), buf)
return f.db.Write(batch, nil)
}
// m.blockKey returns a byte slice encoding the following information:
// keyTypeBlock (1 byte)
// folder (64 bytes)
// folder (4 bytes)
// block hash (32 bytes)
// file name (variable size)
func blockKeyInto(o, hash []byte, folder, file string) []byte {
reqLen := 1 + 64 + 32 + len(file)
func blockKeyInto(o, hash []byte, folder uint32, file string) []byte {
reqLen := keyPrefixLen + keyFolderLen + keyHashLen + len(file)
if cap(o) < reqLen {
o = make([]byte, reqLen)
} else {
o = o[:reqLen]
}
o[0] = KeyTypeBlock
copy(o[1:], []byte(folder))
for i := len(folder); i < 64; i++ {
o[1+i] = 0
}
copy(o[1+64:], []byte(hash))
copy(o[1+64+32:], []byte(file))
binary.BigEndian.PutUint32(o[keyPrefixLen:], folder)
copy(o[keyPrefixLen+keyFolderLen:], []byte(hash))
copy(o[keyPrefixLen+keyFolderLen+keyHashLen:], []byte(file))
return o
}
func fromBlockKey(data []byte) (string, string) {
if len(data) < 1+64+32+1 {
// blockKeyName returns the file name from the block key
func blockKeyName(data []byte) string {
if len(data) < keyPrefixLen+keyFolderLen+keyHashLen+1 {
panic("Incorrect key length")
}
if data[0] != KeyTypeBlock {
panic("Incorrect key type")
}
file := string(data[1+64+32:])
slice := data[1 : 1+64]
izero := bytes.IndexByte(slice, 0)
if izero > -1 {
return string(slice[:izero]), file
}
return string(slice), file
file := string(data[keyPrefixLen+keyFolderLen+keyHashLen:])
return file
}
+6 -5
View File
@@ -10,6 +10,7 @@ import (
"testing"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syndtr/goleveldb/leveldb/util"
)
func genBlocks(n int) []protocol.BlockInfo {
@@ -55,7 +56,7 @@ func setup() (*Instance, *BlockFinder) {
}
func dbEmpty(db *Instance) bool {
iter := db.NewIterator(nil, nil)
iter := db.NewIterator(util.BytesPrefix([]byte{KeyTypeBlock}), nil)
defer iter.Release()
if iter.Next() {
return false
@@ -70,7 +71,7 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
t.Fatal("db not empty")
}
m := NewBlockMap(db, "folder1")
m := NewBlockMap(db, db.folderIdx.ID([]byte("folder1")))
f3.Flags |= protocol.FlagDirectory
@@ -152,8 +153,8 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
func TestBlockFinderLookup(t *testing.T) {
db, f := setup()
m1 := NewBlockMap(db, "folder1")
m2 := NewBlockMap(db, "folder2")
m1 := NewBlockMap(db, db.folderIdx.ID([]byte("folder1")))
m2 := NewBlockMap(db, db.folderIdx.ID([]byte("folder2")))
err := m1.Add([]protocol.FileInfo{f1})
if err != nil {
@@ -221,7 +222,7 @@ func TestBlockFinderFix(t *testing.T) {
return true
}
m := NewBlockMap(db, "folder1")
m := NewBlockMap(db, db.folderIdx.ID([]byte("folder1")))
err := m.Add([]protocol.FileInfo{f1})
if err != nil {
t.Fatal(err)
+2
View File
@@ -42,6 +42,8 @@ const (
KeyTypeDeviceStatistic
KeyTypeFolderStatistic
KeyTypeVirtualMtime
KeyTypeFolderIdx
KeyTypeDeviceIdx
)
type fileVersion struct {
+114
View File
@@ -0,0 +1,114 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package db
import (
"bytes"
"github.com/syndtr/goleveldb/leveldb"
)
// convertKeyFormat converts from the v0.12 to the v0.13 database format, to
// avoid having to do rescan. The change is in the key format for folder
// labels, so we basically just iterate over the database rewriting keys as
// necessary.
func convertKeyFormat(from, to *leveldb.DB) error {
l.Infoln("Converting database key format")
blocks, files, globals, unchanged := 0, 0, 0, 0
dbi := newDBInstance(to)
i := from.NewIterator(nil, nil)
for i.Next() {
key := i.Key()
switch key[0] {
case KeyTypeBlock:
folder, file := oldFromBlockKey(key)
folderIdx := dbi.folderIdx.ID([]byte(folder))
hash := key[1+64:]
newKey := blockKeyInto(nil, hash, folderIdx, file)
if err := to.Put(newKey, i.Value(), nil); err != nil {
return err
}
blocks++
case KeyTypeDevice:
newKey := dbi.deviceKey(oldDeviceKeyFolder(key), oldDeviceKeyDevice(key), oldDeviceKeyName(key))
if err := to.Put(newKey, i.Value(), nil); err != nil {
return err
}
files++
case KeyTypeGlobal:
newKey := dbi.globalKey(oldGlobalKeyFolder(key), oldGlobalKeyName(key))
if err := to.Put(newKey, i.Value(), nil); err != nil {
return err
}
globals++
case KeyTypeVirtualMtime:
// Cannot be converted, we drop it instead :(
default:
if err := to.Put(key, i.Value(), nil); err != nil {
return err
}
unchanged++
}
}
l.Infof("Converted %d blocks, %d files, %d globals (%d unchanged).", blocks, files, globals, unchanged)
return nil
}
func oldDeviceKeyFolder(key []byte) []byte {
folder := key[1 : 1+64]
izero := bytes.IndexByte(folder, 0)
if izero < 0 {
return folder
}
return folder[:izero]
}
func oldDeviceKeyDevice(key []byte) []byte {
return key[1+64 : 1+64+32]
}
func oldDeviceKeyName(key []byte) []byte {
return key[1+64+32:]
}
func oldGlobalKeyName(key []byte) []byte {
return key[1+64:]
}
func oldGlobalKeyFolder(key []byte) []byte {
folder := key[1 : 1+64]
izero := bytes.IndexByte(folder, 0)
if izero < 0 {
return folder
}
return folder[:izero]
}
func oldFromBlockKey(data []byte) (string, string) {
if len(data) < 1+64+32+1 {
panic("Incorrect key length")
}
if data[0] != KeyTypeBlock {
panic("Incorrect key type")
}
file := string(data[1+64+32:])
slice := data[1 : 1+64]
izero := bytes.IndexByte(slice, 0)
if izero > -1 {
return string(slice[:izero]), file
}
return string(slice), file
}
+136
View File
@@ -0,0 +1,136 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package db
import (
"archive/zip"
"io"
"os"
"path/filepath"
"testing"
"github.com/syndtr/goleveldb/leveldb"
)
func TestLabelConversion(t *testing.T) {
os.RemoveAll("testdata/oldformat.db")
defer os.RemoveAll("testdata/oldformat.db")
os.RemoveAll("testdata/newformat.db")
defer os.RemoveAll("testdata/newformat.db")
if err := unzip("testdata/oldformat.db.zip", "testdata"); err != nil {
t.Fatal(err)
}
odb, err := leveldb.OpenFile("testdata/oldformat.db", nil)
if err != nil {
t.Fatal(err)
}
ldb, err := leveldb.OpenFile("testdata/newformat.db", nil)
if err != nil {
t.Fatal(err)
}
if err = convertKeyFormat(odb, ldb); err != nil {
t.Fatal(err)
}
ldb.Close()
odb.Close()
inst, err := Open("testdata/newformat.db")
if err != nil {
t.Fatal(err)
}
fs := NewFileSet("default", inst)
files, deleted, _ := fs.GlobalSize()
if files+deleted != 953 {
// Expected number of global entries determined by
// ../../bin/stindex testdata/oldformat.db/ | grep global | grep -c default
t.Errorf("Conversion error, global list differs (%d != 953)", files+deleted)
}
files, deleted, _ = fs.LocalSize()
if files+deleted != 953 {
t.Errorf("Conversion error, device list differs (%d != 953)", files+deleted)
}
f := NewBlockFinder(inst)
// [block] F:"default" H:1c25dea9003cc16216e2a22900be1ec1cc5aaf270442904e2f9812c314e929d8 N:"f/f2/f25f1b3e6e029231b933531b2138796d" I:3
h := []byte{0x1c, 0x25, 0xde, 0xa9, 0x00, 0x3c, 0xc1, 0x62, 0x16, 0xe2, 0xa2, 0x29, 0x00, 0xbe, 0x1e, 0xc1, 0xcc, 0x5a, 0xaf, 0x27, 0x04, 0x42, 0x90, 0x4e, 0x2f, 0x98, 0x12, 0xc3, 0x14, 0xe9, 0x29, 0xd8}
found := 0
f.Iterate([]string{"default"}, h, func(folder, file string, idx int32) bool {
if folder == "default" && file == filepath.FromSlash("f/f2/f25f1b3e6e029231b933531b2138796d") && idx == 3 {
found++
}
return true
})
if found != 1 {
t.Errorf("Found %d blocks instead of expected 1", found)
}
inst.Close()
}
func unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer func() {
if err := r.Close(); err != nil {
panic(err)
}
}()
os.MkdirAll(dest, 0755)
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(f *zip.File) error {
rc, err := f.Open()
if err != nil {
return err
}
defer func() {
if err := rc.Close(); err != nil {
panic(err)
}
}()
path := filepath.Join(dest, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(path, f.Mode())
} else {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
panic(err)
}
}()
_, err = io.Copy(f, rc)
if err != nil {
return err
}
}
return nil
}
for _, f := range r.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
return nil
}
+195 -50
View File
@@ -8,11 +8,15 @@ package db
import (
"bytes"
"encoding/binary"
"os"
"path/filepath"
"sort"
"strings"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
"github.com/syndtr/goleveldb/leveldb/iterator"
@@ -25,14 +29,33 @@ type deletionHandler func(t readWriteTransaction, folder, device, name []byte, d
type Instance struct {
*leveldb.DB
folderIdx *smallIndex
deviceIdx *smallIndex
}
const (
keyPrefixLen = 1
keyFolderLen = 4 // indexed
keyDeviceLen = 4 // indexed
keyHashLen = 32
)
func Open(file string) (*Instance, error) {
opts := &opt.Options{
OpenFilesCacheCapacity: 100,
WriteBuffer: 4 << 20,
}
if _, err := os.Stat(file); os.IsNotExist(err) {
// The file we are looking to open does not exist. This may be the
// first launch so we should look for an old version and try to
// convert it.
if err := checkConvertDatabase(file); err != nil {
l.Infoln("Converting old database:", err)
l.Infoln("Will rescan from scratch.")
}
}
db, err := leveldb.OpenFile(file, opts)
if leveldbIsCorrupted(err) {
db, err = leveldb.RecoverFile(file, opts)
@@ -60,9 +83,12 @@ func OpenMemory() *Instance {
}
func newDBInstance(db *leveldb.DB) *Instance {
return &Instance{
i := &Instance{
DB: db,
}
i.folderIdx = newSmallIndex(i, []byte{KeyTypeFolderIdx})
i.deviceIdx = newSmallIndex(i, []byte{KeyTypeDeviceIdx})
return i
}
func (db *Instance) Compact() error {
@@ -72,13 +98,10 @@ func (db *Instance) Compact() error {
func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker, deleteFn deletionHandler) int64 {
sort.Sort(fileList(fs)) // sort list on name, same as in the database
start := db.deviceKey(folder, device, nil) // before all folder/device files
limit := db.deviceKey(folder, device, []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
t := db.newReadWriteTransaction()
defer t.close()
dbi := t.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, device, nil)[:keyPrefixLen+keyFolderLen+keyDeviceLen]), nil)
defer dbi.Release()
moreDb := dbi.Next()
@@ -237,13 +260,10 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, l
}
func (db *Instance) withHave(folder, device []byte, truncate bool, fn Iterator) {
start := db.deviceKey(folder, device, nil) // before all folder/device files
limit := db.deviceKey(folder, device, []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
t := db.newReadOnlyTransaction()
defer t.close()
dbi := t.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, device, nil)[:keyPrefixLen+keyFolderLen+keyDeviceLen]), nil)
defer dbi.Release()
for dbi.Next() {
@@ -258,13 +278,10 @@ func (db *Instance) withHave(folder, device []byte, truncate bool, fn Iterator)
}
func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte, f FileInfoTruncated) bool) {
start := db.deviceKey(folder, nil, nil) // before all folder/device files
limit := db.deviceKey(folder, protocol.LocalDeviceID[:], []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
t := db.newReadWriteTransaction()
defer t.close()
dbi := t.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, nil, nil)[:keyPrefixLen+keyFolderLen]), nil)
defer dbi.Release()
for dbi.Next() {
@@ -359,7 +376,10 @@ func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator
l.Debugf("vl.versions[0].device: %x", vl.versions[0].device)
l.Debugf("name: %q (%x)", name, name)
l.Debugf("fk: %q", fk)
l.Debugf("fk: %x %x %x", fk[1:1+64], fk[1+64:1+64+32], fk[1+64+32:])
l.Debugf("fk: %x %x %x",
fk[keyPrefixLen:keyPrefixLen+keyFolderLen],
fk[keyPrefixLen+keyFolderLen:keyPrefixLen+keyFolderLen+keyDeviceLen],
fk[keyPrefixLen+keyFolderLen+keyDeviceLen:])
panic(err)
}
@@ -403,13 +423,10 @@ func (db *Instance) availability(folder, file []byte) []protocol.DeviceID {
}
func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator) {
start := db.globalKey(folder, nil)
limit := db.globalKey(folder, []byte{0xff, 0xff, 0xff, 0xff})
t := db.newReadOnlyTransaction()
defer t.close()
dbi := t.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
dbi := t.NewIterator(util.BytesPrefix(db.globalKey(folder, nil)[:keyPrefixLen+keyFolderLen]), nil)
defer dbi.Release()
var fk []byte
@@ -546,9 +563,7 @@ func (db *Instance) checkGlobals(folder []byte, globalSize *sizeTracker) {
t := db.newReadWriteTransaction()
defer t.close()
start := db.globalKey(folder, nil)
limit := db.globalKey(folder, []byte{0xff, 0xff, 0xff, 0xff})
dbi := t.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
dbi := t.NewIterator(util.BytesPrefix(db.globalKey(folder, nil)[:keyPrefixLen+keyFolderLen]), nil)
defer dbi.Release()
var fk []byte
@@ -598,71 +613,72 @@ func (db *Instance) checkGlobals(folder []byte, globalSize *sizeTracker) {
// deviceKey returns a byte slice encoding the following information:
// keyTypeDevice (1 byte)
// folder (64 bytes)
// device (32 bytes)
// folder (4 bytes)
// device (4 bytes)
// name (variable size)
func (db *Instance) deviceKey(folder, device, file []byte) []byte {
return db.deviceKeyInto(nil, folder, device, file)
}
func (db *Instance) deviceKeyInto(k []byte, folder, device, file []byte) []byte {
reqLen := 1 + 64 + 32 + len(file)
reqLen := keyPrefixLen + keyFolderLen + keyDeviceLen + len(file)
if len(k) < reqLen {
k = make([]byte, reqLen)
}
k[0] = KeyTypeDevice
if len(folder) > 64 {
panic("folder name too long")
}
copy(k[1:], []byte(folder))
copy(k[1+64:], device[:])
copy(k[1+64+32:], []byte(file))
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.folderIdx.ID(folder))
binary.BigEndian.PutUint32(k[keyPrefixLen+keyFolderLen:], db.deviceIdx.ID(device))
copy(k[keyPrefixLen+keyFolderLen+keyDeviceLen:], []byte(file))
return k[:reqLen]
}
// deviceKeyName returns the device ID from the key
func (db *Instance) deviceKeyName(key []byte) []byte {
return key[1+64+32:]
return key[keyPrefixLen+keyFolderLen+keyDeviceLen:]
}
// deviceKeyFolder returns the folder name from the key
func (db *Instance) deviceKeyFolder(key []byte) []byte {
folder := key[1 : 1+64]
izero := bytes.IndexByte(folder, 0)
if izero < 0 {
return folder
folder, ok := db.folderIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
if !ok {
panic("bug: lookup of nonexistent folder ID")
}
return folder[:izero]
return folder
}
// deviceKeyDevice returns the device ID from the key
func (db *Instance) deviceKeyDevice(key []byte) []byte {
return key[1+64 : 1+64+32]
device, ok := db.deviceIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen+keyFolderLen:]))
if !ok {
panic("bug: lookup of nonexistent device ID")
}
return device
}
// globalKey returns a byte slice encoding the following information:
// keyTypeGlobal (1 byte)
// folder (64 bytes)
// folder (4 bytes)
// name (variable size)
func (db *Instance) globalKey(folder, file []byte) []byte {
k := make([]byte, 1+64+len(file))
k := make([]byte, keyPrefixLen+keyFolderLen+len(file))
k[0] = KeyTypeGlobal
if len(folder) > 64 {
panic("folder name too long")
}
copy(k[1:], []byte(folder))
copy(k[1+64:], []byte(file))
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.folderIdx.ID(folder))
copy(k[keyPrefixLen+keyFolderLen:], []byte(file))
return k
}
// globalKeyName returns the filename from the key
func (db *Instance) globalKeyName(key []byte) []byte {
return key[1+64:]
return key[keyPrefixLen+keyFolderLen:]
}
// globalKeyFolder returns the folder name from the key
func (db *Instance) globalKeyFolder(key []byte) []byte {
folder := key[1 : 1+64]
izero := bytes.IndexByte(folder, 0)
if izero < 0 {
return folder
folder, ok := db.folderIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
if !ok {
panic("bug: lookup of nonexistent folder ID")
}
return folder[:izero]
return folder
}
func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
@@ -692,3 +708,132 @@ func leveldbIsCorrupted(err error) bool {
return false
}
// checkConvertDatabase tries to convert an existing old (v0.11) database to
// new (v0.13) format.
func checkConvertDatabase(dbFile string) error {
oldLoc := filepath.Join(filepath.Dir(dbFile), "index-v0.11.0.db")
if _, err := os.Stat(oldLoc); os.IsNotExist(err) {
// The old database file does not exist; that's ok, continue as if
// everything succeeded.
return nil
} else if err != nil {
// Any other error is weird.
return err
}
// There exists a database in the old format. We run a one time
// conversion from old to new.
fromDb, err := leveldb.OpenFile(oldLoc, nil)
if err != nil {
return err
}
toDb, err := leveldb.OpenFile(dbFile, nil)
if err != nil {
return err
}
err = convertKeyFormat(fromDb, toDb)
if err != nil {
return err
}
err = toDb.Close()
if err != nil {
return err
}
// We've done this one, we don't want to do it again (if the user runs
// -reset or so). We don't care too much about errors any more at this stage.
fromDb.Close()
osutil.Rename(oldLoc, oldLoc+".converted")
return nil
}
// A smallIndex is an in memory bidirectional []byte to uint32 map. It gives
// fast lookups in both directions and persists to the database. Don't use for
// storing more items than fit comfortably in RAM.
type smallIndex struct {
db *Instance
prefix []byte
id2val map[uint32]string
val2id map[string]uint32
nextID uint32
mut sync.Mutex
}
func newSmallIndex(db *Instance, prefix []byte) *smallIndex {
idx := &smallIndex{
db: db,
prefix: prefix,
id2val: make(map[uint32]string),
val2id: make(map[string]uint32),
mut: sync.NewMutex(),
}
idx.load()
return idx
}
// load iterates over the prefix space in the database and populates the in
// memory maps.
func (i *smallIndex) load() {
tr := i.db.newReadOnlyTransaction()
it := tr.NewIterator(util.BytesPrefix(i.prefix), nil)
for it.Next() {
val := string(it.Value())
id := binary.BigEndian.Uint32(it.Key()[len(i.prefix):])
i.id2val[id] = val
i.val2id[val] = id
if id >= i.nextID {
i.nextID = id + 1
}
}
it.Release()
tr.close()
}
// ID returns the index number for the given byte slice, allocating a new one
// and persisting this to the database if necessary.
func (i *smallIndex) ID(val []byte) uint32 {
i.mut.Lock()
// intentionally avoiding defer here as we want this call to be as fast as
// possible in the general case (folder ID already exists). The map lookup
// with the conversion of []byte to string is compiler optimized to not
// copy the []byte, which is why we don't assign it to a temp variable
// here.
if id, ok := i.val2id[string(val)]; ok {
i.mut.Unlock()
return id
}
id := i.nextID
i.nextID++
valStr := string(val)
i.val2id[valStr] = id
i.id2val[id] = valStr
key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id
copy(key, i.prefix)
binary.BigEndian.PutUint32(key[len(i.prefix):], id)
i.db.Put(key, val, nil)
i.mut.Unlock()
return id
}
// Val returns the value for the given index number, or (nil, false) if there
// is no such index number.
func (i *smallIndex) Val(id uint32) ([]byte, bool) {
i.mut.Lock()
val, ok := i.id2val[id]
i.mut.Unlock()
if !ok {
return nil, false
}
return []byte(val), true
}
+5 -2
View File
@@ -16,7 +16,9 @@ func TestDeviceKey(t *testing.T) {
dev := []byte("device67890123456789012345678901")
name := []byte("name")
db := &Instance{}
db := OpenMemory()
db.folderIdx.ID(fld)
db.deviceIdx.ID(dev)
key := db.deviceKey(fld, dev, name)
@@ -38,7 +40,8 @@ func TestGlobalKey(t *testing.T) {
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
name := []byte("name")
db := &Instance{}
db := OpenMemory()
db.folderIdx.ID(fld)
key := db.globalKey(fld, name)
+2 -2
View File
@@ -97,7 +97,7 @@ func NewFileSet(folder string, db *Instance) *FileSet {
localVersion: make(map[protocol.DeviceID]int64),
folder: folder,
db: db,
blockmap: NewBlockMap(db, folder),
blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
mutex: sync.NewMutex(),
}
@@ -244,7 +244,7 @@ func DropFolder(db *Instance, folder string) {
db.dropFolder([]byte(folder))
bm := &BlockMap{
db: db,
folder: folder,
folder: db.folderIdx.ID([]byte(folder)),
}
bm.Drop()
NewVirtualMtimeRepo(db, folder).Drop()
Binary file not shown.
+5 -2
View File
@@ -7,6 +7,7 @@
package db
import (
"encoding/binary"
"fmt"
"time"
)
@@ -24,10 +25,12 @@ type VirtualMtimeRepo struct {
}
func NewVirtualMtimeRepo(ldb *Instance, folder string) *VirtualMtimeRepo {
prefix := string(KeyTypeVirtualMtime) + folder
var prefix [5]byte // key type + 4 bytes folder idx number
prefix[0] = KeyTypeVirtualMtime
binary.BigEndian.PutUint32(prefix[1:], ldb.folderIdx.ID([]byte(folder)))
return &VirtualMtimeRepo{
ns: NewNamespacedKV(ldb, prefix),
ns: NewNamespacedKV(ldb, string(prefix[:])),
}
}
+8 -8
View File
@@ -26,7 +26,7 @@ type CachingMux struct {
*suture.Supervisor
finders []cachedFinder
caches []*cache
mut sync.Mutex
mut sync.RWMutex
}
// A cachedFinder is a Finder with associated cache timeouts.
@@ -54,7 +54,7 @@ type cachedError interface {
func NewCachingMux() *CachingMux {
return &CachingMux{
Supervisor: suture.NewSimple("discover.cachingMux"),
mut: sync.NewMutex(),
mut: sync.NewRWMutex(),
}
}
@@ -75,7 +75,7 @@ func (m *CachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration, p
func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) {
var pdirect []prioritizedAddress
m.mut.Lock()
m.mut.RLock()
for i, finder := range m.finders {
if cacheEntry, ok := m.caches[i].Get(deviceID); ok {
// We have a cache entry. Lets see what it says.
@@ -129,7 +129,7 @@ func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays
m.caches[i].Set(deviceID, entry)
}
}
m.mut.Unlock()
m.mut.RUnlock()
direct = uniqueSortedAddrs(pdirect)
relays = uniqueSortedRelays(relays)
@@ -149,12 +149,12 @@ func (m *CachingMux) Error() error {
}
func (m *CachingMux) ChildErrors() map[string]error {
m.mut.Lock()
children := make(map[string]error, len(m.finders))
m.mut.RLock()
for _, f := range m.finders {
children[f.String()] = f.Error()
}
m.mut.Unlock()
m.mut.RUnlock()
return children
}
@@ -163,7 +163,7 @@ func (m *CachingMux) Cache() map[protocol.DeviceID]CacheEntry {
// children's caches.
res := make(map[protocol.DeviceID]CacheEntry)
m.mut.Lock()
m.mut.RLock()
for i := range m.finders {
// Each finder[i] has a corresponding cache at cache[i]. Go through it
// and populate the total, if it's newer than what's already in there.
@@ -183,7 +183,7 @@ func (m *CachingMux) Cache() map[protocol.DeviceID]CacheEntry {
}
}
}
m.mut.Unlock()
m.mut.RUnlock()
return res
}
+52
View File
@@ -91,3 +91,55 @@ func (f *fakeDiscovery) String() string {
func (f *fakeDiscovery) Cache() map[protocol.DeviceID]CacheEntry {
return nil
}
func TestCacheSlowLookup(t *testing.T) {
c := NewCachingMux()
c.ServeBackground()
defer c.Stop()
// Add a slow discovery service.
started := make(chan struct{})
f1 := &slowDiscovery{time.Second, started}
c.Add(f1, time.Minute, 0, 0)
// Start a lookup, which will take at least a second
t0 := time.Now()
go c.Lookup(protocol.LocalDeviceID)
<-started // The slow lookup method has been called so we're inside the lock
// It should be possible to get ChildErrors while it's running
c.ChildErrors()
// Only a small amount of time should have passed, not the full second
diff := time.Since(t0)
if diff > 500*time.Millisecond {
t.Error("ChildErrors was blocked for", diff)
}
}
type slowDiscovery struct {
delay time.Duration
started chan struct{}
}
func (f *slowDiscovery) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) {
close(f.started)
time.Sleep(f.delay)
return nil, nil, nil
}
func (f *slowDiscovery) Error() error {
return nil
}
func (f *slowDiscovery) String() string {
return "fake"
}
func (f *slowDiscovery) Cache() map[protocol.DeviceID]CacheEntry {
return nil
}
+41 -6
View File
@@ -520,11 +520,7 @@ func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.F
l.Debugf("IDX(in): %s %q: %d files", deviceID, folder, len(fs))
if !m.folderSharedWith(folder, deviceID) {
events.Default.Log(events.FolderRejected, map[string]string{
"folder": folder,
"device": deviceID.String(),
})
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, deviceID)
l.Debugf("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, deviceID)
return
}
@@ -566,7 +562,7 @@ func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []prot
l.Debugf("%v IDXUP(in): %s / %q: %d files", m, deviceID, folder, len(fs))
if !m.folderSharedWith(folder, deviceID) {
l.Infof("Update for 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, deviceID)
l.Debugf("Update for 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, deviceID)
return
}
@@ -596,6 +592,10 @@ func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []prot
func (m *Model) folderSharedWith(folder string, deviceID protocol.DeviceID) bool {
m.fmut.RLock()
defer m.fmut.RUnlock()
return m.folderSharedWithUnlocked(folder, deviceID)
}
func (m *Model) folderSharedWithUnlocked(folder string, deviceID protocol.DeviceID) bool {
for _, nfolder := range m.deviceFolders[deviceID] {
if nfolder == folder {
return true
@@ -629,6 +629,37 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
m.pmut.Unlock()
// Check the peer device's announced folders against our own. Emits events
// for folders that we don't expect (unknown or not shared).
m.fmut.Lock()
nextFolder:
for _, folder := range cm.Folders {
cfg := m.folderCfgs[folder.ID]
if _, err := protocol.HashAlgorithmFromFlagBits(folder.Flags); err != nil {
// The hash algorithm failed to deserialize, so it's not SHA256
// (the only acceptable algorithm).
l.Warnf("Device %v: %v", deviceID, err)
cfg.Invalid = err.Error() + " from " + deviceID.String()
m.cfg.SetFolder(cfg)
if srv := m.folderRunners[folder.ID]; srv != nil {
srv.setError(fmt.Errorf(cfg.Invalid))
}
continue nextFolder
}
if !m.folderSharedWithUnlocked(folder.ID, deviceID) {
events.Default.Log(events.FolderRejected, map[string]string{
"folder": folder.ID,
"device": deviceID.String(),
})
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
}
}
m.fmut.Unlock()
events.Default.Log(events.DeviceConnected, event)
l.Infof(`Device %s client is "%s %s" named "%s"`, deviceID, cm.ClientName, cm.ClientVersion, cm.DeviceName)
@@ -893,6 +924,10 @@ func (m *Model) GetIgnores(folder string) ([]string, []string, error) {
return lines, nil, fmt.Errorf("Folder %s does not exist", folder)
}
if !cfg.HasMarker() {
return lines, nil, fmt.Errorf("Folder %s stopped", folder)
}
fd, err := os.Open(filepath.Join(cfg.Path(), ".stignore"))
if err != nil {
if os.IsNotExist(err) {
+3 -5
View File
@@ -514,13 +514,11 @@ func TestIgnores(t *testing.T) {
t.Error("No error")
}
// Invalid path, marker should be missing, hence returns an error.
m.AddFolder(config.FolderConfiguration{ID: "fresh", RawPath: "XXX"})
ignores, _, err = m.GetIgnores("fresh")
if err != nil {
t.Error(err)
}
if len(ignores) > 0 {
t.Errorf("Expected no ignores, got: %v", ignores)
if err == nil {
t.Error("No error")
}
}
+9
View File
@@ -14,6 +14,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/syncthing/syncthing/lib/config"
@@ -1493,6 +1494,14 @@ func removeDevice(devices []protocol.DeviceID, device protocol.DeviceID) []proto
}
func (p *rwFolder) moveForConflict(name string) error {
if strings.Contains(filepath.Base(name), ".sync-conflict-") {
l.Infoln("Conflict for", name, "which is already a conflict copy; not copying again.")
if err := osutil.Remove(name); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
if p.maxConflicts == 0 {
if err := osutil.Remove(name); err != nil && !os.IsNotExist(err) {
return err
@@ -4,8 +4,6 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build !windows
package osutil
import (
-83
View File
@@ -1,83 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build windows
package osutil
import (
"net"
"os"
"strings"
"syscall"
"unsafe"
)
// Modified version of:
// http://stackoverflow.com/questions/23529663/how-to-get-all-addresses-and-masks-from-local-interfaces-in-go
// v4 only!
func getAdapterList() (*syscall.IpAdapterInfo, error) {
b := make([]byte, 10240)
l := uint32(len(b))
a := (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0]))
// TODO(mikio): GetAdaptersInfo returns IP_ADAPTER_INFO that
// contains IPv4 address list only. We should use another API
// for fetching IPv6 stuff from the kernel.
err := syscall.GetAdaptersInfo(a, &l)
if err == syscall.ERROR_BUFFER_OVERFLOW {
b = make([]byte, l)
a = (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0]))
err = syscall.GetAdaptersInfo(a, &l)
}
if err != nil {
return nil, os.NewSyscallError("GetAdaptersInfo", err)
}
return a, nil
}
func GetLans() ([]*net.IPNet, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
nets := make([]*net.IPNet, 0, len(ifaces))
aList, err := getAdapterList()
if err != nil {
return nil, err
}
for _, ifi := range ifaces {
for ai := aList; ai != nil; ai = ai.Next {
index := ai.Index
if ifi.Index == int(index) {
ipl := &ai.IpAddressList
for ; ipl != nil; ipl = ipl.Next {
ipStr := strings.Trim(string(ipl.IpAddress.String[:]), "\x00")
maskStr := strings.Trim(string(ipl.IpMask.String[:]), "\x00")
ip := net.ParseIP(ipStr)
maskip := net.ParseIP(maskStr)
if ip.IsUnspecified() || maskip.IsUnspecified() {
continue
}
nets = append(nets, &net.IPNet{
IP: ip,
Mask: net.IPv4Mask(
maskip[net.IPv6len-net.IPv4len],
maskip[net.IPv6len-net.IPv4len+1],
maskip[net.IPv6len-net.IPv4len+2],
maskip[net.IPv6len-net.IPv4len+3],
),
})
}
}
}
}
return nets, err
}
+54
View File
@@ -0,0 +1,54 @@
// Copyright (C) 2016 The Protocol Authors.
package protocol
import "fmt"
type HashAlgorithm int
const (
SHA256 HashAlgorithm = iota
)
func (h HashAlgorithm) String() string {
switch h {
case SHA256:
return "sha256"
default:
return "unknown"
}
}
// FlagBits returns the bits that we should or into the folder flag field to
// indicate the hash algorithm.
func (h HashAlgorithm) FlagBits() uint32 {
switch h {
case SHA256:
return FolderHashSHA256 << FolderHashShiftBits
default:
panic("unknown hash algorithm")
}
}
func (h *HashAlgorithm) UnmarshalText(bs []byte) error {
switch string(bs) {
case "sha256":
*h = SHA256
return nil
}
return fmt.Errorf("Unknown hash algorithm %q", string(bs))
}
func (h *HashAlgorithm) MarshalText() ([]byte, error) {
return []byte(h.String()), nil
}
func HashAlgorithmFromFlagBits(flags uint32) (HashAlgorithm, error) {
algo := flags >> FolderHashShiftBits & FolderHashMask
switch algo {
case FolderHashSHA256:
return SHA256, nil
default:
return 0, fmt.Errorf("Unknown hash algorithm %d", algo)
}
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright (C) 2016 The Protocol Authors.
package protocol
import "testing"
/*
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved | Hash |D|P|R|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
func TestHashAlgorithmFromFlagBits(t *testing.T) {
// SHA256 is algorithm zero, shifted three bits to the left (for clarity,
// I know it doesn't actually do anything).
sha256 := uint32(0 << 3)
h, err := HashAlgorithmFromFlagBits(sha256)
if err != nil {
t.Error(err)
}
if h != SHA256 {
t.Error("Zero should have unmarshalled as SHA256")
}
// Any other algorithm is unknown
unknown := uint32(1 << 3)
_, err = HashAlgorithmFromFlagBits(unknown)
if err == nil {
t.Error("Unknown algo should not have unmarshalled")
}
}
+7
View File
@@ -66,6 +66,13 @@ const (
FlagFolderReadOnly uint32 = 1 << 0
FlagFolderIgnorePerms = 1 << 1
FlagFolderIgnoreDelete = 1 << 2
// The folder hash algorithm IDs, to be put in the flags field by shifting
// left FolderHashShiftBits
FolderHashSHA256 = 0
// ... 1 through 15 currently reserved
FolderHashMask = 15
FolderHashShiftBits = 3
)
// ClusterConfigMessage.Folders.Devices flags
-12
View File
@@ -34,7 +34,6 @@ func TestMain(m *testing.M) {
}
func TestHeaderFunctions(t *testing.T) {
t.Parallel()
f := func(ver, id, typ int) bool {
ver = int(uint(ver) % 16)
id = int(uint(id) % 4096)
@@ -49,7 +48,6 @@ func TestHeaderFunctions(t *testing.T) {
}
func TestHeaderLayout(t *testing.T) {
t.Parallel()
var e, a uint32
// Version are the first four bits
@@ -75,7 +73,6 @@ func TestHeaderLayout(t *testing.T) {
}
func TestPing(t *testing.T) {
t.Parallel()
ar, aw := io.Pipe()
br, bw := io.Pipe()
@@ -95,7 +92,6 @@ func TestPing(t *testing.T) {
}
func TestVersionErr(t *testing.T) {
t.Parallel()
m0 := newTestModel()
m1 := newTestModel()
@@ -123,7 +119,6 @@ func TestVersionErr(t *testing.T) {
}
func TestTypeErr(t *testing.T) {
t.Parallel()
m0 := newTestModel()
m1 := newTestModel()
@@ -151,7 +146,6 @@ func TestTypeErr(t *testing.T) {
}
func TestClose(t *testing.T) {
t.Parallel()
m0 := newTestModel()
m1 := newTestModel()
@@ -187,7 +181,6 @@ func TestClose(t *testing.T) {
}
func TestElementSizeExceededNested(t *testing.T) {
t.Parallel()
m := ClusterConfigMessage{
ClientName: "longstringlongstringlongstringinglongstringlongstringlonlongstringlongstringlon",
}
@@ -198,7 +191,6 @@ func TestElementSizeExceededNested(t *testing.T) {
}
func TestMarshalIndexMessage(t *testing.T) {
t.Parallel()
f := func(m1 IndexMessage) bool {
for i, f := range m1.Files {
m1.Files[i].CachedSize = 0
@@ -219,7 +211,6 @@ func TestMarshalIndexMessage(t *testing.T) {
}
func TestMarshalRequestMessage(t *testing.T) {
t.Parallel()
f := func(m1 RequestMessage) bool {
return testMarshal(t, "request", &m1, &RequestMessage{})
}
@@ -230,7 +221,6 @@ func TestMarshalRequestMessage(t *testing.T) {
}
func TestMarshalResponseMessage(t *testing.T) {
t.Parallel()
f := func(m1 ResponseMessage) bool {
if len(m1.Data) == 0 {
m1.Data = nil
@@ -244,7 +234,6 @@ func TestMarshalResponseMessage(t *testing.T) {
}
func TestMarshalClusterConfigMessage(t *testing.T) {
t.Parallel()
f := func(m1 ClusterConfigMessage) bool {
return testMarshal(t, "clusterconfig", &m1, &ClusterConfigMessage{})
}
@@ -255,7 +244,6 @@ func TestMarshalClusterConfigMessage(t *testing.T) {
}
func TestMarshalCloseMessage(t *testing.T) {
t.Parallel()
f := func(m1 CloseMessage) bool {
return testMarshal(t, "close", &m1, &CloseMessage{})
}
+14 -2
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-BEP" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-BEP" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-bep \- Block Exchange Protocol v1
.
@@ -366,7 +366,7 @@ folder Flags field contains the following single bit flags:
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
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
| Reserved |D|P|R|
| Reserved | Hash |D|P|R|
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
.ft P
.fi
@@ -384,6 +384,18 @@ permissions for.
.TP
.B Bit 29 ("D", Ignore Deletes)
is set for folders that the device will ignore deletes for.
.TP
.B Bits 25\-28 (Hash)
indicates the hash algorithm in use for this folder, as a four bit integer.
The value values are:
.INDENT 7.0
.IP \(bu 2
0: SHA256
.IP \(bu 2
1: Murmur3\-128
.IP \(bu 2
2\-15: Reserved for future usage
.UNINDENT
.UNINDENT
.sp
The \fBOptions\fP field contains a list of options that apply to the folder.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-CONFIG" "5" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-CONFIG" "5" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-config \- Syncthing Configuration
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-DEVICE-IDS" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-DEVICE-IDS" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-device-ids \- Understanding Device IDs
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-EVENT-API" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-EVENT-API" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-event-api \- Event API
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-FAQ" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-FAQ" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-faq \- Frequently Asked Questions
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-GLOBALDISCO" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-GLOBALDISCO" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-globaldisco \- Global Discovery Protocol v3
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-LOCALDISCO" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-LOCALDISCO" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-localdisco \- Local Discovery Protocol v3
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-NETWORKING" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-NETWORKING" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-networking \- Firewall Setup
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-RELAY" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-RELAY" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-relay \- Relay Protocol v1
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-REST-API" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-REST-API" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-rest-api \- REST API
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-SECURITY" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-SECURITY" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-security \- Security Principles
.
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-STIGNORE" "5" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING-STIGNORE" "5" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing-stignore \- Prevent files from being synchronized to other nodes
.
+3 -2
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "TODO" "7" "December 21, 2015" "v0.12" "Syncthing"
.TH "TODO" "7" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
Todo \- Keep automatic backups of deleted files by other nodes
.
@@ -126,7 +126,8 @@ folderpath="$1"
filepath="$2"
# First ensure the dir where we need to store the file exists
mkdir \-p \(gadirname "$versionspath/$filepath"\(ga
outpath=\(gadirname "$versionspath/$filepath"\(ga
mkdir \-p "$outpath"
# Then move the file there
mv \-f "$folderpath/$filepath" "$versionspath/$filepath"
.ft P
+1 -1
View File
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING" "1" "December 21, 2015" "v0.12" "Syncthing"
.TH "SYNCTHING" "1" "January 01, 2016" "v0.12" "Syncthing"
.SH NAME
syncthing \- Syncthing
.
+27 -24
View File
@@ -14,7 +14,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"os"
@@ -192,30 +191,34 @@ func alterFiles(dir string) error {
return osutil.TryRename(path, newPath)
}
// Switch between files and directories
case r == 3 && comps > 3 && rand.Float64() < 0.2:
if !info.Mode().IsRegular() {
err = removeAll(path)
if err != nil {
/*
This doesn't in fact work. Sometimes it appears to. We need to get this sorted...
// Switch between files and directories
case r == 3 && comps > 3 && rand.Float64() < 0.2:
if !info.Mode().IsRegular() {
err = removeAll(path)
if err != nil {
return err
}
d1 := []byte("I used to be a dir: " + path)
err := ioutil.WriteFile(path, d1, 0644)
if err != nil {
return err
}
} else {
err := osutil.Remove(path)
if err != nil {
return err
}
err = os.MkdirAll(path, 0755)
if err != nil {
return err
}
generateFiles(path, 10, 20, "../LICENSE")
}
return err
}
d1 := []byte("I used to be a dir: " + path)
err := ioutil.WriteFile(path, d1, 0644)
if err != nil {
return err
}
} else {
err := osutil.Remove(path)
if err != nil {
return err
}
err = os.MkdirAll(path, 0755)
if err != nil {
return err
}
generateFiles(path, 10, 20, "../LICENSE")
}
return err
*/
/*
This fails. Bug?