Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ea22b1850 | |||
| 2c1323ece6 | |||
| adb7fb43cb | |||
| d59fd9c22d | |||
| 6f743f3138 | |||
| 5a7fad0bcd | |||
| 5d2414dfa9 | |||
| bef2425025 | |||
| e8b4286c93 | |||
| 2e9bf0b67c | |||
| 935c273c8f | |||
| b993b41847 | |||
| 1be40cc4fa | |||
| d628b731d1 | |||
| 21e116aa45 | |||
| d77d8ff803 | |||
| 31f64186ae | |||
| 1a703efa78 | |||
| 8b7b0a03eb | |||
| 0761d804a4 | |||
| 3ad42d9279 | |||
| bd41e21c26 | |||
| 10fe23b8f2 | |||
| 39899e40bf | |||
| 5d337bb24f | |||
| dd5909568f | |||
| 38166e976f | |||
| d6a7ffe0d4 | |||
| 2ebc6996a2 | |||
| 2e840134d2 | |||
| 66e1be33cf | |||
| 591959261c | |||
| 459930df09 | |||
| 674fc566bb | |||
| 09832abe50 | |||
| eabd2fc936 | |||
| 6720906ee5 | |||
| abb96802cb | |||
| 29fa05ae05 | |||
| 49387f9494 | |||
| 953482de53 | |||
| 8cf3a7aeda | |||
| b8c5cf1142 | |||
| 236f121c4e | |||
| 2467678bd4 | |||
| e87c1abd4e | |||
| dffc34559b | |||
| 80f2a9a6bf | |||
| 4aa6ecb122 | |||
| ccfcdf7f48 | |||
| 4eb23a38b1 | |||
| cb38213444 | |||
| 842b6111db | |||
| ea54525a33 | |||
| 893cc025f9 | |||
| b81c8d2e1b | |||
| 4b07535e86 | |||
| 0d2fe320a7 | |||
| f294113d01 | |||
| 1c7af1a72e | |||
| e61f424ade | |||
| fa1cfd94d0 |
@@ -1,7 +1,6 @@
|
||||
# Syncthing
|
||||
|
||||
[](http://build.syncthing.net/job/syncthing/lastBuild/)
|
||||
[](https://ci.appveyor.com/project/calmh/syncthing)
|
||||
[](http://godoc.org/github.com/syncthing/syncthing)
|
||||
[](https://www.mozilla.org/MPL/2.0/)
|
||||
|
||||
|
||||
@@ -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'
|
||||
@@ -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()
|
||||
|
||||
@@ -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[:]
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -195,7 +195,7 @@ code.ng-binding{
|
||||
}
|
||||
|
||||
|
||||
/* progess bars */
|
||||
/* progress bars */
|
||||
.progress-bar {
|
||||
background-color: #217dbb !important;
|
||||
}
|
||||
|
||||
@@ -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}})."
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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}})."
|
||||
}
|
||||
@@ -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}})."
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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}})."
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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ä.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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. ",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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」と入力してください。",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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ą.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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}})."
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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}})."
|
||||
}
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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\" для автоматичного визначення адреси.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "天",
|
||||
|
||||
@@ -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
@@ -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> <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> <span translate>Folder Master</span></th>
|
||||
<tr ng-if="folder.type != 'readwrite'">
|
||||
<th><span class="fa fa-fw fa-lock"></span> <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> <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> <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> <span translate>Override Changes</span>
|
||||
</button>
|
||||
<span class="pull-right">
|
||||
@@ -429,6 +430,19 @@
|
||||
<th><span class="fa fa-fw fa-tachometer"></span> <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> <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> <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> <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> <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> <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> <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> <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> <span translate>Compression</span></th>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<p translate>Copyright © 2014-2016 the following Contributors:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, 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>
|
||||
<a href="http://docs.syncthing.net/users/foldermaster.html" target="_blank"><span class="fa fa-book"></span> <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>
|
||||
<a href="http://docs.syncthing.net/users/foldermaster.html" target="_blank"><span class="fa fa-book"></span> <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>
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -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
File diff suppressed because it is too large
Load Diff
+110
-15
@@ -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
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Vendored
+14
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 ©URI
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+42
-59
@@ -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")
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user