Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2941a813c2 | ||
|
|
0a022d38fa | ||
|
|
7ed3b3dd3a | ||
|
|
ce52963d2b | ||
|
|
9a1922fdc6 | ||
|
|
967424a538 | ||
|
|
83131103cf | ||
|
|
9f4a0d3216 | ||
|
|
c1591a5efd | ||
|
|
918ef4dff8 | ||
|
|
0d9a04c713 | ||
|
|
0c0c69f0cf | ||
|
|
943e80e26c | ||
|
|
058a327584 | ||
|
|
1eca4170f7 | ||
|
|
c268e4ad1b | ||
|
|
d4f81e8791 | ||
|
|
4f0680c3c8 | ||
|
|
8c26fe44c3 | ||
|
|
8c7d9f3dd2 | ||
|
|
f241b7e79a | ||
|
|
dc1f3503be | ||
|
|
c2a5e180b8 | ||
|
|
7351217489 | ||
|
|
aa42aafe33 | ||
|
|
b2da0120d6 | ||
|
|
9e84e09c26 | ||
|
|
32c1a9bc45 | ||
|
|
a7e95922c1 | ||
|
|
1392d0bc14 | ||
|
|
0f9fa9507e | ||
|
|
1087535d8f | ||
|
|
0e51f51979 | ||
|
|
90e0141ac5 | ||
|
|
8435a8678e | ||
|
|
bd2888fc3b | ||
|
|
6578ffe2c9 | ||
|
|
175340522f | ||
|
|
afc917b582 | ||
|
|
9f4cd7716e | ||
|
|
29b0017445 | ||
|
|
910a7c619a | ||
|
|
273fac2028 | ||
|
|
a323d85d32 | ||
|
|
491a33de0b | ||
|
|
d6a0a44432 | ||
|
|
752533489a | ||
|
|
e4e3c19e96 | ||
|
|
4ddb066728 | ||
|
|
958bbbc8cb | ||
|
|
abbcd1f436 | ||
|
|
db494f2afc | ||
|
|
985ea29940 |
@@ -48,8 +48,8 @@ Lord Landon Agahnim <lordlandon@gmail.com>
|
||||
Marc Laporte <marc@marclaporte.com> <marc@laporte.name>
|
||||
Marc Pujol <kilburn@la3.org>
|
||||
Marcin Dziadus <dziadus.marcin@gmail.com>
|
||||
Mateon1 <matin1111@wp.pl>
|
||||
Matt Burke <mburke@amplify.com>
|
||||
Mateusz Naściszewski <matin1111@wp.pl>
|
||||
Matt Burke <mburke@amplify.com> <burkemw3@gmail.com>
|
||||
Michael Jephcote <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
||||
Michael Tilli <pyfisch@gmail.com>
|
||||
Pascal Jungblut <github@pascalj.com> <mail@pascal-jungblut.com>
|
||||
|
||||
Generated
+7
-3
@@ -19,7 +19,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/xdr",
|
||||
"Rev": "5f7208e86762911861c94f1849eddbfc0a60cbf0"
|
||||
"Rev": "47c0042d09a827b81ee62497f99e5e0c7f0bd31c"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/golang/snappy",
|
||||
@@ -64,11 +64,15 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/internal/iana",
|
||||
"Rev": "db8e4de5b2d6653f66aea53094624468caad15d2"
|
||||
"Rev": "4b709d93778b93d2f34943e3142c71578d83ad31"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/ipv6",
|
||||
"Rev": "db8e4de5b2d6653f66aea53094624468caad15d2"
|
||||
"Rev": "4b709d93778b93d2f34943e3142c71578d83ad31"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/proxy",
|
||||
"Rev": "4b709d93778b93d2f34943e3142c71578d83ad31"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/text/transform",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ go:
|
||||
|
||||
install:
|
||||
- export PATH=$PATH:$HOME/gopath/bin
|
||||
- go get code.google.com/p/go.tools/cmd/cover
|
||||
- go get golang.org/x/tools/cover
|
||||
- go get github.com/mattn/goveralls
|
||||
|
||||
script:
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
xdr
|
||||
===
|
||||
|
||||
[](https://travis-ci.org/calmh/xdr)
|
||||
[](https://circleci.com/gh/calmh/xdr)
|
||||
[](https://coveralls.io/r/calmh/xdr?branch=master)
|
||||
[](http://godoc.org/github.com/calmh/xdr)
|
||||
[](http://opensource.org/licenses/MIT)
|
||||
|
||||
+26
-11
@@ -28,6 +28,7 @@ type fieldInfo struct {
|
||||
Encoder string // the encoder name, i.e. "Uint64" for Read/WriteUint64
|
||||
Convert string // what to convert to when encoding, i.e. "uint64"
|
||||
Max int // max size for slices and strings
|
||||
Submax int // max size for strings inside slices
|
||||
}
|
||||
|
||||
type structInfo struct {
|
||||
@@ -156,7 +157,11 @@ func (o *{{.TypeName}}) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
{{if ne $fieldInfo.Convert ""}}
|
||||
o.{{$fieldInfo.Name}}[i] = {{$fieldInfo.FieldType}}(xr.Read{{$fieldInfo.Encoder}}())
|
||||
{{else if $fieldInfo.IsBasic}}
|
||||
o.{{$fieldInfo.Name}}[i] = xr.Read{{$fieldInfo.Encoder}}()
|
||||
{{if ge $fieldInfo.Submax 1}}
|
||||
o.{{$fieldInfo.Name}}[i] = xr.Read{{$fieldInfo.Encoder}}Max({{$fieldInfo.Submax}})
|
||||
{{else}}
|
||||
o.{{$fieldInfo.Name}}[i] = xr.Read{{$fieldInfo.Encoder}}()
|
||||
{{end}}
|
||||
{{else}}
|
||||
(&o.{{$fieldInfo.Name}}[i]).DecodeXDRFrom(xr)
|
||||
{{end}}
|
||||
@@ -166,7 +171,7 @@ func (o *{{.TypeName}}) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
return xr.Error()
|
||||
}`))
|
||||
|
||||
var maxRe = regexp.MustCompile(`\Wmax:(\d+)`)
|
||||
var maxRe = regexp.MustCompile(`(?:\Wmax:)(\d+)(?:\s*,\s*(\d+))?`)
|
||||
|
||||
type typeSet struct {
|
||||
Type string
|
||||
@@ -198,11 +203,15 @@ func handleStruct(t *ast.StructType) []fieldInfo {
|
||||
}
|
||||
|
||||
fn := sf.Names[0].Name
|
||||
var max = 0
|
||||
var max1, max2 int
|
||||
if sf.Comment != nil {
|
||||
c := sf.Comment.List[0].Text
|
||||
if m := maxRe.FindStringSubmatch(c); m != nil {
|
||||
max, _ = strconv.Atoi(m[1])
|
||||
m := maxRe.FindStringSubmatch(c)
|
||||
if len(m) >= 2 {
|
||||
max1, _ = strconv.Atoi(m[1])
|
||||
}
|
||||
if len(m) >= 3 {
|
||||
max2, _ = strconv.Atoi(m[2])
|
||||
}
|
||||
if strings.Contains(c, "noencode") {
|
||||
continue
|
||||
@@ -220,14 +229,16 @@ func handleStruct(t *ast.StructType) []fieldInfo {
|
||||
FieldType: tn,
|
||||
Encoder: enc.Encoder,
|
||||
Convert: enc.Type,
|
||||
Max: max,
|
||||
Max: max1,
|
||||
Submax: max2,
|
||||
}
|
||||
} else {
|
||||
f = fieldInfo{
|
||||
Name: fn,
|
||||
IsBasic: false,
|
||||
FieldType: tn,
|
||||
Max: max,
|
||||
Max: max1,
|
||||
Submax: max2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +256,8 @@ func handleStruct(t *ast.StructType) []fieldInfo {
|
||||
FieldType: tn,
|
||||
Encoder: enc.Encoder,
|
||||
Convert: enc.Type,
|
||||
Max: max,
|
||||
Max: max1,
|
||||
Submax: max2,
|
||||
}
|
||||
} else if enc, ok := xdrEncoders[tn]; ok {
|
||||
f = fieldInfo{
|
||||
@@ -255,14 +267,16 @@ func handleStruct(t *ast.StructType) []fieldInfo {
|
||||
FieldType: tn,
|
||||
Encoder: enc.Encoder,
|
||||
Convert: enc.Type,
|
||||
Max: max,
|
||||
Max: max1,
|
||||
Submax: max2,
|
||||
}
|
||||
} else {
|
||||
f = fieldInfo{
|
||||
Name: fn,
|
||||
IsSlice: true,
|
||||
FieldType: tn,
|
||||
Max: max,
|
||||
Max: max1,
|
||||
Submax: max2,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +284,8 @@ func handleStruct(t *ast.StructType) []fieldInfo {
|
||||
f = fieldInfo{
|
||||
Name: fn,
|
||||
FieldType: ft.Sel.Name,
|
||||
Max: max,
|
||||
Max: max1,
|
||||
Submax: max2,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||
|
||||
// Package iana provides protocol number resources managed by the Internet Assigned Numbers Authority (IANA).
|
||||
package iana
|
||||
package iana // import "golang.org/x/net/internal/iana"
|
||||
|
||||
// Differentiated Services Field Codepoints (DSCP), Updated: 2013-06-25
|
||||
const (
|
||||
|
||||
+3
-3
@@ -42,7 +42,7 @@
|
||||
// The outgoing packets will be labeled DiffServ assured forwarding
|
||||
// class 1 low drop precedence, known as AF11 packets.
|
||||
//
|
||||
// if err := ipv6.NewConn(c).SetTrafficClass(DiffServAF11); err != nil {
|
||||
// if err := ipv6.NewConn(c).SetTrafficClass(0x28); err != nil {
|
||||
// // error handling
|
||||
// }
|
||||
// if _, err := c.Write(data); err != nil {
|
||||
@@ -124,7 +124,7 @@
|
||||
//
|
||||
// The application can also send both unicast and multicast packets.
|
||||
//
|
||||
// p.SetTrafficClass(DiffServCS0)
|
||||
// p.SetTrafficClass(0x0)
|
||||
// p.SetHopLimit(16)
|
||||
// if _, err := p.WriteTo(data[:n], nil, src); err != nil {
|
||||
// // error handling
|
||||
@@ -237,4 +237,4 @@
|
||||
// MLDv1 and starts to listen to multicast traffic.
|
||||
// In the fallback case, ExcludeSourceSpecificGroup and
|
||||
// IncludeSourceSpecificGroup may return an error.
|
||||
package ipv6
|
||||
package ipv6 // import "golang.org/x/net/ipv6"
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type direct struct{}
|
||||
|
||||
// Direct is a direct proxy: one that makes network connections directly.
|
||||
var Direct = direct{}
|
||||
|
||||
func (direct) Dial(network, addr string) (net.Conn, error) {
|
||||
return net.Dial(network, addr)
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A PerHost directs connections to a default Dialer unless the hostname
|
||||
// requested matches one of a number of exceptions.
|
||||
type PerHost struct {
|
||||
def, bypass Dialer
|
||||
|
||||
bypassNetworks []*net.IPNet
|
||||
bypassIPs []net.IP
|
||||
bypassZones []string
|
||||
bypassHosts []string
|
||||
}
|
||||
|
||||
// NewPerHost returns a PerHost Dialer that directs connections to either
|
||||
// defaultDialer or bypass, depending on whether the connection matches one of
|
||||
// the configured rules.
|
||||
func NewPerHost(defaultDialer, bypass Dialer) *PerHost {
|
||||
return &PerHost{
|
||||
def: defaultDialer,
|
||||
bypass: bypass,
|
||||
}
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the given network through either
|
||||
// defaultDialer or bypass.
|
||||
func (p *PerHost) Dial(network, addr string) (c net.Conn, err error) {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return p.dialerForRequest(host).Dial(network, addr)
|
||||
}
|
||||
|
||||
func (p *PerHost) dialerForRequest(host string) Dialer {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
for _, net := range p.bypassNetworks {
|
||||
if net.Contains(ip) {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
for _, bypassIP := range p.bypassIPs {
|
||||
if bypassIP.Equal(ip) {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
return p.def
|
||||
}
|
||||
|
||||
for _, zone := range p.bypassZones {
|
||||
if strings.HasSuffix(host, zone) {
|
||||
return p.bypass
|
||||
}
|
||||
if host == zone[1:] {
|
||||
// For a zone "example.com", we match "example.com"
|
||||
// too.
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
for _, bypassHost := range p.bypassHosts {
|
||||
if bypassHost == host {
|
||||
return p.bypass
|
||||
}
|
||||
}
|
||||
return p.def
|
||||
}
|
||||
|
||||
// AddFromString parses a string that contains comma-separated values
|
||||
// specifying hosts that should use the bypass proxy. Each value is either an
|
||||
// IP address, a CIDR range, a zone (*.example.com) or a hostname
|
||||
// (localhost). A best effort is made to parse the string and errors are
|
||||
// ignored.
|
||||
func (p *PerHost) AddFromString(s string) {
|
||||
hosts := strings.Split(s, ",")
|
||||
for _, host := range hosts {
|
||||
host = strings.TrimSpace(host)
|
||||
if len(host) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(host, "/") {
|
||||
// We assume that it's a CIDR address like 127.0.0.0/8
|
||||
if _, net, err := net.ParseCIDR(host); err == nil {
|
||||
p.AddNetwork(net)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
p.AddIP(ip)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(host, "*.") {
|
||||
p.AddZone(host[1:])
|
||||
continue
|
||||
}
|
||||
p.AddHost(host)
|
||||
}
|
||||
}
|
||||
|
||||
// AddIP specifies an IP address that will use the bypass proxy. Note that
|
||||
// this will only take effect if a literal IP address is dialed. A connection
|
||||
// to a named host will never match an IP.
|
||||
func (p *PerHost) AddIP(ip net.IP) {
|
||||
p.bypassIPs = append(p.bypassIPs, ip)
|
||||
}
|
||||
|
||||
// AddNetwork specifies an IP range that will use the bypass proxy. Note that
|
||||
// this will only take effect if a literal IP address is dialed. A connection
|
||||
// to a named host will never match.
|
||||
func (p *PerHost) AddNetwork(net *net.IPNet) {
|
||||
p.bypassNetworks = append(p.bypassNetworks, net)
|
||||
}
|
||||
|
||||
// AddZone specifies a DNS suffix that will use the bypass proxy. A zone of
|
||||
// "example.com" matches "example.com" and all of its subdomains.
|
||||
func (p *PerHost) AddZone(zone string) {
|
||||
if strings.HasSuffix(zone, ".") {
|
||||
zone = zone[:len(zone)-1]
|
||||
}
|
||||
if !strings.HasPrefix(zone, ".") {
|
||||
zone = "." + zone
|
||||
}
|
||||
p.bypassZones = append(p.bypassZones, zone)
|
||||
}
|
||||
|
||||
// AddHost specifies a hostname that will use the bypass proxy.
|
||||
func (p *PerHost) AddHost(host string) {
|
||||
if strings.HasSuffix(host, ".") {
|
||||
host = host[:len(host)-1]
|
||||
}
|
||||
p.bypassHosts = append(p.bypassHosts, host)
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type recordingProxy struct {
|
||||
addrs []string
|
||||
}
|
||||
|
||||
func (r *recordingProxy) Dial(network, addr string) (net.Conn, error) {
|
||||
r.addrs = append(r.addrs, addr)
|
||||
return nil, errors.New("recordingProxy")
|
||||
}
|
||||
|
||||
func TestPerHost(t *testing.T) {
|
||||
var def, bypass recordingProxy
|
||||
perHost := NewPerHost(&def, &bypass)
|
||||
perHost.AddFromString("localhost,*.zone,127.0.0.1,10.0.0.1/8,1000::/16")
|
||||
|
||||
expectedDef := []string{
|
||||
"example.com:123",
|
||||
"1.2.3.4:123",
|
||||
"[1001::]:123",
|
||||
}
|
||||
expectedBypass := []string{
|
||||
"localhost:123",
|
||||
"zone:123",
|
||||
"foo.zone:123",
|
||||
"127.0.0.1:123",
|
||||
"10.1.2.3:123",
|
||||
"[1000::]:123",
|
||||
}
|
||||
|
||||
for _, addr := range expectedDef {
|
||||
perHost.Dial("tcp", addr)
|
||||
}
|
||||
for _, addr := range expectedBypass {
|
||||
perHost.Dial("tcp", addr)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expectedDef, def.addrs) {
|
||||
t.Errorf("Hosts which went to the default proxy didn't match. Got %v, want %v", def.addrs, expectedDef)
|
||||
}
|
||||
if !reflect.DeepEqual(expectedBypass, bypass.addrs) {
|
||||
t.Errorf("Hosts which went to the bypass proxy didn't match. Got %v, want %v", bypass.addrs, expectedBypass)
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package proxy provides support for a variety of protocols to proxy network
|
||||
// data.
|
||||
package proxy // import "golang.org/x/net/proxy"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// A Dialer is a means to establish a connection.
|
||||
type Dialer interface {
|
||||
// Dial connects to the given address via the proxy.
|
||||
Dial(network, addr string) (c net.Conn, err error)
|
||||
}
|
||||
|
||||
// Auth contains authentication parameters that specific Dialers may require.
|
||||
type Auth struct {
|
||||
User, Password string
|
||||
}
|
||||
|
||||
// FromEnvironment returns the dialer specified by the proxy related variables in
|
||||
// the environment.
|
||||
func FromEnvironment() Dialer {
|
||||
allProxy := os.Getenv("all_proxy")
|
||||
if len(allProxy) == 0 {
|
||||
return Direct
|
||||
}
|
||||
|
||||
proxyURL, err := url.Parse(allProxy)
|
||||
if err != nil {
|
||||
return Direct
|
||||
}
|
||||
proxy, err := FromURL(proxyURL, Direct)
|
||||
if err != nil {
|
||||
return Direct
|
||||
}
|
||||
|
||||
noProxy := os.Getenv("no_proxy")
|
||||
if len(noProxy) == 0 {
|
||||
return proxy
|
||||
}
|
||||
|
||||
perHost := NewPerHost(proxy, Direct)
|
||||
perHost.AddFromString(noProxy)
|
||||
return perHost
|
||||
}
|
||||
|
||||
// proxySchemes is a map from URL schemes to a function that creates a Dialer
|
||||
// from a URL with such a scheme.
|
||||
var proxySchemes map[string]func(*url.URL, Dialer) (Dialer, error)
|
||||
|
||||
// RegisterDialerType takes a URL scheme and a function to generate Dialers from
|
||||
// a URL with that scheme and a forwarding Dialer. Registered schemes are used
|
||||
// by FromURL.
|
||||
func RegisterDialerType(scheme string, f func(*url.URL, Dialer) (Dialer, error)) {
|
||||
if proxySchemes == nil {
|
||||
proxySchemes = make(map[string]func(*url.URL, Dialer) (Dialer, error))
|
||||
}
|
||||
proxySchemes[scheme] = f
|
||||
}
|
||||
|
||||
// FromURL returns a Dialer given a URL specification and an underlying
|
||||
// Dialer for it to make network requests.
|
||||
func FromURL(u *url.URL, forward Dialer) (Dialer, error) {
|
||||
var auth *Auth
|
||||
if u.User != nil {
|
||||
auth = new(Auth)
|
||||
auth.User = u.User.Username()
|
||||
if p, ok := u.User.Password(); ok {
|
||||
auth.Password = p
|
||||
}
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "socks5":
|
||||
return SOCKS5("tcp", u.Host, auth, forward)
|
||||
}
|
||||
|
||||
// If the scheme doesn't match any of the built-in schemes, see if it
|
||||
// was registered by another package.
|
||||
if proxySchemes != nil {
|
||||
if f, ok := proxySchemes[u.Scheme]; ok {
|
||||
return f(u, forward)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("proxy: unknown scheme: " + u.Scheme)
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFromURL(t *testing.T) {
|
||||
endSystem, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen failed: %v", err)
|
||||
}
|
||||
defer endSystem.Close()
|
||||
gateway, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen failed: %v", err)
|
||||
}
|
||||
defer gateway.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go socks5Gateway(t, gateway, endSystem, socks5Domain, &wg)
|
||||
|
||||
url, err := url.Parse("socks5://user:password@" + gateway.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("url.Parse failed: %v", err)
|
||||
}
|
||||
proxy, err := FromURL(url, Direct)
|
||||
if err != nil {
|
||||
t.Fatalf("FromURL failed: %v", err)
|
||||
}
|
||||
_, port, err := net.SplitHostPort(endSystem.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("net.SplitHostPort failed: %v", err)
|
||||
}
|
||||
if c, err := proxy.Dial("tcp", "localhost:"+port); err != nil {
|
||||
t.Fatalf("FromURL.Dial failed: %v", err)
|
||||
} else {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestSOCKS5(t *testing.T) {
|
||||
endSystem, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen failed: %v", err)
|
||||
}
|
||||
defer endSystem.Close()
|
||||
gateway, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen failed: %v", err)
|
||||
}
|
||||
defer gateway.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go socks5Gateway(t, gateway, endSystem, socks5IP4, &wg)
|
||||
|
||||
proxy, err := SOCKS5("tcp", gateway.Addr().String(), nil, Direct)
|
||||
if err != nil {
|
||||
t.Fatalf("SOCKS5 failed: %v", err)
|
||||
}
|
||||
if c, err := proxy.Dial("tcp", endSystem.Addr().String()); err != nil {
|
||||
t.Fatalf("SOCKS5.Dial failed: %v", err)
|
||||
} else {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func socks5Gateway(t *testing.T, gateway, endSystem net.Listener, typ byte, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
c, err := gateway.Accept()
|
||||
if err != nil {
|
||||
t.Errorf("net.Listener.Accept failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
b := make([]byte, 32)
|
||||
var n int
|
||||
if typ == socks5Domain {
|
||||
n = 4
|
||||
} else {
|
||||
n = 3
|
||||
}
|
||||
if _, err := io.ReadFull(c, b[:n]); err != nil {
|
||||
t.Errorf("io.ReadFull failed: %v", err)
|
||||
return
|
||||
}
|
||||
if _, err := c.Write([]byte{socks5Version, socks5AuthNone}); err != nil {
|
||||
t.Errorf("net.Conn.Write failed: %v", err)
|
||||
return
|
||||
}
|
||||
if typ == socks5Domain {
|
||||
n = 16
|
||||
} else {
|
||||
n = 10
|
||||
}
|
||||
if _, err := io.ReadFull(c, b[:n]); err != nil {
|
||||
t.Errorf("io.ReadFull failed: %v", err)
|
||||
return
|
||||
}
|
||||
if b[0] != socks5Version || b[1] != socks5Connect || b[2] != 0x00 || b[3] != typ {
|
||||
t.Errorf("got an unexpected packet: %#02x %#02x %#02x %#02x", b[0], b[1], b[2], b[3])
|
||||
return
|
||||
}
|
||||
if typ == socks5Domain {
|
||||
copy(b[:5], []byte{socks5Version, 0x00, 0x00, socks5Domain, 9})
|
||||
b = append(b, []byte("localhost")...)
|
||||
} else {
|
||||
copy(b[:4], []byte{socks5Version, 0x00, 0x00, socks5IP4})
|
||||
}
|
||||
host, port, err := net.SplitHostPort(endSystem.Addr().String())
|
||||
if err != nil {
|
||||
t.Errorf("net.SplitHostPort failed: %v", err)
|
||||
return
|
||||
}
|
||||
b = append(b, []byte(net.ParseIP(host).To4())...)
|
||||
p, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
t.Errorf("strconv.Atoi failed: %v", err)
|
||||
return
|
||||
}
|
||||
b = append(b, []byte{byte(p >> 8), byte(p)}...)
|
||||
if _, err := c.Write(b); err != nil {
|
||||
t.Errorf("net.Conn.Write failed: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// SOCKS5 returns a Dialer that makes SOCKSv5 connections to the given address
|
||||
// with an optional username and password. See RFC 1928.
|
||||
func SOCKS5(network, addr string, auth *Auth, forward Dialer) (Dialer, error) {
|
||||
s := &socks5{
|
||||
network: network,
|
||||
addr: addr,
|
||||
forward: forward,
|
||||
}
|
||||
if auth != nil {
|
||||
s.user = auth.User
|
||||
s.password = auth.Password
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type socks5 struct {
|
||||
user, password string
|
||||
network, addr string
|
||||
forward Dialer
|
||||
}
|
||||
|
||||
const socks5Version = 5
|
||||
|
||||
const (
|
||||
socks5AuthNone = 0
|
||||
socks5AuthPassword = 2
|
||||
)
|
||||
|
||||
const socks5Connect = 1
|
||||
|
||||
const (
|
||||
socks5IP4 = 1
|
||||
socks5Domain = 3
|
||||
socks5IP6 = 4
|
||||
)
|
||||
|
||||
var socks5Errors = []string{
|
||||
"",
|
||||
"general failure",
|
||||
"connection forbidden",
|
||||
"network unreachable",
|
||||
"host unreachable",
|
||||
"connection refused",
|
||||
"TTL expired",
|
||||
"command not supported",
|
||||
"address type not supported",
|
||||
}
|
||||
|
||||
// Dial connects to the address addr on the network net via the SOCKS5 proxy.
|
||||
func (s *socks5) Dial(network, addr string) (net.Conn, error) {
|
||||
switch network {
|
||||
case "tcp", "tcp6", "tcp4":
|
||||
default:
|
||||
return nil, errors.New("proxy: no support for SOCKS5 proxy connections of type " + network)
|
||||
}
|
||||
|
||||
conn, err := s.forward.Dial(s.network, s.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
closeConn := &conn
|
||||
defer func() {
|
||||
if closeConn != nil {
|
||||
(*closeConn).Close()
|
||||
}
|
||||
}()
|
||||
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("proxy: failed to parse port number: " + portStr)
|
||||
}
|
||||
if port < 1 || port > 0xffff {
|
||||
return nil, errors.New("proxy: port number out of range: " + portStr)
|
||||
}
|
||||
|
||||
// the size here is just an estimate
|
||||
buf := make([]byte, 0, 6+len(host))
|
||||
|
||||
buf = append(buf, socks5Version)
|
||||
if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 {
|
||||
buf = append(buf, 2 /* num auth methods */, socks5AuthNone, socks5AuthPassword)
|
||||
} else {
|
||||
buf = append(buf, 1 /* num auth methods */, socks5AuthNone)
|
||||
}
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return nil, errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return nil, errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
if buf[0] != 5 {
|
||||
return nil, errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0])))
|
||||
}
|
||||
if buf[1] == 0xff {
|
||||
return nil, errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication")
|
||||
}
|
||||
|
||||
if buf[1] == socks5AuthPassword {
|
||||
buf = buf[:0]
|
||||
buf = append(buf, 1 /* password protocol version */)
|
||||
buf = append(buf, uint8(len(s.user)))
|
||||
buf = append(buf, s.user...)
|
||||
buf = append(buf, uint8(len(s.password)))
|
||||
buf = append(buf, s.password...)
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return nil, errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return nil, errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if buf[1] != 0 {
|
||||
return nil, errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password")
|
||||
}
|
||||
}
|
||||
|
||||
buf = buf[:0]
|
||||
buf = append(buf, socks5Version, socks5Connect, 0 /* reserved */)
|
||||
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
buf = append(buf, socks5IP4)
|
||||
ip = ip4
|
||||
} else {
|
||||
buf = append(buf, socks5IP6)
|
||||
}
|
||||
buf = append(buf, ip...)
|
||||
} else {
|
||||
if len(host) > 255 {
|
||||
return nil, errors.New("proxy: destination hostname too long: " + host)
|
||||
}
|
||||
buf = append(buf, socks5Domain)
|
||||
buf = append(buf, byte(len(host)))
|
||||
buf = append(buf, host...)
|
||||
}
|
||||
buf = append(buf, byte(port>>8), byte(port))
|
||||
|
||||
if _, err := conn.Write(buf); err != nil {
|
||||
return nil, errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
|
||||
return nil, errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
failure := "unknown error"
|
||||
if int(buf[1]) < len(socks5Errors) {
|
||||
failure = socks5Errors[buf[1]]
|
||||
}
|
||||
|
||||
if len(failure) > 0 {
|
||||
return nil, errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure)
|
||||
}
|
||||
|
||||
bytesToDiscard := 0
|
||||
switch buf[3] {
|
||||
case socks5IP4:
|
||||
bytesToDiscard = net.IPv4len
|
||||
case socks5IP6:
|
||||
bytesToDiscard = net.IPv6len
|
||||
case socks5Domain:
|
||||
_, err := io.ReadFull(conn, buf[:1])
|
||||
if err != nil {
|
||||
return nil, errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
bytesToDiscard = int(buf[0])
|
||||
default:
|
||||
return nil, errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr)
|
||||
}
|
||||
|
||||
if cap(buf) < bytesToDiscard {
|
||||
buf = make([]byte, bytesToDiscard)
|
||||
} else {
|
||||
buf = buf[:bytesToDiscard]
|
||||
}
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
return nil, errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
// Also need to discard the port number
|
||||
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
|
||||
return nil, errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error())
|
||||
}
|
||||
|
||||
closeConn = nil
|
||||
return conn, nil
|
||||
}
|
||||
@@ -18,7 +18,7 @@ brbecker <brbecker@gmail.com>
|
||||
brendanlong <self@brendanlong.com>
|
||||
brgmnn <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
|
||||
bsidhom <bsidhom@gmail.com>
|
||||
burkemw3 <mburke@amplify.com>
|
||||
burkemw3 <mburke@amplify.com> <burkemw3@gmail.com>
|
||||
calmh <jakob@nym.se>
|
||||
canton7 <antony.male@gmail.com>
|
||||
cdata <chris@scriptolo.gy>
|
||||
|
||||
@@ -48,7 +48,7 @@ Please see the [Syncthing documentation site][6].
|
||||
|
||||
All code is licensed under the [MPLv2 License][7].
|
||||
|
||||
[1]: https://github.com/syncthing/specs/blob/master/BEPv1.md
|
||||
[1]: http://docs.syncthing.net/specs/bep-v1.html
|
||||
[2]: http://docs.syncthing.net/intro/getting-started.html
|
||||
[3]: https://github.com/syncthing/syncthing/blob/master/etc
|
||||
[4]: https://webchat.freenode.net/
|
||||
|
||||
@@ -195,7 +195,11 @@ func bench(pkg string) {
|
||||
}
|
||||
|
||||
func install(pkg string, tags []string) {
|
||||
os.Setenv("GOBIN", "./bin")
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
os.Setenv("GOBIN", filepath.Join(cwd, "bin"))
|
||||
args := []string{"install", "-v", "-ldflags", ldflags()}
|
||||
if len(tags) > 0 {
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
|
||||
+25
-1
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/connections"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/dialer"
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
@@ -435,6 +436,8 @@ func upgradeViaRest() error {
|
||||
r.Header.Set("X-API-Key", cfg.GUI().APIKey())
|
||||
|
||||
tr := &http.Transport{
|
||||
Dial: dialer.Dial,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{
|
||||
@@ -513,6 +516,7 @@ func syncthingMain() {
|
||||
|
||||
l.Infoln(LongVersion)
|
||||
l.Infoln("My ID:", myID)
|
||||
printHashRate()
|
||||
|
||||
// Emit the Starting event, now that we know who we are.
|
||||
|
||||
@@ -631,6 +635,10 @@ func syncthingMain() {
|
||||
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
|
||||
}
|
||||
|
||||
protectedFiles := []string{
|
||||
locations[locDatabase], locations[locConfigFile], locations[locCertFile], locations[locKeyFile],
|
||||
}
|
||||
|
||||
// Remove database entries for folders that no longer exist in the config
|
||||
folders := cfg.Folders()
|
||||
for _, folder := range db.ListFolders(ldb) {
|
||||
@@ -640,7 +648,7 @@ func syncthingMain() {
|
||||
}
|
||||
}
|
||||
|
||||
m := model.NewModel(cfg, myID, myName, "syncthing", Version, ldb)
|
||||
m := model.NewModel(cfg, myID, myName, "syncthing", Version, ldb, protectedFiles)
|
||||
cfg.Subscribe(m)
|
||||
|
||||
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
|
||||
@@ -833,6 +841,22 @@ func syncthingMain() {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// printHashRate prints the hashing performance in MB/s, formatting it with
|
||||
// appropriate precision for the value, i.e. 182 MB/s, 18 MB/s, 1.8 MB/s, 0.18
|
||||
// MB/s.
|
||||
func printHashRate() {
|
||||
hashRate := cpuBench(3, 100*time.Millisecond)
|
||||
|
||||
decimals := 0
|
||||
if hashRate < 1 {
|
||||
decimals = 2
|
||||
} else if hashRate < 10 {
|
||||
decimals = 1
|
||||
}
|
||||
|
||||
l.Infof("Single thread hash performance is ~%.*f MB/s", decimals, hashRate)
|
||||
}
|
||||
|
||||
func loadConfig(cfgFile string) (*config.Wrapper, string, error) {
|
||||
info, err := os.Stat(cfgFile)
|
||||
if err != nil {
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestFolderErrors(t *testing.T) {
|
||||
|
||||
// Case 1 - new folder, directory and marker created
|
||||
|
||||
m := model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m := model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err != nil {
|
||||
@@ -73,7 +73,7 @@ func TestFolderErrors(t *testing.T) {
|
||||
Folders: []config.FolderConfiguration{fcfg},
|
||||
})
|
||||
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err != nil {
|
||||
@@ -96,7 +96,7 @@ func TestFolderErrors(t *testing.T) {
|
||||
{Name: "dummyfile"},
|
||||
})
|
||||
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder marker missing" {
|
||||
@@ -127,7 +127,7 @@ func TestFolderErrors(t *testing.T) {
|
||||
Folders: []config.FolderConfiguration{fcfg},
|
||||
})
|
||||
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder path missing" {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/dialer"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
@@ -111,15 +112,7 @@ func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
|
||||
var mem runtime.MemStats
|
||||
runtime.ReadMemStats(&mem)
|
||||
res["memoryUsageMiB"] = (mem.Sys - mem.HeapReleased) / 1024 / 1024
|
||||
|
||||
var perf float64
|
||||
for i := 0; i < 5; i++ {
|
||||
p := cpuBench()
|
||||
if p > perf {
|
||||
perf = p
|
||||
}
|
||||
}
|
||||
res["sha256Perf"] = perf
|
||||
res["sha256Perf"] = cpuBench(5, 125*time.Millisecond)
|
||||
|
||||
bytes, err := memorySize()
|
||||
if err == nil {
|
||||
@@ -261,7 +254,7 @@ func (s *usageReportingService) sendUsageReport() error {
|
||||
if BuildEnv == "android" {
|
||||
// This works around the lack of DNS resolution on Android... :(
|
||||
transp.Dial = func(network, addr string) (net.Conn, error) {
|
||||
return net.Dial(network, "194.126.249.13:443")
|
||||
return dialer.Dial(network, "194.126.249.13:443")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +293,17 @@ func (s *usageReportingService) Stop() {
|
||||
}
|
||||
|
||||
// cpuBench returns CPU performance as a measure of single threaded SHA-256 MiB/s
|
||||
func cpuBench() float64 {
|
||||
func cpuBench(iterations int, duration time.Duration) float64 {
|
||||
var perf float64
|
||||
for i := 0; i < iterations; i++ {
|
||||
if v := cpuBenchOnce(duration); v > perf {
|
||||
perf = v
|
||||
}
|
||||
}
|
||||
return perf
|
||||
}
|
||||
|
||||
func cpuBenchOnce(duration time.Duration) float64 {
|
||||
chunkSize := 100 * 1 << 10
|
||||
h := sha256.New()
|
||||
bs := make([]byte, chunkSize)
|
||||
@@ -308,7 +311,7 @@ func cpuBench() float64 {
|
||||
|
||||
t0 := time.Now()
|
||||
b := 0
|
||||
for time.Since(t0) < 125*time.Millisecond {
|
||||
for time.Since(t0) < duration {
|
||||
h.Write(bs)
|
||||
b += chunkSize
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Устройство {{device}} ({{address}}) желае да се свърже. Добави ново устройство?",
|
||||
"Devices": "Устройства",
|
||||
"Disconnected": "Прекрати Връзката",
|
||||
"Discovery": "Откриване",
|
||||
"Documentation": "Документация",
|
||||
"Download Rate": "Скорост на Теглене",
|
||||
"Downloaded": "Изтеглен",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositiu {{device}} ({{address}}) vol conectar-se. Afegir nou dispositiu?",
|
||||
"Devices": "Dispositius",
|
||||
"Disconnected": "Desconnectat",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Documentació",
|
||||
"Download Rate": "Tasca de descarrega",
|
||||
"Downloaded": "Descarregat",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositiu {{device}} ({{address}}) vol connectar-se. Afegir nou dispositiu?",
|
||||
"Devices": "Dispositius",
|
||||
"Disconnected": "Desconnectat",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Documentació",
|
||||
"Download Rate": "Velocitat de descàrrega",
|
||||
"Downloaded": "Descarregat",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Přístroj {{device}} ({{address}}) žádá o připojení. Chcete ho přidat?",
|
||||
"Devices": "Přístroje",
|
||||
"Disconnected": "Odpojen",
|
||||
"Discovery": "Oznamování",
|
||||
"Documentation": "Dokumentace",
|
||||
"Download Rate": "Rychlost stahování",
|
||||
"Downloaded": "Staženo",
|
||||
@@ -79,7 +80,7 @@
|
||||
"Ignore Patterns": "Ignorované vzory",
|
||||
"Ignore Permissions": "Ignorovat oprávnění",
|
||||
"Incoming Rate Limit (KiB/s)": "Omezení příchozí rychlosti (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Nesprávné nastavení může poškodit obsah Vašich složek a učinit Syncthing nefunkční.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Nesprávné nastavení může poškodit obsah Vašich adresářů a učinit Syncthing nefunkční.",
|
||||
"Introducer": "Zavaděč",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Prohození zadané podmínky (např. nevynechat)",
|
||||
"Keep Versions": "Ponechat verze",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Gerät {{device}} ({{address}}) möchte sich verbinden. Gerät hinzufügen?",
|
||||
"Devices": "Geräte",
|
||||
"Disconnected": "Getrennt",
|
||||
"Discovery": "Gerätesuche",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Download-Rate",
|
||||
"Downloaded": "Heruntergeladen",
|
||||
@@ -148,7 +149,7 @@
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Kurze ID für das Verzeichnis. Muss auf allen Verbunds-Geräten gleich sein.",
|
||||
"Show ID": "ID anzeigen",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstatt der Geräte ID angezeigt. Wird als optionaler Gerätename an die anderen Clients im Cluster weitergegeben.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird anstatt der Geräte ID im Verbunds-Status angezeigt.Wird auf den Namen aktualisiert, den das Gerät angibt, wenn nichts eingetragen wird.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird auf diesem Gerät als Gerätename angezeigt und an die anderen Geräte im Geräte-Verbund weitergegeben. Wenn kein Gerätename anegegeben wird, wird der Name des entfernten Gerätes genommen.",
|
||||
"Shutdown": "Herunterfahren",
|
||||
"Shutdown Complete": "Vollständig Heruntergefahren",
|
||||
"Simple File Versioning": "Einfache Dateiversionierung",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Η συσκευή {{device}} ({{address}}) επιθυμεί να συνδεθεί. Προσθήκη της νέας συσκευής1",
|
||||
"Devices": "Συσκευές",
|
||||
"Disconnected": "Αποσυνδεδεμένος",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Τεκμηρίωση",
|
||||
"Download Rate": "Ταχύτητα λήψης",
|
||||
"Downloaded": "Έχει ληφθεί",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Disconnected",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Download Rate",
|
||||
"Downloaded": "Downloaded",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Disconnected",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Download Rate",
|
||||
"Downloaded": "Downloaded",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositivo {{device}} ({{address}}) quiere conectarse. ¿Añadir nuevo dispositivo?",
|
||||
"Devices": "Dispositivos",
|
||||
"Disconnected": "Desconectado",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Documentación",
|
||||
"Download Rate": "Velocidad de descarga",
|
||||
"Downloaded": "Descargado",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositivo {{device}} ({{address}}) se quiere conectar. ¿Agregar nuevo dispositivo?",
|
||||
"Devices": "Dispositivos",
|
||||
"Disconnected": "Desconectado",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Documentación",
|
||||
"Download Rate": "Tasa de descarga",
|
||||
"Downloaded": "Descargado",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Laite {{device}} ({{address}}) haluaa yhdistää. Lisää uusi laite?",
|
||||
"Devices": "Laitteet",
|
||||
"Disconnected": "Yhteys katkaistu",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Dokumentaatio",
|
||||
"Download Rate": "Latausmäärä",
|
||||
"Downloaded": "Ladattu",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "L'appareil {{device}} ({{address}}) veut se connecter. Voulez-vous ajouter cette appareil ?",
|
||||
"Devices": "Appareil",
|
||||
"Disconnected": "Déconnecté",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Débit de réception",
|
||||
"Downloaded": "Téléchargé",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "L'appareil {{device}} ({{address}}) veut se connecter. Voulez-vous ajouter cette appareil ?",
|
||||
"Devices": "Appareil",
|
||||
"Disconnected": "Déconnecté",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Débit de réception",
|
||||
"Downloaded": "Téléchargé",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"Connection Error": "Kapcsolódási hiba",
|
||||
"Copied from elsewhere": "Másolva máshonnan",
|
||||
"Copied from original": "Másolva az eredetiről",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 az alábbi Közreműködők",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 az alábbi Közreműködők:",
|
||||
"Delete": "Törlés",
|
||||
"Deleted": "Törölve",
|
||||
"Device ID": "Eszköz azonosító",
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "{{device}} ({{address}}) csatlakozni szeretne. Hozzáadod az új eszközt?",
|
||||
"Devices": "Eszközök",
|
||||
"Disconnected": "Kapcsolat bontva",
|
||||
"Discovery": "Felfedezés",
|
||||
"Documentation": "Dokumentáció",
|
||||
"Download Rate": "Letöltési sebesség",
|
||||
"Downloaded": "Letöltve",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"Add new folder?": "Aggiungere una nuova cartella?",
|
||||
"Address": "Indirizzo",
|
||||
"Addresses": "Indirizzi",
|
||||
"Advanced": "Advanced",
|
||||
"Advanced Configuration": "Advanced Configuration",
|
||||
"Advanced": "Avanzato",
|
||||
"Advanced Configuration": "Configurazione avanzata",
|
||||
"All Data": "Tutti i Dati",
|
||||
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
|
||||
"Alphabetic": "Alfabetico",
|
||||
@@ -19,7 +19,7 @@
|
||||
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Qualsiasi dispositivo configurato in un introduttore verrà aggiunto anche a questo dispositivo.",
|
||||
"Automatic upgrades": "Aggiornamenti automatici",
|
||||
"Be careful!": "Be careful!",
|
||||
"Be careful!": "Fai attenzione!",
|
||||
"Bugs": "Bug",
|
||||
"CPU Utilization": "Utilizzo CPU",
|
||||
"Changelog": "Changelog",
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Il dispositivo {{device}} ({{address}}) chiede di connettersi. Aggiungere il nuovo dispositivo?",
|
||||
"Devices": "Dispositivi",
|
||||
"Disconnected": "Disconnesso",
|
||||
"Discovery": "Individuazione",
|
||||
"Documentation": "Documentazione",
|
||||
"Download Rate": "Velocità Download",
|
||||
"Downloaded": "Scaricato",
|
||||
@@ -49,23 +50,23 @@
|
||||
"Edit Folder": "Modifica Cartella",
|
||||
"Editing": "Modifica di",
|
||||
"Enable UPnP": "Attiva UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserisci indirizzi separati da virgola (\"tcp://ip:porta\", \"tcp://host:porta\") oppure \"dynamic\" per effettuare il rilevamento automatico dell'indirizzo.",
|
||||
"Enter ignore patterns, one per line.": "Inserisci gli schemi di esclusione, uno per riga.",
|
||||
"Error": "Errore",
|
||||
"External File Versioning": "Controllo Versione Esterno",
|
||||
"Failed Items": "Failed Items",
|
||||
"Failed Items": "Elementi errati",
|
||||
"File Pull Order": "Ordine di prelievo dei file",
|
||||
"File Versioning": "Controllo Versione dei File",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Il software evita i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "I file sono spostati nella certella .stversions quando vengono sostituiti o cancellati da Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "I file sostituiti o eliminati da Syncthing vengono datati e spostati in una cartella .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.": "I file sono protetti dalle modifiche effettuate negli altri dispositivi, ma le modifiche effettuate in questo dispositivo verranno inviate anche al resto del cluster.",
|
||||
"Folder": "Folder",
|
||||
"Folder": "Cartella",
|
||||
"Folder ID": "ID Cartella",
|
||||
"Folder Master": "Cartella Principale",
|
||||
"Folder Path": "Percorso Cartella",
|
||||
"Folders": "Cartelle",
|
||||
"GUI": "GUI",
|
||||
"GUI": "Interfaccia grafica utente",
|
||||
"GUI Authentication Password": "Password di Autenticazione dell'Utente",
|
||||
"GUI Authentication User": "Utente dell'Interfaccia Grafica",
|
||||
"GUI Listen Addresses": "Indirizzi dell'Interfaccia Grafica",
|
||||
@@ -74,12 +75,12 @@
|
||||
"Global Discovery Server": "Server di Ricerca Globale",
|
||||
"Global State": "Stato Globale",
|
||||
"Help": "Aiuto",
|
||||
"Home page": "Home page",
|
||||
"Home page": "Pagina home",
|
||||
"Ignore": "Ignora",
|
||||
"Ignore Patterns": "Schemi Esclusione File",
|
||||
"Ignore Permissions": "Ignora Permessi",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite Velocità in Ingresso (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configurazione incorretta potrebbe danneggiare il contenuto delle cartelle e rendere Syncthing inoperativo.",
|
||||
"Introducer": "Introduttore",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversione della condizione indicata (ad es. non escludere)",
|
||||
"Keep Versions": "Versioni Mantenute",
|
||||
@@ -93,7 +94,7 @@
|
||||
"Major Upgrade": "Aggiornamento principale",
|
||||
"Maximum Age": "Durata Massima",
|
||||
"Metadata Only": "Solo i Metadati",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "Minimo spazio libero su disco",
|
||||
"Move to top of queue": "Posiziona in cima alla coda",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Metacarattere multi-livello (corrisponde alle cartelle e alle sotto-cartelle)",
|
||||
"Never": "Mai",
|
||||
@@ -106,15 +107,15 @@
|
||||
"OK": "OK",
|
||||
"Off": "Disattiva",
|
||||
"Oldest First": "Prima il meno recente",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Out of Sync",
|
||||
"Options": "Opzioni",
|
||||
"Out of Sync": "Non sincronizzato",
|
||||
"Out of Sync Items": "Elementi Non Sincronizzati",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite Velocità in Uscita (KiB/s)",
|
||||
"Override Changes": "Ignora Modifiche",
|
||||
"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": "Percorso della cartella nel computer locale. Verrà creata se non esiste già. Il carattere tilde (~) può essere utilizzato come scorciatoia per",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Percorso di salvataggio delle versioni (lasciare vuoto per utilizzare la cartella predefinita .stversions in questa cartella).",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Pause": "Pausa",
|
||||
"Paused": "In pausa",
|
||||
"Please consult the release notes before performing a major upgrade.": "Si prega di consultare le note di rilascio prima di eseguire un aggiornamento principale.",
|
||||
"Please wait": "Attendere prego",
|
||||
"Preview": "Anteprima",
|
||||
@@ -122,17 +123,17 @@
|
||||
"Quick guide to supported patterns": "Guida veloce agli schemi supportati",
|
||||
"RAM Utilization": "Utilizzo RAM",
|
||||
"Random": "Casuale",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Relayed via": "Reindirizzato tramite",
|
||||
"Relays": "Servers di reindirizzamento",
|
||||
"Release Notes": "Note di rilascio",
|
||||
"Remove": "Remove",
|
||||
"Remove": "Rimuovi",
|
||||
"Rescan": "Riscansiona",
|
||||
"Rescan All": "Riscansiona Tutto",
|
||||
"Rescan Interval": "Intervallo Scansione",
|
||||
"Restart": "Riavvia",
|
||||
"Restart Needed": "Riavvio Necessario",
|
||||
"Restarting": "Riavvio",
|
||||
"Resume": "Resume",
|
||||
"Resume": "Riprendi",
|
||||
"Reused": "Riutilizzato",
|
||||
"Save": "Salva",
|
||||
"Scanning": "Scansione in corso",
|
||||
@@ -157,7 +158,7 @@
|
||||
"Source Code": "Codice Sorgente",
|
||||
"Staggered File Versioning": "Controllo Versione Cadenzato",
|
||||
"Start Browser": "Avvia Browser",
|
||||
"Statistics": "Statistics",
|
||||
"Statistics": "Statistiche",
|
||||
"Stopped": "Fermato",
|
||||
"Support": "Supporto",
|
||||
"Sync Protocol Listen Addresses": "Indirizzi del Protocollo di Sincronizzazione",
|
||||
@@ -180,18 +181,18 @@
|
||||
"The folder ID must be unique.": "L'ID della cartella dev'essere unico.",
|
||||
"The folder path cannot be blank.": "Il percorso della cartella non può essere vuoto.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Vengono utilizzati i seguenti intervalli temporali: per la prima ora viene mantenuta una versione ogni 30 secondi, per il primo giorno viene mantenuta una versione ogni ora, per i primi 30 giorni viene mantenuta una versione al giorno, successivamente viene mantenuta una versione ogni settimana fino al periodo massimo impostato.",
|
||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
||||
"The following items could not be synchronized.": "Non è stato possibile sincronizzare i seguenti elementi",
|
||||
"The maximum age must be a number and cannot be blank.": "La durata massima dev'essere un numero e non può essere vuoto.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La durata massima di una versione (in giorni, imposta a 0 per mantenere le versioni per sempre).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Lo spazio libero minimo su disco deve essere un numero non negativo tra 0 e 100 (inclusi)",
|
||||
"The number of days must be a number and cannot be blank.": "Il numero di giorni deve essere un numero e non può essere vuoto.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Il numero di giorni per conservare i file nel cestino. Zero significa per sempre.",
|
||||
"The number of old versions to keep, per file.": "Il numero di vecchie versioni da mantenere, per file.",
|
||||
"The number of versions must be a number and cannot be blank.": "Il numero di versioni dev'essere un numero e non può essere vuoto.",
|
||||
"The path cannot be blank.": "Il percorso non può essere vuoto.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Il limite di banda deve essere un numero non negativo (da 0 a infinito)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "L'intervallo di scansione deve essere un numero superiore a zero secondi.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Verranno effettuati tentativi in automatico e verranno sincronizzati quando l'errore sarà risolto.",
|
||||
"This is a major version upgrade.": "Questo è un aggiornamento di versione principale",
|
||||
"Trash Can File Versioning": "Controllo Versione con Cestino",
|
||||
"Unknown": "Sconosciuto",
|
||||
@@ -212,7 +213,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Quando aggiungi una nuova cartella, ricordati che gli ID vengono utilizzati per collegare le cartelle nei dispositivi. Distinguono maiuscole e minuscole e devono corrispondere esattamente su tutti i dispositivi.",
|
||||
"Yes": "Sì",
|
||||
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
|
||||
"days": "days",
|
||||
"days": "giorni",
|
||||
"full documentation": "documentazione completa",
|
||||
"items": "elementi",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vuole condividere la cartella \"{{folder}}\"."
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "デバイス {{device}} ({{address}}) が接続を求めています。新しいデバイスとして追加しますか?",
|
||||
"Devices": "デバイス",
|
||||
"Disconnected": "切断中",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "マニュアル",
|
||||
"Download Rate": "ダウンロード速度",
|
||||
"Downloaded": "ダウンロード済",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "다른 기기 {{device}} ({{address}}) 에서 접속을 요청했습니다. 새 장치를 추가하시겠습니까?",
|
||||
"Devices": "기기",
|
||||
"Disconnected": "연결 끊김",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "문서",
|
||||
"Download Rate": "다운로드 속도",
|
||||
"Downloaded": "다운로드됨",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"Add new folder?": "Pridėti naują aplanką?",
|
||||
"Address": "Adresas",
|
||||
"Addresses": "Adresai",
|
||||
"Advanced": "Advanced",
|
||||
"Advanced Configuration": "Advanced Configuration",
|
||||
"Advanced": "Pažangus",
|
||||
"Advanced Configuration": "Pažangus nustatymai",
|
||||
"All Data": "Visiems duomenims",
|
||||
"Allow Anonymous Usage Reporting?": "Siųsti anonimišką vartojimo ataskaitą?",
|
||||
"Alphabetic": "Abėcėlės tvarka",
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Įrenginys {{device}} ({{address}}) nori prisijungti. Pridėti naują įrenginį?",
|
||||
"Devices": "Įrenginiai",
|
||||
"Disconnected": "Atsijungęs",
|
||||
"Discovery": "Lokacija",
|
||||
"Documentation": "Aprašymas",
|
||||
"Download Rate": "Parsisiuntimo greitis",
|
||||
"Downloaded": "Parsisiųstas",
|
||||
@@ -53,19 +54,19 @@
|
||||
"Enter ignore patterns, one per line.": "Suveskite nepaisomus šablonus, kiekvieną naujoje eilutėje.",
|
||||
"Error": "Klaida",
|
||||
"External File Versioning": "Išorinis versijų valdymas",
|
||||
"Failed Items": "Failed Items",
|
||||
"Failed Items": "Nepavykę siuntimai",
|
||||
"File Pull Order": "Failų siuntimo tvarka",
|
||||
"File Versioning": "Versijų valdymas",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Ieškant pakeitimų, į failų leidimų bitus yra nekreipiama dėmesio. Naudoti FAT failų sistemose.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Failai perkeliami į .stversions aplanką kai tampa pakeisti arba ištrinti.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Programai Syncthing pakeičiant ar ištrinant failus, jie yra perkeliami į datomis pažymėtas versijas, aplanke .stversions.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Failai apsaugoti nuo pakeitimų atliktų kituose įrenginiuose, bet pakeitimai šiame įrenginyje bus nusiųsti kitiems.",
|
||||
"Folder": "Folder",
|
||||
"Folder": "Aplankas",
|
||||
"Folder ID": "Aplanko ID",
|
||||
"Folder Master": "Aplanko vadovas",
|
||||
"Folder Path": "Kelias iki apkanko",
|
||||
"Folders": "Aplankai",
|
||||
"GUI": "GUI",
|
||||
"GUI": "Valdymo skydelis",
|
||||
"GUI Authentication Password": "Valdymo skydelio slaptažodis",
|
||||
"GUI Authentication User": "Valdymo skydelio vartotojo vardas",
|
||||
"GUI Listen Addresses": "Valdymo skydelio adresas",
|
||||
@@ -74,7 +75,7 @@
|
||||
"Global Discovery Server": "Visuotinio matomumo serveris",
|
||||
"Global State": "Visuotinė būsena",
|
||||
"Help": "Pagalba",
|
||||
"Home page": "Home page",
|
||||
"Home page": "Pagrindinis puslapis",
|
||||
"Ignore": "Ignoruoti",
|
||||
"Ignore Patterns": "Nepaisyti šablonų",
|
||||
"Ignore Permissions": "Nepaisyti failų prieigos leidimų",
|
||||
@@ -113,8 +114,8 @@
|
||||
"Override Changes": "Perrašyti pakeitimus",
|
||||
"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": "Kelias iki aplanko šiame kompiuteryje. Bus sukurtas, jei neegzistuoja. Tildės simbolis (~) gali būti naudojamas kaip trumpinys",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Kelias, kur bus saugomos versijos (palikite tuščią numatytam .stversions aplankui).",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Pause": "Sustabdyti",
|
||||
"Paused": "Sustabdyta",
|
||||
"Please consult the release notes before performing a major upgrade.": "Peržvelkite laidos informaciją prieš atlikdami stambų atnaujinimą.",
|
||||
"Please wait": "Prašome palaukti",
|
||||
"Preview": "Peržiūra",
|
||||
@@ -122,8 +123,8 @@
|
||||
"Quick guide to supported patterns": "Trumpas leistinų šablonų vadovas",
|
||||
"RAM Utilization": "Atminties naudojimas",
|
||||
"Random": "Atsitiktinė",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Relayed via": "Retransliuojama per",
|
||||
"Relays": "Retransliatoriai",
|
||||
"Release Notes": "Laidos Informacija",
|
||||
"Remove": "Pašalinti",
|
||||
"Rescan": "Nuskaityti iš naujo",
|
||||
@@ -132,7 +133,7 @@
|
||||
"Restart": "Perleisti",
|
||||
"Restart Needed": "Reikalingas perleidimas",
|
||||
"Restarting": "Persileidžia",
|
||||
"Resume": "Resume",
|
||||
"Resume": "Pratęsti",
|
||||
"Reused": "Pakartotinas",
|
||||
"Save": "Išsaugoti",
|
||||
"Scanning": "Skenuojama",
|
||||
@@ -180,7 +181,7 @@
|
||||
"The folder ID must be unique.": "Aplanko ID turi būti unikalus.",
|
||||
"The folder path cannot be blank.": "Kelias iki aplanko negali būti tuščias.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Šie pertraukų nustatymai naudojami: pirmą valandą versijos laikomos 30 sekundžių, pirmą dieną versijos laikomos valandą, pirmas 30 dienų versijos laikomos parą, kol nebus viršytas nustatytas maksimalus amžius.",
|
||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
||||
"The following items could not be synchronized.": "Nepavyko parsiųsti šių failų",
|
||||
"The maximum age must be a number and cannot be blank.": "Maksimalus amžius turi būti skaitmuo ir negali būti tuščias laukelis.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimalus laikas kurį bus saugojama versija (dienomis, nustatykite 0 norėdami saugoti amžinai).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
@@ -191,7 +192,7 @@
|
||||
"The path cannot be blank.": "Kelias negali būti tuščias.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Srauto maksimalus greitis privalo būti ne neigiamas skaičius (0: nėra apribojimo)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Failus bus automatiškai badoma parsiųsti dar kartą kai išspręsite klaidas",
|
||||
"This is a major version upgrade.": "Tai yra stambus atnaujinimas.",
|
||||
"Trash Can File Versioning": "Šiukšliadėžės versijų valdymas",
|
||||
"Unknown": "Nežinoma",
|
||||
@@ -212,7 +213,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Kai įvedate naują aplanką neužmirškite, kad jis bus naudojamas visuose įrenginiuose. Svarbu visur įvesti visiškai tokį pat aplanko vardą neužmirštant apie didžiąsias ir mažąsias raides.",
|
||||
"Yes": "Taip",
|
||||
"You must keep at least one version.": "Būtina saugoti bent vieną versiją.",
|
||||
"days": "days",
|
||||
"days": "dienos",
|
||||
"full documentation": "pilna dokumentacija",
|
||||
"items": "įrašai",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} nori dalintis aplanku \"{{folder}}\""
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enhet {{device}} ({{address}}) ønsker å koble seg til. Legg til ny enhet?",
|
||||
"Devices": "Enheter",
|
||||
"Disconnected": "Frakoblet",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Dokumentasjon",
|
||||
"Download Rate": "Nedlastingsrate",
|
||||
"Downloaded": "Lastet ned",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Apparaat {{device}} ({{address}}) wil verbinding maken. Dit apparaat toevoegen?",
|
||||
"Devices": "Apparaten",
|
||||
"Disconnected": "Niet verbonden",
|
||||
"Discovery": "Zoeken",
|
||||
"Documentation": "Documentatie",
|
||||
"Download Rate": "Downloadsnelheid",
|
||||
"Downloaded": "Gedownload",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Eininga {{device}} ({{address}}) vil kopla seg til. Vil du leggja ho til?",
|
||||
"Devices": "Einingar",
|
||||
"Disconnected": "Fråkopla",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Dokumentasjon",
|
||||
"Download Rate": "Nedlastingsfart",
|
||||
"Downloaded": "Lasta ned",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Urządzenie {{device}} ({{address}}) chce się połączyć. Zezwolić?",
|
||||
"Devices": "Urządzenia",
|
||||
"Disconnected": "Rozłączony",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Dokumentacja",
|
||||
"Download Rate": "Prędkość pobierania",
|
||||
"Downloaded": "Pobrane",
|
||||
@@ -49,7 +50,7 @@
|
||||
"Edit Folder": "Edytuj folder",
|
||||
"Editing": "Edytowanie",
|
||||
"Enable UPnP": "Włącz UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Wpisz oddzielone przecinkiem adresy (\"tcp://ip:port\", \"tcp://host:port\") lub \"dynamic\" by przeprowadzić automatyczne odnalezienie adresu.",
|
||||
"Enter ignore patterns, one per line.": "Wprowadz wzorce ignorowania, jeden w każdej linii.",
|
||||
"Error": "Błąd",
|
||||
"External File Versioning": "Zewnętrzne wersjonowanie pliku",
|
||||
@@ -93,7 +94,7 @@
|
||||
"Major Upgrade": "Ważna aktualizacja",
|
||||
"Maximum Age": "Maksymalny wiek",
|
||||
"Metadata Only": "Tylko metadane",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "Minimum wolnego miejsca na dysku",
|
||||
"Move to top of queue": "Przenieś na początek kolejki",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Wieloznaczność na poziomie katalogów i plików (uwzględnia nazwy folderów i plików)",
|
||||
"Never": "Nigdy",
|
||||
@@ -107,14 +108,14 @@
|
||||
"Off": "Wyłącz",
|
||||
"Oldest First": "Najstarsze na początku",
|
||||
"Options": "Opcje",
|
||||
"Out of Sync": "Utracono synchronizację",
|
||||
"Out of Sync": "Niezsynchronizowane",
|
||||
"Out of Sync Items": "Niezsynchronizowane pliki",
|
||||
"Outgoing Rate Limit (KiB/s)": "Ograniczenie prędkości wysyłania (KiB/s)",
|
||||
"Override Changes": "Nadpisz zmiany",
|
||||
"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": "Ścieżka do lokalnego folderu. Zostanie utworzona jeżeli nie istnieje.\nZnak tyldy (~) może zostać użyty jako skrót do",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ścieżka gdzie będą przechowywane wersje (pozostaw puste dla domyślnego folderu .stversions)",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Pause": "Zatrzymaj",
|
||||
"Paused": "Zatrzymany",
|
||||
"Please consult the release notes before performing a major upgrade.": "Zaleca się przeanalizowanie \"release notes\" przed przeprowadzeniem znaczącej aktualizacji.",
|
||||
"Please wait": "Proszę czekać",
|
||||
"Preview": "Podgląd",
|
||||
@@ -122,17 +123,17 @@
|
||||
"Quick guide to supported patterns": "Krótki przewodnik po obsługiwanych wzorcach",
|
||||
"RAM Utilization": "Użycie pamięci RAM",
|
||||
"Random": "Losowo",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Relayed via": "Przekazane przez",
|
||||
"Relays": "Przekaźnik",
|
||||
"Release Notes": "Informacje o wydaniu",
|
||||
"Remove": "Remove",
|
||||
"Remove": "Usuń",
|
||||
"Rescan": "Skanuj ponownie",
|
||||
"Rescan All": "Skanuj wszystko ponownie",
|
||||
"Rescan Interval": "Interwał skanowania",
|
||||
"Restart": "Uruchom ponownie",
|
||||
"Restart Needed": "Wymagane ponowne uruchomienie",
|
||||
"Restarting": "Uruchamianie ponowne",
|
||||
"Resume": "Resume",
|
||||
"Resume": "Wznów",
|
||||
"Reused": "Ponownie użyte",
|
||||
"Save": "Zapisz",
|
||||
"Scanning": "Skanowanie",
|
||||
@@ -183,13 +184,13 @@
|
||||
"The following items could not be synchronized.": "Następujące elementy nie mogły zostać zsynchronizowane.",
|
||||
"The maximum age must be a number and cannot be blank.": "Maksymalny wiek musi być liczbą i nie może być pusty.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksymalny czas zachowania wersji (w dniach, ustaw 0 aby zachować na zawsze)",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Procent minimalnej ilości wolnego miejsca na dysku musi być nieujemną liczbą od 0 do 100 (włącznie).",
|
||||
"The number of days must be a number and cannot be blank.": "Ilość dni musi być dodatnia.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Liczba dni przez które pliki trzymane będą w koszu. Zero oznacza brak ograniczeń.",
|
||||
"The number of old versions to keep, per file.": "Liczba wersji pliku do zachowania.",
|
||||
"The number of versions must be a number and cannot be blank.": "Liczba wersji musi być liczbą i nie może być pusta.",
|
||||
"The path cannot be blank.": "Ścieżka nie może być pusta.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "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 is a major version upgrade.": "To jest ważna aktualizacja",
|
||||
@@ -212,7 +213,7 @@
|
||||
"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",
|
||||
"You must keep at least one version.": "Musisz posiadać przynajmniej jedną wersję",
|
||||
"days": "days",
|
||||
"days": "dni",
|
||||
"full documentation": "pełna dokumentacja",
|
||||
"items": "pozycji",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{folder}}\""
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "O dispositivo {{device}} ({{address}}) quer se conectar. Adicionar novo dispositivo?",
|
||||
"Devices": "Dispositivos",
|
||||
"Disconnected": "Desconectado",
|
||||
"Discovery": "Descoberta",
|
||||
"Documentation": "Documentação",
|
||||
"Download Rate": "Velocidade de recepção",
|
||||
"Downloaded": "Recebido",
|
||||
@@ -197,7 +198,7 @@
|
||||
"Unknown": "Desconhecida",
|
||||
"Unshared": "Não compartilhada",
|
||||
"Unused": "Não utilizado",
|
||||
"Up to Date": "Sincronizada",
|
||||
"Up to Date": "Em sincronia",
|
||||
"Updated": "Atualizado",
|
||||
"Upgrade": "Atualização",
|
||||
"Upgrade To {%version%}": "Atualizar para {{version}}",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "O dispositivo {{device}} ({{address}}) quer conectar-se. Adiciono este novo dispositivo?",
|
||||
"Devices": "Dispositivos",
|
||||
"Disconnected": "Desconectado",
|
||||
"Discovery": "Busca",
|
||||
"Documentation": "Documentação",
|
||||
"Download Rate": "Velocidade de recepção",
|
||||
"Downloaded": "Recebido",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Dispozitivul{{dispoztiv}}({{adresă}})vrea sa se conecteze.Adaug un dispozitiv nou?",
|
||||
"Devices": "Dispozitiv",
|
||||
"Disconnected": "Deconectat",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Documentaţie",
|
||||
"Download Rate": "Viteză de Descărcare",
|
||||
"Downloaded": "Descărcat",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Устройство {{device}} ({{address}}) хочет подключиться. Добавить новое устройство?",
|
||||
"Devices": "Устройства",
|
||||
"Disconnected": "Нет соединения",
|
||||
"Discovery": "Обнаружение",
|
||||
"Documentation": "Документация",
|
||||
"Download Rate": "Скорость загрузки",
|
||||
"Downloaded": "Загружено",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
|
||||
"A negative number of days doesn't make sense.": "Negativt antal dagar är inte troligt.",
|
||||
"A new major version may not be compatible with previous versions.": "En ny huvudversion kan eventuellt vara inkompatibel med tidigare versioner.",
|
||||
"API Key": "API-nyckel",
|
||||
"About": "Om",
|
||||
@@ -10,8 +10,8 @@
|
||||
"Add new folder?": "Lägg till katalog?",
|
||||
"Address": "Adress",
|
||||
"Addresses": "Adresser",
|
||||
"Advanced": "Advanced",
|
||||
"Advanced Configuration": "Advanced Configuration",
|
||||
"Advanced": "Avancerat",
|
||||
"Advanced Configuration": "Avancerad konfiguration",
|
||||
"All Data": "All data",
|
||||
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistik?",
|
||||
"Alphabetic": "Alfabetisk",
|
||||
@@ -19,11 +19,11 @@
|
||||
"Anonymous Usage Reporting": "Anonym användarstatistik",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Enheter konfigurerade på en introduktörsenhet kommer också att läggas till den här enheten.",
|
||||
"Automatic upgrades": "Automatisk uppgradering",
|
||||
"Be careful!": "Be careful!",
|
||||
"Be careful!": "Var aktsam!",
|
||||
"Bugs": "Buggar",
|
||||
"CPU Utilization": "CPU-användning",
|
||||
"Changelog": "Changelog",
|
||||
"Clean out after": "Clean out after",
|
||||
"Clean out after": "Rensa efteråt",
|
||||
"Close": "Stäng",
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, vid början av en rad.",
|
||||
@@ -33,13 +33,14 @@
|
||||
"Copied from original": "Oförändrat",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 följande medverkande:",
|
||||
"Delete": "Radera",
|
||||
"Deleted": "Deleted",
|
||||
"Deleted": "Borttaget",
|
||||
"Device ID": "Enhets-ID",
|
||||
"Device Identification": "Enhetsidentifikation",
|
||||
"Device Name": "Enhetsnamn",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enheten {{device}} ({{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
"Devices": "Enheter",
|
||||
"Disconnected": "Ej ansluten",
|
||||
"Discovery": "Uppslagning",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Nedladdningshastighet",
|
||||
"Downloaded": "Nerladdat",
|
||||
@@ -49,18 +50,18 @@
|
||||
"Edit Folder": "Redigera katalog",
|
||||
"Editing": "Redigerar",
|
||||
"Enable UPnP": "Använd UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\")-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
|
||||
"Enter ignore patterns, one per line.": "Ange filmönster, ett per rad.",
|
||||
"Error": "Fel",
|
||||
"External File Versioning": "Extern versionshantering",
|
||||
"Failed Items": "Failed Items",
|
||||
"Failed Items": "Misslyckade filer",
|
||||
"File Pull Order": "Hämtningsprioritering av filer",
|
||||
"File Versioning": "Versionshantering",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Filrättigheter ignoreras vid sökning efter förändringar. Används på FAT-filsystem.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till katalogen .stversions om de ersätts eller raderas av Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till datummärkta versioner i en .stversions-mapp när de ersatts eller raderats av Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer skyddas från ändringar gjorda på andra enheter, men ändringar som görs på den här noden skickas till de andra klustermedlemmarna.",
|
||||
"Folder": "Folder",
|
||||
"Folder": "Katalog",
|
||||
"Folder ID": "Katalog-ID",
|
||||
"Folder Master": "Huvudlagring",
|
||||
"Folder Path": "Sökväg",
|
||||
@@ -74,12 +75,12 @@
|
||||
"Global Discovery Server": "Global uppslagningsserver",
|
||||
"Global State": "Global status",
|
||||
"Help": "Hjälp",
|
||||
"Home page": "Home page",
|
||||
"Home page": "Hemsida",
|
||||
"Ignore": "Ignorera",
|
||||
"Ignore Patterns": "Ignorerade filmönster",
|
||||
"Ignore Permissions": "Ignorera filrättigheter",
|
||||
"Incoming Rate Limit (KiB/s)": "Max nedladdningshastighet (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Inkorrekt konfiguration kan skada innehållet i katalogen and få Syncthing att sluta fungera.",
|
||||
"Introducer": "introduktör",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Vänder på villkoret, d.v.s. exkluderar inte.",
|
||||
"Keep Versions": "Behåll versioner",
|
||||
@@ -89,11 +90,11 @@
|
||||
"Later": "Senare",
|
||||
"Local Discovery": "Lokal uppslagning",
|
||||
"Local State": "Lokal status",
|
||||
"Local State (Total)": "Local State (Total)",
|
||||
"Local State (Total)": "Lokal status (Total)",
|
||||
"Major Upgrade": "Stor uppgradering",
|
||||
"Maximum Age": "Högsta åldersgräns",
|
||||
"Metadata Only": "Endast metadata",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "Minimum ledigt diskutrymme",
|
||||
"Move to top of queue": "Flytta till överst i kön",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Jokertecken som representerar noll eller fler godtyckliga tecken, även över kataloggränser.",
|
||||
"Never": "Aldrig",
|
||||
@@ -106,15 +107,15 @@
|
||||
"OK": "OK",
|
||||
"Off": "Av",
|
||||
"Oldest First": "Äldst först",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Out of Sync",
|
||||
"Options": "Alternativ",
|
||||
"Out of Sync": "Osynkad",
|
||||
"Out of Sync Items": "Osynkade poster",
|
||||
"Outgoing Rate Limit (KiB/s)": "Max uppladdningshastighet (KiB/s)",
|
||||
"Override Changes": "Skriv över ändringar",
|
||||
"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": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sökväg där versioner sparas (lämna tomt för att använda .stversions i den ordinarie katalogen).",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Pause": "Paus",
|
||||
"Paused": "Pausad",
|
||||
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
|
||||
"Please wait": "Var god vänta",
|
||||
"Preview": "Förhandsgranska",
|
||||
@@ -122,17 +123,17 @@
|
||||
"Quick guide to supported patterns": "Snabb guide till filmönster som stöds",
|
||||
"RAM Utilization": "Minnesanvändning",
|
||||
"Random": "Slumpmässig",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Relayed via": "Vidarbefordras via",
|
||||
"Relays": "Vidarbefordringar",
|
||||
"Release Notes": "versionsnyheter",
|
||||
"Remove": "Remove",
|
||||
"Remove": "Ta bort",
|
||||
"Rescan": "Uppdatera",
|
||||
"Rescan All": "Uppdatera alla",
|
||||
"Rescan Interval": "Uppdateringsintervall",
|
||||
"Restart": "Starta om",
|
||||
"Restart Needed": "Omstart behövs",
|
||||
"Restarting": "Startar om",
|
||||
"Resume": "Resume",
|
||||
"Resume": "Återuppta",
|
||||
"Reused": "Återanvänt",
|
||||
"Save": "Spara",
|
||||
"Scanning": "Uppdaterar",
|
||||
@@ -157,7 +158,7 @@
|
||||
"Source Code": "Källkod",
|
||||
"Staggered File Versioning": "Versionshantering i intervall",
|
||||
"Start Browser": "Starta browser",
|
||||
"Statistics": "Statistics",
|
||||
"Statistics": "Statistik",
|
||||
"Stopped": "Stoppad",
|
||||
"Support": "Support",
|
||||
"Sync Protocol Listen Addresses": "Address för inkommande anslutningar",
|
||||
@@ -180,25 +181,25 @@
|
||||
"The folder ID must be unique.": "Katalog-ID:t måste vara unikt.",
|
||||
"The folder path cannot be blank.": "Ange en sökväg.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De följande intervallen används: varje 30 sekunder under den första timmen; varje timme under den första dagen; varje dag för de första 30 dagarna; varje vecka tills den maximala åldersgränsen uppnås.",
|
||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
||||
"The following items could not be synchronized.": "Följande filer kunde inte synkroniseras.",
|
||||
"The maximum age must be a number and cannot be blank.": "Åldersgränsen måste vara ett tal och kan inte lämnas tomt.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tiden att behålla en version (i dagar, sätt till 0 för att behålla versioner för evigt).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Minimum ledigt diskutrymme i procent måste vara en icke negativ siffra mellan 0 och 100 (inklusive).",
|
||||
"The number of days must be a number and cannot be blank.": "Antalet dagar måste vara en siffra och får inte vara tomt.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Antal dagar som filer ligger kvar i papperskorgen. Noll betyder för alltid.",
|
||||
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas, per fil.",
|
||||
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett nummer och kan inte lämnas tomt.",
|
||||
"The path cannot be blank.": "Ange en sökväg",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Frekvensgränsen måste vara ett icke-negativt tal (0: ingen gräns)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Förnyelseintervallet måste vara ett positivt antal sekunder",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "De omprövas automatiskt och kommer att synkroniseras när felet är löst.",
|
||||
"This is a major version upgrade.": "Det här är en stor uppgradering.",
|
||||
"Trash Can File Versioning": "Trash Can File Versioning",
|
||||
"Trash Can File Versioning": "Versionshantering på filer i papperskorgen",
|
||||
"Unknown": "Okänt",
|
||||
"Unshared": "Inte delad",
|
||||
"Unused": "Oanvänd",
|
||||
"Up to Date": "Helt uppdaterad",
|
||||
"Updated": "Updated",
|
||||
"Updated": "Uppdaterad",
|
||||
"Upgrade": "Uppgradering",
|
||||
"Upgrade To {%version%}": "Uppgradera till {{version}}",
|
||||
"Upgrading": "Uppgraderar",
|
||||
@@ -212,7 +213,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny katalog, tänk på att katalog-ID:t knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
|
||||
"days": "days",
|
||||
"days": "dagar",
|
||||
"full documentation": "fullständig dokumentation",
|
||||
"items": "poster",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\"."
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Пристрій {{device}} ({{address}}) намагається під’єднатися. Додати новий пристрій?",
|
||||
"Devices": "Пристрої",
|
||||
"Disconnected": "З’єднання відсутнє",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "Документація",
|
||||
"Download Rate": "Швидкість завантаження",
|
||||
"Downloaded": "Завантажено",
|
||||
@@ -212,7 +213,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Коли додаєте нову директорію, пам’ятайте, що ID цієї директорії використовується для того, щоб зв’язувати директорії разом між вузлами. Назви є чутливими до регістра та повинні співпадати точно між усіма вузлами.",
|
||||
"Yes": "Так",
|
||||
"You must keep at least one version.": "Ви повинні зберігати щонайменше одну версію.",
|
||||
"days": "days",
|
||||
"days": "днів",
|
||||
"full documentation": "повна документація",
|
||||
"items": "елементи",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хоче поділитися директорією \"{{folder}}\"."
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "设备:{{device}} 地址:({{address}}) 请求连接。是否添加新设备?",
|
||||
"Devices": "设备",
|
||||
"Disconnected": "连接已断开",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "文档",
|
||||
"Download Rate": "下载速度",
|
||||
"Downloaded": "已下载",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "裝置 {{device}} ({{address}}) 想要連線。要新增裝置嗎?",
|
||||
"Devices": "裝置",
|
||||
"Disconnected": "斷線",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "說明文件",
|
||||
"Download Rate": "下載速率",
|
||||
"Downloaded": "已下載",
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<li class="auto-generated">Marc Laporte</li>
|
||||
<li class="auto-generated">Marc Pujol</li>
|
||||
<li class="auto-generated">Marcin Dziadus</li>
|
||||
<li class="auto-generated">Mateon1</li>
|
||||
<li class="auto-generated">Mateusz Naściszewski</li>
|
||||
<li class="auto-generated">Matt Burke</li>
|
||||
<li class="auto-generated">Michael Jephcote</li>
|
||||
<li class="auto-generated">Michael Tilli</li>
|
||||
|
||||
@@ -1135,6 +1135,7 @@ angular.module('syncthing.core')
|
||||
};
|
||||
$scope.currentFolder.rescanIntervalS = 60;
|
||||
$scope.currentFolder.minDiskFreePct = 1;
|
||||
$scope.currentFolder.maxConflicts = -1;
|
||||
$scope.currentFolder.order = "random";
|
||||
$scope.currentFolder.fileVersioningSelector = "none";
|
||||
$scope.currentFolder.trashcanClean = 0;
|
||||
@@ -1156,6 +1157,7 @@ angular.module('syncthing.core')
|
||||
selectedDevices: {},
|
||||
rescanIntervalS: 60,
|
||||
minDiskFreePct: 1,
|
||||
maxConflicts: -1,
|
||||
order: "random",
|
||||
fileVersioningSelector: "none",
|
||||
trashcanClean: 0,
|
||||
|
||||
+33
-33
File diff suppressed because one or more lines are too long
+41
-22
@@ -11,6 +11,7 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
@@ -42,13 +43,15 @@ func NewBroadcast(port int) *Broadcast {
|
||||
}
|
||||
|
||||
b.br = &broadcastReader{
|
||||
port: port,
|
||||
outbox: b.outbox,
|
||||
port: port,
|
||||
outbox: b.outbox,
|
||||
connMut: sync.NewMutex(),
|
||||
}
|
||||
b.Add(b.br)
|
||||
b.bw = &broadcastWriter{
|
||||
port: port,
|
||||
inbox: b.inbox,
|
||||
port: port,
|
||||
inbox: b.inbox,
|
||||
connMut: sync.NewMutex(),
|
||||
}
|
||||
b.Add(b.bw)
|
||||
|
||||
@@ -72,9 +75,10 @@ func (b *Broadcast) Error() error {
|
||||
}
|
||||
|
||||
type broadcastWriter struct {
|
||||
port int
|
||||
inbox chan []byte
|
||||
conn *net.UDPConn
|
||||
port int
|
||||
inbox chan []byte
|
||||
conn *net.UDPConn
|
||||
connMut sync.Mutex
|
||||
errorHolder
|
||||
}
|
||||
|
||||
@@ -82,14 +86,17 @@ func (w *broadcastWriter) Serve() {
|
||||
l.Debugln(w, "starting")
|
||||
defer l.Debugln(w, "stopping")
|
||||
|
||||
var err error
|
||||
w.conn, err = net.ListenUDP("udp4", nil)
|
||||
conn, err := net.ListenUDP("udp4", nil)
|
||||
if err != nil {
|
||||
l.Debugln(err)
|
||||
w.setError(err)
|
||||
return
|
||||
}
|
||||
defer w.conn.Close()
|
||||
defer conn.Close()
|
||||
|
||||
w.connMut.Lock()
|
||||
w.conn = conn
|
||||
w.connMut.Unlock()
|
||||
|
||||
for bs := range w.inbox {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
@@ -118,9 +125,9 @@ func (w *broadcastWriter) Serve() {
|
||||
for _, ip := range dsts {
|
||||
dst := &net.UDPAddr{IP: ip, Port: w.port}
|
||||
|
||||
w.conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
_, err := w.conn.WriteTo(bs, dst)
|
||||
w.conn.SetWriteDeadline(time.Time{})
|
||||
conn.SetWriteDeadline(time.Now().Add(time.Second))
|
||||
_, err := conn.WriteTo(bs, dst)
|
||||
conn.SetWriteDeadline(time.Time{})
|
||||
|
||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||
// Write timeouts should not happen. We treat it as a fatal
|
||||
@@ -154,7 +161,11 @@ func (w *broadcastWriter) Serve() {
|
||||
}
|
||||
|
||||
func (w *broadcastWriter) Stop() {
|
||||
w.conn.Close()
|
||||
w.connMut.Lock()
|
||||
if w.conn != nil {
|
||||
w.conn.Close()
|
||||
}
|
||||
w.connMut.Unlock()
|
||||
}
|
||||
|
||||
func (w *broadcastWriter) String() string {
|
||||
@@ -162,9 +173,10 @@ func (w *broadcastWriter) String() string {
|
||||
}
|
||||
|
||||
type broadcastReader struct {
|
||||
port int
|
||||
outbox chan recv
|
||||
conn *net.UDPConn
|
||||
port int
|
||||
outbox chan recv
|
||||
conn *net.UDPConn
|
||||
connMut sync.Mutex
|
||||
errorHolder
|
||||
}
|
||||
|
||||
@@ -172,18 +184,21 @@ func (r *broadcastReader) Serve() {
|
||||
l.Debugln(r, "starting")
|
||||
defer l.Debugln(r, "stopping")
|
||||
|
||||
var err error
|
||||
r.conn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: r.port})
|
||||
conn, err := net.ListenUDP("udp4", &net.UDPAddr{Port: r.port})
|
||||
if err != nil {
|
||||
l.Debugln(err)
|
||||
r.setError(err)
|
||||
return
|
||||
}
|
||||
defer r.conn.Close()
|
||||
defer conn.Close()
|
||||
|
||||
r.connMut.Lock()
|
||||
r.conn = conn
|
||||
r.connMut.Unlock()
|
||||
|
||||
bs := make([]byte, 65536)
|
||||
for {
|
||||
n, addr, err := r.conn.ReadFrom(bs)
|
||||
n, addr, err := conn.ReadFrom(bs)
|
||||
if err != nil {
|
||||
l.Debugln(err)
|
||||
r.setError(err)
|
||||
@@ -206,7 +221,11 @@ func (r *broadcastReader) Serve() {
|
||||
}
|
||||
|
||||
func (r *broadcastReader) Stop() {
|
||||
r.conn.Close()
|
||||
r.connMut.Lock()
|
||||
if r.conn != nil {
|
||||
r.conn.Close()
|
||||
}
|
||||
r.connMut.Unlock()
|
||||
}
|
||||
|
||||
func (r *broadcastReader) String() string {
|
||||
|
||||
+15
-9
@@ -112,6 +112,7 @@ type FolderConfiguration struct {
|
||||
ScanProgressIntervalS int `xml:"scanProgressIntervalS" json:"scanProgressIntervalS"` // Set to a negative value to disable. Value of 0 will get replaced with value of 2 (default value)
|
||||
PullerSleepS int `xml:"pullerSleepS" json:"pullerSleepS"`
|
||||
PullerPauseS int `xml:"pullerPauseS" json:"pullerPauseS"`
|
||||
MaxConflicts int `xml:"maxConflicts" json:"maxConflicts"`
|
||||
|
||||
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
|
||||
}
|
||||
@@ -249,7 +250,7 @@ type OptionsConfiguration struct {
|
||||
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"`
|
||||
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"`
|
||||
@@ -563,14 +564,6 @@ func ChangeRequiresRestart(from, to Configuration) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func convertV10V11(cfg *Configuration) {
|
||||
// Set minimum disk free of existing folders to 1%
|
||||
for i := range cfg.Folders {
|
||||
cfg.Folders[i].MinDiskFreePct = 1
|
||||
}
|
||||
cfg.Version = 11
|
||||
}
|
||||
|
||||
func convertV11V12(cfg *Configuration) {
|
||||
// Change listen address schema
|
||||
for i, addr := range cfg.Options.ListenAddress {
|
||||
@@ -614,9 +607,22 @@ func convertV11V12(cfg *Configuration) {
|
||||
cfg.Options.LocalAnnPort = 21027
|
||||
}
|
||||
|
||||
// Set MaxConflicts to unlimited
|
||||
for i := range cfg.Folders {
|
||||
cfg.Folders[i].MaxConflicts = -1
|
||||
}
|
||||
|
||||
cfg.Version = 12
|
||||
}
|
||||
|
||||
func convertV10V11(cfg *Configuration) {
|
||||
// Set minimum disk free of existing folders to 1%
|
||||
for i := range cfg.Folders {
|
||||
cfg.Folders[i].MinDiskFreePct = 1
|
||||
}
|
||||
cfg.Version = 11
|
||||
}
|
||||
|
||||
func convertV9V10(cfg *Configuration) {
|
||||
// Enable auto normalization on existing folders.
|
||||
for i := range cfg.Folders {
|
||||
|
||||
@@ -37,7 +37,7 @@ func TestDefaultValues(t *testing.T) {
|
||||
LocalAnnEnabled: true,
|
||||
LocalAnnPort: 21027,
|
||||
LocalAnnMCAddr: "[ff12::8384]:21027",
|
||||
RelayServers: []string{"dynamic+https://relays.syncthing.net"},
|
||||
RelayServers: []string{"dynamic+https://relays.syncthing.net/endpoint"},
|
||||
MaxSendKbps: 0,
|
||||
MaxRecvKbps: 0,
|
||||
ReconnectIntervalS: 60,
|
||||
@@ -100,6 +100,7 @@ func TestDeviceConfig(t *testing.T) {
|
||||
Hashers: 0,
|
||||
AutoNormalize: true,
|
||||
MinDiskFreePct: 1,
|
||||
MaxConflicts: -1,
|
||||
},
|
||||
}
|
||||
expectedDevices := []DeviceConfiguration{
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@
|
||||
<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>
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"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/relay/client"
|
||||
@@ -368,11 +367,6 @@ func (s *connectionSvc) connect() {
|
||||
l.Debugln("Sucessfully joined relay session", inv)
|
||||
}
|
||||
|
||||
err = osutil.SetTCPOptions(conn.(*net.TCPConn))
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
}
|
||||
|
||||
var tc *tls.Conn
|
||||
|
||||
if inv.ServerSocket {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/dialer"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
@@ -37,17 +38,12 @@ func tcpDialer(uri *url.URL, tlsCfg *tls.Config) (*tls.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn, err := net.DialTCP("tcp", nil, raddr)
|
||||
conn, err := dialer.Dial(raddr.Network(), raddr.String())
|
||||
if err != nil {
|
||||
l.Debugln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = osutil.SetTCPOptions(conn)
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
}
|
||||
|
||||
tc := tls.Client(conn, tlsCfg)
|
||||
err = tc.Handshake()
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build benchmark
|
||||
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
var files, oneFile, firstHalf, secondHalf []protocol.FileInfo
|
||||
var fs *db.FileSet
|
||||
|
||||
func init() {
|
||||
for i := 0; i < 1000; i++ {
|
||||
files = append(files, protocol.FileInfo{
|
||||
Name: fmt.Sprintf("file%d", i),
|
||||
Version: protocol.Vector{{ID: myID, Value: 1000}},
|
||||
Blocks: genBlocks(i),
|
||||
})
|
||||
}
|
||||
|
||||
middle := len(files) / 2
|
||||
firstHalf = files[:middle]
|
||||
secondHalf = files[middle:]
|
||||
oneFile = firstHalf[middle-1 : middle]
|
||||
|
||||
ldb, _ := tempDB()
|
||||
fs = db.NewFileSet("test", ldb)
|
||||
fs.Replace(remoteDevice0, files)
|
||||
fs.Replace(protocol.LocalDeviceID, firstHalf)
|
||||
}
|
||||
|
||||
func tempDB() (*leveldb.DB, string) {
|
||||
dir, err := ioutil.TempDir("", "syncthing")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
db, err := leveldb.OpenFile(filepath.Join(dir, "db"), &opt.Options{OpenFilesCacheCapacity: 100})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return db, dir
|
||||
}
|
||||
|
||||
func BenchmarkReplaceAll(b *testing.B) {
|
||||
ldb, dir := tempDB()
|
||||
defer func() {
|
||||
ldb.Close()
|
||||
os.RemoveAll(dir)
|
||||
}()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
m := db.NewFileSet("test", ldb)
|
||||
m.Replace(protocol.LocalDeviceID, files)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkUpdateOneChanged(b *testing.B) {
|
||||
changed := make([]protocol.FileInfo, 1)
|
||||
changed[0] = oneFile[0]
|
||||
changed[0].Version = changed[0].Version.Update(myID)
|
||||
changed[0].Blocks = genBlocks(len(changed[0].Blocks))
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if i%1 == 0 {
|
||||
fs.Update(protocol.LocalDeviceID, changed)
|
||||
} else {
|
||||
fs.Update(protocol.LocalDeviceID, oneFile)
|
||||
}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkUpdateOneUnchanged(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
fs.Update(protocol.LocalDeviceID, oneFile)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkNeedHalf(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
count := 0
|
||||
fs.WithNeed(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
if count != len(secondHalf) {
|
||||
b.Errorf("wrong length %d != %d", count, len(secondHalf))
|
||||
}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkHave(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
count := 0
|
||||
fs.WithHave(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
if count != len(firstHalf) {
|
||||
b.Errorf("wrong length %d != %d", count, len(firstHalf))
|
||||
}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkGlobal(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
count := 0
|
||||
fs.WithGlobal(func(fi db.FileIntf) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
if count != len(files) {
|
||||
b.Errorf("wrong length %d != %d", count, len(files))
|
||||
}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkNeedHalfTruncated(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
count := 0
|
||||
fs.WithNeedTruncated(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
if count != len(secondHalf) {
|
||||
b.Errorf("wrong length %d != %d", count, len(secondHalf))
|
||||
}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkHaveTruncated(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
count := 0
|
||||
fs.WithHaveTruncated(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
if count != len(firstHalf) {
|
||||
b.Errorf("wrong length %d != %d", count, len(firstHalf))
|
||||
}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func BenchmarkGlobalTruncated(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
count := 0
|
||||
fs.WithGlobalTruncated(func(fi db.FileIntf) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
if count != len(files) {
|
||||
b.Errorf("wrong length %d != %d", count, len(files))
|
||||
}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
+55
-12
@@ -26,6 +26,8 @@ import (
|
||||
|
||||
var blockFinder *BlockFinder
|
||||
|
||||
const maxBatchSize = 256 << 10
|
||||
|
||||
type BlockMap struct {
|
||||
db *leveldb.DB
|
||||
folder string
|
||||
@@ -42,14 +44,23 @@ func NewBlockMap(db *leveldb.DB, folder string) *BlockMap {
|
||||
func (m *BlockMap) Add(files []protocol.FileInfo) error {
|
||||
batch := new(leveldb.Batch)
|
||||
buf := make([]byte, 4)
|
||||
var key []byte
|
||||
for _, file := range files {
|
||||
if batch.Len() > maxBatchSize {
|
||||
if err := m.db.Write(batch, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
batch.Reset()
|
||||
}
|
||||
|
||||
if file.IsDirectory() || file.IsDeleted() || file.IsInvalid() {
|
||||
continue
|
||||
}
|
||||
|
||||
for i, block := range file.Blocks {
|
||||
binary.BigEndian.PutUint32(buf, uint32(i))
|
||||
batch.Put(m.blockKey(block.Hash, file.Name), buf)
|
||||
key = m.blockKeyInto(key, block.Hash, file.Name)
|
||||
batch.Put(key, buf)
|
||||
}
|
||||
}
|
||||
return m.db.Write(batch, nil)
|
||||
@@ -59,21 +70,31 @@ func (m *BlockMap) Add(files []protocol.FileInfo) error {
|
||||
func (m *BlockMap) Update(files []protocol.FileInfo) error {
|
||||
batch := new(leveldb.Batch)
|
||||
buf := make([]byte, 4)
|
||||
var key []byte
|
||||
for _, file := range files {
|
||||
if batch.Len() > maxBatchSize {
|
||||
if err := m.db.Write(batch, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
batch.Reset()
|
||||
}
|
||||
|
||||
if file.IsDirectory() {
|
||||
continue
|
||||
}
|
||||
|
||||
if file.IsDeleted() || file.IsInvalid() {
|
||||
for _, block := range file.Blocks {
|
||||
batch.Delete(m.blockKey(block.Hash, file.Name))
|
||||
key = m.blockKeyInto(key, block.Hash, file.Name)
|
||||
batch.Delete(key)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for i, block := range file.Blocks {
|
||||
binary.BigEndian.PutUint32(buf, uint32(i))
|
||||
batch.Put(m.blockKey(block.Hash, file.Name), buf)
|
||||
key = m.blockKeyInto(key, block.Hash, file.Name)
|
||||
batch.Put(key, buf)
|
||||
}
|
||||
}
|
||||
return m.db.Write(batch, nil)
|
||||
@@ -82,9 +103,18 @@ func (m *BlockMap) Update(files []protocol.FileInfo) error {
|
||||
// Discard block map state, removing the given files
|
||||
func (m *BlockMap) Discard(files []protocol.FileInfo) error {
|
||||
batch := new(leveldb.Batch)
|
||||
var key []byte
|
||||
for _, file := range files {
|
||||
if batch.Len() > maxBatchSize {
|
||||
if err := m.db.Write(batch, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
batch.Reset()
|
||||
}
|
||||
|
||||
for _, block := range file.Blocks {
|
||||
batch.Delete(m.blockKey(block.Hash, file.Name))
|
||||
key = m.blockKeyInto(key, block.Hash, file.Name)
|
||||
batch.Delete(key)
|
||||
}
|
||||
}
|
||||
return m.db.Write(batch, nil)
|
||||
@@ -93,9 +123,16 @@ func (m *BlockMap) Discard(files []protocol.FileInfo) error {
|
||||
// Drop block map, removing all entries related to this block map from the db.
|
||||
func (m *BlockMap) Drop() error {
|
||||
batch := new(leveldb.Batch)
|
||||
iter := m.db.NewIterator(util.BytesPrefix(m.blockKey(nil, "")[:1+64]), nil)
|
||||
iter := m.db.NewIterator(util.BytesPrefix(m.blockKeyInto(nil, nil, "")[:1+64]), nil)
|
||||
defer iter.Release()
|
||||
for iter.Next() {
|
||||
if batch.Len() > maxBatchSize {
|
||||
if err := m.db.Write(batch, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
batch.Reset()
|
||||
}
|
||||
|
||||
batch.Delete(iter.Key())
|
||||
}
|
||||
if iter.Error() != nil {
|
||||
@@ -104,8 +141,8 @@ func (m *BlockMap) Drop() error {
|
||||
return m.db.Write(batch, nil)
|
||||
}
|
||||
|
||||
func (m *BlockMap) blockKey(hash []byte, file string) []byte {
|
||||
return toBlockKey(hash, m.folder, file)
|
||||
func (m *BlockMap) blockKeyInto(o, hash []byte, file string) []byte {
|
||||
return blockKeyInto(o, hash, m.folder, file)
|
||||
}
|
||||
|
||||
type BlockFinder struct {
|
||||
@@ -134,8 +171,9 @@ func (f *BlockFinder) String() string {
|
||||
// reason. The iterator finally returns the result, whether or not a
|
||||
// satisfying block was eventually found.
|
||||
func (f *BlockFinder) Iterate(folders []string, hash []byte, iterFn func(string, string, int32) bool) bool {
|
||||
var key []byte
|
||||
for _, folder := range folders {
|
||||
key := toBlockKey(hash, folder, "")
|
||||
key = blockKeyInto(key, hash, folder, "")
|
||||
iter := f.db.NewIterator(util.BytesPrefix(key), nil)
|
||||
defer iter.Release()
|
||||
|
||||
@@ -157,8 +195,8 @@ func (f *BlockFinder) Fix(folder, file string, index int32, oldHash, newHash []b
|
||||
binary.BigEndian.PutUint32(buf, uint32(index))
|
||||
|
||||
batch := new(leveldb.Batch)
|
||||
batch.Delete(toBlockKey(oldHash, folder, file))
|
||||
batch.Put(toBlockKey(newHash, folder, file), buf)
|
||||
batch.Delete(blockKeyInto(nil, oldHash, folder, file))
|
||||
batch.Put(blockKeyInto(nil, newHash, folder, file), buf)
|
||||
return f.db.Write(batch, nil)
|
||||
}
|
||||
|
||||
@@ -167,8 +205,13 @@ func (f *BlockFinder) Fix(folder, file string, index int32, oldHash, newHash []b
|
||||
// folder (64 bytes)
|
||||
// block hash (32 bytes)
|
||||
// file name (variable size)
|
||||
func toBlockKey(hash []byte, folder, file string) []byte {
|
||||
o := make([]byte, 1+64+32+len(file))
|
||||
func blockKeyInto(o, hash []byte, folder, file string) []byte {
|
||||
reqLen := 1 + 64 + 32 + len(file)
|
||||
if cap(o) < reqLen {
|
||||
o = make([]byte, reqLen)
|
||||
} else {
|
||||
o = o[:reqLen]
|
||||
}
|
||||
o[0] = KeyTypeBlock
|
||||
copy(o[1:], []byte(folder))
|
||||
copy(o[1+64:], []byte(hash))
|
||||
|
||||
+93
-36
@@ -12,7 +12,6 @@ package db
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
@@ -169,9 +168,7 @@ func globalKeyFolder(key []byte) []byte {
|
||||
|
||||
type deletionHandler func(db dbReader, batch dbWriter, folder, device, name []byte, dbi iterator.Iterator) int64
|
||||
|
||||
func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo, deleteFn deletionHandler) int64 {
|
||||
runtime.GC()
|
||||
|
||||
func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker, deleteFn deletionHandler) int64 {
|
||||
sort.Sort(fileList(fs)) // sort list on name, same as in the database
|
||||
|
||||
start := deviceKey(folder, device, nil) // before all folder/device files
|
||||
@@ -196,6 +193,7 @@ func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.File
|
||||
fsi := 0
|
||||
var maxLocalVer int64
|
||||
|
||||
isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
|
||||
for {
|
||||
var newName, oldName []byte
|
||||
moreFs := fsi < len(fs)
|
||||
@@ -223,10 +221,13 @@ func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.File
|
||||
if lv := ldbInsert(batch, folder, device, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if isLocalDevice {
|
||||
localSize.addFile(fs[fsi])
|
||||
}
|
||||
if fs[fsi].IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, newName)
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, newName, globalSize)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, folder, device, fs[fsi])
|
||||
ldbUpdateGlobal(snap, batch, folder, device, fs[fsi], globalSize)
|
||||
}
|
||||
fsi++
|
||||
|
||||
@@ -242,10 +243,14 @@ func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.File
|
||||
if lv := ldbInsert(batch, folder, device, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if isLocalDevice {
|
||||
localSize.removeFile(ef)
|
||||
localSize.addFile(fs[fsi])
|
||||
}
|
||||
if fs[fsi].IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, newName)
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, newName, globalSize)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, folder, device, fs[fsi])
|
||||
ldbUpdateGlobal(snap, batch, folder, device, fs[fsi], globalSize)
|
||||
}
|
||||
} else {
|
||||
l.Debugln("generic replace; equal - ignore")
|
||||
@@ -285,21 +290,19 @@ func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.File
|
||||
return maxLocalVer
|
||||
}
|
||||
|
||||
func ldbReplace(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo) int64 {
|
||||
func ldbReplace(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) int64 {
|
||||
// TODO: Return the remaining maxLocalVer?
|
||||
return ldbGenericReplace(db, folder, device, fs, func(db dbReader, batch dbWriter, folder, device, name []byte, dbi iterator.Iterator) int64 {
|
||||
return ldbGenericReplace(db, folder, device, fs, localSize, globalSize, func(db dbReader, batch dbWriter, folder, device, name []byte, dbi iterator.Iterator) int64 {
|
||||
// Database has a file that we are missing. Remove it.
|
||||
l.Debugf("delete; folder=%q device=%v name=%q", folder, protocol.DeviceIDFromBytes(device), name)
|
||||
ldbRemoveFromGlobal(db, batch, folder, device, name)
|
||||
ldbRemoveFromGlobal(db, batch, folder, device, name, globalSize)
|
||||
l.Debugf("batch.Delete %p %x", batch, dbi.Key())
|
||||
batch.Delete(dbi.Key())
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
func ldbUpdate(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo) int64 {
|
||||
runtime.GC()
|
||||
|
||||
func ldbUpdate(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) int64 {
|
||||
batch := new(leveldb.Batch)
|
||||
l.Debugf("new batch %p", batch)
|
||||
snap, err := db.GetSnapshot()
|
||||
@@ -314,19 +317,24 @@ func ldbUpdate(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo) in
|
||||
|
||||
var maxLocalVer int64
|
||||
var fk []byte
|
||||
isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
|
||||
for _, f := range fs {
|
||||
name := []byte(f.Name)
|
||||
fk = deviceKeyInto(fk[:cap(fk)], folder, device, name)
|
||||
l.Debugf("snap.Get %p %x", snap, fk)
|
||||
bs, err := snap.Get(fk, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
if isLocalDevice {
|
||||
localSize.addFile(f)
|
||||
}
|
||||
|
||||
if lv := ldbInsert(batch, folder, device, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if f.IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, name)
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, name, globalSize)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, folder, device, f)
|
||||
ldbUpdateGlobal(snap, batch, folder, device, f, globalSize)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -339,13 +347,18 @@ func ldbUpdate(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo) in
|
||||
// Flags might change without the version being bumped when we set the
|
||||
// invalid flag on an existing file.
|
||||
if !ef.Version.Equal(f.Version) || ef.Flags != f.Flags {
|
||||
if isLocalDevice {
|
||||
localSize.removeFile(ef)
|
||||
localSize.addFile(f)
|
||||
}
|
||||
|
||||
if lv := ldbInsert(batch, folder, device, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if f.IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, name)
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, name, globalSize)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, folder, device, f)
|
||||
ldbUpdateGlobal(snap, batch, folder, device, f, globalSize)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +403,7 @@ func ldbInsert(batch dbWriter, folder, device []byte, file protocol.FileInfo) in
|
||||
// ldbUpdateGlobal adds this device+version to the version list for the given
|
||||
// file. If the device is already present in the list, the version is updated.
|
||||
// If the file does not have an entry in the global list, it is created.
|
||||
func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device []byte, file protocol.FileInfo) bool {
|
||||
func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device []byte, file protocol.FileInfo, globalSize *sizeTracker) bool {
|
||||
l.Debugf("update global; folder=%q device=%v file=%q version=%d", folder, protocol.DeviceIDFromBytes(device), file.Name, file.Version)
|
||||
name := []byte(file.Name)
|
||||
gk := globalKey(folder, name)
|
||||
@@ -400,7 +413,8 @@ func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device []byte, file pr
|
||||
}
|
||||
|
||||
var fl versionList
|
||||
|
||||
var oldFile protocol.FileInfo
|
||||
var hasOldFile bool
|
||||
// Remove the device from the current version list
|
||||
if svl != nil {
|
||||
err = fl.UnmarshalXDR(svl)
|
||||
@@ -414,6 +428,13 @@ func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device []byte, file pr
|
||||
// No need to do anything
|
||||
return false
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
// Keep the current newest file around so we can subtract it from
|
||||
// the globalSize if we replace it.
|
||||
oldFile, hasOldFile = ldbGet(db, folder, fl.versions[0].device, name)
|
||||
}
|
||||
|
||||
fl.versions = append(fl.versions[:i], fl.versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
@@ -425,6 +446,7 @@ func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device []byte, file pr
|
||||
version: file.Version,
|
||||
}
|
||||
|
||||
insertedAt := -1
|
||||
// Find a position in the list to insert this file. The file at the front
|
||||
// of the list is the newer, the "global".
|
||||
for i := range fl.versions {
|
||||
@@ -433,6 +455,7 @@ func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device []byte, file pr
|
||||
// The version at this point in the list is equal to or lesser
|
||||
// ("older") than us. We insert ourselves in front of it.
|
||||
fl.versions = insertVersion(fl.versions, i, nv)
|
||||
insertedAt = i
|
||||
goto done
|
||||
|
||||
case protocol.ConcurrentLesser, protocol.ConcurrentGreater:
|
||||
@@ -448,6 +471,7 @@ func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device []byte, file pr
|
||||
}
|
||||
if file.WinsConflict(of) {
|
||||
fl.versions = insertVersion(fl.versions, i, nv)
|
||||
insertedAt = i
|
||||
goto done
|
||||
}
|
||||
}
|
||||
@@ -455,8 +479,28 @@ func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device []byte, file pr
|
||||
|
||||
// We didn't find a position for an insert above, so append to the end.
|
||||
fl.versions = append(fl.versions, nv)
|
||||
insertedAt = len(fl.versions) - 1
|
||||
|
||||
done:
|
||||
if insertedAt == 0 {
|
||||
// We just inserted a new newest version. Fixup the global size
|
||||
// calculation.
|
||||
if !file.Version.Equal(oldFile.Version) {
|
||||
globalSize.addFile(file)
|
||||
if hasOldFile {
|
||||
// We have the old file that was removed at the head of the list.
|
||||
globalSize.removeFile(oldFile)
|
||||
} else if len(fl.versions) > 1 {
|
||||
// The previous newest version is now at index 1, grab it from there.
|
||||
oldFile, ok := ldbGet(db, folder, fl.versions[1].device, name)
|
||||
if !ok {
|
||||
panic("file referenced in version list does not exist")
|
||||
}
|
||||
globalSize.removeFile(oldFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
l.Debugf("batch.Put %p %x", batch, gk)
|
||||
l.Debugf("new global after update: %v", fl)
|
||||
batch.Put(gk, fl.MustMarshalXDR())
|
||||
@@ -474,7 +518,7 @@ func insertVersion(vl []fileVersion, i int, v fileVersion) []fileVersion {
|
||||
// ldbRemoveFromGlobal removes the device from the global version list for the
|
||||
// given file. If the version list is empty after this, the file entry is
|
||||
// removed entirely.
|
||||
func ldbRemoveFromGlobal(db dbReader, batch dbWriter, folder, device, file []byte) {
|
||||
func ldbRemoveFromGlobal(db dbReader, batch dbWriter, folder, device, file []byte, globalSize *sizeTracker) {
|
||||
l.Debugf("remove from global; folder=%q device=%v file=%q", folder, protocol.DeviceIDFromBytes(device), file)
|
||||
|
||||
gk := globalKey(folder, file)
|
||||
@@ -491,8 +535,17 @@ func ldbRemoveFromGlobal(db dbReader, batch dbWriter, folder, device, file []byt
|
||||
panic(err)
|
||||
}
|
||||
|
||||
removed := false
|
||||
for i := range fl.versions {
|
||||
if bytes.Compare(fl.versions[i].device, device) == 0 {
|
||||
if i == 0 && globalSize != nil {
|
||||
f, ok := ldbGet(db, folder, device, file)
|
||||
if !ok {
|
||||
panic("removing nonexistent file")
|
||||
}
|
||||
globalSize.removeFile(f)
|
||||
removed = true
|
||||
}
|
||||
fl.versions = append(fl.versions[:i], fl.versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
@@ -505,6 +558,13 @@ func ldbRemoveFromGlobal(db dbReader, batch dbWriter, folder, device, file []byt
|
||||
l.Debugf("batch.Put %p %x", batch, gk)
|
||||
l.Debugf("new global after remove: %v", fl)
|
||||
batch.Put(gk, fl.MustMarshalXDR())
|
||||
if removed {
|
||||
f, ok := ldbGet(db, folder, fl.versions[0].device, file)
|
||||
if !ok {
|
||||
panic("new global is nonexistent file")
|
||||
}
|
||||
globalSize.addFile(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,8 +596,6 @@ func ldbWithHave(db *leveldb.DB, folder, device []byte, truncate bool, fn Iterat
|
||||
}
|
||||
|
||||
func ldbWithAllFolderTruncated(db *leveldb.DB, folder []byte, fn func(device []byte, f FileInfoTruncated) bool) {
|
||||
runtime.GC()
|
||||
|
||||
start := deviceKey(folder, nil, nil) // before all folder/device files
|
||||
limit := deviceKey(folder, protocol.LocalDeviceID[:], []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
|
||||
snap, err := db.GetSnapshot()
|
||||
@@ -565,7 +623,7 @@ func ldbWithAllFolderTruncated(db *leveldb.DB, folder []byte, fn func(device []b
|
||||
case "", ".", "..", "/": // A few obviously invalid filenames
|
||||
l.Infof("Dropping invalid filename %q from database", f.Name)
|
||||
batch := new(leveldb.Batch)
|
||||
ldbRemoveFromGlobal(db, batch, folder, device, nil)
|
||||
ldbRemoveFromGlobal(db, batch, folder, device, nil, nil)
|
||||
batch.Delete(dbi.Key())
|
||||
db.Write(batch, nil)
|
||||
continue
|
||||
@@ -641,8 +699,6 @@ func ldbGetGlobal(db *leveldb.DB, folder, file []byte, truncate bool) (FileIntf,
|
||||
}
|
||||
|
||||
func ldbWithGlobal(db *leveldb.DB, folder, prefix []byte, truncate bool, fn Iterator) {
|
||||
runtime.GC()
|
||||
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -722,8 +778,6 @@ func ldbAvailability(db *leveldb.DB, folder, file []byte) []protocol.DeviceID {
|
||||
}
|
||||
|
||||
func ldbWithNeed(db *leveldb.DB, folder, device []byte, truncate bool, fn Iterator) {
|
||||
runtime.GC()
|
||||
|
||||
start := globalKey(folder, nil)
|
||||
limit := globalKey(folder, []byte{0xff, 0xff, 0xff, 0xff})
|
||||
snap, err := db.GetSnapshot()
|
||||
@@ -822,8 +876,6 @@ nextFile:
|
||||
}
|
||||
|
||||
func ldbListFolders(db *leveldb.DB) []string {
|
||||
runtime.GC()
|
||||
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -855,8 +907,6 @@ func ldbListFolders(db *leveldb.DB) []string {
|
||||
}
|
||||
|
||||
func ldbDropFolder(db *leveldb.DB, folder []byte) {
|
||||
runtime.GC()
|
||||
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -900,9 +950,7 @@ func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
|
||||
return tf, err
|
||||
}
|
||||
|
||||
func ldbCheckGlobals(db *leveldb.DB, folder []byte) {
|
||||
defer runtime.GC()
|
||||
|
||||
func ldbCheckGlobals(db *leveldb.DB, folder []byte, globalSize *sizeTracker) {
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -937,8 +985,9 @@ func ldbCheckGlobals(db *leveldb.DB, folder []byte) {
|
||||
|
||||
name := globalKeyName(gk)
|
||||
var newVL versionList
|
||||
for _, version := range vl.versions {
|
||||
for i, version := range vl.versions {
|
||||
fk = deviceKeyInto(fk[:cap(fk)], folder, version.device, name)
|
||||
|
||||
l.Debugf("snap.Get %p %x", snap, fk)
|
||||
_, err := snap.Get(fk, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
@@ -948,6 +997,14 @@ func ldbCheckGlobals(db *leveldb.DB, folder []byte) {
|
||||
panic(err)
|
||||
}
|
||||
newVL.versions = append(newVL.versions, version)
|
||||
|
||||
if i == 0 {
|
||||
fi, ok := ldbGet(snap, folder, version.device, name)
|
||||
if !ok {
|
||||
panic("nonexistent global master file")
|
||||
}
|
||||
globalSize.addFile(fi)
|
||||
}
|
||||
}
|
||||
|
||||
if len(newVL.versions) != len(vl.versions) {
|
||||
|
||||
+64
-3
@@ -13,6 +13,8 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
stdsync "sync"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
@@ -25,6 +27,8 @@ type FileSet struct {
|
||||
folder string
|
||||
db *leveldb.DB
|
||||
blockmap *BlockMap
|
||||
localSize sizeTracker
|
||||
globalSize sizeTracker
|
||||
}
|
||||
|
||||
// FileIntf is the set of methods implemented by both protocol.FileInfo and
|
||||
@@ -43,6 +47,52 @@ type FileIntf interface {
|
||||
// continue iteration, false to stop.
|
||||
type Iterator func(f FileIntf) bool
|
||||
|
||||
type sizeTracker struct {
|
||||
files int
|
||||
deleted int
|
||||
bytes int64
|
||||
mut stdsync.Mutex
|
||||
}
|
||||
|
||||
func (s *sizeTracker) addFile(f FileIntf) {
|
||||
if f.IsInvalid() {
|
||||
return
|
||||
}
|
||||
|
||||
s.mut.Lock()
|
||||
if f.IsDeleted() {
|
||||
s.deleted++
|
||||
} else {
|
||||
s.files++
|
||||
}
|
||||
s.bytes += f.Size()
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *sizeTracker) removeFile(f FileIntf) {
|
||||
if f.IsInvalid() {
|
||||
return
|
||||
}
|
||||
|
||||
s.mut.Lock()
|
||||
if f.IsDeleted() {
|
||||
s.deleted--
|
||||
} else {
|
||||
s.files--
|
||||
}
|
||||
s.bytes -= f.Size()
|
||||
if s.deleted < 0 || s.files < 0 {
|
||||
panic("bug: removed more than added")
|
||||
}
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *sizeTracker) Size() (files, deleted int, bytes int64) {
|
||||
s.mut.Lock()
|
||||
defer s.mut.Unlock()
|
||||
return s.files, s.deleted, s.bytes
|
||||
}
|
||||
|
||||
func NewFileSet(folder string, db *leveldb.DB) *FileSet {
|
||||
var s = FileSet{
|
||||
localVersion: make(map[protocol.DeviceID]int64),
|
||||
@@ -52,7 +102,7 @@ func NewFileSet(folder string, db *leveldb.DB) *FileSet {
|
||||
mutex: sync.NewMutex(),
|
||||
}
|
||||
|
||||
ldbCheckGlobals(db, []byte(folder))
|
||||
ldbCheckGlobals(db, []byte(folder), &s.globalSize)
|
||||
|
||||
var deviceID protocol.DeviceID
|
||||
ldbWithAllFolderTruncated(db, []byte(folder), func(device []byte, f FileInfoTruncated) bool {
|
||||
@@ -60,6 +110,9 @@ func NewFileSet(folder string, db *leveldb.DB) *FileSet {
|
||||
if f.LocalVersion > s.localVersion[deviceID] {
|
||||
s.localVersion[deviceID] = f.LocalVersion
|
||||
}
|
||||
if deviceID == protocol.LocalDeviceID {
|
||||
s.localSize.addFile(f)
|
||||
}
|
||||
return true
|
||||
})
|
||||
l.Debugf("loaded localVersion for %q: %#v", folder, s.localVersion)
|
||||
@@ -73,7 +126,7 @@ func (s *FileSet) Replace(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
normalizeFilenames(fs)
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
s.localVersion[device] = ldbReplace(s.db, []byte(s.folder), device[:], fs)
|
||||
s.localVersion[device] = ldbReplace(s.db, []byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
|
||||
if len(fs) == 0 {
|
||||
// Reset the local version if all files were removed.
|
||||
s.localVersion[device] = 0
|
||||
@@ -102,7 +155,7 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
s.blockmap.Discard(discards)
|
||||
s.blockmap.Update(updates)
|
||||
}
|
||||
if lv := ldbUpdate(s.db, []byte(s.folder), device[:], fs); lv > s.localVersion[device] {
|
||||
if lv := ldbUpdate(s.db, []byte(s.folder), device[:], fs, &s.localSize, &s.globalSize); lv > s.localVersion[device] {
|
||||
s.localVersion[device] = lv
|
||||
}
|
||||
}
|
||||
@@ -178,6 +231,14 @@ func (s *FileSet) LocalVersion(device protocol.DeviceID) int64 {
|
||||
return s.localVersion[device]
|
||||
}
|
||||
|
||||
func (s *FileSet) LocalSize() (files, deleted int, bytes int64) {
|
||||
return s.localSize.Size()
|
||||
}
|
||||
|
||||
func (s *FileSet) GlobalSize() (files, deleted int, bytes int64) {
|
||||
return s.globalSize.Size()
|
||||
}
|
||||
|
||||
// ListFolders returns the folder IDs seen in the database.
|
||||
func ListFolders(db *leveldb.DB) []string {
|
||||
return ldbListFolders(db)
|
||||
|
||||
+46
-174
@@ -173,6 +173,29 @@ func TestGlobalSet(t *testing.T) {
|
||||
t.Errorf("Global incorrect;\n A: %v !=\n E: %v", g, expectedGlobal)
|
||||
}
|
||||
|
||||
globalFiles, globalDeleted, globalBytes := 0, 0, int64(0)
|
||||
for _, f := range g {
|
||||
if f.IsInvalid() {
|
||||
continue
|
||||
}
|
||||
if f.IsDeleted() {
|
||||
globalDeleted++
|
||||
} else {
|
||||
globalFiles++
|
||||
}
|
||||
globalBytes += f.Size()
|
||||
}
|
||||
gsFiles, gsDeleted, gsBytes := m.GlobalSize()
|
||||
if gsFiles != globalFiles {
|
||||
t.Errorf("Incorrect GlobalSize files; %d != %d", gsFiles, globalFiles)
|
||||
}
|
||||
if gsDeleted != globalDeleted {
|
||||
t.Errorf("Incorrect GlobalSize deleted; %d != %d", gsDeleted, globalDeleted)
|
||||
}
|
||||
if gsBytes != globalBytes {
|
||||
t.Errorf("Incorrect GlobalSize bytes; %d != %d", gsBytes, globalBytes)
|
||||
}
|
||||
|
||||
h := fileList(haveList(m, protocol.LocalDeviceID))
|
||||
sort.Sort(h)
|
||||
|
||||
@@ -180,6 +203,29 @@ func TestGlobalSet(t *testing.T) {
|
||||
t.Errorf("Have incorrect;\n A: %v !=\n E: %v", h, localTot)
|
||||
}
|
||||
|
||||
haveFiles, haveDeleted, haveBytes := 0, 0, int64(0)
|
||||
for _, f := range h {
|
||||
if f.IsInvalid() {
|
||||
continue
|
||||
}
|
||||
if f.IsDeleted() {
|
||||
haveDeleted++
|
||||
} else {
|
||||
haveFiles++
|
||||
}
|
||||
haveBytes += f.Size()
|
||||
}
|
||||
lsFiles, lsDeleted, lsBytes := m.LocalSize()
|
||||
if lsFiles != haveFiles {
|
||||
t.Errorf("Incorrect LocalSize files; %d != %d", lsFiles, haveFiles)
|
||||
}
|
||||
if lsDeleted != haveDeleted {
|
||||
t.Errorf("Incorrect LocalSize deleted; %d != %d", lsDeleted, haveDeleted)
|
||||
}
|
||||
if lsBytes != haveBytes {
|
||||
t.Errorf("Incorrect LocalSize bytes; %d != %d", lsBytes, haveBytes)
|
||||
}
|
||||
|
||||
h = fileList(haveList(m, remoteDevice0))
|
||||
sort.Sort(h)
|
||||
|
||||
@@ -371,180 +417,6 @@ func TestInvalidAvailability(t *testing.T) {
|
||||
t.Error("Incorrect availability for 'none':", av)
|
||||
}
|
||||
}
|
||||
func Benchmark10kReplace(b *testing.B) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 10000; i++ {
|
||||
local = append(local, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
m := db.NewFileSet("test", ldb)
|
||||
m.Replace(protocol.LocalDeviceID, local)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark10kUpdateChg(b *testing.B) {
|
||||
var remote []protocol.FileInfo
|
||||
for i := 0; i < 10000; i++ {
|
||||
remote = append(remote, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
m := db.NewFileSet("test", ldb)
|
||||
m.Replace(remoteDevice0, remote)
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 10000; i++ {
|
||||
local = append(local, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
|
||||
m.Replace(protocol.LocalDeviceID, local)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StopTimer()
|
||||
for j := range local {
|
||||
local[j].Version = local[j].Version.Update(myID)
|
||||
}
|
||||
b.StartTimer()
|
||||
m.Update(protocol.LocalDeviceID, local)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark10kUpdateSme(b *testing.B) {
|
||||
var remote []protocol.FileInfo
|
||||
for i := 0; i < 10000; i++ {
|
||||
remote = append(remote, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
m := db.NewFileSet("test", ldb)
|
||||
m.Replace(remoteDevice0, remote)
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 10000; i++ {
|
||||
local = append(local, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
|
||||
m.Replace(protocol.LocalDeviceID, local)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
m.Update(protocol.LocalDeviceID, local)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark10kNeed2k(b *testing.B) {
|
||||
var remote []protocol.FileInfo
|
||||
for i := 0; i < 10000; i++ {
|
||||
remote = append(remote, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
m := db.NewFileSet("test", ldb)
|
||||
m.Replace(remoteDevice0, remote)
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 8000; i++ {
|
||||
local = append(local, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
for i := 8000; i < 10000; i++ {
|
||||
local = append(local, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{1, 980}}})
|
||||
}
|
||||
|
||||
m.Replace(protocol.LocalDeviceID, local)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
fs := needList(m, protocol.LocalDeviceID)
|
||||
if l := len(fs); l != 2000 {
|
||||
b.Errorf("wrong length %d != 2k", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark10kHaveFullList(b *testing.B) {
|
||||
var remote []protocol.FileInfo
|
||||
for i := 0; i < 10000; i++ {
|
||||
remote = append(remote, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
m := db.NewFileSet("test", ldb)
|
||||
m.Replace(remoteDevice0, remote)
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 2000; i++ {
|
||||
local = append(local, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
for i := 2000; i < 10000; i++ {
|
||||
local = append(local, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{1, 980}}})
|
||||
}
|
||||
|
||||
m.Replace(protocol.LocalDeviceID, local)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
fs := haveList(m, protocol.LocalDeviceID)
|
||||
if l := len(fs); l != 10000 {
|
||||
b.Errorf("wrong length %d != 10k", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark10kGlobal(b *testing.B) {
|
||||
var remote []protocol.FileInfo
|
||||
for i := 0; i < 10000; i++ {
|
||||
remote = append(remote, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
m := db.NewFileSet("test", ldb)
|
||||
m.Replace(remoteDevice0, remote)
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 2000; i++ {
|
||||
local = append(local, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{ID: myID, Value: 1000}}})
|
||||
}
|
||||
for i := 2000; i < 10000; i++ {
|
||||
local = append(local, protocol.FileInfo{Name: fmt.Sprintf("file%d", i), Version: protocol.Vector{{1, 980}}})
|
||||
}
|
||||
|
||||
m.Replace(protocol.LocalDeviceID, local)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
fs := globalList(m)
|
||||
if l := len(fs); l != 10000 {
|
||||
b.Errorf("wrong length %d != 10k", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalReset(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
|
||||
+31
-9
@@ -6,22 +6,44 @@
|
||||
|
||||
package db
|
||||
|
||||
import "github.com/syncthing/syncthing/lib/protocol"
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/calmh/xdr"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type FileInfoTruncated struct {
|
||||
protocol.FileInfo
|
||||
ActualSize int64
|
||||
}
|
||||
|
||||
func (f *FileInfoTruncated) UnmarshalXDR(bs []byte) error {
|
||||
err := f.FileInfo.UnmarshalXDR(bs)
|
||||
f.ActualSize = f.FileInfo.Size()
|
||||
f.FileInfo.Blocks = nil
|
||||
return err
|
||||
func (o *FileInfoTruncated) UnmarshalXDR(bs []byte) error {
|
||||
var br = bytes.NewReader(bs)
|
||||
var xr = xdr.NewReader(br)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (f FileInfoTruncated) Size() int64 {
|
||||
return f.ActualSize
|
||||
func (o *FileInfoTruncated) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.Name = xr.ReadStringMax(8192)
|
||||
o.Flags = xr.ReadUint32()
|
||||
o.Modified = int64(xr.ReadUint64())
|
||||
(&o.Version).DecodeXDRFrom(xr)
|
||||
o.LocalVersion = int64(xr.ReadUint64())
|
||||
_BlocksSize := int(xr.ReadUint32())
|
||||
if _BlocksSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Blocks", _BlocksSize, 1000000)
|
||||
}
|
||||
if _BlocksSize > 1000000 {
|
||||
return xdr.ElementSizeExceeded("Blocks", _BlocksSize, 1000000)
|
||||
}
|
||||
|
||||
buf := make([]byte, 64)
|
||||
for i := 0; i < _BlocksSize; i++ {
|
||||
size := xr.ReadUint32()
|
||||
o.CachedSize += int64(size)
|
||||
xr.ReadBytesMaxInto(64, buf)
|
||||
}
|
||||
return xr.Error()
|
||||
}
|
||||
|
||||
func BlocksToSize(num int) int64 {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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 dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
var (
|
||||
l = logger.DefaultLogger.NewFacility("dialer", "Dialing connections")
|
||||
dialer = proxy.FromEnvironment()
|
||||
usingProxy = dialer != proxy.Direct
|
||||
)
|
||||
|
||||
func init() {
|
||||
l.SetDebug("dialer", strings.Contains(os.Getenv("STTRACE"), "dialer") || os.Getenv("STTRACE") == "all")
|
||||
if usingProxy {
|
||||
http.DefaultTransport = &http.Transport{
|
||||
Dial: Dial,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
// Defer this, so that logging gets setup.
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
l.Infoln("Proxy settings detected")
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Dial tries dialing via proxy if a proxy is configured, and falls back to
|
||||
// a direct connection if no proxy is defined, or connecting via proxy fails.
|
||||
func Dial(network, addr string) (net.Conn, error) {
|
||||
if usingProxy {
|
||||
conn, err := dialer.Dial(network, addr)
|
||||
if err == nil {
|
||||
l.Debugf("Dialing %s address %s via proxy - success, %s -> %s", network, addr, conn.LocalAddr(), conn.RemoteAddr())
|
||||
if tcpconn, ok := conn.(*net.TCPConn); ok {
|
||||
osutil.SetTCPOptions(tcpconn)
|
||||
}
|
||||
return dialerConn{
|
||||
conn, newDialerAddr(network, addr),
|
||||
}, nil
|
||||
}
|
||||
l.Debugf("Dialing %s address %s via proxy - error %s", network, addr, err)
|
||||
}
|
||||
|
||||
conn, err := proxy.Direct.Dial(network, addr)
|
||||
if err == nil {
|
||||
l.Debugf("Dialing %s address %s directly - success, %s -> %s", network, addr, conn.LocalAddr(), conn.RemoteAddr())
|
||||
if tcpconn, ok := conn.(*net.TCPConn); ok {
|
||||
osutil.SetTCPOptions(tcpconn)
|
||||
}
|
||||
} else {
|
||||
l.Debugf("Dialing %s address %s directly - error %s", network, addr, err)
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
type dialerConn struct {
|
||||
net.Conn
|
||||
addr net.Addr
|
||||
}
|
||||
|
||||
func (c dialerConn) RemoteAddr() net.Addr {
|
||||
return c.addr
|
||||
}
|
||||
|
||||
func newDialerAddr(network, addr string) net.Addr {
|
||||
netaddr, err := net.ResolveIPAddr(network, addr)
|
||||
if err == nil {
|
||||
return netaddr
|
||||
}
|
||||
return fallbackAddr{network, addr}
|
||||
}
|
||||
|
||||
type fallbackAddr struct {
|
||||
network string
|
||||
addr string
|
||||
}
|
||||
|
||||
func (a fallbackAddr) Network() string {
|
||||
return a.network
|
||||
}
|
||||
|
||||
func (a fallbackAddr) String() string {
|
||||
return a.addr
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
stdsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/dialer"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
@@ -41,6 +42,7 @@ type httpClient interface {
|
||||
const (
|
||||
defaultReannounceInterval = 30 * time.Minute
|
||||
announceErrorRetryInterval = 5 * time.Minute
|
||||
requestTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type announcement struct {
|
||||
@@ -72,7 +74,10 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, rela
|
||||
// certificate to prove our identity, and may or may not verify the server
|
||||
// certificate depending on the insecure setting.
|
||||
var announceClient httpClient = &http.Client{
|
||||
Timeout: requestTimeout,
|
||||
Transport: &http.Transport{
|
||||
Dial: dialer.Dial,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: opts.insecure,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
@@ -86,7 +91,10 @@ func NewGlobal(server string, cert tls.Certificate, addrList AddressLister, rela
|
||||
// The http.Client used for queries. We don't need to present our
|
||||
// certificate here, so lets not include it. May be insecure if requested.
|
||||
var queryClient httpClient = &http.Client{
|
||||
Timeout: requestTimeout,
|
||||
Transport: &http.Transport{
|
||||
Dial: dialer.Dial,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: opts.insecure,
|
||||
},
|
||||
|
||||
@@ -79,11 +79,26 @@ func TestGlobalOverHTTP(t *testing.T) {
|
||||
mux.HandleFunc("/", s.handler)
|
||||
go http.Serve(list, mux)
|
||||
|
||||
// This should succeed
|
||||
direct, relays, 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")
|
||||
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")
|
||||
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)
|
||||
}
|
||||
@@ -231,6 +246,11 @@ type fakeDiscoveryServer struct {
|
||||
}
|
||||
|
||||
func (s *fakeDiscoveryServer) handler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/block" {
|
||||
// Never return for requests here
|
||||
select {}
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
s.announce, _ = ioutil.ReadAll(r.Body)
|
||||
w.WriteHeader(204)
|
||||
|
||||
+160
-167
@@ -39,12 +39,10 @@ import (
|
||||
|
||||
// How many files to send in each Index/IndexUpdate message.
|
||||
const (
|
||||
indexTargetSize = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed)
|
||||
indexPerFileSize = 250 // Each FileInfo is approximately this big, in bytes, excluding BlockInfos
|
||||
indexPerBlockSize = 40 // Each BlockInfo is approximately this big
|
||||
indexBatchSize = 1000 // Either way, don't include more files than this
|
||||
reqValidationTime = time.Hour // How long to cache validation entries for Request messages
|
||||
reqValidationCacheSize = 1000 // How many entries to aim for in the validation cache size
|
||||
indexTargetSize = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed)
|
||||
indexPerFileSize = 250 // Each FileInfo is approximately this big, in bytes, excluding BlockInfos
|
||||
indexPerBlockSize = 40 // Each BlockInfo is approximately this big
|
||||
indexBatchSize = 1000 // Either way, don't include more files than this
|
||||
)
|
||||
|
||||
type service interface {
|
||||
@@ -72,6 +70,7 @@ type Model struct {
|
||||
id protocol.DeviceID
|
||||
shortID uint64
|
||||
cacheIgnoredFiles bool
|
||||
protectedFiles []string
|
||||
|
||||
deviceName string
|
||||
clientName string
|
||||
@@ -91,9 +90,6 @@ type Model struct {
|
||||
deviceVer map[protocol.DeviceID]string
|
||||
devicePaused map[protocol.DeviceID]bool
|
||||
pmut sync.RWMutex // protects the above
|
||||
|
||||
reqValidationCache map[string]time.Time // folder / file name => time when confirmed to exist
|
||||
rvmut sync.RWMutex // protects reqValidationCache
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -103,39 +99,38 @@ var (
|
||||
// NewModel creates and starts a new model. The model starts in read-only mode,
|
||||
// where it sends index information to connected peers and responds to requests
|
||||
// for file data without altering the local folder in any way.
|
||||
func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName, clientVersion string, ldb *leveldb.DB) *Model {
|
||||
func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName, clientVersion string, ldb *leveldb.DB, protectedFiles []string) *Model {
|
||||
m := &Model{
|
||||
Supervisor: suture.New("model", suture.Spec{
|
||||
Log: func(line string) {
|
||||
l.Debugln(line)
|
||||
},
|
||||
}),
|
||||
cfg: cfg,
|
||||
db: ldb,
|
||||
finder: db.NewBlockFinder(ldb),
|
||||
progressEmitter: NewProgressEmitter(cfg),
|
||||
id: id,
|
||||
shortID: id.Short(),
|
||||
cacheIgnoredFiles: cfg.Options().CacheIgnoredFiles,
|
||||
deviceName: deviceName,
|
||||
clientName: clientName,
|
||||
clientVersion: clientVersion,
|
||||
folderCfgs: make(map[string]config.FolderConfiguration),
|
||||
folderFiles: make(map[string]*db.FileSet),
|
||||
folderDevices: make(map[string][]protocol.DeviceID),
|
||||
deviceFolders: make(map[protocol.DeviceID][]string),
|
||||
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
|
||||
folderIgnores: make(map[string]*ignore.Matcher),
|
||||
folderRunners: make(map[string]service),
|
||||
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
||||
conn: make(map[protocol.DeviceID]Connection),
|
||||
deviceVer: make(map[protocol.DeviceID]string),
|
||||
devicePaused: make(map[protocol.DeviceID]bool),
|
||||
reqValidationCache: make(map[string]time.Time),
|
||||
cfg: cfg,
|
||||
db: ldb,
|
||||
finder: db.NewBlockFinder(ldb),
|
||||
progressEmitter: NewProgressEmitter(cfg),
|
||||
id: id,
|
||||
shortID: id.Short(),
|
||||
cacheIgnoredFiles: cfg.Options().CacheIgnoredFiles,
|
||||
protectedFiles: protectedFiles,
|
||||
deviceName: deviceName,
|
||||
clientName: clientName,
|
||||
clientVersion: clientVersion,
|
||||
folderCfgs: make(map[string]config.FolderConfiguration),
|
||||
folderFiles: make(map[string]*db.FileSet),
|
||||
folderDevices: make(map[string][]protocol.DeviceID),
|
||||
deviceFolders: make(map[protocol.DeviceID][]string),
|
||||
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
|
||||
folderIgnores: make(map[string]*ignore.Matcher),
|
||||
folderRunners: make(map[string]service),
|
||||
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
||||
conn: make(map[protocol.DeviceID]Connection),
|
||||
deviceVer: make(map[protocol.DeviceID]string),
|
||||
devicePaused: make(map[protocol.DeviceID]bool),
|
||||
|
||||
fmut: sync.NewRWMutex(),
|
||||
pmut: sync.NewRWMutex(),
|
||||
rvmut: sync.NewRWMutex(),
|
||||
fmut: sync.NewRWMutex(),
|
||||
pmut: sync.NewRWMutex(),
|
||||
}
|
||||
if cfg.Options().ProgressUpdateIntervalS > -1 {
|
||||
go m.progressEmitter.Serve()
|
||||
@@ -187,11 +182,41 @@ func (m *Model) StartFolderRW(folder string) {
|
||||
p.versioner = versioner
|
||||
}
|
||||
|
||||
m.warnAboutOverwritingProtectedFiles(folder)
|
||||
|
||||
m.Add(p)
|
||||
|
||||
l.Okln("Ready to synchronize", folder, "(read-write)")
|
||||
}
|
||||
|
||||
func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
|
||||
if m.folderCfgs[folder].ReadOnly {
|
||||
return
|
||||
}
|
||||
|
||||
folderLocation := m.folderCfgs[folder].Path()
|
||||
ignores := m.folderIgnores[folder]
|
||||
|
||||
var filesAtRisk []string
|
||||
for _, protectedFilePath := range m.protectedFiles {
|
||||
// check if file is synced in this folder
|
||||
if !strings.HasPrefix(protectedFilePath, folderLocation) {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if file is ignored
|
||||
if ignores.Match(protectedFilePath) {
|
||||
continue
|
||||
}
|
||||
|
||||
filesAtRisk = append(filesAtRisk, protectedFilePath)
|
||||
}
|
||||
|
||||
if len(filesAtRisk) > 0 {
|
||||
l.Warnln("Some protected files may be overwritten and cause issues. See http://docs.syncthing.net/users/config.html#syncing-configuration-files for more information. The at risk files are:", strings.Join(filesAtRisk, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -304,8 +329,6 @@ func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
|
||||
// Completion returns the completion status, in percent, for the given device
|
||||
// and folder.
|
||||
func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
var tot int64
|
||||
|
||||
m.fmut.RLock()
|
||||
rf, ok := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
@@ -313,29 +336,22 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
return 0 // Folder doesn't exist, so we hardly have any of it
|
||||
}
|
||||
|
||||
rf.WithGlobalTruncated(func(f db.FileIntf) bool {
|
||||
if !f.IsDeleted() {
|
||||
tot += f.Size()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
_, _, tot := rf.GlobalSize()
|
||||
if tot == 0 {
|
||||
return 100 // Folder is empty, so we have all of it
|
||||
}
|
||||
|
||||
var need int64
|
||||
rf.WithNeedTruncated(device, func(f db.FileIntf) bool {
|
||||
if !f.IsDeleted() {
|
||||
need += f.Size()
|
||||
}
|
||||
need += f.Size()
|
||||
return true
|
||||
})
|
||||
|
||||
res := 100 * (1 - float64(need)/float64(tot))
|
||||
l.Debugf("%v Completion(%s, %q): %f (%d / %d)", m, device, folder, res, need, tot)
|
||||
needRatio := float64(need) / float64(tot)
|
||||
completionPct := 100 * (1 - needRatio)
|
||||
l.Debugf("%v Completion(%s, %q): %f (%d / %d = %f)", m, device, folder, completionPct, need, tot, needRatio)
|
||||
|
||||
return res
|
||||
return completionPct
|
||||
}
|
||||
|
||||
func sizeOf(fs []protocol.FileInfo) (files, deleted int, bytes int64) {
|
||||
@@ -364,13 +380,7 @@ func (m *Model) GlobalSize(folder string) (nfiles, deleted int, bytes int64) {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
rf.WithGlobalTruncated(func(f db.FileIntf) bool {
|
||||
fs, de, by := sizeOfFile(f)
|
||||
nfiles += fs
|
||||
deleted += de
|
||||
bytes += by
|
||||
return true
|
||||
})
|
||||
nfiles, deleted, bytes = rf.GlobalSize()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -381,16 +391,7 @@ func (m *Model) LocalSize(folder string) (nfiles, deleted int, bytes int64) {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
rf.WithHaveTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
|
||||
if f.IsInvalid() {
|
||||
return true
|
||||
}
|
||||
fs, de, by := sizeOfFile(f)
|
||||
nfiles += fs
|
||||
deleted += de
|
||||
bytes += by
|
||||
return true
|
||||
})
|
||||
nfiles, deleted, bytes = rf.LocalSize()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -584,6 +585,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
|
||||
event := map[string]string{
|
||||
"id": deviceID.String(),
|
||||
"deviceName": cm.DeviceName,
|
||||
"clientName": cm.ClientName,
|
||||
"clientVersion": cm.ClientVersion,
|
||||
}
|
||||
@@ -600,18 +602,15 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
|
||||
events.Default.Log(events.DeviceConnected, event)
|
||||
|
||||
l.Infof(`Device %s client is "%s %s"`, deviceID, cm.ClientName, cm.ClientVersion)
|
||||
l.Infof(`Device %s client is "%s %s named %s"`, deviceID, cm.ClientName, cm.ClientVersion, cm.DeviceName)
|
||||
|
||||
var changed bool
|
||||
|
||||
if name := cm.GetOption("name"); name != "" {
|
||||
l.Infof("Device %s name is %q", deviceID, name)
|
||||
device, ok := m.cfg.Devices()[deviceID]
|
||||
if ok && device.Name == "" {
|
||||
device.Name = name
|
||||
m.cfg.SetDevice(device)
|
||||
changed = true
|
||||
}
|
||||
device, ok := m.cfg.Devices()[deviceID]
|
||||
if ok && device.Name == "" {
|
||||
device.Name = cm.DeviceName
|
||||
m.cfg.SetDevice(device)
|
||||
changed = true
|
||||
}
|
||||
|
||||
if m.cfg.Devices()[deviceID].Introducer {
|
||||
@@ -634,11 +633,20 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
if _, ok := m.cfg.Devices()[id]; !ok {
|
||||
// The device is currently unknown. Add it to the config.
|
||||
|
||||
addresses := []string{"dynamic"}
|
||||
for _, addr := range device.Addresses {
|
||||
if addr != "dynamic" {
|
||||
addresses = append(addresses, addr)
|
||||
}
|
||||
}
|
||||
|
||||
l.Infof("Adding device %v to config (vouched for by introducer %v)", id, deviceID)
|
||||
newDeviceCfg := config.DeviceConfiguration{
|
||||
DeviceID: id,
|
||||
Name: device.Name,
|
||||
Compression: m.cfg.Devices()[deviceID].Compression,
|
||||
Addresses: []string{"dynamic"},
|
||||
Addresses: addresses,
|
||||
CertName: device.CertName,
|
||||
}
|
||||
|
||||
// The introducers' introducers are also our introducers.
|
||||
@@ -712,99 +720,71 @@ func (m *Model) Close(device protocol.DeviceID, err error) {
|
||||
// Implements the protocol.Model interface.
|
||||
func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset int64, hash []byte, flags uint32, options []protocol.Option, buf []byte) error {
|
||||
if offset < 0 {
|
||||
return protocol.ErrNoSuchFile
|
||||
return protocol.ErrInvalid
|
||||
}
|
||||
|
||||
if !m.folderSharedWith(folder, deviceID) {
|
||||
l.Warnf("Request from %s for file %s in unshared folder %q", deviceID, name, folder)
|
||||
return protocol.ErrNoSuchFile
|
||||
return protocol.ErrInvalid
|
||||
}
|
||||
|
||||
if flags != 0 {
|
||||
// We don't currently support or expect any flags.
|
||||
return fmt.Errorf("protocol error: unknown flags 0x%x in Request message", flags)
|
||||
}
|
||||
|
||||
// Verify that the requested file exists in the local model. We only need
|
||||
// to validate this file if we haven't done so recently, so we keep a
|
||||
// cache of successfull results. "Recently" can be quite a long time, as
|
||||
// we remove validation cache entries when we detect local changes. If
|
||||
// we're out of sync here and the file actually doesn't exist any more, or
|
||||
// has shrunk or something, then we'll anyway get a read error that we
|
||||
// pass on to the other side.
|
||||
|
||||
m.rvmut.RLock()
|
||||
validated := m.reqValidationCache[folder+"/"+name]
|
||||
m.rvmut.RUnlock()
|
||||
|
||||
if time.Since(validated) > reqValidationTime {
|
||||
m.fmut.RLock()
|
||||
folderFiles, ok := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
if !ok {
|
||||
l.Warnf("Request from %s for file %s in nonexistent folder %q", deviceID, name, folder)
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
// This call is really expensive for large files, as we load the full
|
||||
// block list which may be megabytes and megabytes of data to allocate
|
||||
// space for, read, and deserialize.
|
||||
lf, ok := folderFiles.Get(protocol.LocalDeviceID, name)
|
||||
if !ok {
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
if lf.IsInvalid() || lf.IsDeleted() {
|
||||
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d; invalid: %v", m, deviceID, folder, name, offset, len(buf), lf)
|
||||
return protocol.ErrInvalid
|
||||
}
|
||||
|
||||
if offset > lf.Size() {
|
||||
l.Debugf("%v REQ(in; nonexistent): %s: %q o=%d s=%d", m, deviceID, name, offset, len(buf))
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
m.rvmut.Lock()
|
||||
m.reqValidationCache[folder+"/"+name] = time.Now()
|
||||
if len(m.reqValidationCache) > reqValidationCacheSize {
|
||||
// Don't let the cache grow infinitely
|
||||
for name, validated := range m.reqValidationCache {
|
||||
if time.Since(validated) > time.Minute {
|
||||
delete(m.reqValidationCache, name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.reqValidationCache) > reqValidationCacheSize*9/10 {
|
||||
// The first clean didn't help much, we're still over 90%
|
||||
// full; we may have synced a lot of files lately. Prune the
|
||||
// cache more aggressively by removing every other item so we
|
||||
// don't get stuck doing useless cache cleaning.
|
||||
i := 0
|
||||
for name := range m.reqValidationCache {
|
||||
if i%2 == 0 {
|
||||
delete(m.reqValidationCache, name)
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
m.rvmut.Unlock()
|
||||
return protocol.ErrInvalid
|
||||
}
|
||||
|
||||
if deviceID != protocol.LocalDeviceID {
|
||||
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
}
|
||||
m.fmut.RLock()
|
||||
fn := filepath.Join(m.folderCfgs[folder].Path(), name)
|
||||
folderPath := m.folderCfgs[folder].Path()
|
||||
folderIgnores := m.folderIgnores[folder]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
// filepath.Join() returns a filepath.Clean()ed path, which (quoting the
|
||||
// docs for clarity here):
|
||||
//
|
||||
// Clean returns the shortest path name equivalent to path by purely lexical
|
||||
// processing. It applies the following rules iteratively until no further
|
||||
// processing can be done:
|
||||
//
|
||||
// 1. Replace multiple Separator elements with a single one.
|
||||
// 2. Eliminate each . path name element (the current directory).
|
||||
// 3. Eliminate each inner .. path name element (the parent directory)
|
||||
// along with the non-.. element that precedes it.
|
||||
// 4. Eliminate .. elements that begin a rooted path:
|
||||
// that is, replace "/.." by "/" at the beginning of a path,
|
||||
// assuming Separator is '/'.
|
||||
fn := filepath.Join(folderPath, name)
|
||||
|
||||
if !strings.HasPrefix(fn, folderPath) {
|
||||
// Request tries to escape!
|
||||
l.Debugf("%v Invalid REQ(in) tries to escape: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
return protocol.ErrInvalid
|
||||
}
|
||||
|
||||
if folderIgnores != nil {
|
||||
// "rn" becomes the relative name of the file within the folder. This is
|
||||
// different than the original "name" parameter in that it's been
|
||||
// cleaned from any possible funny business.
|
||||
if rn, err := filepath.Rel(folderPath, fn); err != nil {
|
||||
return err
|
||||
} else if folderIgnores.Match(rn) {
|
||||
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
}
|
||||
|
||||
var reader io.ReaderAt
|
||||
var err error
|
||||
if info, err := os.Lstat(fn); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
||||
target, _, err := symlinks.Read(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
l.Debugln("symlinks.Read:", err)
|
||||
if os.IsNotExist(err) {
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
return protocol.ErrGeneric
|
||||
}
|
||||
reader = strings.NewReader(target)
|
||||
} else {
|
||||
@@ -812,7 +792,11 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
// at any moment.
|
||||
reader, err = os.Open(fn)
|
||||
if err != nil {
|
||||
return err
|
||||
l.Debugln("os.Open:", err)
|
||||
if os.IsNotExist(err) {
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
return protocol.ErrGeneric
|
||||
}
|
||||
|
||||
defer reader.(*os.File).Close()
|
||||
@@ -820,7 +804,8 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
|
||||
_, err = reader.ReadAt(buf, offset)
|
||||
if err != nil {
|
||||
return err
|
||||
l.Debugln("reader.ReadAt:", err)
|
||||
return protocol.ErrGeneric
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1114,12 +1099,6 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
|
||||
m.fmut.RUnlock()
|
||||
files.Update(protocol.LocalDeviceID, fs)
|
||||
|
||||
m.rvmut.Lock()
|
||||
for _, f := range fs {
|
||||
delete(m.reqValidationCache, folder+"/"+f.Name)
|
||||
}
|
||||
m.rvmut.Unlock()
|
||||
|
||||
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
|
||||
"folder": folder,
|
||||
"items": len(fs),
|
||||
@@ -1465,31 +1444,45 @@ func (m *Model) numHashers(folder string) int {
|
||||
// clusterConfig returns a ClusterConfigMessage that is correct for the given peer device
|
||||
func (m *Model) clusterConfig(device protocol.DeviceID) protocol.ClusterConfigMessage {
|
||||
cm := protocol.ClusterConfigMessage{
|
||||
DeviceName: m.deviceName,
|
||||
ClientName: m.clientName,
|
||||
ClientVersion: m.clientVersion,
|
||||
Options: []protocol.Option{
|
||||
{
|
||||
Key: "name",
|
||||
Value: m.deviceName,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
m.fmut.RLock()
|
||||
for _, folder := range m.deviceFolders[device] {
|
||||
folderCfg := m.cfg.Folders()[folder]
|
||||
cr := protocol.Folder{
|
||||
ID: folder,
|
||||
}
|
||||
var flags uint32
|
||||
if folderCfg.ReadOnly {
|
||||
flags |= protocol.FlagFolderReadOnly
|
||||
}
|
||||
if folderCfg.IgnorePerms {
|
||||
flags |= protocol.FlagFolderIgnorePerms
|
||||
}
|
||||
if folderCfg.IgnoreDelete {
|
||||
flags |= protocol.FlagFolderIgnoreDelete
|
||||
}
|
||||
cr.Flags = flags
|
||||
for _, device := range m.folderDevices[folder] {
|
||||
// DeviceID is a value type, but with an underlying array. Copy it
|
||||
// so we don't grab aliases to the same array later on in device[:]
|
||||
device := device
|
||||
// TODO: Set read only bit when relevant
|
||||
// TODO: Set read only bit when relevant, and when we have per device
|
||||
// access controls.
|
||||
deviceCfg := m.cfg.Devices()[device]
|
||||
cn := protocol.Device{
|
||||
ID: device[:],
|
||||
Flags: protocol.FlagShareTrusted,
|
||||
ID: device[:],
|
||||
Name: deviceCfg.Name,
|
||||
Addresses: deviceCfg.Addresses,
|
||||
Compression: uint32(deviceCfg.Compression),
|
||||
CertName: deviceCfg.CertName,
|
||||
Flags: protocol.FlagShareTrusted,
|
||||
}
|
||||
if deviceCfg := m.cfg.Devices()[device]; deviceCfg.Introducer {
|
||||
|
||||
if deviceCfg.Introducer {
|
||||
cn.Flags |= protocol.FlagIntroducer
|
||||
}
|
||||
cr.Devices = append(cr.Devices, cn)
|
||||
|
||||
+16
-21
@@ -92,7 +92,7 @@ func init() {
|
||||
func TestRequest(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
|
||||
// device1 shares default, but device2 doesn't
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
@@ -168,7 +168,7 @@ func BenchmarkIndex_100(b *testing.B) {
|
||||
|
||||
func benchmarkIndex(b *testing.B, nfiles int) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolderRO("default")
|
||||
m.ServeBackground()
|
||||
@@ -197,7 +197,7 @@ func BenchmarkIndexUpdate_10000_1(b *testing.B) {
|
||||
|
||||
func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolderRO("default")
|
||||
m.ServeBackground()
|
||||
@@ -262,7 +262,7 @@ func (FakeConnection) Statistics() protocol.Statistics {
|
||||
|
||||
func BenchmarkRequest(b *testing.B) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
m.ScanFolder("default")
|
||||
@@ -318,7 +318,7 @@ func TestDeviceRename(t *testing.T) {
|
||||
cfg := config.Wrap("tmpconfig.xml", rawCfg)
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
|
||||
fc := FakeConnection{
|
||||
id: device1,
|
||||
@@ -341,18 +341,13 @@ func TestDeviceRename(t *testing.T) {
|
||||
t.Errorf("Device already has a name")
|
||||
}
|
||||
|
||||
ccm.Options = []protocol.Option{
|
||||
{
|
||||
Key: "name",
|
||||
Value: "tester",
|
||||
},
|
||||
}
|
||||
ccm.DeviceName = "tester"
|
||||
m.ClusterConfig(device1, ccm)
|
||||
if cfg.Devices()[device1].Name != "tester" {
|
||||
t.Errorf("Device did not get a name")
|
||||
}
|
||||
|
||||
ccm.Options[0].Value = "tester2"
|
||||
ccm.DeviceName = "tester2"
|
||||
m.ClusterConfig(device1, ccm)
|
||||
if cfg.Devices()[device1].Name != "tester" {
|
||||
t.Errorf("Device name got overwritten")
|
||||
@@ -398,7 +393,7 @@ func TestClusterConfig(t *testing.T) {
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
|
||||
m := NewModel(config.Wrap("/tmp/test", cfg), protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(config.Wrap("/tmp/test", cfg), protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(cfg.Folders[0])
|
||||
m.AddFolder(cfg.Folders[1])
|
||||
m.ServeBackground()
|
||||
@@ -469,7 +464,7 @@ func TestIgnores(t *testing.T) {
|
||||
ioutil.WriteFile("testdata/.stignore", []byte(".*\nquux\n"), 0644)
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolderRO("default")
|
||||
m.ServeBackground()
|
||||
@@ -544,7 +539,7 @@ func TestIgnores(t *testing.T) {
|
||||
|
||||
func TestRefuseUnknownBits(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
|
||||
@@ -603,7 +598,7 @@ func TestROScanRecovery(t *testing.T) {
|
||||
|
||||
os.RemoveAll(fcfg.RawPath)
|
||||
|
||||
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
|
||||
m.AddFolder(fcfg)
|
||||
m.StartFolderRO("default")
|
||||
m.ServeBackground()
|
||||
@@ -687,7 +682,7 @@ func TestRWScanRecovery(t *testing.T) {
|
||||
|
||||
os.RemoveAll(fcfg.RawPath)
|
||||
|
||||
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
|
||||
m.AddFolder(fcfg)
|
||||
m.StartFolderRW("default")
|
||||
m.ServeBackground()
|
||||
@@ -750,7 +745,7 @@ func TestRWScanRecovery(t *testing.T) {
|
||||
|
||||
func TestGlobalDirectoryTree(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
|
||||
@@ -1000,7 +995,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
|
||||
func TestGlobalDirectorySelfFixing(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
|
||||
@@ -1174,7 +1169,7 @@ func BenchmarkTree_100_10(b *testing.B) {
|
||||
|
||||
func benchmarkTree(b *testing.B, n1, n2 int) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
|
||||
@@ -1192,7 +1187,7 @@ func benchmarkTree(b *testing.B, n1, n2 int) {
|
||||
|
||||
func TestIgnoreDelete(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
|
||||
// This folder should ignore external deletes
|
||||
cfg := defaultFolderConfig
|
||||
|
||||
@@ -136,3 +136,9 @@ func (t *ProgressEmitter) BytesCompleted(folder string) (bytes int64) {
|
||||
func (t *ProgressEmitter) String() string {
|
||||
return fmt.Sprintf("ProgressEmitter@%p", t)
|
||||
}
|
||||
|
||||
func (t *ProgressEmitter) lenRegistry() int {
|
||||
t.mut.Lock()
|
||||
defer t.mut.Unlock()
|
||||
return len(t.registry)
|
||||
}
|
||||
|
||||
@@ -109,6 +109,18 @@ func (q *jobQueue) Shuffle() {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *jobQueue) lenQueued() int {
|
||||
q.mut.Lock()
|
||||
defer q.mut.Unlock()
|
||||
return len(q.queued)
|
||||
}
|
||||
|
||||
func (q *jobQueue) lenProgress() int {
|
||||
q.mut.Lock()
|
||||
defer q.mut.Unlock()
|
||||
return len(q.progress)
|
||||
}
|
||||
|
||||
func (q *jobQueue) SortSmallestFirst() {
|
||||
q.mut.Lock()
|
||||
defer q.mut.Unlock()
|
||||
|
||||
+46
-23
@@ -79,17 +79,18 @@ type rwFolder struct {
|
||||
progressEmitter *ProgressEmitter
|
||||
virtualMtimeRepo *db.VirtualMtimeRepo
|
||||
|
||||
folder string
|
||||
dir string
|
||||
scanIntv time.Duration
|
||||
versioner versioner.Versioner
|
||||
ignorePerms bool
|
||||
copiers int
|
||||
pullers int
|
||||
shortID uint64
|
||||
order config.PullOrder
|
||||
sleep time.Duration
|
||||
pause time.Duration
|
||||
folder string
|
||||
dir string
|
||||
scanIntv time.Duration
|
||||
versioner versioner.Versioner
|
||||
ignorePerms bool
|
||||
copiers int
|
||||
pullers int
|
||||
shortID uint64
|
||||
order config.PullOrder
|
||||
maxConflicts int
|
||||
sleep time.Duration
|
||||
pause time.Duration
|
||||
|
||||
stop chan struct{}
|
||||
queue *jobQueue
|
||||
@@ -115,14 +116,15 @@ func newRWFolder(m *Model, shortID uint64, cfg config.FolderConfiguration) *rwFo
|
||||
progressEmitter: m.progressEmitter,
|
||||
virtualMtimeRepo: db.NewVirtualMtimeRepo(m.db, cfg.ID),
|
||||
|
||||
folder: cfg.ID,
|
||||
dir: cfg.Path(),
|
||||
scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second,
|
||||
ignorePerms: cfg.IgnorePerms,
|
||||
copiers: cfg.Copiers,
|
||||
pullers: cfg.Pullers,
|
||||
shortID: shortID,
|
||||
order: cfg.Order,
|
||||
folder: cfg.ID,
|
||||
dir: cfg.Path(),
|
||||
scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second,
|
||||
ignorePerms: cfg.IgnorePerms,
|
||||
copiers: cfg.Copiers,
|
||||
pullers: cfg.Pullers,
|
||||
shortID: shortID,
|
||||
order: cfg.Order,
|
||||
maxConflicts: cfg.MaxConflicts,
|
||||
|
||||
stop: make(chan struct{}),
|
||||
queue: newJobQueue(),
|
||||
@@ -757,7 +759,7 @@ func (p *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
// of deleting. Also merge with the version vector we had, to indicate
|
||||
// we have resolved the conflict.
|
||||
file.Version = file.Version.Merge(cur.Version)
|
||||
err = osutil.InWritableDir(moveForConflict, realName)
|
||||
err = osutil.InWritableDir(p.moveForConflict, realName)
|
||||
} else if p.versioner != nil {
|
||||
err = osutil.InWritableDir(p.versioner.Archive, realName)
|
||||
} else {
|
||||
@@ -1254,7 +1256,7 @@ func (p *rwFolder) performFinish(state *sharedPullerState) error {
|
||||
// we have resolved the conflict.
|
||||
|
||||
state.file.Version = state.file.Version.Merge(state.version)
|
||||
if err = osutil.InWritableDir(moveForConflict, state.realName); err != nil {
|
||||
if err = osutil.InWritableDir(p.moveForConflict, state.realName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1447,7 +1449,14 @@ func removeDevice(devices []protocol.DeviceID, device protocol.DeviceID) []proto
|
||||
return devices
|
||||
}
|
||||
|
||||
func moveForConflict(name string) error {
|
||||
func (p *rwFolder) moveForConflict(name string) error {
|
||||
if p.maxConflicts == 0 {
|
||||
if err := osutil.Remove(name); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ext := filepath.Ext(name)
|
||||
withoutExt := name[:len(name)-len(ext)]
|
||||
newName := withoutExt + time.Now().Format(".sync-conflict-20060102-150405") + ext
|
||||
@@ -1457,7 +1466,21 @@ func moveForConflict(name string) error {
|
||||
// the user has already moved it away, or the conflict was between a
|
||||
// remote modification and a local delete. In either way it does not
|
||||
// matter, go ahead as if the move succeeded.
|
||||
return nil
|
||||
err = nil
|
||||
}
|
||||
if p.maxConflicts > -1 {
|
||||
matches, gerr := osutil.Glob(withoutExt + ".sync-conflict-????????-??????" + ext)
|
||||
if gerr == nil && len(matches) > p.maxConflicts {
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
|
||||
for _, match := range matches[p.maxConflicts:] {
|
||||
gerr = osutil.Remove(match)
|
||||
if gerr != nil {
|
||||
l.Debugln(p, "removing extra conflict", gerr)
|
||||
}
|
||||
}
|
||||
} else if gerr != nil {
|
||||
l.Debugln(p, "globbing for conflicts", gerr)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
+19
-31
@@ -70,7 +70,7 @@ func TestHandleFile(t *testing.T) {
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
m.updateLocals("default", []protocol.FileInfo{existingFile})
|
||||
@@ -126,7 +126,7 @@ func TestHandleFileWithTemp(t *testing.T) {
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
m.updateLocals("default", []protocol.FileInfo{existingFile})
|
||||
@@ -188,7 +188,7 @@ func TestCopierFinder(t *testing.T) {
|
||||
requiredFile.Name = "file2"
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
m.updateLocals("default", []protocol.FileInfo{existingFile})
|
||||
@@ -265,7 +265,7 @@ func TestCopierCleanup(t *testing.T) {
|
||||
}
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
// Create a file
|
||||
@@ -314,7 +314,7 @@ func TestCopierCleanup(t *testing.T) {
|
||||
// if it fails to find the block.
|
||||
func TestLastResortPulling(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
// Add a file to index (with the incorrect block representation, as content
|
||||
@@ -389,7 +389,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
emitter := NewProgressEmitter(defaultConfig)
|
||||
@@ -409,7 +409,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
p.queue.Push("filex", 0, 0)
|
||||
p.queue.Pop()
|
||||
|
||||
if len(p.queue.progress) != 1 {
|
||||
if p.queue.lenProgress() != 1 {
|
||||
t.Fatal("Expected file in progress")
|
||||
}
|
||||
|
||||
@@ -437,7 +437,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 len(p.progressEmitter.registry) != 1 || len(p.queue.progress) != 1 || len(p.queue.queued) != 0 {
|
||||
if p.progressEmitter.lenRegistry() != 1 || p.queue.lenProgress() != 1 || p.queue.lenQueued() != 0 {
|
||||
t.Fatal("Could not find file")
|
||||
}
|
||||
|
||||
@@ -452,24 +452,16 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
t.Fatal("File not closed?")
|
||||
}
|
||||
|
||||
p.queue.mut.Lock()
|
||||
lenQProgr := len(p.queue.progress)
|
||||
lenQQueued := len(p.queue.queued)
|
||||
p.queue.mut.Unlock()
|
||||
if len(p.progressEmitter.registry) != 0 || lenQProgr != 0 || lenQQueued != 0 {
|
||||
t.Fatal("Still registered", len(p.progressEmitter.registry), len(p.queue.progress), len(p.queue.queued))
|
||||
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())
|
||||
}
|
||||
|
||||
// Doing it again should have no effect
|
||||
finisherChan <- state
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
p.queue.mut.Lock()
|
||||
lenQProgr = len(p.queue.progress)
|
||||
lenQQueued = len(p.queue.queued)
|
||||
p.queue.mut.Unlock()
|
||||
if len(p.progressEmitter.registry) != 0 || lenQProgr != 0 || lenQQueued != 0 {
|
||||
t.Fatal("Still registered")
|
||||
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())
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Didn't get anything to the finisher")
|
||||
@@ -489,7 +481,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
defer os.Remove("testdata/" + defTempNamer.TempName("filex"))
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db)
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
emitter := NewProgressEmitter(defaultConfig)
|
||||
@@ -509,7 +501,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
p.queue.Push("filex", 0, 0)
|
||||
p.queue.Pop()
|
||||
|
||||
if len(p.queue.progress) != 1 {
|
||||
if p.queue.lenProgress() != 1 {
|
||||
t.Fatal("Expected file in progress")
|
||||
}
|
||||
|
||||
@@ -530,7 +522,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 len(p.progressEmitter.registry) != 1 || len(p.queue.progress) != 1 || len(p.queue.queued) != 0 {
|
||||
if p.progressEmitter.lenRegistry() != 1 || p.queue.lenProgress() != 1 || p.queue.lenQueued() != 0 {
|
||||
t.Fatal("Could not find file")
|
||||
}
|
||||
|
||||
@@ -545,20 +537,16 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
t.Fatal("File not closed?")
|
||||
}
|
||||
|
||||
p.queue.mut.Lock()
|
||||
lenQProgr := len(p.queue.progress)
|
||||
lenQQueued := len(p.queue.queued)
|
||||
p.queue.mut.Unlock()
|
||||
if len(p.progressEmitter.registry) != 0 || lenQProgr != 0 || lenQQueued != 0 {
|
||||
t.Fatal("Still registered", len(p.progressEmitter.registry), lenQProgr, lenQQueued)
|
||||
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())
|
||||
}
|
||||
|
||||
// Doing it again should have no effect
|
||||
finisherChan <- state
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
if len(p.progressEmitter.registry) != 0 || len(p.queue.progress) != 0 || len(p.queue.queued) != 0 {
|
||||
t.Fatal("Still registered")
|
||||
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())
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Didn't get anything to the finisher")
|
||||
|
||||
+10
-1
@@ -33,9 +33,13 @@ func (f FileInfo) Size() (bytes int64) {
|
||||
if f.IsDeleted() || f.IsDirectory() {
|
||||
return 128
|
||||
}
|
||||
if f.CachedSize > 0 {
|
||||
return f.CachedSize
|
||||
}
|
||||
for _, b := range f.Blocks {
|
||||
bytes += int64(b.Size)
|
||||
}
|
||||
f.CachedSize = bytes
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,6 +114,7 @@ type ResponseMessage struct {
|
||||
}
|
||||
|
||||
type ClusterConfigMessage struct {
|
||||
DeviceName string // max:64
|
||||
ClientName string // max:64
|
||||
ClientVersion string // max:64
|
||||
Folders []Folder // max:1000000
|
||||
@@ -133,7 +138,11 @@ type Folder struct {
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
ID []byte // max:32
|
||||
ID []byte // max:32
|
||||
Name string // max:64
|
||||
Addresses []string // max:64,2083
|
||||
Compression uint32
|
||||
CertName string // max:64
|
||||
MaxLocalVersion int64
|
||||
Flags uint32
|
||||
Options []Option // max:64
|
||||
|
||||
@@ -557,6 +557,12 @@ ClusterConfigMessage 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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of Device Name |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Device Name (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of Client Name |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
@@ -584,6 +590,7 @@ ClusterConfigMessage Structure:
|
||||
|
||||
|
||||
struct ClusterConfigMessage {
|
||||
string DeviceName<64>;
|
||||
string ClientName<64>;
|
||||
string ClientVersion<64>;
|
||||
Folder Folders<1000000>;
|
||||
@@ -617,6 +624,10 @@ func (o ClusterConfigMessage) AppendXDR(bs []byte) ([]byte, error) {
|
||||
}
|
||||
|
||||
func (o ClusterConfigMessage) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
if l := len(o.DeviceName); l > 64 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("DeviceName", l, 64)
|
||||
}
|
||||
xw.WriteString(o.DeviceName)
|
||||
if l := len(o.ClientName); l > 64 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("ClientName", l, 64)
|
||||
}
|
||||
@@ -660,6 +671,7 @@ func (o *ClusterConfigMessage) UnmarshalXDR(bs []byte) error {
|
||||
}
|
||||
|
||||
func (o *ClusterConfigMessage) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.DeviceName = xr.ReadStringMax(64)
|
||||
o.ClientName = xr.ReadStringMax(64)
|
||||
o.ClientVersion = xr.ReadStringMax(64)
|
||||
_FoldersSize := int(xr.ReadUint32())
|
||||
@@ -830,6 +842,28 @@ Device Structure:
|
||||
\ ID (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of Name |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Name (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Number of Addresses |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of Addresses |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Addresses (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Compression |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of Cert Name |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Cert Name (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
+ Max Local Version (64 bits) +
|
||||
| |
|
||||
@@ -846,6 +880,10 @@ Device Structure:
|
||||
|
||||
struct Device {
|
||||
opaque ID<32>;
|
||||
string Name<64>;
|
||||
string Addresses<64>;
|
||||
unsigned int Compression;
|
||||
string CertName<64>;
|
||||
hyper MaxLocalVersion;
|
||||
unsigned int Flags;
|
||||
Option Options<64>;
|
||||
@@ -882,6 +920,22 @@ func (o Device) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("ID", l, 32)
|
||||
}
|
||||
xw.WriteBytes(o.ID)
|
||||
if l := len(o.Name); l > 64 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("Name", l, 64)
|
||||
}
|
||||
xw.WriteString(o.Name)
|
||||
if l := len(o.Addresses); l > 64 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("Addresses", l, 64)
|
||||
}
|
||||
xw.WriteUint32(uint32(len(o.Addresses)))
|
||||
for i := range o.Addresses {
|
||||
xw.WriteString(o.Addresses[i])
|
||||
}
|
||||
xw.WriteUint32(o.Compression)
|
||||
if l := len(o.CertName); l > 64 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("CertName", l, 64)
|
||||
}
|
||||
xw.WriteString(o.CertName)
|
||||
xw.WriteUint64(uint64(o.MaxLocalVersion))
|
||||
xw.WriteUint32(o.Flags)
|
||||
if l := len(o.Options); l > 64 {
|
||||
@@ -910,6 +964,20 @@ func (o *Device) UnmarshalXDR(bs []byte) error {
|
||||
|
||||
func (o *Device) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.ID = xr.ReadBytesMax(32)
|
||||
o.Name = xr.ReadStringMax(64)
|
||||
_AddressesSize := int(xr.ReadUint32())
|
||||
if _AddressesSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Addresses", _AddressesSize, 64)
|
||||
}
|
||||
if _AddressesSize > 64 {
|
||||
return xdr.ElementSizeExceeded("Addresses", _AddressesSize, 64)
|
||||
}
|
||||
o.Addresses = make([]string, _AddressesSize)
|
||||
for i := range o.Addresses {
|
||||
o.Addresses[i] = xr.ReadStringMax(2083)
|
||||
}
|
||||
o.Compression = xr.ReadUint32()
|
||||
o.CertName = xr.ReadStringMax(64)
|
||||
o.MaxLocalVersion = int64(xr.ReadUint64())
|
||||
o.Flags = xr.ReadUint32()
|
||||
_OptionsSize := int(xr.ReadUint32())
|
||||
|
||||
@@ -61,6 +61,13 @@ const (
|
||||
FlagRequestTemporary uint32 = 1 << iota
|
||||
)
|
||||
|
||||
// ClusterConfigMessage.Folders flags
|
||||
const (
|
||||
FlagFolderReadOnly uint32 = 1 << 0
|
||||
FlagFolderIgnorePerms = 1 << 1
|
||||
FlagFolderIgnoreDelete = 1 << 2
|
||||
)
|
||||
|
||||
// ClusterConfigMessage.Folders.Devices flags
|
||||
const (
|
||||
FlagShareTrusted uint32 = 1 << 0
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -19,11 +20,21 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
c0ID = NewDeviceID([]byte{1})
|
||||
c1ID = NewDeviceID([]byte{2})
|
||||
c0ID = NewDeviceID([]byte{1})
|
||||
c1ID = NewDeviceID([]byte{2})
|
||||
quickCfg = &quick.Config{}
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
if flag.Lookup("test.short").Value.String() != "false" {
|
||||
quickCfg.MaxCount = 10
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestHeaderFunctions(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := func(ver, id, typ int) bool {
|
||||
ver = int(uint(ver) % 16)
|
||||
id = int(uint(id) % 4096)
|
||||
@@ -38,6 +49,7 @@ func TestHeaderFunctions(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHeaderLayout(t *testing.T) {
|
||||
t.Parallel()
|
||||
var e, a uint32
|
||||
|
||||
// Version are the first four bits
|
||||
@@ -63,6 +75,7 @@ func TestHeaderLayout(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
t.Parallel()
|
||||
ar, aw := io.Pipe()
|
||||
br, bw := io.Pipe()
|
||||
|
||||
@@ -82,6 +95,7 @@ func TestPing(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestVersionErr(t *testing.T) {
|
||||
t.Parallel()
|
||||
m0 := newTestModel()
|
||||
m1 := newTestModel()
|
||||
|
||||
@@ -109,6 +123,7 @@ func TestVersionErr(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTypeErr(t *testing.T) {
|
||||
t.Parallel()
|
||||
m0 := newTestModel()
|
||||
m1 := newTestModel()
|
||||
|
||||
@@ -136,6 +151,7 @@ func TestTypeErr(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
t.Parallel()
|
||||
m0 := newTestModel()
|
||||
m1 := newTestModel()
|
||||
|
||||
@@ -171,6 +187,7 @@ func TestClose(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestElementSizeExceededNested(t *testing.T) {
|
||||
t.Parallel()
|
||||
m := ClusterConfigMessage{
|
||||
ClientName: "longstringlongstringlongstringinglongstringlongstringlonlongstringlongstringlon",
|
||||
}
|
||||
@@ -181,11 +198,7 @@ func TestElementSizeExceededNested(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalIndexMessage(t *testing.T) {
|
||||
var quickCfg = &quick.Config{MaxCountScale: 10}
|
||||
if testing.Short() {
|
||||
quickCfg = nil
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
f := func(m1 IndexMessage) bool {
|
||||
for i, f := range m1.Files {
|
||||
m1.Files[i].CachedSize = 0
|
||||
@@ -206,11 +219,7 @@ func TestMarshalIndexMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalRequestMessage(t *testing.T) {
|
||||
var quickCfg = &quick.Config{MaxCountScale: 10}
|
||||
if testing.Short() {
|
||||
quickCfg = nil
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
f := func(m1 RequestMessage) bool {
|
||||
return testMarshal(t, "request", &m1, &RequestMessage{})
|
||||
}
|
||||
@@ -221,11 +230,7 @@ func TestMarshalRequestMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalResponseMessage(t *testing.T) {
|
||||
var quickCfg = &quick.Config{MaxCountScale: 10}
|
||||
if testing.Short() {
|
||||
quickCfg = nil
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
f := func(m1 ResponseMessage) bool {
|
||||
if len(m1.Data) == 0 {
|
||||
m1.Data = nil
|
||||
@@ -239,11 +244,7 @@ func TestMarshalResponseMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalClusterConfigMessage(t *testing.T) {
|
||||
var quickCfg = &quick.Config{MaxCountScale: 10}
|
||||
if testing.Short() {
|
||||
quickCfg = nil
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
f := func(m1 ClusterConfigMessage) bool {
|
||||
return testMarshal(t, "clusterconfig", &m1, &ClusterConfigMessage{})
|
||||
}
|
||||
@@ -254,11 +255,7 @@ func TestMarshalClusterConfigMessage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMarshalCloseMessage(t *testing.T) {
|
||||
var quickCfg = &quick.Config{MaxCountScale: 10}
|
||||
if testing.Short() {
|
||||
quickCfg = nil
|
||||
}
|
||||
|
||||
t.Parallel()
|
||||
f := func(m1 CloseMessage) bool {
|
||||
return testMarshal(t, "close", &m1, &CloseMessage{})
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/dialer"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
@@ -156,6 +157,8 @@ func (p *Process) Get(path string) ([]byte, error) {
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
Dial: dialer.Dial,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
}
|
||||
|
||||
+21
-262
@@ -5,278 +5,37 @@ package client
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/relay/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type ProtocolClient struct {
|
||||
URI *url.URL
|
||||
Invitations chan protocol.SessionInvitation
|
||||
type relayClientFactory func(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) RelayClient
|
||||
|
||||
closeInvitationsOnFinish bool
|
||||
var (
|
||||
supportedSchemes = map[string]relayClientFactory{
|
||||
"relay": newStaticClient,
|
||||
"dynamic+http": newDynamicClient,
|
||||
"dynamic+https": newDynamicClient,
|
||||
}
|
||||
)
|
||||
|
||||
config *tls.Config
|
||||
|
||||
timeout time.Duration
|
||||
|
||||
stop chan struct{}
|
||||
stopped chan struct{}
|
||||
|
||||
conn *tls.Conn
|
||||
|
||||
mut sync.RWMutex
|
||||
connected bool
|
||||
latency time.Duration
|
||||
type RelayClient interface {
|
||||
Serve()
|
||||
Stop()
|
||||
StatusOK() bool
|
||||
Latency() time.Duration
|
||||
String() string
|
||||
Invitations() chan protocol.SessionInvitation
|
||||
URI() *url.URL
|
||||
}
|
||||
|
||||
func NewProtocolClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) *ProtocolClient {
|
||||
closeInvitationsOnFinish := false
|
||||
if invitations == nil {
|
||||
closeInvitationsOnFinish = true
|
||||
invitations = make(chan protocol.SessionInvitation)
|
||||
func NewClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) (RelayClient, error) {
|
||||
factory, ok := supportedSchemes[uri.Scheme]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Unsupported scheme: %s", uri.Scheme)
|
||||
}
|
||||
|
||||
return &ProtocolClient{
|
||||
URI: uri,
|
||||
Invitations: invitations,
|
||||
|
||||
closeInvitationsOnFinish: closeInvitationsOnFinish,
|
||||
|
||||
config: configForCerts(certs),
|
||||
|
||||
timeout: time.Minute * 2,
|
||||
|
||||
stop: make(chan struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
|
||||
mut: sync.NewRWMutex(),
|
||||
connected: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProtocolClient) Serve() {
|
||||
c.stop = make(chan struct{})
|
||||
c.stopped = make(chan struct{})
|
||||
defer close(c.stopped)
|
||||
|
||||
if err := c.connect(); err != nil {
|
||||
l.Debugln("Relay connect:", err)
|
||||
return
|
||||
}
|
||||
|
||||
l.Debugln(c, "connected", c.conn.RemoteAddr())
|
||||
|
||||
if err := c.join(); err != nil {
|
||||
c.conn.Close()
|
||||
l.Infoln("Relay join:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.conn.SetDeadline(time.Time{}); err != nil {
|
||||
c.conn.Close()
|
||||
l.Infoln("Relay set deadline:", err)
|
||||
return
|
||||
}
|
||||
|
||||
l.Debugln(c, "joined", c.conn.RemoteAddr(), "via", c.conn.LocalAddr())
|
||||
|
||||
defer c.cleanup()
|
||||
c.mut.Lock()
|
||||
c.connected = true
|
||||
c.mut.Unlock()
|
||||
|
||||
messages := make(chan interface{})
|
||||
errors := make(chan error, 1)
|
||||
|
||||
go messageReader(c.conn, messages, errors)
|
||||
|
||||
timeout := time.NewTimer(c.timeout)
|
||||
|
||||
for {
|
||||
select {
|
||||
case message := <-messages:
|
||||
timeout.Reset(c.timeout)
|
||||
l.Debugf("%s received message %T", c, message)
|
||||
|
||||
switch msg := message.(type) {
|
||||
case protocol.Ping:
|
||||
if err := protocol.WriteMessage(c.conn, protocol.Pong{}); err != nil {
|
||||
l.Infoln("Relay write:", err)
|
||||
return
|
||||
|
||||
}
|
||||
l.Debugln(c, "sent pong")
|
||||
|
||||
case protocol.SessionInvitation:
|
||||
ip := net.IP(msg.Address)
|
||||
if len(ip) == 0 || ip.IsUnspecified() {
|
||||
msg.Address = c.conn.RemoteAddr().(*net.TCPAddr).IP[:]
|
||||
}
|
||||
c.Invitations <- msg
|
||||
|
||||
default:
|
||||
l.Infoln("Relay: protocol error: unexpected message %v", msg)
|
||||
return
|
||||
}
|
||||
|
||||
case <-c.stop:
|
||||
l.Debugln(c, "stopping")
|
||||
return
|
||||
|
||||
case err := <-errors:
|
||||
l.Infoln("Relay received:", err)
|
||||
return
|
||||
|
||||
case <-timeout.C:
|
||||
l.Debugln(c, "timed out")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProtocolClient) Stop() {
|
||||
if c.stop == nil {
|
||||
return
|
||||
}
|
||||
|
||||
close(c.stop)
|
||||
<-c.stopped
|
||||
}
|
||||
|
||||
func (c *ProtocolClient) StatusOK() bool {
|
||||
c.mut.RLock()
|
||||
con := c.connected
|
||||
c.mut.RUnlock()
|
||||
return con
|
||||
}
|
||||
|
||||
func (c *ProtocolClient) Latency() time.Duration {
|
||||
c.mut.RLock()
|
||||
lat := c.latency
|
||||
c.mut.RUnlock()
|
||||
return lat
|
||||
}
|
||||
|
||||
func (c *ProtocolClient) String() string {
|
||||
return fmt.Sprintf("ProtocolClient@%p", c)
|
||||
}
|
||||
|
||||
func (c *ProtocolClient) connect() error {
|
||||
if c.URI.Scheme != "relay" {
|
||||
return fmt.Errorf("Unsupported relay schema: %v", c.URI.Scheme)
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
tcpConn, err := net.Dial("tcp", c.URI.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mut.Lock()
|
||||
c.latency = time.Since(t0)
|
||||
c.mut.Unlock()
|
||||
|
||||
conn := tls.Client(tcpConn, c.config)
|
||||
if err = conn.Handshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := performHandshakeAndValidation(conn, c.URI); err != nil {
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ProtocolClient) cleanup() {
|
||||
if c.closeInvitationsOnFinish {
|
||||
close(c.Invitations)
|
||||
c.Invitations = make(chan protocol.SessionInvitation)
|
||||
}
|
||||
|
||||
l.Debugln(c, "cleaning up")
|
||||
|
||||
c.mut.Lock()
|
||||
c.connected = false
|
||||
c.mut.Unlock()
|
||||
|
||||
c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *ProtocolClient) join() error {
|
||||
if err := protocol.WriteMessage(c.conn, protocol.JoinRelayRequest{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message, err := protocol.ReadMessage(c.conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch msg := message.(type) {
|
||||
case protocol.Response:
|
||||
if msg.Code != 0 {
|
||||
return fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("protocol error: expecting response got %v", msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func performHandshakeAndValidation(conn *tls.Conn, uri *url.URL) error {
|
||||
if err := conn.Handshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cs := conn.ConnectionState()
|
||||
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != protocol.ProtocolName {
|
||||
return fmt.Errorf("protocol negotiation error")
|
||||
}
|
||||
|
||||
q := uri.Query()
|
||||
relayIDs := q.Get("id")
|
||||
if relayIDs != "" {
|
||||
relayID, err := syncthingprotocol.DeviceIDFromString(relayIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("relay address contains invalid verification id: %s", err)
|
||||
}
|
||||
|
||||
certs := cs.PeerCertificates
|
||||
if cl := len(certs); cl != 1 {
|
||||
return fmt.Errorf("unexpected certificate count: %d", cl)
|
||||
}
|
||||
|
||||
remoteID := syncthingprotocol.NewDeviceID(certs[0].Raw)
|
||||
if remoteID != relayID {
|
||||
return fmt.Errorf("relay id does not match. Expected %v got %v", relayID, remoteID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func messageReader(conn net.Conn, messages chan<- interface{}, errors chan<- error) {
|
||||
for {
|
||||
msg, err := protocol.ReadMessage(conn)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
messages <- msg
|
||||
}
|
||||
return factory(uri, certs, invitations), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/relay/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type dynamicClient struct {
|
||||
pooladdr *url.URL
|
||||
certs []tls.Certificate
|
||||
invitations chan protocol.SessionInvitation
|
||||
closeInvitationsOnFinish bool
|
||||
|
||||
mut sync.RWMutex
|
||||
client RelayClient
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func newDynamicClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) RelayClient {
|
||||
closeInvitationsOnFinish := false
|
||||
if invitations == nil {
|
||||
closeInvitationsOnFinish = true
|
||||
invitations = make(chan protocol.SessionInvitation)
|
||||
}
|
||||
return &dynamicClient{
|
||||
pooladdr: uri,
|
||||
certs: certs,
|
||||
invitations: invitations,
|
||||
closeInvitationsOnFinish: closeInvitationsOnFinish,
|
||||
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *dynamicClient) Serve() {
|
||||
c.mut.Lock()
|
||||
c.stop = make(chan struct{})
|
||||
c.mut.Unlock()
|
||||
|
||||
uri := *c.pooladdr
|
||||
|
||||
// Trim off the `dynamic+` prefix
|
||||
uri.Scheme = uri.Scheme[8:]
|
||||
|
||||
l.Debugln(c, "looking up dynamic relays")
|
||||
|
||||
data, err := http.Get(uri.String())
|
||||
if err != nil {
|
||||
l.Debugln(c, "failed to lookup dynamic relays", err)
|
||||
return
|
||||
}
|
||||
|
||||
var ann dynamicAnnouncement
|
||||
err = json.NewDecoder(data.Body).Decode(&ann)
|
||||
data.Body.Close()
|
||||
if err != nil {
|
||||
l.Debugln(c, "failed to lookup dynamic relays", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer c.cleanup()
|
||||
|
||||
var addrs []string
|
||||
for _, relayAnn := range ann.Relays {
|
||||
ruri, err := url.Parse(relayAnn.URL)
|
||||
if err != nil {
|
||||
l.Debugln(c, "failed to parse dynamic relay address", relayAnn.URL, err)
|
||||
continue
|
||||
}
|
||||
l.Debugln(c, "found", ruri)
|
||||
addrs = append(addrs, ruri.String())
|
||||
}
|
||||
|
||||
for _, addr := range relayAddressesSortedByLatency(addrs) {
|
||||
select {
|
||||
case <-c.stop:
|
||||
l.Debugln(c, "stopping")
|
||||
return
|
||||
default:
|
||||
ruri, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
l.Debugln(c, "skipping relay", addr, err)
|
||||
continue
|
||||
}
|
||||
client, err := NewClient(ruri, c.certs, c.invitations)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
c.mut.Lock()
|
||||
c.client = client
|
||||
c.mut.Unlock()
|
||||
|
||||
c.client.Serve()
|
||||
|
||||
c.mut.Lock()
|
||||
c.client = nil
|
||||
c.mut.Unlock()
|
||||
}
|
||||
}
|
||||
l.Debugln(c, "could not find a connectable relay")
|
||||
}
|
||||
|
||||
func (c *dynamicClient) Stop() {
|
||||
c.mut.RLock()
|
||||
defer c.mut.RUnlock()
|
||||
close(c.stop)
|
||||
if c.client == nil {
|
||||
return
|
||||
}
|
||||
c.client.Stop()
|
||||
}
|
||||
|
||||
func (c *dynamicClient) StatusOK() bool {
|
||||
c.mut.RLock()
|
||||
defer c.mut.RUnlock()
|
||||
if c.client == nil {
|
||||
return false
|
||||
}
|
||||
return c.client.StatusOK()
|
||||
}
|
||||
|
||||
func (c *dynamicClient) Latency() time.Duration {
|
||||
c.mut.RLock()
|
||||
defer c.mut.RUnlock()
|
||||
if c.client == nil {
|
||||
return time.Hour
|
||||
}
|
||||
return c.client.Latency()
|
||||
}
|
||||
|
||||
func (c *dynamicClient) String() string {
|
||||
return fmt.Sprintf("DynamicClient:%p:%s@%s", c, c.URI(), c.pooladdr)
|
||||
}
|
||||
|
||||
func (c *dynamicClient) URI() *url.URL {
|
||||
c.mut.RLock()
|
||||
defer c.mut.RUnlock()
|
||||
if c.client == nil {
|
||||
return c.pooladdr
|
||||
}
|
||||
return c.client.URI()
|
||||
}
|
||||
|
||||
func (c *dynamicClient) Invitations() chan protocol.SessionInvitation {
|
||||
c.mut.RLock()
|
||||
inv := c.invitations
|
||||
c.mut.RUnlock()
|
||||
return inv
|
||||
}
|
||||
|
||||
func (c *dynamicClient) cleanup() {
|
||||
c.mut.Lock()
|
||||
if c.closeInvitationsOnFinish {
|
||||
close(c.invitations)
|
||||
c.invitations = make(chan protocol.SessionInvitation)
|
||||
}
|
||||
c.mut.Unlock()
|
||||
}
|
||||
|
||||
// This is the announcement recieved from the relay server;
|
||||
// {"relays": [{"url": "relay://10.20.30.40:5060"}, ...]}
|
||||
type dynamicAnnouncement struct {
|
||||
Relays []struct {
|
||||
URL string
|
||||
}
|
||||
}
|
||||
|
||||
// relayAddressesSortedByLatency adds local latency to the relay, and sorts them
|
||||
// by sum latency, and returns the addresses.
|
||||
func relayAddressesSortedByLatency(input []string) []string {
|
||||
relays := make(relayList, len(input))
|
||||
for i, relay := range input {
|
||||
if latency, err := osutil.GetLatencyForURL(relay); err == nil {
|
||||
relays[i] = relayWithLatency{relay, int(latency / time.Millisecond)}
|
||||
} else {
|
||||
relays[i] = relayWithLatency{relay, int(time.Hour / time.Millisecond)}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(relays)
|
||||
|
||||
addresses := make([]string, len(relays))
|
||||
for i, relay := range relays {
|
||||
addresses[i] = relay.relay
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
type relayWithLatency struct {
|
||||
relay string
|
||||
latency int
|
||||
}
|
||||
|
||||
type relayList []relayWithLatency
|
||||
|
||||
func (l relayList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l relayList) Less(a, b int) bool {
|
||||
return l[a].latency < l[b].latency
|
||||
}
|
||||
|
||||
func (l relayList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/dialer"
|
||||
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/relay/protocol"
|
||||
)
|
||||
@@ -20,10 +21,12 @@ func GetInvitationFromRelay(uri *url.URL, id syncthingprotocol.DeviceID, certs [
|
||||
return protocol.SessionInvitation{}, fmt.Errorf("Unsupported relay scheme: %v", uri.Scheme)
|
||||
}
|
||||
|
||||
conn, err := tls.Dial("tcp", uri.Host, configForCerts(certs))
|
||||
rconn, err := dialer.Dial("tcp", uri.Host)
|
||||
if err != nil {
|
||||
return protocol.SessionInvitation{}, err
|
||||
}
|
||||
|
||||
conn := tls.Client(rconn, configForCerts(certs))
|
||||
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
|
||||
if err := performHandshakeAndValidation(conn, uri); err != nil {
|
||||
@@ -63,7 +66,7 @@ func GetInvitationFromRelay(uri *url.URL, id syncthingprotocol.DeviceID, certs [
|
||||
func JoinSession(invitation protocol.SessionInvitation) (net.Conn, error) {
|
||||
addr := net.JoinHostPort(net.IP(invitation.Address).String(), strconv.Itoa(int(invitation.Port)))
|
||||
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
conn, err := dialer.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -99,7 +102,11 @@ func JoinSession(invitation protocol.SessionInvitation) (net.Conn, error) {
|
||||
func TestRelay(uri *url.URL, certs []tls.Certificate, sleep time.Duration, times int) bool {
|
||||
id := syncthingprotocol.NewDeviceID(certs[0].Certificate[0])
|
||||
invs := make(chan protocol.SessionInvitation, 1)
|
||||
c := NewProtocolClient(uri, certs, invs)
|
||||
c, err := NewClient(uri, certs, invs)
|
||||
if err != nil {
|
||||
close(invs)
|
||||
return false
|
||||
}
|
||||
go c.Serve()
|
||||
defer func() {
|
||||
close(invs)
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/relay/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type staticClient struct {
|
||||
uri *url.URL
|
||||
invitations chan protocol.SessionInvitation
|
||||
|
||||
closeInvitationsOnFinish bool
|
||||
|
||||
config *tls.Config
|
||||
|
||||
timeout time.Duration
|
||||
|
||||
stop chan struct{}
|
||||
stopped chan struct{}
|
||||
|
||||
conn *tls.Conn
|
||||
|
||||
mut sync.RWMutex
|
||||
connected bool
|
||||
latency time.Duration
|
||||
}
|
||||
|
||||
func newStaticClient(uri *url.URL, certs []tls.Certificate, invitations chan protocol.SessionInvitation) RelayClient {
|
||||
closeInvitationsOnFinish := false
|
||||
if invitations == nil {
|
||||
closeInvitationsOnFinish = true
|
||||
invitations = make(chan protocol.SessionInvitation)
|
||||
}
|
||||
|
||||
return &staticClient{
|
||||
uri: uri,
|
||||
invitations: invitations,
|
||||
|
||||
closeInvitationsOnFinish: closeInvitationsOnFinish,
|
||||
|
||||
config: configForCerts(certs),
|
||||
|
||||
timeout: time.Minute * 2,
|
||||
|
||||
stop: make(chan struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
|
||||
mut: sync.NewRWMutex(),
|
||||
connected: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *staticClient) Serve() {
|
||||
c.stop = make(chan struct{})
|
||||
c.stopped = make(chan struct{})
|
||||
defer close(c.stopped)
|
||||
|
||||
if err := c.connect(); err != nil {
|
||||
l.Debugln("Relay connect:", err)
|
||||
return
|
||||
}
|
||||
|
||||
l.Debugln(c, "connected", c.conn.RemoteAddr())
|
||||
|
||||
if err := c.join(); err != nil {
|
||||
c.conn.Close()
|
||||
l.Infoln("Relay join:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.conn.SetDeadline(time.Time{}); err != nil {
|
||||
c.conn.Close()
|
||||
l.Infoln("Relay set deadline:", err)
|
||||
return
|
||||
}
|
||||
|
||||
l.Debugln(c, "joined", c.conn.RemoteAddr(), "via", c.conn.LocalAddr())
|
||||
|
||||
defer c.cleanup()
|
||||
c.mut.Lock()
|
||||
c.connected = true
|
||||
c.mut.Unlock()
|
||||
|
||||
messages := make(chan interface{})
|
||||
errors := make(chan error, 1)
|
||||
|
||||
go messageReader(c.conn, messages, errors)
|
||||
|
||||
timeout := time.NewTimer(c.timeout)
|
||||
|
||||
for {
|
||||
select {
|
||||
case message := <-messages:
|
||||
timeout.Reset(c.timeout)
|
||||
l.Debugf("%s received message %T", c, message)
|
||||
|
||||
switch msg := message.(type) {
|
||||
case protocol.Ping:
|
||||
if err := protocol.WriteMessage(c.conn, protocol.Pong{}); err != nil {
|
||||
l.Infoln("Relay write:", err)
|
||||
return
|
||||
|
||||
}
|
||||
l.Debugln(c, "sent pong")
|
||||
|
||||
case protocol.SessionInvitation:
|
||||
ip := net.IP(msg.Address)
|
||||
if len(ip) == 0 || ip.IsUnspecified() {
|
||||
msg.Address = c.conn.RemoteAddr().(*net.TCPAddr).IP[:]
|
||||
}
|
||||
c.invitations <- msg
|
||||
|
||||
default:
|
||||
l.Infoln("Relay: protocol error: unexpected message %v", msg)
|
||||
return
|
||||
}
|
||||
|
||||
case <-c.stop:
|
||||
l.Debugln(c, "stopping")
|
||||
return
|
||||
|
||||
case err := <-errors:
|
||||
l.Infoln("Relay received:", err)
|
||||
return
|
||||
|
||||
case <-timeout.C:
|
||||
l.Debugln(c, "timed out")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *staticClient) Stop() {
|
||||
if c.stop == nil {
|
||||
return
|
||||
}
|
||||
|
||||
close(c.stop)
|
||||
<-c.stopped
|
||||
}
|
||||
|
||||
func (c *staticClient) StatusOK() bool {
|
||||
c.mut.RLock()
|
||||
con := c.connected
|
||||
c.mut.RUnlock()
|
||||
return con
|
||||
}
|
||||
|
||||
func (c *staticClient) Latency() time.Duration {
|
||||
c.mut.RLock()
|
||||
lat := c.latency
|
||||
c.mut.RUnlock()
|
||||
return lat
|
||||
}
|
||||
|
||||
func (c *staticClient) String() string {
|
||||
return fmt.Sprintf("StaticClient:%p@%s", c, c.URI())
|
||||
}
|
||||
|
||||
func (c *staticClient) URI() *url.URL {
|
||||
return c.uri
|
||||
}
|
||||
|
||||
func (c *staticClient) Invitations() chan protocol.SessionInvitation {
|
||||
c.mut.RLock()
|
||||
inv := c.invitations
|
||||
c.mut.RUnlock()
|
||||
return inv
|
||||
}
|
||||
|
||||
func (c *staticClient) connect() error {
|
||||
if c.uri.Scheme != "relay" {
|
||||
return fmt.Errorf("Unsupported relay schema: %v", c.uri.Scheme)
|
||||
}
|
||||
|
||||
t0 := time.Now()
|
||||
tcpConn, err := net.Dial("tcp", c.uri.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.mut.Lock()
|
||||
c.latency = time.Since(t0)
|
||||
c.mut.Unlock()
|
||||
|
||||
conn := tls.Client(tcpConn, c.config)
|
||||
if err = conn.Handshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := conn.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := performHandshakeAndValidation(conn, c.uri); err != nil {
|
||||
conn.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *staticClient) cleanup() {
|
||||
l.Debugln(c, "cleaning up")
|
||||
c.mut.Lock()
|
||||
if c.closeInvitationsOnFinish {
|
||||
close(c.invitations)
|
||||
c.invitations = make(chan protocol.SessionInvitation)
|
||||
}
|
||||
c.connected = false
|
||||
c.mut.Unlock()
|
||||
|
||||
c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *staticClient) join() error {
|
||||
if err := protocol.WriteMessage(c.conn, protocol.JoinRelayRequest{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message, err := protocol.ReadMessage(c.conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch msg := message.(type) {
|
||||
case protocol.Response:
|
||||
if msg.Code != 0 {
|
||||
return fmt.Errorf("Incorrect response code %d: %s", msg.Code, msg.Message)
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("protocol error: expecting response got %v", msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func performHandshakeAndValidation(conn *tls.Conn, uri *url.URL) error {
|
||||
if err := conn.Handshake(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cs := conn.ConnectionState()
|
||||
if !cs.NegotiatedProtocolIsMutual || cs.NegotiatedProtocol != protocol.ProtocolName {
|
||||
return fmt.Errorf("protocol negotiation error")
|
||||
}
|
||||
|
||||
q := uri.Query()
|
||||
relayIDs := q.Get("id")
|
||||
if relayIDs != "" {
|
||||
relayID, err := syncthingprotocol.DeviceIDFromString(relayIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("relay address contains invalid verification id: %s", err)
|
||||
}
|
||||
|
||||
certs := cs.PeerCertificates
|
||||
if cl := len(certs); cl != 1 {
|
||||
return fmt.Errorf("unexpected certificate count: %d", cl)
|
||||
}
|
||||
|
||||
remoteID := syncthingprotocol.NewDeviceID(certs[0].Raw)
|
||||
if remoteID != relayID {
|
||||
return fmt.Errorf("relay id does not match. Expected %v got %v", relayID, remoteID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func messageReader(conn net.Conn, messages chan<- interface{}, errors chan<- error) {
|
||||
for {
|
||||
msg, err := protocol.ReadMessage(conn)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
messages <- msg
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file).
|
||||
|
||||
//go:generate -command genxdr go run ../../syncthing/Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||
//go:generate -command genxdr go run ../../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||
//go:generate genxdr -o packets_xdr.go packets.go
|
||||
|
||||
package protocol
|
||||
|
||||
+19
-118
@@ -8,16 +8,13 @@ package relay
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"net/url"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/relay/client"
|
||||
"github.com/syncthing/syncthing/lib/relay/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
@@ -35,7 +32,7 @@ type Svc struct {
|
||||
tlsCfg *tls.Config
|
||||
|
||||
tokens map[string]suture.ServiceToken
|
||||
clients map[string]*client.ProtocolClient
|
||||
clients map[string]client.RelayClient
|
||||
mut sync.RWMutex
|
||||
invitations chan protocol.SessionInvitation
|
||||
conns chan *tls.Conn
|
||||
@@ -57,7 +54,7 @@ func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config) *Svc {
|
||||
tlsCfg: tlsCfg,
|
||||
|
||||
tokens: make(map[string]suture.ServiceToken),
|
||||
clients: make(map[string]*client.ProtocolClient),
|
||||
clients: make(map[string]client.RelayClient),
|
||||
mut: sync.NewRWMutex(),
|
||||
invitations: make(chan protocol.SessionInvitation),
|
||||
conns: conns,
|
||||
@@ -75,7 +72,8 @@ func NewSvc(cfg *config.Wrapper, tlsCfg *tls.Config) *Svc {
|
||||
}
|
||||
|
||||
eventBc := &eventBroadcaster{
|
||||
svc: svc,
|
||||
svc: svc,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
svc.Add(receiver)
|
||||
@@ -106,61 +104,17 @@ func (s *Svc) CommitConfiguration(from, to config.Configuration) bool {
|
||||
existing[uri.String()] = uri
|
||||
}
|
||||
|
||||
// Query dynamic addresses, and pick the closest relay from the ones they provide.
|
||||
for key, uri := range existing {
|
||||
if uri.Scheme != "dynamic+http" && uri.Scheme != "dynamic+https" {
|
||||
continue
|
||||
}
|
||||
delete(existing, key)
|
||||
|
||||
// Trim off the `dynamic+` prefix
|
||||
uri.Scheme = uri.Scheme[8:]
|
||||
|
||||
l.Debugln("Looking up dynamic relays from", uri)
|
||||
|
||||
data, err := http.Get(uri.String())
|
||||
if err != nil {
|
||||
l.Debugln("Failed to lookup dynamic relays", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var ann dynamicAnnouncement
|
||||
err = json.NewDecoder(data.Body).Decode(&ann)
|
||||
data.Body.Close()
|
||||
if err != nil {
|
||||
l.Debugln("Failed to lookup dynamic relays", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var dynRelayAddrs []string
|
||||
for _, relayAnn := range ann.Relays {
|
||||
ruri, err := url.Parse(relayAnn.URL)
|
||||
if err != nil {
|
||||
l.Debugln("Failed to parse dynamic relay address", relayAnn.URL, err)
|
||||
continue
|
||||
}
|
||||
l.Debugln("Found", ruri, "via", uri)
|
||||
dynRelayAddrs = append(dynRelayAddrs, ruri.String())
|
||||
}
|
||||
|
||||
if len(dynRelayAddrs) > 0 {
|
||||
dynRelayAddrs = relayAddressesSortedByLatency(dynRelayAddrs)
|
||||
closestRelay := dynRelayAddrs[0]
|
||||
l.Debugln("Picking", closestRelay, "as closest dynamic relay from", uri)
|
||||
ruri, _ := url.Parse(closestRelay)
|
||||
existing[closestRelay] = ruri
|
||||
} else {
|
||||
l.Debugln("No dynamic relay found on", uri)
|
||||
}
|
||||
}
|
||||
|
||||
s.mut.Lock()
|
||||
|
||||
for key, uri := range existing {
|
||||
_, ok := s.tokens[key]
|
||||
if !ok {
|
||||
l.Debugln("Connecting to relay", uri)
|
||||
c := client.NewProtocolClient(uri, s.tlsCfg.Certificates, s.invitations)
|
||||
c, err := client.NewClient(uri, s.tlsCfg.Certificates, s.invitations)
|
||||
if err != nil {
|
||||
l.Debugln("Failed to connect to relay", uri, err)
|
||||
continue
|
||||
}
|
||||
s.tokens[key] = s.Add(c)
|
||||
s.clients[key] = c
|
||||
}
|
||||
@@ -197,8 +151,8 @@ func (s *Svc) Relays() []string {
|
||||
|
||||
s.mut.RLock()
|
||||
relays := make([]string, 0, len(s.clients))
|
||||
for uri := range s.clients {
|
||||
relays = append(relays, uri)
|
||||
for _, client := range s.clients {
|
||||
relays = append(relays, client.URI().String())
|
||||
}
|
||||
s.mut.RUnlock()
|
||||
|
||||
@@ -216,14 +170,14 @@ func (s *Svc) RelayStatus(uri string) (time.Duration, bool) {
|
||||
}
|
||||
|
||||
s.mut.RLock()
|
||||
client, ok := s.clients[uri]
|
||||
for _, client := range s.clients {
|
||||
if client.URI().String() == uri {
|
||||
return client.Latency(), client.StatusOK()
|
||||
}
|
||||
}
|
||||
s.mut.RUnlock()
|
||||
|
||||
if !ok || !client.StatusOK() {
|
||||
return time.Hour, false
|
||||
}
|
||||
|
||||
return client.Latency(), true
|
||||
return time.Hour, false
|
||||
}
|
||||
|
||||
// Accept returns a new *tls.Conn. The connection is already handshaken.
|
||||
@@ -249,11 +203,6 @@ func (r *invitationReceiver) Serve() {
|
||||
continue
|
||||
}
|
||||
|
||||
err = osutil.SetTCPOptions(conn.(*net.TCPConn))
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
}
|
||||
|
||||
var tc *tls.Conn
|
||||
|
||||
if inv.ServerSocket {
|
||||
@@ -282,7 +231,7 @@ func (r *invitationReceiver) Stop() {
|
||||
// The eventBroadcaster sends a RelayStateChanged event when the relay status
|
||||
// changes. We need this somewhat ugly polling mechanism as there's currently
|
||||
// no way to get the event feed directly from the relay lib. This may be
|
||||
// somethign to revisit later, possibly.
|
||||
// something to revisit later, possibly.
|
||||
type eventBroadcaster struct {
|
||||
svc *Svc
|
||||
stop chan struct{}
|
||||
@@ -327,51 +276,3 @@ func (e *eventBroadcaster) Serve() {
|
||||
func (e *eventBroadcaster) Stop() {
|
||||
close(e.stop)
|
||||
}
|
||||
|
||||
// This is the announcement recieved from the relay server;
|
||||
// {"relays": [{"url": "relay://10.20.30.40:5060"}, ...]}
|
||||
type dynamicAnnouncement struct {
|
||||
Relays []struct {
|
||||
URL string
|
||||
}
|
||||
}
|
||||
|
||||
// relayAddressesSortedByLatency adds local latency to the relay, and sorts them
|
||||
// by sum latency, and returns the addresses.
|
||||
func relayAddressesSortedByLatency(input []string) []string {
|
||||
relays := make(relayList, len(input))
|
||||
for i, relay := range input {
|
||||
if latency, err := osutil.GetLatencyForURL(relay); err == nil {
|
||||
relays[i] = relayWithLatency{relay, int(latency / time.Millisecond)}
|
||||
} else {
|
||||
relays[i] = relayWithLatency{relay, int(time.Hour / time.Millisecond)}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(relays)
|
||||
|
||||
addresses := make([]string, len(relays))
|
||||
for i, relay := range relays {
|
||||
addresses[i] = relay.relay
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
type relayWithLatency struct {
|
||||
relay string
|
||||
latency int
|
||||
}
|
||||
|
||||
type relayList []relayWithLatency
|
||||
|
||||
func (l relayList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l relayList) Less(a, b int) bool {
|
||||
return l[a].latency < l[b].latency
|
||||
}
|
||||
|
||||
func (l relayList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/dialer"
|
||||
"github.com/syncthing/syncthing/lib/signature"
|
||||
)
|
||||
|
||||
@@ -38,6 +39,8 @@ const DisabledByCompilation = false
|
||||
// binary contents before accepting the upgrade.
|
||||
var insecureHTTP = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Dial: dialer.Dial,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
@@ -86,7 +89,7 @@ func SelectLatestRelease(version string, rels []Release) (Release, error) {
|
||||
|
||||
sort.Sort(SortByRelease(rels))
|
||||
// Check for a beta build
|
||||
beta := strings.Contains(version, "-beta")
|
||||
beta := strings.Contains(version, "-")
|
||||
|
||||
for _, rel := range rels {
|
||||
if rel.Prerelease && !beta {
|
||||
|
||||
@@ -62,7 +62,7 @@ var upgrades = map[string]string{
|
||||
"v0.10.21": "v0.10.30",
|
||||
"v0.10.29": "v0.10.30",
|
||||
"v0.10.31": "v0.10.30",
|
||||
"v0.10.0-alpha": "v0.10.30",
|
||||
"v0.10.0-alpha": "v0.11.0-beta0",
|
||||
"v0.10.0-beta": "v0.11.0-beta0",
|
||||
"v0.11.0-beta0+40-g53cb66e-dirty": "v0.11.0-beta0",
|
||||
}
|
||||
|
||||
+2
-1
@@ -25,6 +25,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/dialer"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
@@ -289,7 +290,7 @@ func parseResponse(deviceType string, resp []byte) (IGD, error) {
|
||||
}
|
||||
|
||||
func localIP(url *url.URL) (string, error) {
|
||||
conn, err := net.Dial("tcp", url.Host)
|
||||
conn, err := dialer.Dial("tcp", url.Host)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ pages=(
|
||||
syncthing-versioning.7
|
||||
syncthing-bep.7
|
||||
syncthing-localdisco.7
|
||||
syncthing-globaldisco.7
|
||||
)
|
||||
|
||||
for page in "${pages[@]}" ; do
|
||||
|
||||
+1439
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "September 23, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.
|
||||
@@ -75,7 +75,7 @@ site request forgery).
|
||||
.UNINDENT
|
||||
.SH CONFIG FILE FORMAT
|
||||
.sp
|
||||
The following is shows the default configuration file:
|
||||
The following shows the default configuration file:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
@@ -166,7 +166,7 @@ element:
|
||||
The folder ID, must be unique. (mandatory)
|
||||
.TP
|
||||
.B path
|
||||
The oath to the directory where the folder is stored on this
|
||||
The path to the directory where the folder is stored on this
|
||||
device; not sent to other devices. (mandatory)
|
||||
.TP
|
||||
.B ro
|
||||
@@ -417,7 +417,7 @@ are \fBudp4://\fP (UDP over IPv4), \fBudp6://\fP (UDP over IPv6) and
|
||||
.TP
|
||||
.B globalAnnounceEnabled
|
||||
Whether to announce this device to the global announce (discovery) server,
|
||||
and also use it to look up other device.
|
||||
and also use it to look up other devices.
|
||||
.TP
|
||||
.B localAnnounceEnabled
|
||||
Whether to send announcements to the local LAN, also use such
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "September 23, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.
|
||||
@@ -252,7 +252,7 @@ requires already having the public key to verify.
|
||||
.IP \(bu 2
|
||||
Announcements to the global announce server could be done using TLS,
|
||||
so the server calculates the device ID based on the certificate
|
||||
instead of trusting to the device to tell the truth.
|
||||
instead of trusting the device to tell the truth.
|
||||
.IP \(bu 2
|
||||
The user could statically configure IP or host name for the devices.
|
||||
.IP \(bu 2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-EVENT-API" "7" "September 23, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.
|
||||
@@ -306,9 +306,9 @@ Where block size is 128KB.
|
||||
.sp
|
||||
Files/folders appearing in the event data imply that the download has
|
||||
been started for that file/folder, where disappearing implies that the
|
||||
downloads has been finished or failed for that file/folder. There is
|
||||
downloads have been finished or failed for that file/folder. There is
|
||||
always a last event emitted with no data, which implies all downloads
|
||||
being finished/failed.
|
||||
have finished/failed.
|
||||
.SS FolderCompletion
|
||||
.sp
|
||||
The \fBFolderCompletion\fP event is emitted when the local or remote
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-FAQ" "7" "September 23, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.
|
||||
@@ -164,7 +164,7 @@ enable versioning, but we encourage the use of other tools to keep your data
|
||||
safe from your (or our) mistakes.
|
||||
.SS Why is there no iOS client?
|
||||
.sp
|
||||
Alternative implementation Syncthing (using the Syncthing protocol) are being
|
||||
An alternative implementation of Syncthing (using the Syncthing protocol) is being
|
||||
developed at this point in time to enable iOS support. Additionally, it seems
|
||||
that the next version of Go will support the darwin\-arm architecture such that
|
||||
we can compile the mainstream code for the iOS platform.
|
||||
@@ -260,7 +260,7 @@ bugs.
|
||||
.sp
|
||||
Syncthing logs to stdout by default. On Windows Syncthing by default also
|
||||
creates \fBsyncthing.log\fP in Syncthing\(aqs home directory (check \fB\-help\fP to see
|
||||
where that is). Command line option \fB\-logfile\fP can be used to specify user\-defined logfile.
|
||||
where that is). Command line option \fB\-logfile\fP can be used to specify a user\-defined logfile.
|
||||
.SS How do I upgrade Syncthing?
|
||||
.INDENT 0.0
|
||||
.IP \(bu 2
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.
|
||||
.nr rst2man-indent-level 0
|
||||
.
|
||||
.de1 rstReportMargin
|
||||
\\$1 \\n[an-margin]
|
||||
level \\n[rst2man-indent-level]
|
||||
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
-
|
||||
\\n[rst2man-indent0]
|
||||
\\n[rst2man-indent1]
|
||||
\\n[rst2man-indent2]
|
||||
..
|
||||
.de1 INDENT
|
||||
.\" .rstReportMargin pre:
|
||||
. RS \\$1
|
||||
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
|
||||
. nr rst2man-indent-level +1
|
||||
.\" .rstReportMargin post:
|
||||
..
|
||||
.de UNINDENT
|
||||
. RE
|
||||
.\" indent \\n[an-margin]
|
||||
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.nr rst2man-indent-level -1
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.SH ANNOUNCEMENTS
|
||||
.sp
|
||||
A device should announce itself at startup. It does this by an HTTPS POST to
|
||||
the announce server URL (with the path usually being "/", but this is of
|
||||
course up to the discovery server). The POST has a JSON payload listing direct
|
||||
connection addresses (if any) and relay addresses (if any):
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
{
|
||||
direct: ["tcp://192.0.2.45:22000", "tcp://:22202"],
|
||||
relays: [{"url": "relay://192.0.2.99:22028", "latency": 142}]
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
It\(aqs OK for either of the "direct" or "relays" fields to be either the empty
|
||||
list (\fB[]\fP), \fBnull\fP, or missing entirely. An announcment with both fields missing
|
||||
or empty is however not useful...
|
||||
.sp
|
||||
Any empty or unspecified IP addresses (i.e. addresses like \fBtcp://:22000\fP,
|
||||
\fBtcp://0.0.0.0:22000\fP, \fBtcp://[::]:22000\fP) are interpreted as referring to
|
||||
the source IP address of the announcement.
|
||||
.sp
|
||||
The device ID of the announcing device is not part of the announcement.
|
||||
Instead, the server requires that the client perform certificate
|
||||
authentication. The device ID is deduced from the presented certificate.
|
||||
.sp
|
||||
The server response is empty, with code \fB204\fP (No Content) on success. If no
|
||||
certificate was presented, status \fB403\fP (Forbidden) is returned. If the
|
||||
posted data doesn\(aqt conform to the expected format, \fB400\fP (Bad Request) is
|
||||
returned.
|
||||
.sp
|
||||
In successfull responses, the server may return a
|
||||
.nf
|
||||
\(ga\(ga
|
||||
.fi
|
||||
Reannounce\-After"
|
||||
.nf
|
||||
\(ga\(ga
|
||||
.fi
|
||||
header
|
||||
containing the number of seconds after which the client should perform a new
|
||||
announcement.
|
||||
.sp
|
||||
In error responses, the server may return a \fBRetry\-After\fP header containing
|
||||
the number of seconds after which the client should retry.
|
||||
.sp
|
||||
Performing announcements significantly more often than indicated by the
|
||||
\fBReannounce\-After\fP or \fBRetry\-After\fP headers may result in the client being
|
||||
throttled. In such cases the server may respond with status code \fB429\fP (Too
|
||||
Many Requests).
|
||||
.SH QUERIES
|
||||
.sp
|
||||
Queries are performed as HTTPS GET requests to the announce server URL. The
|
||||
requested device ID is passed as the query parameter "device", in canonical
|
||||
string form, i.e. \fBhttps://announce.syncthing.net/?device=ABC12345\-....\fP
|
||||
.sp
|
||||
Successfull responses will have status code \fB200\fP (OK) and carry a JSON payload
|
||||
of the same format as the announcement above. The response will not contain
|
||||
empty or unspecified addresses.
|
||||
.sp
|
||||
If the "device" query parameter is missing or malformed, the status code 400
|
||||
(Bad Request) is returned.
|
||||
.sp
|
||||
If the device ID is of a valid format but not found in the registry, 404 (Not
|
||||
Found) is returned.
|
||||
.sp
|
||||
If the client has exceeded a rate limit, the server may respond with 429 (Too
|
||||
Many Requests).
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2015, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
@@ -0,0 +1,189 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v3
|
||||
.
|
||||
.nr rst2man-indent-level 0
|
||||
.
|
||||
.de1 rstReportMargin
|
||||
\\$1 \\n[an-margin]
|
||||
level \\n[rst2man-indent-level]
|
||||
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
-
|
||||
\\n[rst2man-indent0]
|
||||
\\n[rst2man-indent1]
|
||||
\\n[rst2man-indent2]
|
||||
..
|
||||
.de1 INDENT
|
||||
.\" .rstReportMargin pre:
|
||||
. RS \\$1
|
||||
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
|
||||
. nr rst2man-indent-level +1
|
||||
.\" .rstReportMargin post:
|
||||
..
|
||||
.de UNINDENT
|
||||
. RE
|
||||
.\" indent \\n[an-margin]
|
||||
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.nr rst2man-indent-level -1
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.SH MODE OF OPERATION
|
||||
.sp
|
||||
Each participating device periodically sends an Announcement packet. It also
|
||||
keeps a table of the announcements it has seen. There is no way to solicit a
|
||||
reply; the only message type is Announcement.
|
||||
.sp
|
||||
On multihomed hosts the announcement packets should be sent on each interface
|
||||
on which Syncthing will accept connections.
|
||||
.sp
|
||||
For IPv4, the Announcement packet is broadcast either to the link\-specific
|
||||
broadcast address, or to the generic link\-local broadcast address
|
||||
\fB255.255.255.255\fP, with destination port 21027.
|
||||
.sp
|
||||
For IPv6, the Announcement packet is multicast to the transient link\-local
|
||||
multicast address \fB[ff12::8384]\fP, with destination port 21027.
|
||||
.sp
|
||||
It is recommended that local discovery Announcement packets be sent on a 30 to
|
||||
60 second interval, possibly with immediate transmissions when a previously
|
||||
unknown device is discovered.
|
||||
.SH DEVICE ID
|
||||
.sp
|
||||
The device ID is the SHA\-256 (32 bytes) of the device X.509 certificate. See
|
||||
device\-ids in the Syncthing documentation.
|
||||
.SH ANNOUNCEMENT PACKET
|
||||
.sp
|
||||
The Announcement packet has the following structure:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
Announce 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
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Magic |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Device Structure \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Number of Extra Devices |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Zero or more Device Structures \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
Device 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
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of ID |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e ID (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Number of Addresses |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Zero or more Address Structures \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Number of Relays |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e Zero or more Relay Structures \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
Address 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
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of URL |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e URL (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
|
||||
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
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Length of URL |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
/ /
|
||||
\e URL (variable length) \e
|
||||
/ /
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Latency |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
The corresponding XDR representation is as follows (see
|
||||
\fI\%RFC4506\fP <\fBhttp://tools.ietf.org/html/rfc4506\fP> for the XDR format):
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
struct Announcement {
|
||||
unsigned int Magic;
|
||||
Device This;
|
||||
Device Extra<>;
|
||||
}
|
||||
|
||||
struct Device {
|
||||
opaque ID<32>;
|
||||
Address Addresses<16>;
|
||||
Relay Relays<16>;
|
||||
}
|
||||
|
||||
struct Address {
|
||||
string URL<2083>;
|
||||
}
|
||||
|
||||
struct Relay {
|
||||
string URL<2083>;
|
||||
int Latency;
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
The first Device structure contains information about the sending
|
||||
device. The following zero or more Extra devices contain information
|
||||
about other devices known to the sending device.
|
||||
.sp
|
||||
In the \fBDevice\fP structure, field \fBDeviceID\fP is the SHA\-256 (32
|
||||
bytes) of the device X.509 certificate, as explained in section \fIDevice
|
||||
ID\fP\&.
|
||||
.sp
|
||||
For each \fBAddress\fP and \fBRelay\fP the \fBURL\fP field contains the actual
|
||||
target address. Direct connections (the \fBAddress\fP list) will
|
||||
typically have the \fBtcp://\fP scheme. Relay connections will typically use the
|
||||
\fBrelay://\fP scheme.
|
||||
.sp
|
||||
The \fBLatency\fP field contains the approximate latency for a TCP handshake
|
||||
(i.e. three packet round trips) between the other device and the relay, in
|
||||
milliseconds.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
2015, The Syncthing Authors
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-NETWORKING" "7" "September 23, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-REST-API" "7" "September 23, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.
|
||||
@@ -352,7 +352,7 @@ This is an expensive call, increasing CPU and RAM usage on the device. Use spari
|
||||
.SS GET /rest/db/completion
|
||||
.sp
|
||||
Returns the completion percentage (0 to 100) for a given device and
|
||||
folder.Takes \fBdevice\fP and \fBfolder\fP parameters.
|
||||
folder. Takes \fBdevice\fP and \fBfolder\fP parameters.
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-SECURITY" "7" "September 23, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-STIGNORE" "5" "September 23, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-stignore \- Prevent files from being synchronized to other nodes
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "TODO" "7" "September 23, 2015" "v0.11" "Syncthing"
|
||||
.TH "TODO" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.SH NAME
|
||||
Todo \- Keep automatic backups of deleted files by other nodes
|
||||
.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user