Compare commits

...

62 Commits

Author SHA1 Message Date
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
Jakob Borg 459930df09 gui, man: Update docs & translations
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3034
2016-05-04 19:47:08 +00:00
Audrius Butkevicius 674fc566bb lib/connections: Refactor
1. Removes separate relay lists and relay clients/services, just makes it a listen address
2. Easier plugging-in of other transports
3. Allows "hot" disabling and enabling NAT services
4. Allows "hot" listen address changes
5. Changes listen address list with a preferable "default" value just like for discovery
6. Debounces global discovery announcements as external addresses change (which it might alot upon starting)
7. Stops this whole "pick other peers relay by latency". This information is no longer available,
   but I don't think it matters as most of the time other peer only has one relay.
8. Rename ListenAddress to ListenAddresses, as well as in javascript land.
9. Stop serializing deprecated values to JSON

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2982
2016-05-04 19:38:12 +00:00
Jakob Borg 09832abe50 lib/config: Change folder type attribute to a FolderType type
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3032
2016-05-04 11:26:36 +00:00
Audrius Butkevicius eabd2fc936 lib/model: Use factories for creating folders
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3029
2016-05-04 10:47:33 +00:00
Jakob Borg 6720906ee5 lib/ignore: Refactor: notMatched should be one of the constants
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3031
2016-05-04 07:15:56 +00:00
Audrius Butkevicius abb96802cb lib/ignores: Use bitmask for result
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3030
2016-05-01 15:58:23 +00:00
Audrius Butkevicius 29fa05ae05 lib/model: Discard download progress upon receiving an index update (fixes #2993)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3023
2016-05-01 06:49:29 +00:00
Audrius Butkevicius 49387f9494 lib/protocol: Clean up error values, unused flags
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3025
2016-04-30 04:35:38 +00:00
Alex 953482de53 cmd/stvanity: x509.GenerateCertificate requires pointer for public key
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3022
2016-04-28 22:22:33 +00:00
Audrius Butkevicius 8cf3a7aeda lib/model: Prettify tests (fixes #3014)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3016
2016-04-26 20:19:30 +00:00
Jakob Borg b8c5cf1142 lib/model: Refactor: complete renaming of p -> f
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3015
2016-04-26 15:11:19 +00:00
Lars K.W. Gohlke 236f121c4e lib/model: Refactor out folder and folderscan types, simplify somewhat
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3007
2016-04-26 14:01:46 +00:00
Audrius Butkevicius 2467678bd4 lib/dialer: Add env var to disable proxy fallback (fixes #3006)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3009
2016-04-24 16:30:20 +00:00
Lars K.W. Gohlke e87c1abd4e all: Clean up dead code
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3005
2016-04-22 21:15:36 +00:00
Lars K.W. Gohlke dffc34559b lib/config: Remove dead code
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3004
2016-04-22 20:30:58 +00:00
Lars K.W. Gohlke 80f2a9a6bf readme: Remove Appveyor icon
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3003
2016-04-22 20:30:37 +00:00
Audrius Butkevicius 4aa6ecb122 lib/model: Do not use WRONLY (ref #2584)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2994
2016-04-22 08:12:10 +00:00
Jakob Borg ccfcdf7f48 cmd/syncthing: Don't compact database at startup
This happens automatically in the background anyway, and it can take a
long time on low powered devices at an inconvenient time. We just want
to get up and running as quickly as possible.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3000
2016-04-22 07:34:11 +00:00
Jakob Borg 4eb23a38b1 cmd/stvanity: Use Go 1.3 compatible interface
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2998
2016-04-20 08:16:42 +00:00
Jakob Borg cb38213444 build: Remove Appveyor and old CircleCI stuff
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2997
2016-04-20 07:12:01 +00:00
Jakob Borg 842b6111db vendor: Update github.com/gobwas/glob to solve 1.3 build issue
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2996
2016-04-20 07:05:08 +00:00
Audrius Butkevicius ea54525a33 lib/connections: Try not to deadlock (fixes #2987)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2991
2016-04-18 20:25:31 +00:00
Jakob Borg 893cc025f9 cmd/syncthing: Accept ISO-8859-1 and UTF-8 in HTTP BasicAuth header (fixes #2779)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2989
2016-04-18 20:24:38 +00:00
Jakob Borg b81c8d2e1b lib/model: Drop incoming updates for ignored items (fixes #1701)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2975
2016-04-18 18:35:31 +00:00
Jakob Borg 4b07535e86 gui: Downgrade Angular 1.5.3 -> 1.2.9 (fixes #2961)
I haven't been able to figure out the problem, despite a lot of
experimenting with this...

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2988
2016-04-18 12:34:29 +00:00
Jakob Borg 0d2fe320a7 cmd/stvanity: New utility to create vanity device IDs
A potential practical use is to encode a short version of the hostname
at the beginning of the device ID.

For example:

	jb@syno:~/s/g/s/s/c/stvanity $ stvanity abc
	Want 15 bits for prefix "ABC", about 3.3e+04 certs to test (statistically speaking)
	Found ABCFPWS-JKDIFV3-E5IUAQW-DK53WVR-HY7XWBS-56H33GR-CJQI67Q-VGXRMAW
	Saved to cert.pem, key.pem

	jb@syno:~/s/g/s/s/c/stvanity $ stvanity $(hostname)
	Want 20 bits for prefix "SYNO", about 1e+06 certs to test (statistically speaking)
	Trying 554 certs/s, tested 8307 so far in 15s, expect ~32m total time to complete
	Trying 543 certs/s, tested 16277 so far in 30s, expect ~32m total time to complete
	...

The rest is just a matter of patience.

	jb@syno:~/s/g/s/s/c/stvanity $ stvanity syncthing
	Want 50 bits for prefix "SYNCTHI-NG", about 1.1e+15 certs to test (statistically speaking)
	Trying 529 certs/s, tested 7941 so far in 15s, expect ~67443 years total time to complete
	...

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2986
2016-04-17 20:42:26 +00:00
Jakob Borg f294113d01 cmd/stdisco: New utility to debug local discovery
When run without parameters, attempts to listen for local discovery
announcements just like Syncthing, and prints them.

With -send, it also sends fake discovery packets. This can be used on
two or more computers simultaneously to verify that they can see each
other.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2985
2016-04-17 18:47:38 +00:00
Audrius Butkevicius 1c7af1a72e lib/upnp: Fix port order
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2980
2016-04-16 22:44:07 +00:00
Audrius Butkevicius e61f424ade lib/{nat,pmp}: Fix shadowing and nil IPs
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2979
2016-04-16 16:48:07 +00:00
Jakob Borg fa1cfd94d0 lib/versioner: Refactor for testing, speed up test
Test now takes <1 second instead of 100 seconds

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/2978
2016-04-15 14:26:39 +00:00
153 changed files with 13121 additions and 21862 deletions
-1
View File
@@ -1,7 +1,6 @@
# Syncthing
[![Latest Build (Official)](https://img.shields.io/jenkins/s/http/build.syncthing.net/syncthing.svg?style=flat-square&label=unix%20build)](http://build.syncthing.net/job/syncthing/lastBuild/)
[![AppVeyor Build](https://img.shields.io/appveyor/ci/calmh/syncthing/master.svg?style=flat-square&label=windows%20build)](https://ci.appveyor.com/project/calmh/syncthing)
[![API Documentation](https://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](http://godoc.org/github.com/syncthing/syncthing)
[![MPLv2 License](https://img.shields.io/badge/license-MPLv2-blue.svg?style=flat-square)](https://www.mozilla.org/MPL/2.0/)
-12
View File
@@ -1,12 +0,0 @@
version: '{branch}-{build}'
clone_folder: C:\src\github.com\syncthing\syncthing
init:
- go version
environment:
GOPATH: C:\
build_script:
- go run build.go zip
test_script:
- go run build.go test
artifacts:
- path: '*.zip'
-17
View File
@@ -674,13 +674,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 +686,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()
+126
View File
@@ -0,0 +1,126 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"bytes"
"crypto/rand"
"flag"
"log"
"strings"
"time"
"github.com/syncthing/syncthing/lib/beacon"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/protocol"
)
var (
all = false // print all packets, not just first from each device/source
fake = false // send fake packets to lure out other devices faster
mc = "[ff12::8384]:21027"
bc = 21027
)
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 recognizable.
randomPrefix = []byte{148, 223, 23, 4, 148}
// Our random, fake, device ID that we use when sending announcements.
myID = randomDeviceID()
)
func main() {
flag.BoolVar(&all, "all", all, "Print all received announcements (not only first)")
flag.BoolVar(&fake, "fake", fake, "Send fake announcements")
flag.StringVar(&mc, "mc", mc, "IPv6 multicast address")
flag.IntVar(&bc, "bc", bc, "IPv4 broadcast port number")
flag.Parse()
if fake {
log.Println("My ID:", protocol.DeviceIDFromBytes(myID))
}
runbeacon(beacon.NewMulticast(mc), fake)
runbeacon(beacon.NewBroadcast(bc), fake)
select {}
}
func runbeacon(bc beacon.Interface, fake bool) {
go bc.Serve()
go recv(bc)
if fake {
go send(bc)
}
}
// receives and prints discovery announcements
func recv(bc beacon.Interface) {
seen := make(map[string]bool)
for {
data, src := bc.Recv()
var ann discover.Announce
ann.UnmarshalXDR(data)
if bytes.Equal(ann.This.ID, myID) {
// This is one of our own fake packets, don't print it.
continue
}
// Print announcement details for the first packet from a given
// device ID and source address, or if -all was given.
key := string(ann.This.ID) + src.String()
if all || !seen[key] {
log.Printf("Announcement from %v\n", src)
log.Printf(" %v at %s\n", protocol.DeviceIDFromBytes(ann.This.ID), strings.Join(addrStrs(ann.This), ", "))
for _, dev := range ann.Extra {
log.Printf(" %v at %s\n", protocol.DeviceIDFromBytes(dev.ID), strings.Join(addrStrs(dev), ", "))
}
seen[key] = true
}
}
}
// sends fake discovery announcements once every second
func send(bc beacon.Interface) {
ann := discover.Announce{
Magic: discover.AnnouncementMagic,
This: discover.Device{
ID: myID,
Addresses: []discover.Address{
{URL: "tcp://fake.example.com:12345"},
},
},
}
bs, _ := ann.MarshalXDR()
for {
bc.Send(bs)
time.Sleep(time.Second)
}
}
// returns the list of address URLs
func addrStrs(dev discover.Device) []string {
ss := make([]string, len(dev.Addresses))
for i, addr := range dev.Addresses {
ss[i] = addr.URL
}
return ss
}
// returns a random but recognizable device ID
func randomDeviceID() []byte {
var id [32]byte
copy(id[:], randomPrefix)
rand.Read(id[len(randomPrefix):])
return id[:]
}
+6 -10
View File
@@ -49,9 +49,8 @@ func main() {
}
type checkResult struct {
server string
direct []string
relays []discover.Relay
server string
addresses []string
error
}
@@ -76,17 +75,14 @@ func checkServers(deviceID protocol.DeviceID, servers ...string) {
if res.error != nil {
fmt.Println(" " + res.error.Error())
}
for _, addr := range res.direct {
for _, addr := range res.addresses {
fmt.Println(" address:", addr)
}
for _, rel := range res.relays {
fmt.Printf(" relay: %s (%d ms)\n", rel.URL, rel.Latency)
}
}
}
func checkServer(deviceID protocol.DeviceID, server string) checkResult {
disco, err := discover.NewGlobal(server, tls.Certificate{}, nil, nil)
disco, err := discover.NewGlobal(server, tls.Certificate{}, nil)
if err != nil {
return checkResult{error: err}
}
@@ -98,8 +94,8 @@ func checkServer(deviceID protocol.DeviceID, server string) checkResult {
})
go func() {
direct, relays, err := disco.Lookup(deviceID)
res <- checkResult{direct: direct, relays: relays, error: err}
addresses, err := disco.Lookup(deviceID)
res <- checkResult{addresses: addresses, error: err}
}()
return <-res
+13 -6
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)
}
+212
View File
@@ -0,0 +1,212 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"fmt"
"math/big"
mr "math/rand"
"os"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/syncthing/syncthing/lib/protocol"
)
type result struct {
id protocol.DeviceID
priv *ecdsa.PrivateKey
derBytes []byte
}
func main() {
flag.Parse()
prefix := strings.ToUpper(strings.Replace(flag.Arg(0), "-", "", -1))
if len(prefix) > 7 {
prefix = prefix[:7] + "-" + prefix[7:]
}
found := make(chan result)
stop := make(chan struct{})
var count int64
// Print periodic progress reports.
go printProgress(prefix, &count)
// Run one certificate generator per CPU core.
var wg sync.WaitGroup
for i := 0; i < runtime.GOMAXPROCS(-1); i++ {
wg.Add(1)
go func() {
generatePrefixed(prefix, &count, found, stop)
wg.Done()
}()
}
// Save the result, when one has been found.
res := <-found
close(stop)
wg.Wait()
fmt.Println("Found", res.id)
saveCert(res.priv, res.derBytes)
fmt.Println("Saved to cert.pem, key.pem")
}
// Try certificates until one is found that has the prefix at the start of
// the resulting device ID. Increments count atomically, sends the result to
// found, returns when stop is closed.
func generatePrefixed(prefix string, count *int64, found chan<- result, stop <-chan struct{}) {
notBefore := time.Now()
notAfter := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
template := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(mr.Int63()),
Subject: pkix.Name{
CommonName: "syncthing",
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for {
select {
case <-stop:
return
default:
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
id := protocol.NewDeviceID(derBytes)
atomic.AddInt64(count, 1)
if strings.HasPrefix(id.String(), prefix) {
select {
case found <- result{id, priv, derBytes}:
case <-stop:
}
return
}
}
}
func printProgress(prefix string, count *int64) {
started := time.Now()
wantBits := 5 * len(prefix)
if wantBits > 63 {
fmt.Printf("Want %d bits for prefix %q, refusing to boil the ocean.\n", wantBits, prefix)
os.Exit(1)
}
expectedIterations := float64(int(1) << uint(wantBits))
fmt.Printf("Want %d bits for prefix %q, about %.2g certs to test (statistically speaking)\n", wantBits, prefix, expectedIterations)
for _ = range time.NewTicker(15 * time.Second).C {
tried := atomic.LoadInt64(count)
elapsed := time.Since(started)
rate := float64(tried) / elapsed.Seconds()
expected := timeStr(expectedIterations / rate)
fmt.Printf("Trying %.0f certs/s, tested %d so far in %v, expect ~%s total time to complete\n", rate, tried, elapsed/time.Second*time.Second, expected)
}
}
func saveCert(priv interface{}, derBytes []byte) {
certOut, err := os.Create("cert.pem")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = certOut.Close()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
block, err := pemBlockForKey(priv)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = pem.Encode(keyOut, block)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = keyOut.Close()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func pemBlockForKey(priv interface{}) (*pem.Block, error) {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}, nil
case *ecdsa.PrivateKey:
b, err := x509.MarshalECPrivateKey(k)
if err != nil {
return nil, err
}
return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}, nil
default:
return nil, fmt.Errorf("unknown key type")
}
}
func timeStr(seconds float64) string {
if seconds < 60 {
return fmt.Sprintf("%.0fs", seconds)
}
if seconds < 3600 {
return fmt.Sprintf("%.0fm", seconds/60)
}
if seconds < 86400 {
return fmt.Sprintf("%.0fh", seconds/3600)
}
if seconds < 86400*365 {
return fmt.Sprintf("%.0f days", seconds/3600)
}
return fmt.Sprintf("%.0f years", seconds/86400/365)
}
+48 -47
View File
@@ -35,7 +35,6 @@ import (
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/stats"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/tlsutil"
@@ -51,21 +50,21 @@ var (
)
type apiService struct {
id protocol.DeviceID
cfg configIntf
httpsCertFile string
httpsKeyFile string
assetDir string
themes []string
model modelIntf
eventSub events.BufferedSubscription
discoverer discover.CachingMux
relayService relay.Service
fss *folderSummaryService
systemConfigMut sync.Mutex // serializes posts to /rest/system/config
stop chan struct{} // signals intentional stop
configChanged chan struct{} // signals intentional listener close due to config change
started chan struct{} // signals startup complete, for testing only
id protocol.DeviceID
cfg configIntf
httpsCertFile string
httpsKeyFile string
assetDir string
themes []string
model modelIntf
eventSub events.BufferedSubscription
discoverer discover.CachingMux
connectionsService connectionsIntf
fss *folderSummaryService
systemConfigMut sync.Mutex // serializes posts to /rest/system/config
stop chan struct{} // signals intentional stop
configChanged chan struct{} // signals intentional listener close due to config change
started chan struct{} // signals startup complete, for testing only
listener net.Listener
listenerMut sync.Mutex
@@ -113,25 +112,30 @@ type configIntf interface {
Folders() map[string]config.FolderConfiguration
Devices() map[protocol.DeviceID]config.DeviceConfiguration
Save() error
ListenAddresses() []string
}
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, relayService relay.Service, errors, systemLog logger.Recorder) (*apiService, error) {
type connectionsIntf interface {
Status() map[string]interface{}
}
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder) (*apiService, error) {
service := &apiService{
id: id,
cfg: cfg,
httpsCertFile: httpsCertFile,
httpsKeyFile: httpsKeyFile,
assetDir: assetDir,
model: m,
eventSub: eventSub,
discoverer: discoverer,
relayService: relayService,
systemConfigMut: sync.NewMutex(),
stop: make(chan struct{}),
configChanged: make(chan struct{}),
listenerMut: sync.NewMutex(),
guiErrors: errors,
systemLog: systemLog,
id: id,
cfg: cfg,
httpsCertFile: httpsCertFile,
httpsKeyFile: httpsKeyFile,
assetDir: assetDir,
model: m,
eventSub: eventSub,
discoverer: discoverer,
connectionsService: connectionsService,
systemConfigMut: sync.NewMutex(),
stop: make(chan struct{}),
configChanged: make(chan struct{}),
listenerMut: sync.NewMutex(),
guiErrors: errors,
systemLog: systemLog,
}
seen := make(map[string]struct{})
@@ -714,6 +718,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)
@@ -825,18 +830,9 @@ func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
res["discoveryMethods"] = discoMethods
res["discoveryErrors"] = discoErrors
}
if s.relayService != nil {
res["relaysEnabled"] = true
relayClientStatus := make(map[string]bool)
relayClientLatency := make(map[string]int)
for _, relay := range s.relayService.Relays() {
latency, ok := s.relayService.RelayStatus(relay)
relayClientStatus[relay] = ok
relayClientLatency[relay] = int(latency / time.Millisecond)
}
res["relayClientStatus"] = relayClientStatus
res["relayClientLatency"] = relayClientLatency
}
res["connectionServiceStatus"] = s.connectionsService.Status()
cpuUsageLock.RLock()
var cpusum float64
for _, p := range cpuUsagePercent {
@@ -945,10 +941,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
@@ -1210,7 +1211,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 {
+43 -8
View File
@@ -78,19 +78,42 @@ func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguratio
return
}
// Check if the username is correct, assuming it was sent as UTF-8
username := string(fields[0])
if username != cfg.User {
emitLoginAttempt(false, username)
error()
return
if username == cfg.User {
goto usernameOK
}
if err := bcrypt.CompareHashAndPassword([]byte(cfg.Password), fields[1]); err != nil {
emitLoginAttempt(false, username)
error()
return
// ... check it again, converting it from assumed ISO-8859-1 to UTF-8
username = string(iso88591ToUTF8(fields[0]))
if username == cfg.User {
goto usernameOK
}
// Neither of the possible interpretations match the configured username
emitLoginAttempt(false, username)
error()
return
usernameOK:
// Check password as given (assumes UTF-8 encoding)
password := fields[1]
if err := bcrypt.CompareHashAndPassword([]byte(cfg.Password), password); err == nil {
goto passwordOK
}
// ... check it again, converting it from assumed ISO-8859-1 to UTF-8
password = iso88591ToUTF8(password)
if err := bcrypt.CompareHashAndPassword([]byte(cfg.Password), password); err == nil {
goto passwordOK
}
// Neither of the attempts to verify the password checked out
emitLoginAttempt(false, username)
error()
return
passwordOK:
sessionid := util.RandomString(32)
sessionsMut.Lock()
sessions[sessionid] = true
@@ -105,3 +128,15 @@ func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguratio
next.ServeHTTP(w, r)
})
}
// Convert an ISO-8859-1 encoded byte string to UTF-8. Works by the
// principle that ISO-8859-1 bytes are equivalent to unicode code points,
// that a rune slice is a list of code points, and that stringifying a slice
// of runes generates UTF-8 in Go.
func iso88591ToUTF8(s []byte) []byte {
runes := make([]rune, len(s))
for i := range s {
runes[i] = rune(s[i])
}
return []byte(string(runes))
}
+106 -32
View File
@@ -137,13 +137,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,40 +189,11 @@ type httpTestCase struct {
}
func TestAPIServiceRequests(t *testing.T) {
model := new(mockedModel)
cfg := new(mockedConfig)
httpsCertFile := "../../test/h1/https-cert.pem"
httpsKeyFile := "../../test/h1/https-key.pem"
assetDir := "../../gui"
eventSub := new(mockedEventSub)
discoverer := new(mockedCachingMux)
relayService := new(mockedRelayService)
errorLog := new(mockedLoggerRecorder)
systemLog := new(mockedLoggerRecorder)
// Instantiate the API service
svc, err := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
eventSub, discoverer, relayService, errorLog, systemLog)
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
_ = svc
// Make sure the API service is listening, and get the URL to use.
addr := svc.listener.Addr()
if addr == nil {
t.Fatal("Nil listening address from API service")
}
tcpAddr, err := net.ResolveTCPAddr("tcp", addr.String())
if err != nil {
t.Fatal("Weird address from API service:", err)
}
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
// Actually start the API service
supervisor := suture.NewSimple("API test")
supervisor.Add(svc)
supervisor.ServeBackground()
cases := []httpTestCase{
// /rest/db
@@ -417,3 +388,106 @@ func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase) {
return
}
}
func TestHTTPLogin(t *testing.T) {
cfg := new(mockedConfig)
cfg.gui.User = "üser"
cfg.gui.Password = "$2a$10$IdIZTxTg/dCNuNEGlmLynOjqg4B1FvDKuIV5e0BB3pnWVHNb8.GSq" // bcrypt of "räksmörgås" in UTF-8
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal(err)
}
// Verify rejection when not using authorization
req, _ := http.NewRequest("GET", baseURL+"/rest/system/status", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Unexpected non-401 return code %d for unauthed request", resp.StatusCode)
}
// Verify that incorrect password is rejected
req.SetBasicAuth("üser", "rksmrgs")
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Unexpected non-401 return code %d for incorrect password", resp.StatusCode)
}
// Verify that incorrect username is rejected
req.SetBasicAuth("user", "räksmörgås") // string literals in Go source code are in UTF-8
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusUnauthorized {
t.Errorf("Unexpected non-401 return code %d for incorrect username", resp.StatusCode)
}
// Verify that UTF-8 auth works
req.SetBasicAuth("üser", "räksmörgås") // string literals in Go source code are in UTF-8
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected non-200 return code %d for authed request (UTF-8)", resp.StatusCode)
}
// Verify that ISO-8859-1 auth
req.SetBasicAuth("\xfcser", "r\xe4ksm\xf6rg\xe5s") // escaped ISO-8859-1
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Unexpected non-200 return code %d for authed request (ISO-8859-1)", resp.StatusCode)
}
}
func startHTTP(cfg *mockedConfig) (string, error) {
model := new(mockedModel)
httpsCertFile := "../../test/h1/https-cert.pem"
httpsKeyFile := "../../test/h1/https-key.pem"
assetDir := "../../gui"
eventSub := new(mockedEventSub)
discoverer := new(mockedCachingMux)
connections := new(mockedConnections)
errorLog := new(mockedLoggerRecorder)
systemLog := new(mockedLoggerRecorder)
// Instantiate the API service
svc, err := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
eventSub, discoverer, connections, errorLog, systemLog)
if err != nil {
return "", err
}
// Make sure the API service is listening, and get the URL to use.
addr := svc.listener.Addr()
if addr == nil {
return "", fmt.Errorf("Nil listening address from API service")
}
tcpAddr, err := net.ResolveTCPAddr("tcp", addr.String())
if err != nil {
return "", fmt.Errorf("Weird address from API service: %v", err)
}
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
// Actually start the API service
supervisor := suture.NewSimple("API test")
supervisor.Add(svc)
supervisor.ServeBackground()
return baseURL, nil
}
+21 -66
View File
@@ -17,7 +17,6 @@ import (
"net"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"os/signal"
"path/filepath"
@@ -38,19 +37,13 @@ import (
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/logger"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/symlinks"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/util"
// Registers NAT service providers
_ "github.com/syncthing/syncthing/lib/pmp"
_ "github.com/syncthing/syncthing/lib/upnp"
"github.com/thejerf/suture"
)
@@ -662,13 +655,6 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
}
}
// Pack and optimize the database
if err := ldb.Compact(); err != nil {
// I don't think this is fatal, but who knows. If it is, we'll surely
// get an error when trying to write to the db later.
l.Infoln("Compacting database:", err)
}
m := model.NewModel(cfg, myID, myDeviceName(cfg), "syncthing", Version, ldb, protectedFiles)
cfg.Subscribe(m)
@@ -698,51 +684,11 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
}
m.Index(device, folderCfg.ID, nil, 0, nil)
}
// Routine to pull blocks from other devices to synchronize the local
// folder. Does not run when we are in read only (publish only) mode.
if folderCfg.ReadOnly {
m.StartFolderRO(folderCfg.ID)
} else {
m.StartFolderRW(folderCfg.ID)
}
m.StartFolder(folderCfg.ID)
}
mainService.Add(m)
// Start NAT service
var natService *nat.Service
var mappings []*nat.Mapping
if opts.NATEnabled {
natService = nat.NewService(myID, cfg)
for _, addrStr := range opts.ListenAddress {
uri, err := url.Parse(addrStr)
if err != nil {
l.Fatalf("Failed to parse listen address %s: %v", addrStr, err)
}
if uri.Scheme == "tcp" || uri.Scheme == "tcp4" {
addr, err := net.ResolveTCPAddr(uri.Scheme, uri.Host)
if err != nil {
l.Fatalln("Bad listen address:", err)
}
if addr.Port == 0 {
l.Fatalf("Listen address %s: invalid port", uri)
}
mappings = append(mappings, natService.NewMapping(nat.TCP, addr.IP, addr.Port))
}
}
mainService.Add(natService)
}
// Start relay management
var relayService relay.Service
if opts.RelaysEnabled {
relayService = relay.NewService(cfg, tlsCfg)
mainService.Add(relayService)
}
// Start discovery
cachedDiscovery := discover.NewCachingMux()
@@ -750,13 +696,13 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// Start connection management
connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, mappings, relayService, bepProtocolName, tlsDefaultCommonName, lans)
mainService.Add(connectionService)
connectionsService := connections.NewService(cfg, myID, m, tlsCfg, cachedDiscovery, bepProtocolName, tlsDefaultCommonName, lans)
mainService.Add(connectionsService)
if cfg.Options().GlobalAnnEnabled {
for _, srv := range cfg.GlobalDiscoveryServers() {
l.Infoln("Using discovery server", srv)
gd, err := discover.NewGlobal(srv, cert, connectionService, relayService)
gd, err := discover.NewGlobal(srv, cert, connectionsService)
if err != nil {
l.Warnln("Global discovery:", err)
continue
@@ -771,14 +717,14 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
if cfg.Options().LocalAnnEnabled {
// v4 broadcasts
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), connectionService, relayService)
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), connectionsService)
if err != nil {
l.Warnln("IPv4 local discovery:", err)
} else {
cachedDiscovery.Add(bcd, 0, 0, ipv4LocalDiscoveryPriority)
}
// v6 multicasts
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, connectionService, relayService)
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, connectionsService)
if err != nil {
l.Warnln("IPv6 local discovery:", err)
} else {
@@ -788,7 +734,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// GUI
setupGUI(mainService, cfg, m, apiSub, cachedDiscovery, relayService, errors, systemLog, runtimeOptions)
setupGUI(mainService, cfg, m, apiSub, cachedDiscovery, connectionsService, errors, systemLog, runtimeOptions)
if runtimeOptions.cpuProfile {
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
@@ -970,7 +916,7 @@ func startAuditing(mainService *suture.Supervisor) {
l.Infoln("Audit log in", auditFile)
}
func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub events.BufferedSubscription, discoverer discover.CachingMux, relayService relay.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) {
func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService *connections.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) {
guiCfg := cfg.GUI()
if !guiCfg.Enabled {
@@ -981,7 +927,7 @@ func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Mode
l.Warnln("Insecure admin access is enabled.")
}
api, err := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, relayService, errors, systemLog)
api, err := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, connectionsService, errors, systemLog)
if err != nil {
l.Fatalln("Cannot start GUI:", err)
}
@@ -1000,8 +946,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 := util.RandomString(5) + "-" + util.RandomString(5)
defaultFolder = config.NewFolderConfiguration(folderID, locations[locDefFolder])
defaultFolder.Label = "Default Folder (" + folderID + ")"
defaultFolder.RescanIntervalS = 60
defaultFolder.MinDiskFreePct = 1
defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}}
@@ -1030,7 +977,15 @@ func defaultConfig(myName string) config.Configuration {
if err != nil {
l.Fatalln("get free port (BEP):", err)
}
newCfg.Options.ListenAddress = []string{fmt.Sprintf("tcp://0.0.0.0:%d", port)}
if port == 22000 {
newCfg.Options.ListenAddresses = []string{"default"}
} else {
newCfg.Options.ListenAddresses = []string{
fmt.Sprintf("tcp://%s", net.JoinHostPort("0.0.0.0", strconv.Itoa(port))),
"dynamic+https://relays.syncthing.net/endpoint",
}
}
return newCfg
}
+8 -2
View File
@@ -11,10 +11,16 @@ import (
"github.com/syncthing/syncthing/lib/protocol"
)
type mockedConfig struct{}
type mockedConfig struct {
gui config.GUIConfiguration
}
func (c *mockedConfig) GUI() config.GUIConfiguration {
return config.GUIConfiguration{}
return c.gui
}
func (c *mockedConfig) ListenAddresses() []string {
return nil
}
func (c *mockedConfig) Raw() config.Configuration {
+13
View File
@@ -0,0 +1,13 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package main
type mockedConnections struct{}
func (m *mockedConnections) Status() map[string]interface{} {
return nil
}
+2 -2
View File
@@ -26,8 +26,8 @@ func (m *mockedCachingMux) Stop() {
// from events.Finder
func (m *mockedCachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []discover.Relay, err error) {
return nil, nil, nil
func (m *mockedCachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, err error) {
return nil, nil
}
func (m *mockedCachingMux) Error() error {
-37
View File
@@ -1,37 +0,0 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"crypto/tls"
"time"
)
type mockedRelayService struct{}
// from suture.Service
func (s *mockedRelayService) Serve() {
select {}
}
func (s *mockedRelayService) Stop() {
}
// from relay.Service
func (s *mockedRelayService) Accept() *tls.Conn {
return nil
}
func (s *mockedRelayService) Relays() []string {
return nil
}
func (s *mockedRelayService) RelayStatus(uri string) (time.Duration, bool) {
return 0, false
}
+7 -6
View File
@@ -16,6 +16,7 @@ import (
"net/http"
"runtime"
"sort"
"strings"
"time"
"github.com/syncthing/syncthing/lib/config"
@@ -133,7 +134,7 @@ func reportData(cfg configIntf, m modelIntf) map[string]interface{} {
for _, cfg := range cfg.Folders() {
rescanIntvs = append(rescanIntvs, cfg.RescanIntervalS)
if cfg.ReadOnly {
if cfg.Type == config.FolderTypeReadOnly {
folderUses["readonly"]++
}
if cfg.IgnorePerms {
@@ -203,16 +204,16 @@ func reportData(cfg configIntf, m modelIntf) map[string]interface{} {
}
defaultRelayServers, otherRelayServers := 0, 0
for _, addr := range cfg.Options().RelayServers {
switch addr {
case "dynamic+https://relays.syncthing.net/endpoint":
for _, addr := range cfg.ListenAddresses() {
switch {
case addr == "dynamic+https://relays.syncthing.net/endpoint":
defaultRelayServers++
default:
case strings.HasPrefix(addr, "relay://") || strings.HasPrefix(addr, "dynamic+http"):
otherRelayServers++
}
}
res["relays"] = map[string]interface{}{
"enabled": cfg.Options().RelaysEnabled,
"enabled": defaultRelayServers+otherAnnounceServers > 0,
"defaultServers": defaultRelayServers,
"otherServers": otherRelayServers,
}
+5 -11
View File
@@ -8,7 +8,6 @@ package main
import (
"fmt"
"strings"
"github.com/syncthing/syncthing/lib/events"
)
@@ -147,17 +146,12 @@ func (s *verboseService) formatEvent(ev events.Event) string {
data := ev.Data.(map[string]string)
device := data["device"]
return fmt.Sprintf("Device %v was resumed", device)
case events.ExternalPortMappingChanged:
case events.ListenAddressesChanged:
data := ev.Data.(map[string]interface{})
protocol := data["protocol"]
local := data["local"]
added := data["added"]
removed := data["removed"]
return fmt.Sprintf("External port mapping changed; protocol: %s, local: %s, added: %s, removed: %s", protocol, local, added, removed)
case events.RelayStateChanged:
data := ev.Data.(map[string][]string)
newRelays := data["new"]
return fmt.Sprintf("Relay state changed; connected relay(s) are %s.", strings.Join(newRelays, ", "))
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)
+1 -1
View File
@@ -195,7 +195,7 @@ code.ng-binding{
}
/* progess bars */
/* progress bars */
.progress-bar {
background-color: #217dbb !important;
}
+13 -12
View File
@@ -8,13 +8,13 @@
"Add": "Добави",
"Add Device": "Добави устройство",
"Add Folder": "Добави папка",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Добави отдалечено устройство",
"Add new folder?": "Добави нова папка?",
"Address": "Адрес",
"Addresses": "Адреси",
"Advanced": "Допълнителни",
"Advanced Configuration": "Допълнителни настройки",
"Advanced settings": "Advanced settings",
"Advanced settings": "Допълнителни настройки",
"All Data": "Всички данни",
"Allow Anonymous Usage Reporting?": "Разреши анонимно докладване за употребата на програмата?",
"Alphabetic": "Азбучен ред",
@@ -34,12 +34,12 @@
"Connection Error": "Грешка при свързването",
"Copied from elsewhere": "Копиране от някъде другаде",
"Copied from original": "Копиран от оригинала",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Всички правата запазени © 2014-2016 Сътрудници:",
"Copyright © 2015 the following Contributors:": "Всички правата запазени © 2015 Сътрудници:",
"Danger!": "Опасност!",
"Delete": "Изтрий",
"Deleted": "Изтрито",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство \"{{name}}\" ({{device}}) на {{address}} желае да се свърже. Добави ново устройство?",
"Device ID": "Идентификатор на устройство",
"Device Identification": "Идентификатор на устройство",
"Device Name": "Име на устройство",
@@ -55,6 +55,7 @@
"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\", за да автоматично откриване на наличните адреси.",
@@ -70,7 +71,7 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Защитава файловете от промени направени на други устройства, но промените направени на това устройство ще бъдат синхронизирани с останалите устройства.",
"Folder": "Папка",
"Folder ID": "Идентификатор на папката",
"Folder Label": "Folder Label",
"Folder Label": "Етикет на папката",
"Folder Master": "Главна папка",
"Folder Path": "Път до папката",
"Folders": "Папки",
@@ -116,7 +117,7 @@
"OK": "ОК",
"Off": "Изключено",
"Oldest First": "Първо най-старите",
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
"Optional descriptive label for the folder. Can be different on each device.": "Допълнително разяснеие за етикета на папката. Може да бъде различно всяко устройство.",
"Options": "Настройки",
"Out of Sync": "Несинхронизирано",
"Out of Sync Items": "Несинхронизирани елементи",
@@ -138,9 +139,9 @@
"Relayed via": "Препратено през",
"Relays": "Препращачи",
"Release Notes": "Бележки по обновяването",
"Remote Devices": "Remote Devices",
"Remote Devices": "Отделечени устройства",
"Remove": "Премахни",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Задължителен идентификатор за тази папка. Трябва да бъде един и същ на всички устройства.",
"Rescan": "Сканирай повторно",
"Rescan All": "Сканирай повторно всички",
"Rescan Interval": "Интервал за повторно сканиране",
@@ -211,7 +212,7 @@
"The rate limit must be a non-negative number (0: no limit)": "Ограничението на скоростта трябва да бъде положително число (0: неограничено)",
"The rescan interval must be a non-negative number of seconds.": "Интервала на сканиране трябва да бъде не отрицателно число в секунди.",
"They are retried automatically and will be synced when the error is resolved.": "Ще бъдат спрени и автоматично синхронизирани, когато грешката бъде оправена.",
"This Device": "This Device",
"This Device": "Това устройство",
"This can easily give hackers access to read and change any files on your computer.": "Това дава лесен достъп на хакери да разглеждат и променят всякакви файлове на компютъра Ви.",
"This is a major version upgrade.": "Това е нова основна версия.",
"Trash Can File Versioning": "Само на файловете в кошчето",
@@ -229,7 +230,7 @@
"Version": "Версия",
"Versions Path": "Път до версиите",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версиите биват изтривани автоматично, когато са по-стари от максималната възраст или надминават броя файлове разрешени в даден интервал.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Внимание, това е вътрешна папка на вече съществуваща папка \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Когато добавяш ново устройство помни, че твоето устройство също трябва да бъде добавено от другата страна.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Когато добавяш нов идентификатор на папка помни, че той се използва за свързване на папките на различни устройства. Главни/малки букви са от значение и трябва да са еднакви на всички устройства.",
"Yes": "Да",
@@ -238,6 +239,6 @@
"full documentation": "пълна документация",
"items": "елемента",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} желае да сподели папка \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} желае е да сподели папка \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} желае да сподели папка \"{{folderlabel}}\" ({{folder}})."
}
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Modificar dispositiu",
"Edit Folder": "Modificar carpeta",
"Editing": "Modificant",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable UPnP": "Habilitat UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introdueix adreces separades per comes (\"tcp://ip:port\", \"tcp://host:port\") o \"dinàmic\" per realitzar descobriments automàtics de l'adreça.",
+20 -19
View File
@@ -1,5 +1,5 @@
{
"A device with that ID is already added.": "A device with that ID is already added.",
"A device with that ID is already added.": "Un dispositiu amb eixa ID ja s'ha afegit.",
"A negative number of days doesn't make sense.": "Un nombre negatiu de dies no té sentit.",
"A new major version may not be compatible with previous versions.": "Una nova versión amb canvis importants pot no ser compatible amb versions prèvies.",
"API Key": "Clau API",
@@ -8,13 +8,13 @@
"Add": "Afegir",
"Add Device": "Afegir dispositiu",
"Add Folder": "Afegir carpeta",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Afegir Dispositiu Remot.",
"Add new folder?": "Afegir nova carpeta?",
"Address": "Direcció",
"Addresses": "Direccions",
"Advanced": "Avançat",
"Advanced Configuration": "Configuració avançada",
"Advanced settings": "Advanced settings",
"Advanced settings": "Ajustos avançats.",
"All Data": "Totes les dades",
"Allow Anonymous Usage Reporting?": "Permetre informes d'ús anònim?",
"Alphabetic": "Alfabètic",
@@ -34,12 +34,12 @@
"Connection Error": "Error de connexió",
"Copied from elsewhere": "Copiat de qualsevol lloc",
"Copied from original": "Copiat de l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 els següents Col·laboradors:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 els següents Col·laboradors:",
"Danger!": "Perill!",
"Delete": "Esborrar",
"Deleted": "Esborrat",
"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?": "Dispositiu \"{{name}}\" ({{device}} a l'adreça {{address}}) vol connectar. Afegir nou dispositiu?",
"Device ID": "ID del dispositiu",
"Device Identification": "Identificació del dispositiu",
"Device Name": "Nom del dispositiu",
@@ -55,7 +55,8 @@
"Edit Device": "Editar dispositiu",
"Edit Folder": "Editar carpeta",
"Editing": "Editant",
"Enable Relaying": "Enable Relaying",
"Enable NAT traversal": "Permetre NAT transversal",
"Enable Relaying": "Permetre Transmissions",
"Enable UPnP": "Activar UPnp",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introdueix adreces separades per coma (\"tcp://ip:port\", \"tcp://host:port\") o \"dynamic\" per a realitzar el descobriment automàtic de l'adreça.",
"Enter ignore patterns, one per line.": "Introduïr patrons a ignorar, un per línia.",
@@ -70,7 +71,7 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Els fitxers són protegits dels canvis fets en altres dispositius, però els canvis fets en aquest dispositiu seràn enviats a la resta del grup (cluster).",
"Folder": "Carpeta",
"Folder ID": "ID de carpeta",
"Folder Label": "Folder Label",
"Folder Label": "Etiqueta de la Carpeta",
"Folder Master": "Carpeta principal",
"Folder Path": "Ruta de la carpeta",
"Folders": "Carpetes",
@@ -80,8 +81,8 @@
"GUI Listen Addresses": "Direcció d'escolta de l'Interfície Gràfica d'Usuari (GUI)",
"Generate": "Generar",
"Global Discovery": "Descobriment global",
"Global Discovery Server": "Servidor de descobriment global",
"Global Discovery Servers": "Global Discovery Servers",
"Global Discovery Server": "Servidor de Descobriment Global",
"Global Discovery Servers": "Servidors de Descobriment Global",
"Global State": "Estat global",
"Help": "Ajuda",
"Home page": "Pàgina inicial",
@@ -116,7 +117,7 @@
"OK": "OK",
"Off": "Off",
"Oldest First": "El més vell primer",
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
"Optional descriptive label for the folder. Can be different on each device.": "Etiqueta descriptiva opcional per la carpeta. Pot ser diferent en cada dispositiu.",
"Options": "Opcions",
"Out of Sync": "Sense sincronització",
"Out of Sync Items": "Dispositius sense sincronitzar",
@@ -134,13 +135,13 @@
"Quick guide to supported patterns": "Guía ràpida de patrons suportats",
"RAM Utilization": "Utilització de la RAM",
"Random": "Aleatori",
"Relay Servers": "Relay Servers",
"Relay Servers": "Servidors de Transmissió",
"Relayed via": "Transmitit via",
"Relays": "Transmissions",
"Release Notes": "Notes de la versió",
"Remote Devices": "Remote Devices",
"Remote Devices": "Dispositius Remots",
"Remove": "Eliminar",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificador necessari per la carpeta. Deu ser el mateix en tots els dispositius del cluster.",
"Rescan": "Tornar a buscar",
"Rescan All": "Tornar a buscar tot",
"Rescan Interval": "Interval de nova busca",
@@ -163,7 +164,7 @@
"Shared With": "Compartit amb",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador curt per a la carpeta. Deu ser el mateix en tots els dispositius del grup (cluster).",
"Show ID": "Mostrar ID",
"Show QR": "Show QR",
"Show QR": "Mostrar QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrat en lloc de l'ID del dispositiu en l'estat del grup (cluster). S'anunciarà als altres dispositius com el nom opcional per defecte.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrat en lloc de l'ID del dispositiu en l'estat del grup (cluster). S'actualitzarà al nom que el dispositiu anuncia si es deixa buit.",
"Shutdown": "Apagar",
@@ -189,7 +190,7 @@
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan disponibles públicament en {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració ha sigut gravada però no activada. Syncthing deu reiniciar per tal d'activar la nova configuració.",
"The device ID cannot be blank.": "L'ID del dispositiu no pot estar buida.",
"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).": "L'ID del dispositiu que hi ha que introduïr ací es pot trobar en el menú \"Accions > Mostrar ID\" en l'altre dispositiu. Els espais i les barres son opcionals (ignorats).",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID del dispositiu que hi ha que introduïr ací es pot trobar en el menú \"Editar > Mostrar ID\" en l'altre dispositiu. Els espais i les barres son opcionals (ignorats).",
"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.": "L'informe encriptat d'ús s'envia diariament. S'utilitza per a rastrejar plataformes comuns, tamanys de carpetes i versions de l'aplicació. Si el conjunt de dades enviat a l'informe es canvia, se li demanarà a vosté l'autorització altra vegada.\n",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID del dispositiu introduïda no pareix vàlida. Deuria ser una cadena de 52 o 56 caracters consistents en lletres i nombre, amb espais i barres opcionals.",
@@ -211,7 +212,7 @@
"The rate limit must be a non-negative number (0: no limit)": "El llímit del ritme deu ser un nombre no negatiu (0: sense llímit)",
"The rescan interval must be a non-negative number of seconds.": "L'interval de reescaneig deu ser un nombre positiu de segons.",
"They are retried automatically and will be synced when the error is resolved.": "Es reintenta automàticament i es sincronitzaràn quant el resolga l'error.",
"This Device": "This Device",
"This Device": "Aquest Dispositiu",
"This can easily give hackers access to read and change any files on your computer.": "Açò pot donar accés fàcilment als hackers per a llegir i canviar qualsevol fitxer al teu ordinador.",
"This is a major version upgrade.": "Aquesta és una actualització important de la versió.",
"Trash Can File Versioning": "Versionat d'arxius de la paperera",
@@ -229,7 +230,7 @@
"Version": "Versió",
"Versions Path": "Ruta de les versions",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions s'esborren automàticament si són més antigues que l'edat màxima o excedixen el nombre de fitxer permesos en un 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%}\".": "Perill! Aquesta ruta és un subdirectori d'una carpeta que ja existeix nomenada \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quant s'afig un nou dispositiu, hi ha que tindre en compte que aquest dispositiu deu ser afegit també en l'altre costat.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quant s'afig una nova carpeta, hi ha que tindre en compte que l'ID de la carpeta s'utilitza per a juntar les carpetes entre dispositius. Són sensibles a les majúscules i deuen coincidir exactament entre tots els dispositius.",
"Yes": "Sí",
@@ -238,6 +239,6 @@
"full documentation": "Documentació completa",
"items": "Elements",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartit la carpeta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vol compartir la carpeta \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vol compartir la carpeta \"{{folderlabel}}\" ({{folder}})."
}
+3 -2
View File
@@ -55,6 +55,7 @@
"Edit Device": "Upravit přístroj",
"Edit Folder": "Upravit adresář",
"Editing": "Upravuje se",
"Enable NAT traversal": "Povolit NAT přenos",
"Enable Relaying": "Povolit přenašeče",
"Enable UPnP": "Povolit UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Zadejte adresy oddělené čárkou (\"tcp://ip:port\", \"tcp://host:port\") nebo \"dynamic\" pro automatické zjišťování adres.",
@@ -116,7 +117,7 @@
"OK": "OK",
"Off": "Vypnuta",
"Oldest First": "Od nejstaršího",
"Optional descriptive label for the folder. Can be different on each device.": "Toto zařízení",
"Optional descriptive label for the folder. Can be different on each device.": "Volitelný popisek adresáře. Může být rozdílný na každém zařízení.",
"Options": "Nastavení",
"Out of Sync": "Nesesynchronizováno",
"Out of Sync Items": "Nesesynchronizované položky",
@@ -239,5 +240,5 @@
"items": "položky",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce sdílet adresář \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} chce sdílet adresář \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} chce sdílet adresář \"{{folderlabel}}\" ({{folder}})."
}
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Rediger enhed",
"Edit Folder": "Rediger mappe",
"Editing": "Redigerer",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable UPnP": "Anvend UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv kommaseparerede adresser (\"tcp://ip:port\", \"tcp://host:port\") eller \"dynamic\" for at benytte automatisk opdagelse af adressen.",
+3 -2
View File
@@ -55,6 +55,7 @@
"Edit Device": "Gerät bearbeiten",
"Edit Folder": "Verzeichnis bearbeiten",
"Editing": "Bearbeitet",
"Enable NAT traversal": "NAT-Traversal aktivieren",
"Enable Relaying": "Weiterleitung aktivieren",
"Enable UPnP": "UPnP aktivieren",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Kommagetrennte Adressen (\"tcp://ip:port\", \"tcp://host:port\") oder \"dynamic\" eingeben, um die Adresse automatisch zu ermitteln.",
@@ -65,7 +66,7 @@
"File Pull Order": "Dateiübertragungsreihenfolge",
"File Versioning": "Dateiversionierung",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Dateizugriffsrechte beim Suchen nach Veränderungen ignorieren. Bei FAT-Dateisystemen zu verwenden.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Wenn Dateien von Syncthing ersetzt oder gelöscht werden sollen, werden sie vorher in den .stversions Ordner verschoben.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Wenn Syncthing Dateien ersetzt oder löscht, werden sie in das .stversions Verzeichnis verschoben.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dateien werden, bevor Syncthing sie löscht oder ersetzt, datiert in das Verzeichnis .stversions verschoben.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dateien sind auf diesem Gerät schreibgeschützt. Auf diesem Gerät durchgeführte Veränderungen werden aber auf den Rest des Verbunds übertragen.",
"Folder": "Verzeichnis",
@@ -122,7 +123,7 @@
"Out of Sync Items": "Nicht synchronisierte Objekte",
"Outgoing Rate Limit (KiB/s)": "Limit Datenrate (ausgehend) (KB/s)",
"Override Changes": "Änderungen überschreiben",
"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": "Pfad zum Verzeichnis auf dem lokalen Gerät. Ordner werden erzeugt, wenn sie nicht existieren. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
"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": "Pfad zum Verzeichnis auf dem lokalen Gerät. Verzeichnis wird erzeugt, wenn es nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Pfad in dem alte Dateiversionen gespeichert werden sollen (ohne Angabe wird das Verzeichnis .stversions im Verzeichnis verwendet).",
"Pause": "Pause",
"Paused": "Pausiert",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Επεξεργασία συσκευής",
"Edit Folder": "Επεξεργασία φακέλου",
"Editing": "Επεξεργασία σε εξέλιξη",
"Enable NAT traversal": "Enable 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.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Editing",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable UPnP": "Enable UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
+14 -13
View File
@@ -8,13 +8,13 @@
"Add": "Agregar",
"Add Device": "Agregar Dispositivo",
"Add Folder": "Agregar Carpeta",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Añadir Dispositivo Remoto",
"Add new folder?": "¿Agregar una carpeta nueva?",
"Address": "Dirección",
"Addresses": "Direcciones",
"Advanced": "Avanzado",
"Advanced Configuration": "Configuración Avanzada",
"Advanced settings": "Advanced settings",
"Advanced settings": "Ajustes avanzados",
"All Data": "Todos los datos",
"Allow Anonymous Usage Reporting?": "¿Deseas permitir el envío anónimo de informes de uso?",
"Alphabetic": "Alfabético",
@@ -34,12 +34,12 @@
"Connection Error": "Error de conexión",
"Copied from elsewhere": "Copiado de otro sitio",
"Copied from original": "Copiado del original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes Colaboradores:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 los siguientes Colaboradores:",
"Danger!": "¡Peligro!",
"Delete": "Eliminar",
"Deleted": "Eliminado",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "El dispositivo \"{{name}}\" ({{device}} en la dirección {{address}}) quiere conectarse. Añadir nuevo dispositivo?",
"Device ID": "ID del Dispositivo",
"Device Identification": "Identificación del Dispositivo",
"Device Name": "Nombre del Dispositivo",
@@ -55,6 +55,7 @@
"Edit Device": "Editar dispositivo",
"Edit Folder": "Editar repositorio",
"Editing": "Editando",
"Enable NAT traversal": "Permitir NAT transversal",
"Enable Relaying": "Habilitar Retransmisión",
"Enable UPnP": "Habilitar UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduzca las direcciones, separadas por comas (\"tcp://ip:port\", \"tcp://host:port\"), o \"dynamic\" para llevar a cabo el descubrimiento automático de la dirección.",
@@ -70,7 +71,7 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Los ficheros son protegidos por los cambios hechos en otros dispositivos, pero los cambios hechos en este dispositivo serán enviados al resto del grupo (cluster).",
"Folder": "Carpeta",
"Folder ID": "ID de carpeta",
"Folder Label": "Folder Label",
"Folder Label": "Etiqueta de la Carpeta",
"Folder Master": "Carpeta principal",
"Folder Path": "Ruta de la carpeta",
"Folders": "Carpetas",
@@ -116,7 +117,7 @@
"OK": "OK",
"Off": "Desconectar",
"Oldest First": "El más antiguo primero",
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
"Optional descriptive label for the folder. Can be different on each device.": "Etiqueta descriptiva opcional para la carpeta. Puede ser diferente en cada dispositivo.",
"Options": "Opciones",
"Out of Sync": "No sincronizado",
"Out of Sync Items": "Elementos no sincronizados",
@@ -138,9 +139,9 @@
"Relayed via": "Respaldada a través",
"Relays": "Respaldos",
"Release Notes": "Notas de la versión",
"Remote Devices": "Remote Devices",
"Remote Devices": "Dispositivos Remotos",
"Remove": "Eliminar",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificador requerido para la carpeta. Debe ser el mismo en todos los dispositivos del clúster.",
"Rescan": "Volver a analizar",
"Rescan All": "Volver a analizar Todo",
"Rescan Interval": "Intervalo de análisis",
@@ -189,7 +190,7 @@
"The aggregated statistics are publicly available at {%url%}.": "Las estadísticas agregadas están disponibles públicamente en {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido grabada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
"The device ID cannot be blank.": "La ID del dispositivo no puede estar vacía.",
"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).": "El ID del dispositivo que hay que introducir aquí se puede encontrar en el diálogo \"Acciones > Mostrar ID\" en el otro dispositivo. Los espacios y las barras son opcionales (ignorados).",
"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).": "La ID del dispositivo que hay que introducir aquí puede encontrarse en el menú \"Editar > Mostrar ID\" en el otro dispositivo. Los espacios y barras son opcionales (ignorados).",
"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.": "El informe encriptado de uso se envía diariamente. Se usa para rastrear plataformas comunes, tamaños de carpetas y versiones de la aplicación. Si el conjunto de datos enviados en el informes se cambia, se le pedirá a usted autorización de nuevo.",
"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.": "La ID del dispositivo introducida no parece válida. Debe ser una cadena de 52 ó 56 caracteres formada por letras y números, con espacios y guiones opcionales.",
@@ -211,7 +212,7 @@
"The rate limit must be a non-negative number (0: no limit)": "El límite de velocidad debe ser un número no negativo (0: sin límite)",
"The rescan interval must be a non-negative number of seconds.": "El intervalo de actualización debe ser un número positivo de segundos.",
"They are retried automatically and will be synced when the error is resolved.": "Se reintentarán de forma automática y se sincronizarán cuando se resuelva el error.",
"This Device": "This Device",
"This Device": "Este Dispositivo",
"This can easily give hackers access to read and change any files on your computer.": "Esto podría permitir fácilmente el acceso a hackers para leer y modificar cualquier fichero de tu equipo.",
"This is a major version upgrade.": "Hay una actualización importante.",
"Trash Can File Versioning": "Versionado de archivos de la papelera",
@@ -229,7 +230,7 @@
"Version": "Versión",
"Versions Path": "Ruta de las versiones",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Las versiones se borran automáticamente si son más antiguas que la edad máxima o exceden el número de ficheros permitidos en un intervalo.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Peligro! Esta ruta es un subdirectorio de una carpeta ya existente llamada \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Cuando añada un nuevo dispositivo, tenga en cuenta que este debe añadirse también en el otro lado.",
"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.": "Cuando añada una nueva carpeta, tenga en cuenta que su ID se usa para unir carpetas entre dispositivos. Son sensibles a las mayúsculas y deben coincidir exactamente entre todos los dispositivos.",
"Yes": "Si",
@@ -238,6 +239,6 @@
"full documentation": "Documentación completa",
"items": "Elementos",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta \"{{folderlabel}}\" ({{folder}})."
}
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Editar dispositivo",
"Edit Folder": "Editar repositorio",
"Editing": "Editando",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable UPnP": "Permitir 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.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Muokkaa laitetta",
"Edit Folder": "Muokkaa kansiota",
"Editing": "Muokkaus",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable UPnP": "Ota UPnP käyttöön",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Syötä osoitteet pilkuilla erotettuina (\"tcp://ip:portti, tcp://nimi:portti\") tai \"dynamic\" käyttääksesi osoitteen automaattista selvitystä.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Éditer le périphérique",
"Edit Folder": "Éditer le répertoire",
"Editing": "Édition",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable UPnP": "Activer l'UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://host:port\") séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
+3 -2
View File
@@ -55,6 +55,7 @@
"Edit Device": "Éditer la machine",
"Edit Folder": "Éditer le dossier",
"Editing": "Édition",
"Enable NAT traversal": "Activer le transfert NAT",
"Enable Relaying": "Activer le relayage",
"Enable UPnP": "Activer l'UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://host:port\") séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
@@ -151,7 +152,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",
@@ -178,7 +179,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.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Apparaat bewurkje",
"Edit Folder": "Map bewurkje",
"Editing": "Bewurkjen",
"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.",
+2 -1
View File
@@ -21,7 +21,7 @@
"An external command handles the versioning. It has to remove the file from the synced folder.": "Külső program kezeli a fájl verziókövetést. A fájlt el kell távolítania a szinkronizált mappából.",
"Anonymous Usage Reporting": "Névtelen felhasználási adatok küldése",
"Any devices configured on an introducer device will be added to this device as well.": "A bevezető eszközön beállított minden eszköz hozzá lesz adva ehhez az eszközhöz is.",
"Automatic upgrades": "Automatikus frissítés",
"Automatic upgrades": "Automatikus frissítések",
"Be careful!": "Légy óvatos!",
"Bugs": "Hibák",
"CPU Utilization": "Processzor használat",
@@ -55,6 +55,7 @@
"Edit Device": "Eszköz szerkesztése",
"Edit Folder": "Mappa szerkesztése",
"Editing": "Szerkesztés",
"Enable NAT traversal": "NAT bejárás engedélyezése",
"Enable Relaying": "Közvetítés engedélyezése",
"Enable UPnP": "UPnP engedélyezése",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Vesszővel elválasztva több cím is bevihető (\"tcp://ip:port\", \"tcp://host:port\"), az automatikus felderítéshez a 'dynamic' kulcsszó használatos. ",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Sunting Perangkat",
"Edit Folder": "Sunting Folder",
"Editing": "Menyunting",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Aktifkan Relay",
"Enable UPnP": "Aktifkan UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Masukkan alamat, pisahkan dengan koma (\"tcp://ip:port\", \"tcp://host:port\") atau \"dynamic\" untuk menjalankan penemuan otomatis alamat tersebut.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Modifica Dispositivo",
"Edit Folder": "Modifica Cartella",
"Editing": "Modifica di",
"Enable NAT traversal": "Abilita NAT trasversale",
"Enable Relaying": "Abilita relaying",
"Enable UPnP": "Attiva UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserisci indirizzi separati da virgola (\"tcp://ip:porta\", \"tcp://host:porta\") oppure \"dynamic\" per effettuare il rilevamento automatico dell'indirizzo.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "デバイスの編集",
"Edit Folder": "フォルダーの編集",
"Editing": "編集中",
"Enable NAT traversal": "NATトラバーサルを有効にする",
"Enable Relaying": "中継サーバー経由の通信を有効にする",
"Enable UPnP": "UPnPを有効にする",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "アドレスを指定する場合は「tcp://IPアドレス:ポート」または「tcp://ホスト名:ポート」をコンマで区切って入力してください。自動探索を行う場合は「dynamic」と入力してください。",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "기기 편집",
"Edit Folder": "폴더 편집",
"Editing": "편집",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable UPnP": "UPnP 활성화",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
+4 -3
View File
@@ -16,10 +16,10 @@
"Advanced Configuration": "Išplėstinė konfigūracija",
"Advanced settings": "Išplėstiniai nustatymai",
"All Data": "Visiems duomenims",
"Allow Anonymous Usage Reporting?": "Siųsti anonimišką vartojimo ataskaitą?",
"Allow Anonymous Usage Reporting?": "Siųsti anoniminę naudojimo ataskaitą?",
"Alphabetic": "Abėcėlės tvarka",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Išorinė komanda apdoroja versijų valdymą. Ji turi pašalinti failą iš sinchronizuoto aplanko.",
"Anonymous Usage Reporting": "Anoniminė vartojimo ataskaita",
"Anonymous Usage Reporting": "Anoniminė naudojimo ataskaita",
"Any devices configured on an introducer device will be added to this device as well.": "Visi supažindintojo įrenginiai bus pridėti prie jūsų įrenginių sąrašo.",
"Automatic upgrades": "Automatiniai atnaujinimai",
"Be careful!": "Būkite atsargūs!",
@@ -27,7 +27,7 @@
"CPU Utilization": "Procesoriaus panaudojimas",
"Changelog": "Pasikeitimai",
"Clean out after": "Išvalyti po",
"Close": "Uždaryti",
"Close": "Užverti",
"Command": "Komanda",
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
"Compression": "Kompresija",
@@ -55,6 +55,7 @@
"Edit Device": "Keisti įrenginį",
"Edit Folder": "Keisti aplanką",
"Editing": "Redagavimas",
"Enable NAT traversal": "Leisti kirsti NAT",
"Enable Relaying": "Įjungti retransliavimą",
"Enable UPnP": "Įjungti UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Įveskite kableliais atskirtus (\"tcp://ip:prievadas\", \"tcp://serveris:prievadas\") adresus arba \"dynamic\", kad atliktumėte automatinį adresų aptikimą.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Rediger Enhet",
"Edit Folder": "Rediger Mappe",
"Editing": "Redigerer",
"Enable NAT traversal": "Enable NAT traversal",
"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.",
+14 -13
View File
@@ -8,13 +8,13 @@
"Add": "Toevoegen",
"Add Device": "Apparaat toevoegen",
"Add Folder": "Map toevoegen",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Voeg extern apparaat toe",
"Add new folder?": "Voeg nieuwe map toe?",
"Address": "Adres",
"Addresses": "Adressen",
"Advanced": "Geavanceerd",
"Advanced Configuration": "Geavanceerde configuratie",
"Advanced settings": "Advanced settings",
"Advanced settings": "Geavanceerde instellingen",
"All Data": "Alle gegevens",
"Allow Anonymous Usage Reporting?": "Versturen van anonieme gebruikersstatistieken toestaan?",
"Alphabetic": "Alfabetisch",
@@ -34,12 +34,12 @@
"Connection Error": "Verbindingsfout",
"Copied from elsewhere": "Gekopieerd vanaf elders",
"Copied from original": "Gekopieerd van het origineel",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 voor de volgende contributanten:",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 de volgende Bijdragers:",
"Danger!": "Gevaar!",
"Danger!": "Let op!",
"Delete": "Verwijderen",
"Deleted": "Verwijderd",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Apparaat \"{{name}}\" ({{device}} at {{address}}) wil verbinden. Wil je dit toestaan?",
"Device ID": "Apparaat-ID",
"Device Identification": "Apparaat-identificatie",
"Device Name": "Naam apparaat",
@@ -55,6 +55,7 @@
"Edit Device": "Bewerk apparaat",
"Edit Folder": "Bewerk map",
"Editing": "Bezig met bewerken",
"Enable NAT traversal": "Activeer NAT traversal",
"Enable Relaying": "Activeer doorsturen",
"Enable UPnP": "UPnP gebruiken",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Voer door komma's gescheiden (\"tcp://ip:port\", \"tcp://host:port\") adressen in of voer \"dynamisch\" in om automatische ontdekking van het adres uit te voeren.",
@@ -70,7 +71,7 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Bestanden zijn beschermt tegen aanpassingen die gemaakt zijn door andere apparaten, maar aanpassingen op dit apparaat worden doorgestuurd naar de rest van het cluster.",
"Folder": "Map",
"Folder ID": "Map-ID",
"Folder Label": "Folder Label",
"Folder Label": "Map label",
"Folder Master": "Hoofdmap",
"Folder Path": "Maplocatie",
"Folders": "Mappen",
@@ -116,7 +117,7 @@
"OK": "OK",
"Off": "Uit",
"Oldest First": "Oudste eerst",
"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.": "Optioneel label met een beschrijving voor de map. Kan verschillend zijn op elk apparaat.",
"Options": "Opties",
"Out of Sync": "Niet gesynchroniseerd",
"Out of Sync Items": "Niet-gesynchroniseerde items",
@@ -138,9 +139,9 @@
"Relayed via": "Doorgestuurd via",
"Relays": "Relais",
"Release Notes": "Release notes",
"Remote Devices": "Remote Devices",
"Remote Devices": "Externe apparaten",
"Remove": "Verwijderen",
"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.": "De identifier voor de map is verplicht. Dit moet hetzelfde zijn op alle apparaten in het cluster. ",
"Rescan": "Opnieuw scannen",
"Rescan All": "Scan alles opnieuw",
"Rescan Interval": "Scanfrequentie",
@@ -211,7 +212,7 @@
"The rate limit must be a non-negative number (0: no limit)": "De snelheidslimiet moet een positief nummer zijn (0: geen limiet)",
"The rescan interval must be a non-negative number of seconds.": "De scanfrequentie moet een positief getal in seconden zijn.",
"They are retried automatically and will be synced when the error is resolved.": "Het wordt automatisch opnieuw geprobeerd. Bestanden worden gesynchroniseerd als de fout is hersteld.",
"This Device": "This Device",
"This Device": "Dit apparaat",
"This can easily give hackers access to read and change any files on your computer.": "Dit kan kwaadwilligen eenvoudig toegang geven tot het lezen en wijzigen van bestanden op jouw computer.",
"This is a major version upgrade.": "Dit is een grote update.",
"Trash Can File Versioning": "Versiebeheer bestanden prullenbak",
@@ -229,7 +230,7 @@
"Version": "Versie",
"Versions Path": "Bestandspad versies",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versies worden automatisch verwijderd als deze ouder zijn dan de maximale leeftijd of als ze het maximaal aantal toegestane bestanden per interval overschrijden. ",
"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%}\".": "Let op, dit bestandspad is een submap van een bestaande map \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Wanneer een nieuw toestel wordt toegevoegd, houd er dan rekening mee dat dit toestel ook aan de andere kant moet worden toegevoegd.",
"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.": "Houd er bij het toevoegen van nieuwe mappen rekening mee dat het map-ID gebruikt wordt om mappen tussen apparaten te verbinden. Dit ID is hoofdlettergevoelig en moet identiek zijn op andere apparaten.",
"Yes": "Ja",
@@ -238,6 +239,6 @@
"full documentation": "volledige documentatie",
"items": "objecten",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wil de map \"{{folder}}\" delen.",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wil de map \"{{folderLabel}}\" ({{folder}}) delen.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wil de map \"{{folderlabel}}\" ({{folder}}) delen."
}
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Rediger Eining",
"Edit Folder": "Rediger Mappe",
"Editing": "Redigerer",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Aktiver Reléer",
"Enable UPnP": "Aktiver UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Skriv inn adresser med komma mellom kvar adresse (\"tcp://ip:port\", \"tcp://host:port\"), eller \"dynamic\" for å automatisk søkja opp adressa.",
+13 -12
View File
@@ -8,13 +8,13 @@
"Add": "Dodaj",
"Add Device": "Dodaj urządzenie",
"Add Folder": "Dodaj folder",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Dodaj urządzenie zdalne",
"Add new folder?": "Dodać nowy folder?",
"Address": "Adres",
"Addresses": "Adresy",
"Advanced": "Zaawansowane",
"Advanced Configuration": "Konfiguracja zaawansowana",
"Advanced settings": "Advanced settings",
"Advanced settings": "Ustawienia zaawansowane",
"All Data": "Wszystkie dane",
"Allow Anonymous Usage Reporting?": "Zezwalaj na anonimowe statystyki użycia?",
"Alphabetic": "Alfabetycznie",
@@ -34,12 +34,12 @@
"Connection Error": "Błąd 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 the following Contributors:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016: ",
"Copyright © 2015 the following Contributors:": "Copyright © 2015: ",
"Danger!": "Niebezpieczne!",
"Delete": "Usuń",
"Deleted": "Usunięto",
"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?": "Urządzenie \"{{name}}\" {{device}} ({{address}}) chce się połączyć. Dodać urządzenie?",
"Device ID": "ID urządzenia",
"Device Identification": "Identyfikator urządzenia",
"Device Name": "Nazwa urządzenia",
@@ -55,6 +55,7 @@
"Edit Device": "Edytuj urządzenie",
"Edit Folder": "Edytuj folder",
"Editing": "Edytowanie",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Włącz przekazywanie",
"Enable UPnP": "Włącz UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Wpisz oddzielone przecinkiem adresy (\"tcp://ip:port\", \"tcp://host:port\") lub \"dynamic\" by przeprowadzić automatyczne odnalezienie adresu.",
@@ -70,7 +71,7 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Pliki są zabezpieczone przed zmianami na innym urządzeniu, jednak zmiany w tym urządzeniu będą wysłane do reszty.",
"Folder": "Folder",
"Folder ID": "ID folderu",
"Folder Label": "Folder Label",
"Folder Label": "Etykieta folderu",
"Folder Master": "Główny folder",
"Folder Path": "Ścieżka folderu",
"Folders": "Foldery",
@@ -116,7 +117,7 @@
"OK": "OK",
"Off": "Wyłącz",
"Oldest First": "Najstarsze na początku",
"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.": "Opcjonalna opisowa etykieta dla folderu. Może być różna na każdym urządzeniu.",
"Options": "Opcje",
"Out of Sync": "Niezsynchronizowane",
"Out of Sync Items": "Niezsynchronizowane pliki",
@@ -138,9 +139,9 @@
"Relayed via": "Przekazane przez",
"Relays": "Przekaźniki",
"Release Notes": "Informacje o wydaniu",
"Remote Devices": "Remote Devices",
"Remote Devices": "Urządzenia zdalne",
"Remove": "Usuń",
"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.": "Wymagany identyfikator dla folderu. Musi być taki sam dla wszystkich urządzeń.",
"Rescan": "Skanuj ponownie",
"Rescan All": "Skanuj wszystko ponownie",
"Rescan Interval": "Interwał skanowania",
@@ -211,7 +212,7 @@
"The rate limit must be a non-negative number (0: no limit)": "Ograniczenie prędkości powinno być nieujemną liczbą całkowitą (0: brak ograniczeń)",
"The rescan interval must be a non-negative number of seconds.": "Interwał skanowania musi być niezerową liczbą sekund.",
"They are retried automatically and will be synced when the error is resolved.": "Ponowne próby zachodzą automatycznie, synchronizacja nastąpi po usunięciu usterki.",
"This Device": "This Device",
"This Device": "To urządzenie",
"This can easily give hackers access to read and change any files on your computer.": "Może to umożliwić osobom trzecim dostęp do odczytu i zmian dowolnych plików na urządzeniu.",
"This is a major version upgrade.": "To jest ważna aktualizacja",
"Trash Can File Versioning": "Kontrola werjsi plików w koszu",
@@ -229,7 +230,7 @@
"Version": "Wersja",
"Versions Path": "Ścieżka wersji",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Wersje zostają automatycznie usunięte jeżeli są starsze niż maksymalny wiek lub przekraczają liczbę dopuszczalnych wersji.",
"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%}\".": "Ostrzerzenie, ta ścieżka to podkatalog istniejącego folderu \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Gdy dodajesz nowe urządzenie, pamiętaj że urządzenie musi zostać dodane także po drugiej stronie.",
"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.": "Przy dodawaniu nowego folderu, pamiętaj, że ID użyte jest do łączenia folderów pomiędzy urządzeniami. Wielkość liter ciągu ma znaczenie musi zgadzać się na wszystkich urządzeniach.",
"Yes": "Tak",
@@ -238,6 +239,6 @@
"full documentation": "pełna dokumentacja",
"items": "pozycji",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{folder}}\"",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} chce udostępnić folder \"{{folderLabel}}\" ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} chce udostępnić folder \"{{folderlabel}}\" ({{folder}})."
}
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Editar dispositivo",
"Edit Folder": "Editar pasta",
"Editing": "Editando",
"Enable NAT traversal": "Habilitar NAT",
"Enable Relaying": "Habilitar retransmissão",
"Enable UPnP": "Habilitar UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Insira endereços (\"tcp://ip:porta\", \"tcp://host:porta\") separados por vírgula ou \"dynamic\" para executar a descoberta automática do endereço.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Editar dispositivo",
"Edit Folder": "Editar pasta",
"Editing": "Editando",
"Enable NAT traversal": "Activar travessia de NAT",
"Enable Relaying": "Permitir retransmissão",
"Enable UPnP": "Activar UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços separados por vírgulas (\"tcp://ip:porto\", \"tcp://máquina:porto\") ou \"dynamic\" para detectar automaticamente os endereços.",
+2 -1
View File
@@ -55,6 +55,7 @@
"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\" для автоматического поиска адресов.",
@@ -239,5 +240,5 @@
"items": "элементы",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderLabel}}» ({{folder}}).",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderlabel}}» ({{folder}})."
}
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Redigera enhet",
"Edit Folder": "Redigera katalog",
"Editing": "Redigerar",
"Enable NAT traversal": "Aktivera NAT traversering",
"Enable Relaying": "Aktivera reläa",
"Enable UPnP": "Använd UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\")-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Cihaz Düzenle",
"Edit Folder": "Klasör Düzenle",
"Editing": "Düzenleniyor",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
"Enable UPnP": "UPnP Etkinleştir",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Adreslerin keşfinin otomatik olarak gerçekleştirilmesi için ya adresleri virgülle ayırarak (\"tcp://ip:port\", \"tcp://host:port\") girin, ya da \"dynamic\" kelimesini girin.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Налаштування пристрою",
"Edit Folder": "Налаштування директорії",
"Editing": "Редагування",
"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.": "Введіть розділені комою (\"tcp://ip:port\", \"tcp://host:port\") адреси або \"dynamic\" для автоматичного визначення адреси.",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "Ch.sửa thiết bị",
"Edit Folder": "Ch.sửa thư mục",
"Editing": "Đang ch.sửa",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Bật chế độ ch.tiếp",
"Enable UPnP": "Bật UPnP",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Nhập các địa chỉ ngăn cách bởi dấu phẩy (\"tcp://ip:port\", \"tcp://host:port\") hoặc \"dynamic\" để tiến hành dò tìm địa chỉ tự động.",
+15 -14
View File
@@ -13,12 +13,12 @@
"Address": "地址",
"Addresses": "地址列表",
"Advanced": "高级",
"Advanced Configuration": "高级置",
"Advanced Configuration": "高级置",
"Advanced settings": "高级设置",
"All Data": "所有数据",
"Allow Anonymous Usage Reporting?": "允许匿名使用报告?",
"Alphabetic": "字母顺序",
"An external command handles the versioning. It has to remove the file from the synced folder.": "使用外部命令接管版本控制。当文件在其他设备被删除时,本机的 Syncthing 会调用该外部命令,并将文件路径以参数形式传递给该命令。该命令必须自行从同步文件夹中删除该文件。",
"An external command handles the versioning. It has to remove the file from the synced folder.": "使用外部命令接管版本控制。该命令必须自行从同步文件夹中删除该文件。",
"Anonymous Usage Reporting": "匿名使用报告",
"Any devices configured on an introducer device will be added to this device as well.": "在介绍人设备上被添加的其它设备,也将会被添加到本机。",
"Automatic upgrades": "自动升级",
@@ -34,12 +34,12 @@
"Connection Error": "连接出错",
"Copied from elsewhere": "从其他设备复制",
"Copied from original": "从源复制",
"Copyright © 2014-2016 the following Contributors:": "版权所有© 2014-2016 以下贡献者:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 以下贡献者",
"Copyright © 2015 the following Contributors:": "版权 ©2015 由下列贡献者所有:",
"Danger!": "危险!",
"Delete": "删除",
"Deleted": "已删除",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "设备 \"{{name}}\" (位于 {{address}} 的 {{device}}) 请求连接。是否添加新设备?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "设备 \"{{name}}\"位于 {{address}} 的 {{device}}请求连接。是否添加新设备?",
"Device ID": "设备标识",
"Device Identification": "设备标识",
"Device Name": "设备名",
@@ -55,6 +55,7 @@
"Edit Device": "编辑设备选项",
"Edit Folder": "编辑文件夹选项",
"Editing": "正在编辑",
"Enable NAT traversal": "启用 NAT 遍历",
"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\" 表示自动寻找地址。",
@@ -64,7 +65,7 @@
"Failed Items": "失败的项目",
"File Pull Order": "文件拉取顺序",
"File Versioning": "版本控制",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "当找文件更时,忽略文件权限。用FAT文件系统。",
"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.": "当文件被 Syncthing 替换或删时,将会被移动到 .stversions 文件夹",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by 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.": "在其它设备中对该文件夹内文件的修改并不会被同步到本机,但是在本机上对其的修改,则会被同步到其它设备中。",
@@ -74,7 +75,7 @@
"Folder Master": "主文件夹",
"Folder Path": "文件夹路径",
"Folders": "文件夹",
"GUI": "图形界面",
"GUI": "图形用户界面",
"GUI Authentication Password": "图形管理界面密码",
"GUI Authentication User": "图形管理界面用户名",
"GUI Listen Addresses": "图形管理界面监听地址",
@@ -103,7 +104,7 @@
"Major Upgrade": "重大更新",
"Maximum Age": "历史版本最长保留时间",
"Metadata Only": "仅元数据",
"Minimum Free Disk Space": "最低空闲磁盘空间",
"Minimum Free Disk Space": "最低可用磁盘空间",
"Move to top of queue": "移动到队列顶端",
"Multi level wildcard (matches multiple directory levels)": "多级通配符(用以匹配多层文件夹)",
"Never": "从未",
@@ -116,7 +117,7 @@
"OK": "确定",
"Off": "关闭",
"Oldest First": "旧文件优先",
"Optional descriptive label for the folder. Can be different on each device.": "可选的文件夹表述标签。在不同设备上可以不一致。",
"Optional descriptive label for the folder. Can be different on each device.": "可选的文件夹说明性标签。在不同设备上可以不一致。",
"Options": "选项",
"Out of Sync": "未同步",
"Out of Sync Items": "未同步的项目",
@@ -163,9 +164,9 @@
"Shared With": "共享给",
"Short identifier for the folder. Must be the same on all cluster devices.": "文件夹的别名。必须在所有设备上保持一致。",
"Show ID": "显示设备标识",
"Show QR": "显示二维码",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "在设备丛中,显示该名称,而不是设备标识。亦会作为一个可选的默认名称被发送到其他设备。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "在设备丛中,将会显示本名称,而不是设备标识。如果设置为空,则会使用目标设备提供的默认名称。",
"Show QR": "显示 QR 码",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "在设备丛中,显示该名称,而不是设备 ID。亦会作为一个可选的默认名称被发送到其他设备。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "在设备丛中,将会显示本名称,而不是设备 ID。如果设置为空,则会使用目标设备提供的默认名称。",
"Shutdown": "关闭 Syncthing",
"Shutdown Complete": "关闭完成",
"Simple File Versioning": "简易版本控制",
@@ -189,7 +190,7 @@
"The aggregated statistics are publicly available at {%url%}.": "全局统计公布于 {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "设置已经保存,但是还未生效。Syncthing 需要重启以启用新的设置。",
"The device ID cannot be blank.": "设备标识不能为空",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "在这里所需要输入的设备标识,可以在目标设备的“操作->显示设备标识”中看到。空格和横线可选(将会被忽略)。",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "在这里所需要输入的设备 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).": "在这里所需要输入的设备标识,可以在目标设备的“选项->显示设备标识”中看到。空格和横线可选(将会被忽略)。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "经过加密的使用报告会每天发送。它用来跟踪统计使用本软件的平台,文件夹大小,以及本软件的版本。如果报告的内容有任何变化,本对话框会再次弹出提示您。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "输入的设备标识似乎无效。设备标识长度必须为52或56的字母和数字,空格和横线不算在内。",
@@ -202,7 +203,7 @@
"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).": "最低空间磁盘空间的数值必须介于 0-100 的正整数",
"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.": "文件保存在回收站的天数。零表示永久。",
"The number of old versions to keep, per file.": "每个文件保留的版本数量上限。",
@@ -231,7 +232,7 @@
"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%}\".": "警告, 该路径是一个已经存在文件夹\"{{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.": "若你添加了新文件夹,记住文件夹标识是用以在不同设备间建立联系的。在不同设备间拥有相同标识的文件夹将会被同步。且文件夹标识大小写敏感。",
"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 的文件夹将会被同步。且文件夹 ID 区分大小写。",
"Yes": "是",
"You must keep at least one version.": "您必须保留至少一个版本",
"days": "天",
+1
View File
@@ -55,6 +55,7 @@
"Edit Device": "編輯裝置",
"Edit Folder": "編輯資料夾",
"Editing": "正在編輯",
"Enable NAT traversal": "Enable 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.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
+32 -27
View File
@@ -231,7 +231,7 @@
<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.readOnly ? 'fa-lock' : 'fa-folder']"></span>
<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>
@@ -294,7 +294,7 @@
<span tooltip data-original-title="{{scanRate(folder.id) | binary}}B/s">~ {{scanRemaining(folder.id)}}</span>
</td>
</tr>
<tr ng-if="!folder.readOnly && (folderStatus(folder) === 'outofsync' || hasFailedFiles(folder.id))">
<tr ng-if="folder.type != 'readonly' && (folderStatus(folder) === 'outofsync' || hasFailedFiles(folder.id))">
<th><span class="fa fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Failed Items</span></th>
<!-- Show the number of failed items as a link to bring up the list. -->
<td ng-if="hasFailedFiles(folder.id)" class="text-right">
@@ -305,10 +305,11 @@
<span class="fa fa-spinner fa-pulse"></span>
</td>
</tr>
<tr ng-if="folder.readOnly">
<th><span class="fa fa-fw fa-lock"></span>&nbsp;<span translate>Folder Master</span></th>
<tr ng-if="folder.type != 'readwrite'">
<th><span class="fa fa-fw fa-lock"></span>&nbsp;<span translate>Folder Type</span></th>
<td class="text-right">
<span translate>Yes</span>
<span ng-if="folder.type == 'readonly'" translate>Master</span>
<span ng-if="folder.type != 'readonly'">{{ folder.type.charAt(0).toUpperCase() + folder.type.slice(1) }}</span>
</td>
</tr>
<tr ng-if="model[folder.id].ignorePatterns">
@@ -351,7 +352,7 @@
<th><span class="fa fa-fw fa-share-alt"></span>&nbsp;<span translate>Shared With</span></th>
<td class="text-right">{{sharesFolder(folder)}}</td>
</tr>
<tr ng-if="!folder.readOnly && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
<tr ng-if="folder.type != 'readonly' && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
<th><span class="fa fa-fw fa-exchange"></span>&nbsp;<span translate>Last File Received</span></th>
<td class="text-right">
<span tooltip data-original-title="{{folderStats[folder.id].lastFile.filename}} @ {{folderStats[folder.id].lastFile.at | date:'yyyy-MM-dd HH:mm:ss'}}">
@@ -365,7 +366,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.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">
@@ -429,6 +430,19 @@
<th><span class="fa fa-fw fa-tachometer"></span>&nbsp;<span translate>CPU Utilization</span></th>
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
</tr>
<tr>
<th><span class="fa fa-fw fa-sitemap"></span>&nbsp;<span translate>Listeners</span></th>
<td class="text-right">
<span ng-if="listenersFailed.length == 0" class="data text-success">
<span>{{listenersTotal}}/{{listenersTotal}}</span>
</span>
<span ng-if="listenersFailed.length != 0" class="data" ng-class="{'text-danger': listenersFailed.length == listenersTotal}">
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{listenersFailed.join('<br>\n')}}">
{{listenersTotal-listenersFailed.length}}/{{listenersTotal}}
</span>
</span>
</td>
</tr>
<tr ng-if="system.discoveryEnabled">
<th><span class="fa fa-fw fa-map-signs"></span>&nbsp;<span translate>Discovery</span></th>
<td class="text-right">
@@ -442,19 +456,6 @@
</span>
</td>
</tr>
<tr ng-if="system.relaysEnabled">
<th><span class="fa fa-fw fa-sitemap"></span>&nbsp;<span translate>Relays</span></th>
<td class="text-right">
<span ng-if="relaysFailed.length == 0" class="data text-success">
<span>{{relaysTotal}}/{{relaysTotal}}</span>
</span>
<span ng-if="relaysFailed.length != 0" class="data" ng-class="{'text-danger': relaysFailed.length == relaysTotal}">
<span popover data-trigger="hover" data-placement="bottom" data-html="true" data-content="{{relaysFailed.join('<br>\n')}}">
{{relaysTotal-relaysFailed.length}}/{{relaysTotal}}
</span>
</span>
</td>
</tr>
<tr>
<th><span class="fa fa-fw fa-clock-o"></span>&nbsp;<span translate>Uptime</span></th>
<td class="text-right">{{system.uptime | duration:"m"}}</td>
@@ -502,13 +503,17 @@
<th><span class="fa fa-fw fa-cloud-upload"></span>&nbsp;<span translate>Upload Rate</span></th>
<td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
</tr>
<tr>
<th>
<span class="fa fa-fw fa-link"></span>
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('direct') == 0" >Address</span>
<span translate ng-if="connections[deviceCfg.deviceID].type.indexOf('relay') == 0" >Relayed via</span>
</th>
<td class="text-right">{{deviceAddr(deviceCfg)}}</td>
<tr ng-if="connections[deviceCfg.deviceID].connected">
<th><span class="fa fa-fw fa-link"></span>&nbsp<span translate>Address</span></th>
<td class="text-right">
<span tooltip data-original-title="{{ connections[deviceCfg.deviceID].type.indexOf('Relay') > -1 ? '' : connections[deviceCfg.deviceID].type }}">
{{deviceAddr(deviceCfg)}}
</span>
</td>
</tr>
<tr ng-if="connections[deviceCfg.deviceID].connected && connections[deviceCfg.deviceID].type.indexOf('Relay') > -1" tooltip data-original-title="Connections via relays might be rate limited by the relay">
<th><span class="fa fa-fw fa-warning text-danger"></span>&nbsp<span translate>Connection Type</span></th>
<td class="text-right">{{connections[deviceCfg.deviceID].type}}</td>
</tr>
<tr ng-if="deviceCfg.compression != 'metadata'">
<th><span class="fa fa-fw fa-compress"></span>&nbsp;<span translate>Compression</span></th>
@@ -11,7 +11,7 @@
<p translate>Copyright &copy; 2014-2016 the following Contributors:</p>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Ben Schulz, Caleb Callaway, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Andrew Dunham, Antony Male, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Harte, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Lars K.W. Gohlke, Laurent Etiemble, Lord Landon Agahnim, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
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.
</div>
</div>
<hr/>
@@ -352,9 +352,8 @@ angular.module('syncthing.core')
var hasConfig = !isEmptyObject($scope.config);
$scope.config = config;
$scope.config.options._listenAddressStr = $scope.config.options.listenAddress.join(', ');
$scope.config.options._listenAddressesStr = $scope.config.options.listenAddresses.join(', ');
$scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
$scope.config.options._relayServersStr = $scope.config.options.relayServers.join(', ');
$scope.devices = $scope.config.devices;
$scope.devices.forEach(function (deviceCfg) {
@@ -390,6 +389,15 @@ angular.module('syncthing.core')
$scope.myID = data.myID;
$scope.system = data;
var listenersFailed = [];
for (var address in data.connectionServiceStatus) {
if (data.connectionServiceStatus[address].error) {
listenersFailed.push(address + ": " + data.connectionServiceStatus[address].error);
}
}
$scope.listenersFailed = listenersFailed;
$scope.listenersTotal = Object.keys(data.connectionServiceStatus).length;
$scope.discoveryTotal = data.discoveryMethods;
var discoveryFailed = [];
for (var disco in data.discoveryErrors) {
@@ -398,18 +406,6 @@ angular.module('syncthing.core')
}
}
$scope.discoveryFailed = discoveryFailed;
var relaysFailed = [];
var relaysTotal = 0;
for (var relay in data.relayClientStatus) {
if (!data.relayClientStatus[relay]) {
relaysFailed.push(relay);
}
relaysTotal++;
}
$scope.relaysFailed = relaysFailed;
$scope.relaysTotal = relaysTotal;
console.log("refreshSystem", data);
}).error($scope.emitHTTPError);
}
@@ -892,7 +888,7 @@ angular.module('syncthing.core')
$scope.config.options = angular.copy($scope.tmpOptions);
$scope.config.gui = angular.copy($scope.tmpGUI);
['listenAddress', 'globalAnnounceServers', 'relayServers'].forEach(function (key) {
['listenAddresses', 'globalAnnounceServers'].forEach(function (key) {
$scope.config.options[key] = $scope.config.options["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
return x.trim();
});
@@ -1201,6 +1197,7 @@ angular.module('syncthing.core')
$scope.currentFolder = {
selectedDevices: {},
id: $scope.createRandomFolderId(),
type: "readwrite",
rescanIntervalS: 60,
minDiskFreePct: 1,
maxConflicts: 10,
@@ -91,13 +91,13 @@
<!-- Left column -->
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentFolder.readOnly"> <span translate>Folder Master</span>
</label>
&nbsp;<a href="http://docs.syncthing.net/users/foldermaster.html" target="_blank"><span class="fa fa-book"></span>&nbsp;<span translate>Help</span></a>
</div>
<p translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
<label translate>Folder Type</label>
&nbsp;<a href="http://docs.syncthing.net/users/foldermaster.html" target="_blank"><span class="fa fa-book"></span>&nbsp;<span translate>Help</span></a>
<select class="form-control" ng-model="currentFolder.type">
<option value="readwrite" translate>Normal</option>
<option value="readonly" translate>Master</option>
</select>
<p ng-if="currentFolder.type == 'readonly'" translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
</div>
<div class="form-group">
<div class="checkbox">
@@ -15,8 +15,8 @@
</div>
<div class="form-group">
<label translate for="ListenAddressStr">Sync Protocol Listen Addresses</label>
<input id="ListenAddressStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressStr">
<label translate for="ListenAddressesStr">Sync Protocol Listen Addresses</label>
<input id="ListenAddressesStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressesStr">
</div>
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}">
@@ -56,25 +56,24 @@
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="RelaysEnabled" type="checkbox" ng-model="tmpOptions.relaysEnabled"> <span translate>Enable Relaying</span>
</label>
<div 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>
<div class="clearfix"></div>
<div class="form-group">
<label translate for="RelayServersStr">Relay Servers</label>
<input ng-disabled="!tmpOptions.relaysEnabled" id="RelayServersStr" class="form-control" type="text" ng-model="tmpOptions._relayServersStr">
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.globalAnnounceEnabled"> <span translate>Global Discovery</span>
</label>
<div 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>
+1 -1
View File
@@ -1,6 +1,6 @@
The files contained herein are:
- angular 1.5.3
- angular 1.2.9
- angular-translate 2.9.0.1
- angular-translate-loader-static-files 2.11.0
- angular-dirPagination 759009c
+8417 -18571
View File
File diff suppressed because it is too large Load Diff
+110 -15
View File
@@ -11,6 +11,8 @@ import (
"encoding/json"
"encoding/xml"
"io"
"io/ioutil"
"net/url"
"os"
"sort"
"strings"
@@ -21,26 +23,32 @@ import (
const (
OldestHandledVersion = 10
CurrentVersion = 13
CurrentVersion = 14
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.
DefaultListenAddresses = []string{
"tcp://0.0.0.0:22000",
"dynamic+https://relays.syncthing.net/endpoint",
}
// DefaultDiscoveryServersV4 should be substituted when the configuration
// contains <globalAnnounceServer>default-v4</globalAnnounceServer>. This is
// done by the "consumer" of the configuration, as we don't want these
// saved to the config.
// contains <globalAnnounceServer>default-v4</globalAnnounceServer>.
DefaultDiscoveryServersV4 = []string{
"https://discovery-v4-1.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden
"https://discovery-v4-2.syncthing.net/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 45.55.230.38, USA
"https://discovery-v4-3.syncthing.net/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 128.199.95.124, Singapore
"https://discovery-v4-1.syncthing.net/v2/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden
"https://discovery-v4-2.syncthing.net/v2/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 45.55.230.38, USA
"https://discovery-v4-3.syncthing.net/v2/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 128.199.95.124, Singapore
}
// DefaultDiscoveryServersV6 should be substituted when the configuration
// contains <globalAnnounceServer>default-v6</globalAnnounceServer>.
DefaultDiscoveryServersV6 = []string{
"https://discovery-v6-1.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden
"https://discovery-v6-2.syncthing.net/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 2604:a880:800:10::182:a001, USA
"https://discovery-v6-3.syncthing.net/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 2400:6180:0:d0::d9:d001, Singapore
"https://discovery-v6-1.syncthing.net/v2/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden
"https://discovery-v6-2.syncthing.net/v2/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 2604:a880:800:10::182:a001, USA
"https://discovery-v6-3.syncthing.net/v2/?id=VK6HNJ3-VVMM66S-HRVWSCR-IXEHL2H-U4AQ4MW-UCPQBWX-J2L2UBK-NVZRDQZ", // 2400:6180:0:d0::d9:d001, Singapore
}
// DefaultDiscoveryServers should be substituted when the configuration
// contains <globalAnnounceServer>default</globalAnnounceServer>.
@@ -85,7 +93,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)
@@ -168,7 +181,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
}
}
cfg.Options.ListenAddress = util.UniqueStrings(cfg.Options.ListenAddress)
cfg.Options.ListenAddresses = util.UniqueStrings(cfg.Options.ListenAddresses)
cfg.Options.GlobalAnnServers = util.UniqueStrings(cfg.Options.GlobalAnnServers)
if cfg.Version > 0 && cfg.Version < OldestHandledVersion {
@@ -185,6 +198,9 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
if cfg.Version == 12 {
convertV12V13(cfg)
}
if cfg.Version == 13 {
convertV13V14(cfg)
}
// Build a list of available devices
existingDevices := make(map[protocol.DeviceID]bool)
@@ -238,22 +254,101 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
}
}
func convertV12V13(cfg *Configuration) {
func convertV13V14(cfg *Configuration) {
// Not using the ignore cache is the new default. Disable it on existing
// configurations.
cfg.Options.CacheIgnoredFiles = false
// 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
}
}
// 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/"
addr = uri.String()
}
newAddrs = append(newAddrs, addr)
}
cfg.Options.GlobalAnnServers = newAddrs
for i, fcfg := range cfg.Folders {
if fcfg.DeprecatedReadOnly {
cfg.Folders[i].Type = FolderTypeReadOnly
} else {
cfg.Folders[i].Type = FolderTypeReadWrite
}
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) {
// Change listen address schema
for i, addr := range cfg.Options.ListenAddress {
for i, addr := range cfg.Options.ListenAddresses {
if len(addr) > 0 && !strings.HasPrefix(addr, "tcp://") {
cfg.Options.ListenAddress[i] = util.Address("tcp", addr)
cfg.Options.ListenAddresses[i] = util.Address("tcp", addr)
}
}
+80 -22
View File
@@ -12,7 +12,9 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"testing"
@@ -31,13 +33,12 @@ func init() {
func TestDefaultValues(t *testing.T) {
expected := OptionsConfiguration{
ListenAddress: []string{"tcp://0.0.0.0:22000"},
ListenAddresses: []string{"default"},
GlobalAnnServers: []string{"default"},
GlobalAnnEnabled: true,
LocalAnnEnabled: true,
LocalAnnPort: 21027,
LocalAnnMCAddr: "[ff12::8384]:21027",
RelayServers: []string{"dynamic+https://relays.syncthing.net/endpoint"},
MaxSendKbps: 0,
MaxRecvKbps: 0,
ReconnectIntervalS: 60,
@@ -59,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,
}
@@ -94,7 +95,7 @@ func TestDeviceConfig(t *testing.T) {
ID: "test",
RawPath: "testdata",
Devices: []FolderDeviceConfiguration{{DeviceID: device1}, {DeviceID: device4}},
ReadOnly: true,
Type: FolderTypeReadOnly,
RescanIntervalS: 600,
Copiers: 0,
Pullers: 0,
@@ -147,28 +148,27 @@ func TestDeviceConfig(t *testing.T) {
}
}
func TestNoListenAddress(t *testing.T) {
func TestNoListenAddresses(t *testing.T) {
cfg, err := Load("testdata/nolistenaddress.xml", device1)
if err != nil {
t.Error(err)
}
expected := []string{""}
actual := cfg.Options().ListenAddress
actual := cfg.Options().ListenAddresses
if diff, equal := messagediff.PrettyDiff(expected, actual); !equal {
t.Errorf("Unexpected ListenAddress. Diff:\n%s", diff)
t.Errorf("Unexpected ListenAddresses. Diff:\n%s", diff)
}
}
func TestOverriddenValues(t *testing.T) {
expected := OptionsConfiguration{
ListenAddress: []string{"tcp://:23000"},
ListenAddresses: []string{"tcp://:23000"},
GlobalAnnServers: []string{"udp4://syncthing.nym.se:22026"},
GlobalAnnEnabled: false,
LocalAnnEnabled: false,
LocalAnnPort: 42123,
LocalAnnMCAddr: "quux:3232",
RelayServers: []string{"relay://123.123.123.123:1234", "relay://125.125.125.125:1255"},
MaxSendKbps: 1234,
MaxRecvKbps: 2341,
ReconnectIntervalS: 6000,
@@ -192,7 +192,7 @@ func TestOverriddenValues(t *testing.T) {
URPostInsecurely: true,
ReleasesURL: "https://localhost/releases",
AlwaysLocalNets: []string{},
OverwriteNames: true,
OverwriteRemoteDevNames: true,
TempIndexMinBlocks: 100,
}
@@ -357,20 +357,20 @@ func TestIssue1750(t *testing.T) {
t.Fatal(err)
}
if cfg.Options().ListenAddress[0] != "tcp://:23000" {
t.Errorf("%q != %q", cfg.Options().ListenAddress[0], "tcp://:23000")
if cfg.Options().ListenAddresses[0] != "tcp://:23000" {
t.Errorf("%q != %q", cfg.Options().ListenAddresses[0], "tcp://:23000")
}
if cfg.Options().ListenAddress[1] != "tcp://:23001" {
t.Errorf("%q != %q", cfg.Options().ListenAddress[1], "tcp://:23001")
if cfg.Options().ListenAddresses[1] != "tcp://:23001" {
t.Errorf("%q != %q", cfg.Options().ListenAddresses[1], "tcp://:23001")
}
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[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[1] != "udp4://syncthing.nym.se:22027" {
t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[1], "udp4://syncthing.nym.se:22027")
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/")
}
}
@@ -461,13 +461,13 @@ func TestNewSaveLoad(t *testing.T) {
func TestPrepare(t *testing.T) {
var cfg Configuration
if cfg.Folders != nil || cfg.Devices != nil || cfg.Options.ListenAddress != nil {
if cfg.Folders != nil || cfg.Devices != nil || cfg.Options.ListenAddresses != nil {
t.Error("Expected nil")
}
cfg.prepare(device1)
if cfg.Folders == nil || cfg.Devices == nil || cfg.Options.ListenAddress == nil {
if cfg.Folders == nil || cfg.Devices == nil || cfg.Options.ListenAddresses == nil {
t.Error("Unexpected nil")
}
}
@@ -488,7 +488,7 @@ func TestCopy(t *testing.T) {
cfg.Devices[0].Addresses[0] = "wrong"
cfg.Folders[0].Devices[0].DeviceID = protocol.DeviceID{0, 1, 2, 3}
cfg.Options.ListenAddress[0] = "wrong"
cfg.Options.ListenAddresses[0] = "wrong"
cfg.GUI.APIKey = "wrong"
bsChanged, err := json.MarshalIndent(cfg, "", " ")
@@ -620,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])
}
}
}
+7 -1
View File
@@ -20,8 +20,8 @@ type FolderConfiguration struct {
ID string `xml:"id,attr" json:"id"`
Label string `xml:"label,attr" json:"label"`
RawPath string `xml:"path,attr" json:"path"`
Type FolderType `xml:"type,attr" json:"type"`
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
ReadOnly bool `xml:"ro,attr" json:"readOnly"`
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
@@ -41,6 +41,8 @@ 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,omitempty" json:"-"`
}
type FolderDeviceConfiguration struct {
@@ -136,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 {
+41
View File
@@ -0,0 +1,41 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package config
type FolderType int
const (
FolderTypeReadWrite FolderType = iota // default is readwrite
FolderTypeReadOnly
)
func (t FolderType) String() string {
switch t {
case FolderTypeReadWrite:
return "readwrite"
case FolderTypeReadOnly:
return "readonly"
default:
return "unknown"
}
}
func (t FolderType) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}
func (t *FolderType) UnmarshalText(bs []byte) error {
switch string(bs) {
case "readwrite":
*t = FolderTypeReadWrite
case "readonly":
*t = FolderTypeReadOnly
default:
*t = FolderTypeReadWrite
}
return nil
}
+10 -12
View File
@@ -7,13 +7,12 @@
package config
type OptionsConfiguration struct {
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
ListenAddresses []string `xml:"listenAddress" json:"listenAddresses" default:"default"`
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027"`
RelayServers []string `xml:"relayServer" json:"relayServers" default:"dynamic+https://relays.syncthing.net/endpoint"`
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
@@ -37,25 +36,24 @@ 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"`
DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes"`
DeprecatedUPnPRenewalM int `xml:"upnpRenewalMinutes"`
DeprecatedUPnPTimeoutS int `xml:"upnpTimeoutSeconds"`
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 {
c := orig
c.ListenAddress = make([]string, len(orig.ListenAddress))
copy(c.ListenAddress, orig.ListenAddress)
c.ListenAddresses = make([]string, len(orig.ListenAddresses))
copy(c.ListenAddresses, orig.ListenAddresses)
c.GlobalAnnServers = make([]string, len(orig.GlobalAnnServers))
copy(c.GlobalAnnServers, orig.GlobalAnnServers)
c.RelayServers = make([]string, len(orig.RelayServers))
copy(c.RelayServers, orig.RelayServers)
c.AlwaysLocalNets = make([]string, len(orig.AlwaysLocalNets))
copy(c.AlwaysLocalNets, orig.AlwaysLocalNets)
return c
+2 -4
View File
@@ -1,4 +1,4 @@
<configuration version="13">
<configuration version="14">
<options>
<listenAddress>tcp://:23000</listenAddress>
<allowDelete>false</allowDelete>
@@ -7,8 +7,6 @@
<localAnnounceEnabled>false</localAnnounceEnabled>
<localAnnouncePort>42123</localAnnouncePort>
<localAnnounceMCAddr>quux:3232</localAnnounceMCAddr>
<relayServer>relay://123.123.123.123:1234</relayServer>
<relayServer>relay://125.125.125.125:1255</relayServer>
<parallelRequests>32</parallelRequests>
<maxSendKbps>1234</maxSendKbps>
<maxRecvKbps>2341</maxRecvKbps>
@@ -34,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>
+14
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>
+13 -5
View File
@@ -46,11 +46,6 @@ type CommitResponse struct {
RequiresRestart bool
}
var ResponseNoRestart = CommitResponse{
ValidationError: nil,
RequiresRestart: false,
}
// A wrapper around a Configuration that manages loads, saves and published
// notifications of changes to registered Handlers
@@ -324,3 +319,16 @@ func (w *Wrapper) GlobalDiscoveryServers() []string {
}
return util.UniqueStrings(servers)
}
func (w *Wrapper) ListenAddresses() []string {
var addresses []string
for _, addr := range w.cfg.Options.ListenAddresses {
switch addr {
case "default":
addresses = append(addresses, DefaultListenAddresses...)
default:
addresses = append(addresses, addr)
}
}
return util.UniqueStrings(addresses)
}
-645
View File
@@ -1,645 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"encoding/binary"
"fmt"
"io"
"net"
"net/url"
"sync"
"time"
"github.com/juju/ratelimit"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/model"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay"
"github.com/syncthing/syncthing/lib/relay/client"
"github.com/syncthing/syncthing/lib/util"
"github.com/thejerf/suture"
)
type DialerFactory func(*url.URL, *tls.Config) (*tls.Conn, error)
type ListenerFactory func(*url.URL, *tls.Config, chan<- model.IntermediateConnection)
var (
dialers = make(map[string]DialerFactory, 0)
listeners = make(map[string]ListenerFactory, 0)
)
type Model interface {
protocol.Model
AddConnection(conn model.Connection, hello protocol.HelloMessage)
ConnectedTo(remoteID protocol.DeviceID) bool
IsPaused(remoteID protocol.DeviceID) bool
OnHello(protocol.DeviceID, net.Addr, protocol.HelloMessage)
GetHello(protocol.DeviceID) protocol.HelloMessage
}
// Service listens on TLS and dials configured unconnected devices. Successful
// connections are handed to the model.
type Service struct {
*suture.Supervisor
cfg *config.Wrapper
myID protocol.DeviceID
model Model
tlsCfg *tls.Config
discoverer discover.Finder
conns chan model.IntermediateConnection
mappings []*nat.Mapping
relayService relay.Service
bepProtocolName string
tlsDefaultCommonName string
lans []*net.IPNet
writeRateLimit *ratelimit.Bucket
readRateLimit *ratelimit.Bucket
lastRelayCheck map[protocol.DeviceID]time.Time
mut sync.RWMutex
connType map[protocol.DeviceID]model.ConnectionType
relaysEnabled bool
}
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, mappings []*nat.Mapping,
relayService relay.Service, bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) *Service {
service := &Service{
Supervisor: suture.NewSimple("connections.Service"),
cfg: cfg,
myID: myID,
model: mdl,
tlsCfg: tlsCfg,
discoverer: discoverer,
mappings: mappings,
relayService: relayService,
conns: make(chan model.IntermediateConnection),
bepProtocolName: bepProtocolName,
tlsDefaultCommonName: tlsDefaultCommonName,
lans: lans,
connType: make(map[protocol.DeviceID]model.ConnectionType),
relaysEnabled: cfg.Options().RelaysEnabled,
lastRelayCheck: make(map[protocol.DeviceID]time.Time),
}
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))
}
if service.cfg.Options().MaxRecvKbps > 0 {
service.readRateLimit = ratelimit.NewBucketWithRate(float64(1024*service.cfg.Options().MaxRecvKbps), int64(5*1024*service.cfg.Options().MaxRecvKbps))
}
// There are several moving parts here; one routine per listening address
// to handle incoming connections, one routine to periodically attempt
// outgoing connections, one routine to the the common handling
// regardless of whether the connection was incoming or outgoing.
// Furthermore, a relay service which handles incoming requests to connect
// via the relays.
//
// TODO: Clean shutdown, and/or handling config changes on the fly. We
// partly do this now - new devices and addresses will be picked up, but
// not new listen addresses and we don't support disconnecting devices
// that are removed and so on...
service.Add(serviceFunc(service.connect))
for _, addr := range service.cfg.Options().ListenAddress {
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse listen address:", addr, err)
continue
}
listener, ok := listeners[uri.Scheme]
if !ok {
l.Infoln("Unknown listen address scheme:", uri.String())
continue
}
l.Debugln("listening on", uri)
service.Add(serviceFunc(func() {
listener(uri, service.tlsCfg, service.conns)
}))
}
service.Add(serviceFunc(service.handle))
if service.relayService != nil {
service.Add(serviceFunc(service.acceptRelayConns))
}
for _, mapping := range mappings {
mapping.OnChanged(func(m *nat.Mapping, added, removed []nat.Address) {
events.Default.Log(events.ExternalPortMappingChanged, map[string]interface{}{
"protocol": m.Protocol(),
"local": m.Address().String(),
"added": added,
"removed": removed,
})
})
}
return service
}
func (s *Service) handle() {
next:
for c := range s.conns {
cs := c.ConnectionState()
// We should have negotiated the next level protocol "bep/1.0" as part
// of the TLS handshake. Unfortunately this can't be a hard error,
// because there are implementations out there that don't support
// protocol negotiation (iOS for one...).
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != s.bepProtocolName {
l.Infof("Peer %s did not negotiate bep/1.0", c.RemoteAddr())
}
// We should have received exactly one certificate from the other
// side. If we didn't, they don't have a device ID and we drop the
// connection.
certs := cs.PeerCertificates
if cl := len(certs); cl != 1 {
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.RemoteAddr())
c.Close()
continue
}
remoteCert := certs[0]
remoteID := protocol.NewDeviceID(remoteCert.Raw)
// The device ID should not be that of ourselves. It can happen
// though, especially in the presence of NAT hairpinning, multiple
// clients between the same NAT gateway, and global discovery.
if remoteID == s.myID {
l.Infof("Connected to myself (%s) - should not happen", remoteID)
c.Close()
continue
}
hello, err := exchangeHello(c, s.model.GetHello(remoteID))
if err != nil {
l.Infof("Failed to exchange Hello messages with %s (%s): %s", remoteID, c.RemoteAddr(), err)
c.Close()
continue next
}
s.model.OnHello(remoteID, c.RemoteAddr(), hello)
// 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
ct, ok := s.connType[remoteID]
if ok && !ct.IsDirect() && c.Type.IsDirect() {
l.Debugln("Switching connections", remoteID)
s.model.Close(remoteID, fmt.Errorf("switching connections"))
} else if s.model.ConnectedTo(remoteID) {
// We should not already be connected to the other party. TODO: This
// could use some better handling. If the old connection is dead but
// hasn't timed out yet we may want to drop *that* connection and keep
// this one. But in case we are two devices connecting to each other
// in parallel we don't want to do that or we end up with no
// connections still established...
l.Infof("Connected to already connected device (%s)", remoteID)
c.Close()
skip = true
} else if s.model.IsPaused(remoteID) {
l.Infof("Connection from paused device (%s)", remoteID)
c.Close()
skip = true
}
s.mut.RUnlock()
if skip {
continue
}
for deviceID, deviceCfg := range s.cfg.Devices() {
if deviceID == remoteID {
// Verify the name on the certificate. By default we set it to
// "syncthing" when generating, but the user may have replaced
// the certificate and used another name.
certName := deviceCfg.CertName
if certName == "" {
certName = s.tlsDefaultCommonName
}
err := remoteCert.VerifyHostname(certName)
if err != nil {
// Incorrect certificate name is something the user most
// likely wants to know about, since it's an advanced
// config. Warn instead of Info.
l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.RemoteAddr(), err)
c.Close()
continue next
}
// If rate limiting is set, and based on the address we should
// limit the connection, then we wrap it in a limiter.
limit := s.shouldLimit(c.RemoteAddr())
wr := io.Writer(c.Conn)
if limit && s.writeRateLimit != nil {
wr = NewWriteLimiter(c.Conn, s.writeRateLimit)
}
rd := io.Reader(c.Conn)
if limit && s.readRateLimit != nil {
rd = NewReadLimiter(c.Conn, s.readRateLimit)
}
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type)
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
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(model.Connection{
Conn: c,
Connection: protoConn,
Type: c.Type,
}, hello)
s.connType[remoteID] = c.Type
s.mut.Unlock()
continue next
}
}
l.Infof("Connection from %s (%s) with ignored device ID %s", c.RemoteAddr(), c.Type, remoteID)
c.Close()
}
}
func (s *Service) connect() {
delay := time.Second
for {
l.Debugln("Reconnect loop")
nextDevice:
for deviceID, deviceCfg := range s.cfg.Devices() {
if deviceID == s.myID {
continue
}
l.Debugln("Reconnect loop for", deviceID)
s.mut.RLock()
paused := s.model.IsPaused(deviceID)
connected := s.model.ConnectedTo(deviceID)
ct, ok := s.connType[deviceID]
relaysEnabled := s.relaysEnabled
s.mut.RUnlock()
if paused {
continue
}
if connected && ok && ct.IsDirect() {
l.Debugln("Already connected to", deviceID, "via", ct.String())
continue
}
addrs, relays := s.resolveAddresses(deviceID, deviceCfg.Addresses)
for _, addr := range addrs {
if conn := s.connectDirect(deviceID, addr); conn != nil {
l.Debugln("Connecting to", deviceID, "via", addr, "succeeded")
if connected {
s.model.Close(deviceID, fmt.Errorf("switching connections"))
}
s.conns <- model.IntermediateConnection{
Conn: conn,
Type: model.ConnectionTypeDirectDial,
}
continue nextDevice
}
l.Debugln("Connecting to", deviceID, "via", addr, "failed")
}
// Only connect via relays if not already connected
// Also, do not set lastRelayCheck time if we have no relays,
// as otherwise when we do discover relays, we might have to
// wait up to RelayReconnectIntervalM to connect again.
// Also, do not try relays if we are explicitly told not to.
if connected || len(relays) == 0 || !relaysEnabled {
l.Debugln("Not connecting via relay", connected, len(relays) == 0, !relaysEnabled)
continue nextDevice
}
reconIntv := time.Duration(s.cfg.Options().RelayReconnectIntervalM) * time.Minute
if last, ok := s.lastRelayCheck[deviceID]; ok && time.Since(last) < reconIntv {
l.Debugln("Skipping connecting via relay to", deviceID, "last checked at", last)
continue nextDevice
}
l.Debugln("Trying relay connections to", deviceID, relays)
s.lastRelayCheck[deviceID] = time.Now()
for _, addr := range relays {
if conn := s.connectViaRelay(deviceID, addr); conn != nil {
l.Debugln("Connecting to", deviceID, "via", addr, "succeeded")
s.conns <- model.IntermediateConnection{
Conn: conn,
Type: model.ConnectionTypeRelayDial,
}
continue nextDevice
}
l.Debugln("Connecting to", deviceID, "via", addr, "failed")
}
}
time.Sleep(delay)
delay *= 2
if maxD := time.Duration(s.cfg.Options().ReconnectIntervalS) * time.Second; delay > maxD {
delay = maxD
}
}
}
func (s *Service) resolveAddresses(deviceID protocol.DeviceID, inAddrs []string) (addrs []string, relays []discover.Relay) {
for _, addr := range inAddrs {
if addr == "dynamic" {
if s.discoverer != nil {
if t, r, err := s.discoverer.Lookup(deviceID); err == nil {
addrs = append(addrs, t...)
relays = append(relays, r...)
}
}
} else {
addrs = append(addrs, addr)
}
}
return
}
func (s *Service) connectDirect(deviceID protocol.DeviceID, addr string) *tls.Conn {
uri, err := url.Parse(addr)
if err != nil {
l.Infoln("Failed to parse connection url:", addr, err)
return nil
}
dialer, ok := dialers[uri.Scheme]
if !ok {
l.Infoln("Unknown address schema", uri)
return nil
}
l.Debugln("dial", deviceID, uri)
conn, err := dialer(uri, s.tlsCfg)
if err != nil {
l.Debugln("dial failed", deviceID, uri, err)
return nil
}
return conn
}
func (s *Service) connectViaRelay(deviceID protocol.DeviceID, addr discover.Relay) *tls.Conn {
uri, err := url.Parse(addr.URL)
if err != nil {
l.Infoln("Failed to parse relay connection url:", addr, err)
return nil
}
inv, err := client.GetInvitationFromRelay(uri, deviceID, s.tlsCfg.Certificates, 10*time.Second)
if err != nil {
l.Debugf("Failed to get invitation for %s from %s: %v", deviceID, uri, err)
return nil
}
l.Debugln("Succesfully retrieved relay invitation", inv, "from", uri)
conn, err := client.JoinSession(inv)
if err != nil {
l.Debugf("Failed to join relay session %s: %v", inv, err)
return nil
}
l.Debugln("Successfully joined relay session", inv)
var tc *tls.Conn
if inv.ServerSocket {
tc = tls.Server(conn, s.tlsCfg)
} else {
tc = tls.Client(conn, s.tlsCfg)
}
err = tc.Handshake()
if err != nil {
l.Infof("TLS handshake (BEP/relay %s): %v", inv, err)
tc.Close()
return nil
}
return tc
}
func (s *Service) acceptRelayConns() {
for {
conn := s.relayService.Accept()
s.conns <- model.IntermediateConnection{
Conn: conn,
Type: model.ConnectionTypeRelayAccept,
}
}
}
func (s *Service) shouldLimit(addr net.Addr) bool {
if s.cfg.Options().LimitBandwidthInLan {
return true
}
tcpaddr, ok := addr.(*net.TCPAddr)
if !ok {
return true
}
for _, lan := range s.lans {
if lan.Contains(tcpaddr.IP) {
return false
}
}
return !tcpaddr.IP.IsLoopback()
}
func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
return nil
}
func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
s.mut.Lock()
s.relaysEnabled = to.Options.RelaysEnabled
s.mut.Unlock()
// We require a restart if a device as been removed.
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
for _, dev := range to.Devices {
newDevices[dev.DeviceID] = true
}
for _, dev := range from.Devices {
if !newDevices[dev.DeviceID] {
return false
}
}
return true
}
// ExternalAddresses returns a list of addresses that are our best guess for
// where we are reachable from the outside. As a special case, we may return
// one or more addresses with an empty IP address (0.0.0.0 or ::) and just
// port number - this means that the outside address of a NAT gateway should
// be substituted.
func (s *Service) ExternalAddresses() []string {
return s.addresses(false)
}
// AllAddresses returns a list of addresses that are our best guess for where
// we are reachable from the local network. Same conditions as
// ExternalAddresses, but private IPv4 addresses are included.
func (s *Service) AllAddresses() []string {
return s.addresses(true)
}
func (s *Service) addresses(includePrivateIPV4 bool) []string {
var addrs []string
// Grab our listen addresses from the config. Unspecified ones are passed
// on verbatim (to be interpreted by a global discovery server or local
// discovery peer). Public addresses are passed on verbatim. Private
// addresses are filtered.
for _, addrStr := range s.cfg.Options().ListenAddress {
addrURL, err := url.Parse(addrStr)
if err != nil {
l.Infoln("Listen address", addrStr, "is invalid:", err)
continue
}
addr, err := net.ResolveTCPAddr(addrURL.Scheme, addrURL.Host)
if err != nil {
l.Infoln("Listen address", addrStr, "is invalid:", err)
continue
}
if addr.IP == nil || addr.IP.IsUnspecified() {
// Address like 0.0.0.0:22000 or [::]:22000 or :22000; include as is.
addrs = append(addrs, util.Address(addrURL.Scheme, addr.String()))
} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
// A public address; include as is.
addrs = append(addrs, util.Address(addrURL.Scheme, addr.String()))
} else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() {
// A private IPv4 address.
addrs = append(addrs, util.Address(addrURL.Scheme, addr.String()))
}
}
// Add addresses provided by the mappings from the NAT service.
for _, mapping := range s.mappings {
for _, addr := range mapping.ExternalAddresses() {
addrs = append(addrs, fmt.Sprintf("tcp://%s", addr))
}
}
return addrs
}
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 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
}
defer c.SetDeadline(time.Time{})
header := make([]byte, 8)
msg := h.MustMarshalXDR()
binary.BigEndian.PutUint32(header[:4], protocol.HelloMessageMagic)
binary.BigEndian.PutUint32(header[4:], uint32(len(msg)))
if _, err := c.Write(header); err != nil {
return protocol.HelloMessage{}, err
}
if _, err := c.Write(msg); err != nil {
return protocol.HelloMessage{}, err
}
if _, err := io.ReadFull(c, header); err != nil {
return protocol.HelloMessage{}, err
}
if binary.BigEndian.Uint32(header[:4]) != protocol.HelloMessageMagic {
return protocol.HelloMessage{}, fmt.Errorf("incorrect magic")
}
msgSize := binary.BigEndian.Uint32(header[4:])
if msgSize > 1024 {
return protocol.HelloMessage{}, fmt.Errorf("hello message too big")
}
buf := make([]byte, msgSize)
var hello protocol.HelloMessage
if _, err := io.ReadFull(c, buf); err != nil {
return protocol.HelloMessage{}, err
}
if err := hello.UnmarshalXDR(buf); err != nil {
return protocol.HelloMessage{}, err
}
return hello, nil
}
// serviceFunc wraps a function to create a suture.Service without stop
// functionality.
type serviceFunc func()
func (f serviceFunc) Serve() { f() }
func (f serviceFunc) Stop() {}
-99
View File
@@ -1,99 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"net"
"net/url"
"strings"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/model"
)
func init() {
for _, network := range []string{"tcp", "tcp4", "tcp6"} {
dialers[network] = makeTCPDialer(network)
listeners[network] = makeTCPListener(network)
}
}
func makeTCPDialer(network string) DialerFactory {
return func(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) {
// Check that there is a port number in uri.Host, otherwise add one.
host, port, err := net.SplitHostPort(uri.Host)
if err != nil && strings.HasPrefix(err.Error(), "missing port") {
// addr is on the form "1.2.3.4"
uri.Host = net.JoinHostPort(uri.Host, "22000")
} else if err == nil && port == "" {
// addr is on the form "1.2.3.4:"
uri.Host = net.JoinHostPort(host, "22000")
}
// Don't try to resolve the address before dialing. The dialer may be a
// proxy, and we should let the proxy do the resolving in that case.
conn, err := dialer.Dial(network, uri.Host)
if err != nil {
l.Debugln(err)
return nil, err
}
tc := tls.Client(conn, tlsCfg)
err = tc.Handshake()
if err != nil {
tc.Close()
return nil, err
}
return tc, nil
}
}
func makeTCPListener(network string) ListenerFactory {
return func(uri *url.URL, tlsCfg *tls.Config, conns chan<- model.IntermediateConnection) {
tcaddr, err := net.ResolveTCPAddr(network, uri.Host)
if err != nil {
l.Fatalln("listen (BEP/tcp):", err)
return
}
listener, err := net.ListenTCP(network, tcaddr)
if err != nil {
l.Fatalln("listen (BEP/tcp):", err)
return
}
for {
conn, err := listener.Accept()
if err != nil {
l.Warnln("Accepting connection (BEP/tcp):", err)
continue
}
l.Debugln("connect from", conn.RemoteAddr())
err = dialer.SetTCPOptions(conn.(*net.TCPConn))
if err != nil {
l.Infoln(err)
}
tc := tls.Server(conn, tlsCfg)
err = tc.Handshake()
if err != nil {
l.Infoln("TLS handshake (BEP/tcp):", err)
tc.Close()
continue
}
conns <- model.IntermediateConnection{
Conn: tc,
Type: model.ConnectionTypeDirectAccept,
}
}
}
}
+92
View File
@@ -0,0 +1,92 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"net"
"net/url"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/relay/client"
)
const relayPriority = 200
func init() {
dialers["relay"] = relayDialerFactory{}
}
type relayDialer struct {
cfg *config.Wrapper
tlsCfg *tls.Config
}
func (d *relayDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConnection, error) {
inv, err := client.GetInvitationFromRelay(uri, id, d.tlsCfg.Certificates, 10*time.Second)
if err != nil {
return IntermediateConnection{}, err
}
conn, err := client.JoinSession(inv)
if err != nil {
return IntermediateConnection{}, err
}
err = dialer.SetTCPOptions(conn.(*net.TCPConn))
if err != nil {
conn.Close()
return IntermediateConnection{}, err
}
var tc *tls.Conn
if inv.ServerSocket {
tc = tls.Server(conn, d.tlsCfg)
} else {
tc = tls.Client(conn, d.tlsCfg)
}
err = tc.Handshake()
if err != nil {
tc.Close()
return IntermediateConnection{}, err
}
return IntermediateConnection{tc, "Relay (Client)", relayPriority}, nil
}
func (relayDialer) Priority() int {
return relayPriority
}
func (d *relayDialer) RedialFrequency() time.Duration {
return time.Duration(d.cfg.Options().RelayReconnectIntervalM) * time.Minute
}
type relayDialerFactory struct{}
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"
}
+181
View File
@@ -0,0 +1,181 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"net"
"net/url"
"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() {
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
factory listenerFactory
err error
client client.RelayClient
mut sync.RWMutex
}
func (t *relayListener) Serve() {
t.mut.Lock()
t.err = nil
t.mut.Unlock()
clnt, err := client.NewClient(t.uri, t.tlsCfg.Certificates, nil, 10*time.Second)
if err != nil {
t.mut.Lock()
t.err = err
t.mut.Unlock()
l.Warnln("listen (BEP/relay):", err)
return
}
go clnt.Serve()
t.mut.Lock()
t.client = clnt
t.mut.Unlock()
oldURI := clnt.URI()
for {
select {
case inv, ok := <-t.client.Invitations():
if !ok {
return
}
conn, err := client.JoinSession(inv)
if err != nil {
l.Warnln("Joining relay session (BEP/relay):", err)
continue
}
err = dialer.SetTCPOptions(conn.(*net.TCPConn))
if err != nil {
l.Infoln(err)
}
var tc *tls.Conn
if inv.ServerSocket {
tc = tls.Server(conn, t.tlsCfg)
} else {
tc = tls.Client(conn, t.tlsCfg)
}
err = tc.Handshake()
if err != nil {
tc.Close()
l.Infoln("TLS handshake (BEP/relay):", err)
continue
}
t.conns <- IntermediateConnection{tc, "Relay (Server)", relayPriority}
// Poor mans notifier that informs the connection service that the
// relay URI has changed. This can only happen when we connect to a
// relay via dynamic+http(s) pool, which upon a relay failing/dropping
// us, would pick a different one.
case <-time.After(10 * time.Second):
currentURI := clnt.URI()
if currentURI != oldURI {
oldURI = currentURI
t.notifyAddressesChanged(t)
}
}
}
}
func (t *relayListener) Stop() {
t.mut.RLock()
if t.client != nil {
t.client.Stop()
}
t.mut.RUnlock()
}
func (t *relayListener) URI() *url.URL {
return t.uri
}
func (t *relayListener) WANAddresses() []*url.URL {
t.mut.RLock()
client := t.client
t.mut.RUnlock()
if client == nil {
return nil
}
curi := client.URI()
if curi == nil {
return nil
}
return []*url.URL{curi}
}
func (t *relayListener) LANAddresses() []*url.URL {
return t.WANAddresses()
}
func (t *relayListener) Error() error {
t.mut.RLock()
err := t.err
var cerr error
if t.client != nil {
cerr = t.client.Error()
}
t.mut.RUnlock()
if err != nil {
return err
}
return cerr
}
func (t *relayListener) Factory() listenerFactory {
return t.factory
}
func (t *relayListener) String() string {
return t.uri.String()
}
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,
factory: f,
}
}
func (relayListenerFactory) Enabled(cfg config.Configuration) bool {
return cfg.Options.RelaysEnabled
}
+628
View File
@@ -0,0 +1,628 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"net/url"
"time"
"github.com/juju/ratelimit"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/util"
// Registers NAT service providers
_ "github.com/syncthing/syncthing/lib/pmp"
_ "github.com/syncthing/syncthing/lib/upnp"
"github.com/thejerf/suture"
)
var (
dialers = make(map[string]dialerFactory, 0)
listeners = make(map[string]listenerFactory, 0)
)
// Service listens and dials all configured unconnected devices, via supported
// dialers. Successful connections are handed to the model.
type Service struct {
*suture.Supervisor
cfg *config.Wrapper
myID protocol.DeviceID
model Model
tlsCfg *tls.Config
discoverer discover.Finder
conns chan IntermediateConnection
bepProtocolName string
tlsDefaultCommonName string
lans []*net.IPNet
writeRateLimit *ratelimit.Bucket
readRateLimit *ratelimit.Bucket
natService *nat.Service
natServiceToken *suture.ServiceToken
listenersMut sync.RWMutex
listeners map[string]genericListener
listenerTokens map[string]suture.ServiceToken
curConMut sync.Mutex
currentConnection map[protocol.DeviceID]Connection
}
func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder,
bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) *Service {
service := &Service{
Supervisor: suture.NewSimple("connections.Service"),
cfg: cfg,
myID: myID,
model: mdl,
tlsCfg: tlsCfg,
discoverer: discoverer,
conns: make(chan IntermediateConnection),
bepProtocolName: bepProtocolName,
tlsDefaultCommonName: tlsDefaultCommonName,
lans: lans,
natService: nat.NewService(myID, cfg),
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))
}
if service.cfg.Options().MaxRecvKbps > 0 {
service.readRateLimit = ratelimit.NewBucketWithRate(float64(1024*service.cfg.Options().MaxRecvKbps), int64(5*1024*service.cfg.Options().MaxRecvKbps))
}
// There are several moving parts here; one routine per listening address
// (handled in configuration changing) to handle incoming connections,
// one routine to periodically attempt outgoing connections, one routine to
// the the common handling regardless of whether the connection was
// incoming or outgoing.
service.Add(serviceFunc(service.connect))
service.Add(serviceFunc(service.handle))
raw := cfg.Raw()
// Actually starts the listeners and NAT service
service.CommitConfiguration(raw, raw)
return service
}
var (
errDisabled = errors.New("disabled by configuration")
)
func (s *Service) handle() {
next:
for c := range s.conns {
cs := c.ConnectionState()
// We should have negotiated the next level protocol "bep/1.0" as part
// of the TLS handshake. Unfortunately this can't be a hard error,
// because there are implementations out there that don't support
// protocol negotiation (iOS for one...).
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != s.bepProtocolName {
l.Infof("Peer %s did not negotiate bep/1.0", c.RemoteAddr())
}
// We should have received exactly one certificate from the other
// side. If we didn't, they don't have a device ID and we drop the
// connection.
certs := cs.PeerCertificates
if cl := len(certs); cl != 1 {
l.Infof("Got peer certificate list of length %d != 1 from %s; protocol error", cl, c.RemoteAddr())
c.Close()
continue
}
remoteCert := certs[0]
remoteID := protocol.NewDeviceID(remoteCert.Raw)
// The device ID should not be that of ourselves. It can happen
// though, especially in the presence of NAT hairpinning, multiple
// clients between the same NAT gateway, and global discovery.
if remoteID == s.myID {
l.Infof("Connected to myself (%s) - should not happen", remoteID)
c.Close()
continue
}
hello, err := exchangeHello(c, s.model.GetHello(remoteID))
if err != nil {
l.Infof("Failed to exchange Hello messages with %s (%s): %s", remoteID, c.RemoteAddr(), err)
c.Close()
continue
}
s.model.OnHello(remoteID, c.RemoteAddr(), hello)
// 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.curConMut.Lock()
ct, ok := s.currentConnection[remoteID]
s.curConMut.Unlock()
// Lower priority is better, just like nice etc.
if ok && ct.Priority > c.Priority {
l.Debugln("Switching connections", remoteID)
s.model.Close(remoteID, protocol.ErrSwitchingConnections)
} else if s.model.ConnectedTo(remoteID) {
// We should not already be connected to the other party. TODO: This
// could use some better handling. If the old connection is dead but
// hasn't timed out yet we may want to drop *that* connection and keep
// this one. But in case we are two devices connecting to each other
// in parallel we don't want to do that or we end up with no
// connections still established...
l.Infof("Connected to already connected device (%s)", remoteID)
c.Close()
continue
} else if s.model.IsPaused(remoteID) {
l.Infof("Connection from paused device (%s)", remoteID)
c.Close()
continue
}
for deviceID, deviceCfg := range s.cfg.Devices() {
if deviceID == remoteID {
// Verify the name on the certificate. By default we set it to
// "syncthing" when generating, but the user may have replaced
// the certificate and used another name.
certName := deviceCfg.CertName
if certName == "" {
certName = s.tlsDefaultCommonName
}
err := remoteCert.VerifyHostname(certName)
if err != nil {
// Incorrect certificate name is something the user most
// likely wants to know about, since it's an advanced
// config. Warn instead of Info.
l.Warnf("Bad certificate from %s (%v): %v", remoteID, c.RemoteAddr(), err)
c.Close()
continue next
}
// If rate limiting is set, and based on the address we should
// limit the connection, then we wrap it in a limiter.
limit := s.shouldLimit(c.RemoteAddr())
wr := io.Writer(c)
if limit && s.writeRateLimit != nil {
wr = NewWriteLimiter(c, s.writeRateLimit)
}
rd := io.Reader(c)
if limit && s.readRateLimit != nil {
rd = NewReadLimiter(c, s.readRateLimit)
}
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type)
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
modelConn := Connection{c, protoConn}
l.Infof("Established secure connection to %s at %s", remoteID, name)
l.Debugf("cipher suite: %04X in lan: %t", c.ConnectionState().CipherSuite, !limit)
s.model.AddConnection(modelConn, hello)
s.curConMut.Lock()
s.currentConnection[remoteID] = modelConn
s.curConMut.Unlock()
continue next
}
}
l.Infof("Connection from %s (%s) with ignored device ID %s", c.RemoteAddr(), c.Type, remoteID)
c.Close()
}
}
func (s *Service) connect() {
nextDial := make(map[string]time.Time)
// 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 _, deviceCfg := range cfg.Devices {
deviceID := deviceCfg.DeviceID
if deviceID == s.myID {
continue
}
paused := s.model.IsPaused(deviceID)
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" {
if s.discoverer != nil {
if t, err := s.discoverer.Lookup(deviceID); err == nil {
addrs = append(addrs, t...)
}
}
} else {
addrs = append(addrs, addr)
}
}
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.Infof("Dialer for %s: %v", addr, err)
continue
}
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
}
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)
continue
}
if connected {
s.model.Close(deviceID, protocol.ErrSwitchingConnections)
}
s.conns <- conn
continue nextDevice
}
}
nextDial, sleep = filterAndFindSleepDuration(nextDial, seen, now)
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)
}
}
}
func (s *Service) shouldLimit(addr net.Addr) bool {
if s.cfg.Options().LimitBandwidthInLan {
return true
}
tcpaddr, ok := addr.(*net.TCPAddr)
if !ok {
return true
}
for _, lan := range s.lans {
if lan.Contains(tcpaddr.IP) {
return false
}
}
return !tcpaddr.IP.IsLoopback()
}
func (s *Service) createListener(factory listenerFactory, uri *url.URL) bool {
// must be called with listenerMut held
l.Debugln("Starting listener", uri)
listener := factory.New(uri, s.cfg, s.tlsCfg, s.conns, s.natService)
listener.OnAddressesChanged(s.logListenAddressesChangedEvent)
s.listeners[uri.String()] = listener
s.listenerTokens[uri.String()] = s.Add(listener)
return true
}
func (s *Service) logListenAddressesChangedEvent(l genericListener) {
events.Default.Log(events.ListenAddressesChanged, map[string]interface{}{
"address": l.URI(),
"lan": l.LANAddresses(),
"wan": l.WANAddresses(),
})
}
func (s *Service) VerifyConfiguration(from, to config.Configuration) error {
return nil
}
func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
// We require a restart if a device as been removed.
restart := false
newDevices := make(map[protocol.DeviceID]bool, len(to.Devices))
for _, dev := range to.Devices {
newDevices[dev.DeviceID] = true
}
for _, dev := range from.Devices {
if !newDevices[dev.DeviceID] {
restart = true
}
}
s.listenersMut.Lock()
seen := make(map[string]struct{})
for _, addr := range config.Wrap("", to).ListenAddresses() {
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{}{}
}
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.listenersMut.Unlock()
if to.Options.NATEnabled && s.natServiceToken == nil {
l.Debugln("Starting NAT service")
token := s.Add(s.natService)
s.natServiceToken = &token
} else if !to.Options.NATEnabled && s.natServiceToken != nil {
l.Debugln("Stopping NAT service")
s.Remove(*s.natServiceToken)
s.natServiceToken = nil
}
return !restart
}
func (s *Service) AllAddresses() []string {
s.listenersMut.RLock()
var addrs []string
for _, listener := range s.listeners {
for _, lanAddr := range listener.LANAddresses() {
addrs = append(addrs, lanAddr.String())
}
for _, wanAddr := range listener.WANAddresses() {
addrs = append(addrs, wanAddr.String())
}
}
s.listenersMut.RUnlock()
return util.UniqueStrings(addrs)
}
func (s *Service) ExternalAddresses() []string {
s.listenersMut.RLock()
var addrs []string
for _, listener := range s.listeners {
for _, wanAddr := range listener.WANAddresses() {
addrs = append(addrs, wanAddr.String())
}
}
s.listenersMut.RUnlock()
return util.UniqueStrings(addrs)
}
func (s *Service) Status() map[string]interface{} {
s.listenersMut.RLock()
result := make(map[string]interface{})
for addr, listener := range s.listeners {
status := make(map[string]interface{})
err := listener.Error()
if err != nil {
status["error"] = err.Error()
}
status["lanAddresses"] = urlsToStrings(listener.LANAddresses())
status["wanAddresses"] = urlsToStrings(listener.WANAddresses())
result[addr] = status
}
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
}
defer c.SetDeadline(time.Time{})
header := make([]byte, 8)
msg := h.MustMarshalXDR()
binary.BigEndian.PutUint32(header[:4], protocol.HelloMessageMagic)
binary.BigEndian.PutUint32(header[4:], uint32(len(msg)))
if _, err := c.Write(header); err != nil {
return protocol.HelloMessage{}, err
}
if _, err := c.Write(msg); err != nil {
return protocol.HelloMessage{}, err
}
if _, err := io.ReadFull(c, header); err != nil {
return protocol.HelloMessage{}, err
}
if binary.BigEndian.Uint32(header[:4]) != protocol.HelloMessageMagic {
return protocol.HelloMessage{}, fmt.Errorf("incorrect magic")
}
msgSize := binary.BigEndian.Uint32(header[4:])
if msgSize > 1024 {
return protocol.HelloMessage{}, fmt.Errorf("hello message too big")
}
buf := make([]byte, msgSize)
var hello protocol.HelloMessage
if _, err := io.ReadFull(c, buf); err != nil {
return protocol.HelloMessage{}, err
}
if err := hello.UnmarshalXDR(buf); err != nil {
return protocol.HelloMessage{}, err
}
return hello, nil
}
func filterAndFindSleepDuration(nextDial map[string]time.Time, seen []string, now time.Time) (map[string]time.Time, time.Duration) {
newNextDial := make(map[string]time.Time)
for _, addr := range seen {
nextDialAt, ok := nextDial[addr]
if ok {
newNextDial[addr] = nextDialAt
}
}
min := time.Minute
for _, next := range newNextDial {
cur := next.Sub(now)
if cur < min {
min = cur
}
}
return newNextDial, min
}
func urlsToStrings(urls []*url.URL) []string {
strings := make([]string, len(urls))
for i, url := range urls {
strings[i] = url.String()
}
return strings
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"net"
"net/url"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/nat"
"github.com/syncthing/syncthing/lib/protocol"
)
type IntermediateConnection struct {
*tls.Conn
Type string
Priority int
}
type Connection struct {
IntermediateConnection
protocol.Connection
}
type dialerFactory interface {
New(*config.Wrapper, *tls.Config) genericDialer
Priority() int
Enabled(config.Configuration) bool
String() string
}
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()
Stop()
URI() *url.URL
// A given address can potentially be mutated by the listener.
// For example we bind to tcp://0.0.0.0, but that for example might return
// tcp://gateway1.ip and tcp://gateway2.ip as WAN addresses due to there
// being multiple gateways, and us managing to get a UPnP mapping on both
// and tcp://192.168.0.1 and tcp://10.0.0.1 due to there being multiple
// network interfaces. (The later case for LAN addresses is made up just
// to provide an example)
WANAddresses() []*url.URL
LANAddresses() []*url.URL
Error() error
OnAddressesChanged(func(genericListener))
String() string
Factory() listenerFactory
}
type Model interface {
protocol.Model
AddConnection(conn Connection, hello protocol.HelloMessage)
ConnectedTo(remoteID protocol.DeviceID) bool
IsPaused(remoteID protocol.DeviceID) bool
OnHello(protocol.DeviceID, net.Addr, protocol.HelloMessage)
GetHello(protocol.DeviceID) protocol.HelloMessage
}
// serviceFunc wraps a function to create a suture.Service without stop
// functionality.
type serviceFunc func()
func (f serviceFunc) Serve() { f() }
func (f serviceFunc) Stop() {}
type onAddressesChangedNotifier struct {
callbacks []func(genericListener)
}
func (o *onAddressesChangedNotifier) OnAddressesChanged(callback func(genericListener)) {
o.callbacks = append(o.callbacks, callback)
}
func (o *onAddressesChangedNotifier) notifyAddressesChanged(l genericListener) {
for _, callback := range o.callbacks {
callback(l)
}
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"net"
"net/url"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/dialer"
"github.com/syncthing/syncthing/lib/protocol"
)
const tcpPriority = 10
func init() {
factory := &tcpDialerFactory{}
for _, scheme := range []string{"tcp", "tcp4", "tcp6"} {
dialers[scheme] = factory
}
}
type tcpDialer struct {
cfg *config.Wrapper
tlsCfg *tls.Config
}
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)
if err != nil {
l.Debugln(err)
return IntermediateConnection{}, err
}
tc := tls.Client(conn, d.tlsCfg)
err = tc.Handshake()
if err != nil {
tc.Close()
return IntermediateConnection{}, err
}
return IntermediateConnection{tc, "TCP (Client)", tcpPriority}, nil
}
func (d *tcpDialer) RedialFrequency() time.Duration {
return time.Duration(d.cfg.Options().ReconnectIntervalS) * time.Second
}
type tcpDialerFactory struct{}
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"
}
+229
View File
@@ -0,0 +1,229 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package connections
import (
"crypto/tls"
"net"
"net/url"
"strings"
"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] = factory
}
}
type tcpListener struct {
onAddressesChangedNotifier
uri *url.URL
tlsCfg *tls.Config
stop chan struct{}
conns chan IntermediateConnection
factory listenerFactory
natService *nat.Service
mapping *nat.Mapping
err error
mut sync.RWMutex
}
func (t *tcpListener) Serve() {
t.mut.Lock()
t.err = nil
t.mut.Unlock()
tcaddr, err := net.ResolveTCPAddr(t.uri.Scheme, t.uri.Host)
if err != nil {
t.mut.Lock()
t.err = err
t.mut.Unlock()
l.Infoln("listen (BEP/tcp):", err)
return
}
listener, err := net.ListenTCP(t.uri.Scheme, tcaddr)
if err != nil {
t.mut.Lock()
t.err = err
t.mut.Unlock()
l.Infoln("listen (BEP/tcp):", err)
return
}
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)
})
defer t.natService.RemoveMapping(mapping)
t.mut.Lock()
t.mapping = mapping
t.mut.Unlock()
for {
listener.SetDeadline(time.Now().Add(time.Second))
conn, err := listener.Accept()
select {
case <-t.stop:
if err == nil {
conn.Close()
}
t.mut.Lock()
t.mapping = nil
t.mut.Unlock()
return
default:
}
if err != nil {
if err, ok := err.(*net.OpError); !ok || !err.Timeout() {
l.Warnln("Accepting connection (BEP/tcp):", err)
}
continue
}
l.Debugln("connect from", conn.RemoteAddr())
err = dialer.SetTCPOptions(conn.(*net.TCPConn))
if err != nil {
l.Infoln(err)
}
tc := tls.Server(conn, t.tlsCfg)
err = tc.Handshake()
if err != nil {
l.Infoln("TLS handshake (BEP/tcp):", err)
tc.Close()
continue
}
t.conns <- IntermediateConnection{tc, "TCP (Server)", tcpPriority}
}
}
func (t *tcpListener) Stop() {
close(t.stop)
}
func (t *tcpListener) URI() *url.URL {
return t.uri
}
func (t *tcpListener) WANAddresses() []*url.URL {
uris := t.LANAddresses()
t.mut.RLock()
if t.mapping != nil {
addrs := t.mapping.ExternalAddresses()
for _, addr := range addrs {
uri := *t.uri
// Does net.JoinHostPort internally
uri.Host = addr.String()
uris = append(uris, &uri)
}
}
t.mut.RUnlock()
return uris
}
func (t *tcpListener) LANAddresses() []*url.URL {
return []*url.URL{t.uri}
}
func (t *tcpListener) Error() error {
t.mut.RLock()
err := t.err
t.mut.RUnlock()
return err
}
func (t *tcpListener) String() string {
return t.uri.String()
}
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 (tcpListenerFactory) Enabled(cfg config.Configuration) bool {
return true
}
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 fixupPort(uri *url.URL) *url.URL {
copyURI := *uri
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")
} else if err == nil && port == "" {
// addr is on the form "1.2.3.4:"
copyURI.Host = net.JoinHostPort(host, "22000")
}
return &copyURI
}
+20 -4
View File
@@ -91,10 +91,6 @@ func newDBInstance(db *leveldb.DB) *Instance {
return i
}
func (db *Instance) Compact() error {
return db.CompactRange(util.Range{})
}
func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker, deleteFn deletionHandler) int64 {
sort.Sort(fileList(fs)) // sort list on name, same as in the database
@@ -266,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
@@ -363,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
@@ -374,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 {
+8
View File
@@ -23,6 +23,7 @@ var (
l = logger.DefaultLogger.NewFacility("dialer", "Dialing connections")
proxyDialer = getDialer(proxy.Direct)
usingProxy = proxyDialer != proxy.Direct
noFallback = os.Getenv("ALL_PROXY_NO_FALLBACK") != ""
)
type dialFunc func(network, addr string) (net.Conn, error)
@@ -40,6 +41,9 @@ func init() {
go func() {
time.Sleep(500 * time.Millisecond)
l.Infoln("Proxy settings detected")
if noFallback {
l.Infoln("Proxy fallback disabled")
}
}()
} else {
go func() {
@@ -62,6 +66,10 @@ func dialWithFallback(proxyDialFunc dialFunc, fallbackDialFunc dialFunc, network
}
l.Debugf("Dialing %s address %s via proxy - error %s", network, addr, err)
if noFallback {
return conn, err
}
conn, err = fallbackDialFunc(network, addr)
if err == nil {
l.Debugf("Dialing %s address %s via fallback - success, %s -> %s", network, addr, conn.LocalAddr(), conn.RemoteAddr())
+15 -51
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).
@@ -78,8 +78,8 @@ func (m *cachingMux) Add(finder Finder, cacheTime, negCacheTime time.Duration, p
// Lookup attempts to resolve the device ID using any of the added Finders,
// while obeying the cache settings.
func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) {
var pdirect []prioritizedAddress
func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (addresses []string, err error) {
var paddresses []prioritizedAddress
m.mut.RLock()
for i, finder := range m.finders {
@@ -90,10 +90,9 @@ func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays
// It's a positive, valid entry. Use it.
l.Debugln("cached discovery entry for", deviceID, "at", finder)
l.Debugln(" cache:", cacheEntry)
for _, addr := range cacheEntry.Direct {
pdirect = append(pdirect, prioritizedAddress{finder.priority, addr})
for _, addr := range cacheEntry.Addresses {
paddresses = append(paddresses, prioritizedAddress{finder.priority, addr})
}
relays = append(relays, cacheEntry.Relays...)
continue
}
@@ -109,19 +108,16 @@ func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays
}
// Perform the actual lookup and cache the result.
if td, tr, err := finder.Lookup(deviceID); err == nil {
if addrs, err := finder.Lookup(deviceID); err == nil {
l.Debugln("lookup for", deviceID, "at", finder)
l.Debugln(" direct:", td)
l.Debugln(" relays:", tr)
for _, addr := range td {
pdirect = append(pdirect, prioritizedAddress{finder.priority, addr})
l.Debugln(" addresses:", addrs)
for _, addr := range addrs {
paddresses = append(paddresses, prioritizedAddress{finder.priority, addr})
}
relays = append(relays, tr...)
m.caches[i].Set(deviceID, CacheEntry{
Direct: td,
Relays: tr,
when: time.Now(),
found: len(td)+len(tr) > 0,
Addresses: addrs,
when: time.Now(),
found: len(addrs) > 0,
})
} else {
// Lookup returned error, add a negative cache entry.
@@ -137,13 +133,11 @@ func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays
}
m.mut.RUnlock()
direct = uniqueSortedAddrs(pdirect)
relays = uniqueSortedRelays(relays)
addresses = uniqueSortedAddrs(paddresses)
l.Debugln("lookup results for", deviceID)
l.Debugln(" direct: ", direct)
l.Debugln(" relays: ", relays)
l.Debugln(" addresses: ", addresses)
return direct, relays, nil
return addresses, nil
}
func (m *cachingMux) String() string {
@@ -245,36 +239,6 @@ func uniqueSortedAddrs(ss []prioritizedAddress) []string {
return filtered
}
func uniqueSortedRelays(rs []Relay) []Relay {
m := make(map[string]Relay, len(rs))
for _, r := range rs {
m[r.URL] = r
}
var ur = make([]Relay, 0, len(m))
for _, r := range m {
ur = append(ur, r)
}
sort.Sort(relayList(ur))
return ur
}
type relayList []Relay
func (l relayList) Len() int {
return len(l)
}
func (l relayList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l relayList) Less(a, b int) bool {
return l[a].URL < l[b].URL
}
type prioritizedAddressList []prioritizedAddress
func (l prioritizedAddressList) Len() int {
+18 -27
View File
@@ -15,13 +15,13 @@ import (
)
func TestCacheUnique(t *testing.T) {
direct0 := []string{"tcp://192.0.2.44:22000", "tcp://192.0.2.42:22000"} // prio 0
direct1 := []string{"tcp://192.0.2.43:22000", "tcp://192.0.2.42:22000"} // prio 1
addresses0 := []string{"tcp://192.0.2.44:22000", "tcp://192.0.2.42:22000"} // prio 0
addresses1 := []string{"tcp://192.0.2.43:22000", "tcp://192.0.2.42:22000"} // prio 1
// what we expect from just direct0
direct0Sorted := []string{"tcp://192.0.2.42:22000", "tcp://192.0.2.44:22000"}
// what we expect from just addresses0
addresses0Sorted := []string{"tcp://192.0.2.42:22000", "tcp://192.0.2.44:22000"}
// what we expect from direct0+direct1
// what we expect from addresses0+addresses1
totalSorted := []string{
// first prio 0, sorted
"tcp://192.0.2.42:22000", "tcp://192.0.2.44:22000",
@@ -30,8 +30,6 @@ func TestCacheUnique(t *testing.T) {
// no duplicate .42
}
relays := []Relay{{URL: "relay://192.0.2.44:443"}, {URL: "tcp://192.0.2.45:443"}}
c := NewCachingMux()
c.(*cachingMux).ServeBackground()
defer c.Stop()
@@ -39,45 +37,38 @@ func TestCacheUnique(t *testing.T) {
// Add a fake discovery service and verify we get it's answers through the
// cache.
f1 := &fakeDiscovery{direct0, relays}
f1 := &fakeDiscovery{addresses0}
c.Add(f1, time.Minute, 0, 0)
dir, rel, err := c.Lookup(protocol.LocalDeviceID)
addr, err := c.Lookup(protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(dir, direct0Sorted) {
t.Errorf("Incorrect direct; %+v != %+v", dir, direct0Sorted)
}
if !reflect.DeepEqual(rel, relays) {
t.Errorf("Incorrect relays; %+v != %+v", rel, relays)
if !reflect.DeepEqual(addr, addresses0Sorted) {
t.Errorf("Incorrect addresses; %+v != %+v", addr, addresses0Sorted)
}
// Add one more that answers in the same way and check that we don't
// duplicate or otherwise mess up the responses now.
f2 := &fakeDiscovery{direct1, relays}
f2 := &fakeDiscovery{addresses1}
c.Add(f2, time.Minute, 0, 1)
dir, rel, err = c.Lookup(protocol.LocalDeviceID)
addr, err = c.Lookup(protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(dir, totalSorted) {
t.Errorf("Incorrect direct; %+v != %+v", dir, totalSorted)
}
if !reflect.DeepEqual(rel, relays) {
t.Errorf("Incorrect relays; %+v != %+v", rel, relays)
if !reflect.DeepEqual(addr, totalSorted) {
t.Errorf("Incorrect addresses; %+v != %+v", addr, totalSorted)
}
}
type fakeDiscovery struct {
direct []string
relays []Relay
addresses []string
}
func (f *fakeDiscovery) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) {
return f.direct, f.relays, nil
func (f *fakeDiscovery) Lookup(deviceID protocol.DeviceID) (addresses []string, err error) {
return f.addresses, nil
}
func (f *fakeDiscovery) Error() error {
@@ -126,10 +117,10 @@ type slowDiscovery struct {
started chan struct{}
}
func (f *slowDiscovery) Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error) {
func (f *slowDiscovery) Lookup(deviceID protocol.DeviceID) (addresses []string, err error) {
close(f.started)
time.Sleep(f.delay)
return nil, nil, nil
return nil, nil
}
func (f *slowDiscovery) Error() error {
+2 -9
View File
@@ -15,15 +15,14 @@ import (
// A Finder provides lookup services of some kind.
type Finder interface {
Lookup(deviceID protocol.DeviceID) (direct []string, relays []Relay, err error)
Lookup(deviceID protocol.DeviceID) (address []string, err error)
Error() error
String() string
Cache() map[protocol.DeviceID]CacheEntry
}
type CacheEntry struct {
Direct []string `json:"direct"`
Relays []Relay `json:"relays"`
Addresses []string `json:"addresses"`
when time.Time // When did we get the result
found bool // Is it a success (cacheTime applies) or a failure (negCacheTime applies)?
validUntil time.Time // Validity time, overrides normal calculation
@@ -41,12 +40,6 @@ type FinderMux interface {
ChildStatus() map[string]error
}
// The RelayStatusProvider answers questions about current relay status.
type RelayStatusProvider interface {
Relays() []string
RelayStatus(uri string) (time.Duration, bool)
}
// The AddressLister answers questions about what addresses we are listening
// on.
type AddressLister interface {
+22 -30
View File
@@ -12,6 +12,7 @@ import (
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
@@ -26,7 +27,6 @@ import (
type globalClient struct {
server string
addrList AddressLister
relayStat RelayStatusProvider
announceClient httpClient
queryClient httpClient
noAnnounce bool
@@ -46,8 +46,7 @@ const (
)
type announcement struct {
Direct []string `json:"direct"`
Relays []Relay `json:"relays"`
Addresses []string `json:"addresses"`
}
type serverOptions struct {
@@ -66,7 +65,7 @@ func (e lookupError) CacheFor() time.Duration {
return e.cacheFor
}
func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
func NewGlobal(server string, cert tls.Certificate, addrList AddressLister) (FinderService, error) {
server, opts, err := parseOptions(server)
if err != nil {
return nil, err
@@ -117,7 +116,6 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, rela
cl := &globalClient{
server: server,
addrList: addrList,
relayStat: relayStat,
announceClient: announceClient,
queryClient: queryClient,
noAnnounce: opts.noAnnounce,
@@ -128,12 +126,11 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, rela
return cl, nil
}
// Lookup returns the list of addresses where the given device is available;
// direct, and via relays.
func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
// Lookup returns the list of addresses where the given device is available
func (c *globalClient) Lookup(device protocol.DeviceID) (addresses []string, err error) {
qURL, err := url.Parse(c.server)
if err != nil {
return nil, nil, err
return nil, err
}
q := qURL.Query()
@@ -143,7 +140,7 @@ func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays
resp, err := c.queryClient.Get(qURL.String())
if err != nil {
l.Debugln("globalClient.Lookup", qURL, err)
return nil, nil, err
return nil, err
}
if resp.StatusCode != 200 {
resp.Body.Close()
@@ -155,13 +152,18 @@ func (c *globalClient) Lookup(device protocol.DeviceID) (direct []string, relays
cacheFor: time.Duration(secs) * time.Second,
}
}
return nil, nil, 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()
return ann.Direct, ann.Relays, err
var ann announcement
err = json.Unmarshal(bs, &ann)
return ann.Addresses, err
}
func (c *globalClient) String() string {
@@ -179,13 +181,15 @@ func (c *globalClient) Serve() {
timer := time.NewTimer(0)
defer timer.Stop()
eventSub := events.Default.Subscribe(events.ExternalPortMappingChanged | events.RelayStateChanged)
eventSub := events.Default.Subscribe(events.ListenAddressesChanged)
defer events.Default.Unsubscribe(eventSub)
for {
select {
case <-eventSub.C():
c.sendAnnouncement(timer)
// Defer announcement by 2 seconds, essentially debouncing
// if we have a stream of events incoming in quick succession.
timer.Reset(2 * time.Second)
case <-timer.C:
c.sendAnnouncement(timer)
@@ -200,22 +204,10 @@ func (c *globalClient) sendAnnouncement(timer *time.Timer) {
var ann announcement
if c.addrList != nil {
ann.Direct = c.addrList.ExternalAddresses()
ann.Addresses = c.addrList.ExternalAddresses()
}
if c.relayStat != nil {
for _, relay := range c.relayStat.Relays() {
latency, ok := c.relayStat.RelayStatus(relay)
if ok {
ann.Relays = append(ann.Relays, Relay{
URL: relay,
Latency: int32(latency / time.Millisecond),
})
}
}
}
if len(ann.Direct)+len(ann.Relays) == 0 {
if len(ann.Addresses) == 0 {
c.setError(errors.New("nothing to announce"))
l.Debugln("Nothing to announce")
timer.Reset(announceErrorRetryInterval)
+22 -43
View File
@@ -54,15 +54,15 @@ func TestGlobalOverHTTP(t *testing.T) {
// is only allowed in combination with the "insecure" and "noannounce"
// parameters.
if _, err := NewGlobal("http://192.0.2.42/", tls.Certificate{}, nil, nil); err == nil {
if _, err := NewGlobal("http://192.0.2.42/", tls.Certificate{}, nil); err == nil {
t.Fatal("http is not allowed without insecure and noannounce")
}
if _, err := NewGlobal("http://192.0.2.42/?insecure", tls.Certificate{}, nil, nil); err == nil {
if _, err := NewGlobal("http://192.0.2.42/?insecure", tls.Certificate{}, nil); err == nil {
t.Fatal("http is not allowed without noannounce")
}
if _, err := NewGlobal("http://192.0.2.42/?noannounce", tls.Certificate{}, nil, nil); err == nil {
if _, err := NewGlobal("http://192.0.2.42/?noannounce", tls.Certificate{}, nil); err == nil {
t.Fatal("http is not allowed without insecure")
}
@@ -80,30 +80,27 @@ func TestGlobalOverHTTP(t *testing.T) {
go http.Serve(list, mux)
// This should succeed
direct, relays, err := testLookup("http://" + list.Addr().String() + "?insecure&noannounce")
addresses, err := testLookup("http://" + list.Addr().String() + "?insecure&noannounce")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !testing.Short() {
// This should time out
_, _, err = testLookup("http://" + list.Addr().String() + "/block?insecure&noannounce")
_, err = testLookup("http://" + list.Addr().String() + "/block?insecure&noannounce")
if err == nil {
t.Fatalf("unexpected nil error, should have been a timeout")
}
}
// This should work again
_, _, err = testLookup("http://" + list.Addr().String() + "?insecure&noannounce")
_, err = testLookup("http://" + list.Addr().String() + "?insecure&noannounce")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
t.Errorf("incorrect direct list: %+v", direct)
}
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
t.Errorf("incorrect relays list: %+v", direct)
if len(addresses) != 1 || addresses[0] != "tcp://192.0.2.42::22000" {
t.Errorf("incorrect addresses list: %+v", addresses)
}
}
@@ -136,7 +133,7 @@ func TestGlobalOverHTTPS(t *testing.T) {
// here so we expect the lookup to fail.
url := "https://" + list.Addr().String()
if _, _, err := testLookup(url); err == nil {
if _, err := testLookup(url); err == nil {
t.Fatalf("unexpected nil error when we should have got a certificate error")
}
@@ -144,21 +141,18 @@ func TestGlobalOverHTTPS(t *testing.T) {
// be accepted.
url = "https://" + list.Addr().String() + "?insecure"
if direct, relays, err := testLookup(url); err != nil {
if addresses, err := testLookup(url); err != nil {
t.Fatalf("unexpected error: %v", err)
} else {
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
t.Errorf("incorrect direct list: %+v", direct)
}
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
t.Errorf("incorrect relays list: %+v", direct)
if len(addresses) != 1 || addresses[0] != "tcp://192.0.2.42::22000" {
t.Errorf("incorrect addresses list: %+v", addresses)
}
}
// With "id" set to something incorrect, the checks should fail again.
url = "https://" + list.Addr().String() + "?id=" + protocol.LocalDeviceID.String()
if _, _, err := testLookup(url); err == nil {
if _, err := testLookup(url); err == nil {
t.Fatalf("unexpected nil error for incorrect discovery server ID")
}
@@ -167,14 +161,11 @@ func TestGlobalOverHTTPS(t *testing.T) {
id := protocol.NewDeviceID(cert.Certificate[0])
url = "https://" + list.Addr().String() + "?id=" + id.String()
if direct, relays, err := testLookup(url); err != nil {
if addresses, err := testLookup(url); err != nil {
t.Fatalf("unexpected error: %v", err)
} else {
if len(direct) != 1 || direct[0] != "tcp://192.0.2.42::22000" {
t.Errorf("incorrect direct list: %+v", direct)
}
if len(relays) != 1 || relays[0] != (Relay{URL: "relay://192.0.2.43:443", Latency: 42}) {
t.Errorf("incorrect relays list: %+v", direct)
if len(addresses) != 1 || addresses[0] != "tcp://192.0.2.42::22000" {
t.Errorf("incorrect addresses list: %+v", addresses)
}
}
}
@@ -204,7 +195,7 @@ func TestGlobalAnnounce(t *testing.T) {
go http.Serve(list, mux)
url := "https://" + list.Addr().String() + "?insecure"
disco, err := NewGlobal(url, cert, new(fakeAddressLister), new(fakeRelayStatus))
disco, err := NewGlobal(url, cert, new(fakeAddressLister))
if err != nil {
t.Fatal(err)
}
@@ -223,17 +214,14 @@ func TestGlobalAnnounce(t *testing.T) {
}
if !strings.Contains(string(s.announce), "tcp://0.0.0.0:22000") {
t.Errorf("announce missing direct address: %s", s.announce)
}
if !strings.Contains(string(s.announce), "relay://192.0.2.42:443") {
t.Errorf("announce missing relay address: %s", s.announce)
t.Errorf("announce missing addresses address: %s", s.announce)
}
}
func testLookup(url string) ([]string, []Relay, error) {
disco, err := NewGlobal(url, tls.Certificate{}, nil, nil)
func testLookup(url string) ([]string, error) {
disco, err := NewGlobal(url, tls.Certificate{}, nil)
if err != nil {
return nil, nil, err
return nil, err
}
go disco.Serve()
defer disco.Stop()
@@ -256,7 +244,7 @@ func (s *fakeDiscoveryServer) handler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(204)
} else {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"direct":["tcp://192.0.2.42::22000"], "relays":[{"url": "relay://192.0.2.43:443", "latency": 42}]}`))
w.Write([]byte(`{"addresses":["tcp://192.0.2.42::22000"], "relays":[{"url": "relay://192.0.2.43:443", "latency": 42}]}`))
}
}
@@ -268,12 +256,3 @@ func (f *fakeAddressLister) ExternalAddresses() []string {
func (f *fakeAddressLister) AllAddresses() []string {
return []string{"tcp://0.0.0.0:22000", "tcp://192.168.0.1:22000"}
}
type fakeRelayStatus struct{}
func (f *fakeRelayStatus) Relays() []string {
return []string{"relay://192.0.2.42:443"}
}
func (f *fakeRelayStatus) RelayStatus(uri string) (time.Duration, bool) {
return 42 * time.Millisecond, true
}
+15 -35
View File
@@ -9,7 +9,6 @@ package discover
import (
"bytes"
"encoding/hex"
"errors"
"io"
"net"
"net/url"
@@ -24,10 +23,9 @@ import (
type localClient struct {
*suture.Supervisor
myID protocol.DeviceID
addrList AddressLister
relayStat RelayStatusProvider
name string
myID protocol.DeviceID
addrList AddressLister
name string
beacon beacon.Interface
localBcastStart time.Time
@@ -42,16 +40,11 @@ const (
CacheLifeTime = 3 * BroadcastInterval
)
var (
ErrIncorrectMagic = errors.New("incorrect magic number")
)
func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister, relayStat RelayStatusProvider) (FinderService, error) {
func NewLocal(id protocol.DeviceID, addr string, addrList AddressLister) (FinderService, error) {
c := &localClient{
Supervisor: suture.NewSimple("local"),
myID: id,
addrList: addrList,
relayStat: relayStat,
localBcastTick: time.Tick(BroadcastInterval),
forcedBcastTick: make(chan time.Time),
localBcastStart: time.Now(),
@@ -94,13 +87,11 @@ func (c *localClient) startLocalIPv6Multicasts(localMCAddr string) {
go c.recvAnnouncements(c.beacon)
}
// Lookup returns a list of addresses the device is available at. Local
// discovery never returns relays.
func (c *localClient) Lookup(device protocol.DeviceID) (direct []string, relays []Relay, err error) {
// Lookup returns a list of addresses the device is available at.
func (c *localClient) Lookup(device protocol.DeviceID) (addresses []string, err error) {
if cache, ok := c.Get(device); ok {
if time.Since(cache.when) < CacheLifeTime {
direct = cache.Direct
relays = cache.Relays
addresses = cache.Addresses
}
}
@@ -123,25 +114,11 @@ func (c *localClient) announcementPkt() Announce {
})
}
var relays []Relay
if c.relayStat != nil {
for _, relay := range c.relayStat.Relays() {
latency, ok := c.relayStat.RelayStatus(relay)
if ok {
relays = append(relays, Relay{
URL: relay,
Latency: int32(latency / time.Millisecond),
})
}
}
}
return Announce{
Magic: AnnouncementMagic,
This: Device{
ID: c.myID[:],
Addresses: addrs,
Relays: relays,
},
}
}
@@ -171,6 +148,11 @@ func (c *localClient) recvAnnouncements(b beacon.Interface) {
continue
}
if pkt.Magic != AnnouncementMagic {
l.Debugf("discover: Incorrect magic from %s: %s != %s", addr, pkt.Magic, AnnouncementMagic)
continue
}
l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
var newDevice bool
@@ -230,17 +212,15 @@ func (c *localClient) registerDevice(src net.Addr, device Device) bool {
}
c.Set(id, CacheEntry{
Direct: validAddresses,
Relays: device.Relays,
when: time.Now(),
found: true,
Addresses: validAddresses,
when: time.Now(),
found: true,
})
if isNewDevice {
events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
"device": id.String(),
"addrs": validAddresses,
"relays": device.Relays,
})
}
+1 -7
View File
@@ -10,7 +10,7 @@
package discover
const (
AnnouncementMagic = 0x9D79BC40
AnnouncementMagic = 0x7D79BC40
)
type Announce struct {
@@ -22,14 +22,8 @@ type Announce struct {
type Device struct {
ID []byte // max:32
Addresses []Address // max:16
Relays []Relay // max:16
}
type Address struct {
URL string // max:2083
}
type Relay struct {
URL string `json:"url"` // max:2083
Latency int32 `json:"latency"`
}
+1 -95
View File
@@ -119,26 +119,18 @@ Device Structure:
\ Zero or more Address Structures \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Number of Relays |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Zero or more Relay Structures \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct Device {
opaque ID<32>;
Address Addresses<16>;
Relay Relays<16>;
}
*/
func (o Device) XDRSize() int {
return 4 + len(o.ID) + xdr.Padding(len(o.ID)) +
4 + xdr.SizeOfSlice(o.Addresses) +
4 + xdr.SizeOfSlice(o.Relays)
4 + xdr.SizeOfSlice(o.Addresses)
}
func (o Device) MarshalXDR() ([]byte, error) {
@@ -169,15 +161,6 @@ func (o Device) MarshalXDRInto(m *xdr.Marshaller) error {
return err
}
}
if l := len(o.Relays); l > 16 {
return xdr.ElementSizeExceeded("Relays", l, 16)
}
m.MarshalUint32(uint32(len(o.Relays)))
for i := range o.Relays {
if err := o.Relays[i].MarshalXDRInto(m); err != nil {
return err
}
}
return m.Error
}
@@ -205,24 +188,6 @@ func (o *Device) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
(&o.Addresses[i]).UnmarshalXDRFrom(u)
}
}
_RelaysSize := int(u.UnmarshalUint32())
if _RelaysSize < 0 {
return xdr.ElementSizeExceeded("Relays", _RelaysSize, 16)
} else if _RelaysSize == 0 {
o.Relays = nil
} else {
if _RelaysSize > 16 {
return xdr.ElementSizeExceeded("Relays", _RelaysSize, 16)
}
if _RelaysSize <= len(o.Relays) {
o.Relays = o.Relays[:_RelaysSize]
} else {
o.Relays = make([]Relay, _RelaysSize)
}
for i := range o.Relays {
(&o.Relays[i]).UnmarshalXDRFrom(u)
}
}
return u.Error
}
@@ -279,62 +244,3 @@ func (o *Address) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
o.URL = u.UnmarshalStringMax(2083)
return u.Error
}
/*
Relay Structure:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ URL (length + padded data) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Latency |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct Relay {
string URL<2083>;
int Latency;
}
*/
func (o Relay) XDRSize() int {
return 4 + len(o.URL) + xdr.Padding(len(o.URL)) + 4
}
func (o Relay) MarshalXDR() ([]byte, error) {
buf := make([]byte, o.XDRSize())
m := &xdr.Marshaller{Data: buf}
return buf, o.MarshalXDRInto(m)
}
func (o Relay) MustMarshalXDR() []byte {
bs, err := o.MarshalXDR()
if err != nil {
panic(err)
}
return bs
}
func (o Relay) MarshalXDRInto(m *xdr.Marshaller) error {
if l := len(o.URL); l > 2083 {
return xdr.ElementSizeExceeded("URL", l, 2083)
}
m.MarshalString(o.URL)
m.MarshalUint32(uint32(o.Latency))
return m.Error
}
func (o *Relay) UnmarshalXDR(bs []byte) error {
u := &xdr.Unmarshaller{Data: bs}
return o.UnmarshalXDRFrom(u)
}
func (o *Relay) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
o.URL = u.UnmarshalStringMax(2083)
o.Latency = int32(u.UnmarshalUint32())
return u.Error
}
+3 -6
View File
@@ -39,8 +39,7 @@ const (
FolderCompletion
FolderErrors
FolderScanProgress
ExternalPortMappingChanged
RelayStateChanged
ListenAddressesChanged
LoginAttempt
AllEvents = (1 << iota) - 1
@@ -90,10 +89,8 @@ func (t EventType) String() string {
return "DeviceResumed"
case FolderScanProgress:
return "FolderScanProgress"
case ExternalPortMappingChanged:
return "ExternalPortMappingChanged"
case RelayStateChanged:
return "RelayStateChanged"
case ListenAddressesChanged:
return "ListenAddressesChanged"
case LoginAttempt:
return "LoginAttempt"
default:
+2 -2
View File
@@ -21,8 +21,8 @@ func TestCache(t *testing.T) {
// Set and check some items
c.set("true", Result{true, true})
c.set("false", Result{false, false})
c.set("true", resultInclude|resultDeletable)
c.set("false", 0)
res, ok = c.get("true")
if !res.IsIgnored() || !res.IsDeletable() || ok != true {
+42 -43
View File
@@ -22,44 +22,45 @@ import (
"github.com/syncthing/syncthing/lib/sync"
)
var notMatched = Result{
include: false,
deletable: false,
}
const (
resultNotMatched Result = 0
resultInclude Result = 1 << iota
resultDeletable = 1 << iota
resultFoldCase = 1 << iota
)
type Pattern struct {
pattern string
match glob.Glob
include bool
foldCase bool
deletable bool
pattern string
match glob.Glob
result Result
}
func (p Pattern) String() string {
ret := p.pattern
if !p.include {
if p.result&resultInclude != resultInclude {
ret = "!" + ret
}
if p.foldCase {
if p.result&resultFoldCase == resultFoldCase {
ret = "(?i)" + ret
}
if p.deletable {
if p.result&resultDeletable == resultDeletable {
ret = "(?d)" + ret
}
return ret
}
type Result struct {
include bool
deletable bool
}
type Result uint8
func (r Result) IsIgnored() bool {
return r.include
return r&resultInclude == resultInclude
}
func (r Result) IsDeletable() bool {
return r.include && r.deletable
return r.IsIgnored() && r&resultDeletable == resultDeletable
}
func (r Result) IsCaseFolded() bool {
return r&resultFoldCase == resultFoldCase
}
type Matcher struct {
@@ -123,14 +124,14 @@ func (m *Matcher) Parse(r io.Reader, file string) error {
func (m *Matcher) Match(file string) (result Result) {
if m == nil {
return notMatched
return resultNotMatched
}
m.mut.Lock()
defer m.mut.Unlock()
if len(m.patterns) == 0 {
return notMatched
return resultNotMatched
}
if m.matches != nil {
@@ -150,28 +151,22 @@ func (m *Matcher) Match(file string) (result Result) {
file = filepath.ToSlash(file)
var lowercaseFile string
for _, pattern := range m.patterns {
if pattern.foldCase {
if pattern.result.IsCaseFolded() {
if lowercaseFile == "" {
lowercaseFile = strings.ToLower(file)
}
if pattern.match.Match(lowercaseFile) {
return Result{
pattern.include,
pattern.deletable,
}
return pattern.result
}
} else {
if pattern.match.Match(file) {
return Result{
pattern.include,
pattern.deletable,
}
return pattern.result
}
}
}
// Default to false.
return notMatched
// Default to not matching.
return resultNotMatched
}
// Patterns return a list of the loaded patterns, as they've been parsed
@@ -244,11 +239,15 @@ func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) {
func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]Pattern, error) {
var patterns []Pattern
defaultResult := resultInclude
if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
defaultResult |= resultFoldCase
}
addPattern := func(line string) error {
pattern := Pattern{
pattern: line,
include: true,
foldCase: runtime.GOOS == "darwin" || runtime.GOOS == "windows",
pattern: line,
result: defaultResult,
}
// Allow prefixes to be specified in any order, but only once.
@@ -258,21 +257,21 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
if strings.HasPrefix(line, "!") && !seenPrefix[0] {
seenPrefix[0] = true
line = line[1:]
pattern.include = false
pattern.result ^= resultInclude
} else if strings.HasPrefix(line, "(?i)") && !seenPrefix[1] {
seenPrefix[1] = true
pattern.foldCase = true
pattern.result |= resultFoldCase
line = line[4:]
} else if strings.HasPrefix(line, "(?d)") && !seenPrefix[2] {
seenPrefix[2] = true
pattern.deletable = true
pattern.result |= resultDeletable
line = line[4:]
} else {
break
}
}
if pattern.foldCase {
if pattern.result.IsCaseFolded() {
line = strings.ToLower(line)
}
@@ -281,14 +280,14 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
// 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)
@@ -296,7 +295,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 ") {
@@ -312,7 +311,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)
@@ -320,7 +319,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)
}
+32 -2
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,33 @@ 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)
}
}
}
-52
View File
@@ -1,52 +0,0 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package model
import (
"crypto/tls"
"net"
"github.com/syncthing/syncthing/lib/protocol"
)
type IntermediateConnection struct {
*tls.Conn
Type ConnectionType
}
type Connection struct {
net.Conn
protocol.Connection
Type ConnectionType
}
const (
ConnectionTypeDirectAccept ConnectionType = iota
ConnectionTypeDirectDial
ConnectionTypeRelayAccept
ConnectionTypeRelayDial
)
type ConnectionType int
func (t ConnectionType) String() string {
switch t {
case ConnectionTypeDirectAccept:
return "direct-accept"
case ConnectionTypeDirectDial:
return "direct-dial"
case ConnectionTypeRelayAccept:
return "relay-accept"
case ConnectionTypeRelayDial:
return "relay-dial"
}
return "unknown"
}
func (t ConnectionType) IsDirect() bool {
return t == ConnectionTypeDirectAccept || t == ConnectionTypeDirectDial
}
+7 -5
View File
@@ -28,7 +28,7 @@ type deviceFolderDownloadState struct {
numberOfBlocksInProgress int
}
// 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()
@@ -90,14 +90,16 @@ func (p *deviceFolderDownloadState) NumberOfBlocksInProgress() int {
// deviceDownloadState represents the state of all in progress downloads
// for all folders of a specific device.
type deviceDownloadState struct {
mut sync.RWMutex
folders map[string]*deviceFolderDownloadState
numberOfBlocksInProgress int
mut sync.RWMutex
folders map[string]*deviceFolderDownloadState
}
// Update updates internal state of what has been downloaded into the temporary
// files by the remote device for this specific folder.
func (t *deviceDownloadState) Update(folder string, updates []protocol.FileDownloadProgressUpdate) {
if t == nil {
return
}
t.mut.RLock()
f, ok := t.folders[folder]
t.mut.RUnlock()
@@ -115,7 +117,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 {
+53
View File
@@ -0,0 +1,53 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package model
import "time"
type folder struct {
stateTracker
scan folderscan
model *Model
stop chan struct{}
}
func (f *folder) IndexUpdated() {
}
func (f *folder) DelayScan(next time.Duration) {
f.scan.Delay(next)
}
func (f *folder) Scan(subdirs []string) error {
return f.scan.Scan(subdirs)
}
func (f *folder) Stop() {
close(f.stop)
}
func (f *folder) Jobs() ([]string, []string) {
return nil, nil
}
func (f *folder) BringToFront(string) {}
func (f *folder) scanSubdirsIfHealthy(subDirs []string) error {
if err := f.model.CheckFolderHealth(f.folderID); err != nil {
l.Infoln("Skipping folder", f.folderID, "scan due to folder error:", err)
return err
}
l.Debugln(f, "Scanning subdirectories")
if err := f.model.internalScanFolderSubdirs(f.folderID, subDirs); err != nil {
// Potentially sets the error twice, once in the scanner just
// by doing a check, and once here, if the error returned is
// the same one as returned by CheckFolderHealth, though
// duplicate set is handled by setError.
f.setError(err)
return err
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package model
import (
"math/rand"
"time"
)
type rescanRequest struct {
subdirs []string
err chan error
}
// bundle all folder scan activity
type folderscan struct {
interval time.Duration
timer *time.Timer
now chan rescanRequest
delay chan time.Duration
}
func (s *folderscan) reschedule() {
if s.interval == 0 {
return
}
// Sleep a random time between 3/4 and 5/4 of the configured interval.
sleepNanos := (s.interval.Nanoseconds()*3 + rand.Int63n(2*s.interval.Nanoseconds())) / 4
interval := time.Duration(sleepNanos) * time.Nanosecond
l.Debugln(s, "next rescan in", interval)
s.timer.Reset(interval)
}
func (s *folderscan) Scan(subdirs []string) error {
req := rescanRequest{
subdirs: subdirs,
err: make(chan error),
}
s.now <- req
return <-req.err
}
func (s *folderscan) Delay(next time.Duration) {
s.delay <- next
}
+4 -4
View File
@@ -38,7 +38,7 @@ func (s folderState) String() string {
}
type stateTracker struct {
folder string
folderID string
mut sync.Mutex
current folderState
@@ -61,7 +61,7 @@ func (s *stateTracker) setState(newState folderState) {
*/
eventData := map[string]interface{}{
"folder": s.folder,
"folder": s.folderID,
"to": newState.String(),
"from": s.current.String(),
}
@@ -92,7 +92,7 @@ func (s *stateTracker) setError(err error) {
s.mut.Lock()
if s.current != FolderError || s.err.Error() != err.Error() {
eventData := map[string]interface{}{
"folder": s.folder,
"folder": s.folderID,
"to": FolderError.String(),
"from": s.current.String(),
"error": err.Error(),
@@ -116,7 +116,7 @@ func (s *stateTracker) clearError() {
s.mut.Lock()
if s.current == FolderError {
eventData := map[string]interface{}{
"folder": s.folder,
"folder": s.folderID,
"to": FolderIdle.String(),
"from": s.current.String(),
}
+80 -68
View File
@@ -24,6 +24,7 @@ import (
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/ignore"
@@ -92,7 +93,7 @@ type Model struct {
folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef
fmut sync.RWMutex // protects the above
conn map[protocol.DeviceID]Connection
conn map[protocol.DeviceID]connections.Connection
helloMessages map[protocol.DeviceID]protocol.HelloMessage
deviceClusterConf map[protocol.DeviceID]protocol.ClusterConfigMessage
devicePaused map[protocol.DeviceID]bool
@@ -100,8 +101,11 @@ type Model struct {
pmut sync.RWMutex // protects the above
}
type folderFactory func(*Model, config.FolderConfiguration, versioner.Versioner) service
var (
symlinkWarning = stdsync.Once{}
symlinkWarning = stdsync.Once{}
folderFactories = make(map[config.FolderType]folderFactory, 0)
)
// NewModel creates and starts a new model. The model starts in read-only mode,
@@ -134,7 +138,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
folderRunners: make(map[string]service),
folderRunnerTokens: make(map[string][]suture.ServiceToken),
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
conn: make(map[protocol.DeviceID]Connection),
conn: make(map[protocol.DeviceID]connections.Connection),
helloMessages: make(map[protocol.DeviceID]protocol.HelloMessage),
deviceClusterConf: make(map[protocol.DeviceID]protocol.ClusterConfigMessage),
devicePaused: make(map[protocol.DeviceID]bool),
@@ -158,10 +162,8 @@ func (m *Model) StartDeadlockDetector(timeout time.Duration) {
deadlockDetect(m.pmut, timeout)
}
// StartFolderRW starts read/write processing on the current model. When in
// read/write mode the model will attempt to keep in sync with the cluster by
// pulling needed files from peer devices.
func (m *Model) StartFolderRW(folder string) {
// StartFolder constrcuts the folder service and starts it.
func (m *Model) StartFolder(folder string) {
m.fmut.Lock()
cfg, ok := m.folderCfgs[folder]
if !ok {
@@ -172,37 +174,43 @@ func (m *Model) StartFolderRW(folder string) {
if ok {
panic("cannot start already running folder " + folder)
}
p := newRWFolder(m, m.shortID, cfg)
m.folderRunners[folder] = p
folderFactory, ok := folderFactories[cfg.Type]
if !ok {
panic(fmt.Sprintf("unknown folder type 0x%x", cfg.Type))
}
var ver versioner.Versioner
if len(cfg.Versioning.Type) > 0 {
factory, ok := versioner.Factories[cfg.Versioning.Type]
versionerFactory, ok := versioner.Factories[cfg.Versioning.Type]
if !ok {
l.Fatalf("Requested versioning type %q that does not exist", cfg.Versioning.Type)
}
versioner := factory(folder, cfg.Path(), cfg.Versioning.Params)
if service, ok := versioner.(suture.Service); ok {
ver = versionerFactory(folder, cfg.Path(), cfg.Versioning.Params)
if service, ok := ver.(suture.Service); ok {
// The versioner implements the suture.Service interface, so
// expects to be run in the background in addition to being called
// when files are going to be archived.
token := m.Add(service)
m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
}
p.versioner = versioner
}
p := folderFactory(m, cfg, ver)
m.folderRunners[folder] = p
m.warnAboutOverwritingProtectedFiles(folder)
token := m.Add(p)
m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
m.fmut.Unlock()
l.Infoln("Ready to synchronize", folder, "(read-write)")
l.Infoln("Ready to synchronize", folder, fmt.Sprintf("(%s)", cfg.Type))
}
func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
if m.folderCfgs[folder].ReadOnly {
if m.folderCfgs[folder].Type == config.FolderTypeReadOnly {
return
}
@@ -229,30 +237,6 @@ func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
}
}
// StartFolderRO starts read only processing on the current model. When in
// read only mode the model will announce files to the cluster but not pull in
// any external changes.
func (m *Model) StartFolderRO(folder string) {
m.fmut.Lock()
cfg, ok := m.folderCfgs[folder]
if !ok {
panic("cannot start nonexistent folder " + folder)
}
_, ok = m.folderRunners[folder]
if ok {
panic("cannot start already running folder " + folder)
}
s := newROFolder(m, folder, time.Duration(cfg.RescanIntervalS)*time.Second)
m.folderRunners[folder] = s
token := m.Add(s)
m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
m.fmut.Unlock()
l.Infoln("Ready to synchronize", folder, "(read only; no external updates accepted)")
}
func (m *Model) RemoveFolder(folder string) {
m.fmut.Lock()
m.pmut.Lock()
@@ -294,7 +278,7 @@ type ConnectionInfo struct {
Paused bool
Address string
ClientVersion string
Type ConnectionType
Type string
}
func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
@@ -306,7 +290,7 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
"paused": info.Paused,
"address": info.Address,
"clientVersion": info.ClientVersion,
"type": info.Type.String(),
"type": info.Type,
})
}
@@ -325,7 +309,7 @@ func (m *Model) ConnectionStats() map[string]interface{} {
versionString = hello.ClientName + " " + hello.ClientVersion
}
ci := ConnectionInfo{
ClientVersion: versionString,
ClientVersion: strings.TrimSpace(versionString),
Paused: m.devicePaused[device],
}
if conn, ok := m.conn[device]; ok {
@@ -543,6 +527,7 @@ func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.F
cfg := m.folderCfgs[folder]
files, ok := m.folderFiles[folder]
runner := m.folderRunners[folder]
ignores := m.folderIgnores[folder]
m.fmut.RUnlock()
if runner != nil {
@@ -555,7 +540,11 @@ func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.F
l.Fatalf("Index for nonexistant folder %q", folder)
}
fs = filterIndex(folder, fs, cfg.IgnoreDelete)
m.pmut.RLock()
m.deviceDownloads[deviceID].Update(folder, makeForgetUpdate(fs))
m.pmut.RUnlock()
fs = filterIndex(folder, fs, cfg.IgnoreDelete, ignores)
files.Replace(deviceID, fs)
events.Default.Log(events.RemoteIndexUpdated, map[string]interface{}{
@@ -585,13 +574,18 @@ func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []prot
files := m.folderFiles[folder]
cfg := m.folderCfgs[folder]
runner, ok := m.folderRunners[folder]
ignores := m.folderIgnores[folder]
m.fmut.RUnlock()
if !ok {
l.Fatalf("IndexUpdate for nonexistant folder %q", folder)
}
fs = filterIndex(folder, fs, cfg.IgnoreDelete)
m.pmut.RLock()
m.deviceDownloads[deviceID].Update(folder, makeForgetUpdate(fs))
m.pmut.RUnlock()
fs = filterIndex(folder, fs, cfg.IgnoreDelete, ignores)
files.Update(deviceID, fs)
events.Default.Log(events.RemoteIndexUpdated, map[string]interface{}{
@@ -1011,7 +1005,7 @@ func (m *Model) GetHello(protocol.DeviceID) protocol.HelloMessage {
// AddConnection adds a new peer connection to the model. An initial index will
// be sent to the connected peer, thereafter index updates whenever the local
// folder changes.
func (m *Model) AddConnection(conn Connection, hello protocol.HelloMessage) {
func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloMessage) {
deviceID := conn.ID()
m.pmut.Lock()
@@ -1028,7 +1022,7 @@ func (m *Model) AddConnection(conn Connection, hello protocol.HelloMessage) {
"deviceName": hello.DeviceName,
"clientName": hello.ClientName,
"clientVersion": hello.ClientVersion,
"type": conn.Type.String(),
"type": conn.Type,
}
addr := conn.RemoteAddr()
@@ -1040,13 +1034,6 @@ func (m *Model) AddConnection(conn Connection, hello protocol.HelloMessage) {
l.Infof(`Device %s client is "%s %s" named "%s"`, deviceID, hello.ClientName, hello.ClientVersion, hello.DeviceName)
device, ok := m.cfg.Devices()[deviceID]
if ok && (device.Name == "" || m.cfg.Options().OverwriteNames) {
device.Name = hello.DeviceName
m.cfg.SetDevice(device)
m.cfg.Save()
}
conn.Start()
cm := m.generateClusterConfig(deviceID)
@@ -1060,6 +1047,13 @@ func (m *Model) AddConnection(conn Connection, hello protocol.HelloMessage) {
m.fmut.RUnlock()
m.pmut.Unlock()
device, ok := m.cfg.Devices()[deviceID]
if ok && (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) {
device.Name = hello.DeviceName
m.cfg.SetDevice(device)
m.cfg.Save()
}
m.deviceWasSeen(deviceID)
}
@@ -1083,7 +1077,7 @@ func (m *Model) DownloadProgress(device protocol.DeviceID, folder string, update
cfg, ok := m.folderCfgs[folder]
m.fmut.RUnlock()
if !ok || cfg.ReadOnly || cfg.DisableTempIndexes {
if !ok || cfg.Type == config.FolderTypeReadOnly || cfg.DisableTempIndexes {
return
}
@@ -1358,7 +1352,7 @@ func (m *Model) ScanFolderSubs(folder string, subs []string) error {
return runner.Scan(subs)
}
func (m *Model) internalScanFolderSubs(folder string, subs []string) error {
func (m *Model) internalScanFolderSubdirs(folder string, subs []string) error {
for i, sub := range subs {
sub = osutil.NativeFilename(sub)
if p := filepath.Clean(filepath.Join(folder, sub)); !strings.HasPrefix(p, folder) {
@@ -1407,7 +1401,9 @@ func (m *Model) internalScanFolderSubs(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,
@@ -1423,11 +1419,8 @@ func (m *Model) internalScanFolderSubs(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
@@ -1596,7 +1589,7 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
Label: folderCfg.Label,
}
var flags uint32
if folderCfg.ReadOnly {
if folderCfg.Type == config.FolderTypeReadOnly {
flags |= protocol.FlagFolderReadOnly
}
if folderCfg.IgnorePerms {
@@ -1864,7 +1857,7 @@ func (m *Model) CheckFolderHealth(id string) error {
case !folder.HasMarker():
err = errors.New("folder marker missing")
case !folder.ReadOnly:
case folder.Type != config.FolderTypeReadOnly:
// Check for free space, if it isn't a master folder. We aren't
// going to change the contents of master folders, so we don't
// care about the amount of free space there.
@@ -1941,11 +1934,7 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
// A folder was added.
l.Debugln(m, "adding folder", folderID)
m.AddFolder(cfg)
if cfg.ReadOnly {
m.StartFolderRO(folderID)
} else {
m.StartFolderRW(folderID)
}
m.StartFolder(folderID)
// Drop connections to all devices that can now share the new
// folder.
@@ -2028,6 +2017,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.
@@ -2072,7 +2063,7 @@ func mapDeviceCfgs(devices []config.DeviceConfiguration) map[protocol.DeviceID]s
return m
}
func filterIndex(folder string, fs []protocol.FileInfo, dropDeletes bool) []protocol.FileInfo {
func filterIndex(folder string, fs []protocol.FileInfo, dropDeletes bool, ignores *ignore.Matcher) []protocol.FileInfo {
for i := 0; i < len(fs); {
if fs[i].Flags&^protocol.FlagsAll != 0 {
l.Debugln("dropping update for file with unknown bits set", fs[i])
@@ -2086,6 +2077,10 @@ func filterIndex(folder string, fs []protocol.FileInfo, dropDeletes bool) []prot
l.Debugln("dropping update for unsupported symlink", fs[i])
fs[i] = fs[len(fs)-1]
fs = fs[:len(fs)-1]
} else if ignores != nil && ignores.Match(fs[i].Name).IsIgnored() {
l.Debugln("dropping update for ignored item", fs[i])
fs[i] = fs[len(fs)-1]
fs = fs[:len(fs)-1]
} else {
i++
}
@@ -2210,3 +2205,20 @@ next:
return cleaned
}
// makeForgetUpdate takes an index update and constructs a download progress update
// causing to forget any progress for files which we've just been sent.
func makeForgetUpdate(files []protocol.FileInfo) []protocol.FileDownloadProgressUpdate {
updates := make([]protocol.FileDownloadProgressUpdate, 0, len(files))
for _, file := range files {
if file.IsSymlink() || file.IsDirectory() || file.IsDeleted() {
continue
}
updates = append(updates, protocol.FileDownloadProgressUpdate{
Name: file.Name,
Version: file.Version,
UpdateType: protocol.UpdateTypeForget,
})
}
return updates
}
+131 -17
View File
@@ -8,6 +8,7 @@ package model
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
@@ -22,6 +23,7 @@ import (
"github.com/d4l3k/messagediff"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -85,7 +87,7 @@ func TestRequest(t *testing.T) {
// device1 shares default, but device2 doesn't
m.AddFolder(defaultFolderConfig)
m.StartFolderRO("default")
m.StartFolder("default")
m.ServeBackground()
m.ScanFolder("default")
@@ -159,7 +161,7 @@ func benchmarkIndex(b *testing.B, nfiles int) {
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolderRO("default")
m.StartFolder("default")
m.ServeBackground()
files := genFiles(nfiles)
@@ -188,7 +190,7 @@ func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolderRO("default")
m.StartFolder("default")
m.ServeBackground()
files := genFiles(nfiles)
@@ -292,10 +294,13 @@ func BenchmarkRequest(b *testing.B) {
id: device1,
requestData: []byte("some data to return"),
}
m.AddConnection(Connection{
&net.TCPConn{},
fc,
ConnectionTypeDirectAccept,
m.AddConnection(connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(&fakeConn{}, nil),
Type: "foo",
Priority: 10,
},
Connection: fc,
}, protocol.HelloMessage{})
m.Index(device1, "default", files, 0, nil)
@@ -333,13 +338,16 @@ func TestDeviceRename(t *testing.T) {
t.Errorf("Device already has a name")
}
conn := Connection{
&net.TCPConn{},
&FakeConnection{
conn := connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(&fakeConn{}, nil),
Type: "foo",
Priority: 10,
},
Connection: &FakeConnection{
id: device1,
requestData: []byte("some data to return"),
},
ConnectionTypeDirectAccept,
}
m.AddConnection(conn, hello)
@@ -378,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"
@@ -492,7 +500,7 @@ func TestIgnores(t *testing.T) {
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolderRO("default")
m.StartFolder("default")
m.ServeBackground()
expected := []string{
@@ -609,6 +617,7 @@ func TestROScanRecovery(t *testing.T) {
fcfg := config.FolderConfiguration{
ID: "default",
RawPath: "testdata/rotestfolder",
Type: config.FolderTypeReadOnly,
RescanIntervalS: 1,
}
cfg := config.Wrap("/tmp/test", config.Configuration{
@@ -624,7 +633,7 @@ func TestROScanRecovery(t *testing.T) {
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
m.AddFolder(fcfg)
m.StartFolderRO("default")
m.StartFolder("default")
m.ServeBackground()
waitFor := func(status string) error {
@@ -693,6 +702,7 @@ func TestRWScanRecovery(t *testing.T) {
fcfg := config.FolderConfiguration{
ID: "default",
RawPath: "testdata/rwtestfolder",
Type: config.FolderTypeReadWrite,
RescanIntervalS: 1,
}
cfg := config.Wrap("/tmp/test", config.Configuration{
@@ -708,7 +718,7 @@ func TestRWScanRecovery(t *testing.T) {
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
m.AddFolder(fcfg)
m.StartFolderRW("default")
m.StartFolder("default")
m.ServeBackground()
waitFor := func(status string) error {
@@ -1219,7 +1229,7 @@ func TestIgnoreDelete(t *testing.T) {
m.AddFolder(cfg)
m.ServeBackground()
m.StartFolderRW("default")
m.StartFolder("default")
m.ScanFolder("default")
// Get a currently existing file
@@ -1298,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{},
@@ -1349,3 +1359,107 @@ 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 {
return "network"
}
func (fakeAddr) String() string {
return "address"
}
type fakeConn struct{}
func (fakeConn) Close() error {
return nil
}
func (fakeConn) LocalAddr() net.Addr {
return &fakeAddr{}
}
func (fakeConn) RemoteAddr() net.Addr {
return &fakeAddr{}
}
func (fakeConn) Read([]byte) (int, error) {
return 0, nil
}
func (fakeConn) Write([]byte) (int, error) {
return 0, nil
}
func (fakeConn) SetDeadline(time.Time) error {
return nil
}
func (fakeConn) SetReadDeadline(time.Time) error {
return nil
}
func (fakeConn) SetWriteDeadline(time.Time) error {
return nil
}
-1
View File
@@ -21,7 +21,6 @@ type ProgressEmitter struct {
registry map[string]*sharedPullerState
interval time.Duration
minBlocks int
lastUpdate time.Time
sentDownloadStates map[protocol.DeviceID]*sentDownloadState // States representing what we've sent to the other peer via DownloadProgress messages.
connections map[string][]protocol.Connection
mut sync.Mutex
+43 -101
View File
@@ -8,151 +8,93 @@ package model
import (
"fmt"
"math/rand"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/versioner"
)
func init() {
folderFactories[config.FolderTypeReadOnly] = newROFolder
}
type roFolder struct {
stateTracker
folder string
intv time.Duration
timer *time.Timer
model *Model
stop chan struct{}
scanNow chan rescanRequest
delayScan chan time.Duration
folder
}
type rescanRequest struct {
subs []string
err chan error
}
func newROFolder(model *Model, folder string, interval time.Duration) *roFolder {
func newROFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner) service {
return &roFolder{
stateTracker: stateTracker{
folder: folder,
mut: sync.NewMutex(),
folder: folder{
stateTracker: stateTracker{
folderID: cfg.ID,
mut: sync.NewMutex(),
},
scan: folderscan{
interval: time.Duration(cfg.RescanIntervalS) * time.Second,
timer: time.NewTimer(time.Millisecond),
now: make(chan rescanRequest),
delay: make(chan time.Duration),
},
stop: make(chan struct{}),
model: model,
},
folder: folder,
intv: interval,
timer: time.NewTimer(time.Millisecond),
model: model,
stop: make(chan struct{}),
scanNow: make(chan rescanRequest),
delayScan: make(chan time.Duration),
}
}
func (s *roFolder) Serve() {
l.Debugln(s, "starting")
defer l.Debugln(s, "exiting")
func (f *roFolder) Serve() {
l.Debugln(f, "starting")
defer l.Debugln(f, "exiting")
defer func() {
s.timer.Stop()
f.scan.timer.Stop()
}()
reschedule := func() {
if s.intv == 0 {
return
}
// Sleep a random time between 3/4 and 5/4 of the configured interval.
sleepNanos := (s.intv.Nanoseconds()*3 + rand.Int63n(2*s.intv.Nanoseconds())) / 4
s.timer.Reset(time.Duration(sleepNanos) * time.Nanosecond)
}
initialScanCompleted := false
for {
select {
case <-s.stop:
case <-f.stop:
return
case <-s.timer.C:
if err := s.model.CheckFolderHealth(s.folder); err != nil {
l.Infoln("Skipping folder", s.folder, "scan due to folder error:", err)
reschedule()
case <-f.scan.timer.C:
if err := f.model.CheckFolderHealth(f.folderID); err != nil {
l.Infoln("Skipping folder", f.folderID, "scan due to folder error:", err)
f.scan.reschedule()
continue
}
l.Debugln(s, "rescan")
l.Debugln(f, "rescan")
if err := s.model.internalScanFolderSubs(s.folder, nil); err != nil {
if err := f.model.internalScanFolderSubdirs(f.folderID, nil); err != nil {
// Potentially sets the error twice, once in the scanner just
// by doing a check, and once here, if the error returned is
// the same one as returned by CheckFolderHealth, though
// duplicate set is handled by setError.
s.setError(err)
reschedule()
f.setError(err)
f.scan.reschedule()
continue
}
if !initialScanCompleted {
l.Infoln("Completed initial scan (ro) of folder", s.folder)
l.Infoln("Completed initial scan (ro) of folder", f.folderID)
initialScanCompleted = true
}
if s.intv == 0 {
if f.scan.interval == 0 {
continue
}
reschedule()
f.scan.reschedule()
case req := <-s.scanNow:
if err := s.model.CheckFolderHealth(s.folder); err != nil {
l.Infoln("Skipping folder", s.folder, "scan due to folder error:", err)
req.err <- err
continue
}
case req := <-f.scan.now:
req.err <- f.scanSubdirsIfHealthy(req.subdirs)
l.Debugln(s, "forced rescan")
if err := s.model.internalScanFolderSubs(s.folder, req.subs); err != nil {
// Potentially sets the error twice, once in the scanner just
// by doing a check, and once here, if the error returned is
// the same one as returned by CheckFolderHealth, though
// duplicate set is handled by setError.
s.setError(err)
req.err <- err
continue
}
req.err <- nil
case next := <-s.delayScan:
s.timer.Reset(next)
case next := <-f.scan.delay:
f.scan.timer.Reset(next)
}
}
}
func (s *roFolder) Stop() {
close(s.stop)
}
func (s *roFolder) IndexUpdated() {
}
func (s *roFolder) Scan(subs []string) error {
req := rescanRequest{
subs: subs,
err: make(chan error),
}
s.scanNow <- req
return <-req.err
}
func (s *roFolder) String() string {
return fmt.Sprintf("roFolder/%s@%p", s.folder, s)
}
func (s *roFolder) BringToFront(string) {}
func (s *roFolder) Jobs() ([]string, []string) {
return nil, nil
}
func (s *roFolder) DelayScan(next time.Duration) {
s.delayScan <- next
func (f *roFolder) String() string {
return fmt.Sprintf("roFolder/%s@%p", f.folderID, f)
}
+323 -374
View File
File diff suppressed because it is too large Load Diff
+42 -59
View File
@@ -68,9 +68,14 @@ func setUpModel(file protocol.FileInfo) *Model {
func setUpRwFolder(model *Model) rwFolder {
return rwFolder{
folder: "default",
folder: folder{
stateTracker: stateTracker{
folderID: "default",
},
model: model,
},
dir: "testdata",
model: model,
queue: newJobQueue(),
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
@@ -92,10 +97,10 @@ func TestHandleFile(t *testing.T) {
requiredFile.Blocks = blocks[1:]
m := setUpModel(existingFile)
p := setUpRwFolder(m)
f := setUpRwFolder(m)
copyChan := make(chan copyBlocksState, 1)
p.handleFile(requiredFile, copyChan, nil)
f.handleFile(requiredFile, copyChan, nil)
// Receive the results
toCopy := <-copyChan
@@ -133,10 +138,10 @@ func TestHandleFileWithTemp(t *testing.T) {
requiredFile.Blocks = blocks[1:]
m := setUpModel(existingFile)
p := setUpRwFolder(m)
f := setUpRwFolder(m)
copyChan := make(chan copyBlocksState, 1)
p.handleFile(requiredFile, copyChan, nil)
f.handleFile(requiredFile, copyChan, nil)
// Receive the results
toCopy := <-copyChan
@@ -181,15 +186,15 @@ func TestCopierFinder(t *testing.T) {
requiredFile.Name = "file2"
m := setUpModel(existingFile)
p := setUpRwFolder(m)
f := setUpRwFolder(m)
copyChan := make(chan copyBlocksState)
pullChan := make(chan pullBlockState, 4)
finisherChan := make(chan *sharedPullerState, 1)
// Run a single fetcher routine
go p.copierRoutine(copyChan, pullChan, finisherChan)
go f.copierRoutine(copyChan, pullChan, finisherChan)
p.handleFile(requiredFile, copyChan, finisherChan)
f.handleFile(requiredFile, copyChan, finisherChan)
pulls := []pullBlockState{<-pullChan, <-pullChan, <-pullChan, <-pullChan}
finish := <-finisherChan
@@ -290,16 +295,16 @@ func TestLastResortPulling(t *testing.T) {
return true
}
p := setUpRwFolder(m)
f := setUpRwFolder(m)
copyChan := make(chan copyBlocksState)
pullChan := make(chan pullBlockState, 1)
finisherChan := make(chan *sharedPullerState, 1)
// Run a single copier routine
go p.copierRoutine(copyChan, pullChan, finisherChan)
go f.copierRoutine(copyChan, pullChan, finisherChan)
p.handleFile(file, copyChan, finisherChan)
f.handleFile(file, copyChan, finisherChan)
// Copier should hash empty file, realise that the region it has read
// doesn't match the hash which was advertised by the block map, fix it
@@ -328,24 +333,13 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
emitter := NewProgressEmitter(defaultConfig)
go emitter.Serve()
p := rwFolder{
folder: "default",
dir: "testdata",
model: m,
queue: newJobQueue(),
progressEmitter: emitter,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
f := setUpRwFolder(m)
// queue.Done should be called by the finisher routine
p.queue.Push("filex", 0, 0)
p.queue.Pop()
f.queue.Push("filex", 0, 0)
f.queue.Pop()
if p.queue.lenProgress() != 1 {
if f.queue.lenProgress() != 1 {
t.Fatal("Expected file in progress")
}
@@ -354,10 +348,10 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
finisherBufferChan := make(chan *sharedPullerState)
finisherChan := make(chan *sharedPullerState)
go p.copierRoutine(copyChan, pullChan, finisherBufferChan)
go p.finisherRoutine(finisherChan)
go f.copierRoutine(copyChan, pullChan, finisherBufferChan)
go f.finisherRoutine(finisherChan)
p.handleFile(file, copyChan, finisherChan)
f.handleFile(file, copyChan, finisherChan)
// Receive a block at puller, to indicate that at least a single copier
// loop has been performed.
@@ -373,7 +367,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
case state := <-finisherBufferChan:
// At this point the file should still be registered with both the job
// queue, and the progress emitter. Verify this.
if p.progressEmitter.lenRegistry() != 1 || p.queue.lenProgress() != 1 || p.queue.lenQueued() != 0 {
if f.model.progressEmitter.lenRegistry() != 1 || f.queue.lenProgress() != 1 || f.queue.lenQueued() != 0 {
t.Fatal("Could not find file")
}
@@ -388,16 +382,16 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
t.Fatal("File not closed?")
}
if p.progressEmitter.lenRegistry() != 0 || p.queue.lenProgress() != 0 || p.queue.lenQueued() != 0 {
t.Fatal("Still registered", p.progressEmitter.lenRegistry(), p.queue.lenProgress(), p.queue.lenQueued())
if f.model.progressEmitter.lenRegistry() != 0 || f.queue.lenProgress() != 0 || f.queue.lenQueued() != 0 {
t.Fatal("Still registered", f.model.progressEmitter.lenRegistry(), f.queue.lenProgress(), f.queue.lenQueued())
}
// Doing it again should have no effect
finisherChan <- state
time.Sleep(100 * time.Millisecond)
if p.progressEmitter.lenRegistry() != 0 || p.queue.lenProgress() != 0 || p.queue.lenQueued() != 0 {
t.Fatal("Still registered", p.progressEmitter.lenRegistry(), p.queue.lenProgress(), p.queue.lenQueued())
if f.model.progressEmitter.lenRegistry() != 0 || f.queue.lenProgress() != 0 || f.queue.lenQueued() != 0 {
t.Fatal("Still registered", f.model.progressEmitter.lenRegistry(), f.queue.lenProgress(), f.queue.lenQueued())
}
case <-time.After(time.Second):
t.Fatal("Didn't get anything to the finisher")
@@ -412,24 +406,13 @@ func TestDeregisterOnFailInPull(t *testing.T) {
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
emitter := NewProgressEmitter(defaultConfig)
go emitter.Serve()
p := rwFolder{
folder: "default",
dir: "testdata",
model: m,
queue: newJobQueue(),
progressEmitter: emitter,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
f := setUpRwFolder(m)
// queue.Done should be called by the finisher routine
p.queue.Push("filex", 0, 0)
p.queue.Pop()
f.queue.Push("filex", 0, 0)
f.queue.Pop()
if p.queue.lenProgress() != 1 {
if f.queue.lenProgress() != 1 {
t.Fatal("Expected file in progress")
}
@@ -438,11 +421,11 @@ func TestDeregisterOnFailInPull(t *testing.T) {
finisherBufferChan := make(chan *sharedPullerState)
finisherChan := make(chan *sharedPullerState)
go p.copierRoutine(copyChan, pullChan, finisherBufferChan)
go p.pullerRoutine(pullChan, finisherBufferChan)
go p.finisherRoutine(finisherChan)
go f.copierRoutine(copyChan, pullChan, finisherBufferChan)
go f.pullerRoutine(pullChan, finisherBufferChan)
go f.finisherRoutine(finisherChan)
p.handleFile(file, copyChan, finisherChan)
f.handleFile(file, copyChan, finisherChan)
// Receive at finisher, we should error out as puller has nowhere to pull
// from.
@@ -450,7 +433,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
case state := <-finisherBufferChan:
// At this point the file should still be registered with both the job
// queue, and the progress emitter. Verify this.
if p.progressEmitter.lenRegistry() != 1 || p.queue.lenProgress() != 1 || p.queue.lenQueued() != 0 {
if f.model.progressEmitter.lenRegistry() != 1 || f.queue.lenProgress() != 1 || f.queue.lenQueued() != 0 {
t.Fatal("Could not find file")
}
@@ -465,16 +448,16 @@ func TestDeregisterOnFailInPull(t *testing.T) {
t.Fatal("File not closed?")
}
if p.progressEmitter.lenRegistry() != 0 || p.queue.lenProgress() != 0 || p.queue.lenQueued() != 0 {
t.Fatal("Still registered", p.progressEmitter.lenRegistry(), p.queue.lenProgress(), p.queue.lenQueued())
if f.model.progressEmitter.lenRegistry() != 0 || f.queue.lenProgress() != 0 || f.queue.lenQueued() != 0 {
t.Fatal("Still registered", f.model.progressEmitter.lenRegistry(), f.queue.lenProgress(), f.queue.lenQueued())
}
// Doing it again should have no effect
finisherChan <- state
time.Sleep(100 * time.Millisecond)
if p.progressEmitter.lenRegistry() != 0 || p.queue.lenProgress() != 0 || p.queue.lenQueued() != 0 {
t.Fatal("Still registered", p.progressEmitter.lenRegistry(), p.queue.lenProgress(), p.queue.lenQueued())
if f.model.progressEmitter.lenRegistry() != 0 || f.queue.lenProgress() != 0 || f.queue.lenQueued() != 0 {
t.Fatal("Still registered", f.model.progressEmitter.lenRegistry(), f.queue.lenProgress(), f.queue.lenQueued())
}
case <-time.After(time.Second):
t.Fatal("Didn't get anything to the finisher")
+2 -1
View File
@@ -122,7 +122,8 @@ func (s *sharedPullerState) tempFile() (io.WriterAt, error) {
}
// Attempt to create the temp file
flags := os.O_WRONLY
// RDWR because of issue #2994.
flags := os.O_RDWR
if s.reused == 0 {
flags |= os.O_CREATE | os.O_EXCL
} else {

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