Compare commits
143 Commits
v0.11.0-be
...
v0.11.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0ebf06ff8 | ||
|
|
687b249034 | ||
|
|
d1528dcff0 | ||
|
|
1c31cf6319 | ||
|
|
d54c366150 | ||
|
|
4ff535f883 | ||
|
|
3cbddfe545 | ||
|
|
e3cae69495 | ||
|
|
aee40316f8 | ||
|
|
d754f9ae89 | ||
|
|
3c50b3a9e0 | ||
|
|
fb312a71f7 | ||
|
|
136d79eaa3 | ||
|
|
834336499a | ||
|
|
c9da8237df | ||
|
|
9638dcda0a | ||
|
|
756c5a2604 | ||
|
|
a9c31652b6 | ||
|
|
32a76901a9 | ||
|
|
a5e11c7489 | ||
|
|
f2b12014e1 | ||
|
|
60fcaebfdb | ||
|
|
32fe2cb659 | ||
|
|
1207d54fdd | ||
|
|
3932884688 | ||
|
|
19a2042746 | ||
|
|
4c6eb137da | ||
|
|
57ec2ff915 | ||
|
|
77a161a087 | ||
|
|
0642402449 | ||
|
|
50d377d9fe | ||
|
|
f5211b0697 | ||
|
|
fd4ea46fd7 | ||
|
|
8d8546868d | ||
|
|
8ce547edeb | ||
|
|
a17c48aed6 | ||
|
|
d12db3e7b8 | ||
|
|
15b87ae297 | ||
|
|
02fdf59839 | ||
|
|
d9da02b7a8 | ||
|
|
8f2ad6418d | ||
|
|
ff984425a3 | ||
|
|
ac1058359f | ||
|
|
9afbca3001 | ||
|
|
ec3f17cb9c | ||
|
|
73b9d5c5f9 | ||
|
|
ecc8591c95 | ||
|
|
696b67e4b1 | ||
|
|
266a5116a1 | ||
|
|
131f2be857 | ||
|
|
be7b3a9952 | ||
|
|
bb31b1785b | ||
|
|
2a60f4b1e9 | ||
|
|
33a4fb5a1a | ||
|
|
aece6e8b6c | ||
|
|
7bf55dd14f | ||
|
|
e158f17c2b | ||
|
|
c5027d9478 | ||
|
|
36c1d82146 | ||
|
|
bd4f404d45 | ||
|
|
43d39844f7 | ||
|
|
e041a4d212 | ||
|
|
433b923ea7 | ||
|
|
f8f1c72b44 | ||
|
|
542716e216 | ||
|
|
b35958d024 | ||
|
|
9ee3541655 | ||
|
|
bf7d84c12a | ||
|
|
34c691087e | ||
|
|
08c383012f | ||
|
|
e2420495f3 | ||
|
|
d530c5eda7 | ||
|
|
ef7420ecf6 | ||
|
|
c905a41e2a | ||
|
|
42ff4b5bf0 | ||
|
|
4fb74a32cc | ||
|
|
c741465328 | ||
|
|
fbca537a40 | ||
|
|
83420b0199 | ||
|
|
33d3ba1b45 | ||
|
|
497f85a236 | ||
|
|
a624c302ab | ||
|
|
cebe21a3af | ||
|
|
9eb679d70a | ||
|
|
6d84443db8 | ||
|
|
da8a1f242c | ||
|
|
946d98b71f | ||
|
|
dff51fc707 | ||
|
|
7d954dd5d1 | ||
|
|
c6300a5da8 | ||
|
|
9359daa0d9 | ||
|
|
2322e9cff7 | ||
|
|
a876e1e348 | ||
|
|
6a863c8f71 | ||
|
|
392b006b06 | ||
|
|
96289f42b7 | ||
|
|
1b69c2441c | ||
|
|
8ca85a4918 | ||
|
|
2a31031cbc | ||
|
|
d148cd8ccc | ||
|
|
d1cc1828b8 | ||
|
|
069e8cf122 | ||
|
|
45cbcaca6d | ||
|
|
102a2db1f3 | ||
|
|
9f81c85ca7 | ||
|
|
ba4a6fc0c5 | ||
|
|
aa803ce2ff | ||
|
|
a027a60f5d | ||
|
|
270649535e | ||
|
|
cf80ba71f4 | ||
|
|
b74df18a4a | ||
|
|
5cd2906a39 | ||
|
|
bc37b69d17 | ||
|
|
94f6e400ad | ||
|
|
b95a6ccf80 | ||
|
|
7df9c1b6e4 | ||
|
|
75348c0158 | ||
|
|
75fb14acaf | ||
|
|
5350315b68 | ||
|
|
658e39c270 | ||
|
|
ef7ce6c7e1 | ||
|
|
509e2411bf | ||
|
|
65c906f951 | ||
|
|
1f159e8233 | ||
|
|
936c76119d | ||
|
|
f45865606a | ||
|
|
cfc9776bae | ||
|
|
0cb7ed9e4e | ||
|
|
4b07609458 | ||
|
|
e41e58e781 | ||
|
|
f5030f1c2c | ||
|
|
2a48fb8e87 | ||
|
|
df6dbc5fa4 | ||
|
|
4b1d2839e8 | ||
|
|
a892f80e86 | ||
|
|
b2a79855ae | ||
|
|
ff4974178a | ||
|
|
d7100fd9bc | ||
|
|
0bfb40ae51 | ||
|
|
11c83670d6 | ||
|
|
68ff4f3842 | ||
|
|
ab25cd09ed | ||
|
|
8f05b8f982 |
9
.gitattributes
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Text files use LF line endings in this repository
|
||||
* text=auto
|
||||
|
||||
# Except the dependencies, which we leave alone
|
||||
Godeps/** -text=auto
|
||||
|
||||
# Diffs on these files are meaningless
|
||||
gui.files.go -diff
|
||||
*.svg -diff
|
||||
3
AUTHORS
@@ -11,16 +11,19 @@ Ben Sidhom <bsidhom@gmail.com>
|
||||
Brandon Philips <brandon@ifup.org>
|
||||
Brendan Long <self@brendanlong.com>
|
||||
Caleb Callaway <enlightened.despot@gmail.com>
|
||||
Carsten Hagemann <moter8@gmail.com>
|
||||
Cathryne Linenweaver <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
|
||||
Chris Joel <chris@scriptolo.gy>
|
||||
Colin Kennedy <moshen.colin@gmail.com>
|
||||
Daniel Martí <mvdan@mvdan.cc>
|
||||
Dennis Wilson <dw@risu.io>
|
||||
Dominik Heidler <dominik@heidler.eu>
|
||||
Elias Jarlebring <jarlebring@gmail.com>
|
||||
Emil Hessman <emil@hessman.se>
|
||||
Federico Castagnini <federico.castagnini@gmail.com>
|
||||
Felix Ableitner <me@nutomic.com>
|
||||
Felix Unterpaintner <bigbear2nd@gmail.com>
|
||||
Francois-Xavier Gsell <fxgsell@gmail.com>
|
||||
Gilli Sigurdsson <gilli@vx.is>
|
||||
Jakob Borg <jakob@nym.se>
|
||||
James Patterson <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
||||
|
||||
6
Godeps/Godeps.json
generated
@@ -11,7 +11,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/logger",
|
||||
"Rev": "f50d32b313bec2933a3e1049f7416a29f3413d29"
|
||||
"Rev": "4d4e2801954c5581e4c2a80a3d3beb3b3645fd04"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/luhn",
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/xdr",
|
||||
"Rev": "bccf335c34c01760bdc89f98c952fcda696e27d2"
|
||||
"Rev": "5f7208e86762911861c94f1849eddbfc0a60cbf0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/juju/ratelimit",
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syncthing/protocol",
|
||||
"Rev": "6277c0595c18d42e9db75dfe900463ef093a82d2"
|
||||
"Rev": "e7db2648034fb71b051902a02bc25d4468ed492e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||
|
||||
19
Godeps/_workspace/src/github.com/calmh/logger/logger.go
generated
vendored
@@ -16,6 +16,7 @@ type LogLevel int
|
||||
|
||||
const (
|
||||
LevelDebug LogLevel = iota
|
||||
LevelVerbose
|
||||
LevelInfo
|
||||
LevelOK
|
||||
LevelWarn
|
||||
@@ -83,6 +84,24 @@ func (l *Logger) Debugf(format string, vals ...interface{}) {
|
||||
l.callHandlers(LevelDebug, s)
|
||||
}
|
||||
|
||||
// Infoln logs a line with a VERBOSE prefix.
|
||||
func (l *Logger) Verboseln(vals ...interface{}) {
|
||||
l.mut.Lock()
|
||||
defer l.mut.Unlock()
|
||||
s := fmt.Sprintln(vals...)
|
||||
l.logger.Output(2, "VERBOSE: "+s)
|
||||
l.callHandlers(LevelVerbose, s)
|
||||
}
|
||||
|
||||
// Infof logs a formatted line with a VERBOSE prefix.
|
||||
func (l *Logger) Verbosef(format string, vals ...interface{}) {
|
||||
l.mut.Lock()
|
||||
defer l.mut.Unlock()
|
||||
s := fmt.Sprintf(format, vals...)
|
||||
l.logger.Output(2, "VERBOSE: "+s)
|
||||
l.callHandlers(LevelVerbose, s)
|
||||
}
|
||||
|
||||
// Infoln logs a line with an INFO prefix.
|
||||
func (l *Logger) Infoln(vals ...interface{}) {
|
||||
l.mut.Lock()
|
||||
|
||||
4
Godeps/_workspace/src/github.com/calmh/xdr/bench_test.go
generated
vendored
@@ -67,7 +67,7 @@ func BenchmarkThisEncode(b *testing.B) {
|
||||
func BenchmarkThisEncoder(b *testing.B) {
|
||||
w := xdr.NewWriter(ioutil.Discard)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := s.encodeXDR(w)
|
||||
_, err := s.EncodeXDRInto(w)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func BenchmarkThisDecoder(b *testing.B) {
|
||||
r := xdr.NewReader(rr)
|
||||
var t XDRBenchStruct
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := t.decodeXDR(r)
|
||||
err := t.DecodeXDRFrom(r)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
28
Godeps/_workspace/src/github.com/calmh/xdr/bench_xdr_test.go
generated
vendored
@@ -26,7 +26,9 @@ XDRBenchStruct Structure:
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| 0x0000 | I3 |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| uint8 |
|
||||
/ /
|
||||
\ uint8 Structure \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of Bs0 |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
@@ -69,7 +71,7 @@ struct XDRBenchStruct {
|
||||
|
||||
func (o XDRBenchStruct) EncodeXDR(w io.Writer) (int, error) {
|
||||
var xw = xdr.NewWriter(w)
|
||||
return o.encodeXDR(xw)
|
||||
return o.EncodeXDRInto(xw)
|
||||
}
|
||||
|
||||
func (o XDRBenchStruct) MarshalXDR() ([]byte, error) {
|
||||
@@ -87,11 +89,11 @@ func (o XDRBenchStruct) MustMarshalXDR() []byte {
|
||||
func (o XDRBenchStruct) AppendXDR(bs []byte) ([]byte, error) {
|
||||
var aw = xdr.AppendWriter(bs)
|
||||
var xw = xdr.NewWriter(&aw)
|
||||
_, err := o.encodeXDR(xw)
|
||||
_, err := o.EncodeXDRInto(xw)
|
||||
return []byte(aw), err
|
||||
}
|
||||
|
||||
func (o XDRBenchStruct) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
func (o XDRBenchStruct) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
xw.WriteUint64(o.I1)
|
||||
xw.WriteUint32(o.I2)
|
||||
xw.WriteUint16(o.I3)
|
||||
@@ -111,16 +113,16 @@ func (o XDRBenchStruct) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
|
||||
func (o *XDRBenchStruct) DecodeXDR(r io.Reader) error {
|
||||
xr := xdr.NewReader(r)
|
||||
return o.decodeXDR(xr)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (o *XDRBenchStruct) UnmarshalXDR(bs []byte) error {
|
||||
var br = bytes.NewReader(bs)
|
||||
var xr = xdr.NewReader(br)
|
||||
return o.decodeXDR(xr)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (o *XDRBenchStruct) decodeXDR(xr *xdr.Reader) error {
|
||||
func (o *XDRBenchStruct) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.I1 = xr.ReadUint64()
|
||||
o.I2 = xr.ReadUint32()
|
||||
o.I3 = xr.ReadUint16()
|
||||
@@ -155,7 +157,7 @@ struct repeatReader {
|
||||
|
||||
func (o repeatReader) EncodeXDR(w io.Writer) (int, error) {
|
||||
var xw = xdr.NewWriter(w)
|
||||
return o.encodeXDR(xw)
|
||||
return o.EncodeXDRInto(xw)
|
||||
}
|
||||
|
||||
func (o repeatReader) MarshalXDR() ([]byte, error) {
|
||||
@@ -173,27 +175,27 @@ func (o repeatReader) MustMarshalXDR() []byte {
|
||||
func (o repeatReader) AppendXDR(bs []byte) ([]byte, error) {
|
||||
var aw = xdr.AppendWriter(bs)
|
||||
var xw = xdr.NewWriter(&aw)
|
||||
_, err := o.encodeXDR(xw)
|
||||
_, err := o.EncodeXDRInto(xw)
|
||||
return []byte(aw), err
|
||||
}
|
||||
|
||||
func (o repeatReader) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
func (o repeatReader) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
xw.WriteBytes(o.data)
|
||||
return xw.Tot(), xw.Error()
|
||||
}
|
||||
|
||||
func (o *repeatReader) DecodeXDR(r io.Reader) error {
|
||||
xr := xdr.NewReader(r)
|
||||
return o.decodeXDR(xr)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (o *repeatReader) UnmarshalXDR(bs []byte) error {
|
||||
var br = bytes.NewReader(bs)
|
||||
var xr = xdr.NewReader(br)
|
||||
return o.decodeXDR(xr)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (o *repeatReader) decodeXDR(xr *xdr.Reader) error {
|
||||
func (o *repeatReader) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.data = xr.ReadBytes()
|
||||
return xr.Error()
|
||||
}
|
||||
|
||||
3
Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
generated
vendored
@@ -143,6 +143,9 @@ func (o *{{.TypeName}}) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
{{end}}
|
||||
{{else}}
|
||||
_{{$fieldInfo.Name}}Size := int(xr.ReadUint32())
|
||||
if _{{$fieldInfo.Name}}Size < 0 {
|
||||
return xdr.ElementSizeExceeded("{{$fieldInfo.Name}}", _{{$fieldInfo.Name}}Size, {{$fieldInfo.Max}})
|
||||
}
|
||||
{{if ge $fieldInfo.Max 1}}
|
||||
if _{{$fieldInfo.Name}}Size > {{$fieldInfo.Max}} {
|
||||
return xdr.ElementSizeExceeded("{{$fieldInfo.Name}}", _{{$fieldInfo.Name}}Size, {{$fieldInfo.Max}})
|
||||
|
||||
4
Godeps/_workspace/src/github.com/calmh/xdr/encdec_test.go
generated
vendored
@@ -32,11 +32,11 @@ type TestStruct struct {
|
||||
|
||||
type Opaque [32]byte
|
||||
|
||||
func (u *Opaque) encodeXDR(w *xdr.Writer) (int, error) {
|
||||
func (u *Opaque) EncodeXDRInto(w *xdr.Writer) (int, error) {
|
||||
return w.WriteRaw(u[:])
|
||||
}
|
||||
|
||||
func (u *Opaque) decodeXDR(r *xdr.Reader) (int, error) {
|
||||
func (u *Opaque) DecodeXDRFrom(r *xdr.Reader) (int, error) {
|
||||
return r.ReadRaw(u[:])
|
||||
}
|
||||
|
||||
|
||||
43
Godeps/_workspace/src/github.com/calmh/xdr/encdec_xdr_test.go
generated
vendored
@@ -18,17 +18,23 @@ TestStruct 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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| int |
|
||||
/ /
|
||||
\ int Structure \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| int8 |
|
||||
/ /
|
||||
\ int8 Structure \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| uint8 |
|
||||
/ /
|
||||
\ uint8 Structure \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| int16 |
|
||||
| 0x0000 | I16 |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| 0x0000 | UI16 |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| int32 |
|
||||
| I32 |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| UI32 |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
@@ -52,7 +58,9 @@ TestStruct Structure:
|
||||
\ S (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Opaque |
|
||||
/ /
|
||||
\ Opaque Structure \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Number of SS |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
@@ -68,9 +76,9 @@ struct TestStruct {
|
||||
int I;
|
||||
int8 I8;
|
||||
uint8 UI8;
|
||||
int16 I16;
|
||||
int I16;
|
||||
unsigned int UI16;
|
||||
int32 I32;
|
||||
int I32;
|
||||
unsigned int UI32;
|
||||
hyper I64;
|
||||
unsigned hyper UI64;
|
||||
@@ -84,7 +92,7 @@ struct TestStruct {
|
||||
|
||||
func (o TestStruct) EncodeXDR(w io.Writer) (int, error) {
|
||||
var xw = xdr.NewWriter(w)
|
||||
return o.encodeXDR(xw)
|
||||
return o.EncodeXDRInto(xw)
|
||||
}
|
||||
|
||||
func (o TestStruct) MarshalXDR() ([]byte, error) {
|
||||
@@ -102,11 +110,11 @@ func (o TestStruct) MustMarshalXDR() []byte {
|
||||
func (o TestStruct) AppendXDR(bs []byte) ([]byte, error) {
|
||||
var aw = xdr.AppendWriter(bs)
|
||||
var xw = xdr.NewWriter(&aw)
|
||||
_, err := o.encodeXDR(xw)
|
||||
_, err := o.EncodeXDRInto(xw)
|
||||
return []byte(aw), err
|
||||
}
|
||||
|
||||
func (o TestStruct) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
func (o TestStruct) EncodeXDRInto(xw *xdr.Writer) (int, error) {
|
||||
xw.WriteUint64(uint64(o.I))
|
||||
xw.WriteUint8(uint8(o.I8))
|
||||
xw.WriteUint8(o.UI8)
|
||||
@@ -124,7 +132,7 @@ func (o TestStruct) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("S", l, 1024)
|
||||
}
|
||||
xw.WriteString(o.S)
|
||||
_, err := o.C.encodeXDR(xw)
|
||||
_, err := o.C.EncodeXDRInto(xw)
|
||||
if err != nil {
|
||||
return xw.Tot(), err
|
||||
}
|
||||
@@ -140,16 +148,16 @@ func (o TestStruct) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
|
||||
func (o *TestStruct) DecodeXDR(r io.Reader) error {
|
||||
xr := xdr.NewReader(r)
|
||||
return o.decodeXDR(xr)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (o *TestStruct) UnmarshalXDR(bs []byte) error {
|
||||
var br = bytes.NewReader(bs)
|
||||
var xr = xdr.NewReader(br)
|
||||
return o.decodeXDR(xr)
|
||||
return o.DecodeXDRFrom(xr)
|
||||
}
|
||||
|
||||
func (o *TestStruct) decodeXDR(xr *xdr.Reader) error {
|
||||
func (o *TestStruct) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.I = int(xr.ReadUint64())
|
||||
o.I8 = int8(xr.ReadUint8())
|
||||
o.UI8 = xr.ReadUint8()
|
||||
@@ -161,8 +169,11 @@ func (o *TestStruct) decodeXDR(xr *xdr.Reader) error {
|
||||
o.UI64 = xr.ReadUint64()
|
||||
o.BS = xr.ReadBytesMax(1024)
|
||||
o.S = xr.ReadStringMax(1024)
|
||||
(&o.C).decodeXDR(xr)
|
||||
(&o.C).DecodeXDRFrom(xr)
|
||||
_SSSize := int(xr.ReadUint32())
|
||||
if _SSSize < 0 {
|
||||
return xdr.ElementSizeExceeded("SS", _SSSize, 1024)
|
||||
}
|
||||
if _SSSize > 1024 {
|
||||
return xdr.ElementSizeExceeded("SS", _SSSize, 1024)
|
||||
}
|
||||
|
||||
3
Godeps/_workspace/src/github.com/calmh/xdr/reader.go
generated
vendored
@@ -68,7 +68,8 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
|
||||
if r.err != nil {
|
||||
return nil
|
||||
}
|
||||
if max > 0 && l > max {
|
||||
if l < 0 || max > 0 && l > max {
|
||||
// l may be negative on 32 bit builds
|
||||
r.err = ElementSizeExceeded("bytes field", l, max)
|
||||
return nil
|
||||
}
|
||||
|
||||
27
Godeps/_workspace/src/github.com/syncthing/protocol/message_xdr.go
generated
vendored
@@ -110,12 +110,18 @@ func (o *IndexMessage) UnmarshalXDR(bs []byte) error {
|
||||
func (o *IndexMessage) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.Folder = xr.ReadString()
|
||||
_FilesSize := int(xr.ReadUint32())
|
||||
if _FilesSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Files", _FilesSize, 0)
|
||||
}
|
||||
o.Files = make([]FileInfo, _FilesSize)
|
||||
for i := range o.Files {
|
||||
(&o.Files[i]).DecodeXDRFrom(xr)
|
||||
}
|
||||
o.Flags = xr.ReadUint32()
|
||||
_OptionsSize := int(xr.ReadUint32())
|
||||
if _OptionsSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
if _OptionsSize > 64 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
@@ -236,6 +242,9 @@ func (o *FileInfo) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
(&o.Version).DecodeXDRFrom(xr)
|
||||
o.LocalVersion = int64(xr.ReadUint64())
|
||||
_BlocksSize := int(xr.ReadUint32())
|
||||
if _BlocksSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Blocks", _BlocksSize, 0)
|
||||
}
|
||||
o.Blocks = make([]BlockInfo, _BlocksSize)
|
||||
for i := range o.Blocks {
|
||||
(&o.Blocks[i]).DecodeXDRFrom(xr)
|
||||
@@ -442,6 +451,9 @@ func (o *RequestMessage) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.Hash = xr.ReadBytesMax(64)
|
||||
o.Flags = xr.ReadUint32()
|
||||
_OptionsSize := int(xr.ReadUint32())
|
||||
if _OptionsSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
if _OptionsSize > 64 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
@@ -633,11 +645,17 @@ func (o *ClusterConfigMessage) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.ClientName = xr.ReadStringMax(64)
|
||||
o.ClientVersion = xr.ReadStringMax(64)
|
||||
_FoldersSize := int(xr.ReadUint32())
|
||||
if _FoldersSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Folders", _FoldersSize, 0)
|
||||
}
|
||||
o.Folders = make([]Folder, _FoldersSize)
|
||||
for i := range o.Folders {
|
||||
(&o.Folders[i]).DecodeXDRFrom(xr)
|
||||
}
|
||||
_OptionsSize := int(xr.ReadUint32())
|
||||
if _OptionsSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
if _OptionsSize > 64 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
@@ -750,12 +768,18 @@ func (o *Folder) UnmarshalXDR(bs []byte) error {
|
||||
func (o *Folder) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.ID = xr.ReadStringMax(64)
|
||||
_DevicesSize := int(xr.ReadUint32())
|
||||
if _DevicesSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Devices", _DevicesSize, 0)
|
||||
}
|
||||
o.Devices = make([]Device, _DevicesSize)
|
||||
for i := range o.Devices {
|
||||
(&o.Devices[i]).DecodeXDRFrom(xr)
|
||||
}
|
||||
o.Flags = xr.ReadUint32()
|
||||
_OptionsSize := int(xr.ReadUint32())
|
||||
if _OptionsSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
if _OptionsSize > 64 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
@@ -862,6 +886,9 @@ func (o *Device) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.MaxLocalVersion = int64(xr.ReadUint64())
|
||||
o.Flags = xr.ReadUint32()
|
||||
_OptionsSize := int(xr.ReadUint32())
|
||||
if _OptionsSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
if _OptionsSize > 64 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
|
||||
10
Godeps/_workspace/src/github.com/syncthing/protocol/vector.go
generated
vendored
@@ -103,3 +103,13 @@ func (a Vector) Concurrent(b Vector) bool {
|
||||
comp := a.Compare(b)
|
||||
return comp == ConcurrentGreater || comp == ConcurrentLesser
|
||||
}
|
||||
|
||||
// Counter returns the current value of the given counter ID.
|
||||
func (v Vector) Counter(id uint64) uint64 {
|
||||
for _, c := range v {
|
||||
if c.ID == id {
|
||||
return c.Value
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
14
Godeps/_workspace/src/github.com/syncthing/protocol/vector_test.go
generated
vendored
@@ -118,5 +118,17 @@ func TestMerge(t *testing.T) {
|
||||
t.Errorf("%d: %+v.Merge(%+v) == %+v (expected %+v)", i, tc.a, tc.b, m, tc.m)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCounterValue(t *testing.T) {
|
||||
v0 := Vector{Counter{42, 1}, Counter{64, 5}}
|
||||
if v0.Counter(42) != 1 {
|
||||
t.Error("Counter error, %d != %d", v0.Counter(42), 1)
|
||||
}
|
||||
if v0.Counter(64) != 5 {
|
||||
t.Error("Counter error, %d != %d", v0.Counter(64), 5)
|
||||
}
|
||||
if v0.Counter(72) != 0 {
|
||||
t.Error("Counter error, %d != %d", v0.Counter(72), 0)
|
||||
}
|
||||
}
|
||||
|
||||
1
Godeps/_workspace/src/github.com/thejerf/suture/pre-commit
generated
vendored
@@ -9,3 +9,4 @@ if [ ! -z "$GOLINTOUT" -o "$?" != 0 ]; then
|
||||
fi
|
||||
|
||||
go test
|
||||
|
||||
|
||||
3
NICKS
@@ -3,6 +3,7 @@
|
||||
AudriusButkevicius <audrius.butkevicius@gmail.com>
|
||||
Cathryne <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
|
||||
KayoticSully <kayoticsully@gmail.com>
|
||||
Moter8 <moter8@gmail.com>
|
||||
Nutomic <me@nutomic.com>
|
||||
Rewt0r <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
||||
Vilbrekin <vilbrekin@gmail.com>
|
||||
@@ -23,6 +24,7 @@ facastagnini <federico.castagnini@gmail.com>
|
||||
filoozoom <philippe@schommers.be>
|
||||
frioux <frew@afoolishmanifesto.com> <frioux@gmail.com>
|
||||
gillisig <gilli@vx.is>
|
||||
jarlebring <jarlebring@gmail.com>
|
||||
jedie <github.com@jensdiemer.de> <git@jensdiemer.de>
|
||||
jpjp <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
||||
kamadak <kamada@nanohz.org>
|
||||
@@ -50,3 +52,4 @@ tnn2 <tnn@nygren.pp.se>
|
||||
tojrobinson <tully@tojr.org>
|
||||
uok <ueomkail@gmail.com> <uok@users.noreply.github.com>
|
||||
veeti <veeti.paananen@rojekti.fi>
|
||||
zukoo <fxgsell@gmail.com>
|
||||
|
||||
19
README.md
@@ -5,18 +5,17 @@ syncthing
|
||||
[](http://godoc.org/github.com/syncthing/syncthing)
|
||||
[](https://www.mozilla.org/MPL/2.0/)
|
||||
|
||||
This is the `syncthing` project. The following are the project goals:
|
||||
This is the `syncthing` project which pursues the following goals:
|
||||
|
||||
1. Define a protocol for synchronization of a folder between a number of
|
||||
collaborating devices. The protocol should be well defined, unambiguous,
|
||||
collaborating devices. This protocol should be well defined, unambiguous,
|
||||
easily understood, free to use, efficient, secure and language neutral.
|
||||
This is the [Block Exchange
|
||||
This is called the [Block Exchange
|
||||
Protocol](https://github.com/syncthing/specs/blob/master/BEPv1.md).
|
||||
|
||||
2. Provide the reference implementation to demonstrate the usability of
|
||||
said protocol. This is the `syncthing` utility. It is the hope that
|
||||
alternative, compatible implementations of the protocol will come to
|
||||
exist.
|
||||
said protocol. This is the `syncthing` utility. We hope that
|
||||
alternative, compatible implementations of the protocol will arise.
|
||||
|
||||
The two are evolving together; the protocol is not to be considered
|
||||
stable until syncthing 1.0 is released, at which point it is locked down
|
||||
@@ -32,20 +31,20 @@ There are a few examples for keeping syncthing running in the background
|
||||
on your system in [the etc directory](https://github.com/syncthing/syncthing/blob/master/etc).
|
||||
|
||||
There is an IRC channel, `#syncthing` on Freenode, for talking directly
|
||||
to developers and users (when awake and present, etc.).
|
||||
to developers and users.
|
||||
|
||||
Building
|
||||
--------
|
||||
|
||||
Building Syncthing from source is easy, and there's a
|
||||
[guide](https://github.com/syncthing/syncthing/wiki/Building).
|
||||
that describes it for both Unix and Windows.
|
||||
that describes it for both Unix and Windows systems.
|
||||
|
||||
Signed Releases
|
||||
---------------
|
||||
|
||||
As of v0.10.15 and onwards, git tags and release binaries are GPG signed
|
||||
with the key D26E6ED000654A3E (see http://syncthing.net/security.html).
|
||||
with the key D26E6ED000654A3E (see https://syncthing.net/security.html).
|
||||
For release binaries, MD5 and SHA1 checksums are calculated and signed,
|
||||
available in the md5sum.txt.asc and sha1sum.txt.asc files.
|
||||
|
||||
@@ -57,4 +56,4 @@ documentation](https://github.com/syncthing/syncthing/wiki/) is on the
|
||||
Github wiki.
|
||||
|
||||
All code is licensed under the
|
||||
[MPLv2](https://github.com/syncthing/syncthing/blob/master/LICENSE).
|
||||
[MPLv2 License](https://github.com/syncthing/syncthing/blob/master/LICENSE).
|
||||
|
||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.7 KiB |
55
build.go
@@ -78,6 +78,11 @@ func main() {
|
||||
tags = []string{"noupgrade"}
|
||||
}
|
||||
install("./cmd/...", tags)
|
||||
|
||||
vet("./cmd/syncthing")
|
||||
vet("./internal/...")
|
||||
lint("./cmd/syncthing")
|
||||
lint("./internal/...")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,8 +108,7 @@ func main() {
|
||||
build(pkg, tags)
|
||||
|
||||
case "test":
|
||||
pkg := "./..."
|
||||
test(pkg)
|
||||
test("./...")
|
||||
|
||||
case "assets":
|
||||
assets()
|
||||
@@ -130,6 +134,14 @@ func main() {
|
||||
case "clean":
|
||||
clean()
|
||||
|
||||
case "vet":
|
||||
vet("./cmd/syncthing")
|
||||
vet("./internal/...")
|
||||
|
||||
case "lint":
|
||||
lint("./cmd/syncthing")
|
||||
lint("./internal/...")
|
||||
|
||||
default:
|
||||
log.Fatalf("Unknown command %q", cmd)
|
||||
}
|
||||
@@ -437,10 +449,7 @@ func run(cmd string, args ...string) []byte {
|
||||
func runError(cmd string, args ...string) ([]byte, error) {
|
||||
ecmd := exec.Command(cmd, args...)
|
||||
bs, err := ecmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.TrimSpace(bs), nil
|
||||
return bytes.TrimSpace(bs), err
|
||||
}
|
||||
|
||||
func runPrint(cmd string, args ...string) {
|
||||
@@ -615,3 +624,37 @@ func md5File(file string) error {
|
||||
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
func vet(pkg string) {
|
||||
bs, err := runError("go", "vet", pkg)
|
||||
if err != nil && err.Error() == "exit status 3" || bytes.Contains(bs, []byte("no such tool \"vet\"")) {
|
||||
// Go said there is no go vet
|
||||
log.Println(`- No go vet, no vetting. Try "go get -u golang.org/x/tools/cmd/vet".`)
|
||||
return
|
||||
}
|
||||
|
||||
falseAlarmComposites := regexp.MustCompile("composite literal uses unkeyed fields")
|
||||
exitStatus := regexp.MustCompile("exit status 1")
|
||||
for _, line := range bytes.Split(bs, []byte("\n")) {
|
||||
if falseAlarmComposites.Match(line) || exitStatus.Match(line) {
|
||||
continue
|
||||
}
|
||||
log.Printf("%s", line)
|
||||
}
|
||||
}
|
||||
|
||||
func lint(pkg string) {
|
||||
bs, err := runError("golint", pkg)
|
||||
if err != nil {
|
||||
log.Println(`- No golint, not linting. Try "go get -u github.com/golang/lint/golint".`)
|
||||
return
|
||||
}
|
||||
|
||||
analCommentPolicy := regexp.MustCompile(`exported (function|method|const|type|var) [^\s]+ should have comment`)
|
||||
for _, line := range bytes.Split(bs, []byte("\n")) {
|
||||
if analCommentPolicy.Match(line) {
|
||||
continue
|
||||
}
|
||||
log.Printf("%s", line)
|
||||
}
|
||||
}
|
||||
|
||||
21
build.sh
@@ -118,8 +118,6 @@ case "${1:-default}" in
|
||||
-e "STTRACE=$STTRACE" \
|
||||
syncthing/build:latest \
|
||||
sh -c './build.sh clean \
|
||||
&& go tool vet -composites=false cmd/*/*.go internal/*/*.go \
|
||||
&& ( golint ./cmd/... ; golint ./internal/... ) | egrep -v "comment on exported|should have comment" \
|
||||
&& ./build.sh all \
|
||||
&& STTRACE=all ./build.sh test-cov'
|
||||
;;
|
||||
@@ -138,6 +136,25 @@ case "${1:-default}" in
|
||||
&& git clean -fxd .'
|
||||
;;
|
||||
|
||||
docker-lint)
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
-e "STTRACE=$STTRACE" \
|
||||
syncthing/build:latest \
|
||||
sh -euxc 'go run build.go lint'
|
||||
;;
|
||||
|
||||
|
||||
docker-vet)
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
-e "STTRACE=$STTRACE" \
|
||||
syncthing/build:latest \
|
||||
sh -euxc 'go run build.go vet'
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown build command $1"
|
||||
;;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// 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 ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -17,7 +17,8 @@ no-docs-typos() {
|
||||
grep -v a9339d0627fff439879d157c75077f02c9fac61b |\
|
||||
grep -v 254c63763a3ad42fd82259f1767db526cff94a14 |\
|
||||
grep -v 4b76ec40c07078beaa2c5e250ed7d9bd6276a718 |\
|
||||
grep -v ffc39dfbcb34eacc3ea12327a02b6e7741a2c207
|
||||
grep -v ffc39dfbcb34eacc3ea12327a02b6e7741a2c207 |\
|
||||
grep -v 32a76901a91ff0f663db6f0830e0aedec946e4d0
|
||||
}
|
||||
|
||||
print-missing-authors() {
|
||||
|
||||
69
cmd/syncthing/audit.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
)
|
||||
|
||||
// The auditSvc subscribes to events and writes these in JSON format, one
|
||||
// event per line, to the specified writer.
|
||||
type auditSvc struct {
|
||||
w io.Writer // audit destination
|
||||
stop chan struct{} // signals time to stop
|
||||
started chan struct{} // signals startup complete
|
||||
stopped chan struct{} // signals stop complete
|
||||
}
|
||||
|
||||
func newAuditSvc(w io.Writer) *auditSvc {
|
||||
return &auditSvc{
|
||||
w: w,
|
||||
stop: make(chan struct{}),
|
||||
started: make(chan struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Serve runs the audit service.
|
||||
func (s *auditSvc) Serve() {
|
||||
defer close(s.stopped)
|
||||
sub := events.Default.Subscribe(events.AllEvents)
|
||||
defer events.Default.Unsubscribe(sub)
|
||||
enc := json.NewEncoder(s.w)
|
||||
|
||||
// We're ready to start processing events.
|
||||
close(s.started)
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev := <-sub.C():
|
||||
enc.Encode(ev)
|
||||
case <-s.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the audit service.
|
||||
func (s *auditSvc) Stop() {
|
||||
close(s.stop)
|
||||
}
|
||||
|
||||
// WaitForStart returns once the audit service is ready to receive events, or
|
||||
// immediately if it's already running.
|
||||
func (s *auditSvc) WaitForStart() {
|
||||
<-s.started
|
||||
}
|
||||
|
||||
// WaitForStop returns once the audit service has stopped.
|
||||
// (Needed by the tests.)
|
||||
func (s *auditSvc) WaitForStop() {
|
||||
<-s.stopped
|
||||
}
|
||||
54
cmd/syncthing/auditsvc_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
)
|
||||
|
||||
func TestAuditService(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
svc := newAuditSvc(buf)
|
||||
|
||||
// Event sent before start, will not be logged
|
||||
events.Default.Log(events.Ping, "the first event")
|
||||
|
||||
go svc.Serve()
|
||||
svc.WaitForStart()
|
||||
|
||||
// Event that should end up in the audit log
|
||||
events.Default.Log(events.Ping, "the second event")
|
||||
|
||||
// We need to give the events time to arrive, since the channels are buffered etc.
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
svc.Stop()
|
||||
svc.WaitForStop()
|
||||
|
||||
// This event should not be logged, since we have stopped.
|
||||
events.Default.Log(events.Ping, "the third event")
|
||||
|
||||
result := string(buf.Bytes())
|
||||
t.Log(result)
|
||||
|
||||
if strings.Contains(result, "first event") {
|
||||
t.Error("Unexpected first event")
|
||||
}
|
||||
|
||||
if !strings.Contains(result, "second event") {
|
||||
t.Error("Missing second event")
|
||||
}
|
||||
|
||||
if strings.Contains(result, "third event") {
|
||||
t.Error("Missing third event")
|
||||
}
|
||||
}
|
||||
@@ -15,23 +15,84 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/internal/config"
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
"github.com/syncthing/syncthing/internal/model"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
func listenConnect(myID protocol.DeviceID, m *model.Model, tlsCfg *tls.Config) {
|
||||
var conns = make(chan *tls.Conn)
|
||||
// The connection service listens on TLS and dials configured unconnected
|
||||
// devices. Successful connections are handed to the model.
|
||||
type connectionSvc struct {
|
||||
*suture.Supervisor
|
||||
cfg *config.Wrapper
|
||||
myID protocol.DeviceID
|
||||
model *model.Model
|
||||
tlsCfg *tls.Config
|
||||
conns chan *tls.Conn
|
||||
}
|
||||
|
||||
// Listen
|
||||
for _, addr := range cfg.Options().ListenAddress {
|
||||
go listenTLS(conns, addr, tlsCfg)
|
||||
func newConnectionSvc(cfg *config.Wrapper, myID protocol.DeviceID, model *model.Model, tlsCfg *tls.Config) *connectionSvc {
|
||||
svc := &connectionSvc{
|
||||
Supervisor: suture.NewSimple("connectionSvc"),
|
||||
cfg: cfg,
|
||||
myID: myID,
|
||||
model: model,
|
||||
tlsCfg: tlsCfg,
|
||||
conns: make(chan *tls.Conn),
|
||||
}
|
||||
|
||||
// Connect
|
||||
go dialTLS(m, conns, tlsCfg)
|
||||
// There are several moving parts here; one routine per listening address
|
||||
// to handle incoming connections, one routine to periodically attempt
|
||||
// outgoing connections, and lastly one routine to the the common handling
|
||||
// regardless of whether the connection was incoming or outgoing. It ends
|
||||
// up as in the diagram below. We embed a Supervisor to manage the
|
||||
// routines (i.e. log and restart if they crash or exit, etc).
|
||||
//
|
||||
// +-----------------+
|
||||
// Incoming | +---------------+-+ +-----------------+
|
||||
// Connections | | | | | Outgoing
|
||||
// -------------->| | svc.listen | | | Connections
|
||||
// | | (1 per listen | | svc.connect |-------------->
|
||||
// | | address) | | |
|
||||
// +-+ | | |
|
||||
// +-----------------+ +-----------------+
|
||||
// v v
|
||||
// | |
|
||||
// | |
|
||||
// +------------+-----------+
|
||||
// |
|
||||
// | svc.conns
|
||||
// v
|
||||
// +-----------------+
|
||||
// | |
|
||||
// | |
|
||||
// | svc.handle |------> model.AddConnection()
|
||||
// | |
|
||||
// | |
|
||||
// +-----------------+
|
||||
//
|
||||
// TODO: Clean shutdown, and/or handling config changes on the fly. We
|
||||
// partly do this now - new devices and addresses will be picked up, but
|
||||
// not new listen addresses and we don't support disconnecting devices
|
||||
// that are removed and so on...
|
||||
|
||||
svc.Add(serviceFunc(svc.connect))
|
||||
for _, addr := range svc.cfg.Options().ListenAddress {
|
||||
addr := addr
|
||||
listener := serviceFunc(func() {
|
||||
svc.listen(addr)
|
||||
})
|
||||
svc.Add(listener)
|
||||
}
|
||||
svc.Add(serviceFunc(svc.handle))
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (s *connectionSvc) handle() {
|
||||
next:
|
||||
for conn := range conns {
|
||||
for conn := range s.conns {
|
||||
cs := conn.ConnectionState()
|
||||
|
||||
// We should have negotiated the next level protocol "bep/1.0" as part
|
||||
@@ -55,7 +116,7 @@ next:
|
||||
remoteID := protocol.NewDeviceID(remoteCert.Raw)
|
||||
|
||||
// The device ID should not be that of ourselves. It can happen
|
||||
// though, especially in the presense of NAT hairpinning, multiple
|
||||
// though, especially in the presence of NAT hairpinning, multiple
|
||||
// clients between the same NAT gateway, and global discovery.
|
||||
if remoteID == myID {
|
||||
l.Infof("Connected to myself (%s) - should not happen", remoteID)
|
||||
@@ -67,15 +128,15 @@ next:
|
||||
// could use some better handling. If the old connection is dead but
|
||||
// hasn't timed out yet we may want to drop *that* connection and keep
|
||||
// this one. But in case we are two devices connecting to each other
|
||||
// in parallell we don't want to do that or we end up with no
|
||||
// in parallel we don't want to do that or we end up with no
|
||||
// connections still established...
|
||||
if m.ConnectedTo(remoteID) {
|
||||
if s.model.ConnectedTo(remoteID) {
|
||||
l.Infof("Connected to already connected device (%s)", remoteID)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
for deviceID, deviceCfg := range cfg.Devices() {
|
||||
for deviceID, deviceCfg := range s.cfg.Devices() {
|
||||
if deviceID == remoteID {
|
||||
// Verify the name on the certificate. By default we set it to
|
||||
// "syncthing" when generating, but the user may have replaced
|
||||
@@ -97,7 +158,7 @@ next:
|
||||
// If rate limiting is set, and based on the address we should
|
||||
// limit the connection, then we wrap it in a limiter.
|
||||
|
||||
limit := shouldLimit(conn.RemoteAddr())
|
||||
limit := s.shouldLimit(conn.RemoteAddr())
|
||||
|
||||
wr := io.Writer(conn)
|
||||
if limit && writeRateLimit != nil {
|
||||
@@ -110,7 +171,7 @@ next:
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
|
||||
protoConn := protocol.NewConnection(remoteID, rd, wr, m, name, deviceCfg.Compression)
|
||||
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
|
||||
|
||||
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
||||
if debugNet {
|
||||
@@ -121,12 +182,12 @@ next:
|
||||
"addr": conn.RemoteAddr().String(),
|
||||
})
|
||||
|
||||
m.AddConnection(conn, protoConn)
|
||||
s.model.AddConnection(conn, protoConn)
|
||||
continue next
|
||||
}
|
||||
}
|
||||
|
||||
if !cfg.IgnoredDevice(remoteID) {
|
||||
if !s.cfg.IgnoredDevice(remoteID) {
|
||||
events.Default.Log(events.DeviceRejected, map[string]string{
|
||||
"device": remoteID.String(),
|
||||
"address": conn.RemoteAddr().String(),
|
||||
@@ -140,7 +201,7 @@ next:
|
||||
}
|
||||
}
|
||||
|
||||
func listenTLS(conns chan *tls.Conn, addr string, tlsCfg *tls.Config) {
|
||||
func (s *connectionSvc) listen(addr string) {
|
||||
if debugNet {
|
||||
l.Debugln("listening on", addr)
|
||||
}
|
||||
@@ -166,9 +227,9 @@ func listenTLS(conns chan *tls.Conn, addr string, tlsCfg *tls.Config) {
|
||||
}
|
||||
|
||||
tcpConn := conn.(*net.TCPConn)
|
||||
setTCPOptions(tcpConn)
|
||||
s.setTCPOptions(tcpConn)
|
||||
|
||||
tc := tls.Server(conn, tlsCfg)
|
||||
tc := tls.Server(conn, s.tlsCfg)
|
||||
err = tc.Handshake()
|
||||
if err != nil {
|
||||
l.Infoln("TLS handshake:", err)
|
||||
@@ -176,21 +237,20 @@ func listenTLS(conns chan *tls.Conn, addr string, tlsCfg *tls.Config) {
|
||||
continue
|
||||
}
|
||||
|
||||
conns <- tc
|
||||
s.conns <- tc
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func dialTLS(m *model.Model, conns chan *tls.Conn, tlsCfg *tls.Config) {
|
||||
func (s *connectionSvc) connect() {
|
||||
delay := time.Second
|
||||
for {
|
||||
nextDevice:
|
||||
for deviceID, deviceCfg := range cfg.Devices() {
|
||||
for deviceID, deviceCfg := range s.cfg.Devices() {
|
||||
if deviceID == myID {
|
||||
continue
|
||||
}
|
||||
|
||||
if m.ConnectedTo(deviceID) {
|
||||
if s.model.ConnectedTo(deviceID) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -238,9 +298,9 @@ func dialTLS(m *model.Model, conns chan *tls.Conn, tlsCfg *tls.Config) {
|
||||
continue
|
||||
}
|
||||
|
||||
setTCPOptions(conn)
|
||||
s.setTCPOptions(conn)
|
||||
|
||||
tc := tls.Client(conn, tlsCfg)
|
||||
tc := tls.Client(conn, s.tlsCfg)
|
||||
err = tc.Handshake()
|
||||
if err != nil {
|
||||
l.Infoln("TLS handshake:", err)
|
||||
@@ -248,20 +308,20 @@ func dialTLS(m *model.Model, conns chan *tls.Conn, tlsCfg *tls.Config) {
|
||||
continue
|
||||
}
|
||||
|
||||
conns <- tc
|
||||
s.conns <- tc
|
||||
continue nextDevice
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(delay)
|
||||
delay *= 2
|
||||
if maxD := time.Duration(cfg.Options().ReconnectIntervalS) * time.Second; delay > maxD {
|
||||
if maxD := time.Duration(s.cfg.Options().ReconnectIntervalS) * time.Second; delay > maxD {
|
||||
delay = maxD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func setTCPOptions(conn *net.TCPConn) {
|
||||
func (*connectionSvc) setTCPOptions(conn *net.TCPConn) {
|
||||
var err error
|
||||
if err = conn.SetLinger(0); err != nil {
|
||||
l.Infoln(err)
|
||||
@@ -277,8 +337,8 @@ func setTCPOptions(conn *net.TCPConn) {
|
||||
}
|
||||
}
|
||||
|
||||
func shouldLimit(addr net.Addr) bool {
|
||||
if cfg.Options().LimitBandwidthInLan {
|
||||
func (s *connectionSvc) shouldLimit(addr net.Addr) bool {
|
||||
if s.cfg.Options().LimitBandwidthInLan {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -12,5 +12,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
debugNet = strings.Contains(os.Getenv("STTRACE"), "net") || os.Getenv("STTRACE") == "all"
|
||||
debugNet = strings.Contains(os.Getenv("STTRACE"), "net") || os.Getenv("STTRACE") == "all"
|
||||
debugHTTP = strings.Contains(os.Getenv("STTRACE"), "http") || os.Getenv("STTRACE") == "all"
|
||||
)
|
||||
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/logger"
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
"github.com/syncthing/syncthing/internal/model"
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
"github.com/syncthing/syncthing/internal/upgrade"
|
||||
"github.com/vitrun/qart/qr"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -46,25 +47,33 @@ type guiError struct {
|
||||
var (
|
||||
configInSync = true
|
||||
guiErrors = []guiError{}
|
||||
guiErrorsMut sync.Mutex
|
||||
guiErrorsMut = sync.NewMutex()
|
||||
startTime = time.Now()
|
||||
eventSub *events.BufferedSubscription
|
||||
)
|
||||
|
||||
var (
|
||||
lastEventRequest time.Time
|
||||
lastEventRequestMut sync.Mutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
l.AddHandler(logger.LevelWarn, showGuiError)
|
||||
sub := events.Default.Subscribe(events.AllEvents)
|
||||
eventSub = events.NewBufferedSubscription(sub, 1000)
|
||||
type apiSvc struct {
|
||||
cfg config.GUIConfiguration
|
||||
assetDir string
|
||||
model *model.Model
|
||||
fss *folderSummarySvc
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
|
||||
var err error
|
||||
func newAPISvc(cfg config.GUIConfiguration, assetDir string, m *model.Model) (*apiSvc, error) {
|
||||
svc := &apiSvc{
|
||||
cfg: cfg,
|
||||
assetDir: assetDir,
|
||||
model: m,
|
||||
fss: newFolderSummarySvc(m),
|
||||
}
|
||||
|
||||
var err error
|
||||
svc.listener, err = svc.getListener()
|
||||
return svc, err
|
||||
}
|
||||
|
||||
func (s *apiSvc) getListener() (net.Listener, error) {
|
||||
cert, err := tls.LoadX509KeyPair(locations[locHTTPSCertFile], locations[locHTTPSKeyFile])
|
||||
if err != nil {
|
||||
l.Infoln("Loading HTTPS certificate:", err)
|
||||
@@ -81,7 +90,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
cert, err = newCertificate(locations[locHTTPSCertFile], locations[locHTTPSKeyFile], name)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
@@ -101,55 +110,64 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
},
|
||||
}
|
||||
|
||||
rawListener, err := net.Listen("tcp", cfg.Address)
|
||||
rawListener, err := net.Listen("tcp", s.cfg.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listener := &DowngradingListener{rawListener, tlsCfg}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func (s *apiSvc) Serve() {
|
||||
l.AddHandler(logger.LevelWarn, s.showGuiError)
|
||||
sub := events.Default.Subscribe(events.AllEvents)
|
||||
eventSub = events.NewBufferedSubscription(sub, 1000)
|
||||
defer events.Default.Unsubscribe(sub)
|
||||
|
||||
// The GET handlers
|
||||
getRestMux := http.NewServeMux()
|
||||
getRestMux.HandleFunc("/rest/db/completion", withModel(m, restGetDBCompletion)) // device folder
|
||||
getRestMux.HandleFunc("/rest/db/file", withModel(m, restGetDBFile)) // folder file [blocks]
|
||||
getRestMux.HandleFunc("/rest/db/ignores", withModel(m, restGetDBIgnores)) // folder
|
||||
getRestMux.HandleFunc("/rest/db/need", withModel(m, restGetDBNeed)) // folder
|
||||
getRestMux.HandleFunc("/rest/db/status", withModel(m, restGetDBStatus)) // folder
|
||||
getRestMux.HandleFunc("/rest/db/browse", withModel(m, restGetDBBrowse)) // folder [prefix] [dirsonly] [levels]
|
||||
getRestMux.HandleFunc("/rest/events", restGetEvents) // since [limit]
|
||||
getRestMux.HandleFunc("/rest/stats/device", withModel(m, restGetDeviceStats)) // -
|
||||
getRestMux.HandleFunc("/rest/stats/folder", withModel(m, restGetFolderStats)) // -
|
||||
getRestMux.HandleFunc("/rest/svc/deviceid", restGetDeviceID) // id
|
||||
getRestMux.HandleFunc("/rest/svc/lang", restGetLang) // -
|
||||
getRestMux.HandleFunc("/rest/svc/report", withModel(m, restGetReport)) // -
|
||||
getRestMux.HandleFunc("/rest/system/browse", restGetSystemBrowse) // current
|
||||
getRestMux.HandleFunc("/rest/system/config", restGetSystemConfig) // -
|
||||
getRestMux.HandleFunc("/rest/system/config/insync", RestGetSystemConfigInsync) // -
|
||||
getRestMux.HandleFunc("/rest/system/connections", withModel(m, restGetSystemConnections)) // -
|
||||
getRestMux.HandleFunc("/rest/system/discovery", restGetSystemDiscovery) // -
|
||||
getRestMux.HandleFunc("/rest/system/error", restGetSystemError) // -
|
||||
getRestMux.HandleFunc("/rest/system/ping", restPing) // -
|
||||
getRestMux.HandleFunc("/rest/system/status", restGetSystemStatus) // -
|
||||
getRestMux.HandleFunc("/rest/system/upgrade", restGetSystemUpgrade) // -
|
||||
getRestMux.HandleFunc("/rest/system/version", restGetSystemVersion) // -
|
||||
getRestMux.HandleFunc("/rest/db/completion", s.getDBCompletion) // device folder
|
||||
getRestMux.HandleFunc("/rest/db/file", s.getDBFile) // folder file
|
||||
getRestMux.HandleFunc("/rest/db/ignores", s.getDBIgnores) // folder
|
||||
getRestMux.HandleFunc("/rest/db/need", s.getDBNeed) // folder [perpage] [page]
|
||||
getRestMux.HandleFunc("/rest/db/status", s.getDBStatus) // folder
|
||||
getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
|
||||
getRestMux.HandleFunc("/rest/events", s.getEvents) // since [limit]
|
||||
getRestMux.HandleFunc("/rest/stats/device", s.getDeviceStats) // -
|
||||
getRestMux.HandleFunc("/rest/stats/folder", s.getFolderStats) // -
|
||||
getRestMux.HandleFunc("/rest/svc/deviceid", s.getDeviceID) // id
|
||||
getRestMux.HandleFunc("/rest/svc/lang", s.getLang) // -
|
||||
getRestMux.HandleFunc("/rest/svc/report", s.getReport) // -
|
||||
getRestMux.HandleFunc("/rest/system/browse", s.getSystemBrowse) // current
|
||||
getRestMux.HandleFunc("/rest/system/config", s.getSystemConfig) // -
|
||||
getRestMux.HandleFunc("/rest/system/config/insync", s.getSystemConfigInsync) // -
|
||||
getRestMux.HandleFunc("/rest/system/connections", s.getSystemConnections) // -
|
||||
getRestMux.HandleFunc("/rest/system/discovery", s.getSystemDiscovery) // -
|
||||
getRestMux.HandleFunc("/rest/system/error", s.getSystemError) // -
|
||||
getRestMux.HandleFunc("/rest/system/ping", s.restPing) // -
|
||||
getRestMux.HandleFunc("/rest/system/status", s.getSystemStatus) // -
|
||||
getRestMux.HandleFunc("/rest/system/upgrade", s.getSystemUpgrade) // -
|
||||
getRestMux.HandleFunc("/rest/system/version", s.getSystemVersion) // -
|
||||
|
||||
// The POST handlers
|
||||
postRestMux := http.NewServeMux()
|
||||
postRestMux.HandleFunc("/rest/db/prio", withModel(m, restPostDBPrio)) // folder file
|
||||
postRestMux.HandleFunc("/rest/db/ignores", withModel(m, restPostDBIgnores)) // folder
|
||||
postRestMux.HandleFunc("/rest/db/override", withModel(m, restPostDBOverride)) // folder
|
||||
postRestMux.HandleFunc("/rest/db/scan", withModel(m, restPostDBScan)) // folder [sub...]
|
||||
postRestMux.HandleFunc("/rest/system/config", withModel(m, restPostSystemConfig)) // <body>
|
||||
postRestMux.HandleFunc("/rest/system/discovery", restPostSystemDiscovery) // device addr
|
||||
postRestMux.HandleFunc("/rest/system/error", restPostSystemError) // <body>
|
||||
postRestMux.HandleFunc("/rest/system/error/clear", restPostSystemErrorClear) // -
|
||||
postRestMux.HandleFunc("/rest/system/ping", restPing) // -
|
||||
postRestMux.HandleFunc("/rest/system/reset", withModel(m, restPostSystemReset)) // [folder]
|
||||
postRestMux.HandleFunc("/rest/system/restart", restPostSystemRestart) // -
|
||||
postRestMux.HandleFunc("/rest/system/shutdown", restPostSystemShutdown) // -
|
||||
postRestMux.HandleFunc("/rest/system/upgrade", restPostSystemUpgrade) // -
|
||||
postRestMux.HandleFunc("/rest/db/prio", s.postDBPrio) // folder file [perpage] [page]
|
||||
postRestMux.HandleFunc("/rest/db/ignores", s.postDBIgnores) // folder
|
||||
postRestMux.HandleFunc("/rest/db/override", s.postDBOverride) // folder
|
||||
postRestMux.HandleFunc("/rest/db/scan", s.postDBScan) // folder [sub...]
|
||||
postRestMux.HandleFunc("/rest/system/config", s.postSystemConfig) // <body>
|
||||
postRestMux.HandleFunc("/rest/system/discovery", s.postSystemDiscovery) // device addr
|
||||
postRestMux.HandleFunc("/rest/system/error", s.postSystemError) // <body>
|
||||
postRestMux.HandleFunc("/rest/system/error/clear", s.postSystemErrorClear) // -
|
||||
postRestMux.HandleFunc("/rest/system/ping", s.restPing) // -
|
||||
postRestMux.HandleFunc("/rest/system/reset", s.postSystemReset) // [folder]
|
||||
postRestMux.HandleFunc("/rest/system/restart", s.postSystemRestart) // -
|
||||
postRestMux.HandleFunc("/rest/system/shutdown", s.postSystemShutdown) // -
|
||||
postRestMux.HandleFunc("/rest/system/upgrade", s.postSystemUpgrade) // -
|
||||
|
||||
// Debug endpoints, not for general use
|
||||
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
|
||||
getRestMux.HandleFunc("/rest/debug/peerCompletion", s.getPeerCompletion)
|
||||
|
||||
// A handler that splits requests between the two above and disables
|
||||
// caching
|
||||
@@ -158,43 +176,49 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
// The main routing handler
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/rest/", restMux)
|
||||
mux.HandleFunc("/qr/", getQR)
|
||||
mux.HandleFunc("/qr/", s.getQR)
|
||||
|
||||
// Serve compiled in assets unless an asset directory was set (for development)
|
||||
mux.Handle("/", embeddedStatic(assetDir))
|
||||
mux.Handle("/", embeddedStatic{
|
||||
assetDir: s.assetDir,
|
||||
assets: auto.Assets(),
|
||||
})
|
||||
|
||||
// Wrap everything in CSRF protection. The /rest prefix should be
|
||||
// protected, other requests will grant cookies.
|
||||
handler := csrfMiddleware("/rest", cfg.APIKey, mux)
|
||||
handler := csrfMiddleware("/rest", s.cfg.APIKey, mux)
|
||||
|
||||
// Add our version as a header to responses
|
||||
handler = withVersionMiddleware(handler)
|
||||
|
||||
// Wrap everything in basic auth, if user/password is set.
|
||||
if len(cfg.User) > 0 && len(cfg.Password) > 0 {
|
||||
handler = basicAuthAndSessionMiddleware(cfg, handler)
|
||||
if len(s.cfg.User) > 0 && len(s.cfg.Password) > 0 {
|
||||
handler = basicAuthAndSessionMiddleware(s.cfg, handler)
|
||||
}
|
||||
|
||||
// Redirect to HTTPS if we are supposed to
|
||||
if cfg.UseTLS {
|
||||
if s.cfg.UseTLS {
|
||||
handler = redirectToHTTPSMiddleware(handler)
|
||||
}
|
||||
|
||||
if debugHTTP {
|
||||
handler = debugMiddleware(handler)
|
||||
}
|
||||
|
||||
srv := http.Server{
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
csrv := &folderSummarySvc{model: m}
|
||||
go csrv.Serve()
|
||||
s.fss.ServeBackground()
|
||||
|
||||
go func() {
|
||||
err := srv.Serve(listener)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
err := srv.Serve(s.listener)
|
||||
l.Warnln("API:", err)
|
||||
}
|
||||
|
||||
func (s *apiSvc) Stop() {
|
||||
s.listener.Close()
|
||||
s.fss.Stop()
|
||||
}
|
||||
|
||||
func getPostHandler(get, post http.Handler) http.Handler {
|
||||
@@ -210,6 +234,30 @@ func getPostHandler(get, post http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func debugMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t0 := time.Now()
|
||||
h.ServeHTTP(w, r)
|
||||
ms := 1000 * time.Since(t0).Seconds()
|
||||
|
||||
// The variable `w` is most likely a *http.response, which we can't do
|
||||
// much with since it's a non exported type. We can however peek into
|
||||
// it with reflection to get at the status code and number of bytes
|
||||
// written.
|
||||
var status, written int64
|
||||
if rw := reflect.Indirect(reflect.ValueOf(w)); rw.IsValid() && rw.Kind() == reflect.Struct {
|
||||
if rf := rw.FieldByName("status"); rf.IsValid() && rf.Kind() == reflect.Int {
|
||||
status = rf.Int()
|
||||
}
|
||||
if rf := rw.FieldByName("written"); rf.IsValid() && rf.Kind() == reflect.Int64 {
|
||||
written = rf.Int()
|
||||
}
|
||||
}
|
||||
|
||||
l.Debugf("http: %s %q: status %d, %d bytes in %.02f ms", r.Method, r.URL.String(), status, written, ms)
|
||||
})
|
||||
}
|
||||
|
||||
func redirectToHTTPSMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Add a generous access-control-allow-origin header since we may be
|
||||
@@ -229,7 +277,9 @@ func redirectToHTTPSMiddleware(h http.Handler) http.Handler {
|
||||
|
||||
func noCacheMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store")
|
||||
w.Header().Set("Expires", time.Now().UTC().Format(http.TimeFormat))
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -241,20 +291,14 @@ func withVersionMiddleware(h http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func withModel(m *model.Model, h func(m *model.Model, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h(m, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func restPing(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) restPing(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"ping": "pong",
|
||||
})
|
||||
}
|
||||
|
||||
func restGetSystemVersion(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getSystemVersion(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"version": Version,
|
||||
@@ -264,7 +308,7 @@ func restGetSystemVersion(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func restGetDBBrowse(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getDBBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
prefix := qs.Get("prefix")
|
||||
@@ -277,12 +321,12 @@ func restGetDBBrowse(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
tree := m.GlobalDirectoryTree(folder, prefix, levels, dirsonly)
|
||||
tree := s.model.GlobalDirectoryTree(folder, prefix, levels, dirsonly)
|
||||
|
||||
json.NewEncoder(w).Encode(tree)
|
||||
}
|
||||
|
||||
func restGetDBCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getDBCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var folder = qs.Get("folder")
|
||||
var deviceStr = qs.Get("device")
|
||||
@@ -294,17 +338,17 @@ func restGetDBCompletion(m *model.Model, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
res := map[string]float64{
|
||||
"completion": m.Completion(device, folder),
|
||||
"completion": s.model.Completion(device, folder),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restGetDBStatus(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getDBStatus(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
res := folderSummary(m, folder)
|
||||
res := folderSummary(s.model, folder)
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
@@ -325,7 +369,12 @@ func folderSummary(m *model.Model, folder string) map[string]interface{} {
|
||||
|
||||
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
|
||||
|
||||
res["state"], res["stateChanged"] = m.State(folder)
|
||||
var err error
|
||||
res["state"], res["stateChanged"], err = m.State(folder)
|
||||
if err != nil {
|
||||
res["error"] = err.Error()
|
||||
}
|
||||
|
||||
res["version"] = m.CurrentLocalVersion(folder) + m.RemoteLocalVersion(folder)
|
||||
|
||||
ignorePatterns, _, _ := m.GetIgnores(folder)
|
||||
@@ -340,73 +389,81 @@ func folderSummary(m *model.Model, folder string) map[string]interface{} {
|
||||
return res
|
||||
}
|
||||
|
||||
func restPostDBOverride(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) postDBOverride(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var folder = qs.Get("folder")
|
||||
go m.Override(folder)
|
||||
go s.model.Override(folder)
|
||||
}
|
||||
|
||||
func restGetDBNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var folder = qs.Get("folder")
|
||||
func (s *apiSvc) getDBNeed(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
|
||||
folder := qs.Get("folder")
|
||||
|
||||
page, err := strconv.Atoi(qs.Get("page"))
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
perpage, err := strconv.Atoi(qs.Get("perpage"))
|
||||
if err != nil || perpage < 1 {
|
||||
perpage = 1 << 16
|
||||
}
|
||||
|
||||
progress, queued, rest, total := s.model.NeedFolderFiles(folder, page, perpage)
|
||||
|
||||
progress, queued, rest := m.NeedFolderFiles(folder, 100)
|
||||
// Convert the struct to a more loose structure, and inject the size.
|
||||
output := map[string][]map[string]interface{}{
|
||||
"progress": toNeedSlice(progress),
|
||||
"queued": toNeedSlice(queued),
|
||||
"rest": toNeedSlice(rest),
|
||||
output := map[string]interface{}{
|
||||
"progress": s.toNeedSlice(progress),
|
||||
"queued": s.toNeedSlice(queued),
|
||||
"rest": s.toNeedSlice(rest),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"perpage": perpage,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(output)
|
||||
}
|
||||
|
||||
func restGetSystemConnections(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var res = m.ConnectionStats()
|
||||
func (s *apiSvc) getSystemConnections(w http.ResponseWriter, r *http.Request) {
|
||||
var res = s.model.ConnectionStats()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restGetDeviceStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var res = m.DeviceStatistics()
|
||||
func (s *apiSvc) getDeviceStats(w http.ResponseWriter, r *http.Request) {
|
||||
var res = s.model.DeviceStatistics()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restGetFolderStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var res = m.FolderStatistics()
|
||||
func (s *apiSvc) getFolderStats(w http.ResponseWriter, r *http.Request) {
|
||||
var res = s.model.FolderStatistics()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restGetDBFile(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getDBFile(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
file := qs.Get("file")
|
||||
withBlocks := qs.Get("blocks") != ""
|
||||
gf, _ := m.CurrentGlobalFile(folder, file)
|
||||
lf, _ := m.CurrentFolderFile(folder, file)
|
||||
gf, _ := s.model.CurrentGlobalFile(folder, file)
|
||||
lf, _ := s.model.CurrentFolderFile(folder, file)
|
||||
|
||||
if !withBlocks {
|
||||
gf.Blocks = nil
|
||||
lf.Blocks = nil
|
||||
}
|
||||
|
||||
av := m.Availability(folder, file)
|
||||
av := s.model.Availability(folder, file)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"global": gf,
|
||||
"local": lf,
|
||||
"global": jsonFileInfo(gf),
|
||||
"local": jsonFileInfo(lf),
|
||||
"availability": av,
|
||||
})
|
||||
}
|
||||
|
||||
func restGetSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(cfg.Raw())
|
||||
}
|
||||
|
||||
func restPostSystemConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var newCfg config.Configuration
|
||||
err := json.NewDecoder(r.Body).Decode(&newCfg)
|
||||
if err != nil {
|
||||
@@ -434,11 +491,11 @@ func restPostSystemConfig(m *model.Model, w http.ResponseWriter, r *http.Request
|
||||
// UR was enabled
|
||||
newCfg.Options.URAccepted = usageReportVersion
|
||||
newCfg.Options.URUniqueID = randomString(8)
|
||||
err := sendUsageReport(m)
|
||||
err := sendUsageReport(s.model)
|
||||
if err != nil {
|
||||
l.Infoln("Usage report:", err)
|
||||
}
|
||||
go usageReportingLoop(m)
|
||||
go usageReportingLoop(s.model)
|
||||
} else if newCfg.Options.URAccepted < curAcc {
|
||||
// UR was disabled
|
||||
newCfg.Options.URAccepted = -1
|
||||
@@ -453,52 +510,52 @@ func restPostSystemConfig(m *model.Model, w http.ResponseWriter, r *http.Request
|
||||
cfg.Save()
|
||||
}
|
||||
|
||||
func RestGetSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getSystemConfigInsync(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync})
|
||||
}
|
||||
|
||||
func restPostSystemRestart(w http.ResponseWriter, r *http.Request) {
|
||||
flushResponse(`{"ok": "restarting"}`, w)
|
||||
func (s *apiSvc) postSystemRestart(w http.ResponseWriter, r *http.Request) {
|
||||
s.flushResponse(`{"ok": "restarting"}`, w)
|
||||
go restart()
|
||||
}
|
||||
|
||||
func restPostSystemReset(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) postSystemReset(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
var err error
|
||||
if len(folder) == 0 {
|
||||
err = resetDB()
|
||||
} else {
|
||||
err = m.ResetFolder(folder)
|
||||
err = s.model.ResetFolder(folder)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
if len(folder) == 0 {
|
||||
flushResponse(`{"ok": "resetting database"}`, w)
|
||||
s.flushResponse(`{"ok": "resetting database"}`, w)
|
||||
} else {
|
||||
flushResponse(`{"ok": "resetting folder " + folder}`, w)
|
||||
s.flushResponse(`{"ok": "resetting folder " + folder}`, w)
|
||||
}
|
||||
go restart()
|
||||
}
|
||||
|
||||
func restPostSystemShutdown(w http.ResponseWriter, r *http.Request) {
|
||||
flushResponse(`{"ok": "shutting down"}`, w)
|
||||
func (s *apiSvc) postSystemShutdown(w http.ResponseWriter, r *http.Request) {
|
||||
s.flushResponse(`{"ok": "shutting down"}`, w)
|
||||
go shutdown()
|
||||
}
|
||||
|
||||
func flushResponse(s string, w http.ResponseWriter) {
|
||||
w.Write([]byte(s + "\n"))
|
||||
func (s *apiSvc) flushResponse(resp string, w http.ResponseWriter) {
|
||||
w.Write([]byte(resp + "\n"))
|
||||
f := w.(http.Flusher)
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
var cpuUsagePercent [10]float64 // The last ten seconds
|
||||
var cpuUsageLock sync.RWMutex
|
||||
var cpuUsageLock = sync.NewRWMutex()
|
||||
|
||||
func restGetSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
@@ -526,26 +583,26 @@ func restGetSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restGetSystemError(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getSystemError(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
guiErrorsMut.Lock()
|
||||
json.NewEncoder(w).Encode(map[string][]guiError{"errors": guiErrors})
|
||||
guiErrorsMut.Unlock()
|
||||
}
|
||||
|
||||
func restPostSystemError(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) postSystemError(w http.ResponseWriter, r *http.Request) {
|
||||
bs, _ := ioutil.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
showGuiError(0, string(bs))
|
||||
s.showGuiError(0, string(bs))
|
||||
}
|
||||
|
||||
func restPostSystemErrorClear(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) postSystemErrorClear(w http.ResponseWriter, r *http.Request) {
|
||||
guiErrorsMut.Lock()
|
||||
guiErrors = []guiError{}
|
||||
guiErrorsMut.Unlock()
|
||||
}
|
||||
|
||||
func showGuiError(l logger.LogLevel, err string) {
|
||||
func (s *apiSvc) showGuiError(l logger.LogLevel, err string) {
|
||||
guiErrorsMut.Lock()
|
||||
guiErrors = append(guiErrors, guiError{time.Now(), err})
|
||||
if len(guiErrors) > 5 {
|
||||
@@ -554,7 +611,7 @@ func showGuiError(l logger.LogLevel, err string) {
|
||||
guiErrorsMut.Unlock()
|
||||
}
|
||||
|
||||
func restPostSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) postSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var device = qs.Get("device")
|
||||
var addr = qs.Get("addr")
|
||||
@@ -563,7 +620,7 @@ func restPostSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func restGetSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
devices := map[string][]discover.CacheEntry{}
|
||||
|
||||
@@ -579,16 +636,16 @@ func restGetSystemDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(devices)
|
||||
}
|
||||
|
||||
func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getReport(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(reportData(m))
|
||||
json.NewEncoder(w).Encode(reportData(s.model))
|
||||
}
|
||||
|
||||
func restGetDBIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getDBIgnores(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
ignores, patterns, err := m.GetIgnores(qs.Get("folder"))
|
||||
ignores, patterns, err := s.model.GetIgnores(qs.Get("folder"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
@@ -600,7 +657,7 @@ func restGetDBIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func restPostDBIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) postDBIgnores(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
|
||||
var data map[string][]string
|
||||
@@ -612,25 +669,23 @@ func restPostDBIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
err = m.SetIgnores(qs.Get("folder"), data["ignore"])
|
||||
err = s.model.SetIgnores(qs.Get("folder"), data["ignore"])
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
restGetDBIgnores(m, w, r)
|
||||
s.getDBIgnores(w, r)
|
||||
}
|
||||
|
||||
func restGetEvents(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getEvents(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
sinceStr := qs.Get("since")
|
||||
limitStr := qs.Get("limit")
|
||||
since, _ := strconv.Atoi(sinceStr)
|
||||
limit, _ := strconv.Atoi(limitStr)
|
||||
|
||||
lastEventRequestMut.Lock()
|
||||
lastEventRequest = time.Now()
|
||||
lastEventRequestMut.Unlock()
|
||||
s.fss.gotEventRequest()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
@@ -647,12 +702,12 @@ func restGetEvents(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(evs)
|
||||
}
|
||||
|
||||
func restGetSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
if noUpgrade {
|
||||
http.Error(w, upgrade.ErrUpgradeUnsupported.Error(), 500)
|
||||
return
|
||||
}
|
||||
rel, err := upgrade.LatestGithubRelease(Version)
|
||||
rel, err := upgrade.LatestRelease(Version)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
@@ -660,13 +715,14 @@ func restGetSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
res := make(map[string]interface{})
|
||||
res["running"] = Version
|
||||
res["latest"] = rel.Tag
|
||||
res["newer"] = upgrade.CompareVersions(rel.Tag, Version) == 1
|
||||
res["newer"] = upgrade.CompareVersions(rel.Tag, Version) == upgrade.Newer
|
||||
res["majorNewer"] = upgrade.CompareVersions(rel.Tag, Version) == upgrade.MajorNewer
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restGetDeviceID(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getDeviceID(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
idStr := qs.Get("id")
|
||||
id, err := protocol.DeviceIDFromString(idStr)
|
||||
@@ -682,7 +738,7 @@ func restGetDeviceID(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func restGetLang(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getLang(w http.ResponseWriter, r *http.Request) {
|
||||
lang := r.Header.Get("Accept-Language")
|
||||
var langs []string
|
||||
for _, l := range strings.Split(lang, ",") {
|
||||
@@ -693,15 +749,15 @@ func restGetLang(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(langs)
|
||||
}
|
||||
|
||||
func restPostSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
rel, err := upgrade.LatestGithubRelease(Version)
|
||||
func (s *apiSvc) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
rel, err := upgrade.LatestRelease(Version)
|
||||
if err != nil {
|
||||
l.Warnln("getting latest release:", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
if upgrade.CompareVersions(rel.Tag, Version) == 1 {
|
||||
if upgrade.CompareVersions(rel.Tag, Version) > upgrade.Equal {
|
||||
err = upgrade.To(rel)
|
||||
if err != nil {
|
||||
l.Warnln("upgrading:", err)
|
||||
@@ -709,23 +765,23 @@ func restPostSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
flushResponse(`{"ok": "restarting"}`, w)
|
||||
s.flushResponse(`{"ok": "restarting"}`, w)
|
||||
l.Infoln("Upgrading")
|
||||
stop <- exitUpgrading
|
||||
}
|
||||
}
|
||||
|
||||
func restPostDBScan(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) postDBScan(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
if folder != "" {
|
||||
subs := qs["sub"]
|
||||
err := m.ScanFolderSubs(folder, subs)
|
||||
err := s.model.ScanFolderSubs(folder, subs)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
} else {
|
||||
errors := m.ScanFolders()
|
||||
errors := s.model.ScanFolders()
|
||||
if len(errors) > 0 {
|
||||
http.Error(w, "Error scanning folders", 500)
|
||||
json.NewEncoder(w).Encode(errors)
|
||||
@@ -733,15 +789,15 @@ func restPostDBScan(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func restPostDBPrio(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) postDBPrio(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
file := qs.Get("file")
|
||||
m.BringToFront(folder, file)
|
||||
restGetDBNeed(m, w, r)
|
||||
s.model.BringToFront(folder, file)
|
||||
s.getDBNeed(w, r)
|
||||
}
|
||||
|
||||
func getQR(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getQR(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var text = qs.Get("text")
|
||||
code, err := qr.Encode(text, qr.M)
|
||||
@@ -754,15 +810,15 @@ func getQR(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(code.PNG())
|
||||
}
|
||||
|
||||
func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
tot := map[string]float64{}
|
||||
count := map[string]float64{}
|
||||
|
||||
for _, folder := range cfg.Folders() {
|
||||
for _, device := range folder.DeviceIDs() {
|
||||
deviceStr := device.String()
|
||||
if m.ConnectedTo(device) {
|
||||
tot[deviceStr] += m.Completion(device, folder.ID)
|
||||
if s.model.ConnectedTo(device) {
|
||||
tot[deviceStr] += s.model.Completion(device, folder.ID)
|
||||
} else {
|
||||
tot[deviceStr] = 0
|
||||
}
|
||||
@@ -779,7 +835,7 @@ func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Reques
|
||||
json.NewEncoder(w).Encode(comp)
|
||||
}
|
||||
|
||||
func restGetSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiSvc) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
qs := r.URL.Query()
|
||||
current := qs.Get("current")
|
||||
@@ -788,7 +844,7 @@ func restGetSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(current, pathSeparator) && !strings.HasSuffix(search, pathSeparator) {
|
||||
search = search + pathSeparator
|
||||
}
|
||||
subdirectories, _ := filepath.Glob(search + "*")
|
||||
subdirectories, _ := osutil.Glob(search + "*")
|
||||
ret := make([]string, 0, 10)
|
||||
for _, subdirectory := range subdirectories {
|
||||
info, err := os.Stat(subdirectory)
|
||||
@@ -802,56 +858,57 @@ func restGetSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(ret)
|
||||
}
|
||||
|
||||
func embeddedStatic(assetDir string) http.Handler {
|
||||
assets := auto.Assets()
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
file := r.URL.Path
|
||||
|
||||
if file[0] == '/' {
|
||||
file = file[1:]
|
||||
}
|
||||
|
||||
if len(file) == 0 {
|
||||
file = "index.html"
|
||||
}
|
||||
|
||||
if assetDir != "" {
|
||||
p := filepath.Join(assetDir, filepath.FromSlash(file))
|
||||
_, err := os.Stat(p)
|
||||
if err == nil {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bs, ok := assets[file]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
mtype := mimeTypeForFile(file)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
} else {
|
||||
// ungzip if browser not send gzip accepted header
|
||||
var gr *gzip.Reader
|
||||
gr, _ = gzip.NewReader(bytes.NewReader(bs))
|
||||
bs, _ = ioutil.ReadAll(gr)
|
||||
gr.Close()
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
||||
w.Header().Set("Last-Modified", auto.AssetsBuildDate)
|
||||
|
||||
w.Write(bs)
|
||||
})
|
||||
type embeddedStatic struct {
|
||||
assetDir string
|
||||
assets map[string][]byte
|
||||
}
|
||||
|
||||
func mimeTypeForFile(file string) string {
|
||||
func (s embeddedStatic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
file := r.URL.Path
|
||||
|
||||
if file[0] == '/' {
|
||||
file = file[1:]
|
||||
}
|
||||
|
||||
if len(file) == 0 {
|
||||
file = "index.html"
|
||||
}
|
||||
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, filepath.FromSlash(file))
|
||||
_, err := os.Stat(p)
|
||||
if err == nil {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
bs, ok := s.assets[file]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
mtype := s.mimeTypeForFile(file)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
} else {
|
||||
// ungzip if browser not send gzip accepted header
|
||||
var gr *gzip.Reader
|
||||
gr, _ = gzip.NewReader(bytes.NewReader(bs))
|
||||
bs, _ = ioutil.ReadAll(gr)
|
||||
gr.Close()
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
||||
w.Header().Set("Last-Modified", auto.AssetsBuildDate)
|
||||
|
||||
w.Write(bs)
|
||||
}
|
||||
|
||||
func (s embeddedStatic) mimeTypeForFile(file string) string {
|
||||
// We use a built in table of the common types since the system
|
||||
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||
// to the system.
|
||||
@@ -878,17 +935,49 @@ func mimeTypeForFile(file string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func toNeedSlice(fs []db.FileInfoTruncated) []map[string]interface{} {
|
||||
output := make([]map[string]interface{}, len(fs))
|
||||
for i, file := range fs {
|
||||
output[i] = map[string]interface{}{
|
||||
"name": file.Name,
|
||||
"flags": file.Flags,
|
||||
"modified": file.Modified,
|
||||
"version": file.Version,
|
||||
"localVersion": file.LocalVersion,
|
||||
"size": file.Size(),
|
||||
}
|
||||
func (s *apiSvc) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
|
||||
res := make([]jsonDBFileInfo, len(fs))
|
||||
for i, f := range fs {
|
||||
res[i] = jsonDBFileInfo(f)
|
||||
}
|
||||
return output
|
||||
return res
|
||||
}
|
||||
|
||||
// Type wrappers for nice JSON serialization
|
||||
|
||||
type jsonFileInfo protocol.FileInfo
|
||||
|
||||
func (f jsonFileInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"name": f.Name,
|
||||
"size": protocol.FileInfo(f).Size(),
|
||||
"flags": fmt.Sprintf("%#o", f.Flags),
|
||||
"modified": time.Unix(f.Modified, 0),
|
||||
"localVersion": f.LocalVersion,
|
||||
"numBlocks": len(f.Blocks),
|
||||
"version": jsonVersionVector(f.Version),
|
||||
})
|
||||
}
|
||||
|
||||
type jsonDBFileInfo db.FileInfoTruncated
|
||||
|
||||
func (f jsonDBFileInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"name": f.Name,
|
||||
"size": db.FileInfoTruncated(f).Size(),
|
||||
"flags": fmt.Sprintf("%#o", f.Flags),
|
||||
"modified": time.Unix(f.Modified, 0),
|
||||
"localVersion": f.LocalVersion,
|
||||
"version": jsonVersionVector(f.Version),
|
||||
})
|
||||
}
|
||||
|
||||
type jsonVersionVector protocol.Vector
|
||||
|
||||
func (v jsonVersionVector) MarshalJSON() ([]byte, error) {
|
||||
res := make([]string, len(v))
|
||||
for i, c := range v {
|
||||
res[i] = fmt.Sprintf("%d:%d", c.ID, c.Value)
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
|
||||
@@ -12,16 +12,16 @@ import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/config"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
sessions = make(map[string]bool)
|
||||
sessionsMut sync.Mutex
|
||||
sessionsMut = sync.NewMutex()
|
||||
)
|
||||
|
||||
func basicAuthAndSessionMiddleware(cfg config.GUIConfiguration, next http.Handler) http.Handler {
|
||||
@@ -42,6 +42,10 @@ func basicAuthAndSessionMiddleware(cfg config.GUIConfiguration, next http.Handle
|
||||
}
|
||||
}
|
||||
|
||||
if debugHTTP {
|
||||
l.Debugln("Sessionless HTTP request with authentication; this is expensive.")
|
||||
}
|
||||
|
||||
error := func() {
|
||||
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
|
||||
|
||||
@@ -12,14 +12,14 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
var csrfTokens []string
|
||||
var csrfMut sync.Mutex
|
||||
var csrfMut = sync.NewMutex()
|
||||
|
||||
// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
|
||||
// the request with 403. For / and /index.html, set a new CSRF cookie if none
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
)
|
||||
@@ -29,6 +30,7 @@ const (
|
||||
locLogFile = "logFile"
|
||||
locCsrfTokens = "csrfTokens"
|
||||
locPanicLog = "panicLog"
|
||||
locAuditLog = "auditLog"
|
||||
locDefFolder = "defFolder"
|
||||
)
|
||||
|
||||
@@ -48,7 +50,8 @@ var locations = map[locationEnum]string{
|
||||
locDatabase: "${config}/index-v0.11.0.db",
|
||||
locLogFile: "${config}/syncthing.log", // -logfile on Windows
|
||||
locCsrfTokens: "${config}/csrftokens.txt",
|
||||
locPanicLog: "${config}/panic-20060102-150405.log", // passed through time.Format()
|
||||
locPanicLog: "${config}/panic-${timestamp}.log",
|
||||
locAuditLog: "${config}/audit-${timestamp}.log",
|
||||
locDefFolder: "${home}/Sync",
|
||||
}
|
||||
|
||||
@@ -107,3 +110,14 @@ func homeDir() string {
|
||||
}
|
||||
return home
|
||||
}
|
||||
|
||||
func timestampedLoc(key locationEnum) string {
|
||||
// We take the roundtrip via "${timestamp}" instead of passing the path
|
||||
// directly through time.Format() to avoid issues when the path we are
|
||||
// expanding contains numbers; otherwise for example
|
||||
// /home/user2006/.../panic-20060102-150405.log would get both instances of
|
||||
// 2006 replaced by 2015...
|
||||
tpl := locations[key]
|
||||
now := time.Now().Format("20060102-150405")
|
||||
return strings.Replace(tpl, "${timestamp}", now, -1)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -35,10 +36,10 @@ import (
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
"github.com/syncthing/syncthing/internal/symlinks"
|
||||
"github.com/syncthing/syncthing/internal/upgrade"
|
||||
"github.com/syncthing/syncthing/internal/upnp"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/thejerf/suture"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -50,6 +51,7 @@ var (
|
||||
BuildHost = "unknown"
|
||||
BuildUser = "unknown"
|
||||
IsRelease bool
|
||||
IsBeta bool
|
||||
LongVersion string
|
||||
)
|
||||
|
||||
@@ -77,9 +79,15 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a clean release build.
|
||||
exp := regexp.MustCompile(`^v\d+\.\d+\.\d+(-beta[\d\.]+)?$`)
|
||||
// Check for a clean release build. A release is something like "v0.1.2",
|
||||
// with an optional suffix of letters and dot separated numbers like
|
||||
// "-beta3.47". If there's more stuff, like a plus sign and a commit hash
|
||||
// and so on, then it's not a release. If there's a dash anywhere in
|
||||
// there, it's some kind of beta or prerelease version.
|
||||
|
||||
exp := regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z]+[\d\.]+)?$`)
|
||||
IsRelease = exp.MatchString(Version)
|
||||
IsBeta = strings.Contains(Version, "-")
|
||||
|
||||
stamp, _ := strconv.Atoi(BuildStamp)
|
||||
BuildDate = time.Unix(int64(stamp), 0)
|
||||
@@ -101,8 +109,6 @@ var (
|
||||
readRateLimit *ratelimit.Bucket
|
||||
stop = make(chan int)
|
||||
discoverer *discover.Discoverer
|
||||
externalPort int
|
||||
igd *upnp.IGD
|
||||
cert tls.Certificate
|
||||
lans []*net.IPNet
|
||||
)
|
||||
@@ -143,6 +149,8 @@ are mostly useful for developers. Use with care.
|
||||
- "discover" (the discover package)
|
||||
- "events" (the events package)
|
||||
- "files" (the files package)
|
||||
- "http" (the main package; HTTP requests)
|
||||
- "locks" (the sync package; trace long held locks)
|
||||
- "net" (the main package; connections & network messages)
|
||||
- "model" (the model package)
|
||||
- "scanner" (the scanner package)
|
||||
@@ -186,6 +194,8 @@ var (
|
||||
noConsole bool
|
||||
generateDir string
|
||||
logFile string
|
||||
auditEnabled bool
|
||||
verbose bool
|
||||
noRestart = os.Getenv("STNORESTART") != ""
|
||||
noUpgrade = os.Getenv("STNOUPGRADE") != ""
|
||||
guiAddress = os.Getenv("STGUIADDRESS") // legacy
|
||||
@@ -222,6 +232,8 @@ func main() {
|
||||
flag.BoolVar(&doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
|
||||
flag.BoolVar(&showVersion, "version", false, "Show version")
|
||||
flag.StringVar(&upgradeTo, "upgrade-to", upgradeTo, "Force upgrade directly from specified URL")
|
||||
flag.BoolVar(&auditEnabled, "audit", false, "Write events to audit file")
|
||||
flag.BoolVar(&verbose, "verbose", false, "Print verbose log output")
|
||||
|
||||
flag.Usage = usageFor(flag.CommandLine, usage, fmt.Sprintf(extraUsage, baseDirs["config"]))
|
||||
flag.Parse()
|
||||
@@ -322,7 +334,7 @@ func main() {
|
||||
}
|
||||
|
||||
if doUpgrade || doUpgradeCheck {
|
||||
rel, err := upgrade.LatestGithubRelease(Version)
|
||||
rel, err := upgrade.LatestRelease(Version)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err) // exits 1
|
||||
}
|
||||
@@ -338,7 +350,13 @@ func main() {
|
||||
// Use leveldb database locks to protect against concurrent upgrades
|
||||
_, err = leveldb.OpenFile(locations[locDatabase], &opt.Options{OpenFilesCacheCapacity: 100})
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot upgrade, database seems to be locked. Is another copy of Syncthing already running?")
|
||||
l.Infoln("Attempting upgrade through running Syncthing...")
|
||||
err = upgradeViaRest()
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err)
|
||||
}
|
||||
l.Okln("Syncthing upgrading")
|
||||
return
|
||||
}
|
||||
|
||||
err = upgrade.To(rel)
|
||||
@@ -363,8 +381,65 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func upgradeViaRest() error {
|
||||
cfg, err := config.Load(locations[locConfigFile], protocol.LocalDeviceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
target := cfg.GUI().Address
|
||||
if cfg.GUI().UseTLS {
|
||||
target = "https://" + target
|
||||
} else {
|
||||
target = "http://" + target
|
||||
}
|
||||
r, _ := http.NewRequest("POST", target+"/rest/system/upgrade", nil)
|
||||
r.Header.Set("X-API-Key", cfg.GUI().APIKey)
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
resp, err := client.Do(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
bs, err := ioutil.ReadAll(resp.Body)
|
||||
defer resp.Body.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New(string(bs))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func syncthingMain() {
|
||||
var err error
|
||||
// Create a main service manager. We'll add things to this as we go along.
|
||||
// We want any logging it does to go through our log system, with INFO
|
||||
// severity.
|
||||
mainSvc := suture.New("main", suture.Spec{
|
||||
Log: func(line string) {
|
||||
l.Infoln(line)
|
||||
},
|
||||
})
|
||||
mainSvc.ServeBackground()
|
||||
|
||||
// Set a log prefix similar to the ID we will have later on, or early log
|
||||
// lines look ugly.
|
||||
l.SetPrefix("[start] ")
|
||||
|
||||
if auditEnabled {
|
||||
startAuditing(mainSvc)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
mainSvc.Add(newVerboseSvc())
|
||||
}
|
||||
|
||||
if len(os.Getenv("GOMAXPROCS")) == 0 {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
@@ -373,7 +448,7 @@ func syncthingMain() {
|
||||
events.Default.Log(events.Starting, map[string]string{"home": baseDirs["config"]})
|
||||
|
||||
// Ensure that that we have a certificate and key.
|
||||
cert, err = tls.LoadX509KeyPair(locations[locCertFile], locations[locKeyFile])
|
||||
cert, err := tls.LoadX509KeyPair(locations[locCertFile], locations[locKeyFile])
|
||||
if err != nil {
|
||||
cert, err = newCertificate(locations[locCertFile], locations[locKeyFile], tlsDefaultCommonName)
|
||||
if err != nil {
|
||||
@@ -432,6 +507,10 @@ func syncthingMain() {
|
||||
cfg.Save()
|
||||
}
|
||||
|
||||
if err := checkShortIDs(cfg); err != nil {
|
||||
l.Fatalln("Short device IDs are in conflict. Unlucky!\n Regenerate the device ID of one if the following:\n ", err)
|
||||
}
|
||||
|
||||
if len(profiler) > 0 {
|
||||
go func() {
|
||||
l.Debugln("Starting profiler on", profiler)
|
||||
@@ -489,10 +568,9 @@ func syncthingMain() {
|
||||
}
|
||||
|
||||
dbFile := locations[locDatabase]
|
||||
dbOpts := &opt.Options{OpenFilesCacheCapacity: 100}
|
||||
ldb, err := leveldb.OpenFile(dbFile, dbOpts)
|
||||
ldb, err := leveldb.OpenFile(dbFile, dbOpts())
|
||||
if err != nil && errors.IsCorrupted(err) {
|
||||
ldb, err = leveldb.RecoverFile(dbFile, dbOpts)
|
||||
ldb, err = leveldb.RecoverFile(dbFile, dbOpts())
|
||||
}
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
|
||||
@@ -509,9 +587,18 @@ func syncthingMain() {
|
||||
|
||||
m := model.NewModel(cfg, myID, myName, "syncthing", Version, ldb)
|
||||
|
||||
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
|
||||
it, err := strconv.Atoi(t)
|
||||
if err == nil {
|
||||
m.StartDeadlockDetector(time.Duration(it) * time.Second)
|
||||
}
|
||||
} else if !IsRelease || IsBeta {
|
||||
m.StartDeadlockDetector(20 * 60 * time.Second)
|
||||
}
|
||||
|
||||
// GUI
|
||||
|
||||
setupGUI(cfg, m)
|
||||
setupGUI(mainSvc, cfg, m)
|
||||
|
||||
// Clear out old indexes for other devices. Otherwise we'll start up and
|
||||
// start needing a bunch of files which are nowhere to be found. This
|
||||
@@ -532,18 +619,22 @@ func syncthingMain() {
|
||||
if err != nil {
|
||||
l.Fatalln("Bad listen address:", err)
|
||||
}
|
||||
externalPort = addr.Port
|
||||
|
||||
// UPnP
|
||||
igd = nil
|
||||
// Start discovery
|
||||
|
||||
localPort := addr.Port
|
||||
discoverer = discovery(localPort)
|
||||
|
||||
// Start UPnP. The UPnP service will restart global discovery if the
|
||||
// external port changes.
|
||||
|
||||
if opts.UPnPEnabled {
|
||||
setupUPnP()
|
||||
upnpSvc := newUPnPSvc(cfg, localPort)
|
||||
mainSvc.Add(upnpSvc)
|
||||
}
|
||||
|
||||
// Routine to connect out to configured devices
|
||||
discoverer = discovery(externalPort)
|
||||
go listenConnect(myID, m, tlsCfg)
|
||||
connectionSvc := newConnectionSvc(cfg, myID, m, tlsCfg)
|
||||
mainSvc.Add(connectionSvc)
|
||||
|
||||
for _, folder := range cfg.Folders() {
|
||||
// Routine to pull blocks from other devices to synchronize the local
|
||||
@@ -617,11 +708,61 @@ func syncthingMain() {
|
||||
|
||||
code := <-stop
|
||||
|
||||
mainSvc.Stop()
|
||||
|
||||
l.Okln("Exiting")
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func setupGUI(cfg *config.Wrapper, m *model.Model) {
|
||||
func dbOpts() *opt.Options {
|
||||
// Calculate a sutiable database block cache capacity. We start at the
|
||||
// default of 8 MiB and use larger values for machines with more memory.
|
||||
// In reality, the database will use twice the amount we calculate here,
|
||||
// as it also has two write buffers each sized at half the block cache.
|
||||
|
||||
blockCacheCapacity := 8 << 20
|
||||
if bytes, err := memorySize(); err == nil {
|
||||
if bytes > 74<<30 {
|
||||
// At 74 GiB of RAM, we hit a 256 MiB block cache (per the
|
||||
// calculations below). There's probably no point in growing the
|
||||
// cache beyond this point.
|
||||
blockCacheCapacity = 256 << 20
|
||||
} else if bytes > 8<<30 {
|
||||
// Slowly grow from 128 MiB at 8 GiB of RAM up to 256 MiB for a
|
||||
// ~74 GiB RAM machine
|
||||
blockCacheCapacity = int(bytes/512) + 128 - 16
|
||||
} else if bytes > 512<<20 {
|
||||
// Grow from 8 MiB at start to 128 MiB of cache at 8 GiB of RAM.
|
||||
blockCacheCapacity = int(bytes / 64)
|
||||
}
|
||||
l.Infoln("Database block cache capacity", blockCacheCapacity/1024, "KiB")
|
||||
}
|
||||
|
||||
return &opt.Options{
|
||||
OpenFilesCacheCapacity: 100,
|
||||
BlockCacheCapacity: blockCacheCapacity,
|
||||
WriteBuffer: blockCacheCapacity / 2,
|
||||
}
|
||||
}
|
||||
|
||||
func startAuditing(mainSvc *suture.Supervisor) {
|
||||
auditFile := timestampedLoc(locAuditLog)
|
||||
fd, err := os.OpenFile(auditFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
l.Fatalln("Audit:", err)
|
||||
}
|
||||
|
||||
auditSvc := newAuditSvc(fd)
|
||||
mainSvc.Add(auditSvc)
|
||||
|
||||
// We wait for the audit service to fully start before we return, to
|
||||
// ensure we capture all events from the start.
|
||||
auditSvc.WaitForStart()
|
||||
|
||||
l.Infoln("Audit log in", auditFile)
|
||||
}
|
||||
|
||||
func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model) {
|
||||
opts := cfg.Options()
|
||||
guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey)
|
||||
|
||||
@@ -650,10 +791,12 @@ func setupGUI(cfg *config.Wrapper, m *model.Model) {
|
||||
|
||||
urlShow := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
|
||||
l.Infoln("Starting web GUI on", urlShow)
|
||||
err := startGUI(guiCfg, guiAssets, m)
|
||||
api, err := newAPISvc(guiCfg, guiAssets, m)
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot start GUI:", err)
|
||||
}
|
||||
mainSvc.Add(api)
|
||||
|
||||
if opts.StartBrowser && !noBrowser && !stRestarting {
|
||||
urlOpen := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostOpen, strconv.Itoa(addr.Port)))
|
||||
// Can potentially block if the utility we are invoking doesn't
|
||||
@@ -669,7 +812,7 @@ func defaultConfig(myName string) config.Configuration {
|
||||
newCfg.Folders = []config.FolderConfiguration{
|
||||
{
|
||||
ID: "default",
|
||||
Path: locations[locDefFolder],
|
||||
RawPath: locations[locDefFolder],
|
||||
RescanIntervalS: 60,
|
||||
Devices: []config.FolderDeviceConfiguration{{DeviceID: myID}},
|
||||
},
|
||||
@@ -703,108 +846,6 @@ func generatePingEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
func setupUPnP() {
|
||||
if opts := cfg.Options(); len(opts.ListenAddress) == 1 {
|
||||
_, portStr, err := net.SplitHostPort(opts.ListenAddress[0])
|
||||
if err != nil {
|
||||
l.Warnln("Bad listen address:", err)
|
||||
} else {
|
||||
// Set up incoming port forwarding, if necessary and possible
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
igds := upnp.Discover()
|
||||
if len(igds) > 0 {
|
||||
// Configure the first discovered IGD only. This is a work-around until we have a better mechanism
|
||||
// for handling multiple IGDs, which will require changes to the global discovery service
|
||||
igd = &igds[0]
|
||||
|
||||
externalPort = setupExternalPort(igd, port)
|
||||
if externalPort == 0 {
|
||||
l.Warnln("Failed to create UPnP port mapping")
|
||||
} else {
|
||||
l.Infof("Created UPnP port mapping for external port %d on UPnP device %s.", externalPort, igd.FriendlyIdentifier())
|
||||
|
||||
if opts.UPnPRenewal > 0 {
|
||||
go renewUPnP(port)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
l.Warnln("Multiple listening addresses; not attempting UPnP port mapping")
|
||||
}
|
||||
}
|
||||
|
||||
func setupExternalPort(igd *upnp.IGD, port int) int {
|
||||
if igd == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
r := 1024 + predictableRandom.Intn(65535-1024)
|
||||
err := igd.AddPortMapping(upnp.TCP, r, port, fmt.Sprintf("syncthing-%d", r), cfg.Options().UPnPLease*60)
|
||||
if err == nil {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func renewUPnP(port int) {
|
||||
for {
|
||||
opts := cfg.Options()
|
||||
time.Sleep(time.Duration(opts.UPnPRenewal) * time.Minute)
|
||||
|
||||
// Make sure our IGD reference isn't nil
|
||||
if igd == nil {
|
||||
if debugNet {
|
||||
l.Debugln("Undefined IGD during UPnP port renewal. Re-discovering...")
|
||||
}
|
||||
igds := upnp.Discover()
|
||||
if len(igds) > 0 {
|
||||
// Configure the first discovered IGD only. This is a work-around until we have a better mechanism
|
||||
// for handling multiple IGDs, which will require changes to the global discovery service
|
||||
igd = &igds[0]
|
||||
} else {
|
||||
if debugNet {
|
||||
l.Debugln("Failed to discover IGD during UPnP port mapping renewal.")
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Just renew the same port that we already have
|
||||
if externalPort != 0 {
|
||||
err := igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", opts.UPnPLease*60)
|
||||
if err != nil {
|
||||
l.Warnf("Error renewing UPnP port mapping for external port %d on device %s: %s", externalPort, igd.FriendlyIdentifier(), err.Error())
|
||||
} else if debugNet {
|
||||
l.Debugf("Renewed UPnP port mapping for external port %d on device %s.", externalPort, igd.FriendlyIdentifier())
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Something strange has happened. We didn't have an external port before?
|
||||
// Or perhaps the gateway has changed?
|
||||
// Retry the same port sequence from the beginning.
|
||||
if debugNet {
|
||||
l.Debugln("No UPnP port mapping defined, updating...")
|
||||
}
|
||||
|
||||
forwardedPort := setupExternalPort(igd, port)
|
||||
if forwardedPort != 0 {
|
||||
externalPort = forwardedPort
|
||||
discoverer.StopGlobal()
|
||||
discoverer.StartGlobal(opts.GlobalAnnServers, uint16(forwardedPort))
|
||||
if debugNet {
|
||||
l.Debugf("Updated UPnP port mapping for external port %d on device %s.", forwardedPort, igd.FriendlyIdentifier())
|
||||
}
|
||||
} else {
|
||||
l.Warnf("Failed to update UPnP port mapping for external port on device " + igd.FriendlyIdentifier() + ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resetDB() error {
|
||||
return os.RemoveAll(locations[locDatabase])
|
||||
}
|
||||
@@ -949,7 +990,7 @@ func autoUpgrade() {
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
rel, err := upgrade.LatestGithubRelease(Version)
|
||||
rel, err := upgrade.LatestRelease(Version)
|
||||
if err == upgrade.ErrUpgradeUnsupported {
|
||||
events.Default.Unsubscribe(sub)
|
||||
return
|
||||
@@ -988,6 +1029,7 @@ func autoUpgrade() {
|
||||
func cleanConfigDirectory() {
|
||||
patterns := map[string]time.Duration{
|
||||
"panic-*.log": 7 * 24 * time.Hour, // keep panic logs for a week
|
||||
"audit-*.log": 7 * 24 * time.Hour, // keep audit logs for a week
|
||||
"index": 14 * 24 * time.Hour, // keep old index format for two weeks
|
||||
"config.xml.v*": 30 * 24 * time.Hour, // old config versions for a month
|
||||
"*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist
|
||||
@@ -996,14 +1038,14 @@ func cleanConfigDirectory() {
|
||||
|
||||
for pat, dur := range patterns {
|
||||
pat = filepath.Join(baseDirs["config"], pat)
|
||||
files, err := filepath.Glob(pat)
|
||||
files, err := osutil.Glob(pat)
|
||||
if err != nil {
|
||||
l.Infoln("Cleaning:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
info, err := os.Lstat(file)
|
||||
info, err := osutil.Lstat(file)
|
||||
if err != nil {
|
||||
l.Infoln("Cleaning:", err)
|
||||
continue
|
||||
@@ -1019,3 +1061,18 @@ func cleanConfigDirectory() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkShortIDs verifies that the configuration won't result in duplicate
|
||||
// short ID:s; that is, that the devices in the cluster all have unique
|
||||
// initial 64 bits.
|
||||
func checkShortIDs(cfg *config.Wrapper) error {
|
||||
exists := make(map[uint64]protocol.DeviceID)
|
||||
for deviceID := range cfg.Devices() {
|
||||
shortID := deviceID.Short()
|
||||
if otherID, ok := exists[shortID]; ok {
|
||||
return fmt.Errorf("%v in conflict with %v", deviceID, otherID)
|
||||
}
|
||||
exists[shortID] = deviceID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,19 +20,21 @@ import (
|
||||
)
|
||||
|
||||
func TestFolderErrors(t *testing.T) {
|
||||
// This test intentionally avoids starting the folders. If they are
|
||||
// started, they will perform an initial scan, which will create missing
|
||||
// folder markers and race with the stuff we do in the test.
|
||||
|
||||
fcfg := config.FolderConfiguration{
|
||||
ID: "folder",
|
||||
Path: "testdata/testfolder",
|
||||
ID: "folder",
|
||||
RawPath: "testdata/testfolder",
|
||||
}
|
||||
cfg := config.Wrap("/tmp/test", config.Configuration{
|
||||
Folders: []config.FolderConfiguration{fcfg},
|
||||
})
|
||||
|
||||
for _, file := range []string{".stfolder", "testfolder/.stfolder", "testfolder"} {
|
||||
os.Remove("testdata/" + file)
|
||||
_, err := os.Stat("testdata/" + file)
|
||||
if err == nil {
|
||||
t.Error("Found unexpected file")
|
||||
if err := os.Remove("testdata/" + file); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +59,16 @@ func TestFolderErrors(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
os.Remove("testdata/testfolder/.stfolder")
|
||||
os.Remove("testdata/testfolder/")
|
||||
if err := os.Remove("testdata/testfolder/.stfolder"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Remove("testdata/testfolder/"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Case 2 - new folder, marker created
|
||||
|
||||
fcfg.Path = "testdata/"
|
||||
fcfg.RawPath = "testdata/"
|
||||
cfg = config.Wrap("/tmp/test", config.Configuration{
|
||||
Folders: []config.FolderConfiguration{fcfg},
|
||||
})
|
||||
@@ -79,7 +85,9 @@ func TestFolderErrors(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
os.Remove("testdata/.stfolder")
|
||||
if err := os.Remove("testdata/.stfolder"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Case 3 - Folder marker missing
|
||||
|
||||
@@ -91,7 +99,7 @@ func TestFolderErrors(t *testing.T) {
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "Folder marker missing" {
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder marker missing" {
|
||||
t.Error("Incorrect error: Folder marker missing !=", m.CheckFolderHealth("folder"))
|
||||
}
|
||||
|
||||
@@ -107,10 +115,14 @@ func TestFolderErrors(t *testing.T) {
|
||||
|
||||
// Case 4 - Folder path missing
|
||||
|
||||
os.Remove("testdata/testfolder/.stfolder")
|
||||
os.Remove("testdata/testfolder/")
|
||||
if err := os.Remove("testdata/testfolder/.stfolder"); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Remove("testdata/testfolder"); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
fcfg.Path = "testdata/testfolder"
|
||||
fcfg.RawPath = "testdata/testfolder"
|
||||
cfg = config.Wrap("testdata/subfolder", config.Configuration{
|
||||
Folders: []config.FolderConfiguration{fcfg},
|
||||
})
|
||||
@@ -118,15 +130,17 @@ func TestFolderErrors(t *testing.T) {
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m.AddFolder(fcfg)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "Folder path missing" {
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder path missing" {
|
||||
t.Error("Incorrect error: Folder path missing !=", m.CheckFolderHealth("folder"))
|
||||
}
|
||||
|
||||
// Case 4.1 - recover after folder path missing
|
||||
|
||||
os.Mkdir("testdata/testfolder", 0700)
|
||||
if err := os.Mkdir("testdata/testfolder", 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "Folder marker missing" {
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder marker missing" {
|
||||
t.Error("Incorrect error: Folder marker missing !=", m.CheckFolderHealth("folder"))
|
||||
}
|
||||
|
||||
@@ -140,3 +154,27 @@ func TestFolderErrors(t *testing.T) {
|
||||
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortIDCheck(t *testing.T) {
|
||||
cfg := config.Wrap("/tmp/test", config.Configuration{
|
||||
Devices: []config.DeviceConfiguration{
|
||||
{DeviceID: protocol.DeviceID{8, 16, 24, 32, 40, 48, 56, 0, 0}},
|
||||
{DeviceID: protocol.DeviceID{8, 16, 24, 32, 40, 48, 56, 1, 1}}, // first 56 bits same, differ in the first 64 bits
|
||||
},
|
||||
})
|
||||
|
||||
if err := checkShortIDs(cfg); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
}
|
||||
|
||||
cfg = config.Wrap("/tmp/test", config.Configuration{
|
||||
Devices: []config.DeviceConfiguration{
|
||||
{DeviceID: protocol.DeviceID{8, 16, 24, 32, 40, 48, 56, 64, 0}},
|
||||
{DeviceID: protocol.DeviceID{8, 16, 24, 32, 40, 48, 56, 64, 1}}, // first 64 bits same
|
||||
},
|
||||
})
|
||||
|
||||
if err := checkShortIDs(cfg); err == nil {
|
||||
t.Error("Should have gotten an error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,17 +14,17 @@ import (
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
var (
|
||||
stdoutFirstLines []string // The first 10 lines of stdout
|
||||
stdoutLastLines []string // The last 50 lines of stdout
|
||||
stdoutMut sync.Mutex
|
||||
stdoutMut = sync.NewMutex()
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -124,7 +124,7 @@ func monitorMain() {
|
||||
|
||||
case err = <-exit:
|
||||
if err == nil {
|
||||
// Successfull exit indicates an intentional shutdown
|
||||
// Successful exit indicates an intentional shutdown
|
||||
return
|
||||
} else if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||
@@ -163,7 +163,7 @@ func copyStderr(stderr io.ReadCloser, dst io.Writer) {
|
||||
dst.Write([]byte(line))
|
||||
|
||||
if strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") {
|
||||
panicFd, err = os.Create(time.Now().Format(locations[locPanicLog]))
|
||||
panicFd, err = os.Create(timestampedLoc(locPanicLog))
|
||||
if err != nil {
|
||||
l.Warnln("Create panic log:", err)
|
||||
continue
|
||||
|
||||
@@ -7,45 +7,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
"github.com/syncthing/syncthing/internal/model"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
// The folderSummarySvc adds summary information events (FolderSummary and
|
||||
// FolderCompletion) into the event stream at certain intervals.
|
||||
type folderSummarySvc struct {
|
||||
*suture.Supervisor
|
||||
|
||||
model *model.Model
|
||||
srv suture.Service
|
||||
stop chan struct{}
|
||||
immediate chan string
|
||||
|
||||
// For keeping track of folders to recalculate for
|
||||
foldersMut sync.Mutex
|
||||
folders map[string]struct{}
|
||||
|
||||
// For keeping track of when the last event request on the API was
|
||||
lastEventReq time.Time
|
||||
lastEventReqMut sync.Mutex
|
||||
}
|
||||
|
||||
func (c *folderSummarySvc) Serve() {
|
||||
srv := suture.NewSimple("folderSummarySvc")
|
||||
srv.Add(serviceFunc(c.listenForUpdates))
|
||||
srv.Add(serviceFunc(c.calculateSummaries))
|
||||
func newFolderSummarySvc(m *model.Model) *folderSummarySvc {
|
||||
svc := &folderSummarySvc{
|
||||
Supervisor: suture.NewSimple("folderSummarySvc"),
|
||||
model: m,
|
||||
stop: make(chan struct{}),
|
||||
immediate: make(chan string),
|
||||
folders: make(map[string]struct{}),
|
||||
foldersMut: sync.NewMutex(),
|
||||
lastEventReqMut: sync.NewMutex(),
|
||||
}
|
||||
|
||||
c.immediate = make(chan string)
|
||||
c.stop = make(chan struct{})
|
||||
c.folders = make(map[string]struct{})
|
||||
c.srv = srv
|
||||
svc.Add(serviceFunc(svc.listenForUpdates))
|
||||
svc.Add(serviceFunc(svc.calculateSummaries))
|
||||
|
||||
srv.Serve()
|
||||
return svc
|
||||
}
|
||||
|
||||
func (c *folderSummarySvc) Stop() {
|
||||
// c.srv.Stop() is mostly a no-op here, but we need to call it anyway so
|
||||
// c.srv doesn't try to restart the serviceFuncs when they exit after we
|
||||
// close the stop channel.
|
||||
c.srv.Stop()
|
||||
c.Supervisor.Stop()
|
||||
close(c.stop)
|
||||
}
|
||||
|
||||
@@ -66,21 +72,25 @@ func (c *folderSummarySvc) listenForUpdates() {
|
||||
data := ev.Data.(map[string]interface{})
|
||||
folder := data["folder"].(string)
|
||||
|
||||
if ev.Type == events.StateChanged && data["to"].(string) == "idle" && data["from"].(string) == "syncing" {
|
||||
// The folder changed to idle from syncing. We should do an
|
||||
// immediate refresh to update the GUI. The send to
|
||||
// c.immediate must be nonblocking so that we can continue
|
||||
// handling events.
|
||||
switch ev.Type {
|
||||
case events.StateChanged:
|
||||
if data["to"].(string) == "idle" && data["from"].(string) == "syncing" {
|
||||
// The folder changed to idle from syncing. We should do an
|
||||
// immediate refresh to update the GUI. The send to
|
||||
// c.immediate must be nonblocking so that we can continue
|
||||
// handling events.
|
||||
|
||||
select {
|
||||
case c.immediate <- folder:
|
||||
c.foldersMut.Lock()
|
||||
delete(c.folders, folder)
|
||||
c.foldersMut.Unlock()
|
||||
select {
|
||||
case c.immediate <- folder:
|
||||
c.foldersMut.Lock()
|
||||
delete(c.folders, folder)
|
||||
c.foldersMut.Unlock()
|
||||
|
||||
default:
|
||||
default:
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
default:
|
||||
// This folder needs to be refreshed whenever we do the next
|
||||
// refresh.
|
||||
|
||||
@@ -127,16 +137,13 @@ func (c *folderSummarySvc) calculateSummaries() {
|
||||
// foldersToHandle returns the list of folders needing a summary update, and
|
||||
// clears the list.
|
||||
func (c *folderSummarySvc) foldersToHandle() []string {
|
||||
// We only recalculate sumamries if someone is listening to events
|
||||
// We only recalculate summaries if someone is listening to events
|
||||
// (a request to /rest/events has been made within the last
|
||||
// pingEventInterval).
|
||||
|
||||
lastEventRequestMut.Lock()
|
||||
// XXX: Reaching out to a global var here is very ugly :( Should
|
||||
// we make the gui stuff a proper object with methods on it that
|
||||
// we can query about this kind of thing?
|
||||
last := lastEventRequest
|
||||
lastEventRequestMut.Unlock()
|
||||
c.lastEventReqMut.Lock()
|
||||
last := c.lastEventReq
|
||||
c.lastEventReqMut.Unlock()
|
||||
if time.Since(last) > pingEventInterval {
|
||||
return nil
|
||||
}
|
||||
@@ -182,6 +189,12 @@ func (c *folderSummarySvc) sendSummary(folder string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *folderSummarySvc) gotEventRequest() {
|
||||
c.lastEventReqMut.Lock()
|
||||
c.lastEventReq = time.Now()
|
||||
c.lastEventReqMut.Unlock()
|
||||
}
|
||||
|
||||
// serviceFunc wraps a function to create a suture.Service without stop
|
||||
// functionality.
|
||||
type serviceFunc func()
|
||||
|
||||
111
cmd/syncthing/upnpsvc.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/config"
|
||||
"github.com/syncthing/syncthing/internal/upnp"
|
||||
)
|
||||
|
||||
// The UPnP service runs a loop for discovery of IGDs (Internet Gateway
|
||||
// Devices) and setup/renewal of a port mapping.
|
||||
type upnpSvc struct {
|
||||
cfg *config.Wrapper
|
||||
localPort int
|
||||
}
|
||||
|
||||
func newUPnPSvc(cfg *config.Wrapper, localPort int) *upnpSvc {
|
||||
return &upnpSvc{
|
||||
cfg: cfg,
|
||||
localPort: localPort,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *upnpSvc) Serve() {
|
||||
extPort := 0
|
||||
foundIGD := true
|
||||
|
||||
for {
|
||||
igds := upnp.Discover(time.Duration(s.cfg.Options().UPnPTimeoutS) * time.Second)
|
||||
if len(igds) > 0 {
|
||||
foundIGD = true
|
||||
extPort = s.tryIGDs(igds, extPort)
|
||||
} else if foundIGD {
|
||||
// Only print a notice if we've previously found an IGD or this is
|
||||
// the first time around.
|
||||
foundIGD = false
|
||||
l.Infof("No UPnP device detected")
|
||||
}
|
||||
|
||||
d := time.Duration(s.cfg.Options().UPnPRenewalM) * time.Minute
|
||||
if d == 0 {
|
||||
// We always want to do renewal so lets just pick a nice sane number.
|
||||
d = 30 * time.Minute
|
||||
}
|
||||
time.Sleep(d)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *upnpSvc) Stop() {
|
||||
panic("upnpSvc cannot stop")
|
||||
}
|
||||
|
||||
func (s *upnpSvc) tryIGDs(igds []upnp.IGD, prevExtPort int) int {
|
||||
// Lets try all the IGDs we found and use the first one that works.
|
||||
// TODO: Use all of them, and sort out the resulting mess to the
|
||||
// discovery announcement code...
|
||||
for _, igd := range igds {
|
||||
extPort, err := s.tryIGD(igd, prevExtPort)
|
||||
if err != nil {
|
||||
l.Warnf("Failed to set UPnP port mapping: external port %d on device %s.", extPort, igd.FriendlyIdentifier())
|
||||
continue
|
||||
}
|
||||
|
||||
if extPort != prevExtPort {
|
||||
// External port changed; refresh the discovery announcement.
|
||||
// TODO: Don't reach out to some magic global here?
|
||||
l.Infof("New UPnP port mapping: external port %d to local port %d.", extPort, s.localPort)
|
||||
discoverer.StopGlobal()
|
||||
discoverer.StartGlobal(s.cfg.Options().GlobalAnnServers, uint16(extPort))
|
||||
}
|
||||
if debugNet {
|
||||
l.Debugf("Created/updated UPnP port mapping for external port %d on device %s.", extPort, igd.FriendlyIdentifier())
|
||||
}
|
||||
return extPort
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *upnpSvc) tryIGD(igd upnp.IGD, suggestedPort int) (int, error) {
|
||||
var err error
|
||||
leaseTime := s.cfg.Options().UPnPLeaseM * 60
|
||||
|
||||
if suggestedPort != 0 {
|
||||
// First try renewing our existing mapping.
|
||||
name := fmt.Sprintf("syncthing-%d", suggestedPort)
|
||||
err = igd.AddPortMapping(upnp.TCP, suggestedPort, s.localPort, name, leaseTime)
|
||||
if err == nil {
|
||||
return suggestedPort, nil
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// Then try up to ten random ports.
|
||||
extPort := 1024 + predictableRandom.Intn(65535-1024)
|
||||
name := fmt.Sprintf("syncthing-%d", suggestedPort)
|
||||
err = igd.AddPortMapping(upnp.TCP, extPort, s.localPort, name, leaseTime)
|
||||
if err == nil {
|
||||
return extPort, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
126
cmd/syncthing/verbose.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
)
|
||||
|
||||
// The verbose logging service subscribes to events and prints these in
|
||||
// verbose format to the console using INFO level.
|
||||
type verboseSvc struct {
|
||||
stop chan struct{} // signals time to stop
|
||||
started chan struct{} // signals startup complete
|
||||
}
|
||||
|
||||
func newVerboseSvc() *verboseSvc {
|
||||
return &verboseSvc{
|
||||
stop: make(chan struct{}),
|
||||
started: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Serve runs the verbose logging service.
|
||||
func (s *verboseSvc) Serve() {
|
||||
sub := events.Default.Subscribe(events.AllEvents)
|
||||
defer events.Default.Unsubscribe(sub)
|
||||
|
||||
// We're ready to start processing events.
|
||||
close(s.started)
|
||||
|
||||
for {
|
||||
select {
|
||||
case ev := <-sub.C():
|
||||
formatted := s.formatEvent(ev)
|
||||
if formatted != "" {
|
||||
l.Verboseln(formatted)
|
||||
}
|
||||
case <-s.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop stops the verbose logging service.
|
||||
func (s *verboseSvc) Stop() {
|
||||
close(s.stop)
|
||||
}
|
||||
|
||||
// WaitForStart returns once the verbose logging service is ready to receive
|
||||
// events, or immediately if it's already running.
|
||||
func (s *verboseSvc) WaitForStart() {
|
||||
<-s.started
|
||||
}
|
||||
|
||||
func (s *verboseSvc) formatEvent(ev events.Event) string {
|
||||
switch ev.Type {
|
||||
case events.Ping, events.DownloadProgress:
|
||||
// Skip
|
||||
return ""
|
||||
|
||||
case events.Starting:
|
||||
return fmt.Sprintf("Starting up (%s)", ev.Data.(map[string]string)["home"])
|
||||
case events.StartupComplete:
|
||||
return "Startup complete"
|
||||
|
||||
case events.DeviceDiscovered:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
return fmt.Sprintf("Discovered device %v at %v", data["device"], data["addrs"])
|
||||
case events.DeviceConnected:
|
||||
data := ev.Data.(map[string]string)
|
||||
return fmt.Sprintf("Connected to device %v at %v", data["id"], data["addr"])
|
||||
case events.DeviceDisconnected:
|
||||
data := ev.Data.(map[string]string)
|
||||
return fmt.Sprintf("Disconnected from device %v", data["id"])
|
||||
|
||||
case events.StateChanged:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
return fmt.Sprintf("Folder %q is now %v", data["folder"], data["to"])
|
||||
|
||||
case events.RemoteIndexUpdated:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
return fmt.Sprintf("Device %v sent an index update for %q with %d items", data["device"], data["folder"], data["items"])
|
||||
case events.LocalIndexUpdated:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
return fmt.Sprintf("Updated index for folder %q with %v items", data["folder"], data["items"])
|
||||
|
||||
case events.DeviceRejected:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
return fmt.Sprintf("Rejected connection from device %v at %v", data["device"], data["address"])
|
||||
case events.FolderRejected:
|
||||
data := ev.Data.(map[string]string)
|
||||
return fmt.Sprintf("Rejected unshared folder %q from device %v", data["folder"], data["device"])
|
||||
|
||||
case events.ItemStarted:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
return fmt.Sprintf("Started syncing %q / %q (%v %v)", data["folder"], data["item"], data["action"], data["type"])
|
||||
case events.ItemFinished:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
if err := data["err"]; err != nil {
|
||||
return fmt.Sprintf("Finished syncing %q / %q (%v %v): %v", data["folder"], data["item"], data["action"], data["type"], err)
|
||||
}
|
||||
return fmt.Sprintf("Finished syncing %q / %q (%v %v): Success", data["folder"], data["item"], data["action"], data["type"])
|
||||
|
||||
case events.ConfigSaved:
|
||||
return "Configuration was saved"
|
||||
|
||||
case events.FolderCompletion:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
return fmt.Sprintf("Completion for folder %q on device %v is %v%%", data["folder"], data["device"], data["completion"])
|
||||
case events.FolderSummary:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
sum := data["summary"].(map[string]interface{})
|
||||
delete(sum, "invalid")
|
||||
delete(sum, "ignorePatterns")
|
||||
delete(sum, "stateChanged")
|
||||
return fmt.Sprintf("Summary for folder %q is %v", data["folder"], data["summary"])
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %#v", ev.Type, ev)
|
||||
}
|
||||
@@ -1,27 +1,8 @@
|
||||
This directory contains a configuration for running syncthing under the
|
||||
# Systemd Configuration
|
||||
|
||||
This directory contains configuration files for running syncthing under the
|
||||
"systemd" service manager on Linux both under either a systemd system service or
|
||||
systemd user service.
|
||||
systemd user service. For further documentation take a look at the [systemd
|
||||
section][1] on the Github Wiki.
|
||||
|
||||
1. Install systemd.
|
||||
|
||||
2. If you are running this as a system level service:
|
||||
|
||||
1. Create the user you will be running the service as (foo in this example).
|
||||
|
||||
2. Copy the syncthing@.service files to /etc/systemd/system
|
||||
|
||||
3. Enable and start the service
|
||||
systemctl enable syncthing@foo.service
|
||||
systemctl start syncthing@foo.service
|
||||
|
||||
3. If you are running this as a user level service:
|
||||
|
||||
1. Log in as the user you will be running the service as
|
||||
|
||||
2. Copy the syncthing.service files to /etc/systemd/user
|
||||
|
||||
3. Enable and start the service
|
||||
systemctl --user enable syncthing.service
|
||||
systemctl --user start syncthing.service
|
||||
|
||||
Log output is sent to the journal.
|
||||
[1]: https://github.com/syncthing/syncthing/wiki/Autostart-syncthing#systemd
|
||||
|
||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -111,7 +111,7 @@
|
||||
"Share Folders With Device": "Share Folders With Device",
|
||||
"Share With Devices": "Абагуліць з прыладамі",
|
||||
"Share this folder?": "Абагуліць гэты каталёг ?",
|
||||
"Shared With": "Абагульны з",
|
||||
"Shared With": "Абагулены з",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Short identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Show ID": "Паказаць ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"Addresses": "Adresy",
|
||||
"All Data": "Všechna data",
|
||||
"Allow Anonymous Usage Reporting?": "Povolit anonymní hlášení o používání?",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Verzování obstarává externí příkaz. Musí odstranit soubor ze sdíleného adresáře.",
|
||||
"Anonymous Usage Reporting": "Anonymní hlášení o používání",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Jakékoliv přístroje nakonfigurované na zavaděči budou přidány také na tento přístroj.",
|
||||
"Automatic upgrades": "Automatický upgrade",
|
||||
@@ -23,7 +23,7 @@
|
||||
"Connection Error": "Chyba připojení",
|
||||
"Copied from elsewhere": "Zkopírováno odjinud",
|
||||
"Copied from original": "Zkopírováno z originálu",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 následující přispěvatelé:",
|
||||
"Delete": "Smazat",
|
||||
"Device ID": "ID přístroje",
|
||||
"Device Identification": "Identifikace přístroje",
|
||||
@@ -44,9 +44,9 @@
|
||||
"Enter ignore patterns, one per line.": "Vložit ignorované vzory, jeden na řádek.",
|
||||
"Error": "Chyba",
|
||||
"External File Versioning": "Externí verzování souborů",
|
||||
"File Versioning": "Verze souborů",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"File Versioning": "Verzování souborů",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Bity označující práva souborů jsou při hledání změn ignorovány. Použít pro souborové systémy FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Po nahrazení nebo smazání aplikací Syncthing jsou soubory přesunuty do verzí označených daty v adresáři .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.": "Soubory jsou chráněny před změnami na ostatních přístrojích, ale změny provedené z tohoto přístroje budou rozeslány na zbytek clusteru.",
|
||||
"Folder ID": "ID adresáře",
|
||||
"Folder Master": "Master adresář",
|
||||
@@ -79,7 +79,7 @@
|
||||
"New Device": "Nový přístroj",
|
||||
"New Folder": "Nový adresář",
|
||||
"No": "Ne",
|
||||
"No File Versioning": "Bez verzí souborů",
|
||||
"No File Versioning": "Bez verzování souborů",
|
||||
"Notice": "Oznámení",
|
||||
"OK": "OK",
|
||||
"Off": "Vypnuta",
|
||||
@@ -118,10 +118,10 @@
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Zobrazeno místo ID přístroje na náhledu stavu clusteru. Pokud nebude vyplněno, bude nastaveno na jméno, které přístroj odesílá.",
|
||||
"Shutdown": "Vypnout",
|
||||
"Shutdown Complete": "Vypnutí dokončeno",
|
||||
"Simple File Versioning": "Jednoduché verze souborů",
|
||||
"Simple File Versioning": "Jednoduché verzování souborů",
|
||||
"Single level wildcard (matches within a directory only)": "Jednoúrovňový zástupný znak (shody pouze uvnitř adresáře)",
|
||||
"Source Code": "Zdrojový kód",
|
||||
"Staggered File Versioning": "Vícenásobné verze souborů",
|
||||
"Staggered File Versioning": "Postupné verzování souborů",
|
||||
"Start Browser": "Otevřít prohlížeč",
|
||||
"Stopped": "Pozastaveno",
|
||||
"Support": "Podpora",
|
||||
@@ -132,7 +132,7 @@
|
||||
"Syncthing is restarting.": "Syncthing se restartuje.",
|
||||
"Syncthing is upgrading.": "Syncthing se aktualizuje.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing se zdá být nefunkční, nebo je problém s připojením k Internetu. Opakuji...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing má nejspíše problém s provedením vašeho požadavku. Pokud problém přetrvává, obnovte stránku v prohlížeči nebo restartujte Syncthing.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Souhrnné statistiky jsou veřejně dostupné na {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurace byla uložena, ale není aktivována. Pro aktivaci nové konfigurace je třeba restartovat Syncthing.",
|
||||
"The device ID cannot be blank.": "ID přístroje nemůže být prázdné.",
|
||||
@@ -160,7 +160,7 @@
|
||||
"Upload Rate": "Rychlost odesílání",
|
||||
"Use HTTPS for GUI": "Použít HTTPS pro grafické rozhraní",
|
||||
"Version": "Verze",
|
||||
"Versions Path": "Cesta verzí",
|
||||
"Versions Path": "Cesta k verzím",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Verze jsou automaticky smazány, pokud jsou starší než maximální časový limit nebo překročí počet souborů povolených pro interval.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Při přidávání nového přístroje mějte na paměti, že je ho třeba také zadat na druhé straně.",
|
||||
"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.": "Při přidávání nového adresáře mějte na paměti, že jeho ID je použito ke svázání adresářů napříč přístoji. Rozlišují se malá a velká písmena a musí přesně souhlasit mezi všemi přístroji.",
|
||||
|
||||
@@ -9,21 +9,21 @@
|
||||
"Addresses": "Adressen",
|
||||
"All Data": "Alle Daten",
|
||||
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsberichten erlauben?",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein externer Programmaufruf handhabt die Versionierung. Es muss die Datei aus dem zu synchronisierendem Ordner entfernen.",
|
||||
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Alle Geräte, die beim Verteiler eingetragen sind, werden auch bei diesem Gerät eingetragen",
|
||||
"Automatic upgrades": "automatische Updates",
|
||||
"Bugs": "Fehler",
|
||||
"CPU Utilization": "Prozessorauslastung",
|
||||
"Changelog": "Versionsinfo",
|
||||
"Changelog": "Änderungsprotokoll",
|
||||
"Close": "Schließen",
|
||||
"Command": "Command",
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
|
||||
"Compression": "Komprimierung",
|
||||
"Connection Error": "Verbindungsfehler",
|
||||
"Copied from elsewhere": "Von woanders kopiert",
|
||||
"Copied from original": "Vom Original kopiert",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 die folgenden Unterstützer:",
|
||||
"Delete": "Löschen",
|
||||
"Device ID": "Geräte ID",
|
||||
"Device Identification": "Gerät Identifikation",
|
||||
@@ -43,10 +43,10 @@
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Trage durch ein Komma getrennte \"IP:Port\" Adressen oder \"dynamic\" ein um automatische Adresserkennung durchzuführen.",
|
||||
"Enter ignore patterns, one per line.": "Geben Sie Ignoriermuster ein, eines pro Zeile.",
|
||||
"Error": "Fehler",
|
||||
"External File Versioning": "External File Versioning",
|
||||
"External File Versioning": "Externe Dateiversionierung",
|
||||
"File Versioning": "Dateiversionierung",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Dateizugriffsrechte beim Suchen nach Veränderungen ignorieren. Bei FAT-Dateisystemen zu verwenden.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dateien werden, bevor Syncthing sie löscht oder ersetzt, als datierte Versionen in einen Ordner namens .stversions verschoben.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dateien sind vor Veränderung durch andere Geräte geschützt, auf diesem Gerät durchgeführte Veränderungen werden aber auf den Rest des Verbunds übertragen.",
|
||||
"Folder ID": "Verzeichnis ID",
|
||||
"Folder Master": "Keine Veränderungen zulassen",
|
||||
@@ -132,14 +132,14 @@
|
||||
"Syncthing is restarting.": "Syncthing wird neu gestartet",
|
||||
"Syncthing is upgrading.": "Syncthing wird aktualisiert",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Deiner Internetverbindung. Versuche erneut...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Es scheint als ob Syncthing ein Problem mit der Verarbeitung ihrer Eingabe hat. Bitte laden sie die Seite neu oder führen sie einen Neustart von Syncthing durch, falls das Problem weiterhin besteht.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Die gesammelten Statistiken sind öffentlich verfügbar unter {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber nicht aktiviert. Syncthing muss neugestartet werden um die neue Konfiguration zu aktivieren.",
|
||||
"The device ID cannot be blank.": "Die Geräte ID darf nicht leer sein.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Die hier einzutragende Geräte ID kann im \"Bearbeiten > Zeige ID\"-Dialog auf dem anderen Gerät gefunden werden. Leerzeichen und Bindestriche sind optional (werden ignoriert).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Der verschlüsselte Nutzungsbericht wird täglich gesendet. Er wird benutzt um Statistiken über verwendete Betriebssysteme, Verzeichnis-Größen und Programm-Versionen zu erstellen. Sollte der Bericht in Zukunft weitere Daten erfassen, wird dieses Fenster erneut angezeigt.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Die eingegebene Geräte ID scheint nicht gültig zu sein. Es sollte eine 52 oder 56 stellige Zeichenkette aus Buchstaben und Nummern sein. Leerzeichen und Bindestriche sind optional.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Der erste Kommandozeilenparameter ist der Verzeichnis-Pfad und der zweite Parameter ist der relative Pfad in diesem Ordner.",
|
||||
"The folder ID cannot be blank.": "Die Verzeichnis ID darf nicht leer sein.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis ID muss eine kurze Kennung (64 Zeichen oder weniger) sein. Sie kann nur aus Buchstaben, Zahlen und dem Punkt- (.), Bindestrich- (-), und Unterstrich- (_) Zeichen bestehen.",
|
||||
"The folder ID must be unique.": "Die Verzeichnis ID muss eindeutig sein.",
|
||||
@@ -149,7 +149,7 @@
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Die längste Zeit, die alte Versionen vorgehalten werden (in Tagen, 0 bedeutet, alte Versionen für immer zu behalten).",
|
||||
"The number of old versions to keep, per file.": "Anzahl der alten Versionen, die von jeder Datei gespeichert werden sollen.",
|
||||
"The number of versions must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Zahl und darf nicht leer sein.",
|
||||
"The path cannot be blank.": "The path cannot be blank.",
|
||||
"The path cannot be blank.": "Der Pfad darf nicht leer sein",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Das Suchintervall muss eine nicht negative Anzahl von Sekunden sein.",
|
||||
"Unknown": "Unbekannt",
|
||||
"Unshared": "Ungeteilt",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"Addresses": "Διευθύνσεις",
|
||||
"All Data": "Όλα τα δεδομένα",
|
||||
"Allow Anonymous Usage Reporting?": "Να επιτρέπεται η αποστολή ανώνυμων στοιχείων χρήσης;",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Μια εξωτερική εντολή χειρίζεται την διαχείριση εκδόσεων. Πρέπει να καταργήσετε το αρχείο από το φάκελο συγχρονισμένων.",
|
||||
"Anonymous Usage Reporting": "Ανώνυμα στοιχεία χρήσης",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Αν δηλωθεί σαν «βασικός κόμβος», τότε όλες οι συσκευές που είναι δηλωμένες εκεί θα υπάρχουν και στον τοπικό κόμβο.",
|
||||
"Automatic upgrades": "Αυτόματη αναβάθμιση",
|
||||
@@ -17,13 +17,13 @@
|
||||
"CPU Utilization": "Επιβάρυνση του επεξεργαστή",
|
||||
"Changelog": "Πληροφορίες εκδόσεων",
|
||||
"Close": "Τέλος",
|
||||
"Command": "Command",
|
||||
"Command": "Εντολή",
|
||||
"Comment, when used at the start of a line": "Σχόλιο, όταν χρησιμοποιείται στην αρχή μιας γραμμής",
|
||||
"Compression": "Συμπίεση",
|
||||
"Connection Error": "Σφάλμα σύνδεσης",
|
||||
"Copied from elsewhere": "Έχει αντιγραφεί από κάπου αλλού",
|
||||
"Copied from original": "Έχει αντιγραφεί από το πρωτότυπο",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 από τους παρακάτω συνεισφορείς:",
|
||||
"Delete": "Διαγραφή",
|
||||
"Device ID": "Ταυτότητα συσκευής",
|
||||
"Device Identification": "Ταυτότητα συσκευής",
|
||||
@@ -43,10 +43,10 @@
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Γράψε τις διευθύνσεις IP με τη μορφή «ip:θύρα», διαχωρισμένες με κόμμα. Αλλιώς γράψε «dynamic» για να πραγματοποιηθεί η αυτόματη εύρεση διευθύνσεων.",
|
||||
"Enter ignore patterns, one per line.": "Δώσε τα πρότυπα που θα αγνοηθούν, ένα σε κάθε γραμμή.",
|
||||
"Error": "Σφάλμα",
|
||||
"External File Versioning": "External File Versioning",
|
||||
"External File Versioning": "Εξωτερική διαχείριση εκδόσεων Αρχείου",
|
||||
"File Versioning": "Τήρηση εκδόσεων",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Τα bit αδεια αρχεία αγνοούνται όταν ψάχνουν για αλλαγές. Χρήση σε συστήματα αρχείων FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Τα αρχεία που μετακινηθηκαν στην ημερομηνία που αναγράφεται τις εκδόσεις σε ένα φάκελο .stversions όταν αντικατασταθούν ή να διαγραφθούν από το 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.": "Τα αρχεία προστατεύονται από αλλαγές που γίνονται σε άλλες συσκευές, αλλά όποιες αλλαγές γίνουν σε αυτή τη συσκευή θα αποσταλούν σε όλη τη συστάδα συσκευών.",
|
||||
"Folder ID": "Ταυτότητα φακέλου",
|
||||
"Folder Master": "Να μην επιτρέπονται αλλαγές",
|
||||
@@ -132,14 +132,14 @@
|
||||
"Syncthing is restarting.": "Το Syncthing επανεκκινείται.",
|
||||
"Syncthing is upgrading.": "Το Syncthing αναβαθμίζεται.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Το Syncthing φαίνεται πως είναι απενεργοποιημένο ή υπάρχει πρόβλημα στη σύνδεσή σου στο διαδίκτυο. Προσπαθώ πάλι…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Το Syncthing φαίνεται να αντιμετωπίζει ένα πρόβλημα κατά την επεξεργασία του αιτήματός σας. Παρακαλούμε ανανεώστε την σελίδα ή επανεκκινήστε το Syncthing εάν το πρόβλημα παραμένει.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Τα στατιστικά που έχουν συλλεγεί είναι δημόσια διαθέσιμα στο {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Οι ρυθμίσεις έχουν αποθηκευτεί αλλά δεν έχουν ενεργοποιηθεί. Πρέπει να επανεκκινήσεις το Syncthing για να ισχύσουν οι νέες ρυθμίσεις.",
|
||||
"The device ID cannot be blank.": "Η ταυτότητα της συσκευής δεν μπορεί να είναι κενή",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Η ταυτότητα της συσκευής που θα μπει εδώ βρίσκεται στο μενού «Επεξεργασία > Εμφάνιση ταυτότητας» στην άλλη συσκευή. Κενοί χαρακτήρες και παύλες είναι προαιρετικοί (απλά θα αγνοηθούν).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Η κρυπτογραφημένη αναφορά χρήσης στέλνεται καθημερινά. Χρησιμοποιείται για να παραχθούν στατιστικές για τα λειτουργικά συστήματα που χρησιμοποιούνται, τα μεγέθη των φακέλων και τις εκδόσεις των προγραμμάτων. Αν στο μέλλον συμπεριληφθούν και άλλα δεδομένα στην αναφορά χρήσης, τότε αυτό το παράθυρο θα εμφανιστεί ξανά.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Η ταυτότητα συσκευής που έδωσες δε φαίνεται έγκυρη. Θα πρέπει να είναι μια σειρά από 52 ή 56 χαρακτήρες (γράμματα και αριθμοί). Τα κενά και οι παύλες είναι προαιρετικά (αδιάφορα).",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Η πρώτη παράμετρος γραμμής εντολών είναι η διαδρομή του φακέλου και η δεύτερη παράμετρος είναι η σχετική διαδρομή στο φάκελο.",
|
||||
"The folder ID cannot be blank.": "Η ταυτότητα του φακέλου δεν μπορεί να είναι κενή.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Η ταυτότητα του φακέλου πρέπει να είναι ένα σύντομο αναγνωριστικό (το πολύ 64 χαρακτήρες). Μπορεί να αποτελείται από γράμματα, αριθμούς, την τελεία (.), την παύλα (-) και την κάτω παύλα (_).",
|
||||
"The folder ID must be unique.": "Η ταυτότητα του φακέλου πρέπει να είναι μοναδική.",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"A new major version may not be compatible with previous versions.": "A new major version may not be compatible with previous versions.",
|
||||
"API Key": "API Key",
|
||||
"About": "About",
|
||||
"Add": "Add",
|
||||
@@ -9,6 +10,7 @@
|
||||
"Addresses": "Addresses",
|
||||
"All Data": "All Data",
|
||||
"Allow Anonymous Usage Reporting?": "Allow Anonymous Usage Reporting?",
|
||||
"Alphabetic": "Alphabetic",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"Anonymous Usage Reporting": "Anonymous Usage Reporting",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
|
||||
@@ -44,6 +46,7 @@
|
||||
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||
"Error": "Error",
|
||||
"External File Versioning": "External File Versioning",
|
||||
"File Pull Order": "File Pull Order",
|
||||
"File Versioning": "File Versioning",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
@@ -66,11 +69,13 @@
|
||||
"Introducer": "Introducer",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||
"Keep Versions": "Keep Versions",
|
||||
"Largest First": "Largest First",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last seen": "Last seen",
|
||||
"Later": "Later",
|
||||
"Local Discovery": "Local Discovery",
|
||||
"Local State": "Local State",
|
||||
"Major Upgrade": "Major Upgrade",
|
||||
"Maximum Age": "Maximum Age",
|
||||
"Metadata Only": "Metadata Only",
|
||||
"Move to top of queue": "Move to top of queue",
|
||||
@@ -78,22 +83,27 @@
|
||||
"Never": "Never",
|
||||
"New Device": "New Device",
|
||||
"New Folder": "New Folder",
|
||||
"Newest First": "Newest First",
|
||||
"No": "No",
|
||||
"No File Versioning": "No File Versioning",
|
||||
"Notice": "Notice",
|
||||
"OK": "OK",
|
||||
"Off": "Off",
|
||||
"Oldest First": "Oldest First",
|
||||
"Out Of Sync": "Out Of Sync",
|
||||
"Out of Sync Items": "Out of Sync Items",
|
||||
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
|
||||
"Override Changes": "Override Changes",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Path where versions should be stored (leave empty for the default .stversions folder in the folder).",
|
||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
||||
"Please wait": "Please wait",
|
||||
"Preview": "Preview",
|
||||
"Preview Usage Report": "Preview Usage Report",
|
||||
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||
"RAM Utilization": "RAM Utilization",
|
||||
"Random": "Random",
|
||||
"Release Notes": "Release Notes",
|
||||
"Rescan": "Rescan",
|
||||
"Rescan All": "Rescan All",
|
||||
"Rescan Interval": "Rescan Interval",
|
||||
@@ -120,6 +130,7 @@
|
||||
"Shutdown Complete": "Shutdown Complete",
|
||||
"Simple File Versioning": "Simple File Versioning",
|
||||
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||
"Smallest First": "Smallest First",
|
||||
"Source Code": "Source Code",
|
||||
"Staggered File Versioning": "Staggered File Versioning",
|
||||
"Start Browser": "Start Browser",
|
||||
@@ -151,10 +162,12 @@
|
||||
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
|
||||
"The path cannot be blank.": "The path cannot be blank.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
|
||||
"This is a major version upgrade.": "This is a major version upgrade.",
|
||||
"Unknown": "Unknown",
|
||||
"Unshared": "Unshared",
|
||||
"Unused": "Unused",
|
||||
"Up to Date": "Up to Date",
|
||||
"Upgrade": "Upgrade",
|
||||
"Upgrade To {%version%}": "Upgrade To {{version}}",
|
||||
"Upgrading": "Upgrading",
|
||||
"Upload Rate": "Upload Rate",
|
||||
|
||||
172
gui/assets/lang/lang-fi.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"API Key": "API-avain",
|
||||
"About": "Tietoja",
|
||||
"Add": "Lisää",
|
||||
"Add Device": "Lisää laite",
|
||||
"Add Folder": "Lisää kansio",
|
||||
"Add new folder?": "Lisää uusi kansio?",
|
||||
"Address": "Osoite",
|
||||
"Addresses": "Osoitteet",
|
||||
"All Data": "Kaikki data",
|
||||
"Allow Anonymous Usage Reporting?": "Salli anonyymi käyttöraportointi?",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ulkoinen komento hallitsee versionnin. Sen täytyy poistaa tiedosto synkronoidusta kansiosta.",
|
||||
"Anonymous Usage Reporting": "Anonyymi käyttöraportointi",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Kaikki esittelijäksi määritetyn laitteen tuntemat laitteet lisätään myös tähän laitteeseen.",
|
||||
"Automatic upgrades": "Automaattiset päivitykset",
|
||||
"Bugs": "Bugit",
|
||||
"CPU Utilization": "CPU:n käyttö",
|
||||
"Changelog": "Muutoshistoria",
|
||||
"Close": "Sulje",
|
||||
"Command": "Komento",
|
||||
"Comment, when used at the start of a line": "Kommentti, käytettäessä rivin alussa",
|
||||
"Compression": "Pakkaus",
|
||||
"Connection Error": "Yhteysvirhe",
|
||||
"Copied from elsewhere": "Kopioitu muualta",
|
||||
"Copied from original": "Kopioitu alkuperäisestä lähteestä",
|
||||
"Copyright © 2015 the following Contributors:": "Tekijänoikeus © 2015 seuraavat avustajat:",
|
||||
"Delete": "Poista",
|
||||
"Device ID": "Laitteen ID",
|
||||
"Device Identification": "Laitteen tunniste",
|
||||
"Device Name": "Laitteen nimi",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Laite {{device}} ({{address}}) haluaa yhdistää. Lisää uusi laite?",
|
||||
"Devices": "Laitteet",
|
||||
"Disconnected": "Yhteys katkaistu",
|
||||
"Documentation": "Dokumentaatio",
|
||||
"Download Rate": "Latausmäärä",
|
||||
"Downloaded": "Ladattu",
|
||||
"Downloading": "Ladataan",
|
||||
"Edit": "Muokkaa",
|
||||
"Edit Device": "Muokkaa laitetta",
|
||||
"Edit Folder": "Muokkaa kansiota",
|
||||
"Editing": "Muokkaus",
|
||||
"Enable UPnP": "Ota UPnP käyttöön",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Syötä pilkuin erotettuna osoitteet muodossa \"ip:portti\" tai syötä \"dynamic\" automaattista osoitteiden selvitystä varten.",
|
||||
"Enter ignore patterns, one per line.": "Syötä ohituslausekkeet, yksi riviä kohden.",
|
||||
"Error": "Virhe",
|
||||
"External File Versioning": "Ulkoinen tiedostoversionti",
|
||||
"File Versioning": "Tiedostoversiointi",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Tiedostojen oikeusbitit jätetään huomiotta etsittäessä muutoksia. Käytä FAT-tiedostojärjestelmissä.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Tiedostot siirretään päivämäärällä merkityiksi versioiksi .stversions-kansioon, kun Syncthing korvaa tai poistaa ne.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Tiedostot on suojattu muilla laitteilla tehdyiltä muutoksilta, mutta tällä laitteella tehdyt muutokset lähetetään muuhun ryhmään.",
|
||||
"Folder ID": "Kansion ID",
|
||||
"Folder Master": "Hallitsijakansio",
|
||||
"Folder Path": "Kansion polku",
|
||||
"Folders": "Kansiot",
|
||||
"GUI Authentication Password": "GUI:n salasana",
|
||||
"GUI Authentication User": "GUI:n käyttäjätunnus",
|
||||
"GUI Listen Addresses": "GUI:n kuunteluosoitteet",
|
||||
"Generate": "Generoi",
|
||||
"Global Discovery": "Globaali etsintä",
|
||||
"Global Discovery Server": "Globaali etsintäpalvelin",
|
||||
"Global State": "Globaali tila",
|
||||
"Ignore": "Ohita",
|
||||
"Ignore Patterns": "Ohituslausekkeet",
|
||||
"Ignore Permissions": "Jätä oikeudet huomiotta",
|
||||
"Incoming Rate Limit (KiB/s)": "Sisääntulevan liikenteen rajoitus (KiB/s)",
|
||||
"Introducer": "Esittelijä",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Käänteinen ehto (t.s. älä ohita)",
|
||||
"Keep Versions": "Säilytä versiot",
|
||||
"Last File Received": "Viimeksi vastaanotettu tiedosto",
|
||||
"Last seen": "Nähty viimeksi",
|
||||
"Later": "Myöhemmin",
|
||||
"Local Discovery": "Paikallinen etsintä",
|
||||
"Local State": "Paikallinen tila",
|
||||
"Maximum Age": "Maksimi-ikä",
|
||||
"Metadata Only": "Vain metadata",
|
||||
"Move to top of queue": "Siirrä jonon alkuun",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Monitasoinen jokerimerkki (vaikuttaa useassa kansiotasossa)",
|
||||
"Never": "Ei koskaan",
|
||||
"New Device": "Uusi laite",
|
||||
"New Folder": "Uusi kansio",
|
||||
"No": "Ei",
|
||||
"No File Versioning": "Ei tiedostoversiointia",
|
||||
"Notice": "Huomautus",
|
||||
"OK": "OK",
|
||||
"Off": "Pois",
|
||||
"Out Of Sync": "Ei ajan tasalla",
|
||||
"Out of Sync Items": "Kohteet, jotka eivät ole ajan tasalla",
|
||||
"Outgoing Rate Limit (KiB/s)": "Uloslähtevän liikenteen rajoitus (KiB/s)",
|
||||
"Override Changes": "Ohita muutokset",
|
||||
"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": "Polku kansioon paikallisella tietokoneella. Kansio luodaan, ellei sitä ole olemassa. Tilde-merkkiä (~) voidaan käyttää oikotienä polulle",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Polku jonne versiot tullaan tallentamaan (jätä tyhjäksi oletusvalintaa .stversions varten).",
|
||||
"Please wait": "Ole hyvä ja odota",
|
||||
"Preview": "Esikatselu",
|
||||
"Preview Usage Report": "Esikatsele käyttöraportti",
|
||||
"Quick guide to supported patterns": "Tuettujen lausekkeiden pikaohje",
|
||||
"RAM Utilization": "RAM:n käyttö",
|
||||
"Rescan": "Skannaa uudelleen",
|
||||
"Rescan All": "Skannaa kaikki uudelleen",
|
||||
"Rescan Interval": "Uudelleenskannauksen aikaväli",
|
||||
"Restart": "Käynnistä uudelleen",
|
||||
"Restart Needed": "Uudelleenkäynnistys tarvitaan",
|
||||
"Restarting": "Käynnistetään uudelleen",
|
||||
"Reused": "Uudelleenkäytetty",
|
||||
"Save": "Tallenna",
|
||||
"Scanning": "Skannataan",
|
||||
"Select the devices to share this folder with.": "Valitse laitteet, joiden kanssa tämä kansio jaetaan.",
|
||||
"Select the folders to share with this device.": "Valitse kansiot jaettavaksi tämän laitteen kanssa.",
|
||||
"Settings": "Asetukset",
|
||||
"Share": "Jaa",
|
||||
"Share Folder": "Jaa kansio",
|
||||
"Share Folders With Device": "Jaa kansioita laitteen kanssa",
|
||||
"Share With Devices": "Jaa laitteiden kanssa",
|
||||
"Share this folder?": "Jaa tämä kansio?",
|
||||
"Shared With": "Jaettu seuraavien kanssa",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Lyhyt tunniste kansiolle. Tämän tulee olla sama kaikilla ryhmän laitteilla.",
|
||||
"Show ID": "Näytä ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Näytetään ryhmän tiedoissa laitteen ID:n sijaan. Ilmoitetaan muille laitteille vaihtoehtoisena oletusnimenä.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Näytetään ryhmän tiedoissa laitteen ID:n sijaan. Tyhjä nimi päivitetään laitteen ilmoittamaksi nimeksi.",
|
||||
"Shutdown": "Sammuta",
|
||||
"Shutdown Complete": "Sammutus valmis",
|
||||
"Simple File Versioning": "Yksinkertainen tiedostoversiointi",
|
||||
"Single level wildcard (matches within a directory only)": "Yksitasoinen jokerimerkki (vaikuttaa vain kyseisen kansion sisällä)",
|
||||
"Source Code": "Lähdekoodi",
|
||||
"Staggered File Versioning": "Porrastettu tiedostoversiointi",
|
||||
"Start Browser": "Käynnistä selain",
|
||||
"Stopped": "Pysäytetty",
|
||||
"Support": "Tuki",
|
||||
"Sync Protocol Listen Addresses": "Synkronointiprotokollan kuunteluosoite",
|
||||
"Syncing": "Synkronoidaan",
|
||||
"Syncthing has been shut down.": "Syncthing on sammutettu.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing sisältää seuraavat ohjelmistot tai sen osat:",
|
||||
"Syncthing is restarting.": "Syncthing käynnistyy uudelleen.",
|
||||
"Syncthing is upgrading.": "Syncthing päivittyy.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing näyttää olevan alhaalla tai internetyhteydessä on ongelma. Yritetään uudelleen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ei pysty käsittelemään pyyntöäsi. Ole hyvä ja päivitä sivu tai käynnistä Syncthing uudelleen, jos ongelma jatkuu.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Yhdistetyt tilastot ovat julkisesti saatavilla osoitteessa {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Asetukset on tallennettu, mutta niitä ei ole otettu käyttöön. Syncthingin täytyy käynnistyä uudelleen, jotta uudet asetukset saadaan käyttöön.",
|
||||
"The device ID cannot be blank.": "Laitteen ID ei voi olla tyhjä.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Tähän kohtaan syötettävän ID:n löytää \"Muokkaa > Näytä ID\" -valikosta toisesta laitteesta. Välit ja viivat ovat valinnaisia (jätetään huomiotta).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Salattu käyttöraportti lähetetään päivittäin. Sitä käytetään yleisimpien alustojen, kansioiden kokojen ja sovellusversioiden seuraamiseen. Jos raportitavan datan luonne muuttuu, sinua tullaan huomauttamaan tällä dialogilla uudelleen.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Syötetty laite-ID ei näytä kelpaavalta. Sen tulisi olla 52 tai 56 merkkiä pitkä, joka koostuu kirjaimista ja numeroista, jossa välit ja viivat ovat valinnaisia.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Ensimmäinen komentoriviparametri on kansion polku, toinen parametri on suhteellinen polku kansion sisällä.",
|
||||
"The folder ID cannot be blank.": "Kansion ID ei voi olla tyhjä.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Kansion ID:n tulee olla lyhyt tunniste (64 merkkiä tai vähemmän), joka koostuu vain kirjaimista, numeroista, pisteistä (.), viivoista (-), ja alaviivoista (_).",
|
||||
"The folder ID must be unique.": "Kansion ID:n tulee olla uniikki.",
|
||||
"The folder path cannot be blank.": "Kansion polku ei voi olla tyhjä.",
|
||||
"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.": "Seuraavat aikavälit ovat käytössä: ensimmäisen tunnin ajalta uusi versio säilytetään joka 30 sekunti, ensimmäisen päivän ajalta uusi versio säilytetään tunneittain ja ensimmäisen 30 päivän aikana uusi versio säilytetään päivittäin. Lopulta uusi versio säilytetään viikoittain, kunnes maksimi-ikä saavutetaan.",
|
||||
"The maximum age must be a number and cannot be blank.": "Maksimi-iän tulee olla numero, eikä se voi olla tyhjä.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimiaika versioiden säilytykseen (päivissä, aseta 0 säilyttääksesi versiot ikuisesti).",
|
||||
"The number of old versions to keep, per file.": "Säilytettävien vanhojen versioiden määrä tiedostoa kohden.",
|
||||
"The number of versions must be a number and cannot be blank.": "Versioiden määrän rulee olla numero, eikä se voi olla tyhjä.",
|
||||
"The path cannot be blank.": "Polku ei voi olla tyhjä.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Uudelleenskannauksen aikavälin tulee olla ei-negatiivinen numero sekunteja.",
|
||||
"Unknown": "Tuntematon",
|
||||
"Unshared": "Jakamaton",
|
||||
"Unused": "Käyttämätön",
|
||||
"Up to Date": "Ajan tasalla",
|
||||
"Upgrade To {%version%}": "Päivitä versioon {{version}}",
|
||||
"Upgrading": "Päivitetään",
|
||||
"Upload Rate": "Lähetysmäärä",
|
||||
"Use HTTPS for GUI": "Käytä HTTPS:ää GUI:n kanssa",
|
||||
"Version": "Versio",
|
||||
"Versions Path": "Versioiden polku",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versiot poistetaan automaattisesti mikäli ne ovat vanhempia kuin maksimi-ikä tai niiden määrä ylittää sallitun määrän tietyllä aikavälillä.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Lisättäessä laitetta, muista että tämä laite tulee myös lisätä toiseen laitteeseen.",
|
||||
"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.": "Lisättäessä uutta kansiota, muista että kansion ID:tä käytetään solmimaan kansiot yhteen laitteiden välillä. Ne ovat riippuvaisia kirjankoosta ja niiden tulee täsmätä kaikkien laitteiden välillä.",
|
||||
"Yes": "Kyllä",
|
||||
"You must keep at least one version.": "Sinun tulee säilyttää ainakin yksi versio.",
|
||||
"full documentation": "täysi dokumentaatio",
|
||||
"items": "kohteet",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} haluaa jakaa kansion \"{{folder}}\"."
|
||||
}
|
||||
@@ -134,7 +134,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plaît, rafraîchissez la page ou redémarrer Syncthing si le problème persiste.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été sauvée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
|
||||
"The device ID cannot be blank.": "L'ID de l'appareil ne peut être vide.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID de l'appareil à renseigner peut être trouvé dans le menu \"Éditer > Montrer l'ID\" des autres nœuds. Les espaces et les tirets sont optionnels (ils seront ignorés).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des répertoires et les versions de l'application. Si les données rapportées sont modifiées cette boite de dialogue vous redemandera votre confirmation.",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"Addresses": "Címek",
|
||||
"All Data": "Minden adat",
|
||||
"Allow Anonymous Usage Reporting?": "Engedélyezed a névtelen felhasználási adatok küldését?",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Egy külső program kezeli a fájl verziózást. El kell távolítsa a fájlt a szinkronizált mappából.",
|
||||
"Anonymous Usage Reporting": "Névtelen felhasználási adatok küldése",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Minden eszköz ami a bevezető eszközön lett beállítva hozzá lesz adva ehhez az eszközhöz is.",
|
||||
"Automatic upgrades": "Automatikus frissítés",
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
"Add new folder?": "Aggiungere una nuova cartella?",
|
||||
"Address": "Indirizzo",
|
||||
"Addresses": "Indirizzi",
|
||||
"All Data": "Tutti i dati",
|
||||
"All Data": "Tutti i Dati",
|
||||
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Il controllo versione è gestito da un comando esterno. Quest'ultimo deve rimuovere il file dalla cartella sincronizzata.",
|
||||
"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",
|
||||
@@ -17,13 +17,13 @@
|
||||
"CPU Utilization": "Utilizzo CPU",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Chiudi",
|
||||
"Command": "Command",
|
||||
"Command": "Comando",
|
||||
"Comment, when used at the start of a line": "Per commentare, va inserito all'inizio di una riga",
|
||||
"Compression": "Compressione",
|
||||
"Connection Error": "Errore di Connessione",
|
||||
"Copied from elsewhere": "Copiato da qualche altra parte",
|
||||
"Copied from original": "Copiato dall'originale",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 i seguenti Collaboratori:",
|
||||
"Delete": "Elimina",
|
||||
"Device ID": "ID Dispositivo",
|
||||
"Device Identification": "Identificazione Dispositivo",
|
||||
@@ -34,7 +34,7 @@
|
||||
"Documentation": "Documentazione",
|
||||
"Download Rate": "Velocità Download",
|
||||
"Downloaded": "Scaricato",
|
||||
"Downloading": "Sto scaricando",
|
||||
"Downloading": "Scaricamento in corso",
|
||||
"Edit": "Modifica",
|
||||
"Edit Device": "Modifica Dispositivo",
|
||||
"Edit Folder": "Modifica Cartella",
|
||||
@@ -45,8 +45,8 @@
|
||||
"Error": "Errore",
|
||||
"External File Versioning": "Controllo Versione Esterno",
|
||||
"File Versioning": "Controllo Versione dei File",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"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 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 ID": "ID Cartella",
|
||||
"Folder Master": "Cartella Principale",
|
||||
@@ -72,7 +72,7 @@
|
||||
"Local Discovery": "Individuazione Locale",
|
||||
"Local State": "Stato Locale",
|
||||
"Maximum Age": "Durata Massima",
|
||||
"Metadata Only": "Solo i metadati",
|
||||
"Metadata Only": "Solo i Metadati",
|
||||
"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",
|
||||
@@ -132,14 +132,14 @@
|
||||
"Syncthing is restarting.": "Riavvio di Syncthing in corso.",
|
||||
"Syncthing is upgrading.": "Aggiornamento di Syncthing in corso.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing sembra inattivo, oppure c'è un problema con la tua connessione a Internet. Nuovo tentativo…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Sembra che Syncthing non sia in grado di elaborare il tuo comando. Se il problema persiste prova a ricaricare la pagina nel tuo navigatore oppure prova a riavviare Syncthing.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Le statistiche aggregate sono disponibili pubblicamente su {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Devi riavviare Syncthing per attivare la nuova configurazione.",
|
||||
"The device ID cannot be blank.": "L'ID del dispositivo non può essere vuoto.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Trova l'ID nella finestra di dialogo \"Modifica > Mostra ID\" dell'altro dispositivo, poi inseriscilo qui. Gli spazi e i trattini sono opzionali (ignorati).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Quotidianamente il software invia le statistiche di utilizzo in forma criptata. Questi dati riguardano i sistemi operativi utilizzati, le dimensioni delle cartelle e le versioni del software. Se i dati riportati sono cambiati, verrà mostrata di nuovo questa finestra di dialogo.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID del dispositivo inserito non sembra valido. Dovrebbe essere una stringa di 52 o 56 caratteri costituita da lettere e numeri, con spazi e trattini opzionali.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Il primo parametro della riga di comando è il percorso della cartella e il secondo parametro è il percorso relativo nella cartella.",
|
||||
"The folder ID cannot be blank.": "L'ID della cartella non può essere vuoto.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "L'ID della cartella dev'essere un identificatore breve (64 caratteri o meno) costituito solamente da lettere, numeri, punti (.), trattini (-) e trattini bassi (_).",
|
||||
"The folder ID must be unique.": "L'ID della cartella dev'essere unico.",
|
||||
|
||||
172
gui/assets/lang/lang-ko-KR.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"API Key": "API 키",
|
||||
"About": "궁금하죠",
|
||||
"Add": "추가",
|
||||
"Add Device": "장치 ",
|
||||
"Add Folder": "폴더 추가",
|
||||
"Add new folder?": "새로운 폴더를 추가하시겠습니까?",
|
||||
"Address": "주소",
|
||||
"Addresses": "주소",
|
||||
"All Data": "전체 데이터",
|
||||
"Allow Anonymous Usage Reporting?": "버그 리포트를 보낼까요?",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "외부 명령은 버젼을 처리합니다. 동기화된 폴더에서 파일을 날려버려요.",
|
||||
"Anonymous Usage Reporting": "에러 를 보고 받습니다.",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "추가되는 장치에 이 구성이 추가되요.",
|
||||
"Automatic upgrades": "자동 업데이트",
|
||||
"Bugs": "버그",
|
||||
"CPU Utilization": "프로세스 사용",
|
||||
"Changelog": "바뀐점",
|
||||
"Close": "닫기",
|
||||
"Command": "명령",
|
||||
"Comment, when used at the start of a line": "명령행에서 시작을 할수 있어요.",
|
||||
"Compression": "압축",
|
||||
"Connection Error": "앗!! 프로그램이 죽었다.",
|
||||
"Copied from elsewhere": "다른데서 복사됨",
|
||||
"Copied from original": "원본에서 복사됨.",
|
||||
"Copyright © 2015 the following Contributors:": "이것은 번역할 필요가 있을까.",
|
||||
"Delete": "삭제",
|
||||
"Device ID": "장치 아이디",
|
||||
"Device Identification": "장치 식별자",
|
||||
"Device Name": "장치 이름",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "다른 장치 {{device}} ({{address}}) 에서 접속요구해요..이놈을 추가할까요?\n ",
|
||||
"Devices": "장치들",
|
||||
"Disconnected": "접속끊김",
|
||||
"Documentation": "문서",
|
||||
"Download Rate": "다운로드 속도",
|
||||
"Downloaded": "다운로드됨",
|
||||
"Downloading": "다운로드중",
|
||||
"Edit": "편집",
|
||||
"Edit Device": "노드 편집",
|
||||
"Edit Folder": "노드 편집",
|
||||
"Editing": "편집중",
|
||||
"Enable UPnP": "UPnP 활성화",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "여러 개의 \"ip:port\"주소는 쉼표를 이용하여 분리 입력하시오. \"dynamic\" 입력 시, 자동으로 발견됨",
|
||||
"Enter ignore patterns, one per line.": "한줄에 한개식 패턴 무시 입력합니당",
|
||||
"Error": "에러",
|
||||
"External File Versioning": "외부 파일 버젼 관리.",
|
||||
"File Versioning": "파일 버젼관리",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "변화를 주고 받을때 파일권한 무시합니다. 주로 FAT 에 적용되기도합니다.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by 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.": "파일은 다른장치로부터 보호되지만 내가 변경되는것은 다른 장치에 영향을 줘요.내가 삭제되면 다른놈도 삭제.다른놈이 삭제되면 내가 강제로 동기화해요.",
|
||||
"Folder ID": "폴더 아이디",
|
||||
"Folder Master": "읽기전용[내장치]",
|
||||
"Folder Path": "폴더 경로",
|
||||
"Folders": "폴더들",
|
||||
"GUI Authentication Password": "로그인 비밀번호",
|
||||
"GUI Authentication User": "로그인 사용자",
|
||||
"GUI Listen Addresses": "접속대기 주소",
|
||||
"Generate": "생성",
|
||||
"Global Discovery": "전역 노드 검색",
|
||||
"Global Discovery Server": "전역 노드 검색 서버",
|
||||
"Global State": "전역 중계서버 상태",
|
||||
"Ignore": "보류",
|
||||
"Ignore Patterns": "보류 유형",
|
||||
"Ignore Permissions": "권한 무시",
|
||||
"Incoming Rate Limit (KiB/s)": "전송 대역폭 제한(KB/S)",
|
||||
"Introducer": "유도",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "주어진 조건의 반대(전혀 배제하지 않음)",
|
||||
"Keep Versions": "보관 파일 수량",
|
||||
"Last File Received": "마지막 수신된 파일",
|
||||
"Last seen": "마지막 접속일",
|
||||
"Later": "나중에",
|
||||
"Local Discovery": "로컬 노드 검색",
|
||||
"Local State": "로컬 현황",
|
||||
"Maximum Age": "최대 보존 기간",
|
||||
"Metadata Only": "메타 데이타 만.",
|
||||
"Move to top of queue": "상단으로 이 큐를 이동해요.",
|
||||
"Multi level wildcard (matches multiple directory levels)": "다중 레벨 와일드 카드 (여러 폴더 레벨과 일치해요)",
|
||||
"Never": "사용안함",
|
||||
"New Device": "새로운 디바이스",
|
||||
"New Folder": "새 폴더",
|
||||
"No": "아니오",
|
||||
"No File Versioning": "파일 버젼관리 안함",
|
||||
"Notice": "공지",
|
||||
"OK": "확인",
|
||||
"Off": "꺼짐",
|
||||
"Out Of Sync": "동기화 되지 않음",
|
||||
"Out of Sync Items": "동기화 아이템이 끝났어요.",
|
||||
"Outgoing Rate Limit (KiB/s)": "업로드 속도 제한(KiB/s)",
|
||||
"Override Changes": "덮어쓰기",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "로컬컴퓨터 폴더 경로. 존재하지 않을 시, 자동생성됩니다. 문자 (~)는 - 대한 바로 가기로 사용할 수 있습니다",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "버전 보관 경로(빈칸 작성시, 기본 .stversions 폴더로 지정).",
|
||||
"Please wait": "기다려주십시오",
|
||||
"Preview": "미리보기",
|
||||
"Preview Usage Report": "사용 보고서 미리보기",
|
||||
"Quick guide to supported patterns": "지원 패턴에 대한 안내",
|
||||
"RAM Utilization": "메모리사용량",
|
||||
"Rescan": "재 탐색",
|
||||
"Rescan All": "전체 재탐색",
|
||||
"Rescan Interval": "재 탐색 시간 간격[초]",
|
||||
"Restart": "재시작",
|
||||
"Restart Needed": "재시작 필요함",
|
||||
"Restarting": "재시작 중",
|
||||
"Reused": "재개",
|
||||
"Save": "저장",
|
||||
"Scanning": "탐색중",
|
||||
"Select the devices to share this folder with.": "장치와 공유할 폴더를 선택해유.",
|
||||
"Select the folders to share with this device.": "이 장치와 공유할 폴더를 선택합니다.",
|
||||
"Settings": "설정",
|
||||
"Share": "공유",
|
||||
"Share Folder": "폴더 공유",
|
||||
"Share Folders With Device": "접속노드에 폴더공유",
|
||||
"Share With Devices": "공유 할 노드",
|
||||
"Share this folder?": "폴더를 공유하시겠습니까?",
|
||||
"Shared With": "~와 공유",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "폴더에 대한 식별자 아이디 입니다. 공유하는 장치와 같은 아이디이어야 합니다.",
|
||||
"Show ID": "내 장치 아이디 ",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "장치에 대한 아이디로 표시됩니다. 옵션에 얻은 기본이름으로 다른장치에 통보합니다.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "아이디가 비어있는 경우 기본 값으로 다른 장치에 업데이트 됩니다.",
|
||||
"Shutdown": "종료",
|
||||
"Shutdown Complete": "종료 완료",
|
||||
"Simple File Versioning": "간단한 파일 버젼관리",
|
||||
"Single level wildcard (matches within a directory only)": "단일 레벨 와일드카드(폴더와 일치하는경우)",
|
||||
"Source Code": "소스 코드",
|
||||
"Staggered File Versioning": "시차가 다른 파일 버젼관리",
|
||||
"Start Browser": "시작 브라우저",
|
||||
"Stopped": "동기화 중지됨",
|
||||
"Support": "지원",
|
||||
"Sync Protocol Listen Addresses": "동기화 수신 주소",
|
||||
"Syncing": "동기화중",
|
||||
"Syncthing has been shut down.": "동기화가 종료되었습니다",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing은 다음 소프트웨어에 포함됩니다.",
|
||||
"Syncthing is restarting.": "동기화 재시작 중",
|
||||
"Syncthing is upgrading.": "동기화 업데이트 중",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "동기화가 중지되었습니다. 인터넷/랜 이 활성화 될때까지 재시도합니다.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "동기화중 문제가 발생했습니다. 페이지를 새로 고쳐보거나 그래도 안되면 동기화도구를 다시 시작하세요.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "집계 /통계에서 사용할수 있는 {{URL}}",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "구성이 저장되었지만 활성화되지 않았습니다. 활성화 하려면 다시 시작하세요.",
|
||||
"The device ID cannot be blank.": "장치 아이디는 필수 입력입니다.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "여기에 입력된 장치아이디가 다른 장치에 표시됩니다. 아이디를 확인 편집>아이디보기 해서 해야하며 공백과 -는 무시(선택사항) 입니다.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "암호화 사용 보고서는 매일 전송할겁니다. 디버그시 감사하게 쓸게요.보고할게 또 생기면 대화상자 다시 보여줄게요.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "입력한 장치아이디가 보이지 않아요. 52글자에서 56글자까지인데 숫자와 점 -으로 구성해야되요.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "첫 인자는 폴더 경로이고 두번째 인자는 폴더의 상대 경로입니다.",
|
||||
"The folder ID cannot be blank.": "폴더 아이디는 빈칸으로 하면 절대 안되용.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "폴더 아이디는 64글자 이하이며 숫자 . 와 - 과 _ 만 허용되요.",
|
||||
"The folder ID must be unique.": "폴더 아이디는 유일해야 합니다",
|
||||
"The folder path cannot be blank.": "이 폴더 경로는 비어있지 않습니다",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "다음과 같은 간격이 유지됩니다. 첫번째 버젼은 매일 유지/30일 유지/30초마다 유지 최대 버젼을 모두 유지입니다.",
|
||||
"The maximum age must be a number and cannot be blank.": "최대값은 숫자이어야 합니다. 빈값은 허용하지 않습니다.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "버젼을 유지하는 최대 시간 (일단위 / 버젼을 영원히 유지하려면 0을 입력하세요)",
|
||||
"The number of old versions to keep, per file.": "이전 버젼의 수[보관] 파일단위.",
|
||||
"The number of versions must be a number and cannot be blank.": "버젼의 수는 반드시 있어야 합니다.",
|
||||
"The path cannot be blank.": "경로명이 비어있습니다. 입력하세요.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "재검색 하는 간격입니다.(초단위) 이며 양수로 입력해야합니다.",
|
||||
"Unknown": "알수 없음",
|
||||
"Unshared": "공유되지 않음",
|
||||
"Unused": "사용되지 않음",
|
||||
"Up to Date": "최신 데이터",
|
||||
"Upgrade To {%version%}": "최신 버젼 업데이트",
|
||||
"Upgrading": "업데이트중",
|
||||
"Upload Rate": "전송비율",
|
||||
"Use HTTPS for GUI": "HTTPS 형식 폼 사용",
|
||||
"Version": "버젼",
|
||||
"Versions Path": "파일 버젼 저장 경로",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "보관 기간중에 오래된 파일은 삭제됩니다. [지정한 수가 넘친다면]",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "새 장치를 추가할때 다른측면에서 추가된다하네요.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "새 폴더를 추가시 폴더 아이디는 장치와 폴더를 묶어 공유가 가능합니다. 대소문자를 구분하며 공유장치와 아이디는 일치해야합니다.틀리면 안된다네요.",
|
||||
"Yes": "네",
|
||||
"You must keep at least one version.": "최소 한개의 버젼을 유지해야한다",
|
||||
"full documentation": "전체 문서",
|
||||
"items": "아이템",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 에서 \"{{folder}}\" 폴더 추가들어왔어요."
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
"Addresses": "Adressen",
|
||||
"All Data": "Alle Gegevens",
|
||||
"Allow Anonymous Usage Reporting?": "Bijhouden van anonieme gebruikers statistieken toestaan?",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Versiebeheer gebeurt door een extern commando. Het moet het bestand verwijderen van de gesynchroniseerde map.",
|
||||
"Anonymous Usage Reporting": "Bijhouden anonieme gebruikers statistieken",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Toestellen geconfigureerd op een introductie toestel zullen ook aan dit toestel worden toegevoegd.",
|
||||
"Automatic upgrades": "Automatisch bijwerken",
|
||||
@@ -23,7 +23,7 @@
|
||||
"Connection Error": "Verbindingsfout",
|
||||
"Copied from elsewhere": "Van elders gekopieerd",
|
||||
"Copied from original": "Gekopieerd van het origineel",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 de volgende Bijdragers:",
|
||||
"Delete": "Verwijderen",
|
||||
"Device ID": "Apparaat ID",
|
||||
"Device Identification": "Apparaat identificatie",
|
||||
@@ -43,9 +43,9 @@
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Geef, gescheiden door komma's, \"ip:port\" adressen of \"dynamic\" voor het automatische vinden van de addressen.",
|
||||
"Enter ignore patterns, one per line.": "Geef te negeren patronen, één per regel.",
|
||||
"Error": "Fout",
|
||||
"External File Versioning": "External File Versioning",
|
||||
"External File Versioning": "Extern Bestandsversiebeheer",
|
||||
"File Versioning": "Versiebeheer",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Toegangsrechten voor bestanden worden genegeerd bij het zoeken naar wijzigingen. Gebruik voor FAT bestandssystemen.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by 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.": "Bestanden zijn beschermt tegen aanpassingen gemaakt door andere apparaten maar aanpassingen op dit apparaat worden doorgestuurd naar de rest van de cluster.",
|
||||
"Folder ID": "Folder ID",
|
||||
@@ -132,14 +132,14 @@
|
||||
"Syncthing is restarting.": "Syncthing is aan het herstarten.",
|
||||
"Syncthing is upgrading.": "Syncthing is aan het upgraden.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing lijkt afgesloten te zijn, of er is een verbindingsprobleem met het internet. Nieuwe poging....",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing heeft een probleem met het verwerken van je verzoek. Gelieve de pagina te vernieuwen of Syncthing te herstarten als het probleem zich blijft voordoen.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "The verzamelde statistieken zijn publiek beschikbaar op {{url}}",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De configuratie is opslagen maar nog niet actief. Syncthing moet opnieuw opgestart worden om de nieuwe configuratie te activeren.",
|
||||
"The device ID cannot be blank.": "Het toestel ID mag niet leeg zijn.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Het verwachte toestel ID kan teruggevonden worden in het \"Aanpassen > Toon ID\" scherm op het andere toestel. Spaties en streepjes zijn facultatief (worden genegeerd).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Het versleutelde gebruiksrapport wordt dagelijks opgestuurd en wordt gebruikt om de verschillende platformen, folder groottes en versies op te volgen. Als de reeks gegevens wijzigt zal opnieuw toestemming gevraagd worden.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Dit toestel ID lijkt ongeldig. Het toestel ID bestaat uit 52 of 56 letters en nummers met facultatieve spaties en streepjes.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "De eerste parameter is het pad naar de map en de tweede parameter is het relatieve pad binnenin de map.",
|
||||
"The folder ID cannot be blank.": "De folder ID mag niet leeg zijn.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "De folder ID mag maximaal 64 tekens lang zijn en bestaat enkel uit letters, nummers, punten (.), streepjes (-) en onderstrepingstekens (_).",
|
||||
"The folder ID must be unique.": "De folder ID moet uniek zijn.",
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Desligado",
|
||||
"Out Of Sync": "Não sincronizado",
|
||||
"Out of Sync Items": "Itens não sincronizados",
|
||||
"Out of Sync Items": "Itens por sincronizar",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite da velocidade de envio (KiB/s)",
|
||||
"Override Changes": "Sobrepor alterações",
|
||||
"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": "Caminho para a pasta no computador local. Será criada, caso não exista. O caractere (~) pode ser utilizado como atalho para",
|
||||
|
||||
172
gui/assets/lang/lang-ro-RO.json
Normal file
@@ -0,0 +1,172 @@
|
||||
{
|
||||
"API Key": "Cheie API",
|
||||
"About": "Despre",
|
||||
"Add": "Adaugă",
|
||||
"Add Device": "Adaugă Dispozitiv",
|
||||
"Add Folder": "Adaugă Mapă",
|
||||
"Add new folder?": "Adauga o mapă nouă?",
|
||||
"Address": "Adresă",
|
||||
"Addresses": "Adrese",
|
||||
"All Data": "Toate Datele",
|
||||
"Allow Anonymous Usage Reporting?": "Permiteţi raportarea anonimă de folosire a aplicaţiei?",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "O comandă externă administrează versiunile. Trebuie să şteargă documentul din fişierul sincronizat. ",
|
||||
"Anonymous Usage Reporting": "Raport Anonim despre Folosirea Aplicației",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Toate dispozitivele configurate pe un dispozitiv iniţiator vor fi adăugate şi pe acest dispozitiv. ",
|
||||
"Automatic upgrades": "Actualizare automată",
|
||||
"Bugs": "Bug-uri",
|
||||
"CPU Utilization": "CPU ",
|
||||
"Changelog": "Noutăți",
|
||||
"Close": "Închide",
|
||||
"Command": "Comandă",
|
||||
"Comment, when used at the start of a line": "Comentariu, când este folosit la începutul unei linii",
|
||||
"Compression": "Compresie",
|
||||
"Connection Error": "Eroare de conexiune",
|
||||
"Copied from elsewhere": "Copiat din altă parte",
|
||||
"Copied from original": "Copiat din original",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright ©2015 Următorii Contribuitori:",
|
||||
"Delete": "Şterge",
|
||||
"Device ID": "ID Dispozitiv",
|
||||
"Device Identification": "Identificare Dispozitiv",
|
||||
"Device Name": "Nume Dispozitiv",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Dispozitivul{{dispoztiv}}({{adresă}})vrea sa se conecteze.Adaug un dispozitiv nou?",
|
||||
"Devices": "Dispozitiv",
|
||||
"Disconnected": "Deconectat",
|
||||
"Documentation": "Documentaţie",
|
||||
"Download Rate": "Viteză de Descărcare",
|
||||
"Downloaded": "Descărcat",
|
||||
"Downloading": "Se descarcă",
|
||||
"Edit": "Modifică",
|
||||
"Edit Device": "Modifică Dispozitiv",
|
||||
"Edit Folder": "Modifică Mapa",
|
||||
"Editing": "Modificare",
|
||||
"Enable UPnP": "Activează UPnP",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Adaugă, separate prin virgulă, adresele IP \"ip:port\" sau \"dynamic\" (dinamic) pentru ca adresele să fie descoperite automat.",
|
||||
"Enter ignore patterns, one per line.": "Adaugă șabloanele de ignorare, câte una pe linie.",
|
||||
"Error": "Eroare",
|
||||
"External File Versioning": "Administrare externă a versiunilor documentului",
|
||||
"File Versioning": "Versiune Fișier",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Biții de autorizare sînt excluși cînd se analizează modificările. A se utiliza pe sisteme FAT. ",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Documentele sînt mutate într-un fișier .stversions conținînd versiuni datate atunci cînd sînt șterse sau înlocuite de 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.": "Fișierele sunt protejate de schimbările făcute pe alte dispozitive dar schimbările efectuate pe acest dispozitiv vor fi trimise catre restul grupului.",
|
||||
"Folder ID": "ID Mapă",
|
||||
"Folder Master": "Master Măpi",
|
||||
"Folder Path": "Locaţie Mapei",
|
||||
"Folders": "Mapă",
|
||||
"GUI Authentication Password": "Parolă Interfaţă",
|
||||
"GUI Authentication User": "User Interfaţă",
|
||||
"GUI Listen Addresses": "Adresă Interfaţă",
|
||||
"Generate": "Generează",
|
||||
"Global Discovery": "Găsire Globală",
|
||||
"Global Discovery Server": "Server pentru Găsirea Globală",
|
||||
"Global State": "Status Global",
|
||||
"Ignore": "Ignoră",
|
||||
"Ignore Patterns": "Reguli de excludere",
|
||||
"Ignore Permissions": "Ignoră Permisiuni",
|
||||
"Incoming Rate Limit (KiB/s)": "Limită Viteză de Download (KB/s)",
|
||||
"Introducer": "Dispozitiv Inițiator",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversarea condiției (de ex., nu exclude)",
|
||||
"Keep Versions": "Păstrează Versiuni",
|
||||
"Last File Received": "Ultimul Fișier Primit",
|
||||
"Last seen": "Ultima vizionare",
|
||||
"Later": "Mai tîrziu",
|
||||
"Local Discovery": "Găsire Locală",
|
||||
"Local State": "Status Local",
|
||||
"Maximum Age": "Vârsta Maximă",
|
||||
"Metadata Only": "Doar Metadate",
|
||||
"Move to top of queue": "Mută la începutul listei",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Asterisc de nivel multiplu (corespunde fișierelor și sub-fișierelor)",
|
||||
"Never": "Niciodată",
|
||||
"New Device": "Dispozitiv Nou",
|
||||
"New Folder": "Mapă Nouă",
|
||||
"No": "Nu",
|
||||
"No File Versioning": "Fără versiuni ale documentelor",
|
||||
"Notice": "Mențiuni",
|
||||
"OK": "OK",
|
||||
"Off": "Închis",
|
||||
"Out Of Sync": "Nesincronizat",
|
||||
"Out of Sync Items": "Elemente Nesincronizate",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limită Viteză de Upload (KB/s)",
|
||||
"Override Changes": "Suprascrie Schimbări",
|
||||
"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": "Localizarea fișierului în acest computer. Dacă nu există, va fi creat. Tilda (~) înlocuiește ",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Locul unde vor fi stocate versiunile (a se lăsa neschimbat pentru fișierul .stversions din fișier). ",
|
||||
"Please wait": "Aşteaptă",
|
||||
"Preview": "Previzualizează",
|
||||
"Preview Usage Report": "Vezi raportul de utilizare",
|
||||
"Quick guide to supported patterns": "Ghid rapid pentru regulile suportate",
|
||||
"RAM Utilization": "RAM",
|
||||
"Rescan": "Rescanează",
|
||||
"Rescan All": "Rescaneaza Tot",
|
||||
"Rescan Interval": "Interval Scanare",
|
||||
"Restart": "Restart",
|
||||
"Restart Needed": "Restart Necesar",
|
||||
"Restarting": "Se restartează",
|
||||
"Reused": "Refolosit",
|
||||
"Save": "Salvează",
|
||||
"Scanning": "Scanează",
|
||||
"Select the devices to share this folder with.": "Selectează dispozitivele pentru care să fie disponibil această mapă.",
|
||||
"Select the folders to share with this device.": "Alege mapele pe care vrei sa le imparți cu acest dispozitiv.",
|
||||
"Settings": "Setări",
|
||||
"Share": "Împarte",
|
||||
"Share Folder": "Împarte Mapa",
|
||||
"Share Folders With Device": "Împarte Mapa Cu Dispozitivul",
|
||||
"Share With Devices": "Împarte Cu Dispozitivul",
|
||||
"Share this folder?": "Împarte această mapă?",
|
||||
"Shared With": "Împarte Cu",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificator scurt al fișierului. Trebuie să fie același pe toate mașinile. ",
|
||||
"Show ID": "Arată ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vizibil în locul ID-ului dispozitivului într-un grup. Va fi sugerat celorlalte dispozitive ca nume opţional. ",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vizibil în locul ID-ului dispozitivului într-un grup. Va fi înlocuit de numele sugerat de dispozitiv daca nu este completat. ",
|
||||
"Shutdown": "Opreşte",
|
||||
"Shutdown Complete": "Oprește Complet",
|
||||
"Simple File Versioning": "Versiuni simple ale documentelor",
|
||||
"Single level wildcard (matches within a directory only)": "Asterisc de nivel simplu (corespunde doar unui fişier)",
|
||||
"Source Code": "Cod Sursă",
|
||||
"Staggered File Versioning": "Versiuni eşalonate ale documentelor",
|
||||
"Start Browser": "Lansează Browser",
|
||||
"Stopped": "Oprit",
|
||||
"Support": "Suport Tehnic",
|
||||
"Sync Protocol Listen Addresses": "Adresa protocolului de sincronizare",
|
||||
"Syncing": "Se sincronizează",
|
||||
"Syncthing has been shut down.": "Sincronizarea a fost oprită.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing include următoarele soft-uri sau părţi din ele:",
|
||||
"Syncthing is restarting.": "Syncthing se restartează.",
|
||||
"Syncthing is upgrading.": "Syncthing se actualizează.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing pare a fi oprit sau aveţi probleme cu conexiunea la internet. Reluare... ",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing pare a avea probleme prelucrînd solicitarea dumneavoastră. Reîncărcaţi pagina sau porniţi Syncthing din nou dacă problema continuă. ",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Statisticile în ansamblu sînt accesibile public la {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Configuraţia a fost salvată dar nu şi activată. Syncthing trebuie să repornească pentru a activa noua configuraţie.",
|
||||
"The device ID cannot be blank.": "ID-ul dispozitivului nu poate fi gol.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ID-ul dispozitivului ce trebuie introdus aici poate fi aflat la \"Editează > Arată ID-ul\" pe celălalt dispozitiv. Spaţiile şi liniile oblice sînt opţionale (vor fi ignorate). ",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Raportul codat de utilizare este trimit zilnic. Este folosit pentru studierea platformelor comune, dimensiunilor fişierelor şi versiunea aplicaţiilor. În cazul în care setul de date trimis este modificat, acest dialog va aparea din nou. ",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "ID-ul dispozitivului nu pare a fi valid.El trebuie sa fie format dintrun șir din 52 ori 56 de caractere formate din litere și cifre, cu spatii și linii opțional.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
|
||||
"The folder ID cannot be blank.": "ID-ul mapei nu poate fi gol.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
|
||||
"The folder ID must be unique.": "ID-ul mapei trebuie să fie unic.",
|
||||
"The folder path cannot be blank.": "Locaţia mapei nu poate fi goală.",
|
||||
"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.": "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.",
|
||||
"The maximum age must be a number and cannot be blank.": "Vârsta maximă trebuie să fie un număr şi nu poate fi goală.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Câte zile să se păstreze o versiune (setează 0 pentru nelimitat)",
|
||||
"The number of old versions to keep, per file.": "Numărul de versiuni vechi de salvat per fişier.",
|
||||
"The number of versions must be a number and cannot be blank.": "Numărul de versiuni trebuie să fie un număr şi nu poate fi gol.",
|
||||
"The path cannot be blank.": "Locația nu poate fi goală.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Intervalul de rescanare trebuie să nu fie un număr negativ de secunde. ",
|
||||
"Unknown": "Necunoscut",
|
||||
"Unshared": "Neîmpărțit",
|
||||
"Unused": "Nefolosit",
|
||||
"Up to Date": "La Zi",
|
||||
"Upgrade To {%version%}": "Actualizează La Versiunea {{version}}",
|
||||
"Upgrading": "Se Actualizează",
|
||||
"Upload Rate": "Viteză Upload",
|
||||
"Use HTTPS for GUI": "Foloseşte HTTPS pentru interfaţă",
|
||||
"Version": "Versiune",
|
||||
"Versions Path": "Locaţie Versiuni",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versiunile sînt şterse în mod automat dacă sînt mai vechi decît vîrsta maximă sau depăşesc numărul de documente permise într-un anume interval. ",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Când adaugi un dispozitiv nou, trebuie să adaugi şi dispozitivul curent în dispozitivul nou.",
|
||||
"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.": "Cînd adăugaţi un fişier nou, nu uitaţi că ID-ul fişierului va rămîne acelaşi pe toate dispozitivele. Iar literele mari sînt diferite de literele mici. ",
|
||||
"Yes": "Da",
|
||||
"You must keep at least one version.": "Trebuie să păstrezi cel puţin o versiune.",
|
||||
"full documentation": "toată documentaţia",
|
||||
"items": "obiecte",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{Dispozitivul}} vrea să transmită mapa {{Mapa}}"
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"Connection Error": "Ошибка подключения",
|
||||
"Copied from elsewhere": "Скопировано из другого места",
|
||||
"Copied from original": "Скопировано с оригинала",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Все права защищены ©, 2015 участники:",
|
||||
"Delete": "Удалить",
|
||||
"Device ID": "ID устройства",
|
||||
"Device Identification": "Идентификация устройства",
|
||||
@@ -37,15 +37,15 @@
|
||||
"Downloading": "Загрузка",
|
||||
"Edit": "Изменить",
|
||||
"Edit Device": "Изменить устройство",
|
||||
"Edit Folder": "Изменение папки",
|
||||
"Edit Folder": "Изменить папку",
|
||||
"Editing": "Редактирование",
|
||||
"Enable UPnP": "Включить UPnP",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": " Введите пары \"IP:PORT\" разделённые запятыми, или слово \"dynamic\" для автоматического обнаружения адреса.",
|
||||
"Enter ignore patterns, one per line.": "Введите шаблоны игнорирования, по-одному на строку.",
|
||||
"Enter ignore patterns, one per line.": "Введите шаблоны игнорирования, по одному на строку.",
|
||||
"Error": "Ошибка",
|
||||
"External File Versioning": "External File Versioning",
|
||||
"File Versioning": "Управление версиями",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Права на файлы игнорируются при поиске изменений. Используется на файловой системе FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by 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.": "Файлы защищены от изменений сделанных на других устройствах, но изменения сделанные на этом устройстве будут отправлены всему кластеру.",
|
||||
"Folder ID": "ID папки",
|
||||
@@ -88,7 +88,7 @@
|
||||
"Outgoing Rate Limit (KiB/s)": "Предел скорости отдачи (KiB/s)",
|
||||
"Override Changes": "Перезаписать изменения",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Путь к папке на локальном компьютере. Если её не существует, то она будет создана. Тильда (~) может использоваться как сокращение для",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Путь, где должны храниться версии (оставьте пустым по-умолчанию для папки .stversions в папке).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Путь, где должны храниться версии (оставьте пустым, чтобы использовать папку по умолчанию .stversions внутри папки).",
|
||||
"Please wait": "Пожалуйста, подождите",
|
||||
"Preview": "Предварительный просмотр",
|
||||
"Preview Usage Report": "Посмотреть отчёт об использовании",
|
||||
@@ -132,13 +132,13 @@
|
||||
"Syncthing is restarting.": "Перезапуск Syncthing",
|
||||
"Syncthing is upgrading.": "Обновление Syncthing ",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing столкнулся с проблемой при обработке Вашего запроса. Пожалуйста, обновите страницу или перезапустите Syncthing если проблема повторится.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Суммарная статистика общедоступна на {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурация была сохранена но не активирована. Для активации новой конфигурации необходимо рестартовать Syncthing.",
|
||||
"The device ID cannot be blank.": "ID устройства не может быть пустым.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор устройства для ввода здесь, может быть найден в диалоге \"Редактирование > Показать ID\" на другом устройстве. Пробелы и тире не обязательны (игнорируются).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор устройства, который следует тут ввести, может быть найден в диалоге \"Редактирование > Показать ID\" на другом устройстве. Пробелы и тире не обязательны (игнорируются).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Зашифрованный отчет об использовании отправляется ежедневно. Это используется для отслеживания общих платформ, размеров папок и версий приложения. Если отчетные данные изменятся, вам будет снова показано это диалоговое окно.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введённое ID устройства не валидное. Оно должно состоять из букв и цифр, может включать пробелы и дефисы, его длина должна быть от 52 до 56 символов, ",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введён недопустимый ID устройства. Он должен состоять из букв и цифр, может включать пробелы и дефисы, длина должна быть от 52 до 56 символов, ",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
|
||||
"The folder ID cannot be blank.": "ID папки не может быть пустым.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "ID папки должен быть коротким (не более 64 символов), должен состоять только из букв, цифр, точек (.), дефисов (-) или подчёркиваний (_).",
|
||||
@@ -149,7 +149,7 @@
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максимальный срок хранения версии (в днях, 0 значит вечное хранение).",
|
||||
"The number of old versions to keep, per file.": "Количество хранимых версий файла.",
|
||||
"The number of versions must be a number and cannot be blank.": "Количество версий должно быть числом и не может быть пустым.",
|
||||
"The path cannot be blank.": "The path cannot be blank.",
|
||||
"The path cannot be blank.": "Путь не может быть пустым.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Интервал пересканирования должен быть неотрицательным количеством секунд.",
|
||||
"Unknown": "Неизвестно",
|
||||
"Unshared": "Необщедоступно",
|
||||
@@ -163,7 +163,7 @@
|
||||
"Versions Path": "Путь к версиям",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версии удаляются автоматически, если они существуют дольше максимального срока или превышают разрешённое количество файлов за интервал.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Когда добавляете устройство, помните о том, что это же устройство должно быть добавлено и другой стороной.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Когда добавляете новую папку, помните, что ID папки используются для того, чтобы связывать папки между всеми устройствами. Они чувствительны к регистру и должны совпадать на всех используемых устройствах.",
|
||||
"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.": "Вы должны хранить как минимум одну версию.",
|
||||
"full documentation": "полная документация",
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
"Add new folder?": "Lägg till katalog?",
|
||||
"Address": "Adress",
|
||||
"Addresses": "Adresser",
|
||||
"All Data": "All Data",
|
||||
"All Data": "All data",
|
||||
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistik?",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ett externt kommando sköter versionshanteringen. Det måste ta bort filen från den synkroniserade mappen.",
|
||||
"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",
|
||||
@@ -17,13 +17,13 @@
|
||||
"CPU Utilization": "CPU-användning",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Stäng",
|
||||
"Command": "Command",
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, vid början av en rad.",
|
||||
"Compression": "Compression",
|
||||
"Compression": "Komprimering",
|
||||
"Connection Error": "Anslutningsproblem",
|
||||
"Copied from elsewhere": "Kopierat utifrån",
|
||||
"Copied from original": "Oförändrat",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 följande medverkande:",
|
||||
"Delete": "Radera",
|
||||
"Device ID": "Enhets-ID",
|
||||
"Device Identification": "Enhetsidentifikation",
|
||||
@@ -43,10 +43,10 @@
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade \"ip: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": "External File Versioning",
|
||||
"External File Versioning": "Extern versionshantering",
|
||||
"File Versioning": "Versionshantering",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"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 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 ID": "Katalog-ID",
|
||||
"Folder Master": "Huvudlagring",
|
||||
@@ -72,8 +72,8 @@
|
||||
"Local Discovery": "Lokal uppslagning",
|
||||
"Local State": "Lokal status",
|
||||
"Maximum Age": "Högsta åldersgräns",
|
||||
"Metadata Only": "Metadata Only",
|
||||
"Move to top of queue": "Move to top of queue",
|
||||
"Metadata Only": "Endast metadata",
|
||||
"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",
|
||||
"New Device": "Ny enhet",
|
||||
@@ -82,7 +82,7 @@
|
||||
"No File Versioning": "Ingen versionshantering",
|
||||
"Notice": "Observera",
|
||||
"OK": "OK",
|
||||
"Off": "Off",
|
||||
"Off": "Av",
|
||||
"Out Of Sync": "Osynkad",
|
||||
"Out of Sync Items": "Osynkade poster",
|
||||
"Outgoing Rate Limit (KiB/s)": "Max uppladdningshastighet (KiB/s)",
|
||||
@@ -95,7 +95,7 @@
|
||||
"Quick guide to supported patterns": "Snabb guide till filmönster som stöds",
|
||||
"RAM Utilization": "Minnesanvändning",
|
||||
"Rescan": "Uppdatera",
|
||||
"Rescan All": "Rescan All",
|
||||
"Rescan All": "Uppdatera alla",
|
||||
"Rescan Interval": "Uppdateringsintervall",
|
||||
"Restart": "Starta om",
|
||||
"Restart Needed": "Omstart behövs",
|
||||
@@ -132,14 +132,14 @@
|
||||
"Syncthing is restarting.": "Syncthing startar om.",
|
||||
"Syncthing is upgrading.": "Syncthing uppgraderas.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller finns det problem med din Internetanslutning. Försöker igen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Sammanställd statistik finns publikt tillgänglig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
|
||||
"The device ID cannot be blank.": "Enhets-ID kan inte vara tomt.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets-ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, katalogstorlekar och versioner. Om datan som rapporteras ändras så kommer du att bli tillfrågad igen.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade enhets-ID:t verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den första kommandoparametern är sökvägen till mappen och den andra parametern är den relativa sökvägen i mappen.",
|
||||
"The folder ID cannot be blank.": "Ange ett enhets-ID.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Katalog-ID:t måste vara en kort sträng (64 tecken eller mindre), bestående av endast bokstäver, siffror, punkt (.), bindestreck (-) och understreck (_).",
|
||||
"The folder ID must be unique.": "Katalog-ID:t måste vara unikt.",
|
||||
@@ -149,7 +149,7 @@
|
||||
"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 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.": "The path cannot be blank.",
|
||||
"The path cannot be blank.": "Ange en sökväg",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Förnyelseintervallet måste vara ett positivt antal sekunder",
|
||||
"Unknown": "Okänt",
|
||||
"Unshared": "Inte delad",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"Connection Error": "Bağlantı hatası",
|
||||
"Copied from elsewhere": "Başka bir yerden kopyalanmış",
|
||||
"Copied from original": "Aslından kopyalanmış",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Telif Hakkı © 2015 Katkıda bulunanlar:",
|
||||
"Delete": "Sil",
|
||||
"Device ID": "Cihaz ID",
|
||||
"Device Identification": "Cihaz Kimliği",
|
||||
@@ -43,10 +43,10 @@
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "IP adresleri eklemek için virgül ile ayırarak \"ip:port\" yazın, ya da \"dynamic\" yazarak otomatik bulma işlemini seçin.",
|
||||
"Enter ignore patterns, one per line.": "Yoksayılacak kalıp dizilerini her satıra bir tane olacak şekilde girin.",
|
||||
"Error": "Hata",
|
||||
"External File Versioning": "External File Versioning",
|
||||
"External File Versioning": "Harici Dosya Sürümlendirme",
|
||||
"File Versioning": "Dosya Sürümlendirme",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Değişimleri yoklarken dosya izin bilgilerini ihmal et. FAT dosya sistemlerinde kullanın.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dosyalar Syncthing tarafından değiştirildiğinde ya da silindiğinde, tarih damgalı sürümleri .stversions dizinine taşınır.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosyalar diğer cihazlarda yapılan değişikliklerden korunur, ancak bu cihazdaki değişiklikler kümedeki diğer cihazlara gönderilir.",
|
||||
"Folder ID": "Klasör ID",
|
||||
"Folder Master": "Ana Klasör",
|
||||
@@ -132,7 +132,7 @@
|
||||
"Syncthing is restarting.": "Syncthing yeniden başlatılıyor.",
|
||||
"Syncthing is upgrading.": "Syncthing yükseltiliyor.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing görünüşe durdu veya internetin bağlantınızda problem var. Tekrar deniyor....",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing isteminizi işleme alırken bir sorunla karşılaştı. Lütfen sayfanızı yenileyin veya sorun devam ediyorsa Syncthing'i yeniden başlatın.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Toplanan halka açık istatistiklere ulaşabileceğiniz adres {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Ayarlar kaydedildi ancak aktifleştirilmedi. Aktifleştirmek için Syncthing yeniden başlatılmalı.",
|
||||
"The device ID cannot be blank.": "Cihaz ID boş olamaz.",
|
||||
@@ -149,7 +149,7 @@
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Bir sürümün tutulması için belirlenen azami süre (sürümleri daimi olarak tutabilmek için 0 değeri atayın)",
|
||||
"The number of old versions to keep, per file.": "Dosya başına saklanacak eski sürüm.",
|
||||
"The number of versions must be a number and cannot be blank.": "Sürümlerin sayısı sayı olmalı ve boş bırakılamaz.",
|
||||
"The path cannot be blank.": "The path cannot be blank.",
|
||||
"The path cannot be blank.": "Dizin yolu boş bırakılamaz.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Tarama zaman aralığı, saniye cinsinden negatif olmayan bir sayı olmalıdır.",
|
||||
"Unknown": "Bilinmiyor",
|
||||
"Unshared": "Paylaşılmayan",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"Addresses": "Адреси",
|
||||
"All Data": "Усі дані",
|
||||
"Allow Anonymous Usage Reporting?": "Дозволити програмі збирати анонімну статистику використання?",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Зовнішня команда керування версіями. Вона має видалити файл із директорії, що синхронізується.",
|
||||
"Anonymous Usage Reporting": "Анонімна статистика використання",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Усі пристрої, налаштовані на пристрої-рекомендувачі, будуть додані до поточного пристрою.",
|
||||
"Automatic upgrades": "Автоматичні оновлення",
|
||||
@@ -23,7 +23,7 @@
|
||||
"Connection Error": "Помилка з’єднання",
|
||||
"Copied from elsewhere": "Скопійовано з іншого місця",
|
||||
"Copied from original": "Скопійовано з оригіналу",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 наступних контриб’юторів:",
|
||||
"Delete": "Видалити",
|
||||
"Device ID": "ID пристрою",
|
||||
"Device Identification": "Ідентифікатор пристрою",
|
||||
@@ -45,8 +45,8 @@
|
||||
"Error": "Помилка",
|
||||
"External File Versioning": "Зовнішне керування версіями",
|
||||
"File Versioning": "Керування версіями",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Біти прав доступу до файлів будуть проігноровані під час пошуку змін. Використовуйте на файлових системах FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Файли будуть поміщатися у директорію .stversions із відповідною позначкою часу, коли вони будуть замінятися або видалятися програмою.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файли захищено від змін зроблених на інших пристроях, але зміни зроблені на цьому пристрої будуть надіслані решті кластеру.",
|
||||
"Folder ID": "ID директорії",
|
||||
"Folder Master": "Центральна директорія",
|
||||
@@ -132,7 +132,7 @@
|
||||
"Syncthing is restarting.": "Syncthing перезавантажується.",
|
||||
"Syncthing is upgrading.": "Syncthing оновлюється.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Схоже на те, що Syncthing закритий, або виникла проблема із Інтернет-з’єднанням. Проводиться повторна спроба з’єднання…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Схоже на те, що Syncthing стикнувся з проблемою оброблюючи ваш запит. Будь ласка перезавантажте сторінку в браузері або перезапустіть Syncthing.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Зібрана статистика публічно доступна за посиланням {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфігурацію збережено, але не активовано. Необхідно перезапустити Syncthing для того, щоби активувати нову конфігурацію.",
|
||||
"The device ID cannot be blank.": "ID пристрою не може бути порожнім.",
|
||||
|
||||
@@ -1 +1 @@
|
||||
var validLangs = ["be","bg","ca","cs","de","el","en","en-GB","es","fr","hu","it","lt","nb","nl","nn","pl","pt-BR","pt-PT","ru","sv","tr","uk","zh-CN","zh-TW"]
|
||||
var validLangs = ["be","bg","ca","cs","de","el","en","en-GB","es","fi","fr","hu","it","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","tr","uk","zh-CN","zh-TW"]
|
||||
|
||||
123
gui/index.html
@@ -38,6 +38,12 @@
|
||||
<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li ng-if="upgradeInfo && upgradeInfo.majorNewer">
|
||||
<button type="button" class="btn navbar-btn btn-danger btn-sm" href="" ng-click="upgradeMajor()">
|
||||
<span class="glyphicon glyphicon-chevron-up"></span> 
|
||||
<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown" language-select></li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-cog" aria-label="Edit"></span></a>
|
||||
@@ -193,9 +199,9 @@
|
||||
<th><span class="glyphicon glyphicon-folder-open"></span> <span translate>Folder Path</span></th>
|
||||
<td class="text-right">{{folder.path}}</td>
|
||||
</tr>
|
||||
<tr ng-if="model[folder.id].invalid">
|
||||
<tr ng-if="model[folder.id].invalid || model[folder.id].error">
|
||||
<th><span class="glyphicon glyphicon-warning-sign"></span> <span translate>Error</span></th>
|
||||
<td class="text-right">{{model[folder.id].invalid}}</td>
|
||||
<td class="text-right">{{model[folder.id].invalid || model[folder.id].error}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-globe"></span> <span translate>Global State</span></th>
|
||||
@@ -233,6 +239,17 @@
|
||||
<th><span class="glyphicon glyphicon-refresh"></span> <span translate>Rescan Interval</span></th>
|
||||
<td class="text-right">{{folder.rescanIntervalS}} s</td>
|
||||
</tr>
|
||||
<tr ng-if="folder.order != 'random'">
|
||||
<th><span class="glyphicon glyphicon-sort"></span> <span translate>File Pull Order</span></th>
|
||||
<td class="text-right" ng-switch="folder.order">
|
||||
<span ng-switch-when="random" translate>Random</span>
|
||||
<span ng-switch-when="alphabetic" translate>Alphabetic</span>
|
||||
<span ng-switch-when="smallestFirst" translate>Smallest First</span>
|
||||
<span ng-switch-when="largestFirst" translate>Largest First</span>
|
||||
<span ng-switch-when="oldestFirst" translate>Oldest First</span>
|
||||
<span ng-switch-when="newestFirst" translate>Newest First</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="folder.versioning.type">
|
||||
<th><span class="glyphicon glyphicon-tags"></span> <span translate>File Versioning</span></th>
|
||||
<td class="text-right" ng-switch="folder.versioning.type">
|
||||
@@ -293,11 +310,11 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Download Rate</span></th>
|
||||
<td class="text-right">{{connections['total'].inbps | binary}}B/s ({{connections['total'].inBytesTotal | binary}}B)</td>
|
||||
<td class="text-right">{{connectionsTotal.inbps | binary}}B/s ({{connectionsTotal.inBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
||||
<td class="text-right">{{connections['total'].outbps | binary}}B/s ({{connections['total'].outBytesTotal | binary}}B)</td>
|
||||
<td class="text-right">{{connectionsTotal.outbps | binary}}B/s ({{connectionsTotal.outBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-th"></span> <span translate>RAM Utilization</span></th>
|
||||
@@ -326,7 +343,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
||||
<td class="text-right">{{version}}</td>
|
||||
<td class="text-right">{{versionString()}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -417,10 +434,11 @@
|
||||
<div class="container">
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/wiki" target="_blank"><span class="glyphicon glyphicon-book"></span> <span translate>Documentation</span></a></li>
|
||||
<li><a class="navbar-link" href="https://discourse.syncthing.net" target="_blank"><span class="glyphicon glyphicon-question-sign"></span> <span translate>Support</span></a></li>
|
||||
<li><a class="navbar-link" href="https://forum.syncthing.net" target="_blank"><span class="glyphicon glyphicon-question-sign"></span> <span translate>Support</span></a></li>
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/releases" target="_blank"><span class="glyphicon glyphicon-info-sign"></span> <span translate>Changelog</span></a></li>
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/issues" target="_blank"><span class="glyphicon glyphicon-warning-sign"></span> <span translate>Bugs</span></a></li>
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing" target="_blank"><span class="glyphicon glyphicon-wrench"></span> <span translate>Source Code</span></a></li>
|
||||
<li><a class="navbar-link" href="https://twitter.com/syncthing" target="_blank"><span class="glyphicon glyphicon-send"></span> Twitter</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -463,9 +481,38 @@
|
||||
|
||||
<modal id="idqr" large="yes" status="info" close="yes" icon="qrcode" title="{{'Device Identification' | translate}} — {{deviceName(thisDevice())}}">
|
||||
<div class="well well-sm text-monospace text-center">{{myID}}</div>
|
||||
<img ng-if="myID" class="center-block img-thumbnail" src="qr/?text={{myID}}"/>
|
||||
<img ng-if="myID" class="center-block img-thumbnail" ng-src="qr/?text={{myID}}"/>
|
||||
</modal>
|
||||
|
||||
<!-- Major upgrade modal -->
|
||||
|
||||
<div id="majorUpgrade" class="modal fade" tabindex="-1" data-backdrop="true" data-keyboard="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-danger">
|
||||
<h4 class="modal-title">
|
||||
<span ng-if="icon" class="glyphicon glyphicon-chevron-up"></span>
|
||||
<span translate>Major Upgrade</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<span translate>This is a major version upgrade.</span>
|
||||
<span translate>A new major version may not be compatible with previous versions.</span>
|
||||
<span translate>Please consult the release notes before performing a major upgrade.</span>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/syncthing/syncthing/releases/latest" target="_blank" translate>Release Notes</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="upgrade()"><span class="glyphicon glyphicon-ok"></span> <span translate>Upgrade</span></button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device editor modal -->
|
||||
|
||||
<div id="editDevice" class="modal fade" tabindex="-1">
|
||||
@@ -590,6 +637,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- Left column -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
@@ -608,7 +656,20 @@
|
||||
<p translate class="help-block">File permission bits are ignored when looking for changes. Use on FAT file systems.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right column-->
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate>File Pull Order</label>
|
||||
<select class="form-control" ng-model="currentFolder.order">
|
||||
<option value="random" translate>Random</option>
|
||||
<option value="alphabetic" translate>Alphabetic</option>
|
||||
<option value="smallestFirst" translate>Smallest First</option>
|
||||
<option value="largestFirst" translate>Largest First</option>
|
||||
<option value="oldestFirst" translate>Oldest First</option>
|
||||
<option value="newestFirst" translate>Newest First</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>File Versioning</label>
|
||||
<div class="radio">
|
||||
@@ -907,10 +968,22 @@
|
||||
<hr/>
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr ng-repeat="f in needed.progress" ng-init="a = needAction(f)">
|
||||
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
|
||||
<td title="{{f.name}}">{{f.name | basename}}</td>
|
||||
<td ng-if="a == 'sync' && progress[neededFolder] && progress[neededFolder][f.name]">
|
||||
|
||||
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededTotal">
|
||||
<!-- Icon -->
|
||||
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[f.action]}}"></span> {{needActions[f.action]}}</td>
|
||||
|
||||
<!-- Name -->
|
||||
<td ng-if="f.type != 'queued'" title="{{f.name}}">{{f.name | basename}}</td>
|
||||
<td ng-if="f.type == 'queued'">
|
||||
<a href="" ng-click="bumpFile(neededFolder, f.name)" title="{{'Move to top of queue' | translate}}">
|
||||
<span class="glyphicon glyphicon-eject"></span>
|
||||
</a>
|
||||
<span title="{{f.name}}"> {{f.name | basename}}</span>
|
||||
</td>
|
||||
|
||||
<!-- Size/Progress -->
|
||||
<td ng-if="f.type == 'progress' && f.action == 'sync' && progress[neededFolder] && progress[neededFolder][f.name]">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.name].reused}}%"></div>
|
||||
<div class="progress-bar" style="width: {{progress[neededFolder][f.name].copiedFromOrigin}}%"></div>
|
||||
@@ -922,30 +995,26 @@
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right small-data" ng-if="a != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.name]">
|
||||
<td class="text-right small-data" ng-if="f.type != 'progress' || f.action != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.name]">
|
||||
<span ng-if="f.size > 0">{{f.size | binary}}B</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="f in needed.queued" ng-init="a = needAction(f)">
|
||||
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
|
||||
<td><a href="" ng-if="$index != 0" ng-click="bumpFile(neededFolder, f.name)" title="{{'Move to top of queue' | translate}}"><span class="glyphicon glyphicon-eject"></span></a><span ng-if="$index != 0"> </span><span title="{{f.name}}">{{f.name | basename}}</span></td>
|
||||
<td class="text-right small-data">
|
||||
<span ng-if="f.size > 0">{{f.size | binary}}B</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="f in needed.rest" ng-init="a = needAction(f)">
|
||||
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
|
||||
<td title="{{f.name}}">{{f.name | basename}}</td>
|
||||
<td class="text-right small-data"><span ng-if="f.size > 0">{{f.size | binary}}B</span></td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<dir-pagination-controls on-page-change="neededPageChanged(newPageNumber)"></dir-pagination-controls>
|
||||
<ul class="pagination pull-right">
|
||||
<li ng-repeat="option in [10, 20, 30, 50, 100]" ng-class="{ active: neededPageSize == option }">
|
||||
<a href="#" ng-click="neededChangePageSize(option)">{{option}}</a>
|
||||
<li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</modal>
|
||||
|
||||
<!-- About modal -->
|
||||
|
||||
<modal id="about" large="yes" close="yes" status="info" title="{{'About' | translate}}">
|
||||
<h1 class="text-center"><img alt="Syncthing" title="Syncthing" src="assets/img/logo-horizontal.svg" style="vertical-align: -16px" height="100" width="366"/><br/><small>{{version}}</small></h1>
|
||||
<h1 class="text-center"><img alt="Syncthing" title="Syncthing" src="assets/img/logo-horizontal.svg" style="vertical-align: -16px" height="100" width="366"/><br/><small>{{versionString()}}</small></h1>
|
||||
<hr/>
|
||||
|
||||
<p translate>Copyright © 2015 the following Contributors:</p>
|
||||
@@ -963,16 +1032,19 @@
|
||||
<li class="auto-generated">Brandon Philips</li>
|
||||
<li class="auto-generated">Brendan Long</li>
|
||||
<li class="auto-generated">Caleb Callaway</li>
|
||||
<li class="auto-generated">Carsten Hagemann</li>
|
||||
<li class="auto-generated">Cathryne Linenweaver</li>
|
||||
<li class="auto-generated">Chris Joel</li>
|
||||
<li class="auto-generated">Colin Kennedy</li>
|
||||
<li class="auto-generated">Daniel Martí</li>
|
||||
<li class="auto-generated">Dennis Wilson</li>
|
||||
<li class="auto-generated">Dominik Heidler</li>
|
||||
<li class="auto-generated">Elias Jarlebring</li>
|
||||
<li class="auto-generated">Emil Hessman</li>
|
||||
<li class="auto-generated">Federico Castagnini</li>
|
||||
<li class="auto-generated">Felix Ableitner</li>
|
||||
<li class="auto-generated">Felix Unterpaintner</li>
|
||||
<li class="auto-generated">Francois-Xavier Gsell</li>
|
||||
<li class="auto-generated">Gilli Sigurdsson</li>
|
||||
<li class="auto-generated">Jakob Borg</li>
|
||||
<li class="auto-generated">James Patterson</li>
|
||||
@@ -1025,6 +1097,7 @@
|
||||
<script src="vendor/angular/angular.min.js"></script>
|
||||
<script src="vendor/angular/angular-translate.min.js"></script>
|
||||
<script src="vendor/angular/angular-translate-loader.min.js"></script>
|
||||
<script src="vendor/angular/angular-dirPagination.js"></script>
|
||||
<script src="vendor/jquery/jquery-2.0.3.min.js"></script>
|
||||
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
|
||||
<!-- / vendor scripts -->
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
/*global $: false, angular: false, console: false, validLangs: false */
|
||||
|
||||
var syncthing = angular.module('syncthing', [
|
||||
'angularUtils.directives.dirPagination',
|
||||
'pascalprecht.translate',
|
||||
|
||||
'syncthing.core'
|
||||
@@ -46,25 +47,25 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
|
||||
|
||||
});
|
||||
|
||||
// @TODO: extract global level functions into seperate service(s)
|
||||
// @TODO: extract global level functions into separate service(s)
|
||||
|
||||
function deviceCompare(a, b) {
|
||||
if (typeof a.Name !== 'undefined' && typeof b.Name !== 'undefined') {
|
||||
if (a.Name < b.Name)
|
||||
if (typeof a.name !== 'undefined' && typeof b.name !== 'undefined') {
|
||||
if (a.name < b.name)
|
||||
return -1;
|
||||
return a.Name > b.Name;
|
||||
return a.name > b.name;
|
||||
}
|
||||
if (a.DeviceID < b.DeviceID) {
|
||||
if (a.deviceID < b.deviceID) {
|
||||
return -1;
|
||||
}
|
||||
return a.DeviceID > b.DeviceID;
|
||||
return a.deviceID > b.deviceID;
|
||||
}
|
||||
|
||||
function folderCompare(a, b) {
|
||||
if (a.ID < b.ID) {
|
||||
if (a.id < b.id) {
|
||||
return -1;
|
||||
}
|
||||
return a.ID > b.ID;
|
||||
return a.id > b.id;
|
||||
}
|
||||
|
||||
function folderMap(l) {
|
||||
|
||||
@@ -12,7 +12,7 @@ angular.module('syncthing.core')
|
||||
// progress the browser on some platforms returns a 200 (since the
|
||||
// headers has been flushed with the return code 200), with no data.
|
||||
// This basically means that the connection has been reset, and the call
|
||||
// was not actually sucessful.
|
||||
// was not actually successful.
|
||||
if (!data) {
|
||||
errorFn(data);
|
||||
return;
|
||||
|
||||
@@ -39,6 +39,11 @@ angular.module('syncthing.core')
|
||||
$scope.deviceStats = {};
|
||||
$scope.folderStats = {};
|
||||
$scope.progress = {};
|
||||
$scope.version = {};
|
||||
$scope.needed = [];
|
||||
$scope.neededTotal = 0;
|
||||
$scope.neededCurrentPage = 1;
|
||||
$scope.neededPageSize = 10;
|
||||
|
||||
$(window).bind('beforeunload', function () {
|
||||
navigatingAway = true;
|
||||
@@ -75,7 +80,7 @@ angular.module('syncthing.core')
|
||||
refreshFolderStats();
|
||||
|
||||
$http.get(urlbase + '/system/version').success(function (data) {
|
||||
$scope.version = data.version;
|
||||
$scope.version = data;
|
||||
}).error($scope.emitHTTPError);
|
||||
|
||||
$http.get(urlbase + '/svc/report').success(function (data) {
|
||||
@@ -215,7 +220,7 @@ angular.module('syncthing.core')
|
||||
var copiedFromElsewhere = 100 * s.copiedFromElsewhere / s.total;
|
||||
var pulled = 100 * s.pulled / s.total;
|
||||
var pulling = 100 * s.pulling / s.total;
|
||||
// We try to round up pulling to atleast a percent so that it would be atleast a bit visible.
|
||||
// We try to round up pulling to at least a percent so that it would be at least a bit visible.
|
||||
if (pulling < 1 && pulled + copiedFromElsewhere + copiedFromOrigin + reused <= 99) {
|
||||
pulling = 1;
|
||||
}
|
||||
@@ -367,6 +372,17 @@ angular.module('syncthing.core')
|
||||
id;
|
||||
|
||||
prevDate = now;
|
||||
|
||||
try {
|
||||
data.total.inbps = Math.max(0, (data.total.inBytesTotal - $scope.connectionsTotal.inBytesTotal) / td);
|
||||
data.total.outbps = Math.max(0, (data.total.outBytesTotal - $scope.connectionsTotal.outBytesTotal) / td);
|
||||
} catch (e) {
|
||||
data.total.inbps = 0;
|
||||
data.total.outbps = 0;
|
||||
}
|
||||
$scope.connectionsTotal = data.total;
|
||||
|
||||
data = data.connections;
|
||||
for (id in data) {
|
||||
if (!data.hasOwnProperty(id)) {
|
||||
continue;
|
||||
@@ -403,14 +419,63 @@ angular.module('syncthing.core')
|
||||
}
|
||||
|
||||
function refreshNeed(folder) {
|
||||
$http.get(urlbase + "/db/need?folder=" + encodeURIComponent(folder)).success(function (data) {
|
||||
var url = urlbase + "/db/need?folder=" + encodeURIComponent(folder);
|
||||
url += "&page=" + $scope.neededCurrentPage;
|
||||
url += "&perpage=" + $scope.neededPageSize;
|
||||
$http.get(url).success(function (data) {
|
||||
if ($scope.neededFolder == folder) {
|
||||
console.log("refreshNeed", folder, data);
|
||||
$scope.needed = data;
|
||||
parseNeeded(data);
|
||||
}
|
||||
}).error($scope.emitHTTPError);
|
||||
}
|
||||
|
||||
function needAction(file) {
|
||||
var fDelete = 4096;
|
||||
var fDirectory = 16384;
|
||||
|
||||
if ((file.flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
|
||||
return 'rmdir';
|
||||
} else if ((file.flags & fDelete) === fDelete) {
|
||||
return 'rm';
|
||||
} else if ((file.flags & fDirectory) === fDirectory) {
|
||||
return 'touch';
|
||||
} else {
|
||||
return 'sync';
|
||||
}
|
||||
};
|
||||
|
||||
function parseNeeded(data) {
|
||||
var merged = [];
|
||||
data.progress.forEach(function (item) {
|
||||
item.type = "progress";
|
||||
item.action = needAction(item);
|
||||
merged.push(item);
|
||||
});
|
||||
data.queued.forEach(function (item) {
|
||||
item.type = "queued";
|
||||
item.action = needAction(item);
|
||||
merged.push(item);
|
||||
});
|
||||
data.rest.forEach(function (item) {
|
||||
item.type = "rest";
|
||||
item.action = needAction(item);
|
||||
merged.push(item);
|
||||
});
|
||||
$scope.needed = merged;
|
||||
$scope.neededTotal = data.total;
|
||||
}
|
||||
|
||||
$scope.neededPageChanged = function (page) {
|
||||
$scope.neededCurrentPage = page;
|
||||
refreshNeed($scope.neededFolder);
|
||||
};
|
||||
|
||||
$scope.neededChangePageSize = function (perpage) {
|
||||
$scope.neededPageSize = perpage;
|
||||
refreshNeed($scope.neededFolder);
|
||||
}
|
||||
|
||||
var refreshDeviceStats = debounce(function () {
|
||||
$http.get(urlbase + "/stats/device").success(function (data) {
|
||||
$scope.deviceStats = data;
|
||||
@@ -449,10 +514,14 @@ angular.module('syncthing.core')
|
||||
return 'unshared';
|
||||
}
|
||||
|
||||
if ($scope.model[folderCfg.id].invalid !== '') {
|
||||
if ($scope.model[folderCfg.id].invalid) {
|
||||
return 'stopped';
|
||||
}
|
||||
|
||||
if ($scope.model[folderCfg.id].state == 'error') {
|
||||
return 'stopped'; // legacy, the state is called "stopped" in the GUI
|
||||
}
|
||||
|
||||
return '' + $scope.model[folderCfg.id].state;
|
||||
};
|
||||
|
||||
@@ -482,6 +551,9 @@ angular.module('syncthing.core')
|
||||
if (state == 'scanning') {
|
||||
return 'primary';
|
||||
}
|
||||
if (state == 'error') {
|
||||
return 'danger';
|
||||
}
|
||||
return 'info';
|
||||
};
|
||||
|
||||
@@ -679,6 +751,7 @@ angular.module('syncthing.core')
|
||||
|
||||
$scope.upgrade = function () {
|
||||
restarting = true;
|
||||
$('#majorUpgrade').modal('hide');
|
||||
$('#upgrading').modal();
|
||||
$http.post(urlbase + '/system/upgrade').success(function () {
|
||||
$('#restarting').modal();
|
||||
@@ -688,6 +761,10 @@ angular.module('syncthing.core')
|
||||
});
|
||||
};
|
||||
|
||||
$scope.upgradeMajor = function () {
|
||||
$('#majorUpgrade').modal();
|
||||
};
|
||||
|
||||
$scope.shutdown = function () {
|
||||
restarting = true;
|
||||
$http.post(urlbase + '/system/shutdown').success(function () {
|
||||
@@ -889,6 +966,9 @@ angular.module('syncthing.core')
|
||||
$scope.directoryList = [];
|
||||
|
||||
$scope.$watch('currentFolder.path', function (newvalue) {
|
||||
if (newvalue && newvalue.trim().charAt(0) == '~') {
|
||||
$scope.currentFolder.path = $scope.system.tilde + newvalue.trim().substring(1)
|
||||
}
|
||||
$http.get(urlbase + '/system/browse', {
|
||||
params: { current: newvalue }
|
||||
}).success(function (data) {
|
||||
@@ -959,7 +1039,7 @@ angular.module('syncthing.core')
|
||||
$scope.addFolderAndShare = function (folder, device) {
|
||||
$scope.dismissFolderRejection(folder, device);
|
||||
$scope.currentFolder = {
|
||||
ID: folder,
|
||||
id: folder,
|
||||
selectedDevices: {}
|
||||
};
|
||||
$scope.currentFolder.selectedDevices[device] = true;
|
||||
@@ -1154,24 +1234,11 @@ angular.module('syncthing.core')
|
||||
$('#needed').modal().on('hidden.bs.modal', function () {
|
||||
$scope.neededFolder = undefined;
|
||||
$scope.needed = undefined;
|
||||
$scope.neededTotal = 0;
|
||||
$scope.neededCurrentPage = 1;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.needAction = function (file) {
|
||||
var fDelete = 4096;
|
||||
var fDirectory = 16384;
|
||||
|
||||
if ((file.flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
|
||||
return 'rmdir';
|
||||
} else if ((file.flags & fDelete) === fDelete) {
|
||||
return 'rm';
|
||||
} else if ((file.flags & fDirectory) === fDirectory) {
|
||||
return 'touch';
|
||||
} else {
|
||||
return 'sync';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.override = function (folder) {
|
||||
$http.post(urlbase + "/db/override?folder=" + encodeURIComponent(folder));
|
||||
};
|
||||
@@ -1193,14 +1260,43 @@ angular.module('syncthing.core')
|
||||
};
|
||||
|
||||
$scope.bumpFile = function (folder, file) {
|
||||
$http.post(urlbase + "/db/prio?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file)).success(function (data) {
|
||||
var url = urlbase + "/db/prio?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file);
|
||||
// In order to get the right view of data in the response.
|
||||
url += "&page=" + $scope.neededCurrentPage;
|
||||
url += "&perpage=" + $scope.neededPageSize;
|
||||
$http.post(url).success(function (data) {
|
||||
if ($scope.neededFolder == folder) {
|
||||
console.log("bumpFile", folder, data);
|
||||
$scope.needed = data;
|
||||
parseNeeded(data);
|
||||
}
|
||||
}).error($scope.emitHTTPError);
|
||||
};
|
||||
|
||||
$scope.versionString = function () {
|
||||
if (!$scope.version.version) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var os = {
|
||||
'darwin': 'Mac OS X',
|
||||
'dragonfly': 'DragonFly BSD',
|
||||
'freebsd': 'FreeBSD',
|
||||
'openbsd': 'OpenBSD',
|
||||
'netbsd': 'NetBSD',
|
||||
'linux': 'Linux',
|
||||
'windows': 'Windows',
|
||||
'solaris': 'Solaris',
|
||||
}[$scope.version.os] || $scope.version.os;
|
||||
|
||||
var arch ={
|
||||
'386': '32 bit',
|
||||
'amd64': '64 bit',
|
||||
'arm': 'ARM',
|
||||
}[$scope.version.arch] || $scope.version.arch;
|
||||
|
||||
return $scope.version.version + ', ' + os + ' (' + arch + ')';
|
||||
};
|
||||
|
||||
// pseudo main. called on all definitions assigned
|
||||
initController();
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ angular.module('syncthing.core')
|
||||
$scope.localesNames = availableLocaleNames;
|
||||
$scope.visible = $scope.localesNames && $scope.localesNames['en'];
|
||||
|
||||
// using $watch cause LocaleService.currentLocale will be change after recive async query accpeted-languages
|
||||
// using $watch cause LocaleService.currentLocale will be change after receive async query accepted-languages
|
||||
// in LocaleService.readBrowserLocales
|
||||
var remove_watch = $scope.$watch(LocaleService.getCurrentLocale, function (newValue) {
|
||||
if (newValue) {
|
||||
|
||||
@@ -8,7 +8,7 @@ angular.module('syncthing.core')
|
||||
// we shouldn't validate
|
||||
ctrl.$setValidity('validDeviceid', true);
|
||||
} else {
|
||||
$http.get(urlbase + '/deviceid?id=' + viewValue).success(function (resp) {
|
||||
$http.get(urlbase + '/svc/deviceid?id=' + viewValue).success(function (resp) {
|
||||
if (resp.error) {
|
||||
ctrl.$setValidity('validDeviceid', false);
|
||||
} else {
|
||||
|
||||
@@ -2,8 +2,23 @@ angular.module('syncthing.core')
|
||||
.provider('LocaleService', function () {
|
||||
'use strict';
|
||||
|
||||
function detectLocalStorage() {
|
||||
// Feature detect localStorage; https://mathiasbynens.be/notes/localstorage-pattern
|
||||
try {
|
||||
var uid = new Date();
|
||||
var storage = window.localStorage;
|
||||
storage.setItem(uid, uid);
|
||||
var success = storage.getItem(uid) == uid;
|
||||
storage.removeItem(uid);
|
||||
return storage;
|
||||
} catch (exception) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
var _defaultLocale,
|
||||
_availableLocales;
|
||||
_availableLocales,
|
||||
_localStorage = detectLocalStorage();
|
||||
|
||||
var _SYNLANG = "SYN_LANG"; // const key for localStorage
|
||||
|
||||
@@ -18,6 +33,7 @@ angular.module('syncthing.core')
|
||||
_availableLocales = locales;
|
||||
};
|
||||
|
||||
|
||||
this.$get = ['$http', '$translate', '$location', function ($http, $translate, $location) {
|
||||
|
||||
/**
|
||||
@@ -26,14 +42,17 @@ angular.module('syncthing.core')
|
||||
* @returns promise which on success resolves with a locales array
|
||||
*/
|
||||
function readBrowserLocales() {
|
||||
// @TODO: check if there is nice way to utilize window.navigator.languages or similiar api.
|
||||
// @TODO: check if there is nice way to utilize window.navigator.languages or similar api.
|
||||
|
||||
return $http.get(urlbase + "/lang");
|
||||
return $http.get(urlbase + "/svc/lang");
|
||||
}
|
||||
|
||||
function autoConfigLocale() {
|
||||
var params = $location.search();
|
||||
var savedLang = typeof(localStorage) != 'undefined' && localStorage[_SYNLANG];
|
||||
var savedLang;
|
||||
if (_localStorage) {
|
||||
savedLang = _localStorage[_SYNLANG];
|
||||
}
|
||||
|
||||
if(params.lang) {
|
||||
useLocale(params.lang, true);
|
||||
@@ -84,8 +103,8 @@ angular.module('syncthing.core')
|
||||
function useLocale(language, save2Storage) {
|
||||
if (language) {
|
||||
$translate.use(language).then(function () {
|
||||
if (save2Storage && typeof(localStorage) != 'undefined')
|
||||
localStorage[_SYNLANG] = language;
|
||||
if (save2Storage && _localStorage)
|
||||
_localStorage[_SYNLANG] = language;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
520
gui/vendor/angular/angular-dirPagination.js
vendored
Normal file
@@ -0,0 +1,520 @@
|
||||
/**
|
||||
* dirPagination - AngularJS module for paginating (almost) anything.
|
||||
*
|
||||
*
|
||||
* Credits
|
||||
* =======
|
||||
*
|
||||
* Daniel Tabuenca: https://groups.google.com/d/msg/angular/an9QpzqIYiM/r8v-3W1X5vcJ
|
||||
* for the idea on how to dynamically invoke the ng-repeat directive.
|
||||
*
|
||||
* I borrowed a couple of lines and a few attribute names from the AngularUI Bootstrap project:
|
||||
* https://github.com/angular-ui/bootstrap/blob/master/src/pagination/pagination.js
|
||||
*
|
||||
* Copyright 2014 Michael Bromley <michael@michaelbromley.co.uk>
|
||||
*/
|
||||
|
||||
(function() {
|
||||
|
||||
/**
|
||||
* Config
|
||||
*/
|
||||
var moduleName = 'angularUtils.directives.dirPagination';
|
||||
var DEFAULT_ID = '__default';
|
||||
|
||||
/**
|
||||
* Module
|
||||
*/
|
||||
var module;
|
||||
try {
|
||||
module = angular.module(moduleName);
|
||||
} catch(err) {
|
||||
// named module does not exist, so create one
|
||||
module = angular.module(moduleName, []);
|
||||
}
|
||||
|
||||
module
|
||||
.directive('dirPaginate', ['$compile', '$parse', 'paginationService', dirPaginateDirective])
|
||||
.directive('dirPaginateNoCompile', noCompileDirective)
|
||||
.directive('dirPaginationControls', ['paginationService', 'paginationTemplate', dirPaginationControlsDirective])
|
||||
.filter('itemsPerPage', ['paginationService', itemsPerPageFilter])
|
||||
.service('paginationService', paginationService)
|
||||
.provider('paginationTemplate', paginationTemplateProvider)
|
||||
.run(['$templateCache',dirPaginationControlsTemplateInstaller]);
|
||||
|
||||
function dirPaginateDirective($compile, $parse, paginationService) {
|
||||
|
||||
return {
|
||||
terminal: true,
|
||||
multiElement: true,
|
||||
compile: dirPaginationCompileFn
|
||||
};
|
||||
|
||||
function dirPaginationCompileFn(tElement, tAttrs){
|
||||
|
||||
var expression = tAttrs.dirPaginate;
|
||||
// regex taken directly from https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211
|
||||
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
|
||||
|
||||
var filterPattern = /\|\s*itemsPerPage\s*:[^|]*/;
|
||||
if (match[2].match(filterPattern) === null) {
|
||||
throw 'pagination directive: the \'itemsPerPage\' filter must be set.';
|
||||
}
|
||||
var itemsPerPageFilterRemoved = match[2].replace(filterPattern, '');
|
||||
var collectionGetter = $parse(itemsPerPageFilterRemoved);
|
||||
|
||||
addNoCompileAttributes(tElement);
|
||||
|
||||
// If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any
|
||||
// dir-pagination-controls directives that may be looking for this ID.
|
||||
var rawId = tAttrs.paginationId || DEFAULT_ID;
|
||||
paginationService.registerInstance(rawId);
|
||||
|
||||
return function dirPaginationLinkFn(scope, element, attrs){
|
||||
|
||||
// Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and
|
||||
// potentially register a new ID if it evaluates to a different value than the rawId.
|
||||
var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID;
|
||||
paginationService.registerInstance(paginationId);
|
||||
|
||||
var repeatExpression = getRepeatExpression(expression, paginationId);
|
||||
addNgRepeatToElement(element, attrs, repeatExpression);
|
||||
|
||||
removeTemporaryAttributes(element);
|
||||
var compiled = $compile(element);
|
||||
|
||||
var currentPageGetter = makeCurrentPageGetterFn(scope, attrs, paginationId);
|
||||
paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope);
|
||||
|
||||
if (typeof attrs.totalItems !== 'undefined') {
|
||||
paginationService.setAsyncModeTrue(paginationId);
|
||||
scope.$watch(function() {
|
||||
return $parse(attrs.totalItems)(scope);
|
||||
}, function (result) {
|
||||
if (0 <= result) {
|
||||
paginationService.setCollectionLength(paginationId, result);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
scope.$watchCollection(function() {
|
||||
return collectionGetter(scope);
|
||||
}, function(collection) {
|
||||
if (collection) {
|
||||
paginationService.setCollectionLength(paginationId, collection.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delegate to the link function returned by the new compilation of the ng-repeat
|
||||
compiled(scope);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If a pagination id has been specified, we need to check that it is present as the second argument passed to
|
||||
* the itemsPerPage filter. If it is not there, we add it and return the modified expression.
|
||||
*
|
||||
* @param expression
|
||||
* @param paginationId
|
||||
* @returns {*}
|
||||
*/
|
||||
function getRepeatExpression(expression, paginationId) {
|
||||
var repeatExpression,
|
||||
idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/);
|
||||
|
||||
if (paginationId !== DEFAULT_ID && !idDefinedInFilter) {
|
||||
repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:[^|]*)/, "$1 : '" + paginationId + "'");
|
||||
} else {
|
||||
repeatExpression = expression;
|
||||
}
|
||||
|
||||
return repeatExpression;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the ng-repeat directive to the element. In the case of multi-element (-start, -end) it adds the
|
||||
* appropriate multi-element ng-repeat to the first and last element in the range.
|
||||
* @param element
|
||||
* @param attrs
|
||||
* @param repeatExpression
|
||||
*/
|
||||
function addNgRepeatToElement(element, attrs, repeatExpression) {
|
||||
if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) {
|
||||
// using multiElement mode (dir-paginate-start, dir-paginate-end)
|
||||
attrs.$set('ngRepeatStart', repeatExpression);
|
||||
element.eq(element.length - 1).attr('ng-repeat-end', true);
|
||||
} else {
|
||||
attrs.$set('ngRepeat', repeatExpression);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the dir-paginate-no-compile directive to each element in the tElement range.
|
||||
* @param tElement
|
||||
*/
|
||||
function addNoCompileAttributes(tElement) {
|
||||
angular.forEach(tElement, function(el) {
|
||||
if (el.nodeType === Node.ELEMENT_NODE) {
|
||||
angular.element(el).attr('dir-paginate-no-compile', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the variations on dir-paginate (data-, -start, -end) and the dir-paginate-no-compile directives.
|
||||
* @param element
|
||||
*/
|
||||
function removeTemporaryAttributes(element) {
|
||||
angular.forEach(element, function(el) {
|
||||
if (el.nodeType === Node.ELEMENT_NODE) {
|
||||
angular.element(el).removeAttr('dir-paginate-no-compile');
|
||||
}
|
||||
});
|
||||
element.eq(0).removeAttr('dir-paginate-start').removeAttr('dir-paginate').removeAttr('data-dir-paginate-start').removeAttr('data-dir-paginate');
|
||||
element.eq(element.length - 1).removeAttr('dir-paginate-end').removeAttr('data-dir-paginate-end');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a getter function for the current-page attribute, using the expression provided or a default value if
|
||||
* no current-page expression was specified.
|
||||
*
|
||||
* @param scope
|
||||
* @param attrs
|
||||
* @param paginationId
|
||||
* @returns {*}
|
||||
*/
|
||||
function makeCurrentPageGetterFn(scope, attrs, paginationId) {
|
||||
var currentPageGetter;
|
||||
if (attrs.currentPage) {
|
||||
currentPageGetter = $parse(attrs.currentPage);
|
||||
} else {
|
||||
// if the current-page attribute was not set, we'll make our own
|
||||
var defaultCurrentPage = paginationId + '__currentPage';
|
||||
scope[defaultCurrentPage] = 1;
|
||||
currentPageGetter = $parse(defaultCurrentPage);
|
||||
}
|
||||
return currentPageGetter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a helper directive that allows correct compilation when in multi-element mode (ie dir-paginate-start, dir-paginate-end).
|
||||
* It is dynamically added to all elements in the dir-paginate compile function, and it prevents further compilation of
|
||||
* any inner directives. It is then removed in the link function, and all inner directives are then manually compiled.
|
||||
*/
|
||||
function noCompileDirective() {
|
||||
return {
|
||||
priority: 5000,
|
||||
terminal: true
|
||||
};
|
||||
}
|
||||
|
||||
function dirPaginationControlsTemplateInstaller($templateCache) {
|
||||
$templateCache.put('angularUtils.directives.dirPagination.template', '<ul class="pagination" ng-if="1 < pages.length"><li ng-if="boundaryLinks" ng-class="{ disabled : pagination.current == 1 }"><a href="" ng-click="setCurrent(1)">«</a></li><li ng-if="directionLinks" ng-class="{ disabled : pagination.current == 1 }"><a href="" ng-click="setCurrent(pagination.current - 1)">‹</a></li><li ng-repeat="pageNumber in pages track by $index" ng-class="{ active : pagination.current == pageNumber, disabled : pageNumber == \'...\' }"><a href="" ng-click="setCurrent(pageNumber)">{{ pageNumber }}</a></li><li ng-if="directionLinks" ng-class="{ disabled : pagination.current == pagination.last }"><a href="" ng-click="setCurrent(pagination.current + 1)">›</a></li><li ng-if="boundaryLinks" ng-class="{ disabled : pagination.current == pagination.last }"><a href="" ng-click="setCurrent(pagination.last)">»</a></li></ul>');
|
||||
}
|
||||
|
||||
function dirPaginationControlsDirective(paginationService, paginationTemplate) {
|
||||
|
||||
var numberRegex = /^\d+$/;
|
||||
|
||||
return {
|
||||
restrict: 'AE',
|
||||
templateUrl: function(elem, attrs) {
|
||||
return attrs.templateUrl || paginationTemplate.getPath();
|
||||
},
|
||||
scope: {
|
||||
maxSize: '=?',
|
||||
onPageChange: '&?',
|
||||
paginationId: '=?'
|
||||
},
|
||||
link: dirPaginationControlsLinkFn
|
||||
};
|
||||
|
||||
function dirPaginationControlsLinkFn(scope, element, attrs) {
|
||||
|
||||
// rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has
|
||||
// not yet been linked (e.g. if it is inside an ng-if block), and in that case it prevents this controls directive from assuming that there is
|
||||
// no corresponding dir-paginate directive and wrongly throwing an exception.
|
||||
var rawId = attrs.paginationId || DEFAULT_ID;
|
||||
var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID;
|
||||
|
||||
if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) {
|
||||
var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' ';
|
||||
throw 'pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive.';
|
||||
}
|
||||
|
||||
if (!scope.maxSize) { scope.maxSize = 9; }
|
||||
scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true;
|
||||
scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false;
|
||||
|
||||
var paginationRange = Math.max(scope.maxSize, 5);
|
||||
scope.pages = [];
|
||||
scope.pagination = {
|
||||
last: 1,
|
||||
current: 1
|
||||
};
|
||||
scope.range = {
|
||||
lower: 1,
|
||||
upper: 1,
|
||||
total: 1
|
||||
};
|
||||
|
||||
scope.$watch(function() {
|
||||
return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId);
|
||||
}, function(length) {
|
||||
if (0 < length) {
|
||||
generatePagination();
|
||||
}
|
||||
});
|
||||
|
||||
scope.$watch(function() {
|
||||
return (paginationService.getItemsPerPage(paginationId));
|
||||
}, function(current, previous) {
|
||||
if (current != previous && typeof previous !== 'undefined') {
|
||||
goToPage(scope.pagination.current);
|
||||
}
|
||||
});
|
||||
|
||||
scope.$watch(function() {
|
||||
return paginationService.getCurrentPage(paginationId);
|
||||
}, function(currentPage, previousPage) {
|
||||
if (currentPage != previousPage) {
|
||||
goToPage(currentPage);
|
||||
}
|
||||
});
|
||||
|
||||
scope.setCurrent = function(num) {
|
||||
if (isValidPageNumber(num)) {
|
||||
num = parseInt(num, 10);
|
||||
paginationService.setCurrentPage(paginationId, num);
|
||||
}
|
||||
};
|
||||
|
||||
function goToPage(num) {
|
||||
if (isValidPageNumber(num)) {
|
||||
scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
|
||||
scope.pagination.current = num;
|
||||
updateRangeValues();
|
||||
|
||||
// if a callback has been set, then call it with the page number as an argument
|
||||
if (scope.onPageChange) {
|
||||
scope.onPageChange({ newPageNumber : num });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generatePagination() {
|
||||
var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1;
|
||||
|
||||
scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange);
|
||||
scope.pagination.current = page;
|
||||
scope.pagination.last = scope.pages[scope.pages.length - 1];
|
||||
if (scope.pagination.last < scope.pagination.current) {
|
||||
scope.setCurrent(scope.pagination.last);
|
||||
} else {
|
||||
updateRangeValues();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination
|
||||
* template to display the current page range, e.g. "showing 21 - 40 of 144 results";
|
||||
*/
|
||||
function updateRangeValues() {
|
||||
var currentPage = paginationService.getCurrentPage(paginationId),
|
||||
itemsPerPage = paginationService.getItemsPerPage(paginationId),
|
||||
totalItems = paginationService.getCollectionLength(paginationId);
|
||||
|
||||
scope.range.lower = (currentPage - 1) * itemsPerPage + 1;
|
||||
scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems);
|
||||
scope.range.total = totalItems;
|
||||
}
|
||||
|
||||
function isValidPageNumber(num) {
|
||||
return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the
|
||||
* links used in pagination
|
||||
*
|
||||
* @param currentPage
|
||||
* @param rowsPerPage
|
||||
* @param paginationRange
|
||||
* @param collectionLength
|
||||
* @returns {Array}
|
||||
*/
|
||||
function generatePagesArray(currentPage, collectionLength, rowsPerPage, paginationRange) {
|
||||
var pages = [];
|
||||
var totalPages = Math.ceil(collectionLength / rowsPerPage);
|
||||
var halfWay = Math.ceil(paginationRange / 2);
|
||||
var position;
|
||||
|
||||
if (currentPage <= halfWay) {
|
||||
position = 'start';
|
||||
} else if (totalPages - halfWay < currentPage) {
|
||||
position = 'end';
|
||||
} else {
|
||||
position = 'middle';
|
||||
}
|
||||
|
||||
var ellipsesNeeded = paginationRange < totalPages;
|
||||
var i = 1;
|
||||
while (i <= totalPages && i <= paginationRange) {
|
||||
var pageNumber = calculatePageNumber(i, currentPage, paginationRange, totalPages);
|
||||
|
||||
var openingEllipsesNeeded = (i === 2 && (position === 'middle' || position === 'end'));
|
||||
var closingEllipsesNeeded = (i === paginationRange - 1 && (position === 'middle' || position === 'start'));
|
||||
if (ellipsesNeeded && (openingEllipsesNeeded || closingEllipsesNeeded)) {
|
||||
pages.push('...');
|
||||
} else {
|
||||
pages.push(pageNumber);
|
||||
}
|
||||
i ++;
|
||||
}
|
||||
return pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the position in the sequence of pagination links [i], figure out what page number corresponds to that position.
|
||||
*
|
||||
* @param i
|
||||
* @param currentPage
|
||||
* @param paginationRange
|
||||
* @param totalPages
|
||||
* @returns {*}
|
||||
*/
|
||||
function calculatePageNumber(i, currentPage, paginationRange, totalPages) {
|
||||
var halfWay = Math.ceil(paginationRange/2);
|
||||
if (i === paginationRange) {
|
||||
return totalPages;
|
||||
} else if (i === 1) {
|
||||
return i;
|
||||
} else if (paginationRange < totalPages) {
|
||||
if (totalPages - halfWay < currentPage) {
|
||||
return totalPages - paginationRange + i;
|
||||
} else if (halfWay < currentPage) {
|
||||
return currentPage - halfWay + i;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This filter slices the collection into pages based on the current page number and number of items per page.
|
||||
* @param paginationService
|
||||
* @returns {Function}
|
||||
*/
|
||||
function itemsPerPageFilter(paginationService) {
|
||||
|
||||
return function(collection, itemsPerPage, paginationId) {
|
||||
if (typeof (paginationId) === 'undefined') {
|
||||
paginationId = DEFAULT_ID;
|
||||
}
|
||||
if (!paginationService.isRegistered(paginationId)) {
|
||||
throw 'pagination directive: the itemsPerPage id argument (id: ' + paginationId + ') does not match a registered pagination-id.';
|
||||
}
|
||||
var end;
|
||||
var start;
|
||||
if (collection instanceof Array) {
|
||||
itemsPerPage = parseInt(itemsPerPage) || 9999999999;
|
||||
if (paginationService.isAsyncMode(paginationId)) {
|
||||
start = 0;
|
||||
} else {
|
||||
start = (paginationService.getCurrentPage(paginationId) - 1) * itemsPerPage;
|
||||
}
|
||||
end = start + itemsPerPage;
|
||||
paginationService.setItemsPerPage(paginationId, itemsPerPage);
|
||||
|
||||
return collection.slice(start, end);
|
||||
} else {
|
||||
return collection;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This service allows the various parts of the module to communicate and stay in sync.
|
||||
*/
|
||||
function paginationService() {
|
||||
|
||||
var instances = {};
|
||||
var lastRegisteredInstance;
|
||||
|
||||
this.registerInstance = function(instanceId) {
|
||||
if (typeof instances[instanceId] === 'undefined') {
|
||||
instances[instanceId] = {
|
||||
asyncMode: false
|
||||
};
|
||||
lastRegisteredInstance = instanceId;
|
||||
}
|
||||
};
|
||||
|
||||
this.isRegistered = function(instanceId) {
|
||||
return (typeof instances[instanceId] !== 'undefined');
|
||||
};
|
||||
|
||||
this.getLastInstanceId = function() {
|
||||
return lastRegisteredInstance;
|
||||
};
|
||||
|
||||
this.setCurrentPageParser = function(instanceId, val, scope) {
|
||||
instances[instanceId].currentPageParser = val;
|
||||
instances[instanceId].context = scope;
|
||||
};
|
||||
this.setCurrentPage = function(instanceId, val) {
|
||||
instances[instanceId].currentPageParser.assign(instances[instanceId].context, val);
|
||||
};
|
||||
this.getCurrentPage = function(instanceId) {
|
||||
var parser = instances[instanceId].currentPageParser;
|
||||
return parser ? parser(instances[instanceId].context) : 1;
|
||||
};
|
||||
|
||||
this.setItemsPerPage = function(instanceId, val) {
|
||||
instances[instanceId].itemsPerPage = val;
|
||||
};
|
||||
this.getItemsPerPage = function(instanceId) {
|
||||
return instances[instanceId].itemsPerPage;
|
||||
};
|
||||
|
||||
this.setCollectionLength = function(instanceId, val) {
|
||||
instances[instanceId].collectionLength = val;
|
||||
};
|
||||
this.getCollectionLength = function(instanceId) {
|
||||
return instances[instanceId].collectionLength;
|
||||
};
|
||||
|
||||
this.setAsyncModeTrue = function(instanceId) {
|
||||
instances[instanceId].asyncMode = true;
|
||||
};
|
||||
|
||||
this.isAsyncMode = function(instanceId) {
|
||||
return instances[instanceId].asyncMode;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This provider allows global configuration of the template path used by the dir-pagination-controls directive.
|
||||
*/
|
||||
function paginationTemplateProvider() {
|
||||
|
||||
var templatePath = 'angularUtils.directives.dirPagination.template';
|
||||
|
||||
this.setPath = function(path) {
|
||||
templatePath = path;
|
||||
};
|
||||
|
||||
this.$get = function() {
|
||||
return {
|
||||
getPath: function() {
|
||||
return templatePath;
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
})();
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -44,33 +45,33 @@ type Configuration struct {
|
||||
OriginalVersion int `xml:"-" json:"-"` // The version we read from disk, before any conversion
|
||||
}
|
||||
|
||||
func (orig Configuration) Copy() Configuration {
|
||||
c := orig
|
||||
func (cfg Configuration) Copy() Configuration {
|
||||
newCfg := cfg
|
||||
|
||||
// Deep copy FolderConfigurations
|
||||
c.Folders = make([]FolderConfiguration, len(orig.Folders))
|
||||
for i := range c.Folders {
|
||||
c.Folders[i] = orig.Folders[i].Copy()
|
||||
newCfg.Folders = make([]FolderConfiguration, len(cfg.Folders))
|
||||
for i := range newCfg.Folders {
|
||||
newCfg.Folders[i] = cfg.Folders[i].Copy()
|
||||
}
|
||||
|
||||
// Deep copy DeviceConfigurations
|
||||
c.Devices = make([]DeviceConfiguration, len(orig.Devices))
|
||||
for i := range c.Devices {
|
||||
c.Devices[i] = orig.Devices[i].Copy()
|
||||
newCfg.Devices = make([]DeviceConfiguration, len(cfg.Devices))
|
||||
for i := range newCfg.Devices {
|
||||
newCfg.Devices[i] = cfg.Devices[i].Copy()
|
||||
}
|
||||
|
||||
c.Options = orig.Options.Copy()
|
||||
newCfg.Options = cfg.Options.Copy()
|
||||
|
||||
// DeviceIDs are values
|
||||
c.IgnoredDevices = make([]protocol.DeviceID, len(orig.IgnoredDevices))
|
||||
copy(c.IgnoredDevices, orig.IgnoredDevices)
|
||||
newCfg.IgnoredDevices = make([]protocol.DeviceID, len(cfg.IgnoredDevices))
|
||||
copy(newCfg.IgnoredDevices, cfg.IgnoredDevices)
|
||||
|
||||
return c
|
||||
return newCfg
|
||||
}
|
||||
|
||||
type FolderConfiguration struct {
|
||||
ID string `xml:"id,attr" json:"id"`
|
||||
Path string `xml:"path,attr" json:"path"`
|
||||
RawPath string `xml:"path,attr" json:"path"`
|
||||
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
|
||||
ReadOnly bool `xml:"ro,attr" json:"readOnly"`
|
||||
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
|
||||
@@ -81,22 +82,51 @@ type FolderConfiguration struct {
|
||||
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
|
||||
Pullers int `xml:"pullers" json:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
|
||||
Hashers int `xml:"hashers" json:"hashers"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
|
||||
Order PullOrder `xml:"order" json:"order"`
|
||||
|
||||
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
|
||||
|
||||
deviceIDs []protocol.DeviceID
|
||||
}
|
||||
|
||||
func (orig FolderConfiguration) Copy() FolderConfiguration {
|
||||
c := orig
|
||||
c.Devices = make([]FolderDeviceConfiguration, len(orig.Devices))
|
||||
copy(c.Devices, orig.Devices)
|
||||
func (f FolderConfiguration) Copy() FolderConfiguration {
|
||||
c := f
|
||||
c.Devices = make([]FolderDeviceConfiguration, len(f.Devices))
|
||||
copy(c.Devices, f.Devices)
|
||||
return c
|
||||
}
|
||||
|
||||
func (f FolderConfiguration) Path() string {
|
||||
// This is intentionally not a pointer method, because things like
|
||||
// cfg.Folders["default"].Path() should be valid.
|
||||
|
||||
// Attempt tilde expansion; leave unchanged in case of error
|
||||
if path, err := osutil.ExpandTilde(f.RawPath); err == nil {
|
||||
f.RawPath = path
|
||||
}
|
||||
|
||||
// Attempt absolutification; leave unchanged in case of error
|
||||
if !filepath.IsAbs(f.RawPath) {
|
||||
// Abs() looks like a fairly expensive syscall on Windows, while
|
||||
// IsAbs() is a whole bunch of string mangling. I think IsAbs() may be
|
||||
// somewhat faster in the general case, hence the outer if...
|
||||
if path, err := filepath.Abs(f.RawPath); err == nil {
|
||||
f.RawPath = path
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to enable long filename support on Windows. We may still not
|
||||
// have an absolute path here if the previous steps failed.
|
||||
if runtime.GOOS == "windows" && filepath.IsAbs(f.RawPath) && !strings.HasPrefix(f.RawPath, `\\`) {
|
||||
return `\\?\` + f.RawPath
|
||||
}
|
||||
|
||||
return f.RawPath
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) CreateMarker() error {
|
||||
if !f.HasMarker() {
|
||||
marker := filepath.Join(f.Path, ".stfolder")
|
||||
marker := filepath.Join(f.Path(), ".stfolder")
|
||||
fd, err := os.Create(marker)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -109,7 +139,7 @@ func (f *FolderConfiguration) CreateMarker() error {
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) HasMarker() bool {
|
||||
_, err := os.Stat(filepath.Join(f.Path, ".stfolder"))
|
||||
_, err := os.Stat(filepath.Join(f.Path(), ".stfolder"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -198,8 +228,9 @@ type OptionsConfiguration struct {
|
||||
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
|
||||
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
|
||||
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
|
||||
UPnPLease int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"0"`
|
||||
UPnPRenewal int `xml:"upnpRenewalMinutes" json:"upnpRenewalMinutes" default:"30"`
|
||||
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"0"`
|
||||
UPnPRenewalM int `xml:"upnpRenewalMinutes" json:"upnpRenewalMinutes" default:"30"`
|
||||
UPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"upnpTimeoutSeconds" default:"10"`
|
||||
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
|
||||
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
|
||||
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
|
||||
@@ -285,7 +316,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
for i := range cfg.Folders {
|
||||
folder := &cfg.Folders[i]
|
||||
|
||||
if len(folder.Path) == 0 {
|
||||
if len(folder.RawPath) == 0 {
|
||||
folder.Invalid = "no directory configured"
|
||||
continue
|
||||
}
|
||||
@@ -296,7 +327,7 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
// C:\somedir\ -> C:\somedir\\ -> C:\somedir
|
||||
// This way in the tests, we get away without OS specific separators
|
||||
// in the test configs.
|
||||
folder.Path = filepath.Dir(folder.Path + string(filepath.Separator))
|
||||
folder.RawPath = filepath.Dir(folder.RawPath + string(filepath.Separator))
|
||||
|
||||
if folder.ID == "" {
|
||||
folder.ID = "default"
|
||||
@@ -648,3 +679,57 @@ func randomString(l int) string {
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
type PullOrder int
|
||||
|
||||
const (
|
||||
OrderRandom PullOrder = iota // default is random
|
||||
OrderAlphabetic
|
||||
OrderSmallestFirst
|
||||
OrderLargestFirst
|
||||
OrderOldestFirst
|
||||
OrderNewestFirst
|
||||
)
|
||||
|
||||
func (o PullOrder) String() string {
|
||||
switch o {
|
||||
case OrderRandom:
|
||||
return "random"
|
||||
case OrderAlphabetic:
|
||||
return "alphabetic"
|
||||
case OrderSmallestFirst:
|
||||
return "smallestFirst"
|
||||
case OrderLargestFirst:
|
||||
return "largestFirst"
|
||||
case OrderOldestFirst:
|
||||
return "oldestFirst"
|
||||
case OrderNewestFirst:
|
||||
return "newestFirst"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (o PullOrder) MarshalText() ([]byte, error) {
|
||||
return []byte(o.String()), nil
|
||||
}
|
||||
|
||||
func (o *PullOrder) UnmarshalText(bs []byte) error {
|
||||
switch string(bs) {
|
||||
case "random":
|
||||
*o = OrderRandom
|
||||
case "alphabetic":
|
||||
*o = OrderAlphabetic
|
||||
case "smallestFirst":
|
||||
*o = OrderSmallestFirst
|
||||
case "largestFirst":
|
||||
*o = OrderLargestFirst
|
||||
case "oldestFirst":
|
||||
*o = OrderOldestFirst
|
||||
case "newestFirst":
|
||||
*o = OrderNewestFirst
|
||||
default:
|
||||
*o = OrderRandom
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,8 +11,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
@@ -40,8 +42,9 @@ func TestDefaultValues(t *testing.T) {
|
||||
ReconnectIntervalS: 60,
|
||||
StartBrowser: true,
|
||||
UPnPEnabled: true,
|
||||
UPnPLease: 0,
|
||||
UPnPRenewal: 30,
|
||||
UPnPLeaseM: 0,
|
||||
UPnPRenewalM: 30,
|
||||
UPnPTimeoutS: 10,
|
||||
RestartOnWakeup: true,
|
||||
AutoUpgradeIntervalH: 12,
|
||||
KeepTemporariesH: 24,
|
||||
@@ -78,7 +81,7 @@ func TestDeviceConfig(t *testing.T) {
|
||||
expectedFolders := []FolderConfiguration{
|
||||
{
|
||||
ID: "test",
|
||||
Path: "testdata",
|
||||
RawPath: "testdata",
|
||||
Devices: []FolderDeviceConfiguration{{DeviceID: device1}, {DeviceID: device4}},
|
||||
ReadOnly: true,
|
||||
RescanIntervalS: 600,
|
||||
@@ -145,8 +148,9 @@ func TestOverriddenValues(t *testing.T) {
|
||||
ReconnectIntervalS: 6000,
|
||||
StartBrowser: false,
|
||||
UPnPEnabled: false,
|
||||
UPnPLease: 60,
|
||||
UPnPRenewal: 15,
|
||||
UPnPLeaseM: 60,
|
||||
UPnPRenewalM: 15,
|
||||
UPnPTimeoutS: 15,
|
||||
RestartOnWakeup: false,
|
||||
AutoUpgradeIntervalH: 24,
|
||||
KeepTemporariesH: 48,
|
||||
@@ -297,10 +301,10 @@ func TestVersioningConfig(t *testing.T) {
|
||||
func TestIssue1262(t *testing.T) {
|
||||
cfg, err := Load("testdata/issue-1262.xml", device4)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual := cfg.Folders()["test"].Path
|
||||
actual := cfg.Folders()["test"].RawPath
|
||||
expected := "e:"
|
||||
if runtime.GOOS == "windows" {
|
||||
expected = `e:\`
|
||||
@@ -311,6 +315,51 @@ func TestIssue1262(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsPaths(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("Not useful on non-Windows")
|
||||
return
|
||||
}
|
||||
|
||||
folder := FolderConfiguration{
|
||||
RawPath: `e:\`,
|
||||
}
|
||||
|
||||
expected := `\\?\e:\`
|
||||
actual := folder.Path()
|
||||
if actual != expected {
|
||||
t.Errorf("%q != %q", actual, expected)
|
||||
}
|
||||
|
||||
folder.RawPath = `\\192.0.2.22\network\share`
|
||||
expected = folder.RawPath
|
||||
actual = folder.Path()
|
||||
if actual != expected {
|
||||
t.Errorf("%q != %q", actual, expected)
|
||||
}
|
||||
|
||||
folder.RawPath = `relative\path`
|
||||
expected = folder.RawPath
|
||||
actual = folder.Path()
|
||||
if actual == expected || !strings.HasPrefix(actual, "\\\\?\\") {
|
||||
t.Errorf("%q == %q, expected absolutification", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderPath(t *testing.T) {
|
||||
folder := FolderConfiguration{
|
||||
RawPath: "~/tmp",
|
||||
}
|
||||
|
||||
realPath := folder.Path()
|
||||
if !filepath.IsAbs(realPath) {
|
||||
t.Error(realPath, "should be absolute")
|
||||
}
|
||||
if strings.Contains(realPath, "~") {
|
||||
t.Error(realPath, "should not contain ~")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSaveLoad(t *testing.T) {
|
||||
path := "testdata/temp.xml"
|
||||
os.Remove(path)
|
||||
@@ -391,8 +440,8 @@ func TestRequiresRestart(t *testing.T) {
|
||||
|
||||
newCfg = cfg
|
||||
newCfg.Folders = append(newCfg.Folders, FolderConfiguration{
|
||||
ID: "t1",
|
||||
Path: "t1",
|
||||
ID: "t1",
|
||||
RawPath: "t1",
|
||||
})
|
||||
if !ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("Adding a folder requires restart")
|
||||
@@ -411,7 +460,7 @@ func TestRequiresRestart(t *testing.T) {
|
||||
if ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("No changes done yet")
|
||||
}
|
||||
newCfg.Folders[0].Path = "different"
|
||||
newCfg.Folders[0].RawPath = "different"
|
||||
if !ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("Changing a folder requires restart")
|
||||
}
|
||||
@@ -479,3 +528,51 @@ func TestCopy(t *testing.T) {
|
||||
t.Error("Copy should be unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullOrder(t *testing.T) {
|
||||
wrapper, err := Load("testdata/pullorder.xml", device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
folders := wrapper.Folders()
|
||||
|
||||
expected := []struct {
|
||||
name string
|
||||
order PullOrder
|
||||
}{
|
||||
{"f1", OrderRandom}, // empty value, default
|
||||
{"f2", OrderRandom}, // explicit
|
||||
{"f3", OrderAlphabetic}, // explicit
|
||||
{"f4", OrderRandom}, // unknown value, default
|
||||
{"f5", OrderSmallestFirst}, // explicit
|
||||
{"f6", OrderLargestFirst}, // explicit
|
||||
{"f7", OrderOldestFirst}, // explicit
|
||||
{"f8", OrderNewestFirst}, // explicit
|
||||
}
|
||||
|
||||
// Verify values are deserialized correctly
|
||||
|
||||
for _, tc := range expected {
|
||||
if actual := folders[tc.name].Order; actual != tc.order {
|
||||
t.Errorf("Incorrect pull order for %q: %v != %v", tc.name, actual, tc.order)
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize and deserialize again to verify it survives the transformation
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
cfg := wrapper.Raw()
|
||||
cfg.WriteXML(buf)
|
||||
|
||||
t.Logf("%s", buf.Bytes())
|
||||
|
||||
cfg, err = ReadXML(buf, device1)
|
||||
wrapper = Wrap("testdata/pullorder.xml", cfg)
|
||||
folders = wrapper.Folders()
|
||||
|
||||
for _, tc := range expected {
|
||||
if actual := folders[tc.name].Order; actual != tc.order {
|
||||
t.Errorf("Incorrect pull order for %q: %v != %v", tc.name, actual, tc.order)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
internal/config/testdata/overridenvalues.xml
vendored
@@ -15,6 +15,7 @@
|
||||
<upnpEnabled>false</upnpEnabled>
|
||||
<upnpLeaseMinutes>60</upnpLeaseMinutes>
|
||||
<upnpRenewalMinutes>15</upnpRenewalMinutes>
|
||||
<upnpTimeoutSeconds>15</upnpTimeoutSeconds>
|
||||
<restartOnWakeup>false</restartOnWakeup>
|
||||
<autoUpgradeIntervalH>24</autoUpgradeIntervalH>
|
||||
<keepTemporariesH>48</keepTemporariesH>
|
||||
|
||||
25
internal/config/testdata/pullorder.xml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<configuration version="10">
|
||||
<folder id="f1" directory="testdata/">
|
||||
</folder>
|
||||
<folder id="f2" directory="testdata/">
|
||||
<order>random</order>
|
||||
</folder>
|
||||
<folder id="f3" directory="testdata/">
|
||||
<order>alphabetic</order>
|
||||
</folder>
|
||||
<folder id="f4" directory="testdata/">
|
||||
<order>whatever</order>
|
||||
</folder>
|
||||
<folder id="f5" directory="testdata/">
|
||||
<order>smallestFirst</order>
|
||||
</folder>
|
||||
<folder id="f6" directory="testdata/">
|
||||
<order>largestFirst</order>
|
||||
</folder>
|
||||
<folder id="f7" directory="testdata/">
|
||||
<order>oldestFirst</order>
|
||||
</folder>
|
||||
<folder id="f8" directory="testdata/">
|
||||
<order>newestFirst</order>
|
||||
</folder>
|
||||
</configuration>
|
||||
@@ -10,11 +10,11 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
// An interface to handle configuration changes, and a wrapper type á la
|
||||
@@ -49,7 +49,12 @@ type Wrapper struct {
|
||||
// Wrap wraps an existing Configuration structure and ties it to a file on
|
||||
// disk.
|
||||
func Wrap(path string, cfg Configuration) *Wrapper {
|
||||
w := &Wrapper{cfg: cfg, path: path}
|
||||
w := &Wrapper{
|
||||
cfg: cfg,
|
||||
path: path,
|
||||
mut: sync.NewMutex(),
|
||||
sMut: sync.NewMutex(),
|
||||
}
|
||||
w.replaces = make(chan Configuration)
|
||||
go w.Serve()
|
||||
return w
|
||||
@@ -151,7 +156,7 @@ func (w *Wrapper) SetDevice(dev DeviceConfiguration) {
|
||||
w.replaces <- w.cfg.Copy()
|
||||
}
|
||||
|
||||
// Devices returns a map of folders. Folder structures should not be changed,
|
||||
// Folders returns a map of folders. Folder structures should not be changed,
|
||||
// other than for the purpose of updating via SetFolder().
|
||||
func (w *Wrapper) Folders() map[string]FolderConfiguration {
|
||||
w.mut.Lock()
|
||||
@@ -159,12 +164,6 @@ func (w *Wrapper) Folders() map[string]FolderConfiguration {
|
||||
if w.folderMap == nil {
|
||||
w.folderMap = make(map[string]FolderConfiguration, len(w.cfg.Folders))
|
||||
for _, fld := range w.cfg.Folders {
|
||||
path, err := osutil.ExpandTilde(fld.Path)
|
||||
if err != nil {
|
||||
l.Warnln("home:", err)
|
||||
continue
|
||||
}
|
||||
fld.Path = path
|
||||
w.folderMap[fld.ID] = fld
|
||||
}
|
||||
}
|
||||
@@ -221,31 +220,8 @@ func (w *Wrapper) SetGUI(gui GUIConfiguration) {
|
||||
w.replaces <- w.cfg.Copy()
|
||||
}
|
||||
|
||||
// Sets the folder error state. Emits ConfigSaved to cause a GUI refresh.
|
||||
func (w *Wrapper) SetFolderError(id string, err error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
w.folderMap = nil
|
||||
|
||||
for i := range w.cfg.Folders {
|
||||
if w.cfg.Folders[i].ID == id {
|
||||
errstr := ""
|
||||
if err != nil {
|
||||
errstr = err.Error()
|
||||
}
|
||||
if errstr != w.cfg.Folders[i].Invalid {
|
||||
w.cfg.Folders[i].Invalid = errstr
|
||||
events.Default.Log(events.ConfigSaved, w.cfg)
|
||||
w.replaces <- w.cfg.Copy()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns whether or not connection attempts from the given device should be
|
||||
// silently ignored.
|
||||
// IgnoredDevice returns whether or not connection attempts from the given
|
||||
// device should be silently ignored.
|
||||
func (w *Wrapper) IgnoredDevice(id protocol.DeviceID) bool {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -10,18 +10,17 @@
|
||||
// depending on who calls us. We transform paths to wire-format (NFC and
|
||||
// slashes) on the way to the database, and transform to native format
|
||||
// (varying separator and encoding) on the way back out.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/internal/config"
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
@@ -123,14 +122,15 @@ func NewBlockFinder(db *leveldb.DB, cfg *config.Wrapper) *BlockFinder {
|
||||
}
|
||||
|
||||
f := &BlockFinder{
|
||||
db: db,
|
||||
db: db,
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
f.Changed(cfg.Raw())
|
||||
cfg.Subscribe(f)
|
||||
return f
|
||||
}
|
||||
|
||||
// Implements config.Handler interface
|
||||
// Changed implements config.Handler interface
|
||||
func (f *BlockFinder) Changed(cfg config.Configuration) error {
|
||||
folders := make([]string, len(cfg.Folders))
|
||||
for i, folder := range cfg.Folders {
|
||||
@@ -146,11 +146,11 @@ func (f *BlockFinder) Changed(cfg config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// An iterator function which iterates over all matching blocks for the given
|
||||
// hash. The iterator function has to return either true (if they are happy with
|
||||
// the block) or false to continue iterating for whatever reason.
|
||||
// The iterator finally returns the result, whether or not a satisfying block
|
||||
// was eventually found.
|
||||
// Iterate takes an iterator function which iterates over all matching blocks
|
||||
// for the given hash. The iterator function has to return either true (if
|
||||
// they are happy with the block) or false to continue iterating for whatever
|
||||
// reason. The iterator finally returns the result, whether or not a
|
||||
// satisfying block was eventually found.
|
||||
func (f *BlockFinder) Iterate(hash []byte, iterFn func(string, string, int32) bool) bool {
|
||||
f.mut.RLock()
|
||||
folders := f.folders
|
||||
@@ -171,8 +171,8 @@ func (f *BlockFinder) Iterate(hash []byte, iterFn func(string, string, int32) bo
|
||||
return false
|
||||
}
|
||||
|
||||
// A method for repairing incorrect blockmap entries, removes the old entry
|
||||
// and replaces it with a new entry for the given block
|
||||
// Fix repairs incorrect blockmap entries, removing the old entry and
|
||||
// replacing it with a new entry for the given block
|
||||
func (f *BlockFinder) Fix(folder, file string, index int32, oldHash, newHash []byte) error {
|
||||
buf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(buf, uint32(index))
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
"crypto/rand"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
@@ -132,7 +133,7 @@ func TestConcurrentSetClear(t *testing.T) {
|
||||
|
||||
dur := 30 * time.Second
|
||||
t0 := time.Now()
|
||||
var wg sync.WaitGroup
|
||||
wg := sync.NewWaitGroup()
|
||||
|
||||
os.RemoveAll("testdata/concurrent-set-clear.db")
|
||||
db, err := leveldb.OpenFile("testdata/concurrent-set-clear.db", &opt.Options{OpenFilesCacheCapacity: 10})
|
||||
@@ -188,7 +189,7 @@ func TestConcurrentSetOnly(t *testing.T) {
|
||||
|
||||
dur := 30 * time.Second
|
||||
t0 := time.Now()
|
||||
var wg sync.WaitGroup
|
||||
wg := sync.NewWaitGroup()
|
||||
|
||||
os.RemoveAll("testdata/concurrent-set-only.db")
|
||||
db, err := leveldb.OpenFile("testdata/concurrent-set-only.db", &opt.Options{OpenFilesCacheCapacity: 10})
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
|
||||
var (
|
||||
clockTick int64
|
||||
clockMut sync.Mutex
|
||||
clockMut = sync.NewMutex()
|
||||
)
|
||||
|
||||
func clock(v int64) int64 {
|
||||
@@ -976,11 +976,11 @@ func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
|
||||
var tf FileInfoTruncated
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
return tf, err
|
||||
} else {
|
||||
var tf protocol.FileInfo
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
var tf protocol.FileInfo
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
func ldbCheckGlobals(db *leveldb.DB, folder []byte) {
|
||||
|
||||
@@ -156,6 +156,9 @@ func (o *versionList) UnmarshalXDR(bs []byte) error {
|
||||
|
||||
func (o *versionList) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
_versionsSize := int(xr.ReadUint32())
|
||||
if _versionsSize < 0 {
|
||||
return xdr.ElementSizeExceeded("versions", _versionsSize, 0)
|
||||
}
|
||||
o.versions = make([]fileVersion, _versionsSize)
|
||||
for i := range o.versions {
|
||||
(&o.versions[i]).DecodeXDRFrom(xr)
|
||||
|
||||
@@ -13,10 +13,9 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
@@ -50,6 +49,7 @@ func NewFileSet(folder string, db *leveldb.DB) *FileSet {
|
||||
folder: folder,
|
||||
db: db,
|
||||
blockmap: NewBlockMap(db, folder),
|
||||
mutex: sync.NewMutex(),
|
||||
}
|
||||
|
||||
ldbCheckGlobals(db, []byte(folder))
|
||||
|
||||
@@ -13,15 +13,6 @@ type FileInfoTruncated struct {
|
||||
ActualSize int64
|
||||
}
|
||||
|
||||
func ToTruncated(file protocol.FileInfo) FileInfoTruncated {
|
||||
t := FileInfoTruncated{
|
||||
FileInfo: file,
|
||||
ActualSize: file.Size(),
|
||||
}
|
||||
t.FileInfo.Blocks = nil
|
||||
return t
|
||||
}
|
||||
|
||||
func (f *FileInfoTruncated) UnmarshalXDR(bs []byte) error {
|
||||
err := f.FileInfo.UnmarshalXDR(bs)
|
||||
f.ActualSize = f.FileInfo.Size()
|
||||
|
||||
@@ -9,12 +9,13 @@ package discover
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
var device protocol.DeviceID
|
||||
@@ -97,7 +98,7 @@ func TestUDP4Success(t *testing.T) {
|
||||
|
||||
// Do a lookup in a separate routine
|
||||
addrs := []string{}
|
||||
wg := sync.WaitGroup{}
|
||||
wg := sync.NewWaitGroup()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
addrs = client.Lookup(device)
|
||||
@@ -193,7 +194,7 @@ func TestUDP4Failure(t *testing.T) {
|
||||
|
||||
// Do a lookup in a separate routine
|
||||
addrs := []string{}
|
||||
wg := sync.WaitGroup{}
|
||||
wg := sync.NewWaitGroup()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
addrs = client.Lookup(device)
|
||||
|
||||
@@ -12,16 +12,19 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
func init() {
|
||||
for _, proto := range []string{"udp", "udp4", "udp6"} {
|
||||
Register(proto, func(uri *url.URL, pkt *Announce) (Client, error) {
|
||||
c := &UDPClient{}
|
||||
c := &UDPClient{
|
||||
wg: sync.NewWaitGroup(),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
err := c.Start(uri, pkt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/internal/beacon"
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
type Discoverer struct {
|
||||
@@ -59,6 +59,8 @@ func NewDiscoverer(id protocol.DeviceID, addresses []string) *Discoverer {
|
||||
negCacheCutoff: 3 * time.Minute,
|
||||
registry: make(map[protocol.DeviceID][]CacheEntry),
|
||||
lastLookup: make(map[protocol.DeviceID]time.Time),
|
||||
registryLock: sync.NewRWMutex(),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +142,7 @@ func (d *Discoverer) StartGlobal(servers []string, extPort uint16) {
|
||||
|
||||
d.extPort = extPort
|
||||
pkt := d.announcementPkt()
|
||||
wg := sync.WaitGroup{}
|
||||
wg := sync.NewWaitGroup()
|
||||
clients := make(chan Client, len(servers))
|
||||
for _, address := range servers {
|
||||
wg.Add(1)
|
||||
@@ -216,7 +218,7 @@ func (d *Discoverer) Lookup(device protocol.DeviceID) []string {
|
||||
// server client and one local announcement interval has passed. This is
|
||||
// to avoid finding local peers on their remote address at startup.
|
||||
results := make(chan []string, len(d.clients))
|
||||
wg := sync.WaitGroup{}
|
||||
wg := sync.NewWaitGroup()
|
||||
for _, client := range d.clients {
|
||||
wg.Add(1)
|
||||
go func(c Client) {
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestGlobalDiscovery(t *testing.T) {
|
||||
|
||||
addrs := d.Lookup(device)
|
||||
if len(addrs) != 2 {
|
||||
t.Fatal("Wrong numer of addresses", addrs)
|
||||
t.Fatal("Wrong number of addresses", addrs)
|
||||
}
|
||||
|
||||
for _, addr := range []string{"test.com:1234", "best.com:2345"} {
|
||||
@@ -119,10 +119,10 @@ func TestGlobalDiscovery(t *testing.T) {
|
||||
|
||||
addrs = d.Lookup(device)
|
||||
if len(addrs) != 2 {
|
||||
t.Fatal("Wrong numer of addresses", addrs)
|
||||
t.Fatal("Wrong number of addresses", addrs)
|
||||
}
|
||||
|
||||
// Answer should be cached, so number of lookups should have not incresed
|
||||
// Answer should be cached, so number of lookups should have not increased
|
||||
for _, c := range []*DummyClient{c1, c2, c3} {
|
||||
if len(c.lookups) != 1 || c.lookups[0] != device {
|
||||
t.Fatal("Wrong lookups")
|
||||
|
||||
@@ -172,6 +172,9 @@ func (o *Announce) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.Magic = xr.ReadUint32()
|
||||
(&o.This).DecodeXDRFrom(xr)
|
||||
_ExtraSize := int(xr.ReadUint32())
|
||||
if _ExtraSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Extra", _ExtraSize, 16)
|
||||
}
|
||||
if _ExtraSize > 16 {
|
||||
return xdr.ElementSizeExceeded("Extra", _ExtraSize, 16)
|
||||
}
|
||||
@@ -266,6 +269,9 @@ func (o *Device) UnmarshalXDR(bs []byte) error {
|
||||
func (o *Device) DecodeXDRFrom(xr *xdr.Reader) error {
|
||||
o.ID = xr.ReadBytesMax(32)
|
||||
_AddressesSize := int(xr.ReadUint32())
|
||||
if _AddressesSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Addresses", _AddressesSize, 16)
|
||||
}
|
||||
if _AddressesSize > 16 {
|
||||
return xdr.ElementSizeExceeded("Addresses", _AddressesSize, 16)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ package events
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
stdsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
type EventType int
|
||||
@@ -101,7 +103,6 @@ type Subscription struct {
|
||||
mask EventType
|
||||
id int
|
||||
events chan Event
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
var Default = NewLogger()
|
||||
@@ -113,7 +114,8 @@ var (
|
||||
|
||||
func NewLogger() *Logger {
|
||||
return &Logger{
|
||||
subs: make(map[int]*Subscription),
|
||||
subs: make(map[int]*Subscription),
|
||||
mutex: sync.NewMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,9 +170,6 @@ func (l *Logger) Unsubscribe(s *Subscription) {
|
||||
}
|
||||
|
||||
func (s *Subscription) Poll(timeout time.Duration) (Event, error) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if debug {
|
||||
dl.Debugln("poll", timeout)
|
||||
}
|
||||
@@ -197,15 +196,16 @@ type BufferedSubscription struct {
|
||||
next int
|
||||
cur int
|
||||
mut sync.Mutex
|
||||
cond *sync.Cond
|
||||
cond *stdsync.Cond
|
||||
}
|
||||
|
||||
func NewBufferedSubscription(s *Subscription, size int) *BufferedSubscription {
|
||||
bs := &BufferedSubscription{
|
||||
sub: s,
|
||||
buf: make([]Event, size),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
bs.cond = sync.NewCond(&bs.mut)
|
||||
bs.cond = stdsync.NewCond(bs.mut)
|
||||
go bs.pollingLoop()
|
||||
return bs
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ func Convert(pattern string, flags int) (*regexp.Regexp, error) {
|
||||
return regexp.Compile(pattern)
|
||||
}
|
||||
|
||||
// Matches the pattern against the string, with the given flags,
|
||||
// and returns true if the match is successful.
|
||||
// Match matches the pattern against the string, with the given flags, and
|
||||
// returns true if the match is successful.
|
||||
func Match(pattern, s string, flags int) (bool, error) {
|
||||
exp, err := Convert(pattern, flags)
|
||||
if err != nil {
|
||||
|
||||
@@ -55,6 +55,7 @@ var testcases = []testcase{
|
||||
{"foo.txt", "foo.TXT", CaseFold, true},
|
||||
{"(?i)foo.txt", "foo.TXT", 0, true},
|
||||
{"(?i)**foo.txt", "/dev/tmp/foo.TXT", 0, true},
|
||||
{"(?i)!**foo.txt", "/dev/tmp/foo.TXT", 0, false},
|
||||
|
||||
// These characters are literals in glob, but not in regexp.
|
||||
{"hey$hello", "hey$hello", 0, true},
|
||||
@@ -69,7 +70,9 @@ var testcases = []testcase{
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin":
|
||||
case "windows":
|
||||
testcases = append(testcases, testcase{"foo.txt", "foo.TXT", 0, true})
|
||||
case "darwin":
|
||||
testcases = append(testcases, testcase{"foo.txt", "foo.TXT", 0, true})
|
||||
fallthrough
|
||||
default:
|
||||
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/fnmatch"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
type Pattern struct {
|
||||
@@ -30,9 +30,8 @@ type Pattern struct {
|
||||
func (p Pattern) String() string {
|
||||
if p.include {
|
||||
return p.match.String()
|
||||
} else {
|
||||
return "(?exclude)" + p.match.String()
|
||||
}
|
||||
return "(?exclude)" + p.match.String()
|
||||
}
|
||||
|
||||
type Matcher struct {
|
||||
@@ -48,6 +47,7 @@ func New(withCache bool) *Matcher {
|
||||
m := &Matcher{
|
||||
withCache: withCache,
|
||||
stop: make(chan struct{}),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
if withCache {
|
||||
go m.clean(2 * time.Hour)
|
||||
@@ -94,6 +94,10 @@ func (m *Matcher) Parse(r io.Reader, file string) error {
|
||||
}
|
||||
|
||||
func (m *Matcher) Match(file string) (result bool) {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
@@ -127,6 +131,10 @@ func (m *Matcher) Match(file string) (result bool) {
|
||||
|
||||
// Patterns return a list of the loaded regexp patterns, as strings
|
||||
func (m *Matcher) Patterns() []string {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
// deviceActivity tracks the number of outstanding requests per device and can
|
||||
@@ -23,6 +22,7 @@ type deviceActivity struct {
|
||||
func newDeviceActivity() *deviceActivity {
|
||||
return &deviceActivity{
|
||||
act: make(map[protocol.DeviceID]int),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
type folderState int
|
||||
@@ -28,7 +19,7 @@ const (
|
||||
FolderIdle folderState = iota
|
||||
FolderScanning
|
||||
FolderSyncing
|
||||
FolderCleaning
|
||||
FolderError
|
||||
)
|
||||
|
||||
func (s folderState) String() string {
|
||||
@@ -37,10 +28,10 @@ func (s folderState) String() string {
|
||||
return "idle"
|
||||
case FolderScanning:
|
||||
return "scanning"
|
||||
case FolderCleaning:
|
||||
return "cleaning"
|
||||
case FolderSyncing:
|
||||
return "syncing"
|
||||
case FolderError:
|
||||
return "error"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
@@ -51,10 +42,16 @@ type stateTracker struct {
|
||||
|
||||
mut sync.Mutex
|
||||
current folderState
|
||||
err error
|
||||
changed time.Time
|
||||
}
|
||||
|
||||
// setState sets the new folder state, for states other than FolderError.
|
||||
func (s *stateTracker) setState(newState folderState) {
|
||||
if newState == FolderError {
|
||||
panic("must use setError")
|
||||
}
|
||||
|
||||
s.mut.Lock()
|
||||
if newState != s.current {
|
||||
/* This should hold later...
|
||||
@@ -74,6 +71,7 @@ func (s *stateTracker) setState(newState folderState) {
|
||||
}
|
||||
|
||||
s.current = newState
|
||||
s.err = nil
|
||||
s.changed = time.Now()
|
||||
|
||||
events.Default.Log(events.StateChanged, eventData)
|
||||
@@ -81,9 +79,35 @@ func (s *stateTracker) setState(newState folderState) {
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *stateTracker) getState() (current folderState, changed time.Time) {
|
||||
// getState returns the current state, the time when it last changed, and the
|
||||
// current error or nil.
|
||||
func (s *stateTracker) getState() (current folderState, changed time.Time, err error) {
|
||||
s.mut.Lock()
|
||||
current, changed = s.current, s.changed
|
||||
current, changed, err = s.current, s.changed, s.err
|
||||
s.mut.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// setError sets the folder state to FolderError with the specified error.
|
||||
func (s *stateTracker) setError(err error) {
|
||||
s.mut.Lock()
|
||||
if s.current != FolderError || s.err.Error() != err.Error() {
|
||||
eventData := map[string]interface{}{
|
||||
"folder": s.folder,
|
||||
"to": FolderError.String(),
|
||||
"from": s.current.String(),
|
||||
"error": err.Error(),
|
||||
}
|
||||
|
||||
if !s.changed.IsZero() {
|
||||
eventData["duration"] = time.Since(s.changed).Seconds()
|
||||
}
|
||||
|
||||
s.current = FolderError
|
||||
s.err = err
|
||||
s.changed = time.Now()
|
||||
|
||||
events.Default.Log(events.StateChanged, eventData)
|
||||
}
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
stdsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/syncthing/syncthing/internal/scanner"
|
||||
"github.com/syncthing/syncthing/internal/stats"
|
||||
"github.com/syncthing/syncthing/internal/symlinks"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
"github.com/syncthing/syncthing/internal/versioner"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
@@ -49,8 +50,9 @@ type service interface {
|
||||
Jobs() ([]string, []string) // In progress, Queued
|
||||
BringToFront(string)
|
||||
|
||||
setState(folderState)
|
||||
getState() (folderState, time.Time)
|
||||
setState(state folderState)
|
||||
setError(err error)
|
||||
getState() (folderState, time.Time, error)
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
@@ -85,7 +87,7 @@ type Model struct {
|
||||
}
|
||||
|
||||
var (
|
||||
SymlinkWarning = sync.Once{}
|
||||
SymlinkWarning = stdsync.Once{}
|
||||
)
|
||||
|
||||
// NewModel creates and starts a new model. The model starts in read-only mode,
|
||||
@@ -113,24 +115,27 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
|
||||
protoConn: make(map[protocol.DeviceID]protocol.Connection),
|
||||
rawConn: make(map[protocol.DeviceID]io.Closer),
|
||||
deviceVer: make(map[protocol.DeviceID]string),
|
||||
|
||||
fmut: sync.NewRWMutex(),
|
||||
pmut: sync.NewRWMutex(),
|
||||
}
|
||||
if cfg.Options().ProgressUpdateIntervalS > -1 {
|
||||
go m.progressEmitter.Serve()
|
||||
}
|
||||
|
||||
var timeout = 20 * 60 // seconds
|
||||
if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 {
|
||||
it, err := strconv.Atoi(t)
|
||||
if err == nil {
|
||||
timeout = it
|
||||
}
|
||||
}
|
||||
deadlockDetect(&m.fmut, time.Duration(timeout)*time.Second)
|
||||
deadlockDetect(&m.pmut, time.Duration(timeout)*time.Second)
|
||||
return m
|
||||
}
|
||||
|
||||
// StartRW starts read/write processing on the current model. When in
|
||||
// StartDeadlockDetector starts a deadlock detector on the models locks which
|
||||
// causes panics in case the locks cannot be acquired in the given timeout
|
||||
// period.
|
||||
func (m *Model) StartDeadlockDetector(timeout time.Duration) {
|
||||
l.Infof("Starting deadlock detector with %v timeout", timeout)
|
||||
deadlockDetect(m.fmut, timeout)
|
||||
deadlockDetect(m.pmut, timeout)
|
||||
}
|
||||
|
||||
// StartFolderRW starts read/write processing on the current model. When in
|
||||
// read/write mode the model will attempt to keep in sync with the cluster by
|
||||
// pulling needed files from peer devices.
|
||||
func (m *Model) StartFolderRW(folder string) {
|
||||
@@ -144,7 +149,7 @@ func (m *Model) StartFolderRW(folder string) {
|
||||
if ok {
|
||||
panic("cannot start already running folder " + folder)
|
||||
}
|
||||
p := newRWFolder(m, cfg)
|
||||
p := newRWFolder(m, m.shortID, cfg)
|
||||
m.folderRunners[folder] = p
|
||||
m.fmut.Unlock()
|
||||
|
||||
@@ -153,7 +158,7 @@ func (m *Model) StartFolderRW(folder string) {
|
||||
if !ok {
|
||||
l.Fatalf("Requested versioning type %q that does not exist", cfg.Versioning.Type)
|
||||
}
|
||||
p.versioner = factory(folder, cfg.Path, cfg.Versioning.Params)
|
||||
p.versioner = factory(folder, cfg.Path(), cfg.Versioning.Params)
|
||||
}
|
||||
|
||||
if cfg.LenientMtimes {
|
||||
@@ -163,9 +168,9 @@ func (m *Model) StartFolderRW(folder string) {
|
||||
go p.Serve()
|
||||
}
|
||||
|
||||
// StartRO 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.
|
||||
// StartFolderRO starts read only processing on the current model. When in
|
||||
// read only mode the model will announce files to the cluster but not pull in
|
||||
// any external changes.
|
||||
func (m *Model) StartFolderRO(folder string) {
|
||||
m.fmut.Lock()
|
||||
cfg, ok := m.folderCfgs[folder]
|
||||
@@ -201,7 +206,7 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
// ConnectionStats returns a map with connection statistics for each connected device.
|
||||
func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
||||
func (m *Model) ConnectionStats() map[string]interface{} {
|
||||
type remoteAddrer interface {
|
||||
RemoteAddr() net.Addr
|
||||
}
|
||||
@@ -209,7 +214,8 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
||||
m.pmut.RLock()
|
||||
m.fmut.RLock()
|
||||
|
||||
var res = make(map[string]ConnectionInfo)
|
||||
var res = make(map[string]interface{})
|
||||
conns := make(map[string]ConnectionInfo, len(m.protoConn))
|
||||
for device, conn := range m.protoConn {
|
||||
ci := ConnectionInfo{
|
||||
Statistics: conn.Statistics(),
|
||||
@@ -219,9 +225,11 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
||||
ci.Address = nc.RemoteAddr().String()
|
||||
}
|
||||
|
||||
res[device.String()] = ci
|
||||
conns[device.String()] = ci
|
||||
}
|
||||
|
||||
res["connections"] = conns
|
||||
|
||||
m.fmut.RUnlock()
|
||||
m.pmut.RUnlock()
|
||||
|
||||
@@ -237,7 +245,7 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
||||
return res
|
||||
}
|
||||
|
||||
// Returns statistics about each device
|
||||
// DeviceStatistics returns statistics about each device
|
||||
func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
|
||||
var res = make(map[string]stats.DeviceStatistics)
|
||||
for id := range m.cfg.Devices() {
|
||||
@@ -246,7 +254,7 @@ func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
|
||||
return res
|
||||
}
|
||||
|
||||
// Returns statistics about each folder
|
||||
// FolderStatistics returns statistics about each folder
|
||||
func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
|
||||
var res = make(map[string]stats.FolderStatistics)
|
||||
for id := range m.cfg.Folders() {
|
||||
@@ -255,7 +263,8 @@ func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
|
||||
return res
|
||||
}
|
||||
|
||||
// Returns the completion status, in percent, for the given device and folder.
|
||||
// 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
|
||||
|
||||
@@ -369,53 +378,71 @@ func (m *Model) NeedSize(folder string) (nfiles int, bytes int64) {
|
||||
return
|
||||
}
|
||||
|
||||
// NeedFiles returns the list of currently needed files in progress, queued,
|
||||
// and to be queued on next puller iteration. Also takes a soft cap which is
|
||||
// only respected when adding files from the model rather than the runner queue.
|
||||
func (m *Model) NeedFolderFiles(folder string, max int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) {
|
||||
// NeedFolderFiles returns paginated list of currently needed files in
|
||||
// progress, queued, and to be queued on next puller iteration, as well as the
|
||||
// total number of files currently needed.
|
||||
func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int) {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
var progress, queued, rest []db.FileInfoTruncated
|
||||
var seen map[string]bool
|
||||
total := 0
|
||||
|
||||
runner, ok := m.folderRunners[folder]
|
||||
if ok {
|
||||
progressNames, queuedNames := runner.Jobs()
|
||||
|
||||
progress = make([]db.FileInfoTruncated, len(progressNames))
|
||||
queued = make([]db.FileInfoTruncated, len(queuedNames))
|
||||
seen = make(map[string]bool, len(progressNames)+len(queuedNames))
|
||||
|
||||
for i, name := range progressNames {
|
||||
if f, ok := rf.GetGlobalTruncated(name); ok {
|
||||
progress[i] = f
|
||||
seen[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
for i, name := range queuedNames {
|
||||
if f, ok := rf.GetGlobalTruncated(name); ok {
|
||||
queued[i] = f
|
||||
seen[name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
left := max - len(progress) - len(queued)
|
||||
if max < 1 || left > 0 {
|
||||
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
|
||||
left--
|
||||
ft := f.(db.FileInfoTruncated)
|
||||
if !seen[ft.Name] {
|
||||
rest = append(rest, ft)
|
||||
}
|
||||
return max < 1 || left > 0
|
||||
})
|
||||
}
|
||||
return progress, queued, rest
|
||||
rf, ok := m.folderFiles[folder]
|
||||
if !ok {
|
||||
return nil, nil, nil, 0
|
||||
}
|
||||
return nil, nil, nil
|
||||
|
||||
var progress, queued, rest []db.FileInfoTruncated
|
||||
var seen map[string]struct{}
|
||||
|
||||
skip := (page - 1) * perpage
|
||||
get := perpage
|
||||
|
||||
runner, ok := m.folderRunners[folder]
|
||||
if ok {
|
||||
allProgressNames, allQueuedNames := runner.Jobs()
|
||||
|
||||
var progressNames, queuedNames []string
|
||||
progressNames, skip, get = getChunk(allProgressNames, skip, get)
|
||||
queuedNames, skip, get = getChunk(allQueuedNames, skip, get)
|
||||
|
||||
progress = make([]db.FileInfoTruncated, len(progressNames))
|
||||
queued = make([]db.FileInfoTruncated, len(queuedNames))
|
||||
seen = make(map[string]struct{}, len(progressNames)+len(queuedNames))
|
||||
|
||||
for i, name := range progressNames {
|
||||
if f, ok := rf.GetGlobalTruncated(name); ok {
|
||||
progress[i] = f
|
||||
seen[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for i, name := range queuedNames {
|
||||
if f, ok := rf.GetGlobalTruncated(name); ok {
|
||||
queued[i] = f
|
||||
seen[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rest = make([]db.FileInfoTruncated, 0, perpage)
|
||||
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
|
||||
total++
|
||||
if skip > 0 {
|
||||
skip--
|
||||
return true
|
||||
}
|
||||
if get > 0 {
|
||||
ft := f.(db.FileInfoTruncated)
|
||||
if _, ok := seen[ft.Name]; !ok {
|
||||
rest = append(rest, ft)
|
||||
get--
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return progress, queued, rest, total
|
||||
}
|
||||
|
||||
// Index is called when a new device is connected and we receive their full index.
|
||||
@@ -727,7 +754,7 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
|
||||
}
|
||||
m.fmut.RLock()
|
||||
fn := filepath.Join(m.folderCfgs[folder].Path, name)
|
||||
fn := filepath.Join(m.folderCfgs[folder].Path(), name)
|
||||
m.fmut.RUnlock()
|
||||
|
||||
var reader io.ReaderAt
|
||||
@@ -767,15 +794,23 @@ func (m *Model) ReplaceLocal(folder string, fs []protocol.FileInfo) {
|
||||
|
||||
func (m *Model) CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
m.fmut.RLock()
|
||||
f, ok := m.folderFiles[folder].Get(protocol.LocalDeviceID, file)
|
||||
fs, ok := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
if !ok {
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
f, ok := fs.Get(protocol.LocalDeviceID, file)
|
||||
return f, ok
|
||||
}
|
||||
|
||||
func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
m.fmut.RLock()
|
||||
f, ok := m.folderFiles[folder].GetGlobal(file)
|
||||
fs, ok := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
if !ok {
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
f, ok := fs.GetGlobal(file)
|
||||
return f, ok
|
||||
}
|
||||
|
||||
@@ -810,7 +845,7 @@ func (m *Model) GetIgnores(folder string) ([]string, []string, error) {
|
||||
return lines, nil, fmt.Errorf("Folder %s does not exist", folder)
|
||||
}
|
||||
|
||||
fd, err := os.Open(filepath.Join(cfg.Path, ".stignore"))
|
||||
fd, err := os.Open(filepath.Join(cfg.Path(), ".stignore"))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return lines, nil, nil
|
||||
@@ -826,10 +861,7 @@ func (m *Model) GetIgnores(folder string) ([]string, []string, error) {
|
||||
}
|
||||
|
||||
m.fmut.RLock()
|
||||
var patterns []string
|
||||
if matcher := m.folderIgnores[folder]; matcher != nil {
|
||||
patterns = matcher.Patterns()
|
||||
}
|
||||
patterns := m.folderIgnores[folder].Patterns()
|
||||
m.fmut.RUnlock()
|
||||
|
||||
return lines, patterns, nil
|
||||
@@ -841,7 +873,7 @@ func (m *Model) SetIgnores(folder string, content []string) error {
|
||||
return fmt.Errorf("Folder %s does not exist", folder)
|
||||
}
|
||||
|
||||
fd, err := ioutil.TempFile(cfg.Path, ".syncthing.stignore-"+folder)
|
||||
fd, err := ioutil.TempFile(cfg.Path(), ".syncthing.stignore-"+folder)
|
||||
if err != nil {
|
||||
l.Warnln("Saving .stignore:", err)
|
||||
return err
|
||||
@@ -862,7 +894,7 @@ func (m *Model) SetIgnores(folder string, content []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
file := filepath.Join(cfg.Path, ".stignore")
|
||||
file := filepath.Join(cfg.Path(), ".stignore")
|
||||
err = osutil.Rename(fd.Name(), file)
|
||||
if err != nil {
|
||||
l.Warnln("Saving .stignore:", err)
|
||||
@@ -978,7 +1010,7 @@ func sendIndexTo(initial bool, minLocalVer int64, conn protocol.Connection, fold
|
||||
maxLocalVer = f.LocalVersion
|
||||
}
|
||||
|
||||
if (ignores != nil && ignores.Match(f.Name)) || symlinkInvalid(f.IsSymlink()) {
|
||||
if ignores.Match(f.Name) || symlinkInvalid(f.IsSymlink()) {
|
||||
if debug {
|
||||
l.Debugln("not sending update for ignored/unsupported symlink", f)
|
||||
}
|
||||
@@ -1033,8 +1065,8 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
|
||||
m.fmut.RUnlock()
|
||||
|
||||
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
|
||||
"folder": folder,
|
||||
"numFiles": len(fs),
|
||||
"folder": folder,
|
||||
"items": len(fs),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1073,7 +1105,7 @@ func (m *Model) AddFolder(cfg config.FolderConfiguration) {
|
||||
}
|
||||
|
||||
ignores := ignore.New(m.cfg.Options().CacheIgnoredFiles)
|
||||
_ = ignores.Load(filepath.Join(cfg.Path, ".stignore")) // Ignore error, there might not be an .stignore
|
||||
_ = ignores.Load(filepath.Join(cfg.Path(), ".stignore")) // Ignore error, there might not be an .stignore
|
||||
m.folderIgnores[cfg.ID] = ignores
|
||||
|
||||
m.addedFolder = true
|
||||
@@ -1082,16 +1114,16 @@ func (m *Model) AddFolder(cfg config.FolderConfiguration) {
|
||||
|
||||
func (m *Model) ScanFolders() map[string]error {
|
||||
m.fmut.RLock()
|
||||
var folders = make([]string, 0, len(m.folderCfgs))
|
||||
folders := make([]string, 0, len(m.folderCfgs))
|
||||
for folder := range m.folderCfgs {
|
||||
folders = append(folders, folder)
|
||||
}
|
||||
m.fmut.RUnlock()
|
||||
|
||||
var errors = make(map[string]error, len(m.folderCfgs))
|
||||
var errorsMut sync.Mutex
|
||||
errors := make(map[string]error, len(m.folderCfgs))
|
||||
errorsMut := sync.NewMutex()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg := sync.NewWaitGroup()
|
||||
wg.Add(len(folders))
|
||||
for _, folder := range folders {
|
||||
folder := folder
|
||||
@@ -1101,11 +1133,15 @@ func (m *Model) ScanFolders() map[string]error {
|
||||
errorsMut.Lock()
|
||||
errors[folder] = err
|
||||
errorsMut.Unlock()
|
||||
|
||||
// Potentially sets the error twice, once in the scanner just
|
||||
// by doing a check, and once here, if the error returned is
|
||||
// the same one as returned by CheckFolderHealth, though
|
||||
// duplicate set is handled by SetFolderError
|
||||
m.cfg.SetFolderError(folder, err)
|
||||
// duplicate set is handled by setError.
|
||||
m.fmut.RLock()
|
||||
srv := m.folderRunners[folder]
|
||||
m.fmut.RUnlock()
|
||||
srv.setError(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
@@ -1141,7 +1177,7 @@ func (m *Model) ScanFolderSubs(folder string, subs []string) error {
|
||||
return errors.New("no such folder")
|
||||
}
|
||||
|
||||
_ = ignores.Load(filepath.Join(folderCfg.Path, ".stignore")) // Ignore error, there might not be an .stignore
|
||||
_ = ignores.Load(filepath.Join(folderCfg.Path(), ".stignore")) // Ignore error, there might not be an .stignore
|
||||
|
||||
// Required to make sure that we start indexing at a directory we're already
|
||||
// aware off.
|
||||
@@ -1167,7 +1203,7 @@ nextSub:
|
||||
subs = unifySubs
|
||||
|
||||
w := &scanner.Walker{
|
||||
Dir: folderCfg.Path,
|
||||
Dir: folderCfg.Path(),
|
||||
Subs: subs,
|
||||
Matcher: ignores,
|
||||
BlockSize: protocol.BlockSize,
|
||||
@@ -1176,37 +1212,36 @@ nextSub:
|
||||
CurrentFiler: cFiler{m, folder},
|
||||
IgnorePerms: folderCfg.IgnorePerms,
|
||||
AutoNormalize: folderCfg.AutoNormalize,
|
||||
Hashers: folderCfg.Hashers,
|
||||
Hashers: m.numHashers(folder),
|
||||
ShortID: m.shortID,
|
||||
}
|
||||
|
||||
runner.setState(FolderScanning)
|
||||
defer runner.setState(FolderIdle)
|
||||
fchan, err := w.Walk()
|
||||
|
||||
fchan, err := w.Walk()
|
||||
if err != nil {
|
||||
m.cfg.SetFolderError(folder, err)
|
||||
runner.setError(err)
|
||||
return err
|
||||
}
|
||||
batchSize := 100
|
||||
batch := make([]protocol.FileInfo, 0, batchSize)
|
||||
|
||||
batchSizeFiles := 100
|
||||
batchSizeBlocks := 2048 // about 256 MB
|
||||
|
||||
batch := make([]protocol.FileInfo, 0, batchSizeFiles)
|
||||
blocksHandled := 0
|
||||
|
||||
for f := range fchan {
|
||||
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
|
||||
"folder": folder,
|
||||
"name": f.Name,
|
||||
"modified": time.Unix(f.Modified, 0),
|
||||
"flags": fmt.Sprintf("0%o", f.Flags),
|
||||
"size": f.Size(),
|
||||
})
|
||||
if len(batch) == batchSize {
|
||||
if len(batch) == batchSizeFiles || blocksHandled > batchSizeBlocks {
|
||||
if err := m.CheckFolderHealth(folder); err != nil {
|
||||
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, err)
|
||||
return err
|
||||
}
|
||||
fs.Update(protocol.LocalDeviceID, batch)
|
||||
m.updateLocals(folder, batch)
|
||||
batch = batch[:0]
|
||||
blocksHandled = 0
|
||||
}
|
||||
batch = append(batch, f)
|
||||
blocksHandled += len(f.Blocks)
|
||||
}
|
||||
|
||||
if err := m.CheckFolderHealth(folder); err != nil {
|
||||
@@ -1241,12 +1276,12 @@ nextSub:
|
||||
return true
|
||||
}
|
||||
|
||||
if len(batch) == batchSize {
|
||||
fs.Update(protocol.LocalDeviceID, batch)
|
||||
if len(batch) == batchSizeFiles {
|
||||
m.updateLocals(folder, batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
|
||||
if (ignores != nil && ignores.Match(f.Name)) || symlinkInvalid(f.IsSymlink()) {
|
||||
if ignores.Match(f.Name) || symlinkInvalid(f.IsSymlink()) {
|
||||
// File has been ignored or an unsupported symlink. Set invalid bit.
|
||||
if debug {
|
||||
l.Debugln("setting invalid bit on ignored", f)
|
||||
@@ -1257,15 +1292,8 @@ nextSub:
|
||||
Modified: f.Modified,
|
||||
Version: f.Version, // The file is still the same, so don't bump version
|
||||
}
|
||||
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
|
||||
"folder": folder,
|
||||
"name": f.Name,
|
||||
"modified": time.Unix(f.Modified, 0),
|
||||
"flags": fmt.Sprintf("0%o", f.Flags),
|
||||
"size": f.Size(),
|
||||
})
|
||||
batch = append(batch, nf)
|
||||
} else if _, err := os.Lstat(filepath.Join(folderCfg.Path, f.Name)); err != nil {
|
||||
} else if _, err := osutil.Lstat(filepath.Join(folderCfg.Path(), f.Name)); err != nil {
|
||||
// File has been deleted.
|
||||
|
||||
// We don't specifically verify that the error is
|
||||
@@ -1281,25 +1309,40 @@ nextSub:
|
||||
Modified: f.Modified,
|
||||
Version: f.Version.Update(m.shortID),
|
||||
}
|
||||
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
|
||||
"folder": folder,
|
||||
"name": f.Name,
|
||||
"modified": time.Unix(f.Modified, 0),
|
||||
"flags": fmt.Sprintf("0%o", f.Flags),
|
||||
"size": f.Size(),
|
||||
})
|
||||
batch = append(batch, nf)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(batch) > 0 {
|
||||
fs.Update(protocol.LocalDeviceID, batch)
|
||||
m.updateLocals(folder, batch)
|
||||
}
|
||||
|
||||
runner.setState(FolderIdle)
|
||||
return nil
|
||||
}
|
||||
|
||||
// numHashers returns the number of hasher routines to use for a given folder,
|
||||
// taking into account configuration and available CPU cores.
|
||||
func (m *Model) numHashers(folder string) int {
|
||||
m.fmut.Lock()
|
||||
folderCfg := m.folderCfgs[folder]
|
||||
numFolders := len(m.folderCfgs)
|
||||
m.fmut.Unlock()
|
||||
|
||||
if folderCfg.Hashers > 0 {
|
||||
// Specific value set in the config, use that.
|
||||
return folderCfg.Hashers
|
||||
}
|
||||
|
||||
if perFolder := runtime.GOMAXPROCS(-1) / numFolders; perFolder > 0 {
|
||||
// We have CPUs to spare, divide them per folder.
|
||||
return perFolder
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
// clusterConfig returns a ClusterConfigMessage that is correct for the given peer device
|
||||
func (m *Model) clusterConfig(device protocol.DeviceID) protocol.ClusterConfigMessage {
|
||||
cm := protocol.ClusterConfigMessage{
|
||||
@@ -1339,22 +1382,28 @@ func (m *Model) clusterConfig(device protocol.DeviceID) protocol.ClusterConfigMe
|
||||
return cm
|
||||
}
|
||||
|
||||
func (m *Model) State(folder string) (string, time.Time) {
|
||||
func (m *Model) State(folder string) (string, time.Time, error) {
|
||||
m.fmut.RLock()
|
||||
runner, ok := m.folderRunners[folder]
|
||||
m.fmut.RUnlock()
|
||||
if !ok {
|
||||
return "", time.Time{}
|
||||
// The returned error should be an actual folder error, so returning
|
||||
// errors.New("does not exist") or similar here would be
|
||||
// inappropriate.
|
||||
return "", time.Time{}, nil
|
||||
}
|
||||
state, changed := runner.getState()
|
||||
return state.String(), changed
|
||||
state, changed, err := runner.getState()
|
||||
return state.String(), changed, err
|
||||
}
|
||||
|
||||
func (m *Model) Override(folder string) {
|
||||
m.fmut.RLock()
|
||||
fs := m.folderFiles[folder]
|
||||
fs, ok := m.folderFiles[folder]
|
||||
runner := m.folderRunners[folder]
|
||||
m.fmut.RUnlock()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
runner.setState(FolderScanning)
|
||||
batch := make([]protocol.FileInfo, 0, indexBatchSize)
|
||||
@@ -1476,8 +1525,8 @@ func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly
|
||||
}
|
||||
|
||||
if !dirsonly && base != "" {
|
||||
last[base] = []int64{
|
||||
f.Modified, f.Size(),
|
||||
last[base] = []interface{}{
|
||||
time.Unix(f.Modified, 0), f.Size(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1510,7 +1559,7 @@ func (m *Model) Availability(folder, file string) []protocol.DeviceID {
|
||||
return availableDevices
|
||||
}
|
||||
|
||||
// Bump the given files priority in the job queue
|
||||
// BringToFront bumps the given files priority in the job queue.
|
||||
func (m *Model) BringToFront(folder, file string) {
|
||||
m.pmut.RLock()
|
||||
defer m.pmut.RUnlock()
|
||||
@@ -1521,30 +1570,29 @@ func (m *Model) BringToFront(folder, file string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns current folder error, or nil if the folder is healthy.
|
||||
// Updates the Invalid field on the folder configuration struct, and emits a
|
||||
// ConfigSaved event which causes a GUI refresh.
|
||||
// CheckFolderHealth checks the folder for common errors and returns the
|
||||
// current folder error, or nil if the folder is healthy.
|
||||
func (m *Model) CheckFolderHealth(id string) error {
|
||||
folder, ok := m.cfg.Folders()[id]
|
||||
if !ok {
|
||||
return errors.New("Folder does not exist")
|
||||
return errors.New("folder does not exist")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(folder.Path)
|
||||
fi, err := os.Stat(folder.Path())
|
||||
if m.CurrentLocalVersion(id) > 0 {
|
||||
// Safety check. If the cached index contains files but the
|
||||
// folder doesn't exist, we have a problem. We would assume
|
||||
// that all files have been deleted which might not be the case,
|
||||
// so mark it as invalid instead.
|
||||
if err != nil || !fi.IsDir() {
|
||||
err = errors.New("Folder path missing")
|
||||
err = errors.New("folder path missing")
|
||||
} else if !folder.HasMarker() {
|
||||
err = errors.New("Folder marker missing")
|
||||
err = errors.New("folder marker missing")
|
||||
}
|
||||
} else if os.IsNotExist(err) {
|
||||
// If we don't have any files in the index, and the directory
|
||||
// doesn't exist, try creating it.
|
||||
err = os.MkdirAll(folder.Path, 0700)
|
||||
err = os.MkdirAll(folder.Path(), 0700)
|
||||
if err == nil {
|
||||
err = folder.CreateMarker()
|
||||
}
|
||||
@@ -1554,35 +1602,29 @@ func (m *Model) CheckFolderHealth(id string) error {
|
||||
err = folder.CreateMarker()
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if folder.Invalid != "" {
|
||||
l.Infof("Starting folder %q after error %q", folder.ID, folder.Invalid)
|
||||
m.cfg.SetFolderError(id, nil)
|
||||
m.fmut.RLock()
|
||||
runner, runnerExists := m.folderRunners[folder.ID]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
var oldErr error
|
||||
if runnerExists {
|
||||
_, _, oldErr = runner.getState()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if oldErr != nil && oldErr.Error() != err.Error() {
|
||||
l.Infof("Folder %q error changed: %q -> %q", folder.ID, oldErr, err)
|
||||
} else if oldErr == nil {
|
||||
l.Warnf("Stopping folder %q - %v", folder.ID, err)
|
||||
}
|
||||
|
||||
if folder, ok := m.cfg.Folders()[id]; !ok || folder.Invalid != "" {
|
||||
panic("Unable to unset folder \"" + id + "\" error.")
|
||||
if runnerExists {
|
||||
runner.setError(err)
|
||||
}
|
||||
} else if oldErr != nil {
|
||||
l.Infof("Folder %q error is cleared, restarting", folder.ID)
|
||||
if runnerExists {
|
||||
runner.setState(FolderIdle)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if folder.Invalid == err.Error() {
|
||||
return err
|
||||
}
|
||||
|
||||
// folder is a copy of the original struct, hence Invalid value is
|
||||
// preserved after the set.
|
||||
m.cfg.SetFolderError(id, err)
|
||||
|
||||
if folder.Invalid == "" {
|
||||
l.Warnf("Stopping folder %q - %v", folder.ID, err)
|
||||
} else {
|
||||
l.Infof("Folder %q error changed: %q -> %q", folder.ID, folder.Invalid, err)
|
||||
}
|
||||
|
||||
if folder, ok := m.cfg.Folders()[id]; !ok || folder.Invalid != err.Error() {
|
||||
panic("Unable to set folder \"" + id + "\" error.")
|
||||
}
|
||||
|
||||
return err
|
||||
@@ -1606,9 +1648,23 @@ func (m *Model) String() string {
|
||||
func symlinkInvalid(isLink bool) bool {
|
||||
if !symlinks.Supported && isLink {
|
||||
SymlinkWarning.Do(func() {
|
||||
l.Warnln("Symlinks are disabled, unsupported or require Administrator priviledges. This might cause your folder to appear out of sync.")
|
||||
l.Warnln("Symlinks are disabled, unsupported or require Administrator privileges. This might cause your folder to appear out of sync.")
|
||||
})
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Skips `skip` elements and retrieves up to `get` elements from a given slice.
|
||||
// Returns the resulting slice, plus how much elements are left to skip or
|
||||
// copy to satisfy the values which were provided, given the slice is not
|
||||
// big enough.
|
||||
func getChunk(data []string, skip, get int) ([]string, int, int) {
|
||||
l := len(data)
|
||||
if l <= skip {
|
||||
return []string{}, skip - l, get
|
||||
} else if l < skip+get {
|
||||
return data[skip:l], 0, get - (l - skip)
|
||||
}
|
||||
return data[skip : skip+get], 0, 0
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -35,8 +34,8 @@ func init() {
|
||||
device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
|
||||
|
||||
defaultFolderConfig = config.FolderConfiguration{
|
||||
ID: "default",
|
||||
Path: "testdata",
|
||||
ID: "default",
|
||||
RawPath: "testdata",
|
||||
Devices: []config.FolderDeviceConfiguration{
|
||||
{
|
||||
DeviceID: device1,
|
||||
@@ -540,7 +539,7 @@ func TestIgnores(t *testing.T) {
|
||||
t.Error("No error")
|
||||
}
|
||||
|
||||
m.AddFolder(config.FolderConfiguration{ID: "fresh", Path: "XXX"})
|
||||
m.AddFolder(config.FolderConfiguration{ID: "fresh", RawPath: "XXX"})
|
||||
ignores, _, err = m.GetIgnores("fresh")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
@@ -596,7 +595,7 @@ func TestROScanRecovery(t *testing.T) {
|
||||
|
||||
fcfg := config.FolderConfiguration{
|
||||
ID: "default",
|
||||
Path: "testdata/rotestfolder",
|
||||
RawPath: "testdata/rotestfolder",
|
||||
RescanIntervalS: 1,
|
||||
}
|
||||
cfg := config.Wrap("/tmp/test", config.Configuration{
|
||||
@@ -608,7 +607,7 @@ func TestROScanRecovery(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
os.RemoveAll(fcfg.Path)
|
||||
os.RemoveAll(fcfg.RawPath)
|
||||
|
||||
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
|
||||
@@ -621,26 +620,30 @@ func TestROScanRecovery(t *testing.T) {
|
||||
if time.Now().After(timeout) {
|
||||
return fmt.Errorf("Timed out waiting for status: %s, current status: %s", status, m.cfg.Folders()["default"].Invalid)
|
||||
}
|
||||
if m.cfg.Folders()["default"].Invalid == status {
|
||||
_, _, err := m.State("default")
|
||||
if err == nil && status == "" {
|
||||
return nil
|
||||
}
|
||||
if err != nil && err.Error() == status {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
if err := waitFor("Folder path missing"); err != nil {
|
||||
if err := waitFor("folder path missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
os.Mkdir(fcfg.Path, 0700)
|
||||
os.Mkdir(fcfg.RawPath, 0700)
|
||||
|
||||
if err := waitFor("Folder marker missing"); err != nil {
|
||||
if err := waitFor("folder marker missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
fd, err := os.Create(filepath.Join(fcfg.Path, ".stfolder"))
|
||||
fd, err := os.Create(filepath.Join(fcfg.RawPath, ".stfolder"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
@@ -652,16 +655,16 @@ func TestROScanRecovery(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
os.Remove(filepath.Join(fcfg.Path, ".stfolder"))
|
||||
os.Remove(filepath.Join(fcfg.RawPath, ".stfolder"))
|
||||
|
||||
if err := waitFor("Folder marker missing"); err != nil {
|
||||
if err := waitFor("folder marker missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
os.Remove(fcfg.Path)
|
||||
os.Remove(fcfg.RawPath)
|
||||
|
||||
if err := waitFor("Folder path missing"); err != nil {
|
||||
if err := waitFor("folder path missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
@@ -676,7 +679,7 @@ func TestRWScanRecovery(t *testing.T) {
|
||||
|
||||
fcfg := config.FolderConfiguration{
|
||||
ID: "default",
|
||||
Path: "testdata/rwtestfolder",
|
||||
RawPath: "testdata/rwtestfolder",
|
||||
RescanIntervalS: 1,
|
||||
}
|
||||
cfg := config.Wrap("/tmp/test", config.Configuration{
|
||||
@@ -688,7 +691,7 @@ func TestRWScanRecovery(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
os.RemoveAll(fcfg.Path)
|
||||
os.RemoveAll(fcfg.RawPath)
|
||||
|
||||
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
|
||||
@@ -701,26 +704,30 @@ func TestRWScanRecovery(t *testing.T) {
|
||||
if time.Now().After(timeout) {
|
||||
return fmt.Errorf("Timed out waiting for status: %s, current status: %s", status, m.cfg.Folders()["default"].Invalid)
|
||||
}
|
||||
if m.cfg.Folders()["default"].Invalid == status {
|
||||
_, _, err := m.State("default")
|
||||
if err == nil && status == "" {
|
||||
return nil
|
||||
}
|
||||
if err != nil && err.Error() == status {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
if err := waitFor("Folder path missing"); err != nil {
|
||||
if err := waitFor("folder path missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
os.Mkdir(fcfg.Path, 0700)
|
||||
os.Mkdir(fcfg.RawPath, 0700)
|
||||
|
||||
if err := waitFor("Folder marker missing"); err != nil {
|
||||
if err := waitFor("folder marker missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
fd, err := os.Create(filepath.Join(fcfg.Path, ".stfolder"))
|
||||
fd, err := os.Create(filepath.Join(fcfg.RawPath, ".stfolder"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
@@ -732,16 +739,16 @@ func TestRWScanRecovery(t *testing.T) {
|
||||
return
|
||||
}
|
||||
|
||||
os.Remove(filepath.Join(fcfg.Path, ".stfolder"))
|
||||
os.Remove(filepath.Join(fcfg.RawPath, ".stfolder"))
|
||||
|
||||
if err := waitFor("Folder marker missing"); err != nil {
|
||||
if err := waitFor("folder marker missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
os.Remove(fcfg.Path)
|
||||
os.Remove(fcfg.RawPath)
|
||||
|
||||
if err := waitFor("Folder path missing"); err != nil {
|
||||
if err := waitFor("folder path missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
@@ -753,7 +760,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
b := func(isfile bool, path ...string) protocol.FileInfo {
|
||||
var flags uint32 = protocol.FlagDirectory
|
||||
flags := uint32(protocol.FlagDirectory)
|
||||
blocks := []protocol.BlockInfo{}
|
||||
if isfile {
|
||||
flags = 0
|
||||
@@ -767,7 +774,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
filedata := []int64{0x666, 0xa}
|
||||
filedata := []interface{}{time.Unix(0x666, 0), 0xa}
|
||||
|
||||
testdata := []protocol.FileInfo{
|
||||
b(false, "another"),
|
||||
@@ -839,13 +846,13 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
|
||||
result := m.GlobalDirectoryTree("default", "", -1, false)
|
||||
|
||||
if !reflect.DeepEqual(result, expectedResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(expectedResult))
|
||||
if mm(result) != mm(expectedResult) {
|
||||
t.Errorf("Does not match:\n%#v\n%#v", result, expectedResult)
|
||||
}
|
||||
|
||||
result = m.GlobalDirectoryTree("default", "another", -1, false)
|
||||
|
||||
if !reflect.DeepEqual(result, expectedResult["another"]) {
|
||||
if mm(result) != mm(expectedResult["another"]) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(expectedResult["another"]))
|
||||
}
|
||||
|
||||
@@ -857,7 +864,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
"rootfile": filedata,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -878,7 +885,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
"rootfile": filedata,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -908,7 +915,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -927,7 +934,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -937,7 +944,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
"file": filedata,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -946,7 +953,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
"with": map[string]interface{}{},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -957,7 +964,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -970,7 +977,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -983,7 +990,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -991,7 +998,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
result = m.GlobalDirectoryTree("default", "som", -1, false)
|
||||
currentResult = map[string]interface{}{}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
}
|
||||
@@ -1002,7 +1009,7 @@ func TestGlobalDirectorySelfFixing(t *testing.T) {
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
b := func(isfile bool, path ...string) protocol.FileInfo {
|
||||
var flags uint32 = protocol.FlagDirectory
|
||||
flags := uint32(protocol.FlagDirectory)
|
||||
blocks := []protocol.BlockInfo{}
|
||||
if isfile {
|
||||
flags = 0
|
||||
@@ -1016,7 +1023,7 @@ func TestGlobalDirectorySelfFixing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
filedata := []int64{0x666, 0xa}
|
||||
filedata := []interface{}{time.Unix(0x666, 0).Format(time.RFC3339), 0xa}
|
||||
|
||||
testdata := []protocol.FileInfo{
|
||||
b(true, "another", "directory", "afile"),
|
||||
@@ -1097,7 +1104,7 @@ func TestGlobalDirectorySelfFixing(t *testing.T) {
|
||||
|
||||
result := m.GlobalDirectoryTree("default", "", -1, false)
|
||||
|
||||
if !reflect.DeepEqual(result, expectedResult) {
|
||||
if mm(result) != mm(expectedResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(expectedResult))
|
||||
}
|
||||
|
||||
@@ -1108,7 +1115,7 @@ func TestGlobalDirectorySelfFixing(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -1117,7 +1124,7 @@ func TestGlobalDirectorySelfFixing(t *testing.T) {
|
||||
"invalid": map[string]interface{}{},
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
|
||||
@@ -1126,7 +1133,7 @@ func TestGlobalDirectorySelfFixing(t *testing.T) {
|
||||
result = m.GlobalDirectoryTree("default", "xthis", 1, false)
|
||||
currentResult = map[string]interface{}{}
|
||||
|
||||
if !reflect.DeepEqual(result, currentResult) {
|
||||
if mm(result) != mm(currentResult) {
|
||||
t.Errorf("Does not match:\n%s\n%s", mm(result), mm(currentResult))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ package model
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/config"
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
type ProgressEmitter struct {
|
||||
@@ -27,22 +27,23 @@ type ProgressEmitter struct {
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
// Creates a new progress emitter which emits DownloadProgress events every
|
||||
// interval.
|
||||
// NewProgressEmitter creates a new progress emitter which emits
|
||||
// DownloadProgress events every interval.
|
||||
func NewProgressEmitter(cfg *config.Wrapper) *ProgressEmitter {
|
||||
t := &ProgressEmitter{
|
||||
stop: make(chan struct{}),
|
||||
registry: make(map[string]*sharedPullerState),
|
||||
last: make(map[string]map[string]*pullerProgress),
|
||||
timer: time.NewTimer(time.Millisecond),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
t.Changed(cfg.Raw())
|
||||
cfg.Subscribe(t)
|
||||
return t
|
||||
}
|
||||
|
||||
// Starts progress emitter which starts emitting DownloadProgress events as
|
||||
// the progress happens.
|
||||
// Serve starts the progress emitter which starts emitting DownloadProgress
|
||||
// events as the progress happens.
|
||||
func (t *ProgressEmitter) Serve() {
|
||||
for {
|
||||
select {
|
||||
@@ -80,7 +81,8 @@ func (t *ProgressEmitter) Serve() {
|
||||
}
|
||||
}
|
||||
|
||||
// Interface method to handle configuration changes
|
||||
// Changed implements the config.Handler Interface to handle configuration
|
||||
// changes
|
||||
func (t *ProgressEmitter) Changed(cfg config.Configuration) error {
|
||||
t.mut.Lock()
|
||||
defer t.mut.Unlock()
|
||||
@@ -92,7 +94,7 @@ func (t *ProgressEmitter) Changed(cfg config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stops the emitter.
|
||||
// Stop stops the emitter.
|
||||
func (t *ProgressEmitter) Stop() {
|
||||
t.stop <- struct{}{}
|
||||
}
|
||||
@@ -111,7 +113,7 @@ func (t *ProgressEmitter) Register(s *sharedPullerState) {
|
||||
t.registry[filepath.Join(s.folder, s.file.Name)] = s
|
||||
}
|
||||
|
||||
// Deregister a puller which will stop boardcasting pullers state.
|
||||
// Deregister a puller which will stop broadcasting pullers state.
|
||||
func (t *ProgressEmitter) Deregister(s *sharedPullerState) {
|
||||
t.mut.Lock()
|
||||
defer t.mut.Unlock()
|
||||
@@ -121,7 +123,7 @@ func (t *ProgressEmitter) Deregister(s *sharedPullerState) {
|
||||
delete(t.registry, filepath.Join(s.folder, s.file.Name))
|
||||
}
|
||||
|
||||
// Returns number of bytes completed in the given folder.
|
||||
// BytesCompleted returns the number of bytes completed in the given folder.
|
||||
func (t *ProgressEmitter) BytesCompleted(folder string) (bytes int64) {
|
||||
t.mut.Lock()
|
||||
defer t.mut.Unlock()
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/syncthing/syncthing/internal/config"
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
var timeout = 10 * time.Millisecond
|
||||
@@ -50,7 +51,9 @@ func TestProgressEmitter(t *testing.T) {
|
||||
|
||||
expectTimeout(w, t)
|
||||
|
||||
s := sharedPullerState{}
|
||||
s := sharedPullerState{
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
p.Register(&s)
|
||||
|
||||
expectEvent(w, t, 1)
|
||||
|
||||
@@ -6,21 +6,34 @@
|
||||
|
||||
package model
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
type jobQueue struct {
|
||||
progress []string
|
||||
queued []string
|
||||
queued []jobQueueEntry
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
func newJobQueue() *jobQueue {
|
||||
return &jobQueue{}
|
||||
type jobQueueEntry struct {
|
||||
name string
|
||||
size int64
|
||||
modified int64
|
||||
}
|
||||
|
||||
func (q *jobQueue) Push(file string) {
|
||||
func newJobQueue() *jobQueue {
|
||||
return &jobQueue{
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
func (q *jobQueue) Push(file string, size, modified int64) {
|
||||
q.mut.Lock()
|
||||
q.queued = append(q.queued, file)
|
||||
q.queued = append(q.queued, jobQueueEntry{file, size, modified})
|
||||
q.mut.Unlock()
|
||||
}
|
||||
|
||||
@@ -32,8 +45,7 @@ func (q *jobQueue) Pop() (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
var f string
|
||||
f = q.queued[0]
|
||||
f := q.queued[0].name
|
||||
q.queued = q.queued[1:]
|
||||
q.progress = append(q.progress, f)
|
||||
|
||||
@@ -45,7 +57,7 @@ func (q *jobQueue) BringToFront(filename string) {
|
||||
defer q.mut.Unlock()
|
||||
|
||||
for i, cur := range q.queued {
|
||||
if cur == filename {
|
||||
if cur.name == filename {
|
||||
if i > 0 {
|
||||
// Shift the elements before the selected element one step to
|
||||
// the right, overwriting the selected element
|
||||
@@ -79,7 +91,62 @@ func (q *jobQueue) Jobs() ([]string, []string) {
|
||||
copy(progress, q.progress)
|
||||
|
||||
queued := make([]string, len(q.queued))
|
||||
copy(queued, q.queued)
|
||||
for i := range q.queued {
|
||||
queued[i] = q.queued[i].name
|
||||
}
|
||||
|
||||
return progress, queued
|
||||
}
|
||||
|
||||
func (q *jobQueue) Shuffle() {
|
||||
q.mut.Lock()
|
||||
defer q.mut.Unlock()
|
||||
|
||||
l := len(q.queued)
|
||||
for i := range q.queued {
|
||||
r := rand.Intn(l)
|
||||
q.queued[i], q.queued[r] = q.queued[r], q.queued[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (q *jobQueue) SortSmallestFirst() {
|
||||
q.mut.Lock()
|
||||
defer q.mut.Unlock()
|
||||
|
||||
sort.Sort(smallestFirst(q.queued))
|
||||
}
|
||||
|
||||
func (q *jobQueue) SortLargestFirst() {
|
||||
q.mut.Lock()
|
||||
defer q.mut.Unlock()
|
||||
|
||||
sort.Sort(sort.Reverse(smallestFirst(q.queued)))
|
||||
}
|
||||
|
||||
func (q *jobQueue) SortOldestFirst() {
|
||||
q.mut.Lock()
|
||||
defer q.mut.Unlock()
|
||||
|
||||
sort.Sort(oldestFirst(q.queued))
|
||||
}
|
||||
|
||||
func (q *jobQueue) SortNewestFirst() {
|
||||
q.mut.Lock()
|
||||
defer q.mut.Unlock()
|
||||
|
||||
sort.Sort(sort.Reverse(oldestFirst(q.queued)))
|
||||
}
|
||||
|
||||
// The usual sort.Interface boilerplate
|
||||
|
||||
type smallestFirst []jobQueueEntry
|
||||
|
||||
func (q smallestFirst) Len() int { return len(q) }
|
||||
func (q smallestFirst) Less(a, b int) bool { return q[a].size < q[b].size }
|
||||
func (q smallestFirst) Swap(a, b int) { q[a], q[b] = q[b], q[a] }
|
||||
|
||||
type oldestFirst []jobQueueEntry
|
||||
|
||||
func (q oldestFirst) Len() int { return len(q) }
|
||||
func (q oldestFirst) Less(a, b int) bool { return q[a].modified < q[b].modified }
|
||||
func (q oldestFirst) Swap(a, b int) { q[a], q[b] = q[b], q[a] }
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
func TestJobQueue(t *testing.T) {
|
||||
// Some random actions
|
||||
q := newJobQueue()
|
||||
q.Push("f1")
|
||||
q.Push("f2")
|
||||
q.Push("f3")
|
||||
q.Push("f4")
|
||||
q.Push("f1", 0, 0)
|
||||
q.Push("f2", 0, 0)
|
||||
q.Push("f3", 0, 0)
|
||||
q.Push("f4", 0, 0)
|
||||
|
||||
progress, queued := q.Jobs()
|
||||
if len(progress) != 0 || len(queued) != 4 {
|
||||
@@ -43,7 +43,7 @@ func TestJobQueue(t *testing.T) {
|
||||
t.Fatal("Wrong length", len(progress), len(queued))
|
||||
}
|
||||
|
||||
q.Push(n)
|
||||
q.Push(n, 0, 0)
|
||||
progress, queued = q.Jobs()
|
||||
if len(progress) != 0 || len(queued) != 4 {
|
||||
t.Fatal("Wrong length")
|
||||
@@ -120,10 +120,10 @@ func TestJobQueue(t *testing.T) {
|
||||
|
||||
func TestBringToFront(t *testing.T) {
|
||||
q := newJobQueue()
|
||||
q.Push("f1")
|
||||
q.Push("f2")
|
||||
q.Push("f3")
|
||||
q.Push("f4")
|
||||
q.Push("f1", 0, 0)
|
||||
q.Push("f2", 0, 0)
|
||||
q.Push("f3", 0, 0)
|
||||
q.Push("f4", 0, 0)
|
||||
|
||||
_, queued := q.Jobs()
|
||||
if !reflect.DeepEqual(queued, []string{"f1", "f2", "f3", "f4"}) {
|
||||
@@ -159,12 +159,101 @@ func TestBringToFront(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShuffle(t *testing.T) {
|
||||
q := newJobQueue()
|
||||
q.Push("f1", 0, 0)
|
||||
q.Push("f2", 0, 0)
|
||||
q.Push("f3", 0, 0)
|
||||
q.Push("f4", 0, 0)
|
||||
|
||||
// This test will fail once in eight million times (1 / (4!)^5) :)
|
||||
for i := 0; i < 5; i++ {
|
||||
q.Shuffle()
|
||||
_, queued := q.Jobs()
|
||||
if l := len(queued); l != 4 {
|
||||
t.Fatalf("Weird length %d returned from Jobs()", l)
|
||||
}
|
||||
|
||||
t.Logf("%v", queued)
|
||||
if !reflect.DeepEqual(queued, []string{"f1", "f2", "f3", "f4"}) {
|
||||
// The queue was shuffled
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Error("Queue was not shuffled after five attempts.")
|
||||
}
|
||||
|
||||
func TestSortBySize(t *testing.T) {
|
||||
q := newJobQueue()
|
||||
q.Push("f1", 20, 0)
|
||||
q.Push("f2", 40, 0)
|
||||
q.Push("f3", 30, 0)
|
||||
q.Push("f4", 10, 0)
|
||||
|
||||
q.SortSmallestFirst()
|
||||
|
||||
_, actual := q.Jobs()
|
||||
if l := len(actual); l != 4 {
|
||||
t.Fatalf("Weird length %d returned from Jobs()", l)
|
||||
}
|
||||
expected := []string{"f4", "f1", "f3", "f2"}
|
||||
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("SortSmallestFirst(): %#v != %#v", actual, expected)
|
||||
}
|
||||
|
||||
q.SortLargestFirst()
|
||||
|
||||
_, actual = q.Jobs()
|
||||
if l := len(actual); l != 4 {
|
||||
t.Fatalf("Weird length %d returned from Jobs()", l)
|
||||
}
|
||||
expected = []string{"f2", "f3", "f1", "f4"}
|
||||
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("SortLargestFirst(): %#v != %#v", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortByAge(t *testing.T) {
|
||||
q := newJobQueue()
|
||||
q.Push("f1", 0, 20)
|
||||
q.Push("f2", 0, 40)
|
||||
q.Push("f3", 0, 30)
|
||||
q.Push("f4", 0, 10)
|
||||
|
||||
q.SortOldestFirst()
|
||||
|
||||
_, actual := q.Jobs()
|
||||
if l := len(actual); l != 4 {
|
||||
t.Fatalf("Weird length %d returned from Jobs()", l)
|
||||
}
|
||||
expected := []string{"f4", "f1", "f3", "f2"}
|
||||
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("SortOldestFirst(): %#v != %#v", actual, expected)
|
||||
}
|
||||
|
||||
q.SortNewestFirst()
|
||||
|
||||
_, actual = q.Jobs()
|
||||
if l := len(actual); l != 4 {
|
||||
t.Fatalf("Weird length %d returned from Jobs()", l)
|
||||
}
|
||||
expected = []string{"f2", "f3", "f1", "f4"}
|
||||
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("SortNewestFirst(): %#v != %#v", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkJobQueueBump(b *testing.B) {
|
||||
files := genFiles(b.N)
|
||||
|
||||
q := newJobQueue()
|
||||
for _, f := range files {
|
||||
q.Push(f.Name)
|
||||
q.Push(f.Name, 0, 0)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
@@ -180,7 +269,7 @@ func BenchmarkJobQueuePushPopDone10k(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
q := newJobQueue()
|
||||
for _, f := range files {
|
||||
q.Push(f.Name)
|
||||
q.Push(f.Name, 0, 0)
|
||||
}
|
||||
for _ = range files {
|
||||
n, _ := q.Pop()
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
type roFolder struct {
|
||||
@@ -23,11 +25,14 @@ type roFolder struct {
|
||||
|
||||
func newROFolder(model *Model, folder string, interval time.Duration) *roFolder {
|
||||
return &roFolder{
|
||||
stateTracker: stateTracker{folder: folder},
|
||||
folder: folder,
|
||||
intv: interval,
|
||||
model: model,
|
||||
stop: make(chan struct{}),
|
||||
stateTracker: stateTracker{
|
||||
folder: folder,
|
||||
mut: sync.NewMutex(),
|
||||
},
|
||||
folder: folder,
|
||||
intv: interval,
|
||||
model: model,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +72,8 @@ func (s *roFolder) Serve() {
|
||||
// Potentially sets the error twice, once in the scanner just
|
||||
// by doing a check, and once here, if the error returned is
|
||||
// the same one as returned by CheckFolderHealth, though
|
||||
// duplicate set is handled by SetFolderError
|
||||
s.model.cfg.SetFolderError(s.folder, err)
|
||||
// duplicate set is handled by setError.
|
||||
s.setError(err)
|
||||
reschedule()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
@@ -24,6 +23,7 @@ import (
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
"github.com/syncthing/syncthing/internal/scanner"
|
||||
"github.com/syncthing/syncthing/internal/symlinks"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
"github.com/syncthing/syncthing/internal/versioner"
|
||||
)
|
||||
|
||||
@@ -68,26 +68,33 @@ type rwFolder struct {
|
||||
lenientMtimes bool
|
||||
copiers int
|
||||
pullers int
|
||||
shortID uint64
|
||||
order config.PullOrder
|
||||
|
||||
stop chan struct{}
|
||||
queue *jobQueue
|
||||
dbUpdates chan protocol.FileInfo
|
||||
}
|
||||
|
||||
func newRWFolder(m *Model, cfg config.FolderConfiguration) *rwFolder {
|
||||
func newRWFolder(m *Model, shortID uint64, cfg config.FolderConfiguration) *rwFolder {
|
||||
return &rwFolder{
|
||||
stateTracker: stateTracker{folder: cfg.ID},
|
||||
stateTracker: stateTracker{
|
||||
folder: cfg.ID,
|
||||
mut: sync.NewMutex(),
|
||||
},
|
||||
|
||||
model: m,
|
||||
progressEmitter: m.progressEmitter,
|
||||
|
||||
folder: cfg.ID,
|
||||
dir: cfg.Path,
|
||||
dir: cfg.Path(),
|
||||
scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second,
|
||||
ignorePerms: cfg.IgnorePerms,
|
||||
lenientMtimes: cfg.LenientMtimes,
|
||||
copiers: cfg.Copiers,
|
||||
pullers: cfg.Pullers,
|
||||
shortID: shortID,
|
||||
order: cfg.Order,
|
||||
|
||||
stop: make(chan struct{}),
|
||||
queue: newJobQueue(),
|
||||
@@ -116,6 +123,11 @@ func (p *rwFolder) Serve() {
|
||||
var prevIgnoreHash string
|
||||
|
||||
rescheduleScan := func() {
|
||||
if p.scanIntv == 0 {
|
||||
// We should not run scans, so it should not be rescheduled.
|
||||
return
|
||||
}
|
||||
|
||||
// Sleep a random time between 3/4 and 5/4 of the configured interval.
|
||||
sleepNanos := (p.scanIntv.Nanoseconds()*3 + rand.Int63n(2*p.scanIntv.Nanoseconds())) / 4
|
||||
intv := time.Duration(sleepNanos) * time.Nanosecond
|
||||
@@ -243,8 +255,8 @@ func (p *rwFolder) Serve() {
|
||||
// Potentially sets the error twice, once in the scanner just
|
||||
// by doing a check, and once here, if the error returned is
|
||||
// the same one as returned by CheckFolderHealth, though
|
||||
// duplicate set is handled by SetFolderError
|
||||
p.model.cfg.SetFolderError(p.folder, err)
|
||||
// duplicate set is handled by setError.
|
||||
p.setError(err)
|
||||
rescheduleScan()
|
||||
continue
|
||||
}
|
||||
@@ -277,10 +289,10 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
copyChan := make(chan copyBlocksState)
|
||||
finisherChan := make(chan *sharedPullerState)
|
||||
|
||||
var updateWg sync.WaitGroup
|
||||
var copyWg sync.WaitGroup
|
||||
var pullWg sync.WaitGroup
|
||||
var doneWg sync.WaitGroup
|
||||
updateWg := sync.NewWaitGroup()
|
||||
copyWg := sync.NewWaitGroup()
|
||||
pullWg := sync.NewWaitGroup()
|
||||
doneWg := sync.NewWaitGroup()
|
||||
|
||||
if debug {
|
||||
l.Debugln(p, "c", p.copiers, "p", p.pullers)
|
||||
@@ -336,13 +348,9 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
buckets := map[string][]protocol.FileInfo{}
|
||||
|
||||
folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
|
||||
// Needed items are delivered sorted lexicographically. This isn't
|
||||
// really optimal from a performance point of view - it would be
|
||||
// better if files were handled in random order, to spread the load
|
||||
// over the cluster. But it means that we can be sure that we fully
|
||||
// handle directories before the files that go inside them, which is
|
||||
// nice.
|
||||
// Needed items are delivered sorted lexicographically. We'll handle
|
||||
// directories as they come along, so parents before children. Files
|
||||
// are queued and the order may be changed later.
|
||||
|
||||
file := intf.(protocol.FileInfo)
|
||||
|
||||
@@ -382,13 +390,32 @@ func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
default:
|
||||
// A new or changed file or symlink. This is the only case where we
|
||||
// do stuff concurrently in the background
|
||||
p.queue.Push(file.Name)
|
||||
p.queue.Push(file.Name, file.Size(), file.Modified)
|
||||
}
|
||||
|
||||
changed++
|
||||
return true
|
||||
})
|
||||
|
||||
// Reorder the file queue according to configuration
|
||||
|
||||
switch p.order {
|
||||
case config.OrderRandom:
|
||||
p.queue.Shuffle()
|
||||
case config.OrderAlphabetic:
|
||||
// The queue is already in alphabetic order.
|
||||
case config.OrderSmallestFirst:
|
||||
p.queue.SortSmallestFirst()
|
||||
case config.OrderLargestFirst:
|
||||
p.queue.SortLargestFirst()
|
||||
case config.OrderOldestFirst:
|
||||
p.queue.SortOldestFirst()
|
||||
case config.OrderNewestFirst:
|
||||
p.queue.SortOldestFirst()
|
||||
}
|
||||
|
||||
// Process the file queue
|
||||
|
||||
nextFile:
|
||||
for {
|
||||
fileName, ok := p.queue.Pop()
|
||||
@@ -474,15 +501,19 @@ nextFile:
|
||||
func (p *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
var err error
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"details": db.ToTruncated(file),
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"type": "dir",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"error": err,
|
||||
"type": "dir",
|
||||
"action": "update",
|
||||
})
|
||||
}()
|
||||
|
||||
@@ -497,13 +528,13 @@ func (p *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
l.Debugf("need dir\n\t%v\n\t%v", file, curFile)
|
||||
}
|
||||
|
||||
info, err := os.Lstat(realName)
|
||||
info, err := osutil.Lstat(realName)
|
||||
switch {
|
||||
// There is already something under that name, but it's a file/link.
|
||||
// Most likely a file/link is getting replaced with a directory.
|
||||
// Remove the file/link and fall through to directory creation.
|
||||
case err == nil && (!info.IsDir() || info.Mode()&os.ModeSymlink != 0):
|
||||
err = osutil.InWritableDir(os.Remove, realName)
|
||||
err = osutil.InWritableDir(osutil.Remove, realName)
|
||||
if err != nil {
|
||||
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
|
||||
return
|
||||
@@ -553,15 +584,18 @@ func (p *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
func (p *rwFolder) deleteDir(file protocol.FileInfo) {
|
||||
var err error
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"details": db.ToTruncated(file),
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"type": "dir",
|
||||
"action": "delete",
|
||||
})
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"error": err,
|
||||
"type": "dir",
|
||||
"action": "delete",
|
||||
})
|
||||
}()
|
||||
|
||||
@@ -572,11 +606,11 @@ func (p *rwFolder) deleteDir(file protocol.FileInfo) {
|
||||
files, _ := dir.Readdirnames(-1)
|
||||
for _, file := range files {
|
||||
if defTempNamer.IsTemporary(file) {
|
||||
osutil.InWritableDir(os.Remove, filepath.Join(realName, file))
|
||||
osutil.InWritableDir(osutil.Remove, filepath.Join(realName, file))
|
||||
}
|
||||
}
|
||||
}
|
||||
err = osutil.InWritableDir(os.Remove, realName)
|
||||
err = osutil.InWritableDir(osutil.Remove, realName)
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
p.dbUpdates <- file
|
||||
} else {
|
||||
@@ -588,28 +622,34 @@ func (p *rwFolder) deleteDir(file protocol.FileInfo) {
|
||||
func (p *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
var err error
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"details": db.ToTruncated(file),
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"type": "file",
|
||||
"action": "delete",
|
||||
})
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"error": err,
|
||||
"type": "file",
|
||||
"action": "delete",
|
||||
})
|
||||
}()
|
||||
|
||||
realName := filepath.Join(p.dir, file.Name)
|
||||
|
||||
cur, ok := p.model.CurrentFolderFile(p.folder, file.Name)
|
||||
if ok && cur.Version.Concurrent(file.Version) {
|
||||
// There is a conflict here. Move the file to a conflict copy instead of deleting.
|
||||
if ok && p.inConflict(cur.Version, file.Version) {
|
||||
// There is a conflict here. Move the file to a conflict copy instead
|
||||
// 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)
|
||||
} else if p.versioner != nil {
|
||||
err = osutil.InWritableDir(p.versioner.Archive, realName)
|
||||
} else {
|
||||
err = osutil.InWritableDir(os.Remove, realName)
|
||||
err = osutil.InWritableDir(osutil.Remove, realName)
|
||||
}
|
||||
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
@@ -624,25 +664,31 @@ func (p *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
var err error
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": source.Name,
|
||||
"details": db.ToTruncated(source),
|
||||
"folder": p.folder,
|
||||
"item": source.Name,
|
||||
"type": "file",
|
||||
"action": "delete",
|
||||
})
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": target.Name,
|
||||
"details": db.ToTruncated(source),
|
||||
"folder": p.folder,
|
||||
"item": target.Name,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": source.Name,
|
||||
"error": err,
|
||||
"type": "file",
|
||||
"action": "delete",
|
||||
})
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": target.Name,
|
||||
"error": err,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
}()
|
||||
|
||||
@@ -679,7 +725,7 @@ func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
// get rid of. Attempt to delete it instead so that we make *some*
|
||||
// progress. The target is unhandled.
|
||||
|
||||
err = osutil.InWritableDir(os.Remove, from)
|
||||
err = osutil.InWritableDir(osutil.Remove, from)
|
||||
if err != nil {
|
||||
l.Infof("Puller (folder %q, file %q): delete %q after failed rename: %v", p.folder, target.Name, source.Name, err)
|
||||
return
|
||||
@@ -693,9 +739,10 @@ func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
// changed file.
|
||||
func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState) {
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"details": db.ToTruncated(file),
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
curFile, ok := p.model.CurrentFolderFile(p.folder, file.Name)
|
||||
@@ -718,6 +765,8 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"error": err,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -757,7 +806,7 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
reused = len(file.Blocks) - len(blocks)
|
||||
if reused == 0 {
|
||||
// Otherwise, discard the file ourselves in order for the
|
||||
// sharedpuller not to panic when it fails to exlusively create a
|
||||
// sharedpuller not to panic when it fails to exclusively create a
|
||||
// file which already exists
|
||||
os.Remove(tempName)
|
||||
}
|
||||
@@ -775,6 +824,7 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
reused: reused,
|
||||
ignorePerms: p.ignorePerms,
|
||||
version: curFile.Version,
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
|
||||
if debug {
|
||||
@@ -816,11 +866,17 @@ func (p *rwFolder) shortcutFile(file protocol.FileInfo) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// This may have been a conflict. We should merge the version vectors so
|
||||
// that our clock doesn't move backwards.
|
||||
if cur, ok := p.model.CurrentFolderFile(p.folder, file.Name); ok {
|
||||
file.Version = file.Version.Merge(cur.Version)
|
||||
}
|
||||
|
||||
p.dbUpdates <- file
|
||||
return
|
||||
}
|
||||
|
||||
// shortcutSymlink changes the symlinks type if necessery.
|
||||
// shortcutSymlink changes the symlinks type if necessary.
|
||||
func (p *rwFolder) shortcutSymlink(file protocol.FileInfo) (err error) {
|
||||
err = symlinks.ChangeType(filepath.Join(p.dir, file.Name), file.Flags)
|
||||
if err == nil {
|
||||
@@ -852,7 +908,7 @@ func (p *rwFolder) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pull
|
||||
folderRoots := make(map[string]string)
|
||||
p.model.fmut.RLock()
|
||||
for folder, cfg := range p.model.folderCfgs {
|
||||
folderRoots[folder] = cfg.Path
|
||||
folderRoots[folder] = cfg.Path()
|
||||
}
|
||||
p.model.fmut.RUnlock()
|
||||
|
||||
@@ -921,7 +977,7 @@ func (p *rwFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPul
|
||||
continue
|
||||
}
|
||||
|
||||
// Get an fd to the temporary file. Tehcnically we don't need it until
|
||||
// Get an fd to the temporary file. Technically we don't need it until
|
||||
// after fetching the block, but if we run into an error here there is
|
||||
// no point in issuing the request to the network.
|
||||
fd, err := state.tempFile()
|
||||
@@ -983,6 +1039,8 @@ func (p *rwFolder) performFinish(state *sharedPullerState) {
|
||||
"folder": p.folder,
|
||||
"item": state.file.Name,
|
||||
"error": err,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
}()
|
||||
|
||||
@@ -1011,10 +1069,12 @@ func (p *rwFolder) performFinish(state *sharedPullerState) {
|
||||
}
|
||||
}
|
||||
|
||||
if state.version.Concurrent(state.file.Version) {
|
||||
if p.inConflict(state.version, state.file.Version) {
|
||||
// The new file has been changed in conflict with the existing one. We
|
||||
// should file it away as a conflict instead of just removing or
|
||||
// archiving.
|
||||
// archiving. Also merge with the version vector we had, to indicate
|
||||
// we have resolved the conflict.
|
||||
state.file.Version = state.file.Version.Merge(state.version)
|
||||
err = osutil.InWritableDir(moveForConflict, state.realName)
|
||||
} else if p.versioner != nil {
|
||||
// If we should use versioning, let the versioner archive the old
|
||||
@@ -1031,9 +1091,9 @@ func (p *rwFolder) performFinish(state *sharedPullerState) {
|
||||
|
||||
// If the target path is a symlink or a directory, we cannot copy
|
||||
// over it, hence remove it before proceeding.
|
||||
stat, err := os.Lstat(state.realName)
|
||||
stat, err := osutil.Lstat(state.realName)
|
||||
if err == nil && (stat.IsDir() || stat.Mode()&os.ModeSymlink != 0) {
|
||||
osutil.InWritableDir(os.Remove, state.realName)
|
||||
osutil.InWritableDir(osutil.Remove, state.realName)
|
||||
}
|
||||
// Replace the original content with the new one
|
||||
err = osutil.Rename(state.tempName, state.realName)
|
||||
@@ -1084,6 +1144,8 @@ func (p *rwFolder) finisherRoutine(in <-chan *sharedPullerState) {
|
||||
"folder": p.folder,
|
||||
"item": state.file.Name,
|
||||
"error": state.failed(),
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
}
|
||||
p.model.receivedFile(p.folder, state.file.Name)
|
||||
@@ -1144,6 +1206,22 @@ loop:
|
||||
}
|
||||
}
|
||||
|
||||
func (p *rwFolder) inConflict(current, replacement protocol.Vector) bool {
|
||||
if current.Concurrent(replacement) {
|
||||
// Obvious case
|
||||
return true
|
||||
}
|
||||
if replacement.Counter(p.shortID) > current.Counter(p.shortID) {
|
||||
// The replacement file contains a higher version for ourselves than
|
||||
// what we have. This isn't supposed to be possible, since it's only
|
||||
// we who can increment that counter. We take it as a sign that
|
||||
// something is wrong (our index may have been corrupted or removed)
|
||||
// and flag it as a conflict.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func invalidateFolder(cfg *config.Configuration, folderID string, err error) {
|
||||
for i := range cfg.Folders {
|
||||
folder := &cfg.Folders[i]
|
||||
@@ -1168,5 +1246,13 @@ func moveForConflict(name string) error {
|
||||
ext := filepath.Ext(name)
|
||||
withoutExt := name[:len(name)-len(ext)]
|
||||
newName := withoutExt + time.Now().Format(".sync-conflict-20060102-150405") + ext
|
||||
return os.Rename(name, newName)
|
||||
err := os.Rename(name, newName)
|
||||
if os.IsNotExist(err) {
|
||||
// We were supposed to move a file away but it does not exist. Either
|
||||
// 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
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
}
|
||||
|
||||
// queue.Done should be called by the finisher routine
|
||||
p.queue.Push("filex")
|
||||
p.queue.Push("filex", 0, 0)
|
||||
p.queue.Pop()
|
||||
|
||||
if len(p.queue.progress) != 1 {
|
||||
@@ -410,7 +410,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
|
||||
p.handleFile(file, copyChan, finisherChan)
|
||||
|
||||
// Receive a block at puller, to indicate that atleast a single copier
|
||||
// Receive a block at puller, to indicate that at least a single copier
|
||||
// loop has been performed.
|
||||
toPull := <-pullChan
|
||||
// Wait until copier is trying to pass something down to the puller again
|
||||
@@ -480,7 +480,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
}
|
||||
|
||||
// queue.Done should be called by the finisher routine
|
||||
p.queue.Push("filex")
|
||||
p.queue.Push("filex", 0, 0)
|
||||
p.queue.Pop()
|
||||
|
||||
if len(p.queue.progress) != 1 {
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/internal/db"
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
// A sharedPullerState is kept for each file that is being synced and is kept
|
||||
@@ -59,8 +59,8 @@ type lockedWriterAt struct {
|
||||
}
|
||||
|
||||
func (w lockedWriterAt) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
(*w.mut).Lock()
|
||||
defer (*w.mut).Unlock()
|
||||
return w.wr.WriteAt(p, off)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,14 @@ package model
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/sync"
|
||||
)
|
||||
|
||||
func TestSourceFileOK(t *testing.T) {
|
||||
s := sharedPullerState{
|
||||
realName: "testdata/foo",
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
|
||||
fd, err := s.sourceFile()
|
||||
@@ -42,6 +45,7 @@ func TestSourceFileOK(t *testing.T) {
|
||||
func TestSourceFileBad(t *testing.T) {
|
||||
s := sharedPullerState{
|
||||
realName: "nonexistent",
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
|
||||
fd, err := s.sourceFile()
|
||||
@@ -67,6 +71,7 @@ func TestReadOnlyDir(t *testing.T) {
|
||||
|
||||
s := sharedPullerState{
|
||||
tempName: "testdata/read_only_dir/.temp_name",
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
|
||||
fd, err := s.tempFile()
|
||||
@@ -78,4 +83,5 @@ func TestReadOnlyDir(t *testing.T) {
|
||||
}
|
||||
|
||||
s.fail("Test done", nil)
|
||||
s.finalClose()
|
||||
}
|
||||
|
||||
17
internal/osutil/glob_unix.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func Glob(pattern string) (matches []string, err error) {
|
||||
return filepath.Glob(pattern)
|
||||
}
|
||||