Compare commits

...

70 Commits

Author SHA1 Message Date
Jakob Borg
9bb5988b4e lib/model: Don't deadlock when returning temp index block counts
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3194
2016-05-26 09:16:08 +00:00
Jakob Borg
c513171014 gui: Update translations 2016-05-26 09:49:07 +02:00
Jakob Borg
da5010d37a cmd/syncthing: Use API to generate API Key and folder ID (fixes #3179)
Expose a random string generator in the API and use it when the GUI
needs random strings for API key and folder ID.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3192
2016-05-26 07:25:34 +00:00
Jakob Borg
e6b78e5d56 lib/rand: Break out random functions into separate package
The intention for this package is to provide a combination of the
security of crypto/rand and the convenience of math/rand. It should be
the first choice of random data unless ultimate performance is required
and the usage is provably irrelevant from a security standpoint.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3186
2016-05-26 07:02:56 +00:00
Audrius Butkevicius
410d700ae3 cmd/syncthing: Do not modify events (fixes #3002)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3190
2016-05-26 06:54:44 +00:00
Audrius Butkevicius
fc173bf679 lib/model: Fix wild completion percentages
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3188
2016-05-26 06:53:27 +00:00
Jakob Borg
72154aa668 lib/upgrade: Prefer a minor upgrade over a major (fixes #3163)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3184
2016-05-25 14:01:52 +00:00
Jakob Borg
31b5156191 lib/util: Add secure random numbers source (fixes #3178)
The math/rand package contains lots of convenient functions, for example
to get an integer in a specified range without running into issues
caused by just truncating a number from a different distribution and so
on. But it's insecure, and we use if for things that benefit from being
more secure like session IDs, CSRF tokens and API keys.

This implements a math/rand.Source that reads from crypto/rand.Reader,
this bridging the gap between them. It also updates our RandomString to
use the new source, thus giving us secure session IDs and CSRF tokens.

Some future work remains:

 - Fix API keys by making the generation in the UI use this code as well

 - Refactor out these things into an actual random package, and audit
   our use of randomness everywhere

I'll leave both of those for the future in order to not muddy the waters
on this diff...

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3180
2016-05-25 06:38:38 +00:00
Lars K.W. Gohlke
ebce5d07ac lib/connections: Shorten connection limiting lines
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3177
2016-05-24 21:57:56 +00:00
Audrius Butkevicius
915e1ac7de lib/model: Handle (?d) deletes of directories (fixes #3164)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3170
2016-05-23 23:32:08 +00:00
Lars K.W. Gohlke
b78bfc0a43 build.go: add gometalinter to lint runs
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3085
2016-05-23 21:19:08 +00:00
Lars K.W. Gohlke
30436741a7 build: Also vet and lint build script
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3159
2016-05-23 12:23:55 +00:00
Jakob Borg
98734375f2 cmd/syncthing: Correctly set, parse and compare modified time HTTP headers (fixes #3165)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3167
2016-05-23 12:16:14 +00:00
norgeous
37816e3818 gui: Remove extra href on folder panel titles
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3139
2016-05-22 16:17:33 +00:00
Jakob Borg
4bc2b3f369 gui: Set CSRF stuff earlier (fixes #3138)
We need to set these properties *before* Angular starts making requests,
and doing that from the response to a request is too late. The obvious
choice (to me) would be to use the angular $cookies service, but that
service isn't available until after initialization so we can't use it.
Instead, add a special file that is loaded by index.html and includes
the info we need before the JS app even starts running.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3152
2016-05-22 10:26:09 +00:00
Audrius Butkevicius
00be2bf18d lib/model: Track puller creation times (fixes #3145)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3150
2016-05-22 10:16:09 +00:00
Jakob Borg
44290a66b7 lib/model: Leave temp file in place when final rename fails (fixes #3146)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3148
2016-05-22 09:06:07 +00:00
Jakob Borg
f6cc344623 vendor: Replace github.com/jackpal/gateway with github.com/calmh/gateway (fixes #3142)
Switch to my forked version which contains a fix for this issue. I'll
track upstream in the future if things update there, and attempt to
contribute back fixes...

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3149
2016-05-22 09:04:27 +00:00
Jakob Borg
a89d487510 vendor: Bump github.com/AudriusButkevicius/go-nat-pmp 2016-05-22 17:46:36 +09:00
Jakob Borg
a0ec4467fd cmd/syncthing: Emit new RemoteDownloadProgress event to track remote download progress
Without this the summary service doesn't know to recalculate completion
percentage for remote devices when DownloadProgress messages come in.
That means that completion percentage isn't updated in the GUI while
transfers of large files are ongoing. With this change, it updates
correctly.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3144
2016-05-22 07:52:08 +00:00
Jakob Borg
e7280f1eb5 issue_template: Add note about security issues 2016-05-21 22:49:37 +09:00
Jakob Borg
bf7fcc612d cmd/syncthing: Enforce stricter CSRF policy on /rest GET requests (fixes #3134)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3137
2016-05-21 13:48:55 +00:00
Jakob Borg
cff9bbc9c5 gui, man: Update docs & translations 2016-05-21 22:44:55 +09:00
Audrius Butkevicius
fddca3d2d6 lib/connections: Do not resolve addresses (fixes #3129)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3133
2016-05-21 01:31:23 +00:00
norgeous
9db49fb45e gui: Fix dark theme help button
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3130
2016-05-20 16:50:11 +00:00
Lars K.W. Gohlke
891409aedf cmd/syncthing: Extract flag parsing.
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3126
2016-05-19 21:47:53 +00:00
Lars K.W. Gohlke
77e47066ed build: Extract setGoPath
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3117
2016-05-19 21:01:23 +00:00
Audrius Butkevicius
852759f904 gui: Update translations (fixes #3125) 2016-05-19 19:44:52 +01:00
Jakob Borg
1dbc310c9b cmd/syncthing: Rename event LocalDiskUpdated -> LocalChangeDetected
I think this better reflects what it means. Also tweaks the verbose
format to be more like our other things and lightly refactors the code
to not have the boolean and include the folder in the event.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3121
2016-05-19 07:01:43 +00:00
Nate Morrison
86ca58e2a9 lib/model: Emit LocalDiskUpdated events on detecting local changes
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3055
2016-05-19 00:19:26 +00:00
Lars K.W. Gohlke
22280db5db lib: simplify code
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3119
2016-05-18 22:47:11 +00:00
Jakob Borg
8e060e23e3 lib/connections: Correctly add port to portless tcp:// URLs (fixes #3115)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3116
2016-05-18 14:27:17 +00:00
aviau
6e07742fe9 gui, lib: Add missing licenses (fixes #3100)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3108
2016-05-18 00:10:50 +00:00
Jakob Borg
04d5032055 gui: Fixup authors in about modal 2016-05-18 09:07:47 +09:00
aviau
73ae87fad1 etc: Add documentation key to syncthing-resume.service
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3109
2016-05-17 20:19:35 +00:00
Lars K.W. Gohlke
cd05282369 lib/connection: Remove unused functions
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3110
2016-05-17 20:07:18 +00:00
aviau
ee94d53bda all: Remove execute bit for non-executable files
Skip-check: authors

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3105
2016-05-17 14:39:50 +00:00
Jakob Borg
922e1407c2 lib/config: Don't migrate non-HTTPS-URL discovery servers to new path (fixes #3103)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3104
2016-05-17 13:43:35 +00:00
Jakob Borg
2ea22b1850 gui, man: Update docs & translations
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3101
2016-05-17 12:02:44 +00:00
Jakob Borg
2c1323ece6 lib/connections: Un-deprecate relaysEnabled (fixes #3074)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3098
2016-05-17 00:05:38 +00:00
Audrius Butkevicius
adb7fb43cb vendor: Update go-nat-pmp 2016-05-16 20:46:03 +01:00
Alex
d59fd9c22d lib/config: use correct ReleasesURL when upgrading from v0.13-beta
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3096
2016-05-14 22:03:07 +00:00
Jakob Borg
6f743f3138 Revert "lib/model: Emit LocalDiskUpdated events on detecting local changes"
This reverts commit 5a7fad0bcd.
2016-05-14 10:55:24 +02:00
Nate Morrison
5a7fad0bcd lib/model: Emit LocalDiskUpdated events on detecting local changes
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3055
2016-05-14 08:37:07 +00:00
Jakob Borg
5d2414dfa9 lib/config: Bump config version to 14
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3092
2016-05-13 14:13:24 +00:00
Jakob Borg
bef2425025 cmd/syncthing: Set User-Agent on upgrade checks
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3093
2016-05-13 14:11:59 +00:00
Jakob Borg
e8b4286c93 lib/config: Change upgrade check URL (fixes #3086)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3089
2016-05-13 09:17:10 +00:00
Jakob Borg
2e9bf0b67c lib/upgrade: Increase size limits, send version header
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3088
2016-05-13 09:01:31 +00:00
Lars K.W. Gohlke
935c273c8f cleanup: removed deadcode in connection/tcp_listen.go
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3084
2016-05-12 20:43:11 +00:00
Jakob Borg
b993b41847 lib/config: Minor attribute updates
As discussed in
https://github.com/syncthing/docs/pull/169

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3082
2016-05-12 08:23:18 +00:00
Jakob Borg
1be40cc4fa lib/ignore: Revert comma handling, upgrade globbing package
This was fixed upstream due to our ticket, so we no longer need the
manual handling of commas. Keep the tests and better debug output around
though.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3081
2016-05-12 07:11:16 +00:00
Lars K.W. Gohlke
d628b731d1 build: Remove unused code
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3079
2016-05-11 06:21:30 +00:00
Jakob Borg
21e116aa45 lib/scanner: Refactor scanner.Walk API
The old usage pattern was to create a Walker with a bunch of attributes,
then call Walk() on it and nothing else. This extracts the attributes
into a Config struct and exposes a Walk(cfg Config) method instead, as
there was no reason to expose the state-holding walker type.

Also creates a few no-op implementations of the necessary interfaces
so that we can skip nil checks and simiplify things here and there.

Definitely look at this diff without whitespace.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3060
2016-05-09 18:25:39 +00:00
Jakob Borg
d77d8ff803 lib/connections: Don't look at devices that are already optimally connected
Just an optimization. Required exposing the priority from the factory,
so made that an interface with an extra method instead of just a func
type.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3071
2016-05-09 15:33:25 +00:00
Jakob Borg
31f64186ae lib/connections: More fine grained locking (fixes #3066)
This fixes the deadlock by reducing where we hold the various locks. To
start with it splits up the existing "mut" into a "listenersMut" and a
"curConMut" as these are the two things being protected and I can see no
relation between them that requires a shared lock. It also moves all
model calls outside of the lock, as I see no reason to hold the lock
while calling the model (and it's risky, as proven).

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3069
2016-05-09 15:03:12 +00:00
Jakob Borg
1a703efa78 lib/model: Fix accounting error in rescan with multiple subs (fixes #3028)
When doing prefix scans in the database, "foo" should not be considered
a prefix of "foo2". Instead, it should match "foo" exactly and also
strings with the prefix "foo/". This is more restrictive than what the
standard leveldb prefix scan does so we add some code to enforce it.

Also exposes the initialScanCompleted on the rwfolder for testing, and
change it to be a channel (so we can wait for it from another
goroutine). Otherwise we can't be sure when the initial scan has
completed, and we need to wait for that or it might pick up changes
we're doing at an unexpected time.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3067
2016-05-09 12:56:21 +00:00
Jakob Borg
8b7b0a03eb lib/config: Don't require restart when adding folders/devices or changing listen address
The VersioningConfig change is because it defaults to nil but gets
deserialized to map[string]string{}. Now prepare() enforces a single
representation of the empty map.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3065
2016-05-09 11:30:19 +00:00
Jakob Borg
0761d804a4 cmd/syncthing: Use random folder ID for default folder, limit random charset
This uses the same charset as the Javascript code, excluding confusing
characters like 0, O, I, 1, l etc.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3064
2016-05-09 09:43:40 +00:00
Jakob Borg
3ad42d9279 lib/util: Should seed random number generator on startup
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3063
2016-05-09 09:36:42 +00:00
klemens
bd41e21c26 all: Correct spelling in comments
Skip-check: authors pr-build-mac

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3056
2016-05-08 10:54:22 +00:00
Jakob Borg
10fe23b8f2 script: Don't verify authors on commits tagged 'Skip-check: authors'
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3057
2016-05-08 10:47:57 +00:00
Jakob Borg
39899e40bf cmd/syncthing: Use ReadAll + json.Unmarshal in places were we care about consuming the reader
Because json.NewDecoder(r).Decode(&v) doesn't necessarily consume all
data on the reader, that means an HTTP connection can't be reused. We
don't do a lot of HTTP traffic where we read JSON responses, but the
discovery is one such place. The other two are for POSTs from the GUI,
where it's not exactly critical but still nice if the connection still
can be keep-alive'd after the request as well.

Also ensure that we call req.Body.Close() for clarity, even though this
should by all accounts not really be necessary.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3050
2016-05-06 22:01:56 +00:00
Jakob Borg
5d337bb24f lib/ignore: Handle bare commas in ignore patterns (fixes #3042)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3048
2016-05-06 15:45:11 +00:00
Jakob Borg
dd5909568f lib/upgrade: Don't attempt processing files larger than expected max binary size (ref #3045)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3047
2016-05-06 14:14:19 +00:00
Jakob Borg
38166e976f lib/upgrade: Enforce limits on download archives (fixes #3045)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3046
2016-05-06 13:58:34 +00:00
Jakob Borg
d6a7ffe0d4 lib/upgrade: Auto upgrade signature should cover version & arch (fixes #3044)
New signature is the HMAC of archive name (which includes the release
version and architecture) plus the contents of the binary. This is
expected in a new file "release.sig" which may be present in a
subdirectory. The new release tools put this in [.]metadata/release.sig.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3043
2016-05-06 13:30:35 +00:00
Jakob Borg
2ebc6996a2 cmd/stsigtool: Sign stdin when not given a file to sign, or when given "-"
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3041
2016-05-05 19:05:45 +00:00
Jakob Borg
2e840134d2 lib/protocol: Add Request benchmarks over raw and TLS encrypted TCP channels
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3040
2016-05-04 23:07:07 +00:00
Jakob Borg
66e1be33cf lib/protocol: Delete erroneously checked in test binary
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3039
2016-05-04 22:09:07 +00:00
Jakob Borg
591959261c gui: Fix comparison operator in expression (ref #3035)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3036
2016-05-04 20:30:18 +00:00
151 changed files with 2831 additions and 1097 deletions

View File

@@ -8,6 +8,7 @@ Anderson Mesquita <andersonvom@gmail.com>
Andrew Dunham <andrew@du.nham.ca>
Antony Male <antony.male@gmail.com>
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com> <frioux@gmail.com>
Alexandre Viau <alexandre@alexandreviau.net> <aviau@debian.org>
Audrius Butkevicius <audrius.butkevicius@gmail.com>
Bart De Vries <devriesb@gmail.com>
Ben Curthoys <ben@bencurthoys.com>

View File

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

1
NICKS
View File

@@ -7,6 +7,7 @@ andersonvom <andersonvom@gmail.com>
andrew-d <andrew@du.nham.ca>
asdil12 <dominik@heidler.eu>
AudriusButkevicius <audrius.butkevicius@gmail.com>
aviau <alexandre@alexandreviau.net> <aviau@debian.org>
bencurthoys <ben@bencurthoys.com>
bigbear2nd <bigbear2nd@gmail.com>
brbecker <brbecker@gmail.com>

View File

@@ -117,16 +117,8 @@ func main() {
log.SetOutput(os.Stdout)
log.SetFlags(0)
// If GOPATH isn't set, set it correctly with the assumption that we are
// in $GOPATH/src/github.com/syncthing/syncthing.
if os.Getenv("GOPATH") == "" {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
gopath := filepath.Clean(filepath.Join(cwd, "../../../../"))
log.Println("GOPATH is", gopath)
os.Setenv("GOPATH", gopath)
setGoPath()
}
// We use Go 1.5+ vendoring.
@@ -136,12 +128,7 @@ func main() {
// might have installed during "build.go setup".
os.Setenv("PATH", fmt.Sprintf("%s%cbin%c%s", os.Getenv("GOPATH"), os.PathSeparator, os.PathListSeparator, os.Getenv("PATH")))
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
flag.BoolVar(&noupgrade, "no-upgrade", noupgrade, "Disable upgrade functionality")
flag.StringVar(&version, "version", getVersion(), "Set compiled in version string")
flag.BoolVar(&race, "race", race, "Use race detector")
flag.Parse()
parseFlags()
switch goarch {
case "386", "amd64", "arm", "arm64", "ppc64", "ppc64le":
@@ -230,17 +217,46 @@ func main() {
clean()
case "vet":
vet("build.go")
vet("cmd", "lib")
case "lint":
lint(".")
lint("./cmd/...")
lint("./lib/...")
if isGometalinterInstalled() {
dirs := []string{".", "./cmd/...", "./lib/..."}
gometalinter("deadcode", dirs, "test/util.go")
gometalinter("structcheck", dirs)
gometalinter("varcheck", dirs)
}
default:
log.Fatalf("Unknown command %q", cmd)
}
}
// setGoPath sets GOPATH correctly with the assumption that we are
// in $GOPATH/src/github.com/syncthing/syncthing.
func setGoPath() {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
gopath := filepath.Clean(filepath.Join(cwd, "../../../../"))
log.Println("GOPATH is", gopath)
os.Setenv("GOPATH", gopath)
}
func parseFlags() {
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
flag.BoolVar(&noupgrade, "no-upgrade", noupgrade, "Disable upgrade functionality")
flag.StringVar(&version, "version", getVersion(), "Set compiled in version string")
flag.BoolVar(&race, "race", race, "Use race detector")
flag.Parse()
}
func checkRequiredGoVersion() (float64, bool) {
re := regexp.MustCompile(`go(\d+\.\d+)`)
ver := runtime.Version()
@@ -271,6 +287,7 @@ func setup() {
runPrint("go", "get", "-v", "github.com/axw/gocov/gocov")
runPrint("go", "get", "-v", "github.com/AlekSi/gocov-xml")
runPrint("go", "get", "-v", "bitbucket.org/tebeka/go2xunit")
runPrint("go", "get", "-v", "github.com/alecthomas/gometalinter")
}
func test(pkgs ...string) {
@@ -674,13 +691,6 @@ func buildHost() string {
return h
}
func buildEnvironment() string {
if v := os.Getenv("ENVIRONMENT"); len(v) > 0 {
return v
}
return "default"
}
func buildArch() string {
os := goos
if os == "darwin" {
@@ -693,16 +703,6 @@ func archiveName(target target) string {
return fmt.Sprintf("%s-%s-%s", target.name, buildArch(), version)
}
func run(cmd string, args ...string) []byte {
bs, err := runError(cmd, args...)
if err != nil {
log.Println(cmd, strings.Join(args, " "))
log.Println(string(bs))
log.Fatal(err)
}
return bytes.TrimSpace(bs)
}
func runError(cmd string, args ...string) ([]byte, error) {
ecmd := exec.Command(cmd, args...)
bs, err := ecmd.CombinedOutput()
@@ -869,7 +869,6 @@ func vet(dirs ...string) {
// A genuine error exit from the vet tool.
log.Fatal(err)
}
}
func lint(pkg string) {
@@ -918,3 +917,34 @@ func exitStatus(err error) int {
return -1
}
func isGometalinterInstalled() bool {
if _, err := runError("gometalinter", "--disable-all"); err != nil {
log.Println("gometalinter is not installed")
return false
}
return true
}
func gometalinter(linter string, dirs []string, excludes ...string) {
params := []string{"--disable-all"}
params = append(params, fmt.Sprintf("--deadline=%ds", 60))
params = append(params, "--enable="+linter)
for _, exclude := range excludes {
params = append(params, "--exclude="+exclude)
}
for _, dir := range dirs {
params = append(params, dir)
}
bs, err := runError("gometalinter", params...)
if len(bs) > 0 {
log.Printf("%s", bs)
}
if err != nil {
log.Printf("%v", err)
}
}

View File

@@ -29,7 +29,7 @@ var (
var (
// Static prefix that we use when generating fake device IDs, so that we
// can recognize them ourselves. Also makes the device ID start with
// "STPROBE-" which is humanly recognizeable.
// "STPROBE-" which is humanly recognizable.
randomPrefix = []byte{148, 223, 23, 4, 148}
// Our random, fake, device ID that we use when sending announcements.
@@ -117,7 +117,7 @@ func addrStrs(dev discover.Device) []string {
return ss
}
// returns a random but recognizeable device ID
// returns a random but recognizable device ID
func randomDeviceID() []byte {
var id [32]byte
copy(id[:], randomPrefix)

View File

@@ -8,6 +8,7 @@ package main
import (
"flag"
"io"
"io/ioutil"
"log"
"os"
@@ -31,7 +32,7 @@ Where command is one of:
gen
- generate a new key pair
sign <privkeyfile> <datafile>
sign <privkeyfile> [datafile]
- sign a file
verify <signaturefile> <datafile>
@@ -72,13 +73,19 @@ func sign(keyname, dataname string) {
log.Fatal(err)
}
fd, err := os.Open(dataname)
if err != nil {
log.Fatal(err)
var input io.Reader
if dataname == "-" || dataname == "" {
input = os.Stdin
} else {
fd, err := os.Open(dataname)
if err != nil {
log.Fatal(err)
}
defer fd.Close()
input = fd
}
defer fd.Close()
sig, err := signature.Sign(privkey, fd)
sig, err := signature.Sign(privkey, input)
if err != nil {
log.Fatal(err)
}

View File

@@ -35,11 +35,11 @@ import (
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/stats"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/util"
"github.com/vitrun/qart/qr"
"golang.org/x/crypto/bcrypt"
)
@@ -250,6 +250,7 @@ func (s *apiService) Serve() {
getRestMux.HandleFunc("/rest/svc/deviceid", s.getDeviceID) // id
getRestMux.HandleFunc("/rest/svc/lang", s.getLang) // -
getRestMux.HandleFunc("/rest/svc/report", s.getReport) // -
getRestMux.HandleFunc("/rest/svc/random/string", s.getRandomString) // [length]
getRestMux.HandleFunc("/rest/system/browse", s.getSystemBrowse) // current
getRestMux.HandleFunc("/rest/system/config", s.getSystemConfig) // -
getRestMux.HandleFunc("/rest/system/config/insync", s.getSystemConfigInsync) // -
@@ -298,13 +299,16 @@ func (s *apiService) Serve() {
// Serve compiled in assets unless an asset directory was set (for development)
assets := &embeddedStatic{
theme: s.cfg.GUI().Theme,
lastModified: time.Now(),
lastModified: time.Now().Truncate(time.Second), // must truncate, for the wire precision is 1s
mut: sync.NewRWMutex(),
assetDir: s.assetDir,
assets: auto.Assets(),
}
mux.Handle("/", assets)
// Handle the special meta.js path
mux.HandleFunc("/meta.js", s.getJSMetadata)
s.cfg.Subscribe(assets)
guiCfg := s.cfg.GUI()
@@ -461,10 +465,6 @@ func corsMiddleware(next http.Handler) http.Handler {
//
// See https://www.w3.org/TR/cors/ for details.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Add a generous access-control-allow-origin header since we may be
// redirecting REST requests over protocols
w.Header().Add("Access-Control-Allow-Origin", "*")
// Process OPTIONS requests
if r.Method == "OPTIONS" {
// Only GET/POST Methods are supported
@@ -529,6 +529,14 @@ func (s *apiService) restPing(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{"ping": "pong"})
}
func (s *apiService) getJSMetadata(w http.ResponseWriter, r *http.Request) {
meta, _ := json.Marshal(map[string]string{
"deviceID": s.id.String(),
})
w.Header().Set("Content-Type", "application/javascript")
fmt.Fprintf(w, "var metadata = %s;\n", meta)
}
func (s *apiService) getSystemVersion(w http.ResponseWriter, r *http.Request) {
sendJSON(w, map[string]string{
"version": Version,
@@ -718,6 +726,7 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
defer s.systemConfigMut.Unlock()
to, err := config.ReadJSON(r.Body, myID)
r.Body.Close()
if err != nil {
l.Warnln("decoding posted config:", err)
http.Error(w, err.Error(), http.StatusBadRequest)
@@ -742,7 +751,7 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
if curAcc := s.cfg.Options().URAccepted; to.Options.URAccepted > curAcc {
// UR was enabled
to.Options.URAccepted = usageReportVersion
to.Options.URUniqueID = util.RandomString(8)
to.Options.URUniqueID = rand.String(8)
} else if to.Options.URAccepted < curAcc {
// UR was disabled
to.Options.URAccepted = -1
@@ -922,6 +931,16 @@ func (s *apiService) getReport(w http.ResponseWriter, r *http.Request) {
sendJSON(w, reportData(s.cfg, s.model))
}
func (s *apiService) getRandomString(w http.ResponseWriter, r *http.Request) {
length := 32
if val, _ := strconv.Atoi(r.URL.Query().Get("length")); val > 0 {
length = val
}
str := rand.String(length)
sendJSON(w, map[string]string{"random": str})
}
func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
@@ -940,10 +959,15 @@ func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
func (s *apiService) postDBIgnores(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
var data map[string][]string
err := json.NewDecoder(r.Body).Decode(&data)
bs, err := ioutil.ReadAll(r.Body)
r.Body.Close()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
var data map[string][]string
err = json.Unmarshal(bs, &data)
if err != nil {
http.Error(w, err.Error(), 500)
return
@@ -1205,7 +1229,7 @@ func (s embeddedStatic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check for a compiled in asset for the current theme.
bs, ok := s.assets[theme+"/"+file]
if !ok {
// Check for an overriden default asset.
// Check for an overridden default asset.
if s.assetDir != "" {
p := filepath.Join(s.assetDir, config.DefaultTheme, filepath.FromSlash(file))
if _, err := os.Stat(p); err == nil {
@@ -1222,7 +1246,8 @@ func (s embeddedStatic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
if modifiedSince, err := time.Parse(r.Header.Get("If-Modified-Since"), http.TimeFormat); err == nil && modified.Before(modifiedSince) {
modifiedSince, err := http.ParseTime(r.Header.Get("If-Modified-Since"))
if err == nil && !modified.After(modifiedSince) {
w.WriteHeader(http.StatusNotModified)
return
}
@@ -1241,7 +1266,7 @@ func (s embeddedStatic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
gr.Close()
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
w.Header().Set("Last-Modified", modified.Format(http.TimeFormat))
w.Header().Set("Last-Modified", modified.UTC().Format(http.TimeFormat))
w.Header().Set("Cache-Control", "public")
w.Write(bs)

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ package main
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io/ioutil"
"net"
@@ -137,13 +138,13 @@ func TestAssetsDir(t *testing.T) {
// assetsdir/foo/a exists, overrides compiled in
expectURLToContain(t, s.URL+"/a", "overridden-foo")
// foo/b is compiled in, default/b is overriden, return compiled in
// foo/b is compiled in, default/b is overridden, return compiled in
expectURLToContain(t, s.URL+"/b", "foo")
// only exists as compiled in default/c so use that
expectURLToContain(t, s.URL+"/c", "default")
// only exists as overriden default/d so use that
// only exists as overridden default/d so use that
expectURLToContain(t, s.URL+"/d", "overridden-default")
}
@@ -189,7 +190,9 @@ type httpTestCase struct {
}
func TestAPIServiceRequests(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
@@ -344,13 +347,13 @@ func TestAPIServiceRequests(t *testing.T) {
for _, tc := range cases {
t.Log("Testing", tc.URL, "...")
testHTTPRequest(t, baseURL, tc)
testHTTPRequest(t, baseURL, tc, testAPIKey)
}
}
// testHTTPRequest tries the given test case, comparing the result code,
// content type, and result prefix.
func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase) {
func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase, apikey string) {
timeout := time.Second
if tc.Timeout > 0 {
timeout = tc.Timeout
@@ -359,7 +362,14 @@ func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase) {
Timeout: timeout,
}
resp, err := cli.Get(baseURL + tc.URL)
req, err := http.NewRequest("GET", baseURL+tc.URL, nil)
if err != nil {
t.Errorf("Unexpected error requesting %s: %v", tc.URL, err)
return
}
req.Header.Set("X-API-Key", apikey)
resp, err := cli.Do(req)
if err != nil {
t.Errorf("Unexpected error requesting %s: %v", tc.URL, err)
return
@@ -400,7 +410,7 @@ func TestHTTPLogin(t *testing.T) {
// Verify rejection when not using authorization
req, _ := http.NewRequest("GET", baseURL+"/rest/system/status", nil)
req, _ := http.NewRequest("GET", baseURL, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
@@ -491,3 +501,122 @@ func startHTTP(cfg *mockedConfig) (string, error) {
return baseURL, nil
}
func TestCSRFRequired(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
cli := &http.Client{
Timeout: time.Second,
}
// Getting the base URL (i.e. "/") should succeed.
resp, err := cli.Get(baseURL)
if err != nil {
t.Fatal("Unexpected error from getting base URL:", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatal("Getting base URL should succeed, not", resp.Status)
}
// Find the returned CSRF token for future use
var csrfTokenName, csrfTokenValue string
for _, cookie := range resp.Cookies() {
if strings.HasPrefix(cookie.Name, "CSRF-Token") {
csrfTokenName = cookie.Name
csrfTokenValue = cookie.Value
break
}
}
// Calling on /rest without a token should fail
resp, err = cli.Get(baseURL + "/rest/system/config")
if err != nil {
t.Fatal("Unexpected error from getting /rest/system/config:", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Fatal("Getting /rest/system/config without CSRF token should fail, not", resp.Status)
}
// Calling on /rest with a token should succeed
req, _ := http.NewRequest("GET", baseURL+"/rest/system/config", nil)
req.Header.Set("X-"+csrfTokenName, csrfTokenValue)
resp, err = cli.Do(req)
if err != nil {
t.Fatal("Unexpected error from getting /rest/system/config:", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatal("Getting /rest/system/config with CSRF token should succeed, not", resp.Status)
}
// Calling on /rest with the API key should succeed
req, _ = http.NewRequest("GET", baseURL+"/rest/system/config", nil)
req.Header.Set("X-API-Key", testAPIKey)
resp, err = cli.Do(req)
if err != nil {
t.Fatal("Unexpected error from getting /rest/system/config:", err)
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatal("Getting /rest/system/config with API key should succeed, not", resp.Status)
}
}
func TestRandomString(t *testing.T) {
const testAPIKey = "foobarbaz"
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
cli := &http.Client{
Timeout: time.Second,
}
// The default should be to return a 32 character random string
for _, url := range []string{"/rest/svc/random/string", "/rest/svc/random/string?length=-1", "/rest/svc/random/string?length=yo"} {
req, _ := http.NewRequest("GET", baseURL+url, nil)
req.Header.Set("X-API-Key", testAPIKey)
resp, err := cli.Do(req)
if err != nil {
t.Fatal(err)
}
var res map[string]string
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
t.Fatal(err)
}
if len(res["random"]) != 32 {
t.Errorf("Expected 32 random characters, got %q of length %d", res["random"], len(res["random"]))
}
}
// We can ask for a different length if we like
req, _ := http.NewRequest("GET", baseURL+"/rest/svc/random/string?length=27", nil)
req.Header.Set("X-API-Key", testAPIKey)
resp, err := cli.Do(req)
if err != nil {
t.Fatal(err)
}
var res map[string]string
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
t.Fatal(err)
}
if len(res["random"]) != 27 {
t.Errorf("Expected 27 random characters, got %q of length %d", res["random"], len(res["random"]))
}
}

View File

@@ -39,10 +39,10 @@ import (
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/symlinks"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/util"
"github.com/thejerf/suture"
)
@@ -532,8 +532,9 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
errors := logger.NewRecorder(l, logger.LevelWarn, maxSystemErrors, 0)
systemLog := logger.NewRecorder(l, logger.LevelDebug, maxSystemLog, initialSystemLog)
// Event subscription for the API; must start early to catch the early events.
apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents), 1000)
// Event subscription for the API; must start early to catch the early events. The LocalDiskUpdated
// event might overwhelm the event reciever in some situations so we will not subscribe to it here.
apiSub := events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents&^events.LocalChangeDetected), 1000)
if len(os.Getenv("GOMAXPROCS")) == 0 {
runtime.GOMAXPROCS(runtime.NumCPU())
@@ -760,7 +761,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
if opts.URUniqueID == "" {
// Previously the ID was generated from the node ID. We now need
// to generate a new one.
opts.URUniqueID = util.RandomString(8)
opts.URUniqueID = rand.String(8)
cfg.SetOptions(opts)
cfg.Save()
}
@@ -946,8 +947,9 @@ func defaultConfig(myName string) config.Configuration {
if !noDefaultFolder {
l.Infoln("Default folder created and/or linked to new config")
defaultFolder = config.NewFolderConfiguration("default", locations[locDefFolder])
folderID := rand.String(5) + "-" + rand.String(5)
defaultFolder = config.NewFolderConfiguration(folderID, locations[locDefFolder])
defaultFolder.Label = "Default Folder (" + folderID + ")"
defaultFolder.RescanIntervalS = 60
defaultFolder.MinDiskFreePct = 1
defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}}

View File

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

View File

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

View File

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

View File

@@ -195,7 +195,7 @@ code.ng-binding{
}
/* progess bars */
/* progress bars */
.progress-bar {
background-color: #217dbb !important;
}

View File

@@ -38,7 +38,7 @@ ul+h5 {
text-overflow: ellipsis;
overflow: hidden;
}
.panel-title a:hover {
a.panel-heading:hover {
text-decoration: none;
}

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Коментар, използван в началото на реда",
"Compression": "Компресиране",
"Connection Error": "Грешка при свързването",
"Connection Type": "Вид връзка",
"Copied from elsewhere": "Копиране от някъде другаде",
"Copied from original": "Копиран от оригинала",
"Copyright © 2014-2016 the following Contributors:": "Всички правата запазени © 2014-2016 Сътрудници:",
@@ -74,6 +75,7 @@
"Folder Label": "Етикет на папката",
"Folder Master": "Главна папка",
"Folder Path": "Път до папката",
"Folder Type": "Вид папка",
"Folders": "Папки",
"GUI": "Потребителски интерфейс",
"GUI Authentication Password": "Парола за потребителския интерфейс",
@@ -98,10 +100,12 @@
"Last File Received": "Последния получен файл",
"Last seen": "Последно видян",
"Later": "По-късно",
"Listeners": "Слушащи",
"Local Discovery": "Локално откриване",
"Local State": "Локално състояние",
"Local State (Total)": "Локално състояние (Общо)",
"Major Upgrade": "Основно Обновяване",
"Master": "Главен",
"Maximum Age": "Максимална възраст",
"Metadata Only": "Само мета информация",
"Minimum Free Disk Space": "Минимално свободно дисково пространство",
@@ -113,6 +117,7 @@
"Newest First": "Първо най-новите",
"No": "Не",
"No File Versioning": "Без версии",
"Normal": "Нормален",
"Notice": "Известие",
"OK": "ОК",
"Off": "Изключено",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Comentari quan és usat al principi d'una línia",
"Compression": "Compressió",
"Connection Error": "Error de connexió",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copiat d'un altre lloc",
"Copied from original": "Copiat de l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Carpeta mestra",
"Folder Path": "Camí de carpeta",
"Folder Type": "Folder Type",
"Folders": "Carpetes",
"GUI": "GUI",
"GUI Authentication Password": "Contrasenya d'autenticació GUI",
@@ -98,10 +100,12 @@
"Last File Received": "Últim fitxer rebut",
"Last seen": "Vist per última vegada",
"Later": "Després",
"Listeners": "Listeners",
"Local Discovery": "Descobriment Local",
"Local State": "Estat local",
"Local State (Total)": "Estat local (Total)",
"Major Upgrade": "Actualització major",
"Master": "Master",
"Maximum Age": "Antiguitat Màxima",
"Metadata Only": "Només metadades",
"Minimum Free Disk Space": "Espai de disc lliure mínim",
@@ -113,6 +117,7 @@
"Newest First": "Més nou primer",
"No": "No",
"No File Versioning": "Sense Versionat de Fitxer",
"Normal": "Normal",
"Notice": "Avís",
"OK": "OK",
"Off": "Desactivar",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Comentar, quant s'utilitza al principi d'una línia",
"Compression": "Compresió",
"Connection Error": "Error de connexió",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copiat de qualsevol lloc",
"Copied from original": "Copiat de l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 els següents Col·laboradors:",
@@ -74,6 +75,7 @@
"Folder Label": "Etiqueta de la Carpeta",
"Folder Master": "Carpeta principal",
"Folder Path": "Ruta de la carpeta",
"Folder Type": "Folder Type",
"Folders": "Carpetes",
"GUI": "IGU (Interfície Gràfica d'Usuari)",
"GUI Authentication Password": "Password d'autenticació de l'Interfície Gràfica d'Usuari (GUI)",
@@ -98,10 +100,12 @@
"Last File Received": "Darrer fitxer rebut",
"Last seen": "Vist per última vegada",
"Later": "Més tard",
"Listeners": "Listeners",
"Local Discovery": "Descobriment local",
"Local State": "Estat local",
"Local State (Total)": "Estat Local (Total)",
"Major Upgrade": "Actualització important",
"Master": "Master",
"Maximum Age": "Edat màxima",
"Metadata Only": "Sols metadades",
"Minimum Free Disk Space": "Espai minim de disc lliure",
@@ -113,6 +117,7 @@
"Newest First": "El més nou primer",
"No": "No",
"No File Versioning": "Sense versionat de fitxer",
"Normal": "Normal",
"Notice": "Avís",
"OK": "OK",
"Off": "Off",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Komentář, pokud použito na začátku řádku",
"Compression": "Komprese",
"Connection Error": "Chyba připojení",
"Connection Type": "Typ připojení",
"Copied from elsewhere": "Zkopírováno odjinud",
"Copied from original": "Zkopírováno z originálu",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následující přispěvatelé:",
@@ -74,6 +75,7 @@
"Folder Label": "Jmenovka adresáře",
"Folder Master": "Master adresář",
"Folder Path": "Cesta k adresáři",
"Folder Type": "Typ adresáře",
"Folders": "Adresáře",
"GUI": "GUI",
"GUI Authentication Password": "Přihlašovací heslo pro GUI",
@@ -98,10 +100,12 @@
"Last File Received": "Poslední přijatý soubor",
"Last seen": "Naposledy spatřen",
"Later": "Později",
"Listeners": "Naslouchající",
"Local Discovery": "Místní oznamování",
"Local State": "Místní status",
"Local State (Total)": "Místní status (Celkem)",
"Major Upgrade": "Důležitá aktualizace",
"Master": "Master",
"Maximum Age": "Maximální časový limit",
"Metadata Only": "Pouze metadata",
"Minimum Free Disk Space": "Minimální velikost volného místa na disku",
@@ -113,6 +117,7 @@
"Newest First": "Od nejnovějšího",
"No": "Ne",
"No File Versioning": "Bez verzování souborů",
"Normal": "Normální",
"Notice": "Oznámení",
"OK": "OK",
"Off": "Vypnuta",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Kommentering som bruges i starten af en linje",
"Compression": "Anvend komprimering",
"Connection Error": "Tilslutnings fejl",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Kopieret fra et andet sted",
"Copied from original": "Kopieret fra originalen",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Mastermappe",
"Folder Path": "Mappesti",
"Folder Type": "Folder Type",
"Folders": "Mapper",
"GUI": "GUI",
"GUI Authentication Password": "GUI-kodeord",
@@ -98,10 +100,12 @@
"Last File Received": "Sidste modtaget fil",
"Last seen": "Sidst set",
"Later": "Senere",
"Listeners": "Listeners",
"Local Discovery": "Lokal opslag",
"Local State": "Lokal tilstand",
"Local State (Total)": "Lokal tilstand (total)",
"Major Upgrade": "Ny version",
"Master": "Master",
"Maximum Age": "Maks alder",
"Metadata Only": "Kun metadata",
"Minimum Free Disk Space": "Mindst ledig diskplads",
@@ -113,6 +117,7 @@
"Newest First": "Nyeste først",
"No": "Nej",
"No File Versioning": "Ingen filversion",
"Normal": "Normal",
"Notice": "OBS",
"OK": "OK",
"Off": "Slå fra",

View File

@@ -21,17 +21,18 @@
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein externer Programmaufruf handhabt die Versionierung. Es muss die Datei aus dem zu synchronisierendem Verzeichnis entfernen.",
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
"Any devices configured on an introducer device will be added to this device as well.": "Alle Geräte, die beim Verteiler eingetragen sind, werden auch bei diesem Gerät eingetragen",
"Automatic upgrades": "automatische Updates",
"Automatic upgrades": "Automatische Updates aktivieren",
"Be careful!": "Vorsicht!",
"Bugs": "Fehler",
"CPU Utilization": "Prozessorauslastung",
"Changelog": "Änderungsprotokoll",
"Clean out after": "Löschen nach",
"Close": "Schließen",
"Command": "Kommando",
"Command": "Befehl",
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
"Compression": "Komprimierung",
"Connection Error": "Verbindungsfehler",
"Connection Type": "Verbindungstyp",
"Copied from elsewhere": "Von anderer Quelle kopiert",
"Copied from original": "Vom Original kopiert",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 der folgenden Unterstützer:",
@@ -74,6 +75,7 @@
"Folder Label": "Verzeichnisbezeichnung",
"Folder Master": "Master Verzeichnis - schreibgeschützt",
"Folder Path": "Verzeichnispfad",
"Folder Type": "Ordnertyp",
"Folders": "Verzeichnisse",
"GUI": "GUI",
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
@@ -98,10 +100,12 @@
"Last File Received": "Letzte Änderung",
"Last seen": "Zuletzt online",
"Later": "Später",
"Listeners": "Lauscher",
"Local Discovery": "Lokale Gerätesuche",
"Local State": "Lokaler Status",
"Local State (Total)": "Lokaler Status (Gesamt)",
"Major Upgrade": "Hauptversionsupgrade",
"Master": "Master",
"Maximum Age": "Höchstalter",
"Metadata Only": "Nur Metadaten",
"Minimum Free Disk Space": "Minimal freier Festplattenspeicher",
@@ -113,6 +117,7 @@
"Newest First": "Neueste zuerst",
"No": "Nein",
"No File Versioning": "Keine Dateiversionierung",
"Normal": "Normal",
"Notice": "Hinweis",
"OK": "OK",
"Off": "Aus",
@@ -240,5 +245,5 @@
"items": "Objekte",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} möchte das Verzeichnis \"{{folder}}\" teilen.",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen."
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} möchte den Ordner \"{{folderLabel}}\" ({{folder}}) teilen."
}

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Σχόλιο, όταν χρησιμοποιείται στην αρχή μιας γραμμής",
"Compression": "Συμπίεση",
"Connection Error": "Σφάλμα σύνδεσης",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Έχει αντιγραφεί από κάπου αλλού",
"Copied from original": "Έχει αντιγραφεί από το πρωτότυπο",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Να μην επιτρέπονται αλλαγές",
"Folder Path": "Μονοπάτι φακέλου",
"Folder Type": "Folder Type",
"Folders": "Φάκελοι",
"GUI": "Γραφικό περιβάλλον",
"GUI Authentication Password": "Κωδικός για την πρόσβαση στη διεπαφή",
@@ -98,10 +100,12 @@
"Last File Received": "Πιο πρόσφατο αρχείο",
"Last seen": "Τελευταία φορά συνδεδεμένος",
"Later": "Αργότερα",
"Listeners": "Listeners",
"Local Discovery": "Τοπική ανεύρεση",
"Local State": "Τοπική κατάσταση",
"Local State (Total)": "Τοπική κατάσταση (συνολικά)",
"Major Upgrade": "Σημαντική αναβάθμιση",
"Master": "Master",
"Maximum Age": "Μέγιστη ηλικία",
"Metadata Only": "Μόνο μεταδεδομένα",
"Minimum Free Disk Space": "Ελάχιστος ελεύθερος αποθηκευτικός χώρος",
@@ -113,6 +117,7 @@
"Newest First": "Το νεότερο πρώτα",
"No": "Όχι",
"No File Versioning": "Να μην τηρούνται εκδόσεις",
"Normal": "Normal",
"Notice": "Σημείωση",
"OK": "OK",
"Off": "Απενεργοποιημένο",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression": "Compression",
"Connection Error": "Connection Error",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copied from elsewhere",
"Copied from original": "Copied from original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Folder Master",
"Folder Path": "Folder Path",
"Folder Type": "Folder Type",
"Folders": "Folders",
"GUI": "GUI",
"GUI Authentication Password": "GUI Authentication Password",
@@ -98,10 +100,12 @@
"Last File Received": "Last File Received",
"Last seen": "Last seen",
"Later": "Later",
"Listeners": "Listeners",
"Local Discovery": "Local Discovery",
"Local State": "Local State",
"Local State (Total)": "Local State (Total)",
"Major Upgrade": "Major Upgrade",
"Master": "Master",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
"Minimum Free Disk Space": "Minimum Free Disk Space",
@@ -113,6 +117,7 @@
"Newest First": "Newest First",
"No": "No",
"No File Versioning": "No File Versioning",
"Normal": "Normal",
"Notice": "Notice",
"OK": "OK",
"Off": "Off",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression": "Compression",
"Connection Error": "Connection Error",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copied from elsewhere",
"Copied from original": "Copied from original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Folder Master",
"Folder Path": "Folder Path",
"Folder Type": "Folder Type",
"Folders": "Folders",
"GUI": "GUI",
"GUI Authentication Password": "GUI Authentication Password",
@@ -98,10 +100,12 @@
"Last File Received": "Last File Received",
"Last seen": "Last seen",
"Later": "Later",
"Listeners": "Listeners",
"Local Discovery": "Local Discovery",
"Local State": "Local State",
"Local State (Total)": "Local State (Total)",
"Major Upgrade": "Major Upgrade",
"Master": "Master",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
"Minimum Free Disk Space": "Minimum Free Disk Space",
@@ -113,6 +117,7 @@
"Newest First": "Newest First",
"No": "No",
"No File Versioning": "No File Versioning",
"Normal": "Normal",
"Notice": "Notice",
"OK": "OK",
"Off": "Off",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Comentar, cuando se usa al comienzo de una línea",
"Compression": "Compresión",
"Connection Error": "Error de conexión",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copiado de otro sitio",
"Copied from original": "Copiado del original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes Colaboradores:",
@@ -74,6 +75,7 @@
"Folder Label": "Etiqueta de la Carpeta",
"Folder Master": "Carpeta principal",
"Folder Path": "Ruta de la carpeta",
"Folder Type": "Folder Type",
"Folders": "Carpetas",
"GUI": "GUI",
"GUI Authentication Password": "Password de la Interfaz Gráfica de Usuario (GUI)",
@@ -98,10 +100,12 @@
"Last File Received": "Último fichero recibido",
"Last seen": "Visto por última vez",
"Later": "Más tarde",
"Listeners": "Listeners",
"Local Discovery": "Descubrimiento local",
"Local State": "Estado local",
"Local State (Total)": "Estado Local (Total)",
"Major Upgrade": "Actualización importante",
"Master": "Master",
"Maximum Age": "Edad máxima",
"Metadata Only": "Sólo metadatos",
"Minimum Free Disk Space": "Espacio mínimo libre en disco",
@@ -113,6 +117,7 @@
"Newest First": "El más nuevo primero",
"No": "No",
"No File Versioning": "Sin versionado de fichero",
"Normal": "Normal",
"Notice": "Aviso",
"OK": "OK",
"Off": "Desconectar",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Comentario, cuando es utilizado al inicio de una línea.",
"Compression": "Compresión",
"Connection Error": "Error de conexión",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copiado desde otra parte.",
"Copied from original": "Copiado del original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes contribuidores:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Repositorio maestro",
"Folder Path": "Ruta del repositorio",
"Folder Type": "Folder Type",
"Folders": "Repositorios",
"GUI": "GUI",
"GUI Authentication Password": "Contraseña de autenticación de la GUI",
@@ -98,10 +100,12 @@
"Last File Received": "Último archivo recibido",
"Last seen": "Visto por ultima vez",
"Later": "Más tarde",
"Listeners": "Listeners",
"Local Discovery": "Búsqueda en red local",
"Local State": "Estado local",
"Local State (Total)": "Estado local (total)",
"Major Upgrade": "Actualización mayor",
"Master": "Master",
"Maximum Age": "Edad máxima",
"Metadata Only": "Sólo metadatos",
"Minimum Free Disk Space": "Espacio mínimo libre en disco",
@@ -113,6 +117,7 @@
"Newest First": "Nuevo primero",
"No": "No",
"No File Versioning": "Sin control de versiones de archivos",
"Normal": "Normal",
"Notice": "Aviso",
"OK": "OK",
"Off": "Apagado",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Kommentti, käytettäessä rivin alussa",
"Compression": "Pakkaus",
"Connection Error": "Yhteysvirhe",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Kopioitu muualta",
"Copied from original": "Kopioitu alkuperäisestä lähteestä",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Hallitsijakansio",
"Folder Path": "Kansion polku",
"Folder Type": "Folder Type",
"Folders": "Kansiot",
"GUI": "GUI",
"GUI Authentication Password": "GUI:n salasana",
@@ -98,10 +100,12 @@
"Last File Received": "Viimeksi vastaanotettu tiedosto",
"Last seen": "Nähty viimeksi",
"Later": "Myöhemmin",
"Listeners": "Listeners",
"Local Discovery": "Paikallinen etsintä",
"Local State": "Paikallinen tila",
"Local State (Total)": "Paikallinen tila (Yhteensä)",
"Major Upgrade": "Pääversion päivitys.",
"Master": "Master",
"Maximum Age": "Maksimi-ikä",
"Metadata Only": "Vain metadata",
"Minimum Free Disk Space": "Vapaan levytilan vähimmäismäärä",
@@ -113,6 +117,7 @@
"Newest First": "Uusin ensin",
"No": "Ei",
"No File Versioning": "Ei tiedostoversiointia",
"Normal": "Normal",
"Notice": "Huomautus",
"OK": "OK",
"Off": "Pois",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
"Compression": "Compression",
"Connection Error": "Erreur de connexion",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Copié d'ailleurs",
"Copied from original": "Copié depuis l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Répertoire maître",
"Folder Path": "Chemin du répertoire",
"Folder Type": "Folder Type",
"Folders": "Dossiers",
"GUI": "GUI",
"GUI Authentication Password": "Mot de passe d'authentification GUI",
@@ -98,10 +100,12 @@
"Last File Received": "Dernier fichier reçu",
"Last seen": "Dernière apparition",
"Later": "Plus tard",
"Listeners": "Listeners",
"Local Discovery": "Recherche locale",
"Local State": "État local",
"Local State (Total)": "État local (Total)",
"Major Upgrade": "Mise à jour majeure",
"Master": "Master",
"Maximum Age": "Ancienneté maximum",
"Metadata Only": "Métadonnées uniquement",
"Minimum Free Disk Space": "Espace disque libre minimum",
@@ -113,6 +117,7 @@
"Newest First": "Les plus récents en premier",
"No": "Non",
"No File Versioning": "Pas de version de fichier",
"Normal": "Normal",
"Notice": "Notification",
"OK": "OK",
"Off": "Éteint",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
"Compression": "Compression",
"Connection Error": "Erreur de connexion",
"Connection Type": "Type de connexion",
"Copied from elsewhere": "Copié d'ailleurs",
"Copied from original": "Copié depuis l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016, les contributeurs suivants:",
@@ -74,6 +75,7 @@
"Folder Label": "Étiquette du dossier",
"Folder Master": "Dossier maître",
"Folder Path": "Chemin du dossier",
"Folder Type": "Type de répertoire",
"Folders": "Dossiers",
"GUI": "GUI",
"GUI Authentication Password": "Mot de passe d'authentification GUI",
@@ -98,10 +100,12 @@
"Last File Received": "Dernier fichier reçu",
"Last seen": "Dernière apparition",
"Later": "Plus tard",
"Listeners": "Systèmes en écoute",
"Local Discovery": "Recherche locale",
"Local State": "État local",
"Local State (Total)": "État local (Total)",
"Major Upgrade": "Mise à jour majeure",
"Master": "Maitre",
"Maximum Age": "Ancienneté maximum",
"Metadata Only": "Métadonnées uniquement",
"Minimum Free Disk Space": "Espace disque libre minimum",
@@ -113,6 +117,7 @@
"Newest First": "Les plus récents en premier",
"No": "Non",
"No File Versioning": "Pas de version de fichier",
"Normal": "Normal",
"Notice": "Notification",
"OK": "OK",
"Off": "Éteint",
@@ -152,7 +157,7 @@
"Reused": "Réutilisé",
"Save": "Sauver",
"Scan Time Remaining": "Intervalle entre chaque analyse",
"Scanning": "En cours d'analyse",
"Scanning": "Analyse en cours",
"Select the devices to share this folder with.": "Sélectionner les machines avec qui partager ce dossier.",
"Select the folders to share with this device.": "Sélectionner les dossiers à partager avec cette machine.",
"Settings": "Configuration",
@@ -179,7 +184,7 @@
"Stopped": "Arrêté",
"Support": "Aide",
"Sync Protocol Listen Addresses": "Adresse d'écoute du protocole de synchronisation",
"Syncing": "En cours de synchronisation",
"Syncing": "Synchronisation en cours",
"Syncthing has been shut down.": "Syncthing a été éteint.",
"Syncthing includes the following software or portions thereof:": "Syncthing intègre les logiciels suivants (ou des éléments provenant de ces logiciels) :",
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Kommentaar, wannear as brûkt by it begjin fan in rige",
"Compression": "Kompresje",
"Connection Error": "Ferbiningsflater",
"Connection Type": "Ferbiningstype",
"Copied from elsewhere": "Oernommen fan earne oars",
"Copied from original": "Oernommen fan orizjineel",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 de folgende bydragers:",
@@ -55,7 +56,7 @@
"Edit Device": "Apparaat bewurkje",
"Edit Folder": "Map bewurkje",
"Editing": "Bewurkjen",
"Enable NAT traversal": "Enable NAT traversal",
"Enable NAT traversal": "NAT-trochkruse ynskeakelje",
"Enable Relaying": "Trochjaan tastean",
"Enable UPnP": "UPnP oansette",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Fier troch komma's skieden (\"tcp://ip:port\", \"tcp://host:port\") adressen yn of \"dynamic\" om automatyske ûntdekking fan it adres út te fieren.",
@@ -74,6 +75,7 @@
"Folder Label": "Map-opskrift",
"Folder Master": "Map-master",
"Folder Path": "Map-paad",
"Folder Type": "Maptype",
"Folders": "Mappen",
"GUI": "GUI",
"GUI Authentication Password": "Wachtwurd foar ferifikaasje yn GUI",
@@ -98,10 +100,12 @@
"Last File Received": "Leste triem ûntfongen",
"Last seen": "Lêst sjoen",
"Later": "Letter",
"Listeners": "Harkers",
"Local Discovery": "Lokale ûntdekking",
"Local State": "Lokale tastân",
"Local State (Total)": "Lokale tastân (Folledich)",
"Major Upgrade": "Wichtige fernijing",
"Master": "Master",
"Maximum Age": "Maksimale âldens",
"Metadata Only": "Allinnich metadata",
"Minimum Free Disk Space": "Minimale frije skiifromte",
@@ -113,6 +117,7 @@
"Newest First": "Nijste earst",
"No": "Nee",
"No File Versioning": "Gjin triemferzjebehear",
"Normal": "Normaal",
"Notice": "Notysje",
"OK": "Okee",
"Off": "Ut",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Megjegyzés, a sor elején használva",
"Compression": "Tömörítés",
"Connection Error": "Kapcsolódási hiba",
"Connection Type": "Kapcsolat típus",
"Copied from elsewhere": "Másolva máshonnan",
"Copied from original": "Másolva az eredetiről",
"Copyright © 2014-2016 the following Contributors:": "Szerzői jog © 2014-2016 az alábbi közreműködők:",
@@ -74,6 +75,7 @@
"Folder Label": "Mappa címke",
"Folder Master": "Központi mappa",
"Folder Path": "Mappa elérési útja",
"Folder Type": "Mappa típus",
"Folders": "Mappák",
"GUI": "Grafikus felület",
"GUI Authentication Password": "Grafikus felület jelszava",
@@ -98,10 +100,12 @@
"Last File Received": "Utolsó beérkezett fájl",
"Last seen": "Utoljára látva",
"Later": "Később",
"Listeners": "Kapcsolatok",
"Local Discovery": "Helyi felfedezés",
"Local State": "Helyi állapot",
"Local State (Total)": "Helyi állapot (Teljes)",
"Major Upgrade": "Főverzió frissítés",
"Master": "Központi",
"Maximum Age": "Maximális kor",
"Metadata Only": "Csak metaadatok",
"Minimum Free Disk Space": "Minimális szabad lemezterület",
@@ -113,6 +117,7 @@
"Newest First": "Újabb először",
"No": "Nem",
"No File Versioning": "Nincs fájl verziókövetés",
"Normal": "Normál",
"Notice": "Megjegyzés",
"OK": "Rendben",
"Off": "Kikapcsolva",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Komentar, digunakan saat awal baris",
"Compression": "Kompresi",
"Connection Error": "Koneksi Galat",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Tersalin dari tempat lain",
"Copied from original": "Tersalin dari asal",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Master Folder",
"Folder Path": "Path Folder",
"Folder Type": "Folder Type",
"Folders": "Folder",
"GUI": "GUI",
"GUI Authentication Password": "Sandi Otentikasi GUI",
@@ -98,10 +100,12 @@
"Last File Received": "Last File Received",
"Last seen": "Last seen",
"Later": "Later",
"Listeners": "Listeners",
"Local Discovery": "Local Discovery",
"Local State": "Local State",
"Local State (Total)": "Local State (Total)",
"Major Upgrade": "Major Upgrade",
"Master": "Master",
"Maximum Age": "Maximum Age",
"Metadata Only": "Metadata Only",
"Minimum Free Disk Space": "Minimum Free Disk Space",
@@ -113,6 +117,7 @@
"Newest First": "Newest First",
"No": "No",
"No File Versioning": "No File Versioning",
"Normal": "Normal",
"Notice": "Notice",
"OK": "OK",
"Off": "Off",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Per commentare, va inserito all'inizio di una riga",
"Compression": "Compressione",
"Connection Error": "Errore di Connessione",
"Connection Type": "Tipo di Connessione",
"Copied from elsewhere": "Copiato da qualche altra parte",
"Copied from original": "Copiato dall'originale",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 i seguenti Collaboratori:",
@@ -74,6 +75,7 @@
"Folder Label": "Etichetta per la cartella",
"Folder Master": "Cartella Principale",
"Folder Path": "Percorso Cartella",
"Folder Type": "Tipo di Cartella",
"Folders": "Cartelle",
"GUI": "Interfaccia grafica utente",
"GUI Authentication Password": "Password di Autenticazione dell'Utente",
@@ -98,10 +100,12 @@
"Last File Received": "Ultimo File Ricevuto",
"Last seen": "Ultima connessione",
"Later": "Più Tardi",
"Listeners": "In ascolto",
"Local Discovery": "Individuazione Locale",
"Local State": "Stato Locale",
"Local State (Total)": "Stato Locale (Totale)",
"Major Upgrade": "Aggiornamento principale",
"Master": "Principale",
"Maximum Age": "Durata Massima",
"Metadata Only": "Solo i Metadati",
"Minimum Free Disk Space": "Minimo spazio libero su disco",
@@ -113,6 +117,7 @@
"Newest First": "Prima il più recente",
"No": "No",
"No File Versioning": "Nessun Controllo Versione",
"Normal": "Normale",
"Notice": "Avviso",
"OK": "OK",
"Off": "Disattiva",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "行頭で使用された場合、コメント行",
"Compression": "圧縮",
"Connection Error": "接続エラー",
"Connection Type": "接続種別",
"Copied from elsewhere": "別ファイルからコピー済",
"Copied from original": "元ファイルからコピー済",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "フォルダー名",
"Folder Master": "フォルダーのマスター",
"Folder Path": "フォルダーパス",
"Folder Type": "フォルダーの種類",
"Folders": "フォルダー",
"GUI": "GUI",
"GUI Authentication Password": "GUI認証パスワード",
@@ -98,10 +100,12 @@
"Last File Received": "最後に受信したファイル",
"Last seen": "最終接続日時",
"Later": "後で設定",
"Listeners": "待ち受けポート",
"Local Discovery": "LAN内で探索",
"Local State": "ローカル状態",
"Local State (Total)": "ローカル状態 (合計)",
"Major Upgrade": "メジャーアップグレード",
"Master": "マスター",
"Maximum Age": "最大寿命",
"Metadata Only": "メタデータのみ",
"Minimum Free Disk Space": "同期を停止する最小空きディスク容量",
@@ -113,6 +117,7 @@
"Newest First": "新しい順",
"No": "いいえ",
"No File Versioning": "バージョン管理をしない",
"Normal": "通常",
"Notice": "通知",
"OK": "OK",
"Off": "オフ",
@@ -192,7 +197,7 @@
"The device ID cannot be blank.": "デバイスIDは空欄にできません。",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ここに入力するデバイスIDは、接続したい相手側デバイスの [メニュー]→[IDを表示] で確認することができます。スペースとハイフンは入力しなくてもかまいません。",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ここに入力するデバイスIDは、接続したい相手側デバイスの [メニュー]→[IDを表示] で確認することができます。スペースとハイフンは入力しなくてもかまいません。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "利用状況レポートは暗号化されて毎日送信されます。この情報はプラットフォーム、フォルダの大きさ、アプリのバージョンを調査するために使われます。レポートのデータが変更された場合、このダイアログがまた表示されます。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "利用状況レポートは暗号化されて毎日送信されます。この情報はプラットフォーム、フォルダの大きさ、アプリのバージョンを調査するために使われます。送信するデータセットが変更された場合、このダイアログで再度確認が求められます。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "入力されたデバイスIDが正しくありません。デバイスIDは、52文字または56文字のアルファベットと数字からなる文字列です。スペースとハイフンは入力してもしなくてもかまいません。",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "第1コマンドライン引数はフォルダーのパス、第2引数はフォルダー内の相対パスです。",
"The folder ID cannot be blank.": "フォルダーIDは空欄にできません。",

View File

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

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
"Compression": "Kompresija",
"Connection Error": "Susijungimo klaida",
"Connection Type": "Ryšio tipas",
"Copied from elsewhere": "Nukopijuota iš kitur",
"Copied from original": "Nukopijuota iš originalo",
"Copyright © 2014-2016 the following Contributors:": "Autorių teisės © 2014-2016 šių bendraautorių:",
@@ -74,6 +75,7 @@
"Folder Label": "Aplanko etiketė",
"Folder Master": "Aplanko vadovas",
"Folder Path": "Kelias iki aplanko",
"Folder Type": "Aplanko tipas",
"Folders": "Aplankai",
"GUI": "Valdymo skydelis",
"GUI Authentication Password": "Valdymo skydelio slaptažodis",
@@ -98,10 +100,12 @@
"Last File Received": "Paskutinis priimtas failas",
"Last seen": "Paskutinį kartą matytas",
"Later": "Vėliau",
"Listeners": "Listeners",
"Local Discovery": "Vietinis matomumas",
"Local State": "Vietinė būsena",
"Local State (Total)": "Vietinė būsena (Bendrai)",
"Major Upgrade": "Stambus atnaujinimas",
"Master": "Master",
"Maximum Age": "Maksimalus amžius",
"Metadata Only": "Metaduomenims",
"Minimum Free Disk Space": "Minimum laisvos vietos diske",
@@ -113,6 +117,7 @@
"Newest First": "Naujausi pirmiau",
"No": "Ne",
"No File Versioning": "Nėra versijų valdymo",
"Normal": "Normal",
"Notice": "Įspėjimas",
"OK": "Gerai",
"Off": "Netaikoma",

View File

@@ -8,13 +8,13 @@
"Add": "Legg til",
"Add Device": "Legg til Enhet",
"Add Folder": "Legg til Mappe",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Legg til ekstern enhet",
"Add new folder?": "Legg til ny mappe?",
"Address": "Adresse",
"Addresses": "Adresser",
"Advanced": "Avansert",
"Advanced Configuration": "Avanserte Innstillinger",
"Advanced settings": "Advanced settings",
"Advanced settings": "Avanserte innstillinger ",
"All Data": "Alle data",
"Allow Anonymous Usage Reporting?": "Tillat Anonym Innsamling Av Brukerdata?",
"Alphabetic": "Alfabetisk",
@@ -32,14 +32,15 @@
"Comment, when used at the start of a line": "Kommentar, når det blir brukt i starten av en linje.",
"Compression": "Komprimering",
"Connection Error": "Tilkoblingsfeil",
"Connection Type": "Tilkoblingstype",
"Copied from elsewhere": "Kopiert fra et annet sted",
"Copied from original": "Kopiert fra original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Opphavsrett © 2014-2016 for følgende bidragsytere:",
"Copyright © 2015 the following Contributors:": "Opphavsrett © 2015 de følgende bidragsytere:",
"Danger!": "Fare!",
"Delete": "Slett",
"Deleted": "Slettet",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} {{address}}) ønsker å koble til. Legge til ny enhet?",
"Device ID": "Enhets ID",
"Device Identification": "Enhetskjennemerke",
"Device Name": "Navn på Enhet",
@@ -55,7 +56,7 @@
"Edit Device": "Rediger Enhet",
"Edit Folder": "Rediger Mappe",
"Editing": "Redigerer",
"Enable NAT traversal": "Enable NAT traversal",
"Enable NAT traversal": "Slå på NAT traversering",
"Enable Relaying": "Aktiver relésending",
"Enable UPnP": "Aktiver UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Skriv inn kommaseparerte (\"tcp://ip:port\", \"tcp://host:port\") adresser, eller ordet \"dynamic\" for å gjøre automatisk oppslag for adressen.",
@@ -71,9 +72,10 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskyttet mot endringer som er gjort på andre enheter, men endringer som er gjort på denne enheten blir sendt til resten av gruppen.",
"Folder": "Katalog",
"Folder ID": "Mappe ID",
"Folder Label": "Folder Label",
"Folder Label": "Merkelapp for katalog",
"Folder Master": "Styrende Mappe",
"Folder Path": "Mappeplassering",
"Folder Type": "Katalogtype",
"Folders": "Mapper",
"GUI": "grafisk brukergrensesnitt",
"GUI Authentication Password": "Passord for GUI-autenisering",
@@ -98,10 +100,12 @@
"Last File Received": "Sist Mottatte Fil",
"Last seen": "Sist sett",
"Later": "Senere",
"Listeners": "Lyttere",
"Local Discovery": "Lokalt oppslag",
"Local State": "Lokal Tilstand",
"Local State (Total)": "Lokal Tilstand (Total)",
"Major Upgrade": "Hovedoppgradering",
"Master": "Hoved",
"Maximum Age": "Maksimal Levetid",
"Metadata Only": "Kun metadata",
"Minimum Free Disk Space": "Nødvendig ledig diskplass",
@@ -113,11 +117,12 @@
"Newest First": "Den nyeste først",
"No": "Nei",
"No File Versioning": "Ingen Versjonskontroll",
"Normal": "Normal",
"Notice": "Merknader",
"OK": "OK",
"Off": "Av",
"Oldest First": "Den eldste først",
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
"Optional descriptive label for the folder. Can be different on each device.": "Valgfri merkelapp på katalogen. Denne kan være ulik på forskjellige enheter",
"Options": "Valg",
"Out of Sync": "Ikke synkronisert",
"Out of Sync Items": "Ikke Synkroniserte Element",
@@ -139,9 +144,9 @@
"Relayed via": "Relé via",
"Relays": "Reléer",
"Release Notes": "Utgivelsesnotat",
"Remote Devices": "Remote Devices",
"Remote Devices": "Andre enheter",
"Remove": "Fjern",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Påkrevd identifikator for katalogen. Denne må være lik på alle enheter i samme klynge.",
"Rescan": "Gjennomsøk på nytt",
"Rescan All": "Gjennomsøk alt på nytt",
"Rescan Interval": "Intervall for gjennomsøking",
@@ -212,7 +217,7 @@
"The rate limit must be a non-negative number (0: no limit)": "Hastighetsbegrensningen kan ikke være et negativt tall (0: ingen begrensing)",
"The rescan interval must be a non-negative number of seconds.": "Antall sekund for intervallet kan ikke være negativt.",
"They are retried automatically and will be synced when the error is resolved.": "Disse hentes automatisk og vil synkroniseres når feilen er blitt utbedret.",
"This Device": "This Device",
"This Device": "Denne enheten",
"This can easily give hackers access to read and change any files on your computer.": "Dette kan lett gi hackere tilgang til å lese og endre alle filer på datamaskinen din.",
"This is a major version upgrade.": "Dette er en hovedoppgradering",
"Trash Can File Versioning": "Papirkurv Versjonskontroll",
@@ -230,7 +235,7 @@
"Version": "Versjon",
"Versions Path": "Plassering Av Versjoner",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versjoner blir automatisk slettet når maksimal levetid er nådd eller når antall filer er oversteget.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Advarsel, denne stien er en underkatalog i en eksisterende katalog \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Merk at når en ny enhet blir lagt til må denne også legges til på andre siden.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Når en ny mappe blir lagt til, husk at Mappe-ID blir brukt til å binde sammen mapper mellom enheter. Det er forskjell på store og små bokstaver, så IDene må være identiske på alle enhetene.",
"Yes": "Ja",
@@ -239,6 +244,6 @@
"full documentation": "all dokumentasjon",
"items": "elementer",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} ønsker å dele katalogen \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} ønsker å dele katalogen \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Reageer indien gebruikt aan het begin van een lijn.",
"Compression": "Compressie",
"Connection Error": "Verbindingsfout",
"Connection Type": "Soort verbinding",
"Copied from elsewhere": "Gekopieerd vanaf elders",
"Copied from original": "Gekopieerd van het origineel",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 voor de volgende contributanten:",
@@ -74,6 +75,7 @@
"Folder Label": "Map label",
"Folder Master": "Hoofdmap",
"Folder Path": "Maplocatie",
"Folder Type": "Soort map",
"Folders": "Mappen",
"GUI": "GUI",
"GUI Authentication Password": "GUI-wachtwoord",
@@ -98,10 +100,12 @@
"Last File Received": "Laatst ontvangen bestand",
"Last seen": "Laatst gezien op",
"Later": "Later",
"Listeners": "Luisteraars",
"Local Discovery": "Lokaal zoeken",
"Local State": "Lokale status",
"Local State (Total)": "Lokale status (totaal)",
"Major Upgrade": "Grote update",
"Master": "Master",
"Maximum Age": "Maximum leeftijd",
"Metadata Only": "Alleen metadata",
"Minimum Free Disk Space": "Minimale vrije schijfruimte",
@@ -113,6 +117,7 @@
"Newest First": "Nieuwste eerst",
"No": "Nee",
"No File Versioning": "Geen versiebeheer",
"Normal": "Normaal",
"Notice": "Mededeling",
"OK": "OK",
"Off": "Uit",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Kommentar, når brukt i starten av linja",
"Compression": "Komprimering",
"Connection Error": "Tilkoplingsfeil",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Kopiert frå ein annan stad",
"Copied from original": "Kopiert frå originalen",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Styrande Mappe",
"Folder Path": "Mappeplassering",
"Folder Type": "Folder Type",
"Folders": "Mapper",
"GUI": "grafisk brukargrensesnitt",
"GUI Authentication Password": "GUI Passord",
@@ -98,10 +100,12 @@
"Last File Received": "Siste mottatte fila",
"Last seen": "Sist sett",
"Later": "Seinare",
"Listeners": "Listeners",
"Local Discovery": "Lokal oppdaging",
"Local State": "Lokal Tilstand",
"Local State (Total)": "Lokal tilstand (total)",
"Major Upgrade": "Hovudoppgradering",
"Master": "Master",
"Maximum Age": "Maksimal Levetid",
"Metadata Only": "Berre metadata",
"Minimum Free Disk Space": "Naudsynt ledig diskplass",
@@ -113,6 +117,7 @@
"Newest First": "Nyaste fyrst",
"No": "Nei",
"No File Versioning": "Ingen filutgåvehandtering",
"Normal": "Normal",
"Notice": "Merknad",
"OK": "OK",
"Off": "Av",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Komentarz, jeżeli użyty na początku linii",
"Compression": "Kompresja",
"Connection Error": "Błąd połączenia",
"Connection Type": "Rodzaj połączenia",
"Copied from elsewhere": "Skopiowane z innego miejsca ",
"Copied from original": "Skopiowane z oryginału",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016: ",
@@ -74,6 +75,7 @@
"Folder Label": "Etykieta folderu",
"Folder Master": "Główny folder",
"Folder Path": "Ścieżka folderu",
"Folder Type": "Rodzaj folderu",
"Folders": "Foldery",
"GUI": "GUI",
"GUI Authentication Password": "Hasło",
@@ -98,10 +100,12 @@
"Last File Received": "Ostatni otrzymany plik",
"Last seen": "Ostatnio widziany",
"Later": "Później",
"Listeners": "Nasłuchujący",
"Local Discovery": "Lokalne odnajdywanie",
"Local State": "Status lokalny",
"Local State (Total)": "Status lokalny (suma)",
"Major Upgrade": "Ważna aktualizacja",
"Master": "Mistrz",
"Maximum Age": "Maksymalny wiek",
"Metadata Only": "Tylko metadane",
"Minimum Free Disk Space": "Minimum wolnego miejsca na dysku",
@@ -113,6 +117,7 @@
"Newest First": "Najnowsze na początku",
"No": "Nie",
"No File Versioning": "Bez wersjonowania pliku",
"Normal": "Zwykły",
"Notice": "Wskazówka",
"OK": "OK",
"Off": "Wyłącz",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Comentário, se usado no início de uma linha",
"Compression": "Compressão",
"Connection Error": "Erro de conexão",
"Connection Type": "Tipo da conexão",
"Copied from elsewhere": "Copiado de outro lugar",
"Copied from original": "Copiado do original",
"Copyright © 2014-2016 the following Contributors:": "Direitos reservados © 2014-2016 aos seguintes colaboradores:",
@@ -74,6 +75,7 @@
"Folder Label": "Rótulo da pasta",
"Folder Master": "Pasta mestre",
"Folder Path": "Caminho da pasta",
"Folder Type": "Tipo da pasta",
"Folders": "Pastas",
"GUI": "Interface gráfica",
"GUI Authentication Password": "Senha para acesso à interface",
@@ -98,10 +100,12 @@
"Last File Received": "Último arquivo recebido",
"Last seen": "Visto por último em",
"Later": "Depois",
"Listeners": "Escutadores",
"Local Discovery": "Descoberta local",
"Local State": "Estado local",
"Local State (Total)": "Estado local (total)",
"Major Upgrade": "Atualização \"major\"",
"Master": "Mestre",
"Maximum Age": "Idade máxima",
"Metadata Only": "Somente metadados",
"Minimum Free Disk Space": "Espaço livre mínimo no disco",
@@ -113,6 +117,7 @@
"Newest First": "Mais novo primeiro",
"No": "Não",
"No File Versioning": "Sem versionamento de arquivos",
"Normal": "Normal",
"Notice": "Aviso",
"OK": "OK",
"Off": "Desligada",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Comentário, quando usado no início de uma linha",
"Compression": "Compressão",
"Connection Error": "Erro de ligação",
"Connection Type": "Tipo de ligação",
"Copied from elsewhere": "Copiado doutro sítio",
"Copied from original": "Copiado do original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 os seguintes contribuidores:",
@@ -46,7 +47,7 @@
"Device {%device%} ({%address%}) wants to connect. Add new device?": "O dispositivo {{device}} ({{address}}) quer conectar-se. Adiciono este novo dispositivo?",
"Devices": "Dispositivos",
"Disconnected": "Desconectado",
"Discovery": "Detecção",
"Discovery": "Pesquisa",
"Documentation": "Documentação",
"Download Rate": "Velocidade de recepção",
"Downloaded": "Recebido",
@@ -74,15 +75,16 @@
"Folder Label": "Etiqueta da pasta",
"Folder Master": "Pasta mestre",
"Folder Path": "Caminho da pasta",
"Folder Type": "Tipo de pasta",
"Folders": "Pastas",
"GUI": "GUI",
"GUI Authentication Password": "Senha da autenticação na interface gráfica",
"GUI Authentication User": "Utilizador da autenticação na interface gráfica",
"GUI Listen Addresses": "Endereço de escuta da interface gráfica",
"Generate": "Gerar",
"Global Discovery": "Detecção global",
"Global Discovery Server": "Servidor de detecção global",
"Global Discovery Servers": "Servidores de detecção global",
"Global Discovery": "Pesquisa global",
"Global Discovery Server": "Servidor de pesquisa global",
"Global Discovery Servers": "Servidores de pesquisa global",
"Global State": "Estado global",
"Help": "Ajuda",
"Home page": "Página do projecto",
@@ -98,10 +100,12 @@
"Last File Received": "Último ficheiro recebido",
"Last seen": "Última vez que foi verificado",
"Later": "Mais tarde",
"Local Discovery": "Detecção local",
"Listeners": "Auscultadores",
"Local Discovery": "Pesquisa local",
"Local State": "Estado local",
"Local State (Total)": "Estado local (total)",
"Major Upgrade": "Actualização importante",
"Master": "Mestre",
"Maximum Age": "Idade máxima",
"Metadata Only": "Metadados apenas",
"Minimum Free Disk Space": "Espaço livre mínimo no disco",
@@ -113,6 +117,7 @@
"Newest First": "Primeiro os mais recentes",
"No": "Não",
"No File Versioning": "Nenhuma",
"Normal": "Normal",
"Notice": "Avisos",
"OK": "OK",
"Off": "Desligada",

View File

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

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Kommentar, vid början av en rad.",
"Compression": "Komprimering",
"Connection Error": "Anslutningsproblem",
"Connection Type": "Anslutningstyp",
"Copied from elsewhere": "Kopierat utifrån",
"Copied from original": "Oförändrat",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragande:",
@@ -74,6 +75,7 @@
"Folder Label": "Katalog etikett",
"Folder Master": "Huvudlagring",
"Folder Path": "Sökväg",
"Folder Type": "Katalogtyp",
"Folders": "Kataloger",
"GUI": "GUI",
"GUI Authentication Password": "GUI-lösenord",
@@ -98,10 +100,12 @@
"Last File Received": "Senast Mottagna Fil",
"Last seen": "Senast online",
"Later": "Senare",
"Listeners": "Lyssnare",
"Local Discovery": "Lokal uppslagning",
"Local State": "Lokal status",
"Local State (Total)": "Lokal status (Total)",
"Major Upgrade": "Stor uppgradering",
"Master": "Huvud",
"Maximum Age": "Högsta åldersgräns",
"Metadata Only": "Endast metadata",
"Minimum Free Disk Space": "Minimum ledigt diskutrymme",
@@ -113,6 +117,7 @@
"Newest First": "Nyast först",
"No": "Nej",
"No File Versioning": "Ingen versionshantering",
"Normal": "Normal",
"Notice": "Observera",
"OK": "OK",
"Off": "Av",
@@ -141,7 +146,7 @@
"Release Notes": "versionsnyheter",
"Remote Devices": "Fjärrenheter",
"Remove": "Ta bort",
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för mappen. Måste vara densamma på alla kluster enheter.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för katalogen. Måste vara densamma på alla kluster enheter.",
"Rescan": "Uppdatera",
"Rescan All": "Uppdatera alla",
"Rescan Interval": "Uppdateringsintervall",
@@ -151,7 +156,7 @@
"Resume": "Återuppta",
"Reused": "Återanvänt",
"Save": "Spara",
"Scan Time Remaining": "Skanna Återstående Tid",
"Scan Time Remaining": "Granska återstående tid",
"Scanning": "Uppdaterar",
"Select the devices to share this folder with.": "Ange enheterna att dela den här katalogen med.",
"Select the folders to share with this device.": "Välj kataloger att dela med den här enheten.",
@@ -239,6 +244,6 @@
"full documentation": "fullständig dokumentation",
"items": "poster",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vill dela katalogen \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela katalogen \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Satır başında kullanıldığında açıklama özelliği taşır",
"Compression": "Sıkıştırma",
"Connection Error": "Bağlantı hatası",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Başka bir yerden kopyalanmış",
"Copied from original": "Aslından kopyalanmış",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
@@ -74,6 +75,7 @@
"Folder Label": "Folder Label",
"Folder Master": "Ana Klasör",
"Folder Path": "Klasör Yolu",
"Folder Type": "Folder Type",
"Folders": "Klasörler",
"GUI": "GUI / Kullanıcı Grafik Arayüzü",
"GUI Authentication Password": "GUI Kimlik Doğrulaması için Kullanıcı Parolası",
@@ -98,10 +100,12 @@
"Last File Received": "Alınan Son Dosya",
"Last seen": "Son Görülen",
"Later": "Sonra",
"Listeners": "Listeners",
"Local Discovery": "Yerel Discovery",
"Local State": "Yerel Durum",
"Local State (Total)": "Yerel Durum (Toplamı)",
"Major Upgrade": "Birincil Yükseltme",
"Master": "Master",
"Maximum Age": "Azami Süre",
"Metadata Only": "Sadece Üstveri",
"Minimum Free Disk Space": "En Az Boş Disk Alanı",
@@ -113,6 +117,7 @@
"Newest First": "En yeni olan önce",
"No": "Hayır",
"No File Versioning": "Dosya Sürümlendirmesi Yok",
"Normal": "Normal",
"Notice": "Uyarı",
"OK": "Tamam",
"Off": "Kapalı",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Коментар, якщо використовується на початку рядка",
"Compression": "Стиснення",
"Connection Error": "Помилка з’єднання",
"Connection Type": "Тип з*єднання",
"Copied from elsewhere": "Скопійовано з іншого місця",
"Copied from original": "Скопійовано з оригіналу",
"Copyright © 2014-2016 the following Contributors:": "© 2014-2016 Всі права застережено, вклад внесли:",
@@ -46,7 +47,7 @@
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Пристрій {{device}} ({{address}}) намагається під’єднатися. Додати новий пристрій?",
"Devices": "Пристрої",
"Disconnected": "З’єднання відсутнє",
"Discovery": "Сервери обходу NAT",
"Discovery": "Сервери координації NAT",
"Documentation": "Документація",
"Download Rate": "Швидкість завантаження",
"Downloaded": "Завантажено",
@@ -74,6 +75,7 @@
"Folder Label": "Мітка директорії",
"Folder Master": "Вважати за оригінал",
"Folder Path": "Шлях до директорії",
"Folder Type": "Тип директорії",
"Folders": "Директорії",
"GUI": "Графічний інтерфейс",
"GUI Authentication Password": "Пароль для доступу до панелі управління",
@@ -82,7 +84,7 @@
"Generate": "Згенерувати",
"Global Discovery": "Глобальне виявлення (internet)",
"Global Discovery Server": "Сервер для глобального виявлення",
"Global Discovery Servers": "Сервери глобального виявлення \n(координації обходу NAT)",
"Global Discovery Servers": "Сервери глобального виявлення \n(координації NAT)",
"Global State": "Глобальний статус",
"Help": "Допомога",
"Home page": "Домашня сторінка",
@@ -98,10 +100,12 @@
"Last File Received": "Останній завантажений файл",
"Last seen": "З’являвся останній раз",
"Later": "Пізніше",
"Listeners": "Приймачі (TCP & Relay)",
"Local Discovery": "Локальне виявлення (LAN)",
"Local State": "Локальний статус",
"Local State (Total)": "Локальний статус (загалом)",
"Major Upgrade": "Мажорне оновлення",
"Master": "Головний",
"Maximum Age": "Максимальний вік",
"Metadata Only": "Тільки метадані",
"Minimum Free Disk Space": "Мінімальний вільний простір на диску",
@@ -113,6 +117,7 @@
"Newest First": "Спершу новіші",
"No": "Ні",
"No File Versioning": "Версіонування вимкнено",
"Normal": "Нормальний",
"Notice": "Повідомлення",
"OK": "Гаразд",
"Off": "Вимкнути",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "Bình luận, khi dùng trước đầu dòng",
"Compression": "Nén",
"Connection Error": "Lỗi kết nối",
"Connection Type": "Connection Type",
"Copied from elsewhere": "Đã sao chép từ nơi khác",
"Copied from original": "Đã sao chép từ nguồn",
"Copyright © 2014-2016 the following Contributors:": "Bản quyền © 2014-2016 thuộc về các nhà cộng tác sau:",
@@ -74,6 +75,7 @@
"Folder Label": "Nhãn thư mục",
"Folder Master": "Thư mục Chủ",
"Folder Path": "Đ.dẫn đến th.mục",
"Folder Type": "Folder Type",
"Folders": "Các th.mục",
"GUI": "GUI",
"GUI Authentication Password": "Mật khẩu xác minh GUI",
@@ -98,10 +100,12 @@
"Last File Received": "T.tin nhận được gần đây",
"Last seen": "Thấy lần cuối",
"Later": "Để sau",
"Listeners": "Listeners",
"Local Discovery": "Dò tìm cục bộ",
"Local State": "Tr.thái cục bộ",
"Local State (Total)": "Tr.thái cục bộ (Tổng)",
"Major Upgrade": "Bản n.cấp q.trọng",
"Master": "Master",
"Maximum Age": "Thời hạn tối đa",
"Metadata Only": "Chỉ siêu dữ liệu",
"Minimum Free Disk Space": "Dung lượng đĩa trống tối thiểu",
@@ -113,6 +117,7 @@
"Newest First": "Mới nhất đầu tiên",
"No": "Không",
"No File Versioning": "Không dùng",
"Normal": "Normal",
"Notice": "Chú ý",
"OK": "OK",
"Off": "Tắt",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "注释,在行首使用",
"Compression": "压缩",
"Connection Error": "连接出错",
"Connection Type": "连接类型",
"Copied from elsewhere": "从其他设备复制",
"Copied from original": "从源复制",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 以下贡献者:",
@@ -74,6 +75,7 @@
"Folder Label": "文件夹标签",
"Folder Master": "主文件夹",
"Folder Path": "文件夹路径",
"Folder Type": "文件夹类型",
"Folders": "文件夹",
"GUI": "图形用户界面",
"GUI Authentication Password": "图形管理界面密码",
@@ -98,10 +100,12 @@
"Last File Received": "最后接收的文件",
"Last seen": "最后可见",
"Later": "稍后",
"Listeners": "侦听程序",
"Local Discovery": "在局域网上寻找设备",
"Local State": "本地状态",
"Local State (Total)": "本地状态汇总",
"Major Upgrade": "重大更新",
"Master": "主要",
"Maximum Age": "历史版本最长保留时间",
"Metadata Only": "仅元数据",
"Minimum Free Disk Space": "最低可用磁盘空间",
@@ -113,6 +117,7 @@
"Newest First": "新文件优先",
"No": "否",
"No File Versioning": "不启用版本控制",
"Normal": "正常",
"Notice": "提示",
"OK": "确定",
"Off": "关闭",

View File

@@ -32,6 +32,7 @@
"Comment, when used at the start of a line": "註解,當輸入在一行的開頭時",
"Compression": "壓縮",
"Connection Error": "連線錯誤",
"Connection Type": "Connection Type",
"Copied from elsewhere": "從別處複製",
"Copied from original": "從原處複製",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 下列貢獻者:",
@@ -74,6 +75,7 @@
"Folder Label": "資料夾標籤",
"Folder Master": "主資料夾",
"Folder Path": "資料夾路徑",
"Folder Type": "Folder Type",
"Folders": "資料夾",
"GUI": "GUI",
"GUI Authentication Password": "GUI 認證密碼",
@@ -98,10 +100,12 @@
"Last File Received": "最後接收的檔案",
"Last seen": "最後發現時間",
"Later": "稍後",
"Listeners": "Listeners",
"Local Discovery": "本機探索",
"Local State": "本機狀態",
"Local State (Total)": "本機狀態 (總結)",
"Major Upgrade": "重大更新",
"Master": "Master",
"Maximum Age": "最長保留時間",
"Metadata Only": "僅中繼資料",
"Minimum Free Disk Space": "最少閒置磁碟空間",
@@ -113,6 +117,7 @@
"Newest First": "最新的優先",
"No": "否",
"No File Versioning": "無檔案版本控制",
"Normal": "Normal",
"Notice": "注意",
"OK": "確定",
"Off": "關閉",

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

@@ -50,7 +50,7 @@
</li>
<li class="dropdown" language-select></li>
<li>
<a href="https://docs.syncthing.net/intro/gui.html" target="_blank">
<a class="navbar-link" href="https://docs.syncthing.net/intro/gui.html" target="_blank">
<span class="fa fa-question-circle"></span>&nbsp;<span class="hidden-xs" translate>Help</span>
</a>
</li>
@@ -227,15 +227,13 @@
<h3 translate>Folders</h3>
<div class="panel-group" id="folders">
<div class="panel panel-default" ng-repeat="folder in folderList()">
<div class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer">
<a class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer; display: block;">
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id)}}%"></div>
<div class="panel-progress" ng-show="folderStatus(folder) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id)}}%"></div>
<h4 class="panel-title">
<span class="fa hidden-xs fa-fw" ng-class="[folder.type == 'readonly' ? 'fa-lock' : 'fa-folder']"></span>
<a href="#folder-{{$index}}">
<span ng-show="folder.label.length == 0">{{folder.id}}</span>
<span tooltip data-original-title="{{folder.id}}" ng-show="folder.label.length != 0">{{folder.label}}</span>
</a>
<span ng-show="folder.label.length == 0">{{folder.id}}</span>
<span tooltip data-original-title="{{folder.id}}" ng-show="folder.label.length != 0">{{folder.label}}</span>
<span class="pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
<span ng-switch-when="unknown"><span class="hidden-xs" translate>Unknown</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="unshared"><span class="hidden-xs" translate>Unshared</span><span class="visible-xs">&#9724;</span></span>
@@ -255,7 +253,7 @@
<span ng-switch-when="outofsync"><span class="hidden-xs" translate>Out of Sync</span><span class="visible-xs">&#9724;</span></span>
</span>
</h4>
</div>
</a>
<div id="folder-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<table class="table table-condensed table-striped">
@@ -366,7 +364,7 @@
</table>
</div>
<div class="panel-footer">
<button type="button" class="btn btn-sm btn-danger pull-left" ng-click="override(folder.id)" ng-if="folderStatus(folder) == 'outofsync' && folder.type = 'readonly'">
<button type="button" class="btn btn-sm btn-danger pull-left" ng-click="override(folder.id)" ng-if="folderStatus(folder) == 'outofsync' && folder.type == 'readonly'">
<span class="fa fa-arrow-circle-up"></span>&nbsp;<span translate>Override Changes</span>
</button>
<span class="pull-right">
@@ -401,11 +399,11 @@
<div class="col-md-6">
<h3 translate>This Device</h3>
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
<div class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer">
<a class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer; display: block;">
<h4 class="panel-title">
<identicon data-value="deviceCfg.deviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
</h4>
</div>
</a>
<div id="device-this" class="panel-collapse collapse in">
<div class="panel-body">
<table class="table table-condensed table-striped">
@@ -476,10 +474,10 @@
<h3 translate>Remote Devices</h3>
<div class="panel-group" id="devices">
<div class="panel panel-default" ng-repeat="deviceCfg in otherDevices()">
<div class="panel-heading" data-toggle="collapse" data-parent="#devices" href="#device-{{$index}}" style="cursor: pointer">
<a class="panel-heading" data-toggle="collapse" data-parent="#devices" href="#device-{{$index}}" style="cursor: pointer; display: block;">
<div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.deviceID]._total | number:0}}%"></div>
<h4 class="panel-title">
<identicon data-value="deviceCfg.deviceID"></identicon>&emsp;<a href="#device-{{$index}}">{{deviceName(deviceCfg)}}</a>
<identicon data-value="deviceCfg.deviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
<span ng-switch="deviceStatus(deviceCfg)" class="pull-right text-{{deviceClass(deviceCfg)}}">
<span ng-switch-when="insync"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="syncing">
@@ -490,7 +488,7 @@
<span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">&#9724;</span></span>
</span>
</h4>
</div>
</a>
<div id="device-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<table class="table table-condensed table-striped">
@@ -664,6 +662,7 @@
<script src="assets/lang/valid-langs.js"></script>
<script src="assets/lang/prettyprint.js"></script>
<script src="meta.js"></script>
<script src="syncthing/app.js"></script>
<!-- / gui application code -->

View File

@@ -23,31 +23,9 @@ var syncthing = angular.module('syncthing', [
var urlbase = 'rest';
syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvider) {
$httpProvider.interceptors.push(function xHeadersResponseInterceptor() {
var deviceId = null;
return {
response: function onResponse(response) {
var headers = response.headers();
// angular template cache sends no headers
if(Object.keys(headers).length === 0) {
return response;
}
if (!deviceId) {
deviceId = headers['x-syncthing-id'];
if (deviceId) {
var deviceIdShort = deviceId.substring(0, 5);
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + deviceIdShort;
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIdShort;
}
}
return response;
}
};
});
var deviceIDShort = metadata.deviceID.substr(0, 5);
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + deviceIDShort;
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIDShort;
// language and localisation
@@ -59,7 +37,6 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
LocaleServiceProvider.setAvailableLocales(validLangs);
LocaleServiceProvider.setDefaultLocale('en');
});
// @TODO: extract global level functions into separate service(s)
@@ -112,19 +89,6 @@ function decimals(val, num) {
return decs;
}
function randomString(len) {
var chars = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-';
return randomStringFromCharset(len, chars);
}
function randomStringFromCharset(len, charset) {
var result = '';
for (var i = 0; i < len; i++) {
result += charset[Math.round(Math.random() * (charset.length - 1))];
}
return result;
}
function isEmptyObject(obj) {
var name;
for (name in obj) {

View File

@@ -11,7 +11,7 @@
<p translate>Copyright &copy; 2014-2016 the following Contributors:</p>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Ben Schulz, Caleb Callaway, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Andrew Dunham, Antony Male, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Harte, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Lord Landon Agahnim, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Ben Schulz, Caleb Callaway, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Antony Male, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Harte, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Lord Landon Agahnim, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
</div>
</div>
<hr/>

View File

@@ -1196,7 +1196,6 @@ angular.module('syncthing.core')
$scope.addFolder = function () {
$scope.currentFolder = {
selectedDevices: {},
id: $scope.createRandomFolderId(),
type: "readwrite",
rescanIntervalS: 60,
minDiskFreePct: 1,
@@ -1213,7 +1212,10 @@ angular.module('syncthing.core')
};
$scope.editingExisting = false;
$scope.folderEditor.$setPristine();
$('#editFolder').modal();
$http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
$scope.currentFolder.id = data.random.substr(0, 5) + '-' + data.random.substr(5, 5);
$('#editFolder').modal();
});
};
$scope.addFolderAndShare = function (folder, folderLabel, device) {
@@ -1406,7 +1408,9 @@ angular.module('syncthing.core')
};
$scope.setAPIKey = function (cfg) {
cfg.apiKey = randomString(32);
$http.get(urlbase + '/svc/random/string?length=32').success(function (data) {
cfg.apiKey = data.random;
});
};
$scope.showURPreview = function () {
@@ -1544,11 +1548,6 @@ angular.module('syncthing.core')
return 'text';
};
$scope.createRandomFolderId = function(){
var charset = '2345679abcdefghijkmnopqrstuvwxyzACDEFGHJKLMNPQRSTUVWXYZ';
return randomStringFromCharset(5, charset) + "-" + randomStringFromCharset(5, charset);
};
$scope.themeName = function (theme) {
return theme.replace('-', ' ').replace(/(?:^|\s)\S/g, function (a) {
return a.toUpperCase();

View File

@@ -56,11 +56,24 @@
</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="row">
<div class="col-md-6">
<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>
<div class="col-md-6">
<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>
</div>

View File

@@ -12,6 +12,25 @@
* https://github.com/angular-ui/bootstrap/blob/master/src/pagination/pagination.js
*
* Copyright 2014 Michael Bromley <michael@michaelbromley.co.uk>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
(function() {

View File

@@ -2,6 +2,25 @@
* angular-translate - v2.11.0 - 2016-03-20
*
* Copyright (c) 2016 The angular-translate team, Pascal Precht; Licensed MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {

View File

@@ -2,6 +2,25 @@
* angular-translate - v2.9.0 - 2016-01-24
*
* Copyright (c) 2016 The angular-translate team, Pascal Precht; Licensed MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {

View File

@@ -2,6 +2,25 @@
* @license AngularJS v1.2.9
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
(function(window, document, undefined) {'use strict';
@@ -1747,10 +1766,10 @@ function setupModuleLoader(window) {
/* global
angularModule: true,
version: true,
$LocaleProvider,
$CompileProvider,
htmlAnchorDirective,
inputDirective,
inputDirective,
@@ -3414,11 +3433,11 @@ function annotate(fn) {
* var Ping = function() {
* this.$http = $http;
* };
*
*
* Ping.prototype.send = function() {
* return this.$http.get('/ping');
* };
*
* };
*
* return Ping;
* }]);
* </pre>
@@ -3749,7 +3768,7 @@ function createInjector(modulesToLoad) {
*
* It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor.
* This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`.
*
*
* @example
<example>
<file name="index.html">
@@ -3764,7 +3783,7 @@ function createInjector(modulesToLoad) {
// set the location.hash to the id of
// the element you wish to scroll to.
$location.hash('bottom');
// call $anchorScroll()
$anchorScroll();
}
@@ -3852,7 +3871,7 @@ var $animateMinErr = minErr('$animate');
*/
var $AnimateProvider = ['$provide', function($provide) {
this.$$selectors = {};
@@ -3993,7 +4012,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* @description Moves the position of the provided element within the DOM to be placed
* either after the `after` element or inside of the `parent` element. Once complete, the
* done() callback will be fired (if provided).
*
*
* @param {jQuery/jqLite element} element the element which will be moved around within the
* DOM
* @param {jQuery/jqLite element} parent the parent element where the element will be
@@ -4458,9 +4477,9 @@ function $BrowserProvider(){
*
* @description
* Factory that constructs cache objects and gives access to them.
*
*
* <pre>
*
*
* var cache = $cacheFactory('cacheId');
* expect($cacheFactory.get('cacheId')).toBe(cache);
* expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined();
@@ -4469,8 +4488,8 @@ function $BrowserProvider(){
* cache.put("another key", "another value");
*
* // We've specified no options on creation
* expect(cache.info()).toEqual({id: 'cacheId', size: 2});
*
* expect(cache.info()).toEqual({id: 'cacheId', size: 2});
*
* </pre>
*
*
@@ -4653,7 +4672,7 @@ function $CacheFactoryProvider() {
* The first time a template is used, it is loaded in the template cache for quick retrieval. You
* can load templates directly into the cache in a `script` tag, or by consuming the
* `$templateCache` service directly.
*
*
* Adding via the `script` tag:
* <pre>
* <html ng-app>
@@ -4665,29 +4684,29 @@ function $CacheFactoryProvider() {
* ...
* </html>
* </pre>
*
*
* **Note:** the `script` tag containing the template does not need to be included in the `head` of
* the document, but it must be below the `ng-app` definition.
*
*
* Adding via the $templateCache service:
*
*
* <pre>
* var myApp = angular.module('myApp', []);
* myApp.run(function($templateCache) {
* $templateCache.put('templateId.html', 'This is the content of the template');
* });
* </pre>
*
*
* To retrieve the template later, simply use it in your HTML:
* <pre>
* <div ng-include=" 'templateId.html' "></div>
* </pre>
*
*
* or get it via Javascript:
* <pre>
* $templateCache.get('templateId.html')
* </pre>
*
*
* See {@link ng.$cacheFactory $cacheFactory}.
*
*/
@@ -6809,12 +6828,12 @@ function $DocumentProvider(){
* Any uncaught exception in angular expressions is delegated to this service.
* The default implementation simply delegates to `$log.error` which logs it into
* the browser console.
*
*
* In unit tests, if `angular-mocks.js` is loaded, this service is overridden by
* {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing.
*
* ## Example:
*
*
* <pre>
* angular.module('exceptionOverride', []).factory('$exceptionHandler', function () {
* return function (exception, cause) {
@@ -6823,7 +6842,7 @@ function $DocumentProvider(){
* };
* });
* </pre>
*
*
* This example will override the normal action of `$exceptionHandler`, to make angular
* exceptions fail hard when they happen, instead of just logging to the console.
*
@@ -8322,7 +8341,7 @@ function $IntervalProvider() {
* In tests you can use {@link ngMock.$interval#methods_flush `$interval.flush(millis)`} to
* move forward by `millis` milliseconds and trigger any functions scheduled to run in that
* time.
*
*
* <div class="alert alert-warning">
* **Note**: Intervals created by this service must be explicitly destroyed when you are finished
* with them. In particular they are not automatically destroyed when a controller's scope or a
@@ -8435,7 +8454,7 @@ function $IntervalProvider() {
promise = deferred.promise,
iteration = 0,
skipApply = (isDefined(invokeApply) && !invokeApply);
count = isDefined(count) ? count : 0,
promise.then(null, null, fn);
@@ -9270,7 +9289,7 @@ function $LocationProvider(){
* @description
* Simple service for logging. Default implementation safely writes the message
* into the browser's console (if present).
*
*
* The main purpose of this service is to simplify debugging and troubleshooting.
*
* The default is to log `debug` messages. You can use
@@ -9307,7 +9326,7 @@ function $LocationProvider(){
function $LogProvider(){
var debug = true,
self = this;
/**
* @ngdoc property
* @name ng.$logProvider#debugEnabled
@@ -9324,7 +9343,7 @@ function $LogProvider(){
return debug;
}
};
this.$get = ['$window', function($window){
return {
/**
@@ -9366,12 +9385,12 @@ function $LogProvider(){
* Write an error message
*/
error: consoleLog('error'),
/**
* @ngdoc method
* @name ng.$log#debug
* @methodOf ng.$log
*
*
* @description
* Write a debug message
*/
@@ -12823,7 +12842,7 @@ function $SceDelegateProvider() {
* allowing only the files in a specific directory to do this. Ensuring that the internal API
* exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task.
*
* In the case of AngularJS' SCE service, one uses {@link ng.$sce#methods_trustAs $sce.trustAs}
* In the case of AngularJS' SCE service, one uses {@link ng.$sce#methods_trustAs $sce.trustAs}
* (and shorthand methods such as {@link ng.$sce#methods_trustAsHtml $sce.trustAsHtml}, etc.) to
* obtain values that will be accepted by SCE / privileged contexts.
*
@@ -13572,7 +13591,7 @@ function $TimeoutProvider() {
* will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block.
* @returns {Promise} Promise that will be resolved when the timeout is reached. The value this
* promise will be resolved with is the return value of the `fn` function.
*
*
*/
function timeout(fn, delay, invokeApply) {
var deferred = $q.defer(),
@@ -13806,7 +13825,7 @@ function $WindowProvider(){
*
* The filter function is registered with the `$injector` under the filter name suffix with
* `Filter`.
*
*
* <pre>
* it('should be the same instance', inject(
* function($filterProvider) {
@@ -13882,7 +13901,7 @@ function $FilterProvider($provide) {
}];
////////////////////////////////////////
/* global
currencyFilter: false,
dateFilter: false,
@@ -14590,9 +14609,9 @@ var uppercaseFilter = valueFn(uppercase);
* the value and sign (positive or negative) of `limit`.
*
* @param {Array|string} input Source array or string to be limited.
* @param {string|number} limit The length of the returned array or string. If the `limit` number
* @param {string|number} limit The length of the returned array or string. If the `limit` number
* is positive, `limit` number of items from the beginning of the source array/string are copied.
* If the number is negative, `limit` number of items from the end of the source array/string
* If the number is negative, `limit` number of items from the end of the source array/string
* are copied. The `limit` will be trimmed if it exceeds `array.length`
* @returns {Array|string} A new sub-array or substring of length `limit` or less if input array
* had less than `limit` elements.
@@ -14642,7 +14661,7 @@ var uppercaseFilter = valueFn(uppercase);
function limitToFilter(){
return function(input, limit) {
if (!isArray(input) && !isString(input)) return input;
limit = int(limit);
if (isString(input)) {
@@ -15044,7 +15063,7 @@ var htmlAnchorDirective = valueFn({
</doc:example>
*
* @element INPUT
* @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
* @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
* then special attribute "disabled" will be set on the element
*/
@@ -15079,7 +15098,7 @@ var htmlAnchorDirective = valueFn({
</doc:example>
*
* @element INPUT
* @param {expression} ngChecked If the {@link guide/expression expression} is truthy,
* @param {expression} ngChecked If the {@link guide/expression expression} is truthy,
* then special attribute "checked" will be set on the element
*/
@@ -15114,7 +15133,7 @@ var htmlAnchorDirective = valueFn({
</doc:example>
*
* @element INPUT
* @param {expression} ngReadonly If the {@link guide/expression expression} is truthy,
* @param {expression} ngReadonly If the {@link guide/expression expression} is truthy,
* then special attribute "readonly" will be set on the element
*/
@@ -15133,7 +15152,7 @@ var htmlAnchorDirective = valueFn({
* The `ngSelected` directive solves this problem for the `selected` atttribute.
* This complementary directive is not removed by the browser and so provides
* a permanent reliable place to store the binding information.
*
*
* @example
<doc:example>
<doc:source>
@@ -15153,7 +15172,7 @@ var htmlAnchorDirective = valueFn({
</doc:example>
*
* @element OPTION
* @param {expression} ngSelected If the {@link guide/expression expression} is truthy,
* @param {expression} ngSelected If the {@link guide/expression expression} is truthy,
* then special attribute "selected" will be set on the element
*/
@@ -15189,7 +15208,7 @@ var htmlAnchorDirective = valueFn({
</doc:example>
*
* @element DETAILS
* @param {expression} ngOpen If the {@link guide/expression expression} is truthy,
* @param {expression} ngOpen If the {@link guide/expression expression} is truthy,
* then special attribute "open" will be set on the element
*/
@@ -15275,7 +15294,7 @@ var nullFormCtrl = {
* - `pattern`
* - `required`
* - `url`
*
*
* @description
* `FormController` keeps track of all its controls and nested forms as well as state of them,
* such as being valid/invalid or dirty/pristine.
@@ -17184,14 +17203,14 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) {
*
* @example
Try it here: enter text in text box and watch the greeting change.
<example module="ngBindHtmlExample" deps="angular-sanitize.js">
<file name="index.html">
<div ng-controller="ngBindHtmlCtrl">
<p ng-bind-html="myHTML"></p>
</div>
</file>
<file name="script.js">
angular.module('ngBindHtmlExample', ['ngSanitize'])
@@ -20349,7 +20368,7 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
// We now build up the list of options we need (we merge later)
for (index = 0; length = keys.length, index < length; index++) {
key = index;
if (keyName) {
key = keys[index];
@@ -20557,4 +20576,4 @@ var styleDirective = valueFn({
})(window, document);
!angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}</style>');
!angular.$$csp() && angular.element(document).find('head').prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide{display:none !important;}ng\\:form{display:block;}</style>');

View File

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

View File

@@ -11,26 +11,29 @@ import (
"encoding/json"
"encoding/xml"
"io"
"io/ioutil"
"net/url"
"os"
"path"
"sort"
"strings"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/util"
)
const (
OldestHandledVersion = 10
CurrentVersion = 13
CurrentVersion = 15
MaxRescanIntervalS = 365 * 24 * 60 * 60
)
var (
// DefaultListenAddresses should be substituted when the configuration
// contains <listenAddress>default</listenAddress>. This is
// done by the "consumer" of the configuration, as we don't want these
// saved to the config.
// contains <listenAddress>default</listenAddress>. This is done by the
// "consumer" of the configuration as we don't want these saved to the
// config.
DefaultListenAddresses = []string{
"tcp://0.0.0.0:22000",
"dynamic+https://relays.syncthing.net/endpoint",
@@ -92,7 +95,12 @@ func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
util.SetDefaults(&cfg.Options)
util.SetDefaults(&cfg.GUI)
err := json.NewDecoder(r).Decode(&cfg)
bs, err := ioutil.ReadAll(r)
if err != nil {
return cfg, err
}
err = json.Unmarshal(bs, &cfg)
cfg.OriginalVersion = cfg.Version
cfg.prepare(myID)
@@ -192,6 +200,12 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
if cfg.Version == 12 {
convertV12V13(cfg)
}
if cfg.Version == 13 {
convertV13V14(cfg)
}
if cfg.Version == 14 {
convertV14V15(cfg)
}
// Build a list of available devices
existingDevices := make(map[protocol.DeviceID]bool)
@@ -241,43 +255,85 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
}
if cfg.GUI.APIKey == "" {
cfg.GUI.APIKey = util.RandomString(32)
cfg.GUI.APIKey = rand.String(32)
}
}
func convertV12V13(cfg *Configuration) {
func convertV14V15(cfg *Configuration) {
// Undo v0.13.0 broken migration
for i, addr := range cfg.Options.GlobalAnnServers {
switch addr {
case "default-v4v2/":
cfg.Options.GlobalAnnServers[i] = "default-v4"
case "default-v6v2/":
cfg.Options.GlobalAnnServers[i] = "default-v6"
}
}
cfg.Version = 15
}
func convertV13V14(cfg *Configuration) {
// Not using the ignore cache is the new default. Disable it on existing
// configurations.
cfg.Options.CacheIgnoredFiles = false
cfg.Options.NATEnabled = cfg.Options.DeprecatedUPnPEnabled
cfg.Options.NATLeaseM = cfg.Options.DeprecatedUPnPLeaseM
cfg.Options.NATRenewalM = cfg.Options.DeprecatedUPnPRenewalM
cfg.Options.NATTimeoutS = cfg.Options.DeprecatedUPnPTimeoutS
if cfg.Options.DeprecatedRelaysEnabled {
cfg.Options.ListenAddresses = append(cfg.Options.ListenAddresses, cfg.Options.DeprecatedRelayServers...)
// Replace our two fairly long addresses with 'default' if both exist.
var newAddresses []string
for _, addr := range cfg.Options.ListenAddresses {
if addr != "tcp://0.0.0.0:22000" && addr != "dynamic+https://relays.syncthing.net/endpoint" {
newAddresses = append(newAddresses, addr)
}
}
if len(newAddresses)+2 == len(cfg.Options.ListenAddresses) {
cfg.Options.ListenAddresses = append([]string{"default"}, newAddresses...)
// Migrate UPnP -> NAT options
cfg.Options.NATEnabled = cfg.Options.DeprecatedUPnPEnabled
cfg.Options.DeprecatedUPnPEnabled = false
cfg.Options.NATLeaseM = cfg.Options.DeprecatedUPnPLeaseM
cfg.Options.DeprecatedUPnPLeaseM = 0
cfg.Options.NATRenewalM = cfg.Options.DeprecatedUPnPRenewalM
cfg.Options.DeprecatedUPnPRenewalM = 0
cfg.Options.NATTimeoutS = cfg.Options.DeprecatedUPnPTimeoutS
cfg.Options.DeprecatedUPnPTimeoutS = 0
// Replace the default listen address "tcp://0.0.0.0:22000" with the
// string "default", but only if we also have the default relay pool
// among the relay servers as this is implied by the new "default"
// entry.
hasDefault := false
for _, raddr := range cfg.Options.DeprecatedRelayServers {
if raddr == "dynamic+https://relays.syncthing.net/endpoint" {
for i, addr := range cfg.Options.ListenAddresses {
if addr == "tcp://0.0.0.0:22000" {
cfg.Options.ListenAddresses[i] = "default"
hasDefault = true
break
}
}
break
}
}
cfg.Options.DeprecatedRelaysEnabled = false
// Copy relay addresses into listen addresses.
for _, addr := range cfg.Options.DeprecatedRelayServers {
if hasDefault && addr == "dynamic+https://relays.syncthing.net/endpoint" {
// Skip the default relay address if we already have the
// "default" entry in the list.
continue
}
if addr == "" {
continue
}
cfg.Options.ListenAddresses = append(cfg.Options.ListenAddresses, addr)
}
cfg.Options.DeprecatedRelayServers = nil
// For consistency
sort.Strings(cfg.Options.ListenAddresses)
var newAddrs []string
for _, addr := range cfg.Options.GlobalAnnServers {
if addr != "default" {
uri, err := url.Parse(addr)
if err != nil {
panic(err)
}
uri.Path += "v2/"
uri, err := url.Parse(addr)
if err != nil {
// That's odd. Skip the broken address.
continue
}
if uri.Scheme == "https" {
uri.Path = path.Join(uri.Path, "v2") + "/"
addr = uri.String()
}
@@ -285,8 +341,6 @@ func convertV12V13(cfg *Configuration) {
}
cfg.Options.GlobalAnnServers = newAddrs
cfg.Version = 13
for i, fcfg := range cfg.Folders {
if fcfg.DeprecatedReadOnly {
cfg.Folders[i].Type = FolderTypeReadOnly
@@ -295,6 +349,20 @@ func convertV12V13(cfg *Configuration) {
}
cfg.Folders[i].DeprecatedReadOnly = false
}
// v0.13-beta already had config version 13 but did not get the new URL
if cfg.Options.ReleasesURL == "https://api.github.com/repos/syncthing/syncthing/releases?per_page=30" {
cfg.Options.ReleasesURL = "https://upgrades.syncthing.net/meta.json"
}
cfg.Version = 14
}
func convertV12V13(cfg *Configuration) {
if cfg.Options.ReleasesURL == "https://api.github.com/repos/syncthing/syncthing/releases?per_page=30" {
cfg.Options.ReleasesURL = "https://upgrades.syncthing.net/meta.json"
}
cfg.Version = 13
}
func convertV11V12(cfg *Configuration) {

View File

@@ -12,7 +12,9 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"testing"
@@ -40,6 +42,7 @@ func TestDefaultValues(t *testing.T) {
MaxSendKbps: 0,
MaxRecvKbps: 0,
ReconnectIntervalS: 60,
RelaysEnabled: true,
RelayReconnectIntervalM: 10,
StartBrowser: true,
NATEnabled: true,
@@ -57,9 +60,9 @@ func TestDefaultValues(t *testing.T) {
URURL: "https://data.syncthing.net/newdata",
URInitialDelayS: 1800,
URPostInsecurely: false,
ReleasesURL: "https://api.github.com/repos/syncthing/syncthing/releases?per_page=30",
ReleasesURL: "https://upgrades.syncthing.net/meta.json",
AlwaysLocalNets: []string{},
OverwriteNames: false,
OverwriteRemoteDevNames: false,
TempIndexMinBlocks: 10,
}
@@ -169,6 +172,7 @@ func TestOverriddenValues(t *testing.T) {
MaxSendKbps: 1234,
MaxRecvKbps: 2341,
ReconnectIntervalS: 6000,
RelaysEnabled: false,
RelayReconnectIntervalM: 20,
StartBrowser: false,
NATEnabled: false,
@@ -188,7 +192,7 @@ func TestOverriddenValues(t *testing.T) {
URPostInsecurely: true,
ReleasesURL: "https://localhost/releases",
AlwaysLocalNets: []string{},
OverwriteNames: true,
OverwriteRemoteDevNames: true,
TempIndexMinBlocks: 100,
}
@@ -361,12 +365,12 @@ func TestIssue1750(t *testing.T) {
t.Errorf("%q != %q", cfg.Options().ListenAddresses[1], "tcp://:23001")
}
if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026/v2/" {
t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[0], "udp4://syncthing.nym.se:22026/v2/")
if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026" {
t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[0], "udp4://syncthing.nym.se:22026")
}
if cfg.Options().GlobalAnnServers[1] != "udp4://syncthing.nym.se:22027/v2/" {
t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[1], "udp4://syncthing.nym.se:22027/v2/")
if cfg.Options().GlobalAnnServers[1] != "udp4://syncthing.nym.se:22027" {
t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[1], "udp4://syncthing.nym.se:22027")
}
}
@@ -616,3 +620,61 @@ func TestRemoveDuplicateDevicesFolders(t *testing.T) {
t.Errorf("Incorrect number of folder devices, %d != 2", l)
}
}
func TestV14ListenAddressesMigration(t *testing.T) {
tcs := [][3][]string{
// Default listen plus default relays is now "default"
{
{"tcp://0.0.0.0:22000"},
{"dynamic+https://relays.syncthing.net/endpoint"},
{"default"},
},
// Default listen address without any relay addresses gets converted
// to just the listen address. It's easier this way, and frankly the
// user has gone to some trouble to get the empty string in the
// config to start with...
{
{"tcp://0.0.0.0:22000"}, // old listen addrs
{""}, // old relay addrs
{"tcp://0.0.0.0:22000"}, // new listen addrs
},
// Default listen plus non-default relays gets copied verbatim
{
{"tcp://0.0.0.0:22000"},
{"dynamic+https://other.example.com"},
{"tcp://0.0.0.0:22000", "dynamic+https://other.example.com"},
},
// Non-default listen plus default relays gets copied verbatim
{
{"tcp://1.2.3.4:22000"},
{"dynamic+https://relays.syncthing.net/endpoint"},
{"tcp://1.2.3.4:22000", "dynamic+https://relays.syncthing.net/endpoint"},
},
// Default stuff gets sucked into "default", the rest gets copied
{
{"tcp://0.0.0.0:22000", "tcp://1.2.3.4:22000"},
{"dynamic+https://relays.syncthing.net/endpoint", "relay://other.example.com"},
{"default", "tcp://1.2.3.4:22000", "relay://other.example.com"},
},
}
for _, tc := range tcs {
cfg := Configuration{
Version: 13,
Options: OptionsConfiguration{
ListenAddresses: tc[0],
DeprecatedRelayServers: tc[1],
},
}
convertV13V14(&cfg)
if cfg.Version != 14 {
t.Error("Configuration was not converted")
}
sort.Strings(tc[2])
if !reflect.DeepEqual(cfg.Options.ListenAddresses, tc[2]) {
t.Errorf("Migration error; actual %#v != expected %#v", cfg.Options.ListenAddresses, tc[2])
}
}
}

View File

@@ -42,7 +42,7 @@ type FolderConfiguration struct {
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
cachedPath string
DeprecatedReadOnly bool `xml:"ro,attr" json:"-"`
DeprecatedReadOnly bool `xml:"ro,attr,omitempty" json:"-"`
}
type FolderDeviceConfiguration struct {
@@ -138,6 +138,10 @@ func (f *FolderConfiguration) prepare() {
} else if f.RescanIntervalS < 0 {
f.RescanIntervalS = 0
}
if f.Versioning.Params == nil {
f.Versioning.Params = make(map[string]string)
}
}
func (f *FolderConfiguration) cleanedPath() string {

View File

@@ -16,6 +16,7 @@ type OptionsConfiguration struct {
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
NATEnabled bool `xml:"natEnabled" json:"natEnabled" default:"true"`
@@ -35,17 +36,16 @@ type OptionsConfiguration struct {
SymlinksEnabled bool `xml:"symlinksEnabled" json:"symlinksEnabled" default:"true"`
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
MinHomeDiskFreePct float64 `xml:"minHomeDiskFreePct" json:"minHomeDiskFreePct" default:"1"`
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=30"`
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://upgrades.syncthing.net/meta.json"`
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
OverwriteNames bool `xml:"overwriteNames" json:"overwriteNames" default:"false"`
OverwriteRemoteDevNames bool `xml:"overwriteRemoteDeviceNamesOnConnect" json:"overwriteRemoteDeviceNamesOnConnect" default:"false"`
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
DeprecatedUPnPEnabled bool `xml:"upnpEnabled" json:"-"`
DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes" json:"-"`
DeprecatedUPnPRenewalM int `xml:"upnpRenewalMinutes" json:"-"`
DeprecatedUPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"-"`
DeprecatedRelaysEnabled bool `xml:"relaysEnabled" json:"-"`
DeprecatedRelayServers []string `xml:"relayServer" json:"-"`
DeprecatedUPnPEnabled bool `xml:"upnpEnabled,omitempty" json:"-"`
DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes,omitempty" json:"-"`
DeprecatedUPnPRenewalM int `xml:"upnpRenewalMinutes,omitempty" json:"-"`
DeprecatedUPnPTimeoutS int `xml:"upnpTimeoutSeconds,omitempty" json:"-"`
DeprecatedRelayServers []string `xml:"relayServer,omitempty" json:"-"`
}
func (orig OptionsConfiguration) Copy() OptionsConfiguration {

0
lib/config/testdata/deviceaddressesdynamic.xml vendored Executable file → Normal file
View File

0
lib/config/testdata/deviceaddressesstatic.xml vendored Executable file → Normal file
View File

0
lib/config/testdata/nolistenaddress.xml vendored Executable file → Normal file
View File

4
lib/config/testdata/overridenvalues.xml vendored Executable file → Normal file
View File

@@ -1,4 +1,4 @@
<configuration version="13">
<configuration version="14">
<options>
<listenAddress>tcp://:23000</listenAddress>
<allowDelete>false</allowDelete>
@@ -32,7 +32,7 @@
<urInitialDelayS>800</urInitialDelayS>
<urPostInsecurely>true</urPostInsecurely>
<releasesURL>https://localhost/releases</releasesURL>
<overwriteNames>true</overwriteNames>
<overwriteRemoteDeviceNamesOnConnect>true</overwriteRemoteDeviceNamesOnConnect>
<tempIndexMinBlocks>100</tempIndexMinBlocks>
</options>
</configuration>

View File

@@ -1,5 +1,5 @@
<configuration version="13">
<folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
<folder id="test" path="testdata" ro="true" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
<minDiskFreePct>1</minDiskFreePct>

14
lib/config/testdata/v14.xml vendored Normal file
View File

@@ -0,0 +1,14 @@
<configuration version="14">
<folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
<minDiskFreePct>1</minDiskFreePct>
<maxConflicts>-1</maxConflicts>
</folder>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
<address>tcp://a</address>
</device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
<address>tcp://b</address>
</device>
</configuration>

14
lib/config/testdata/v15.xml vendored Normal file
View File

@@ -0,0 +1,14 @@
<configuration version="15">
<folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
<minDiskFreePct>1</minDiskFreePct>
<maxConflicts>-1</maxConflicts>
</folder>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
<address>tcp://a</address>
</device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
<address>tcp://b</address>
</device>
</configuration>

0
lib/config/testdata/v5.xml vendored Executable file → Normal file
View File

0
lib/config/testdata/versioningconfig.xml vendored Executable file → Normal file
View File

View File

@@ -21,7 +21,7 @@ import (
const relayPriority = 200
func init() {
dialers["relay"] = newRelayDialer
dialers["relay"] = relayDialerFactory{}
}
type relayDialer struct {
@@ -70,13 +70,23 @@ func (d *relayDialer) RedialFrequency() time.Duration {
return time.Duration(d.cfg.Options().RelayReconnectIntervalM) * time.Minute
}
func (d *relayDialer) String() string {
return "Relay Dialer"
}
type relayDialerFactory struct{}
func newRelayDialer(cfg *config.Wrapper, tlsCfg *tls.Config) genericDialer {
func (relayDialerFactory) New(cfg *config.Wrapper, tlsCfg *tls.Config) genericDialer {
return &relayDialer{
cfg: cfg,
tlsCfg: tlsCfg,
}
}
func (relayDialerFactory) Priority() int {
return relayPriority
}
func (relayDialerFactory) Enabled(cfg config.Configuration) bool {
return cfg.Options.RelaysEnabled
}
func (relayDialerFactory) String() string {
return "Relay Dialer"
}

View File

@@ -13,23 +13,26 @@ import (
"sync"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/relay/client"
)
func init() {
listeners["relay"] = newRelayListener
listeners["dynamic+http"] = newRelayListener
listeners["dynamic+https"] = newRelayListener
factory := &relayListenerFactory{}
listeners["relay"] = factory
listeners["dynamic+http"] = factory
listeners["dynamic+https"] = factory
}
type relayListener struct {
onAddressesChangedNotifier
uri *url.URL
tlsCfg *tls.Config
conns chan IntermediateConnection
uri *url.URL
tlsCfg *tls.Config
conns chan IntermediateConnection
factory listenerFactory
err error
client client.RelayClient
@@ -154,14 +157,25 @@ func (t *relayListener) Error() error {
return cerr
}
func (t *relayListener) Factory() listenerFactory {
return t.factory
}
func (t *relayListener) String() string {
return t.uri.String()
}
func newRelayListener(uri *url.URL, tlsCfg *tls.Config, conns chan IntermediateConnection, natService *nat.Service) genericListener {
type relayListenerFactory struct{}
func (f *relayListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan IntermediateConnection, natService *nat.Service) genericListener {
return &relayListener{
uri: uri,
tlsCfg: tlsCfg,
conns: conns,
uri: uri,
tlsCfg: tlsCfg,
conns: conns,
factory: f,
}
}
func (relayListenerFactory) Enabled(cfg config.Configuration) bool {
return cfg.Options.RelaysEnabled
}

View File

@@ -9,6 +9,7 @@ package connections
import (
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
@@ -54,9 +55,11 @@ type Service struct {
natService *nat.Service
natServiceToken *suture.ServiceToken
mut sync.RWMutex
listeners map[string]genericListener
listenerTokens map[string]suture.ServiceToken
listenersMut sync.RWMutex
listeners map[string]genericListener
listenerTokens map[string]suture.ServiceToken
curConMut sync.Mutex
currentConnection map[protocol.DeviceID]Connection
}
@@ -76,20 +79,24 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
lans: lans,
natService: nat.NewService(myID, cfg),
mut: sync.NewRWMutex(),
listeners: make(map[string]genericListener),
listenerTokens: make(map[string]suture.ServiceToken),
listenersMut: sync.NewRWMutex(),
listeners: make(map[string]genericListener),
listenerTokens: make(map[string]suture.ServiceToken),
curConMut: sync.NewMutex(),
currentConnection: make(map[protocol.DeviceID]Connection),
}
cfg.Subscribe(service)
// The rate variables are in KiB/s in the UI (despite the camel casing
// of the name). We multiply by 1024 here to get B/s.
if service.cfg.Options().MaxSendKbps > 0 {
service.writeRateLimit = ratelimit.NewBucketWithRate(float64(1024*service.cfg.Options().MaxSendKbps), int64(5*1024*service.cfg.Options().MaxSendKbps))
options := service.cfg.Options()
if options.MaxSendKbps > 0 {
service.writeRateLimit = ratelimit.NewBucketWithRate(float64(1024*options.MaxSendKbps), int64(5*1024*options.MaxSendKbps))
}
if service.cfg.Options().MaxRecvKbps > 0 {
service.readRateLimit = ratelimit.NewBucketWithRate(float64(1024*service.cfg.Options().MaxRecvKbps), int64(5*1024*service.cfg.Options().MaxRecvKbps))
if options.MaxRecvKbps > 0 {
service.readRateLimit = ratelimit.NewBucketWithRate(float64(1024*options.MaxRecvKbps), int64(5*1024*options.MaxRecvKbps))
}
// There are several moving parts here; one routine per listening address
@@ -108,6 +115,10 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
return service
}
var (
errDisabled = errors.New("disabled by configuration")
)
func (s *Service) handle() {
next:
for c := range s.conns {
@@ -153,9 +164,9 @@ next:
// If we have a relay connection, and the new incoming connection is
// not a relay connection, we should drop that, and prefer the this one.
s.mut.RLock()
skip := false
s.curConMut.Lock()
ct, ok := s.currentConnection[remoteID]
s.curConMut.Unlock()
// Lower priority is better, just like nice etc.
if ok && ct.Priority > c.Priority {
@@ -170,14 +181,10 @@ next:
// connections still established...
l.Infof("Connected to already connected device (%s)", remoteID)
c.Close()
skip = true
continue
} else if s.model.IsPaused(remoteID) {
l.Infof("Connection from paused device (%s)", remoteID)
c.Close()
skip = true
}
s.mut.RUnlock()
if skip {
continue
}
@@ -222,10 +229,10 @@ next:
l.Infof("Established secure connection to %s at %s", remoteID, name)
l.Debugf("cipher suite: %04X in lan: %t", c.ConnectionState().CipherSuite, !limit)
s.mut.Lock()
s.model.AddConnection(modelConn, hello)
s.curConMut.Lock()
s.currentConnection[remoteID] = modelConn
s.mut.Unlock()
s.curConMut.Unlock()
continue next
}
}
@@ -237,32 +244,56 @@ next:
func (s *Service) connect() {
nextDial := make(map[string]time.Time)
delay := time.Second
sleep := time.Second
// Used as delay for the first few connection attempts, increases
// exponentially
initialRampup := time.Second
// Calculated from actual dialers reconnectInterval
var sleep time.Duration
for {
cfg := s.cfg.Raw()
bestDialerPrio := 1<<31 - 1 // worse prio won't build on 32 bit
for _, df := range dialers {
if !df.Enabled(cfg) {
continue
}
if prio := df.Priority(); prio < bestDialerPrio {
bestDialerPrio = prio
}
}
l.Debugln("Reconnect loop")
now := time.Now()
var seen []string
nextDevice:
for deviceID, deviceCfg := range s.cfg.Devices() {
for _, deviceCfg := range cfg.Devices {
deviceID := deviceCfg.DeviceID
if deviceID == s.myID {
continue
}
l.Debugln("Reconnect loop for", deviceID)
s.mut.RLock()
paused := s.model.IsPaused(deviceID)
connected := s.model.ConnectedTo(deviceID)
ct := s.currentConnection[deviceID]
s.mut.RUnlock()
if paused {
continue
}
connected := s.model.ConnectedTo(deviceID)
s.curConMut.Lock()
ct := s.currentConnection[deviceID]
s.curConMut.Unlock()
if connected && ct.Priority == bestDialerPrio {
// Things are already as good as they can get.
continue
}
l.Debugln("Reconnect loop for", deviceID)
var addrs []string
for _, addr := range deviceCfg.Addresses {
if addr == "dynamic" {
@@ -279,35 +310,40 @@ func (s *Service) connect() {
seen = append(seen, addrs...)
for _, addr := range addrs {
nextDialAt, ok := nextDial[addr]
if ok && initialRampup >= sleep && nextDialAt.After(now) {
l.Debugf("Not dialing %v as sleep is %v, next dial is at %s and current time is %s", addr, sleep, nextDialAt, now)
continue
}
// If we fail at any step before actually getting the dialer
// retry in a minute
nextDial[addr] = now.Add(time.Minute)
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse connection url:", addr, err)
l.Infof("Dialer for %s: %v", addr, err)
continue
}
dialerFactory, ok := dialers[uri.Scheme]
if !ok {
l.Debugln("Unknown address schema", uri)
dialerFactory, err := s.getDialerFactory(cfg, uri)
if err == errDisabled {
l.Debugln("Dialer for", uri, "is disabled")
continue
}
if err != nil {
l.Infof("Dialer for %v: %v", uri, err)
continue
}
dialer := dialerFactory(s.cfg, s.tlsCfg)
nextDialAt, ok := nextDial[uri.String()]
// See below for comments on this delay >= sleep check
if delay >= sleep && ok && nextDialAt.After(now) {
l.Debugf("Not dialing as next dial is at %s and current time is %s", nextDialAt, now)
continue
}
nextDial[uri.String()] = now.Add(dialer.RedialFrequency())
if connected && dialer.Priority() >= ct.Priority {
l.Debugf("Not dialing using %s as priorty is less than current connection (%d >= %d)", dialer, dialer.Priority(), ct.Priority)
if connected && dialerFactory.Priority() >= ct.Priority {
l.Debugf("Not dialing using %s as priorty is less than current connection (%d >= %d)", dialerFactory, dialerFactory.Priority(), ct.Priority)
continue
}
dialer := dialerFactory.New(s.cfg, s.tlsCfg)
l.Debugln("dial", deviceCfg.DeviceID, uri)
nextDial[addr] = now.Add(dialer.RedialFrequency())
conn, err := dialer.Dial(deviceID, uri)
if err != nil {
l.Debugln("dial failed", deviceCfg.DeviceID, uri, err)
@@ -325,12 +361,12 @@ func (s *Service) connect() {
nextDial, sleep = filterAndFindSleepDuration(nextDial, seen, now)
// delay variable is used to trigger much more frequent dialing after
// initial startup, essentially causing redials every 1, 2, 4, 8... seconds
if delay < sleep {
time.Sleep(delay)
delay *= 2
if initialRampup < sleep {
l.Debugln("initial rampup; sleep", initialRampup, "and update to", initialRampup*2)
time.Sleep(initialRampup)
initialRampup *= 2
} else {
l.Debugln("sleep until next dial", sleep)
time.Sleep(sleep)
}
}
@@ -353,25 +389,16 @@ func (s *Service) shouldLimit(addr net.Addr) bool {
return !tcpaddr.IP.IsLoopback()
}
func (s *Service) createListener(addr string) {
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse listen address:", addr, err)
return
}
func (s *Service) createListener(factory listenerFactory, uri *url.URL) bool {
// must be called with listenerMut held
listenerFactory, ok := listeners[uri.Scheme]
if !ok {
l.Infoln("Unknown listen address scheme:", uri.String())
return
}
l.Debugln("Starting listener", uri)
listener := listenerFactory(uri, s.tlsCfg, s.conns, s.natService)
listener := factory.New(uri, s.cfg, s.tlsCfg, s.conns, s.natService)
listener.OnAddressesChanged(s.logListenAddressesChangedEvent)
s.mut.Lock()
s.listeners[addr] = listener
s.listenerTokens[addr] = s.Add(listener)
s.mut.Unlock()
s.listeners[uri.String()] = listener
s.listenerTokens[uri.String()] = s.Add(listener)
return true
}
func (s *Service) logListenAddressesChangedEvent(l genericListener) {
@@ -402,30 +429,43 @@ func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
}
}
s.mut.RLock()
existingListeners := s.listeners
s.mut.RUnlock()
s.listenersMut.Lock()
seen := make(map[string]struct{})
for _, addr := range config.Wrap("", to).ListenAddresses() {
if _, ok := existingListeners[addr]; !ok {
l.Debugln("Staring listener", addr)
s.createListener(addr)
if _, ok := s.listeners[addr]; ok {
seen[addr] = struct{}{}
continue
}
uri, err := url.Parse(addr)
if err != nil {
l.Infof("Listener for %s: %v", addr, err)
continue
}
factory, err := s.getListenerFactory(to, uri)
if err == errDisabled {
l.Debugln("Listener for", uri, "is disabled")
continue
}
if err != nil {
l.Infof("Listener for %v: %v", uri, err)
continue
}
s.createListener(factory, uri)
seen[addr] = struct{}{}
}
s.mut.Lock()
for addr := range s.listeners {
if _, ok := seen[addr]; !ok {
for addr, listener := range s.listeners {
if _, ok := seen[addr]; !ok || !listener.Factory().Enabled(to) {
l.Debugln("Stopping listener", addr)
s.Remove(s.listenerTokens[addr])
delete(s.listenerTokens, addr)
delete(s.listeners, addr)
}
}
s.mut.Unlock()
s.listenersMut.Unlock()
if to.Options.NATEnabled && s.natServiceToken == nil {
l.Debugln("Starting NAT service")
@@ -441,7 +481,7 @@ func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
}
func (s *Service) AllAddresses() []string {
s.mut.RLock()
s.listenersMut.RLock()
var addrs []string
for _, listener := range s.listeners {
for _, lanAddr := range listener.LANAddresses() {
@@ -451,24 +491,24 @@ func (s *Service) AllAddresses() []string {
addrs = append(addrs, wanAddr.String())
}
}
s.mut.RUnlock()
s.listenersMut.RUnlock()
return util.UniqueStrings(addrs)
}
func (s *Service) ExternalAddresses() []string {
s.mut.RLock()
s.listenersMut.RLock()
var addrs []string
for _, listener := range s.listeners {
for _, wanAddr := range listener.WANAddresses() {
addrs = append(addrs, wanAddr.String())
}
}
s.mut.RUnlock()
s.listenersMut.RUnlock()
return util.UniqueStrings(addrs)
}
func (s *Service) Status() map[string]interface{} {
s.mut.RLock()
s.listenersMut.RLock()
result := make(map[string]interface{})
for addr, listener := range s.listeners {
status := make(map[string]interface{})
@@ -483,10 +523,36 @@ func (s *Service) Status() map[string]interface{} {
result[addr] = status
}
s.mut.RUnlock()
s.listenersMut.RUnlock()
return result
}
func (s *Service) getDialerFactory(cfg config.Configuration, uri *url.URL) (dialerFactory, error) {
dialerFactory, ok := dialers[uri.Scheme]
if !ok {
return nil, fmt.Errorf("unknown address scheme %q", uri.Scheme)
}
if !dialerFactory.Enabled(cfg) {
return nil, errDisabled
}
return dialerFactory, nil
}
func (s *Service) getListenerFactory(cfg config.Configuration, uri *url.URL) (listenerFactory, error) {
listenerFactory, ok := listeners[uri.Scheme]
if !ok {
return nil, fmt.Errorf("unknown address scheme %q", uri.Scheme)
}
if !listenerFactory.Enabled(cfg) {
return nil, errDisabled
}
return listenerFactory, nil
}
func exchangeHello(c net.Conn, h protocol.HelloMessage) (protocol.HelloMessage, error) {
if err := c.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
return protocol.HelloMessage{}, err

View File

@@ -28,16 +28,22 @@ type Connection struct {
protocol.Connection
}
type dialerFactory func(*config.Wrapper, *tls.Config) genericDialer
type genericDialer interface {
Dial(protocol.DeviceID, *url.URL) (IntermediateConnection, error)
type dialerFactory interface {
New(*config.Wrapper, *tls.Config) genericDialer
Priority() int
RedialFrequency() time.Duration
Enabled(config.Configuration) bool
String() string
}
type listenerFactory func(*url.URL, *tls.Config, chan IntermediateConnection, *nat.Service) genericListener
type genericDialer interface {
Dial(protocol.DeviceID, *url.URL) (IntermediateConnection, error)
RedialFrequency() time.Duration
}
type listenerFactory interface {
New(*url.URL, *config.Wrapper, *tls.Config, chan IntermediateConnection, *nat.Service) genericListener
Enabled(config.Configuration) bool
}
type genericListener interface {
Serve()
@@ -55,6 +61,7 @@ type genericListener interface {
Error() error
OnAddressesChanged(func(genericListener))
String() string
Factory() listenerFactory
}
type Model interface {

View File

@@ -8,7 +8,6 @@ package connections
import (
"crypto/tls"
"net"
"net/url"
"time"
@@ -20,8 +19,9 @@ import (
const tcpPriority = 10
func init() {
factory := &tcpDialerFactory{}
for _, scheme := range []string{"tcp", "tcp4", "tcp6"} {
dialers[scheme] = newTCPDialer
dialers[scheme] = factory
}
}
@@ -33,13 +33,7 @@ type tcpDialer struct {
func (d *tcpDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConnection, error) {
uri = fixupPort(uri)
raddr, err := net.ResolveTCPAddr(uri.Scheme, uri.Host)
if err != nil {
l.Debugln(err)
return IntermediateConnection{}, err
}
conn, err := dialer.DialTimeout(raddr.Network(), raddr.String(), 10*time.Second)
conn, err := dialer.DialTimeout(uri.Scheme, uri.Host, 10*time.Second)
if err != nil {
l.Debugln(err)
return IntermediateConnection{}, err
@@ -55,21 +49,27 @@ func (d *tcpDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConnec
return IntermediateConnection{tc, "TCP (Client)", tcpPriority}, nil
}
func (tcpDialer) Priority() int {
return tcpPriority
}
func (d *tcpDialer) RedialFrequency() time.Duration {
return time.Duration(d.cfg.Options().ReconnectIntervalS) * time.Second
}
func (d *tcpDialer) String() string {
return "TCP Dialer"
}
type tcpDialerFactory struct{}
func newTCPDialer(cfg *config.Wrapper, tlsCfg *tls.Config) genericDialer {
func (tcpDialerFactory) New(cfg *config.Wrapper, tlsCfg *tls.Config) genericDialer {
return &tcpDialer{
cfg: cfg,
tlsCfg: tlsCfg,
}
}
func (tcpDialerFactory) Priority() int {
return tcpPriority
}
func (tcpDialerFactory) Enabled(cfg config.Configuration) bool {
return true
}
func (tcpDialerFactory) String() string {
return "TCP Dialer"
}

View File

@@ -14,30 +14,32 @@ import (
"sync"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/nat"
)
func init() {
factory := &tcpListenerFactory{}
for _, scheme := range []string{"tcp", "tcp4", "tcp6"} {
listeners[scheme] = newTCPListener
listeners[scheme] = factory
}
}
type tcpListener struct {
onAddressesChangedNotifier
uri *url.URL
tlsCfg *tls.Config
stop chan struct{}
conns chan IntermediateConnection
uri *url.URL
tlsCfg *tls.Config
stop chan struct{}
conns chan IntermediateConnection
factory listenerFactory
natService *nat.Service
mapping *nat.Mapping
address *url.URL
err error
mut sync.RWMutex
err error
mut sync.RWMutex
}
func (t *tcpListener) Serve() {
@@ -64,6 +66,9 @@ func (t *tcpListener) Serve() {
}
defer listener.Close()
l.Infof("TCP listener (%v) starting", listener.Addr())
defer l.Infof("TCP listener (%v) shutting down", listener.Addr())
mapping := t.natService.NewMapping(nat.TCP, tcaddr.IP, tcaddr.Port)
mapping.OnChanged(func(_ *nat.Mapping, _, _ []nat.Address) {
t.notifyAddressesChanged(t)
@@ -153,48 +158,25 @@ func (t *tcpListener) String() string {
return t.uri.String()
}
func newTCPListener(uri *url.URL, tlsCfg *tls.Config, conns chan IntermediateConnection, natService *nat.Service) genericListener {
func (t *tcpListener) Factory() listenerFactory {
return t.factory
}
type tcpListenerFactory struct{}
func (f *tcpListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan IntermediateConnection, natService *nat.Service) genericListener {
return &tcpListener{
uri: fixupPort(uri),
tlsCfg: tlsCfg,
conns: conns,
natService: natService,
stop: make(chan struct{}),
factory: f,
}
}
func isPublicIPv4(ip net.IP) bool {
ip = ip.To4()
if ip == nil {
// Not an IPv4 address (IPv6)
return false
}
// IsGlobalUnicast below only checks that it's not link local or
// multicast, and we want to exclude private (NAT:ed) addresses as well.
rfc1918 := []net.IPNet{
{IP: net.IP{10, 0, 0, 0}, Mask: net.IPMask{255, 0, 0, 0}},
{IP: net.IP{172, 16, 0, 0}, Mask: net.IPMask{255, 240, 0, 0}},
{IP: net.IP{192, 168, 0, 0}, Mask: net.IPMask{255, 255, 0, 0}},
}
for _, n := range rfc1918 {
if n.Contains(ip) {
return false
}
}
return ip.IsGlobalUnicast()
}
func isPublicIPv6(ip net.IP) bool {
if ip.To4() != nil {
// Not an IPv6 address (IPv4)
// (To16() returns a v6 mapped v4 address so can't be used to check
// that it's an actual v6 address)
return false
}
return ip.IsGlobalUnicast()
func (tcpListenerFactory) Enabled(cfg config.Configuration) bool {
return true
}
func fixupPort(uri *url.URL) *url.URL {
@@ -203,7 +185,7 @@ func fixupPort(uri *url.URL) *url.URL {
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"
copyURI.Host = net.JoinHostPort(host, "22000")
copyURI.Host = net.JoinHostPort(uri.Host, "22000")
} else if err == nil && port == "" {
// addr is on the form "1.2.3.4:"
copyURI.Host = net.JoinHostPort(host, "22000")

View File

@@ -262,7 +262,17 @@ func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn It
dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, device, prefix)[:keyPrefixLen+keyFolderLen+keyDeviceLen+len(prefix)]), nil)
defer dbi.Release()
slashedPrefix := prefix
if !bytes.HasSuffix(prefix, []byte{'/'}) {
slashedPrefix = append(slashedPrefix, '/')
}
for dbi.Next() {
name := db.deviceKeyName(dbi.Key())
if len(prefix) > 0 && !bytes.Equal(name, prefix) && !bytes.HasPrefix(name, slashedPrefix) {
return
}
// The iterator function may keep a reference to the unmarshalled
// struct, which in turn references the buffer it was unmarshalled
// from. dbi.Value() just returns an internal slice that it reuses, so
@@ -359,6 +369,11 @@ func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator
dbi := t.NewIterator(util.BytesPrefix(db.globalKey(folder, prefix)), nil)
defer dbi.Release()
slashedPrefix := prefix
if !bytes.HasSuffix(prefix, []byte{'/'}) {
slashedPrefix = append(slashedPrefix, '/')
}
var fk []byte
for dbi.Next() {
var vl versionList
@@ -370,7 +385,12 @@ func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator
l.Debugln(dbi.Key())
panic("no versions?")
}
name := db.globalKeyName(dbi.Key())
if len(prefix) > 0 && !bytes.Equal(name, prefix) && !bytes.HasPrefix(name, slashedPrefix) {
return
}
fk = db.deviceKeyInto(fk[:cap(fk)], folder, vl.versions[0].device, name)
bs, err := t.Get(fk, nil)
if err != nil {

View File

@@ -18,7 +18,7 @@ import (
// The CachingMux aggregates results from multiple Finders. Each Finder has
// an associated cache time and negative cache time. The cache time sets how
// long we cache and return successfull lookup results, the negative cache
// long we cache and return successful lookup results, the negative cache
// time sets how long we refrain from asking about the same device ID after
// receiving a negative answer. The value of zero disables caching (positive
// or negative).

View File

@@ -12,6 +12,7 @@ import (
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
@@ -154,9 +155,14 @@ func (c *globalClient) Lookup(device protocol.DeviceID) (addresses []string, err
return nil, err
}
var ann announcement
err = json.NewDecoder(resp.Body).Decode(&ann)
bs, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
resp.Body.Close()
var ann announcement
err = json.Unmarshal(bs, &ann)
return ann.Addresses, err
}

View File

@@ -27,6 +27,7 @@ const (
DeviceRejected
DevicePaused
DeviceResumed
LocalChangeDetected
LocalIndexUpdated
RemoteIndexUpdated
ItemStarted
@@ -35,6 +36,7 @@ const (
FolderRejected
ConfigSaved
DownloadProgress
RemoteDownloadProgress
FolderSummary
FolderCompletion
FolderErrors
@@ -61,6 +63,8 @@ func (t EventType) String() string {
return "DeviceDisconnected"
case DeviceRejected:
return "DeviceRejected"
case LocalChangeDetected:
return "LocalChangeDetected"
case LocalIndexUpdated:
return "LocalIndexUpdated"
case RemoteIndexUpdated:
@@ -77,6 +81,8 @@ func (t EventType) String() string {
return "ConfigSaved"
case DownloadProgress:
return "DownloadProgress"
case RemoteDownloadProgress:
return "RemoteDownloadProgress"
case FolderSummary:
return "FolderSummary"
case FolderCompletion:

View File

@@ -246,8 +246,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
addPattern := func(line string) error {
pattern := Pattern{
pattern: line,
result: defaultResult,
result: defaultResult,
}
// Allow prefixes to be specified in any order, but only once.
@@ -275,19 +274,21 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
line = strings.ToLower(line)
}
pattern.pattern = line
var err error
if strings.HasPrefix(line, "/") {
// Pattern is rooted in the current dir only
pattern.match, err = glob.Compile(line[1:])
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
return fmt.Errorf("invalid pattern %q in ignore file (%v)", line, err)
}
patterns = append(patterns, pattern)
} else if strings.HasPrefix(line, "**/") {
// Add the pattern as is, and without **/ so it matches in current dir
pattern.match, err = glob.Compile(line)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
return fmt.Errorf("invalid pattern %q in ignore file (%v)", line, err)
}
patterns = append(patterns, pattern)
@@ -295,7 +296,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
pattern.pattern = line
pattern.match, err = glob.Compile(line)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
return fmt.Errorf("invalid pattern %q in ignore file (%v)", line, err)
}
patterns = append(patterns, pattern)
} else if strings.HasPrefix(line, "#include ") {
@@ -311,7 +312,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
// current directory and subdirs.
pattern.match, err = glob.Compile(line)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
return fmt.Errorf("invalid pattern %q in ignore file (%v)", line, err)
}
patterns = append(patterns, pattern)
@@ -319,7 +320,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
pattern.pattern = line
pattern.match, err = glob.Compile(line)
if err != nil {
return fmt.Errorf("invalid pattern %q in ignore file", line)
return fmt.Errorf("invalid pattern %q in ignore file (%v)", line, err)
}
patterns = append(patterns, pattern)
}

View File

@@ -295,7 +295,7 @@ func TestCaching(t *testing.T) {
pats.Match(letter)
}
// Verify that outcomes preserved on next laod
// Verify that outcomes preserved on next load
err = pats.Load(fd1.Name())
if err != nil {
@@ -323,7 +323,7 @@ func TestCaching(t *testing.T) {
pats.Match(letter)
}
// Verify that outcomes provided on next laod
// Verify that outcomes provided on next load
err = pats.Load(fd1.Name())
if err != nil {
@@ -635,3 +635,71 @@ func TestAutomaticCaseInsensitivity(t *testing.T) {
}
}
}
func TestCommas(t *testing.T) {
stignore := `
foo,bar.txt
{baz,quux}.txt
`
pats := New(true)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
match bool
}{
{"foo.txt", false},
{"bar.txt", false},
{"foo,bar.txt", true},
{"baz.txt", true},
{"quux.txt", true},
{"baz,quux.txt", false},
}
for _, tc := range tests {
if pats.Match(tc.name).IsIgnored() != tc.match {
t.Errorf("Match of %s was %v, should be %v", tc.name, !tc.match, tc.match)
}
}
}
func TestIssue3164(t *testing.T) {
stignore := `
(?d)(?i)*.part
(?d)(?i)/foo
(?d)(?i)**/bar
`
pats := New(true)
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
expanded := pats.Patterns()
t.Log(expanded)
expected := []string{
"(?d)(?i)*.part",
"(?d)(?i)**/*.part",
"(?d)(?i)*.part/**",
"(?d)(?i)**/*.part/**",
"(?d)(?i)/foo",
"(?d)(?i)/foo/**",
"(?d)(?i)**/bar",
"(?d)(?i)bar",
"(?d)(?i)**/bar/**",
"(?d)(?i)bar/**",
}
if len(expanded) != len(expected) {
t.Errorf("Unmatched count: %d != %d", len(expanded), len(expected))
}
for i := range expanded {
if expanded[i] != expected[i] {
t.Errorf("Pattern %d does not match: %s != %s", i, expanded[i], expected[i])
}
}
}

View File

@@ -23,12 +23,11 @@ type deviceFolderFileDownloadState struct {
// deviceFolderDownloadState holds current download state of all files that
// a remote device is currently downloading in a specific folder.
type deviceFolderDownloadState struct {
mut sync.RWMutex
files map[string]deviceFolderFileDownloadState
numberOfBlocksInProgress int
mut sync.RWMutex
files map[string]deviceFolderFileDownloadState
}
// Has returns wether a block at that specific index, and that specific version of the file
// Has returns whether a block at that specific index, and that specific version of the file
// is currently available on the remote device for pulling from a temporary file.
func (p *deviceFolderDownloadState) Has(file string, version protocol.Vector, index int32) bool {
p.mut.RLock()
@@ -57,7 +56,6 @@ func (p *deviceFolderDownloadState) Update(updates []protocol.FileDownloadProgre
for _, update := range updates {
local, ok := p.files[update.Name]
if update.UpdateType == protocol.UpdateTypeForget && ok && local.version.Equal(update.Version) {
p.numberOfBlocksInProgress -= len(local.blockIndexes)
delete(p.files, update.Name)
} else if update.UpdateType == protocol.UpdateTypeAppend {
if !ok {
@@ -66,25 +64,25 @@ func (p *deviceFolderDownloadState) Update(updates []protocol.FileDownloadProgre
version: update.Version,
}
} else if !local.version.Equal(update.Version) {
p.numberOfBlocksInProgress -= len(local.blockIndexes)
local.blockIndexes = append(local.blockIndexes[:0], update.BlockIndexes...)
local.version = update.Version
} else {
local.blockIndexes = append(local.blockIndexes, update.BlockIndexes...)
}
p.files[update.Name] = local
p.numberOfBlocksInProgress += len(update.BlockIndexes)
}
}
}
// NumberOfBlocksInProgress returns the number of blocks the device has downloaded
// for a specific folder.
func (p *deviceFolderDownloadState) NumberOfBlocksInProgress() int {
// GetBlockCounts returns a map filename -> number of blocks downloaded.
func (p *deviceFolderDownloadState) GetBlockCounts() map[string]int {
p.mut.RLock()
n := p.numberOfBlocksInProgress
res := make(map[string]int, len(p.files))
for name, state := range p.files {
res[name] = len(state.blockIndexes)
}
p.mut.RUnlock()
return n
return res
}
// deviceDownloadState represents the state of all in progress downloads
@@ -117,7 +115,7 @@ func (t *deviceDownloadState) Update(folder string, updates []protocol.FileDownl
f.Update(updates)
}
// Has returns wether block at that specific index, and that specific version of the file
// Has returns whether block at that specific index, and that specific version of the file
// is currently available on the remote device for pulling from a temporary file.
func (t *deviceDownloadState) Has(folder, file string, version protocol.Vector, index int32) bool {
if t == nil {
@@ -134,20 +132,22 @@ func (t *deviceDownloadState) Has(folder, file string, version protocol.Vector,
return f.Has(file, version, index)
}
// NumberOfBlocksInProgress returns the number of blocks the device has downloaded
// for all folders.
func (t *deviceDownloadState) NumberOfBlocksInProgress() int {
// GetBlockCounts returns a map filename -> number of blocks downloaded for the
// given folder.
func (t *deviceDownloadState) GetBlockCounts(folder string) map[string]int {
if t == nil {
return 0
return nil
}
n := 0
t.mut.RLock()
for _, folder := range t.folders {
n += folder.NumberOfBlocksInProgress()
defer t.mut.RUnlock()
for name, state := range t.folders {
if name == folder {
return state.GetBlockCounts()
}
}
t.mut.RUnlock()
return n
return nil
}
func newDeviceDownloadState() *deviceDownloadState {

View File

@@ -374,17 +374,26 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
return 100 // Folder is empty, so we have all of it
}
var need int64
m.pmut.RLock()
counts := m.deviceDownloads[device].GetBlockCounts(folder)
m.pmut.RUnlock()
var need, fileNeed, downloaded int64
rf.WithNeedTruncated(device, func(f db.FileIntf) bool {
need += f.Size()
ft := f.(db.FileInfoTruncated)
// This might might be more than it really is, because some blocks can be of a smaller size.
downloaded = int64(counts[ft.Name] * protocol.BlockSize)
fileNeed = ft.Size() - downloaded
if fileNeed < 0 {
fileNeed = 0
}
need += fileNeed
return true
})
// This might might be more than it really is, because some blocks can be of a smaller size.
m.pmut.RLock()
need -= int64(m.deviceDownloads[device].NumberOfBlocksInProgress() * protocol.BlockSize)
m.pmut.RUnlock()
needRatio := float64(need) / float64(tot)
completionPct := 100 * (1 - needRatio)
l.Debugf("%v Completion(%s, %q): %f (%d / %d = %f)", m, device, folder, completionPct, need, tot, needRatio)
@@ -1048,7 +1057,7 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloM
m.pmut.Unlock()
device, ok := m.cfg.Devices()[deviceID]
if ok && (device.Name == "" || m.cfg.Options().OverwriteNames) {
if ok && (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) {
device.Name = hello.DeviceName
m.cfg.SetDevice(device)
m.cfg.Save()
@@ -1083,7 +1092,14 @@ func (m *Model) DownloadProgress(device protocol.DeviceID, folder string, update
m.pmut.RLock()
m.deviceDownloads[device].Update(folder, updates)
state := m.deviceDownloads[device].GetBlockCounts(folder)
m.pmut.RUnlock()
events.Default.Log(events.RemoteDownloadProgress, map[string]interface{}{
"device": device.String(),
"folder": folder,
"state": state,
})
}
func (m *Model) ResumeDevice(device protocol.DeviceID) {
@@ -1234,6 +1250,21 @@ func sendIndexTo(initial bool, minLocalVer int64, conn protocol.Connection, fold
return maxLocalVer, err
}
func (m *Model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo) {
m.updateLocals(folder, fs)
// Fire the LocalChangeDetected event to notify listeners about local
// updates.
m.fmut.RLock()
path := m.folderCfgs[folder].Path()
m.fmut.RUnlock()
m.localChangeDetected(folder, path, fs)
}
func (m *Model) updateLocalsFromPulling(folder string, fs []protocol.FileInfo) {
m.updateLocals(folder, fs)
}
func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
m.fmut.RLock()
files := m.folderFiles[folder]
@@ -1257,6 +1288,44 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
})
}
func (m *Model) localChangeDetected(folder, path string, files []protocol.FileInfo) {
// For windows paths, strip unwanted chars from the front
path = strings.Replace(path, `\\?\`, "", 1)
for _, file := range files {
objType := "file"
action := "modified"
// If our local vector is verison 1 AND it is the only version vector so far seen for this file then
// it is a new file. Else if it is > 1 it's not new, and if it is 1 but another shortId version vector
// exists then it is new for us but created elsewhere so the file is still not new but modified by us.
// Only if it is truly new do we change this to 'added', else we leave it as 'modified'.
if len(file.Version) == 1 && file.Version[0].Value == 1 {
action = "added"
}
if file.IsDirectory() {
objType = "dir"
}
if file.IsDeleted() {
action = "deleted"
}
// If the file is a level or more deep then the forward slash seperator is embedded
// in the filename and makes the path look wierd on windows, so lets fix it
filename := filepath.FromSlash(file.Name)
// And append it to the filepath
path := filepath.Join(path, filename)
events.Default.Log(events.LocalChangeDetected, map[string]string{
"folder": folder,
"action": action,
"type": objType,
"path": path,
})
}
}
func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
m.pmut.RLock()
nc, ok := m.conn[deviceID]
@@ -1401,7 +1470,9 @@ func (m *Model) internalScanFolderSubdirs(folder string, subs []string) error {
cancel := make(chan struct{})
defer close(cancel)
w := &scanner.Walker{
runner.setState(FolderScanning)
fchan, err := scanner.Walk(scanner.Config{
Folder: folderCfg.ID,
Dir: folderCfg.Path(),
Subs: subs,
@@ -1417,11 +1488,8 @@ func (m *Model) internalScanFolderSubdirs(folder string, subs []string) error {
ShortID: m.shortID,
ProgressTickIntervalS: folderCfg.ScanProgressIntervalS,
Cancel: cancel,
}
})
runner.setState(FolderScanning)
fchan, err := w.Walk()
if err != nil {
// The error we get here is likely an OS level error, which might not be
// as readable as our health check errors. Check if we can get a health
@@ -1445,7 +1513,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subs []string) error {
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, err)
return err
}
m.updateLocals(folder, batch)
m.updateLocalsFromScanning(folder, batch)
batch = batch[:0]
blocksHandled = 0
}
@@ -1457,7 +1525,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subs []string) error {
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, err)
return err
} else if len(batch) > 0 {
m.updateLocals(folder, batch)
m.updateLocalsFromScanning(folder, batch)
}
if len(subs) == 0 {
@@ -1479,7 +1547,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subs []string) error {
iterError = err
return false
}
m.updateLocals(folder, batch)
m.updateLocalsFromScanning(folder, batch)
batch = batch[:0]
}
@@ -1531,7 +1599,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subs []string) error {
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, err)
return err
} else if len(batch) > 0 {
m.updateLocals(folder, batch)
m.updateLocalsFromScanning(folder, batch)
}
runner.setState(FolderIdle)
@@ -1659,7 +1727,7 @@ func (m *Model) Override(folder string) {
fs.WithNeed(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
need := fi.(protocol.FileInfo)
if len(batch) == indexBatchSize {
m.updateLocals(folder, batch)
m.updateLocalsFromScanning(folder, batch)
batch = batch[:0]
}
@@ -1679,7 +1747,7 @@ func (m *Model) Override(folder string) {
return true
})
if len(batch) > 0 {
m.updateLocals(folder, batch)
m.updateLocalsFromScanning(folder, batch)
}
runner.setState(FolderIdle)
}
@@ -2018,6 +2086,8 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
// by themselves.
from.Options.URAccepted = to.Options.URAccepted
from.Options.URUniqueID = to.Options.URUniqueID
from.Options.ListenAddresses = to.Options.ListenAddresses
from.Options.RelaysEnabled = to.Options.RelaysEnabled
// All of the other generic options require restart. Or at least they may;
// removing this check requires going through those options carefully and
// making sure there are individual services that handle them correctly.

View File

@@ -386,7 +386,7 @@ func TestDeviceRename(t *testing.T) {
m.Close(device1, protocol.ErrTimeout)
opts := cfg.Options()
opts.OverwriteNames = true
opts.OverwriteRemoteDevNames = true
cfg.SetOptions(opts)
hello.DeviceName = "tester2"
@@ -1308,7 +1308,7 @@ func TestUnifySubs(t *testing.T) {
[]string{".stfolder", ".stignore"},
},
{
// 7. but the presense of something else unknown forces an actual
// 7. but the presence of something else unknown forces an actual
// scan
[]string{".stfolder", ".stignore", "foo/bar"},
[]string{},
@@ -1360,6 +1360,66 @@ func TestUnifySubs(t *testing.T) {
}
}
func TestIssue3028(t *testing.T) {
// Create two files that we'll delete, one with a name that is a prefix of the other.
if err := ioutil.WriteFile("testdata/testrm", []byte("Hello"), 0644); err != nil {
t.Fatal(err)
}
defer os.Remove("testdata/testrm")
if err := ioutil.WriteFile("testdata/testrm2", []byte("Hello"), 0644); err != nil {
t.Fatal(err)
}
defer os.Remove("testdata/testrm2")
// Create a model and default folder
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
defCfg := defaultFolderConfig.Copy()
defCfg.RescanIntervalS = 86400
m.AddFolder(defCfg)
m.StartFolder("default")
m.ServeBackground()
// Ugly hack for testing: reach into the model for the rwfolder and wait
// for it to complete the initial scan. The risk is that it otherwise
// runs during our modifications and screws up the test.
m.fmut.RLock()
folder := m.folderRunners["default"].(*rwFolder)
m.fmut.RUnlock()
<-folder.initialScanCompleted
// Get a count of how many files are there now
locorigfiles, _, _ := m.LocalSize("default")
globorigfiles, _, _ := m.GlobalSize("default")
// Delete and rescan specifically these two
os.Remove("testdata/testrm")
os.Remove("testdata/testrm2")
m.ScanFolderSubs("default", []string{"testrm", "testrm2"})
// Verify that the number of files decreased by two and the number of
// deleted files increases by two
locnowfiles, locdelfiles, _ := m.LocalSize("default")
globnowfiles, globdelfiles, _ := m.GlobalSize("default")
if locnowfiles != locorigfiles-2 {
t.Errorf("Incorrect local accounting; got %d current files, expected %d", locnowfiles, locorigfiles-2)
}
if globnowfiles != globorigfiles-2 {
t.Errorf("Incorrect global accounting; got %d current files, expected %d", globnowfiles, globorigfiles-2)
}
if locdelfiles != 2 {
t.Errorf("Incorrect local accounting; got %d deleted files, expected 2", locdelfiles)
}
if globdelfiles != 2 {
t.Errorf("Incorrect global accounting; got %d deleted files, expected 2", globdelfiles)
}
}
type fakeAddr struct{}
func (fakeAddr) Network() string {

View File

@@ -245,6 +245,18 @@ func TestSendDownloadProgressMessages(t *testing.T) {
expect(1, state1, protocol.UpdateTypeAppend, v2, []int32{1, 2}, true)
expectEmpty()
// Returns forget and append if sharedPullerState creation timer changes.
state1.available = []int32{1}
state1.availableUpdated = tick()
state1.created = tick()
p.sendDownloadProgressMessages()
expect(0, state1, protocol.UpdateTypeForget, v2, nil, false)
expect(1, state1, protocol.UpdateTypeAppend, v2, []int32{1}, true)
expectEmpty()
// Sends an empty update if new file exists, but does not have any blocks yet. (To indicate that the old blocks are no longer available)
state1.file.Version = v1
state1.available = nil

View File

@@ -100,6 +100,8 @@ type rwFolder struct {
errors map[string]string // path -> error string
errorsMut sync.Mutex
initialScanCompleted chan (struct{}) // exposed for testing
}
func newRWFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner) service {
@@ -135,6 +137,8 @@ func newRWFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Ver
remoteIndex: make(chan struct{}, 1), // This needs to be 1-buffered so that we queue a notification if we're busy doing a pull when it comes.
errorsMut: sync.NewMutex(),
initialScanCompleted: make(chan struct{}),
}
f.configureCopiersAndPullers(cfg)
@@ -186,9 +190,6 @@ func (f *rwFolder) Serve() {
var prevVer int64
var prevIgnoreHash string
// We don't start pulling files until a scan has been completed.
initialScanCompleted := false
for {
select {
case <-f.stop:
@@ -200,7 +201,10 @@ func (f *rwFolder) Serve() {
l.Debugln(f, "remote index updated, rescheduling pull")
case <-f.pullTimer.C:
if !initialScanCompleted {
select {
case <-f.initialScanCompleted:
default:
// We don't start pulling files until a scan has been completed.
l.Debugln(f, "skip (initial)")
f.pullTimer.Reset(f.sleep)
continue
@@ -297,9 +301,11 @@ func (f *rwFolder) Serve() {
if err != nil {
continue
}
if !initialScanCompleted {
select {
case <-f.initialScanCompleted:
default:
l.Infoln("Completed initial scan (rw) of folder", f.folderID)
initialScanCompleted = true
close(f.initialScanCompleted)
}
case req := <-f.scan.now:
@@ -670,8 +676,9 @@ func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
if dir != nil {
files, _ := dir.Readdirnames(-1)
for _, dirFile := range files {
if defTempNamer.IsTemporary(dirFile) || (matcher != nil && matcher.Match(filepath.Join(file.Name, dirFile)).IsDeletable()) {
osutil.InWritableDir(osutil.Remove, filepath.Join(realName, dirFile))
fullDirFile := filepath.Join(file.Name, dirFile)
if defTempNamer.IsTemporary(dirFile) || (matcher != nil && matcher.Match(fullDirFile).IsDeletable()) {
osutil.RemoveAll(fullDirFile)
}
}
dir.Close()
@@ -1005,6 +1012,7 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
version: curFile.Version,
mut: sync.NewRWMutex(),
sparse: f.allowSparse,
created: time.Now(),
}
l.Debugf("%v need file %s; copy %d, reused %v", f, file.Name, len(blocks), reused)
@@ -1291,8 +1299,9 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
}
}
// Replace the original content with the new one
if err := osutil.Rename(state.tempName, state.realName); err != nil {
// Replace the original content with the new one. If it didn't work,
// leave the temp file in place for reuse.
if err := osutil.TryRename(state.tempName, state.realName); err != nil {
return err
}
@@ -1392,7 +1401,9 @@ func (f *rwFolder) dbUpdaterRoutine() {
lastFile = job.file
}
f.model.updateLocals(f.folderID, files)
// All updates to file/folder objects that originated remotely
// (across the network) use this call to updateLocals
f.model.updateLocalsFromPulling(f.folderID, files)
if found {
f.model.receivedFile(f.folderID, lastFile)

View File

@@ -62,7 +62,7 @@ func setUpModel(file protocol.FileInfo) *Model {
model := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
model.AddFolder(defaultFolderConfig)
// Update index
model.updateLocals("default", []protocol.FileInfo{file})
model.updateLocalsFromScanning("default", []protocol.FileInfo{file})
return model
}
@@ -255,7 +255,7 @@ func TestCopierCleanup(t *testing.T) {
file.Blocks = []protocol.BlockInfo{blocks[1]}
file.Version = file.Version.Update(protocol.LocalDeviceID.Short())
// Update index (removing old blocks)
m.updateLocals("default", []protocol.FileInfo{file})
m.updateLocalsFromScanning("default", []protocol.FileInfo{file})
if m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
t.Error("Unexpected block found")
@@ -268,7 +268,7 @@ func TestCopierCleanup(t *testing.T) {
file.Blocks = []protocol.BlockInfo{blocks[0]}
file.Version = file.Version.Update(protocol.LocalDeviceID.Short())
// Update index (removing old blocks)
m.updateLocals("default", []protocol.FileInfo{file})
m.updateLocalsFromScanning("default", []protocol.FileInfo{file})
if !m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
t.Error("Unexpected block found")

View File

@@ -18,6 +18,7 @@ type sentFolderFileDownloadState struct {
blockIndexes []int32
version protocol.Vector
updated time.Time
created time.Time
}
// sentFolderDownloadState represents a state of what we've announced as available
@@ -41,6 +42,7 @@ func (s *sentFolderDownloadState) update(pullers []*sharedPullerState) []protoco
pullerBlockIndexes := puller.Available()
pullerVersion := puller.file.Version
pullerBlockIndexesUpdated := puller.AvailableUpdated()
pullerCreated := puller.created
localFile, ok := s.files[name]
@@ -52,6 +54,7 @@ func (s *sentFolderDownloadState) update(pullers []*sharedPullerState) []protoco
blockIndexes: pullerBlockIndexes,
updated: pullerBlockIndexesUpdated,
version: pullerVersion,
created: pullerCreated,
}
updates = append(updates, protocol.FileDownloadProgressUpdate{
@@ -70,9 +73,9 @@ func (s *sentFolderDownloadState) update(pullers []*sharedPullerState) []protoco
continue
}
if !pullerVersion.Equal(localFile.version) {
// The version has changed, clean up whatever we had for the old
// file, and advertise the new file.
if !pullerVersion.Equal(localFile.version) || !pullerCreated.Equal(localFile.created) {
// The version has changed or the puller was reconstrcuted due to failure.
// Clean up whatever we had for the old file, and advertise the new file.
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: name,
Version: localFile.version,
@@ -87,6 +90,7 @@ func (s *sentFolderDownloadState) update(pullers []*sharedPullerState) []protoco
localFile.blockIndexes = pullerBlockIndexes
localFile.updated = pullerBlockIndexesUpdated
localFile.version = pullerVersion
localFile.created = pullerCreated
continue
}

View File

@@ -29,6 +29,7 @@ type sharedPullerState struct {
ignorePerms bool
version protocol.Vector // The current (old) version
sparse bool
created time.Time
// Mutable, must be locked for access
err error // The first error we hit

View File

@@ -20,7 +20,7 @@ var (
)
// An AtomicWriter is an *os.File that writes to a temporary file in the same
// directory as the final path. On successfull Close the file is renamed to
// directory as the final path. On successful Close the file is renamed to
// it's final path. Any error on Write or during Close is accumulated and
// returned on Close, so a lazy user can ignore errors until Close.
type AtomicWriter struct {

View File

@@ -1,7 +1,31 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// Modified by Zillode to fix https://github.com/syncthing/syncthing/issues/1822
// Sync with https://github.com/golang/go/blob/master/src/os/path.go
// See https://github.com/golang/go/issues/10900

View File

@@ -15,6 +15,7 @@ import (
"path/filepath"
"runtime"
"strings"
"syscall"
"github.com/calmh/du"
"github.com/syncthing/syncthing/lib/sync"
@@ -107,6 +108,78 @@ func Remove(path string) error {
return os.Remove(path)
}
// RemoveAll is a copy of os.RemoveAll, but uses osutil.Remove.
// RemoveAll removes path and any children it contains.
// It removes everything it can but returns the first error
// it encounters. If the path does not exist, RemoveAll
// returns nil (no error).
func RemoveAll(path string) error {
// Simple case: if Remove works, we're done.
err := Remove(path)
if err == nil || os.IsNotExist(err) {
return nil
}
// Otherwise, is this a directory we need to recurse into?
dir, serr := os.Lstat(path)
if serr != nil {
if serr, ok := serr.(*os.PathError); ok && (os.IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) {
return nil
}
return serr
}
if !dir.IsDir() {
// Not a directory; return the error from Remove.
return err
}
// Directory.
fd, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
// Race. It was deleted between the Lstat and Open.
// Return nil per RemoveAll's docs.
return nil
}
return err
}
// Remove contents & return first error.
err = nil
for {
names, err1 := fd.Readdirnames(100)
for _, name := range names {
err1 := RemoveAll(path + string(os.PathSeparator) + name)
if err == nil {
err = err1
}
}
if err1 == io.EOF {
break
}
// If Readdirnames returned an error, use it.
if err == nil {
err = err1
}
if len(names) == 0 {
break
}
}
// Close directory, because windows won't remove opened directory.
fd.Close()
// Remove directory.
err1 := Remove(path)
if err1 == nil || os.IsNotExist(err1) {
return nil
}
if err == nil {
err = err1
}
return err
}
func ExpandTilde(path string) (string, error) {
if path == "~" {
return getHomeDir()

View File

@@ -14,7 +14,7 @@ import (
)
// TCPPing returns the duration required to establish a TCP connection
// to the given host. ICMP packets require root priviledges, hence why we use
// to the given host. ICMP packets require root privileges, hence why we use
// tcp.
func TCPPing(address string) (time.Duration, error) {
start := time.Now()

View File

@@ -10,7 +10,7 @@ package osutil
import "syscall"
// MaximizeOpenFileLimit tries to set the resoure limit RLIMIT_NOFILE (number
// MaximizeOpenFileLimit tries to set the resource limit RLIMIT_NOFILE (number
// of open file descriptors) to the max (hard limit), if the current (soft
// limit) is below the max. Returns the new (though possibly unchanged) limit,
// or an error if it could not be changed.
@@ -35,7 +35,7 @@ func MaximizeOpenFileLimit() (int, error) {
// If the set succeeded, perform a new get to see what happened. We might
// have gotten a value lower than the one in lim.Max, if lim.Max was
// something that indiciated "unlimited" (i.e. intmax).
// something that indicated "unlimited" (i.e. intmax).
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
// We don't really know the correct value here since Getrlimit
// mysteriously failed after working once... Shouldn't ever happen, I

View File

@@ -13,7 +13,7 @@ import (
"time"
"github.com/AudriusButkevicius/go-nat-pmp"
"github.com/jackpal/gateway"
"github.com/calmh/gateway"
"github.com/syncthing/syncthing/lib/nat"
)

View File

@@ -0,0 +1,188 @@
// Copyright (C) 2016 The Protocol Authors.
package protocol
import (
"crypto/tls"
"encoding/binary"
"net"
"testing"
"github.com/syncthing/syncthing/lib/dialer"
)
func BenchmarkRequestsRawTCP(b *testing.B) {
// Benchmarks the rate at which we can serve requests over a single,
// unencrypted TCP channel over the loopback interface.
// Get a connected TCP pair
conn0, conn1, err := getTCPConnectionPair()
if err != nil {
b.Fatal(err)
}
defer conn0.Close()
defer conn1.Close()
// Bench it
benchmarkRequestsConnPair(b, conn0, conn1)
}
func BenchmarkRequestsTLSoTCP(b *testing.B) {
// Benchmarks the rate at which we can serve requests over a single,
// TLS encrypted TCP channel over the loopback interface.
// Load a certificate, skipping this benchmark if it doesn't exist
cert, err := tls.LoadX509KeyPair("../../test/h1/cert.pem", "../../test/h1/key.pem")
if err != nil {
b.Skip(err)
return
}
// Get a connected TCP pair
conn0, conn1, err := getTCPConnectionPair()
if err != nil {
b.Fatal(err)
}
/// TLSify them
conn0, conn1 = negotiateTLS(cert, conn0, conn1)
defer conn0.Close()
defer conn1.Close()
// Bench it
benchmarkRequestsConnPair(b, conn0, conn1)
}
func benchmarkRequestsConnPair(b *testing.B, conn0, conn1 net.Conn) {
// Start up Connections on them
c0 := NewConnection(LocalDeviceID, conn0, conn0, new(fakeModel), "c0", CompressMetadata)
c0.Start()
c1 := NewConnection(LocalDeviceID, conn1, conn1, new(fakeModel), "c1", CompressMetadata)
c1.Start()
// Satisfy the assertions in the protocol by sending an initial cluster config
c0.ClusterConfig(ClusterConfigMessage{})
c1.ClusterConfig(ClusterConfigMessage{})
// Report some useful stats and reset the timer for the actual test
b.ReportAllocs()
b.SetBytes(128 << 10)
b.ResetTimer()
// Request 128 KiB blocks, which will be satisfied by zero copy from the
// other side (we'll get back a full block of zeroes).
var buf []byte
var err error
for i := 0; i < b.N; i++ {
// Use c0 and c1 for each alternating request, so we get as much
// data flowing in both directions.
if i%2 == 0 {
buf, err = c0.Request("folder", "file", int64(i), 128<<10, nil, false)
} else {
buf, err = c1.Request("folder", "file", int64(i), 128<<10, nil, false)
}
if err != nil {
b.Fatal(err)
}
if len(buf) != 128<<10 {
b.Fatal("Incorrect returned buf length", len(buf), "!=", 128<<10)
}
// The fake model is supposed to tag the end of the buffer with the
// requested offset, so we can verify that we get back data for this
// block correctly.
if binary.BigEndian.Uint64(buf[128<<10-8:]) != uint64(i) {
b.Fatal("Bad data returned")
}
}
}
// returns the two endpoints of a TCP connection over lo0
func getTCPConnectionPair() (net.Conn, net.Conn, error) {
lst, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, nil, err
}
// We run the Accept in the background since it's blocking, and we use
// the channel to make the race thingies happy about writing vs reading
// conn0 and err0.
var conn0 net.Conn
var err0 error
done := make(chan struct{})
go func() {
conn0, err0 = lst.Accept()
close(done)
}()
// Dial the connection
conn1, err := net.Dial("tcp", lst.Addr().String())
if err != nil {
return nil, nil, err
}
// Check any error from accept
<-done
if err0 != nil {
return nil, nil, err0
}
// Set the buffer sizes etc as usual
dialer.SetTCPOptions(conn0.(*net.TCPConn))
dialer.SetTCPOptions(conn1.(*net.TCPConn))
return conn0, conn1, nil
}
func negotiateTLS(cert tls.Certificate, conn0, conn1 net.Conn) (net.Conn, net.Conn) {
cfg := &tls.Config{
Certificates: []tls.Certificate{cert},
NextProtos: []string{"bep/1.0"},
ClientAuth: tls.RequestClientCert,
SessionTicketsDisabled: true,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
},
}
tlsc0 := tls.Server(conn0, cfg)
tlsc1 := tls.Client(conn1, cfg)
return tlsc0, tlsc1
}
// The fake model does nothing much
type fakeModel struct{}
func (m *fakeModel) Index(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
}
func (m *fakeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
}
func (m *fakeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, flags uint32, options []Option, buf []byte) error {
// We write the offset to the end of the buffer, so the receiver
// can verify that it did in fact get some data back over the
// connection.
binary.BigEndian.PutUint64(buf[len(buf)-8:], uint64(offset))
return nil
}
func (m *fakeModel) ClusterConfig(deviceID DeviceID, config ClusterConfigMessage) {
}
func (m *fakeModel) Close(deviceID DeviceID, err error) {
}
func (m *fakeModel) DownloadProgress(deviceID DeviceID, folder string, updates []FileDownloadProgressUpdate, flags uint32, options []Option) {
}

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