Compare commits

...

78 Commits

Author SHA1 Message Date
Jakob Borg
b35958d024 Avoid spurious request for /qr?text={{myID}} (fixes #1679) 2015-04-22 09:37:18 +09:00
Audrius Butkevicius
9ee3541655 Merge pull request #1673 from calmh/filestatus-json
Clean up REST JSON a little further
2015-04-21 17:11:06 +01:00
Jakob Borg
bf7d84c12a Clean up REST JSON a little further 2015-04-21 23:28:58 +09:00
Audrius Butkevicius
34c691087e Merge pull request #1674 from calmh/rc-upgrade
Loosen the requirements on what can be upgraded to what
2015-04-21 08:42:33 +01:00
Jakob Borg
08c383012f Loosen the requirements on what can be upgraded to what 2015-04-21 09:06:10 +09:00
Jakob Borg
e2420495f3 Fix type in device sort (fixes #1668) 2015-04-20 22:18:19 +09:00
Audrius Butkevicius
d530c5eda7 Merge pull request #1665 from calmh/wat
Don't initialize subscription in init()
2015-04-20 08:12:58 +01:00
Audrius Butkevicius
ef7420ecf6 Merge pull request #1666 from calmh/cpu-remind
Reminder in debug output to explain high CPU usage
2015-04-20 08:09:56 +01:00
Jakob Borg
c905a41e2a Reminder in debug output to explain high CPU usage 2015-04-20 14:29:38 +09:00
Jakob Borg
42ff4b5bf0 changelog.go should not be built 2015-04-20 14:03:50 +09:00
Jakob Borg
4fb74a32cc Don't initialize subscription in init()
By doing it init(), the monitor process also gets a subscription thing
running, which is unnecessary (and really confused me when seeing it in
the debug output).
2015-04-20 12:58:58 +09:00
Jakob Borg
c741465328 Use versionString() in about modal (fixes #1663) 2015-04-20 08:23:59 +09:00
Jakob Borg
fbca537a40 Merge pull request #1655 from kamadak/fix-nil-deref
Fix nil pointer dereferences in REST with non-existent folders
2015-04-19 17:20:47 +09:00
Jakob Borg
83420b0199 Merge pull request #1654 from AudriusButkevicius/fixes
Fix capitalization (fixes #1652, fixes #1649)
2015-04-19 17:20:05 +09:00
KAMADA Ken'ichi
33d3ba1b45 Fix nil pointer dereferences in REST with non-existent folders 2015-04-18 22:41:47 +09:00
Audrius Butkevicius
497f85a236 Fix capitalization (fixes #1652, fixes #1649) 2015-04-18 11:23:21 +01:00
Audrius Butkevicius
a624c302ab Merge pull request #1648 from calmh/scanner-batches
Don't buffer large files a long time while scanning
2015-04-17 09:05:09 +01:00
Jakob Borg
cebe21a3af Don't buffer large files a long time while scanning 2015-04-17 16:40:09 +09:00
Audrius Butkevicius
9eb679d70a Merge pull request #1647 from calmh/fix-localindexupdated
Homogenize the LocalIndexUpdated event
2015-04-17 08:14:38 +01:00
Jakob Borg
6d84443db8 Homogenize the LocalIndexUpdated event
It had two different formats, and we use "items" instead of "numFiles"
in other places.

(Discovered while documenting :)
2015-04-17 14:22:06 +09:00
Jakob Borg
da8a1f242c Merge pull request #1646 from AudriusButkevicius/readonly
Make targets writeable before removal on Windows (fixes #1610)
2015-04-17 14:21:39 +09:00
Jakob Borg
946d98b71f Merge pull request #1645 from AudriusButkevicius/tests
Fix tests on Windows (fixes #1531)
2015-04-17 14:20:53 +09:00
Audrius Butkevicius
dff51fc707 Make targets writeable before removal on Windows (fixes #1610) 2015-04-16 22:53:53 +01:00
Audrius Butkevicius
7d954dd5d1 Fix tests on Windows (fixes #1531) 2015-04-16 21:18:17 +01:00
Jakob Borg
c6300a5da8 Tone down UPnP errors a little 2015-04-16 23:45:12 +09:00
Jakob Borg
9359daa0d9 Merge branch 'pr-1636'
* pr-1636:
  Store and use _localStorage object
  fix using detect localStorage
2015-04-16 23:44:59 +09:00
Jakob Borg
2322e9cff7 Store and use _localStorage object 2015-04-16 23:44:34 +09:00
Jakob Borg
a876e1e348 Merge remote-tracking branch 'syncthing/pr/1636' into pr-1636
* syncthing/pr/1636:
  fix using detect localStorage
2015-04-16 23:32:48 +09:00
Jakob Borg
6a863c8f71 Translation update 2015-04-16 23:27:27 +09:00
Jakob Borg
392b006b06 Add Moter8 2015-04-16 23:23:34 +09:00
Audrius Butkevicius
96289f42b7 Merge pull request #1644 from syncthing/timeout
UPnP refactor/fixes
2015-04-16 14:32:16 +01:00
Audrius Butkevicius
1b69c2441c Make UPnP discovery requests on each interface explicitly (fixes #1113) 2015-04-16 14:23:36 +01:00
Audrius Butkevicius
8ca85a4918 Merge pull request #1639 from calmh/events
Improve event handling a little bit.
2015-04-16 14:18:52 +01:00
Audrius Butkevicius
2a31031cbc Add unit suffix to UPnP settings 2015-04-16 10:32:22 +01:00
Audrius Butkevicius
d148cd8ccc Make UPnP timeout configurable 2015-04-16 10:32:12 +01:00
Jakob Borg
d1cc1828b8 Improve ItemStarted/ItemFinished events
- Remove full details from ItemStarted (unnecessary, incorrect CamelCase)

 - Add "type" ("file" or "dir") to both events

 - Add "action" (what we tried to do - "delete" or "update") to both
   events.
2015-04-14 23:31:39 +09:00
Jakob Borg
069e8cf122 Don't schedule summaries on all state changes
Prior to this change we schedule summaries on each state change, i.e.
scanning->idle and idle->scanning, which is unnecessary. Now we only do
it on index updates, plus the immediate one on going syncing->idle.
2015-04-14 20:57:42 +09:00
Audrius Butkevicius
45cbcaca6d Merge pull request #1638 from calmh/lstat
Work around broken Lstat on Android
2015-04-14 11:59:20 +01:00
Jakob Borg
102a2db1f3 Work around broken Lstat on Android 2015-04-14 19:53:49 +09:00
Sergey Mishin
9f81c85ca7 fix using detect localStorage 2015-04-13 19:07:39 +03:00
Audrius Butkevicius
ba4a6fc0c5 Merge pull request #1633 from calmh/errorstate
Move folder errors to state
2015-04-13 00:48:13 +01:00
Jakob Borg
aa803ce2ff Move folder errors to state
The "Invalid" config attribute is retained for errors discovered during
config loading (empty path, duplicate ID). This can only be set or
cleared at config loading time.

Errors discovered during runtime (I/O problems, etc) are now in the
folder state instead. Changes to these are sent as any other folder
state change.
2015-04-13 07:43:45 +09:00
Jakob Borg
a027a60f5d Correctly feature detect localStorage (fixes #1632) 2015-04-13 06:50:07 +09:00
Jakob Borg
270649535e Merge pull request #1625 from Moter8/patch-1
Reword and clarify some sentences.
2015-04-10 13:48:14 +02:00
Carsten H
cf80ba71f4 Reword and clarify some sentences 2015-04-10 13:46:38 +02:00
Jakob Borg
b74df18a4a Translation update 2015-04-10 13:32:23 +02:00
Jakob Borg
5cd2906a39 Fix NICKS and authors in index.html 2015-04-10 12:57:43 +02:00
Jakob Borg
bc37b69d17 Add ARM to GUI architectures, and fallback for unknowns 2015-04-10 12:45:53 +02:00
Francois-Xavier Gsell
94f6e400ad fix '~' completion in add folder build assets (fix #1478) 2015-04-10 15:42:52 +08:00
Francois-Xavier Gsell
b95a6ccf80 fix '~' completion in add folder (fix #1478) 2015-04-10 15:42:52 +08:00
Jakob Borg
7df9c1b6e4 Merge pull request #1621 from Zillode/fix-no-upgrade
Fix compilation of -noupgrade builds
2015-04-10 08:30:50 +02:00
Lode Hoste
75348c0158 Fix compilation of -noupgrade builds 2015-04-09 22:44:46 +02:00
Audrius Butkevicius
75fb14acaf Fix integration tests 2015-04-09 16:16:39 +01:00
Audrius Butkevicius
5350315b68 Merge pull request #1614 from calmh/new-short-id
Index reset should generate file conflicts (fixes #1613)
2015-04-09 13:48:37 +01:00
Audrius Butkevicius
658e39c270 Merge pull request #1618 from calmh/id-conflict
Check for short ID conflict at startup
2015-04-09 13:40:32 +01:00
Audrius Butkevicius
ef7ce6c7e1 Merge pull request #1619 from calmh/gui-version
GUI version string includes OS and Arch
2015-04-09 12:29:58 +01:00
Jakob Borg
509e2411bf Merge pull request #1616 from syncthing/rates
Fix total transfer rates (fixes #1615)
2015-04-09 13:08:49 +02:00
Jakob Borg
65c906f951 Merge pull request #1617 from syncthing/integ
Try capturing panics
2015-04-09 13:07:46 +02:00
Audrius Butkevicius
1f159e8233 Fix total transfer rates (fixes #1615) 2015-04-09 12:07:21 +01:00
Jakob Borg
936c76119d Index reset should generate file conflicts (fixes #1613) 2015-04-09 13:06:09 +02:00
Jakob Borg
f45865606a Add initial merge and reset conflict tests 2015-04-09 13:06:09 +02:00
Jakob Borg
cfc9776bae Check for short ID conflict at startup 2015-04-09 13:06:00 +02:00
Audrius Butkevicius
0cb7ed9e4e Try capturing panics 2015-04-09 11:49:02 +01:00
Jakob Borg
4b07609458 GUI version string includes OS and Arch
(Useful when debugging via screenshots...)
2015-04-09 11:33:24 +02:00
Audrius Butkevicius
e41e58e781 Merge pull request #1608 from calmh/xdr-update
Update XDR dependency (fixes #1606)
2015-04-08 14:33:53 +01:00
Jakob Borg
f5030f1c2c Update XDR dependency (fixes #1606) 2015-04-08 14:49:29 +02:00
Jakob Borg
2a48fb8e87 Merge pull request #1607 from syncthing/deadlock
Don't run deadlock detection in release mode unless asked to (fixes #1536)
2015-04-08 14:48:20 +02:00
Audrius Butkevicius
df6dbc5fa4 Only run deadlock detection if asked or non-release/beta (fixes #1536) 2015-04-08 13:40:05 +01:00
Jakob Borg
4b1d2839e8 Correct override PATH in test 2015-04-08 14:23:26 +02:00
Audrius Butkevicius
a892f80e86 Merge pull request #1590 from calmh/long-filenames
Handle long filenames on Windows (fixes #1295)
2015-04-08 13:12:25 +01:00
Jakob Borg
b2a79855ae Handle long filenames on Windows (fixes #1295) 2015-04-08 14:05:39 +02:00
Audrius Butkevicius
ff4974178a Merge pull request #1605 from calmh/http-trace
Add HTTP request tracing
2015-04-07 21:10:51 +01:00
Jakob Borg
d7100fd9bc Add HTTP request tracing 2015-04-07 21:52:47 +02:00
Jakob Borg
0bfb40ae51 discourse -> forum 2015-04-07 16:07:16 +02:00
Jakob Borg
11c83670d6 Merge pull request #1601 from syncthing/conns
Fix GUI
2015-04-07 15:35:54 +02:00
Audrius Butkevicius
68ff4f3842 Fix GUI 2015-04-07 14:24:34 +01:00
Jakob Borg
ab25cd09ed Merge pull request #1600 from syncthing/conns
Change /rest/system/connections output (fixes #1487)
2015-04-07 14:29:40 +02:00
Audrius Butkevicius
8f05b8f982 Change /rest/system/connections output (fixes #1487) 2015-04-07 13:21:03 +01:00
64 changed files with 1417 additions and 596 deletions

View File

@@ -11,6 +11,7 @@ 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>
@@ -21,6 +22,7 @@ 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>

4
Godeps/Godeps.json generated
View File

@@ -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",

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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}})

View File

@@ -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[:])
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -9,3 +9,4 @@ if [ ! -z "$GOLINTOUT" -o "$?" != 0 ]; then
fi
go test

2
NICKS
View File

@@ -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>
@@ -50,3 +51,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>

View File

@@ -5,18 +5,17 @@ syncthing
[![API Documentation](http://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](http://godoc.org/github.com/syncthing/syncthing)
[![MPLv2 License](http://img.shields.io/badge/license-MPLv2-blue.svg?style=flat-square)](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 arrise.
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).

View File

@@ -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 (

View File

@@ -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"
)

View File

@@ -18,6 +18,7 @@ import (
"net/http"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
@@ -56,14 +57,12 @@ var (
lastEventRequestMut sync.Mutex
)
func init() {
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
var err error
l.AddHandler(logger.LevelWarn, showGuiError)
sub := events.Default.Subscribe(events.AllEvents)
eventSub = events.NewBufferedSubscription(sub, 1000)
}
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
var err error
cert, err := tls.LoadX509KeyPair(locations[locHTTPSCertFile], locations[locHTTPSKeyFile])
if err != nil {
@@ -110,7 +109,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
// 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/file", withModel(m, restGetDBFile)) // folder file
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
@@ -180,6 +179,10 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
handler = redirectToHTTPSMiddleware(handler)
}
if debugHTTP {
handler = debugMiddleware(handler)
}
srv := http.Server{
Handler: handler,
ReadTimeout: 10 * time.Second,
@@ -210,6 +213,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
@@ -325,7 +352,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)
@@ -352,7 +384,7 @@ func restGetDBNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
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{}{
output := map[string][]jsonDBFileInfo{
"progress": toNeedSlice(progress),
"queued": toNeedSlice(queued),
"rest": toNeedSlice(rest),
@@ -384,19 +416,13 @@ func restGetDBFile(m *model.Model, 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)
if !withBlocks {
gf.Blocks = nil
lf.Blocks = nil
}
av := m.Availability(folder, file)
json.NewEncoder(w).Encode(map[string]interface{}{
"global": gf,
"local": lf,
"global": jsonFileInfo(gf),
"local": jsonFileInfo(lf),
"availability": av,
})
}
@@ -652,7 +678,7 @@ func restGetSystemUpgrade(w http.ResponseWriter, r *http.Request) {
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
@@ -694,7 +720,7 @@ func restGetLang(w http.ResponseWriter, r *http.Request) {
}
func restPostSystemUpgrade(w http.ResponseWriter, r *http.Request) {
rel, err := upgrade.LatestGithubRelease(Version)
rel, err := upgrade.LatestRelease(Version)
if err != nil {
l.Warnln("getting latest release:", err)
http.Error(w, err.Error(), 500)
@@ -878,17 +904,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 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)
}

View File

@@ -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\"")

View File

@@ -50,6 +50,7 @@ var (
BuildHost = "unknown"
BuildUser = "unknown"
IsRelease bool
IsBeta bool
LongVersion string
)
@@ -77,9 +78,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)
@@ -143,6 +150,7 @@ 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)
- "net" (the main package; connections & network messages)
- "model" (the model package)
- "scanner" (the scanner package)
@@ -322,7 +330,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
}
@@ -432,6 +440,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)
@@ -509,6 +521,15 @@ 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)
@@ -669,7 +690,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}},
},
@@ -711,7 +732,7 @@ func setupUPnP() {
} else {
// Set up incoming port forwarding, if necessary and possible
port, _ := strconv.Atoi(portStr)
igds := upnp.Discover()
igds := upnp.Discover(time.Duration(cfg.Options().UPnPTimeoutS) * time.Second)
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
@@ -723,7 +744,7 @@ func setupUPnP() {
} else {
l.Infof("Created UPnP port mapping for external port %d on UPnP device %s.", externalPort, igd.FriendlyIdentifier())
if opts.UPnPRenewal > 0 {
if opts.UPnPRenewalM > 0 {
go renewUPnP(port)
}
}
@@ -741,7 +762,7 @@ func setupExternalPort(igd *upnp.IGD, port int) int {
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)
err := igd.AddPortMapping(upnp.TCP, r, port, fmt.Sprintf("syncthing-%d", r), cfg.Options().UPnPLeaseM*60)
if err == nil {
return r
}
@@ -752,14 +773,16 @@ func setupExternalPort(igd *upnp.IGD, port int) int {
func renewUPnP(port int) {
for {
opts := cfg.Options()
time.Sleep(time.Duration(opts.UPnPRenewal) * time.Minute)
time.Sleep(time.Duration(opts.UPnPRenewalM) * time.Minute)
// Some values might have changed while we were sleeping
opts = cfg.Options()
// 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()
igds := upnp.Discover(time.Duration(opts.UPnPTimeoutS) * time.Second)
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
@@ -774,7 +797,7 @@ func renewUPnP(port int) {
// Just renew the same port that we already have
if externalPort != 0 {
err := igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", opts.UPnPLease*60)
err := igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", opts.UPnPLeaseM*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 {
@@ -949,7 +972,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
@@ -1003,7 +1026,7 @@ func cleanConfigDirectory() {
}
for _, file := range files {
info, err := os.Lstat(file)
info, err := osutil.Lstat(file)
if err != nil {
l.Infoln("Cleaning:", err)
continue
@@ -1019,3 +1042,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
}

View File

@@ -21,8 +21,8 @@ import (
func TestFolderErrors(t *testing.T) {
fcfg := config.FolderConfiguration{
ID: "folder",
Path: "testdata/testfolder",
ID: "folder",
RawPath: "testdata/testfolder",
}
cfg := config.Wrap("/tmp/test", config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
@@ -42,6 +42,7 @@ func TestFolderErrors(t *testing.T) {
m := model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
m.AddFolder(fcfg)
m.StartFolderRW("folder")
if err := m.CheckFolderHealth("folder"); err != nil {
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
@@ -62,13 +63,14 @@ func TestFolderErrors(t *testing.T) {
// Case 2 - new folder, marker created
fcfg.Path = "testdata/"
fcfg.RawPath = "testdata/"
cfg = config.Wrap("/tmp/test", config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
})
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
m.AddFolder(fcfg)
m.StartFolderRW("folder")
if err := m.CheckFolderHealth("folder"); err != nil {
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
@@ -90,8 +92,9 @@ func TestFolderErrors(t *testing.T) {
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
m.AddFolder(fcfg)
m.StartFolderRW("folder")
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"))
}
@@ -110,15 +113,16 @@ func TestFolderErrors(t *testing.T) {
os.Remove("testdata/testfolder/.stfolder")
os.Remove("testdata/testfolder/")
fcfg.Path = "testdata/testfolder"
fcfg.RawPath = "testdata/testfolder"
cfg = config.Wrap("testdata/subfolder", config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
})
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
m.AddFolder(fcfg)
m.StartFolderRW("folder")
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"))
}
@@ -126,7 +130,7 @@ func TestFolderErrors(t *testing.T) {
os.Mkdir("testdata/testfolder", 0700)
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 +144,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")
}
}

View File

@@ -66,21 +66,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.

View File

@@ -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",
@@ -45,8 +45,8 @@
"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 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ář",
@@ -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é.",

View File

@@ -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",

View File

@@ -23,7 +23,7 @@
"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",
@@ -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",
@@ -132,7 +132,7 @@
"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.",

View File

@@ -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",

View File

@@ -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 пристрою не може бути порожнім.",

View File

@@ -193,9 +193,9 @@
<th><span class="glyphicon glyphicon-folder-open"></span>&emsp;<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>&emsp;<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>&emsp;<span translate>Global State</span></th>
@@ -293,11 +293,11 @@
<tbody>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<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>&emsp;<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>&emsp;<span translate>RAM Utilization</span></th>
@@ -326,7 +326,7 @@
</tr>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Version</span></th>
<td class="text-right">{{version}}</td>
<td class="text-right">{{versionString()}}</td>
</tr>
</tbody>
</table>
@@ -417,7 +417,7 @@
<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>&ensp;<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>&ensp;<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>&ensp;<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>&ensp;<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>&ensp;<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>&ensp;<span translate>Source Code</span></a></li>
@@ -463,7 +463,7 @@
<modal id="idqr" large="yes" status="info" close="yes" icon="qrcode" title="{{'Device Identification' | translate}} &mdash; {{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>
<!-- Device editor modal -->
@@ -945,7 +945,7 @@
<!-- 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 &copy; 2015 the following Contributors:</p>
@@ -963,6 +963,7 @@
<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>
@@ -973,6 +974,7 @@
<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>

View File

@@ -49,22 +49,22 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
// @TODO: extract global level functions into seperate 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) {

View File

@@ -39,6 +39,7 @@ angular.module('syncthing.core')
$scope.deviceStats = {};
$scope.folderStats = {};
$scope.progress = {};
$scope.version = {};
$(window).bind('beforeunload', function () {
navigatingAway = true;
@@ -75,7 +76,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) {
@@ -367,6 +368,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;
@@ -449,10 +461,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 +498,9 @@ angular.module('syncthing.core')
if (state == 'scanning') {
return 'primary';
}
if (state == 'error') {
return 'danger';
}
return 'info';
};
@@ -889,6 +908,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 +981,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;
@@ -1201,6 +1223,31 @@ angular.module('syncthing.core')
}).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();
});

View File

@@ -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 {

View File

@@ -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) {
/**
@@ -28,12 +44,15 @@ angular.module('syncthing.core')
function readBrowserLocales() {
// @TODO: check if there is nice way to utilize window.navigator.languages or similiar 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;
});
}
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -15,6 +15,7 @@ import (
"os"
"path/filepath"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
@@ -70,7 +71,7 @@ func (orig Configuration) Copy() Configuration {
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"`
@@ -94,9 +95,37 @@ func (orig FolderConfiguration) Copy() FolderConfiguration {
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 +138,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 +227,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:"3"`
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 +315,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 +326,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"

View File

@@ -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: 3,
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")
}

View File

@@ -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>

View File

@@ -159,12 +159,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,29 +215,6 @@ 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.
func (w *Wrapper) IgnoredDevice(id protocol.DeviceID) bool {

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -69,7 +69,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:

View File

@@ -1,17 +1,8 @@
// 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
@@ -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()
}

View File

@@ -17,7 +17,6 @@ import (
"net"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@@ -49,8 +48,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 {
@@ -118,18 +118,17 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
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
}
// Starts 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)
}
// StartRW 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.
@@ -144,7 +143,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 +152,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 {
@@ -201,7 +200,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 +208,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 +219,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()
@@ -727,7 +729,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 +769,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 +820,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
@@ -841,7 +851,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 +872,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)
@@ -1033,8 +1043,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 +1083,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,13 +1092,13 @@ 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))
errors := make(map[string]error, len(m.folderCfgs))
var errorsMut sync.Mutex
var wg sync.WaitGroup
@@ -1101,11 +1111,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 +1155,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 +1181,7 @@ nextSub:
subs = unifySubs
w := &scanner.Walker{
Dir: folderCfg.Path,
Dir: folderCfg.Path(),
Subs: subs,
Matcher: ignores,
BlockSize: protocol.BlockSize,
@@ -1181,32 +1195,31 @@ nextSub:
}
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,8 +1254,8 @@ nextSub:
return true
}
if len(batch) == batchSize {
fs.Update(protocol.LocalDeviceID, batch)
if len(batch) == batchSizeFiles {
m.updateLocals(folder, batch)
batch = batch[:0]
}
@@ -1257,15 +1270,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,22 +1287,16 @@ 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
}
@@ -1339,22 +1339,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 +1482,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(),
}
}
@@ -1527,24 +1533,24 @@ func (m *Model) BringToFront(folder, file string) {
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 +1560,21 @@ 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 := m.folderRunners[folder.ID]
m.fmut.RUnlock()
_, _, 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.")
}
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.")
runner.setError(err)
} else if oldErr != nil {
l.Infof("Folder %q error is cleared, restarting", folder.ID)
runner.setState(FolderIdle)
}
return err

View File

@@ -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
}
@@ -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))
}
}
@@ -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))
}
}

View File

@@ -67,8 +67,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
}

View File

@@ -68,13 +68,14 @@ type rwFolder struct {
lenientMtimes bool
copiers int
pullers int
shortID uint64
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},
@@ -82,12 +83,13 @@ func newRWFolder(m *Model, cfg config.FolderConfiguration) *rwFolder {
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,
stop: make(chan struct{}),
queue: newJobQueue(),
@@ -243,8 +245,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
}
@@ -474,15 +476,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 +503,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 +559,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 +581,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 +597,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 +639,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 +700,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 +714,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 +740,8 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
"folder": p.folder,
"item": file.Name,
"error": err,
"type": "file",
"action": "update",
})
return
}
@@ -816,6 +840,12 @@ 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
}
@@ -852,7 +882,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()
@@ -983,6 +1013,8 @@ func (p *rwFolder) performFinish(state *sharedPullerState) {
"folder": p.folder,
"item": state.file.Name,
"error": err,
"type": "file",
"action": "update",
})
}()
@@ -1011,10 +1043,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 +1065,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 +1118,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 +1180,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]

View File

@@ -78,4 +78,5 @@ func TestReadOnlyDir(t *testing.T) {
}
s.fail("Test done", nil)
s.finalClose()
}

View File

@@ -0,0 +1,29 @@
// 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 linux android
package osutil
import (
"os"
"syscall"
"time"
)
// Lstat is like os.Lstat, except lobotomized for Android. See
// https://forum.syncthing.net/t/2395
func Lstat(name string) (fi os.FileInfo, err error) {
for i := 0; i < 10; i++ { // We have to draw the line somewhere
fi, err = os.Lstat(name)
if err, ok := err.(*os.PathError); ok && err.Err == syscall.EINTR {
time.Sleep(time.Duration(i+1) * time.Millisecond)
continue
}
return
}
return
}

View File

@@ -0,0 +1,15 @@
// 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 !linux,!android
package osutil
import "os"
func Lstat(name string) (fi os.FileInfo, err error) {
return os.Lstat(name)
}

View File

@@ -88,6 +88,20 @@ func InWritableDir(fn func(string) error, path string) error {
return fn(path)
}
// On Windows, removes the read-only attribute from the target prior deletion.
func Remove(path string) error {
if runtime.GOOS == "windows" {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.Mode()&0200 == 0 {
os.Chmod(path, 0700)
}
}
return os.Remove(path)
}
func ExpandTilde(path string) (string, error) {
if path == "~" {
return getHomeDir()

View File

@@ -8,6 +8,7 @@ package osutil_test
import (
"os"
"runtime"
"testing"
"github.com/syncthing/syncthing/internal/osutil"
@@ -68,3 +69,97 @@ func TestInWriteableDir(t *testing.T) {
t.Error("testdata/file/foo returned nil error")
}
}
func TestInWritableDirWindowsRemove(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skipf("Tests not required")
return
}
err := os.RemoveAll("testdata")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll("testdata")
create := func(name string) error {
fd, err := os.Create(name)
if err != nil {
return err
}
fd.Close()
return nil
}
os.Mkdir("testdata", 0700)
os.Mkdir("testdata/windows", 0500)
os.Mkdir("testdata/windows/ro", 0500)
create("testdata/windows/ro/readonly")
os.Chmod("testdata/windows/ro/readonly", 0500)
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
err := os.Remove(path)
if err == nil {
t.Errorf("Expected error %s", path)
}
}
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
err := osutil.InWritableDir(osutil.Remove, path)
if err != nil {
t.Errorf("Unexpected error %s: %s", path, err)
}
}
}
func TestInWritableDirWindowsRename(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skipf("Tests not required")
return
}
err := os.RemoveAll("testdata")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll("testdata")
create := func(name string) error {
fd, err := os.Create(name)
if err != nil {
return err
}
fd.Close()
return nil
}
os.Mkdir("testdata", 0700)
os.Mkdir("testdata/windows", 0500)
os.Mkdir("testdata/windows/ro", 0500)
create("testdata/windows/ro/readonly")
os.Chmod("testdata/windows/ro/readonly", 0500)
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
err := os.Rename(path, path+"new")
if err == nil {
t.Errorf("Expected error %s", path)
}
}
rename := func(path string) error {
return osutil.Rename(path, path+"new")
}
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
err := osutil.InWritableDir(rename, path)
if err != nil {
t.Errorf("Unexpected error %s: %s", path, err)
}
_, err = os.Stat(path + "new")
if err != nil {
t.Errorf("Unexpected error %s: %s", path, err)
}
}
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/syncthing/protocol"
"github.com/syncthing/syncthing/internal/ignore"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/symlinks"
"golang.org/x/text/unicode/norm"
)
@@ -193,7 +194,7 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
// We will attempt to normalize it.
normalizedPath := filepath.Join(w.Dir, normalizedRn)
if _, err := os.Lstat(normalizedPath); os.IsNotExist(err) {
if _, err := osutil.Lstat(normalizedPath); os.IsNotExist(err) {
// Nothing exists with the normalized filename. Good.
if err = os.Rename(p, normalizedPath); err != nil {
l.Infof(`Error normalizing UTF8 encoding of file "%s": %v`, rn, err)
@@ -356,7 +357,7 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
}
func checkDir(dir string) error {
if info, err := os.Lstat(dir); err != nil {
if info, err := osutil.Lstat(dir); err != nil {
return err
} else if !info.IsDir() {
return errors.New(dir + ": not a directory")

View File

@@ -203,6 +203,14 @@ func TestNormalization(t *testing.T) {
"5-\xCD\xE2", // EUC-CN "wài" (外) -- ignored (not UTF8)
}
numInvalid := 2
if runtime.GOOS == "windows" {
// On Windows, in case 5 the character gets replaced with a
// replacement character \xEF\xBF\xBD at the point it's written to disk,
// which means it suddenly becomes valid (sort of).
numInvalid--
}
numValid := len(tests) - numInvalid
for _, s1 := range tests {

View File

@@ -60,7 +60,7 @@ func init() {
return
}
stat, err := os.Lstat(path)
stat, err := osutil.Lstat(path)
if err != nil || stat.Mode()&os.ModeSymlink == 0 {
return
}

View File

@@ -27,21 +27,21 @@ import (
"strings"
)
// Returns the latest release, including prereleases or not depending on the argument
func LatestGithubRelease(version string) (Release, error) {
resp, err := http.Get("https://api.github.com/repos/syncthing/syncthing/releases?per_page=10")
// Returns the latest releases, including prereleases or not depending on the argument
func LatestGithubReleases(version string) ([]Release, error) {
resp, err := http.Get("https://api.github.com/repos/syncthing/syncthing/releases?per_page=30")
if err != nil {
return Release{}, err
return nil, err
}
if resp.StatusCode > 299 {
return Release{}, fmt.Errorf("API call returned HTTP error: %s", resp.Status)
return nil, fmt.Errorf("API call returned HTTP error: %s", resp.Status)
}
var rels []Release
json.NewDecoder(resp.Body).Decode(&rels)
resp.Body.Close()
return LatestRelease(version, rels)
return rels, nil
}
type SortByRelease []Release
@@ -56,7 +56,12 @@ func (s SortByRelease) Less(i, j int) bool {
return CompareVersions(s[i].Tag, s[j].Tag) > 0
}
func LatestRelease(version string, rels []Release) (Release, error) {
func LatestRelease(version string) (Release, error) {
rels, _ := LatestGithubReleases(version)
return SelectLatestRelease(version, rels)
}
func SelectLatestRelease(version string, rels []Release) (Release, error) {
if len(rels) == 0 {
return Release{}, ErrVersionUnknown
}

View File

@@ -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 !noupgrade
package upgrade
import (
@@ -65,7 +67,7 @@ var upgrades = map[string]string{
"v0.11.0-beta0+40-g53cb66e-dirty": "v0.11.0-beta0",
}
func TestRelease(t *testing.T) {
func TestGithubRelease(t *testing.T) {
fd, err := os.Open("testdata/github-releases.json")
if err != nil {
t.Errorf("Missing github-release test data")
@@ -76,7 +78,7 @@ func TestRelease(t *testing.T) {
json.NewDecoder(fd).Decode(&rels)
for old, target := range upgrades {
upgrade, err := LatestRelease(old, rels)
upgrade, err := SelectLatestRelease(old, rels)
if err != nil {
t.Error("Error retrieving latest version", err)
}
@@ -85,3 +87,10 @@ func TestRelease(t *testing.T) {
}
}
}
func TestErrorRelease(t *testing.T) {
_, err := SelectLatestRelease("v0.11.0-beta", nil)
if err == nil {
t.Error("Should return an error when no release were available")
}
}

View File

@@ -16,6 +16,6 @@ func upgradeToURL(binary, url string) error {
return ErrUpgradeUnsupported
}
func LatestRelease(prerelease bool) (Release, error) {
func LatestRelease(version string) (Release, error) {
return Release{}, ErrUpgradeUnsupported
}

View File

@@ -91,44 +91,71 @@ type upnpRoot struct {
}
// Discover discovers UPnP InternetGatewayDevices.
// The order in which the devices appear in the result list is not deterministic.
func Discover() []IGD {
var result []IGD
// The order in which the devices appear in the results list is not deterministic.
func Discover(timeout time.Duration) []IGD {
var results []IGD
l.Infoln("Starting UPnP discovery...")
timeout := 3
interfaces, err := net.Interfaces()
if err != nil {
l.Infoln("Listing network interfaces:", err)
return results
}
// Search for InternetGatewayDevice:2 devices
result = append(result, discover("urn:schemas-upnp-org:device:InternetGatewayDevice:2", timeout, result)...)
resultChan := make(chan IGD, 16)
// Search for InternetGatewayDevice:1 devices
// InternetGatewayDevice:2 devices that correctly respond to the IGD:1 request as well will not be re-added to the result list
result = append(result, discover("urn:schemas-upnp-org:device:InternetGatewayDevice:1", timeout, result)...)
if len(result) > 0 && debug {
l.Debugln("UPnP discovery result:")
for _, resultDevice := range result {
l.Debugln("[" + resultDevice.uuid + "]")
for _, resultService := range resultDevice.services {
l.Debugln("* [" + resultService.serviceID + "] " + resultService.serviceURL)
// Aggregator
go func() {
next:
for result := range resultChan {
for _, existingResult := range results {
if existingResult.uuid == result.uuid {
if debug {
l.Debugf("Skipping duplicate result %s with services:", result.uuid)
for _, svc := range result.services {
l.Debugf("* [%s] %s", svc.serviceID, svc.serviceURL)
}
}
goto next
}
}
results = append(results, result)
if debug {
l.Debugf("UPnP discovery result %s with services:", result.uuid)
for _, svc := range result.services {
l.Debugf("* [%s] %s", svc.serviceID, svc.serviceURL)
}
}
}
}()
var wg sync.WaitGroup
for _, intf := range interfaces {
for _, deviceType := range []string{"urn:schemas-upnp-org:device:InternetGatewayDevice:1", "urn:schemas-upnp-org:device:InternetGatewayDevice:2"} {
wg.Add(1)
go func(intf net.Interface, deviceType string) {
discover(&intf, deviceType, timeout, resultChan)
wg.Done()
}(intf, deviceType)
}
}
wg.Wait()
close(resultChan)
suffix := "devices"
if len(result) == 1 {
if len(results) == 1 {
suffix = "device"
}
l.Infof("UPnP discovery complete (found %d %s).", len(result), suffix)
l.Infof("UPnP discovery complete (found %d %s).", len(results), suffix)
return result
return results
}
// Search for UPnP InternetGatewayDevices for <timeout> seconds, ignoring responses from any devices listed in knownDevices.
// The order in which the devices appear in the result list is not deterministic
func discover(deviceType string, timeout int, knownDevices []IGD) []IGD {
func discover(intf *net.Interface, deviceType string, timeout time.Duration, results chan<- IGD) {
ssdp := &net.UDPAddr{IP: []byte{239, 255, 255, 250}, Port: 1900}
tpl := `M-SEARCH * HTTP/1.1
@@ -138,44 +165,41 @@ Man: "ssdp:discover"
Mx: %d
`
searchStr := fmt.Sprintf(tpl, deviceType, timeout)
searchStr := fmt.Sprintf(tpl, deviceType, timeout/time.Second)
search := []byte(strings.Replace(searchStr, "\n", "\r\n", -1))
if debug {
l.Debugln("Starting discovery of device type " + deviceType + "...")
l.Debugln("Starting discovery of device type " + deviceType + " on " + intf.Name)
}
var results []IGD
resultChannel := make(chan IGD, 8)
socket, err := net.ListenMulticastUDP("udp4", nil, &net.UDPAddr{IP: ssdp.IP})
socket, err := net.ListenMulticastUDP("udp4", intf, &net.UDPAddr{IP: ssdp.IP})
if err != nil {
l.Infoln(err)
return results
if debug {
l.Debugln(err)
}
return
}
defer socket.Close() // Make sure our socket gets closed
err = socket.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
err = socket.SetDeadline(time.Now().Add(timeout))
if err != nil {
l.Infoln(err)
return results
return
}
if debug {
l.Debugln("Sending search request for device type " + deviceType + "...")
l.Debugln("Sending search request for device type " + deviceType + " on " + intf.Name)
}
var resultWaitGroup sync.WaitGroup
_, err = socket.WriteTo(search, ssdp)
if err != nil {
l.Infoln(err)
return results
return
}
if debug {
l.Debugln("Listening for UPnP response for device type " + deviceType + "...")
l.Debugln("Listening for UPnP response for device type " + deviceType + " on " + intf.Name)
}
// Listen for responses until a timeout is reached
@@ -184,69 +208,42 @@ Mx: %d
n, _, err := socket.ReadFrom(resp)
if err != nil {
if e, ok := err.(net.Error); !ok || !e.Timeout() {
l.Infoln(err) //legitimate error, not a timeout.
l.Infoln("UPnP read:", err) //legitimate error, not a timeout.
}
break
} else {
// Process results in a separate go routine so we can immediately return to listening for more responses
resultWaitGroup.Add(1)
go handleSearchResponse(deviceType, knownDevices, resp, n, resultChannel, &resultWaitGroup)
}
}
// Wait for all result handlers to finish processing, then close result channel
resultWaitGroup.Wait()
close(resultChannel)
// Collect our results from the result handlers using the result channel
for result := range resultChannel {
// Check for existing results (some routers send multiple response packets)
for _, existingResult := range results {
if existingResult.uuid == result.uuid {
if debug {
l.Debugln("Already processed device with UUID", existingResult.uuid, "continuing...")
}
continue
}
igd, err := parseResponse(deviceType, resp[:n])
if err != nil {
l.Infoln("UPnP parse:", err)
continue
}
// No existing results, okay to append
results = append(results, result)
results <- igd
}
if debug {
l.Debugln("Discovery for device type " + deviceType + " finished.")
l.Debugln("Discovery for device type " + deviceType + " on " + intf.Name + " finished.")
}
return results
}
func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, length int, resultChannel chan<- IGD, resultWaitGroup *sync.WaitGroup) {
defer resultWaitGroup.Done() // Signal when we've finished processing
func parseResponse(deviceType string, resp []byte) (IGD, error) {
if debug {
l.Debugln("Handling UPnP response:\n\n" + string(resp[:length]))
l.Debugln("Handling UPnP response:\n\n" + string(resp))
}
reader := bufio.NewReader(bytes.NewBuffer(resp[:length]))
reader := bufio.NewReader(bytes.NewBuffer(resp))
request := &http.Request{}
response, err := http.ReadResponse(reader, request)
if err != nil {
l.Infoln(err)
return
return IGD{}, err
}
respondingDeviceType := response.Header.Get("St")
if respondingDeviceType != deviceType {
l.Infoln("Unrecognized UPnP device of type " + respondingDeviceType)
return
return IGD{}, errors.New("unrecognized UPnP device of type " + respondingDeviceType)
}
deviceDescriptionLocation := response.Header.Get("Location")
if deviceDescriptionLocation == "" {
l.Infoln("Invalid IGD response: no location specified.")
return
return IGD{}, errors.New("invalid IGD response: no location specified.")
}
deviceDescriptionURL, err := url.Parse(deviceDescriptionLocation)
@@ -257,8 +254,7 @@ func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, le
deviceUSN := response.Header.Get("USN")
if deviceUSN == "" {
l.Infoln("Invalid IGD response: USN not specified.")
return
return IGD{}, errors.New("invalid IGD response: USN not specified.")
}
deviceUUID := strings.TrimLeft(strings.Split(deviceUSN, "::")[0], "uuid:")
@@ -267,39 +263,25 @@ func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, le
l.Infoln("Invalid IGD response: invalid device UUID", deviceUUID, "(continuing anyway)")
}
// Don't re-add devices that are already known
for _, knownDevice := range knownDevices {
if deviceUUID == knownDevice.uuid {
if debug {
l.Debugln("Ignoring known device with UUID " + deviceUUID)
}
return
}
}
response, err = http.Get(deviceDescriptionLocation)
if err != nil {
l.Infoln(err)
return
return IGD{}, err
}
defer response.Body.Close()
if response.StatusCode >= 400 {
l.Infoln(errors.New(response.Status))
return
return IGD{}, errors.New("bad status code:" + response.Status)
}
var upnpRoot upnpRoot
err = xml.NewDecoder(response.Body).Decode(&upnpRoot)
if err != nil {
l.Infoln(err)
return
return IGD{}, err
}
services, err := getServiceDescriptions(deviceDescriptionLocation, upnpRoot.Device)
if err != nil {
l.Infoln(err)
return
return IGD{}, err
}
// Figure out our IP number, on the network used to reach the IGD.
@@ -308,23 +290,16 @@ func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, le
// suggestions on a better way to do this...
localIPAddress, err := localIP(deviceDescriptionURL)
if err != nil {
l.Infoln(err)
return
return IGD{}, err
}
igd := IGD{
return IGD{
uuid: deviceUUID,
friendlyName: upnpRoot.Device.FriendlyName,
url: deviceDescriptionURL,
services: services,
localIPAddress: localIPAddress,
}
resultChannel <- igd
if debug {
l.Debugln("Finished handling of UPnP response.")
}
}, nil
}
func localIP(url *url.URL) (string, error) {

View File

@@ -12,6 +12,8 @@ import (
"os/exec"
"path/filepath"
"strings"
"github.com/syncthing/syncthing/internal/osutil"
)
func init() {
@@ -43,7 +45,7 @@ func NewExternal(folderID, folderPath string, params map[string]string) Versione
// Move away the named file to a version archive. If this function returns
// nil, the named file does not exist any more (has been archived).
func (v External) Archive(filePath string) error {
_, err := os.Lstat(filePath)
_, err := osutil.Lstat(filePath)
if os.IsNotExist(err) {
if debug {
l.Debugln("not archiving nonexistent file", filePath)
@@ -82,7 +84,7 @@ func (v External) Archive(filePath string) error {
}
// return error if the file was not removed
if _, err = os.Lstat(filePath); os.IsNotExist(err) {
if _, err = osutil.Lstat(filePath); os.IsNotExist(err) {
return nil
}
return errors.New("Versioner: file was not removed by external script")

View File

@@ -46,7 +46,7 @@ func NewSimple(folderID, folderPath string, params map[string]string) Versioner
// Move away the named file to a version archive. If this function returns
// nil, the named file does not exist any more (has been archived).
func (v Simple) Archive(filePath string) error {
fileInfo, err := os.Lstat(filePath)
fileInfo, err := osutil.Lstat(filePath)
if os.IsNotExist(err) {
if debug {
l.Debugln("not archiving nonexistent file", filePath)

View File

@@ -210,7 +210,7 @@ func (v Staggered) expire(versions []string) {
var prevAge int64
firstFile := true
for _, file := range versions {
fi, err := os.Lstat(file)
fi, err := osutil.Lstat(file)
if err != nil {
l.Warnln("versioner:", err)
continue
@@ -281,7 +281,7 @@ func (v Staggered) Archive(filePath string) error {
v.mutex.Lock()
defer v.mutex.Unlock()
_, err := os.Lstat(filePath)
_, err := osutil.Lstat(filePath)
if os.IsNotExist(err) {
if debug {
l.Debugln("not archiving nonexistent file", filePath)

View File

@@ -9,6 +9,7 @@
package integration
import (
"io/ioutil"
"log"
"os"
"path/filepath"
@@ -47,43 +48,8 @@ func TestConflict(t *testing.T) {
t.Fatal(err)
}
log.Println("Starting sender...")
sender := syncthingProcess{ // id1
instance: "1",
argv: []string{"-home", "h1"},
port: 8081,
apiKey: apiKey,
}
err = sender.start()
if err != nil {
t.Fatal(err)
}
sender, receiver := coSenderReceiver(t)
defer sender.stop()
// Wait for one scan to succeed, or up to 20 seconds... This is to let
// startup, UPnP etc complete and make sure the sender has the full index
// before they connect.
for i := 0; i < 20; i++ {
err := sender.rescan("default")
if err != nil {
time.Sleep(time.Second)
continue
}
break
}
log.Println("Starting receiver...")
receiver := syncthingProcess{ // id2
instance: "2",
argv: []string{"-home", "h2"},
port: 8082,
apiKey: apiKey,
}
err = receiver.start()
if err != nil {
sender.stop()
t.Fatal(err)
}
defer receiver.stop()
if err = coCompletion(sender, receiver); err != nil {
@@ -213,6 +179,294 @@ func TestConflict(t *testing.T) {
}
}
func TestInitialMergeConflicts(t *testing.T) {
log.Println("Cleaning...")
err := removeAll("s1", "s2", "h1/index*", "h2/index*")
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("s1", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("s2", 0755)
if err != nil {
t.Fatal(err)
}
// File 1 is a conflict
err = ioutil.WriteFile("s1/file1", []byte("hello\n"), 0644)
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile("s2/file1", []byte("goodbye\n"), 0644)
if err != nil {
t.Fatal(err)
}
// File 2 exists on s1 only
err = ioutil.WriteFile("s1/file2", []byte("hello\n"), 0644)
if err != nil {
t.Fatal(err)
}
// File 3 exists on s2 only
err = ioutil.WriteFile("s2/file3", []byte("goodbye\n"), 0644)
if err != nil {
t.Fatal(err)
}
// Let them sync
sender, receiver := coSenderReceiver(t)
defer sender.stop()
defer receiver.stop()
log.Println("Syncing...")
if err = coCompletion(sender, receiver); err != nil {
t.Fatal(err)
}
sender.stop()
receiver.stop()
log.Println("Verifying...")
// s1 should have three-four files (there's a conflict from s2 which may or may not have synced yet)
files, err := filepath.Glob("s1/file*")
if err != nil {
t.Fatal(err)
}
if len(files) < 3 || len(files) > 4 {
t.Errorf("Expected 3-4 files in s1 instead of %d", len(files))
}
// s2 should have four files (there's a conflict)
files, err = filepath.Glob("s2/file*")
if err != nil {
t.Fatal(err)
}
if len(files) != 4 {
t.Errorf("Expected 4 files in s2 instead of %d", len(files))
}
// file1 is in conflict, so there's two versions of that one
files, err = filepath.Glob("s2/file1*")
if err != nil {
t.Fatal(err)
}
if len(files) != 2 {
t.Errorf("Expected 2 'file1' files in s2 instead of %d", len(files))
}
}
func TestResetConflicts(t *testing.T) {
log.Println("Cleaning...")
err := removeAll("s1", "s2", "h1/index*", "h2/index*")
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("s1", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("s2", 0755)
if err != nil {
t.Fatal(err)
}
// Three files on s1
err = ioutil.WriteFile("s1/file1", []byte("hello\n"), 0644)
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile("s1/file2", []byte("hello\n"), 0644)
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile("s2/file3", []byte("hello\n"), 0644)
if err != nil {
t.Fatal(err)
}
// Let them sync
sender, receiver := coSenderReceiver(t)
defer sender.stop()
defer receiver.stop()
log.Println("Syncing...")
if err = coCompletion(sender, receiver); err != nil {
t.Fatal(err)
}
log.Println("Verifying...")
// s1 should have three files
files, err := filepath.Glob("s1/file*")
if err != nil {
t.Fatal(err)
}
if len(files) != 3 {
t.Errorf("Expected 3 files in s1 instead of %d", len(files))
}
// s2 should have three
files, err = filepath.Glob("s2/file*")
if err != nil {
t.Fatal(err)
}
if len(files) != 3 {
t.Errorf("Expected 3 files in s2 instead of %d", len(files))
}
log.Println("Updating...")
// change s2/file2 a few times, so that it's version counter increases.
// This will make the file on the cluster look newer than what we have
// locally after we rest the index, unless we have a fix for that.
err = ioutil.WriteFile("s2/file2", []byte("hello1\n"), 0644)
if err != nil {
t.Fatal(err)
}
err = receiver.rescan("default")
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
err = ioutil.WriteFile("s2/file2", []byte("hello2\n"), 0644)
if err != nil {
t.Fatal(err)
}
err = receiver.rescan("default")
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
err = ioutil.WriteFile("s2/file2", []byte("hello3\n"), 0644)
if err != nil {
t.Fatal(err)
}
err = receiver.rescan("default")
if err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
if err = coCompletion(sender, receiver); err != nil {
t.Fatal(err)
}
// Now nuke the index
log.Println("Resetting...")
receiver.stop()
removeAll("h2/index*")
// s1/file1 (remote) changes while receiver is down
err = ioutil.WriteFile("s1/file1", []byte("goodbye\n"), 0644)
if err != nil {
t.Fatal(err)
}
// s1 must know about it
err = sender.rescan("default")
if err != nil {
t.Fatal(err)
}
// s2/file2 (local) changes while receiver is down
err = ioutil.WriteFile("s2/file2", []byte("goodbye\n"), 0644)
if err != nil {
t.Fatal(err)
}
receiver.start()
log.Println("Syncing...")
if err = coCompletion(sender, receiver); err != nil {
t.Fatal(err)
}
// s2 should have five files (three plus two conflicts)
files, err = filepath.Glob("s2/file*")
if err != nil {
t.Fatal(err)
}
if len(files) != 5 {
t.Errorf("Expected 5 files in s2 instead of %d", len(files))
}
// file1 is in conflict, so there's two versions of that one
files, err = filepath.Glob("s2/file1*")
if err != nil {
t.Fatal(err)
}
if len(files) != 2 {
t.Errorf("Expected 2 'file1' files in s2 instead of %d", len(files))
}
// file2 is in conflict, so there's two versions of that one
files, err = filepath.Glob("s2/file2*")
if err != nil {
t.Fatal(err)
}
if len(files) != 2 {
t.Errorf("Expected 2 'file2' files in s2 instead of %d", len(files))
}
}
func coSenderReceiver(t *testing.T) (syncthingProcess, syncthingProcess) {
log.Println("Starting sender...")
sender := syncthingProcess{ // id1
instance: "1",
argv: []string{"-home", "h1"},
port: 8081,
apiKey: apiKey,
}
err := sender.start()
if err != nil {
t.Fatal(err)
}
log.Println("Starting receiver...")
receiver := syncthingProcess{ // id2
instance: "2",
argv: []string{"-home", "h2"},
port: 8082,
apiKey: apiKey,
}
err = receiver.start()
if err != nil {
sender.stop()
t.Fatal(err)
}
return sender, receiver
}
func coCompletion(p ...syncthingProcess) error {
mainLoop:
for {

View File

@@ -234,7 +234,7 @@ func TestPOSTWithoutCSRF(t *testing.T) {
// Should succeed with CSRF
req, err = http.NewRequest("POST", "http://127.0.0.1:8082/rest/error/clear", nil)
req, err = http.NewRequest("POST", "http://127.0.0.1:8082/rest/system/error/clear", nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -148,7 +148,7 @@ func TestOverride(t *testing.T) {
log.Println("Hitting Override on master...")
resp, err := master.post("/rest/model/override?folder=default", nil)
resp, err := master.post("/rest/db/override?folder=default", nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -118,11 +118,18 @@ func (p *syncthingProcess) stop() error {
raceConditionStart := []byte("WARNING: DATA RACE")
raceConditionSep := []byte("==================")
panicConditionStart := []byte("panic:")
panicConditionSep := []byte(p.id.String()[:5])
sc := bufio.NewScanner(fd)
race := false
_panic := false
for sc.Scan() {
line := sc.Bytes()
if race {
if race || _panic {
if bytes.Contains(line, panicConditionSep) {
_panic = false
continue
}
fmt.Printf("%s\n", line)
if bytes.Contains(line, raceConditionSep) {
race = false
@@ -134,6 +141,11 @@ func (p *syncthingProcess) stop() error {
if err == nil {
err = errors.New("Race condition detected")
}
} else if bytes.Contains(line, panicConditionStart) {
_panic = true
if err == nil {
err = errors.New("Panic detected")
}
}
}
return err