Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c7771ccc5 | ||
|
|
17cd49fbdc | ||
|
|
ad273adb78 | ||
|
|
150e7daf2d | ||
|
|
b004155e8f | ||
|
|
92eed3b33b | ||
|
|
fe7b77198c | ||
|
|
f51b775698 | ||
|
|
939dd5cb31 | ||
|
|
adcbe13ecd | ||
|
|
97dda6a4bb | ||
|
|
9e395eb883 | ||
|
|
60da59623e | ||
|
|
9752ea9ac3 | ||
|
|
279693078a | ||
|
|
19b93045a4 | ||
|
|
5231a09820 | ||
|
|
ab952e6103 | ||
|
|
a418771c04 | ||
|
|
b41590ce38 | ||
|
|
c7dde9499f | ||
|
|
528cbf62ec | ||
|
|
1be4b8bb5d | ||
|
|
c832fc9917 | ||
|
|
4797a94689 | ||
|
|
6948903084 | ||
|
|
94164611ae | ||
|
|
ae298e8902 | ||
|
|
3d8771ecb0 | ||
|
|
28db264e90 | ||
|
|
6af9fa4b81 | ||
|
|
60b4d05860 | ||
|
|
7b93839ed1 | ||
|
|
fdb11d7c06 | ||
|
|
5651847877 | ||
|
|
e1442290b6 | ||
|
|
c45b18cc75 | ||
|
|
bb2ad77987 | ||
|
|
68b1ffec19 | ||
|
|
bc2bb22673 | ||
|
|
83d707fc4b | ||
|
|
175b32e56c | ||
|
|
97b4a6553b | ||
|
|
4ade30e681 | ||
|
|
4e03b4f191 | ||
|
|
bfe1d1d4ca | ||
|
|
8918de85fd | ||
|
|
5e237aecae | ||
|
|
13291ad481 | ||
|
|
a47ee86bee | ||
|
|
62d703f967 | ||
|
|
b2c196e5c7 | ||
|
|
4be6a54bc0 | ||
|
|
8ce8476547 | ||
|
|
d82caf6bd4 | ||
|
|
8ea1e302c3 | ||
|
|
a8799efa94 | ||
|
|
0cfac4e021 | ||
|
|
f6c9642d72 | ||
|
|
5a07f9ddee | ||
|
|
9db75e91ac | ||
|
|
f288e00c37 | ||
|
|
c9edd31993 | ||
|
|
5a7780ab5f | ||
|
|
ac0fba99ad | ||
|
|
6f724a113c | ||
|
|
327cd4cb87 | ||
|
|
25de3a2590 | ||
|
|
06208a703a |
@@ -1,7 +1,40 @@
|
||||
## Reporting Bugs
|
||||
|
||||
Please file bugs in the [Github Issue
|
||||
Tracker](https://github.com/syncthing/syncthing/issues). Include at
|
||||
least the following:
|
||||
|
||||
- What happened
|
||||
|
||||
- What did you expect to happen instead of what *did* happen, if it's
|
||||
not crazy obvious
|
||||
|
||||
- What operating system, operating system version and version of
|
||||
Syncthing you are running
|
||||
|
||||
- The same for other connected nodes, where relevant
|
||||
|
||||
- Screenshot if the issue concerns something visible in the GUI
|
||||
|
||||
- Console log entries, where possible and relevant
|
||||
|
||||
If you're not sure whether something is relevant, erring on the side of
|
||||
too much information will never get you yelled at. :)
|
||||
|
||||
## Contributing Translations
|
||||
|
||||
All translations are done via
|
||||
[Transifex](https://www.transifex.com/projects/p/syncthing/). If you
|
||||
wish to contribute to a translation, just head over there and sign up.
|
||||
Before every release, the language resources are updated from the
|
||||
latest info on Transifex.
|
||||
|
||||
## Contributing Code
|
||||
|
||||
Please do contribute! If you want to contribute but are unsure where to
|
||||
start, the [Contributions Needed
|
||||
topic](http://discourse.syncthing.net/t/contributions-needed/49)
|
||||
lists areas in need of attention.
|
||||
topic](http://discourse.syncthing.net/t/49) lists areas in need of
|
||||
attention. In general, any open issues are fair game!
|
||||
|
||||
## Licensing
|
||||
|
||||
@@ -16,8 +49,8 @@ to add yourself as a separate commit in your first pull request.
|
||||
|
||||
## Building
|
||||
|
||||
[See the
|
||||
documentation](http://discourse.syncthing.net/t/building-syncthing/44)
|
||||
[See the documentation](http://discourse.syncthing.net/t/44) on how to
|
||||
get started with a build environment.
|
||||
|
||||
## Branches
|
||||
|
||||
@@ -44,7 +77,9 @@ Yes please!
|
||||
|
||||
## Style
|
||||
|
||||
`go fmt`
|
||||
- `go fmt`
|
||||
|
||||
- Unix line breaks
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -53,4 +88,3 @@ Yes please!
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ Audrius Butkevicius <audrius.butkevicius@gmail.com>
|
||||
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com>
|
||||
Ben Sidhom <bsidhom@gmail.com>
|
||||
Brandon Philips <brandon@ifup.org>
|
||||
Gilli Sigurdsson <gilli@vx.is>
|
||||
James Patterson <jamespatterson@operamail.com>
|
||||
Jens Diemer <github.com@jensdiemer.de>
|
||||
Philippe Schommers <philippe@schommers.be>
|
||||
Ryan Sullivan <kayoticsully@gmail.com>
|
||||
Tully Robinson <tully@tojr.org>
|
||||
Veeti Paananen <veeti.paananen@rojekti.fi>
|
||||
|
||||
20
Godeps/Godeps.json
generated
@@ -12,23 +12,23 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "code.google.com/p/go.crypto/bcrypt",
|
||||
"Comment": "null-213",
|
||||
"Rev": "aa2644fe4aa50e3b38d75187b4799b1f0c9ddcef"
|
||||
"Comment": "null-216",
|
||||
"Rev": "41cd4647fccc72b0b79ef1bd1fe6735e718257cd"
|
||||
},
|
||||
{
|
||||
"ImportPath": "code.google.com/p/go.crypto/blowfish",
|
||||
"Comment": "null-213",
|
||||
"Rev": "aa2644fe4aa50e3b38d75187b4799b1f0c9ddcef"
|
||||
"Comment": "null-216",
|
||||
"Rev": "41cd4647fccc72b0b79ef1bd1fe6735e718257cd"
|
||||
},
|
||||
{
|
||||
"ImportPath": "code.google.com/p/go.text/transform",
|
||||
"Comment": "null-89",
|
||||
"Rev": "df15baaf13e3f62b6b7a901e74caa3818a7c0a7c"
|
||||
"Comment": "null-90",
|
||||
"Rev": "d65bffbc88a153d23a6d2a864531e6e7c2cde59b"
|
||||
},
|
||||
{
|
||||
"ImportPath": "code.google.com/p/go.text/unicode/norm",
|
||||
"Comment": "null-89",
|
||||
"Rev": "df15baaf13e3f62b6b7a901e74caa3818a7c0a7c"
|
||||
"Comment": "null-90",
|
||||
"Rev": "d65bffbc88a153d23a6d2a864531e6e7c2cde59b"
|
||||
},
|
||||
{
|
||||
"ImportPath": "code.google.com/p/snappy-go/snappy",
|
||||
@@ -41,7 +41,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/xdr",
|
||||
"Rev": "694859acb207675085232438780db923ceb43e96"
|
||||
"Rev": "e1714bbe4764b15490fcc8ebd25d4bd9ea50a4b9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/juju/ratelimit",
|
||||
@@ -49,7 +49,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||
"Rev": "c5955912e3287376475731c5bc59c79a5a799105"
|
||||
"Rev": "c9d6b7be1428942d4cf4f54055b991a8513392eb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vitrun/qart/coding",
|
||||
|
||||
95
Godeps/_workspace/src/code.google.com/p/go.crypto/blowfish/block.go
generated
vendored
@@ -4,6 +4,22 @@
|
||||
|
||||
package blowfish
|
||||
|
||||
// getNextWord returns the next big-endian uint32 value from the byte slice
|
||||
// at the given position in a circular manner, updating the position.
|
||||
func getNextWord(b []byte, pos *int) uint32 {
|
||||
var w uint32
|
||||
j := *pos
|
||||
for i := 0; i < 4; i++ {
|
||||
w = w<<8 | uint32(b[j])
|
||||
j++
|
||||
if j >= len(b) {
|
||||
j = 0
|
||||
}
|
||||
}
|
||||
*pos = j
|
||||
return w
|
||||
}
|
||||
|
||||
// ExpandKey performs a key expansion on the given *Cipher. Specifically, it
|
||||
// performs the Blowfish algorithm's key schedule which sets up the *Cipher's
|
||||
// pi and substitution tables for calls to Encrypt. This is used, primarily,
|
||||
@@ -12,6 +28,7 @@ package blowfish
|
||||
func ExpandKey(key []byte, c *Cipher) {
|
||||
j := 0
|
||||
for i := 0; i < 18; i++ {
|
||||
// Using inlined getNextWord for performance.
|
||||
var d uint32
|
||||
for k := 0; k < 4; k++ {
|
||||
d = d<<8 | uint32(key[j])
|
||||
@@ -54,86 +71,44 @@ func ExpandKey(key []byte, c *Cipher) {
|
||||
func expandKeyWithSalt(key []byte, salt []byte, c *Cipher) {
|
||||
j := 0
|
||||
for i := 0; i < 18; i++ {
|
||||
var d uint32
|
||||
for k := 0; k < 4; k++ {
|
||||
d = d<<8 | uint32(key[j])
|
||||
j++
|
||||
if j >= len(key) {
|
||||
j = 0
|
||||
}
|
||||
}
|
||||
c.p[i] ^= d
|
||||
c.p[i] ^= getNextWord(key, &j)
|
||||
}
|
||||
|
||||
j = 0
|
||||
var expandedSalt [4]uint32
|
||||
for i := range expandedSalt {
|
||||
var d uint32
|
||||
for k := 0; k < 4; k++ {
|
||||
d = d<<8 | uint32(salt[j])
|
||||
j++
|
||||
if j >= len(salt) {
|
||||
j = 0
|
||||
}
|
||||
}
|
||||
expandedSalt[i] = d
|
||||
}
|
||||
|
||||
var l, r uint32
|
||||
for i := 0; i < 18; i += 2 {
|
||||
l ^= expandedSalt[i&2]
|
||||
r ^= expandedSalt[(i&2)+1]
|
||||
l ^= getNextWord(salt, &j)
|
||||
r ^= getNextWord(salt, &j)
|
||||
l, r = encryptBlock(l, r, c)
|
||||
c.p[i], c.p[i+1] = l, r
|
||||
}
|
||||
|
||||
for i := 0; i < 256; i += 4 {
|
||||
l ^= expandedSalt[2]
|
||||
r ^= expandedSalt[3]
|
||||
for i := 0; i < 256; i += 2 {
|
||||
l ^= getNextWord(salt, &j)
|
||||
r ^= getNextWord(salt, &j)
|
||||
l, r = encryptBlock(l, r, c)
|
||||
c.s0[i], c.s0[i+1] = l, r
|
||||
|
||||
l ^= expandedSalt[0]
|
||||
r ^= expandedSalt[1]
|
||||
l, r = encryptBlock(l, r, c)
|
||||
c.s0[i+2], c.s0[i+3] = l, r
|
||||
|
||||
}
|
||||
|
||||
for i := 0; i < 256; i += 4 {
|
||||
l ^= expandedSalt[2]
|
||||
r ^= expandedSalt[3]
|
||||
for i := 0; i < 256; i += 2 {
|
||||
l ^= getNextWord(salt, &j)
|
||||
r ^= getNextWord(salt, &j)
|
||||
l, r = encryptBlock(l, r, c)
|
||||
c.s1[i], c.s1[i+1] = l, r
|
||||
|
||||
l ^= expandedSalt[0]
|
||||
r ^= expandedSalt[1]
|
||||
l, r = encryptBlock(l, r, c)
|
||||
c.s1[i+2], c.s1[i+3] = l, r
|
||||
}
|
||||
|
||||
for i := 0; i < 256; i += 4 {
|
||||
l ^= expandedSalt[2]
|
||||
r ^= expandedSalt[3]
|
||||
for i := 0; i < 256; i += 2 {
|
||||
l ^= getNextWord(salt, &j)
|
||||
r ^= getNextWord(salt, &j)
|
||||
l, r = encryptBlock(l, r, c)
|
||||
c.s2[i], c.s2[i+1] = l, r
|
||||
|
||||
l ^= expandedSalt[0]
|
||||
r ^= expandedSalt[1]
|
||||
l, r = encryptBlock(l, r, c)
|
||||
c.s2[i+2], c.s2[i+3] = l, r
|
||||
}
|
||||
|
||||
for i := 0; i < 256; i += 4 {
|
||||
l ^= expandedSalt[2]
|
||||
r ^= expandedSalt[3]
|
||||
for i := 0; i < 256; i += 2 {
|
||||
l ^= getNextWord(salt, &j)
|
||||
r ^= getNextWord(salt, &j)
|
||||
l, r = encryptBlock(l, r, c)
|
||||
c.s3[i], c.s3[i+1] = l, r
|
||||
|
||||
l ^= expandedSalt[0]
|
||||
r ^= expandedSalt[1]
|
||||
l, r = encryptBlock(l, r, c)
|
||||
c.s3[i+2], c.s3[i+3] = l, r
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,9 +157,3 @@ func decryptBlock(l, r uint32, c *Cipher) (uint32, uint32) {
|
||||
xr ^= c.p[0]
|
||||
return xr, xl
|
||||
}
|
||||
|
||||
func zero(x []uint32) {
|
||||
for i := range x {
|
||||
x[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
76
Godeps/_workspace/src/code.google.com/p/go.crypto/blowfish/blowfish_test.go
generated
vendored
@@ -4,9 +4,7 @@
|
||||
|
||||
package blowfish
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
import "testing"
|
||||
|
||||
type CryptTest struct {
|
||||
key []byte
|
||||
@@ -202,3 +200,75 @@ func TestSaltedCipherKeyLength(t *testing.T) {
|
||||
t.Errorf("NewSaltedCipher with long key, gave error %#v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test vectors generated with Blowfish from OpenSSH.
|
||||
var saltedVectors = [][8]byte{
|
||||
{0x0c, 0x82, 0x3b, 0x7b, 0x8d, 0x01, 0x4b, 0x7e},
|
||||
{0xd1, 0xe1, 0x93, 0xf0, 0x70, 0xa6, 0xdb, 0x12},
|
||||
{0xfc, 0x5e, 0xba, 0xde, 0xcb, 0xf8, 0x59, 0xad},
|
||||
{0x8a, 0x0c, 0x76, 0xe7, 0xdd, 0x2c, 0xd3, 0xa8},
|
||||
{0x2c, 0xcb, 0x7b, 0xee, 0xac, 0x7b, 0x7f, 0xf8},
|
||||
{0xbb, 0xf6, 0x30, 0x6f, 0xe1, 0x5d, 0x62, 0xbf},
|
||||
{0x97, 0x1e, 0xc1, 0x3d, 0x3d, 0xe0, 0x11, 0xe9},
|
||||
{0x06, 0xd7, 0x4d, 0xb1, 0x80, 0xa3, 0xb1, 0x38},
|
||||
{0x67, 0xa1, 0xa9, 0x75, 0x0e, 0x5b, 0xc6, 0xb4},
|
||||
{0x51, 0x0f, 0x33, 0x0e, 0x4f, 0x67, 0xd2, 0x0c},
|
||||
{0xf1, 0x73, 0x7e, 0xd8, 0x44, 0xea, 0xdb, 0xe5},
|
||||
{0x14, 0x0e, 0x16, 0xce, 0x7f, 0x4a, 0x9c, 0x7b},
|
||||
{0x4b, 0xfe, 0x43, 0xfd, 0xbf, 0x36, 0x04, 0x47},
|
||||
{0xb1, 0xeb, 0x3e, 0x15, 0x36, 0xa7, 0xbb, 0xe2},
|
||||
{0x6d, 0x0b, 0x41, 0xdd, 0x00, 0x98, 0x0b, 0x19},
|
||||
{0xd3, 0xce, 0x45, 0xce, 0x1d, 0x56, 0xb7, 0xfc},
|
||||
{0xd9, 0xf0, 0xfd, 0xda, 0xc0, 0x23, 0xb7, 0x93},
|
||||
{0x4c, 0x6f, 0xa1, 0xe4, 0x0c, 0xa8, 0xca, 0x57},
|
||||
{0xe6, 0x2f, 0x28, 0xa7, 0x0c, 0x94, 0x0d, 0x08},
|
||||
{0x8f, 0xe3, 0xf0, 0xb6, 0x29, 0xe3, 0x44, 0x03},
|
||||
{0xff, 0x98, 0xdd, 0x04, 0x45, 0xb4, 0x6d, 0x1f},
|
||||
{0x9e, 0x45, 0x4d, 0x18, 0x40, 0x53, 0xdb, 0xef},
|
||||
{0xb7, 0x3b, 0xef, 0x29, 0xbe, 0xa8, 0x13, 0x71},
|
||||
{0x02, 0x54, 0x55, 0x41, 0x8e, 0x04, 0xfc, 0xad},
|
||||
{0x6a, 0x0a, 0xee, 0x7c, 0x10, 0xd9, 0x19, 0xfe},
|
||||
{0x0a, 0x22, 0xd9, 0x41, 0xcc, 0x23, 0x87, 0x13},
|
||||
{0x6e, 0xff, 0x1f, 0xff, 0x36, 0x17, 0x9c, 0xbe},
|
||||
{0x79, 0xad, 0xb7, 0x40, 0xf4, 0x9f, 0x51, 0xa6},
|
||||
{0x97, 0x81, 0x99, 0xa4, 0xde, 0x9e, 0x9f, 0xb6},
|
||||
{0x12, 0x19, 0x7a, 0x28, 0xd0, 0xdc, 0xcc, 0x92},
|
||||
{0x81, 0xda, 0x60, 0x1e, 0x0e, 0xdd, 0x65, 0x56},
|
||||
{0x7d, 0x76, 0x20, 0xb2, 0x73, 0xc9, 0x9e, 0xee},
|
||||
}
|
||||
|
||||
func TestSaltedCipher(t *testing.T) {
|
||||
var key, salt [32]byte
|
||||
for i := range key {
|
||||
key[i] = byte(i)
|
||||
salt[i] = byte(i + 32)
|
||||
}
|
||||
for i, v := range saltedVectors {
|
||||
c, err := NewSaltedCipher(key[:], salt[:i])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var buf [8]byte
|
||||
c.Encrypt(buf[:], buf[:])
|
||||
if v != buf {
|
||||
t.Errorf("%d: expected %x, got %x", i, v, buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExpandKeyWithSalt(b *testing.B) {
|
||||
key := make([]byte, 32)
|
||||
salt := make([]byte, 16)
|
||||
c, _ := NewCipher(key)
|
||||
for i := 0; i < b.N; i++ {
|
||||
expandKeyWithSalt(key, salt, c)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExpandKey(b *testing.B) {
|
||||
key := make([]byte, 32)
|
||||
c, _ := NewCipher(key)
|
||||
for i := 0; i < b.N; i++ {
|
||||
ExpandKey(key, c)
|
||||
}
|
||||
}
|
||||
|
||||
5
Godeps/_workspace/src/code.google.com/p/go.crypto/blowfish/cipher.go
generated
vendored
@@ -40,8 +40,11 @@ func NewCipher(key []byte) (*Cipher, error) {
|
||||
// NewSaltedCipher creates a returns a Cipher that folds a salt into its key
|
||||
// schedule. For most purposes, NewCipher, instead of NewSaltedCipher, is
|
||||
// sufficient and desirable. For bcrypt compatiblity, the key can be over 56
|
||||
// bytes. Only the first 16 bytes of salt are used.
|
||||
// bytes.
|
||||
func NewSaltedCipher(key, salt []byte) (*Cipher, error) {
|
||||
if len(salt) == 0 {
|
||||
return NewCipher(key)
|
||||
}
|
||||
var result Cipher
|
||||
if k := len(key); k < 1 {
|
||||
return nil, KeySizeError(k)
|
||||
|
||||
25
Godeps/_workspace/src/github.com/calmh/xdr/bench_test.go
generated
vendored
@@ -7,6 +7,8 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/calmh/xdr"
|
||||
)
|
||||
|
||||
type XDRBenchStruct struct {
|
||||
@@ -58,6 +60,16 @@ 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)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type repeatReader struct {
|
||||
data []byte
|
||||
}
|
||||
@@ -86,3 +98,16 @@ func BenchmarkThisDecode(b *testing.B) {
|
||||
rr.Reset(e)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkThisDecoder(b *testing.B) {
|
||||
rr := &repeatReader{e}
|
||||
r := xdr.NewReader(rr)
|
||||
var t XDRBenchStruct
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := t.decodeXDR(r)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
rr.Reset(e)
|
||||
}
|
||||
}
|
||||
|
||||
29
Godeps/_workspace/src/github.com/calmh/xdr/reader.go
generated
vendored
@@ -7,6 +7,8 @@ package xdr
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var ErrElementSizeExceeded = errors.New("element size exceeded")
|
||||
@@ -15,7 +17,6 @@ type Reader struct {
|
||||
r io.Reader
|
||||
err error
|
||||
b [8]byte
|
||||
sb []byte
|
||||
}
|
||||
|
||||
func NewReader(r io.Reader) *Reader {
|
||||
@@ -35,23 +36,17 @@ func (r *Reader) ReadRaw(bs []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (r *Reader) ReadString() string {
|
||||
if r.sb == nil {
|
||||
r.sb = make([]byte, 64)
|
||||
} else {
|
||||
r.sb = r.sb[:cap(r.sb)]
|
||||
}
|
||||
r.sb = r.ReadBytesInto(r.sb)
|
||||
return string(r.sb)
|
||||
return r.ReadStringMax(0)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadStringMax(max int) string {
|
||||
if r.sb == nil {
|
||||
r.sb = make([]byte, 64)
|
||||
} else {
|
||||
r.sb = r.sb[:cap(r.sb)]
|
||||
buf := r.ReadBytesMaxInto(max, nil)
|
||||
bh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
|
||||
sh := reflect.StringHeader{
|
||||
Data: bh.Data,
|
||||
Len: bh.Len,
|
||||
}
|
||||
r.sb = r.ReadBytesMaxInto(max, r.sb)
|
||||
return string(r.sb)
|
||||
return *((*string)(unsafe.Pointer(&sh)))
|
||||
}
|
||||
|
||||
func (r *Reader) ReadBytes() []byte {
|
||||
@@ -80,10 +75,10 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
if l+pad(l) > len(dst) {
|
||||
dst = make([]byte, l+pad(l))
|
||||
if fullLen := l + pad(l); fullLen > len(dst) {
|
||||
dst = make([]byte, fullLen)
|
||||
} else {
|
||||
dst = dst[:l+pad(l)]
|
||||
dst = dst[:fullLen]
|
||||
}
|
||||
|
||||
var n int
|
||||
|
||||
14
Godeps/_workspace/src/github.com/calmh/xdr/writer.go
generated
vendored
@@ -3,7 +3,11 @@
|
||||
|
||||
package xdr
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var padBytes = []byte{0, 0, 0}
|
||||
|
||||
@@ -38,7 +42,13 @@ func (w *Writer) WriteRaw(bs []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (w *Writer) WriteString(s string) (int, error) {
|
||||
return w.WriteBytes([]byte(s))
|
||||
sh := *((*reflect.StringHeader)(unsafe.Pointer(&s)))
|
||||
bh := reflect.SliceHeader{
|
||||
Data: sh.Data,
|
||||
Len: sh.Len,
|
||||
Cap: sh.Len,
|
||||
}
|
||||
return w.WriteBytes(*(*[]byte)(unsafe.Pointer(&bh)))
|
||||
}
|
||||
|
||||
func (w *Writer) WriteBytes(bs []byte) (int, error) {
|
||||
|
||||
5
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db.go
generated
vendored
@@ -481,10 +481,11 @@ func (db *DB) recoverJournal() error {
|
||||
|
||||
buf.Reset()
|
||||
if _, err := buf.ReadFrom(r); err != nil {
|
||||
if strict {
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
continue
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := batch.decode(buf.Bytes()); err != nil {
|
||||
return err
|
||||
|
||||
121
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/journal/journal.go
generated
vendored
@@ -103,18 +103,18 @@ type flusher interface {
|
||||
Flush() error
|
||||
}
|
||||
|
||||
// DroppedError is the error type that passed to Dropper.Drop method.
|
||||
type DroppedError struct {
|
||||
// ErrCorrupted is the error type that generated by corrupted block or chunk.
|
||||
type ErrCorrupted struct {
|
||||
Size int
|
||||
Reason string
|
||||
}
|
||||
|
||||
func (e DroppedError) Error() string {
|
||||
return fmt.Sprintf("leveldb/journal: dropped %d bytes: %s", e.Size, e.Reason)
|
||||
func (e ErrCorrupted) Error() string {
|
||||
return fmt.Sprintf("leveldb/journal: corrupted %d bytes: %s", e.Size, e.Reason)
|
||||
}
|
||||
|
||||
// Dropper is the interface that wrap simple Drop method. The Drop
|
||||
// method will be called when the journal reader dropping a chunk.
|
||||
// method will be called when the journal reader dropping a block or chunk.
|
||||
type Dropper interface {
|
||||
Drop(err error)
|
||||
}
|
||||
@@ -158,76 +158,78 @@ func NewReader(r io.Reader, dropper Dropper, strict, checksum bool) *Reader {
|
||||
}
|
||||
}
|
||||
|
||||
var errSkip = errors.New("leveldb/journal: skipped")
|
||||
|
||||
func (r *Reader) corrupt(n int, reason string, skip bool) error {
|
||||
if r.dropper != nil {
|
||||
r.dropper.Drop(ErrCorrupted{n, reason})
|
||||
}
|
||||
if r.strict && !skip {
|
||||
r.err = ErrCorrupted{n, reason}
|
||||
return r.err
|
||||
}
|
||||
return errSkip
|
||||
}
|
||||
|
||||
// nextChunk sets r.buf[r.i:r.j] to hold the next chunk's payload, reading the
|
||||
// next block into the buffer if necessary.
|
||||
func (r *Reader) nextChunk(wantFirst, skip bool) error {
|
||||
func (r *Reader) nextChunk(first bool) error {
|
||||
for {
|
||||
if r.j+headerSize <= r.n {
|
||||
checksum := binary.LittleEndian.Uint32(r.buf[r.j+0 : r.j+4])
|
||||
length := binary.LittleEndian.Uint16(r.buf[r.j+4 : r.j+6])
|
||||
chunkType := r.buf[r.j+6]
|
||||
|
||||
var err error
|
||||
if checksum == 0 && length == 0 && chunkType == 0 {
|
||||
// Drop entire block.
|
||||
err = DroppedError{r.n - r.j, "zero header"}
|
||||
m := r.n - r.j
|
||||
r.i = r.n
|
||||
r.j = r.n
|
||||
return r.corrupt(m, "zero header", false)
|
||||
} else {
|
||||
m := r.n - r.j
|
||||
r.i = r.j + headerSize
|
||||
r.j = r.j + headerSize + int(length)
|
||||
if r.j > r.n {
|
||||
// Drop entire block.
|
||||
err = DroppedError{m, "chunk length overflows block"}
|
||||
r.i = r.n
|
||||
r.j = r.n
|
||||
return r.corrupt(m, "chunk length overflows block", false)
|
||||
} else if r.checksum && checksum != util.NewCRC(r.buf[r.i-1:r.j]).Value() {
|
||||
// Drop entire block.
|
||||
err = DroppedError{m, "checksum mismatch"}
|
||||
r.i = r.n
|
||||
r.j = r.n
|
||||
return r.corrupt(m, "checksum mismatch", false)
|
||||
}
|
||||
}
|
||||
if wantFirst && err == nil && chunkType != fullChunkType && chunkType != firstChunkType {
|
||||
if skip {
|
||||
// The chunk are intentionally skipped.
|
||||
if chunkType == lastChunkType {
|
||||
skip = false
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
// Drop the chunk.
|
||||
err = DroppedError{r.j - r.i + headerSize, "orphan chunk"}
|
||||
}
|
||||
if first && chunkType != fullChunkType && chunkType != firstChunkType {
|
||||
m := r.j - r.i
|
||||
r.i = r.j
|
||||
// Report the error, but skip it.
|
||||
return r.corrupt(m+headerSize, "orphan chunk", true)
|
||||
}
|
||||
if err == nil {
|
||||
r.last = chunkType == fullChunkType || chunkType == lastChunkType
|
||||
} else {
|
||||
if r.dropper != nil {
|
||||
r.dropper.Drop(err)
|
||||
}
|
||||
if r.strict {
|
||||
r.err = err
|
||||
}
|
||||
r.last = chunkType == fullChunkType || chunkType == lastChunkType
|
||||
return nil
|
||||
}
|
||||
|
||||
// The last block.
|
||||
if r.n < blockSize && r.n > 0 {
|
||||
if !first {
|
||||
return r.corrupt(0, "missing chunk part", false)
|
||||
}
|
||||
r.err = io.EOF
|
||||
return r.err
|
||||
}
|
||||
|
||||
// Read block.
|
||||
n, err := io.ReadFull(r.r, r.buf[:])
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
return err
|
||||
}
|
||||
if r.n < blockSize && r.n > 0 {
|
||||
// This is the last block.
|
||||
if r.j != r.n {
|
||||
r.err = io.ErrUnexpectedEOF
|
||||
} else {
|
||||
r.err = io.EOF
|
||||
}
|
||||
return r.err
|
||||
}
|
||||
n, err := io.ReadFull(r.r, r.buf[:])
|
||||
if err != nil && err != io.ErrUnexpectedEOF {
|
||||
r.err = err
|
||||
return r.err
|
||||
}
|
||||
if n == 0 {
|
||||
if !first {
|
||||
return r.corrupt(0, "missing chunk part", false)
|
||||
}
|
||||
r.err = io.EOF
|
||||
return r.err
|
||||
}
|
||||
@@ -237,29 +239,26 @@ func (r *Reader) nextChunk(wantFirst, skip bool) error {
|
||||
|
||||
// Next returns a reader for the next journal. It returns io.EOF if there are no
|
||||
// more journals. The reader returned becomes stale after the next Next call,
|
||||
// and should no longer be used.
|
||||
// and should no longer be used. If strict is false, the reader will returns
|
||||
// io.ErrUnexpectedEOF error when found corrupted journal.
|
||||
func (r *Reader) Next() (io.Reader, error) {
|
||||
r.seq++
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
skip := !r.last
|
||||
r.i = r.j
|
||||
for {
|
||||
r.i = r.j
|
||||
if r.nextChunk(true, skip) != nil {
|
||||
// So that 'orphan chunk' drop will be reported.
|
||||
skip = false
|
||||
} else {
|
||||
if err := r.nextChunk(true); err == nil {
|
||||
break
|
||||
}
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
} else if err != errSkip {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &singleReader{r, r.seq, nil}, nil
|
||||
}
|
||||
|
||||
// Reset resets the journal reader, allows reuse of the journal reader.
|
||||
// Reset resets the journal reader, allows reuse of the journal reader. Reset returns
|
||||
// last accumulated error.
|
||||
func (r *Reader) Reset(reader io.Reader, dropper Dropper, strict, checksum bool) error {
|
||||
r.seq++
|
||||
err := r.err
|
||||
@@ -296,7 +295,11 @@ func (x *singleReader) Read(p []byte) (int, error) {
|
||||
if r.last {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if x.err = r.nextChunk(false, false); x.err != nil {
|
||||
x.err = r.nextChunk(false)
|
||||
if x.err != nil {
|
||||
if x.err == errSkip {
|
||||
x.err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return 0, x.err
|
||||
}
|
||||
}
|
||||
@@ -320,7 +323,11 @@ func (x *singleReader) ReadByte() (byte, error) {
|
||||
if r.last {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if x.err = r.nextChunk(false, false); x.err != nil {
|
||||
x.err = r.nextChunk(false)
|
||||
if x.err != nil {
|
||||
if x.err == errSkip {
|
||||
x.err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return 0, x.err
|
||||
}
|
||||
}
|
||||
|
||||
145
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/journal/journal_test.go
generated
vendored
@@ -326,3 +326,148 @@ func TestStaleWriter(t *testing.T) {
|
||||
t.Fatalf("stale write #1: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrupt_MissingLastBlock(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
w := NewWriter(buf)
|
||||
|
||||
// First record.
|
||||
ww, err := w.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-1024)); err != nil {
|
||||
t.Fatalf("write #0: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Second record.
|
||||
ww, err = w.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-headerSize)); err != nil {
|
||||
t.Fatalf("write #1: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Cut the last block.
|
||||
b := buf.Bytes()[:blockSize]
|
||||
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
|
||||
|
||||
// First read.
|
||||
rr, err := r.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err := io.Copy(ioutil.Discard, rr)
|
||||
if err != nil {
|
||||
t.Fatalf("read #0: %v", err)
|
||||
}
|
||||
if n != blockSize-1024 {
|
||||
t.Fatalf("read #0: got %d bytes want %d", n, blockSize-1024)
|
||||
}
|
||||
|
||||
// Second read.
|
||||
rr, err = r.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err = io.Copy(ioutil.Discard, rr)
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
t.Fatalf("read #1: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrupt_CorruptedMiddleBlock(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
w := NewWriter(buf)
|
||||
|
||||
// First record.
|
||||
ww, err := w.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize/2)); err != nil {
|
||||
t.Fatalf("write #0: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Second record.
|
||||
ww, err = w.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-headerSize)); err != nil {
|
||||
t.Fatalf("write #1: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Third record.
|
||||
ww, err = w.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ww.Write(bytes.Repeat([]byte("0"), (blockSize-headerSize)+1)); err != nil {
|
||||
t.Fatalf("write #2: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Fourth record.
|
||||
ww, err = w.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ww.Write(bytes.Repeat([]byte("0"), (blockSize-headerSize)+2)); err != nil {
|
||||
t.Fatalf("write #3: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
b := buf.Bytes()
|
||||
// Corrupting block #1.
|
||||
for i := 0; i < 1024; i++ {
|
||||
b[blockSize+i] = '1'
|
||||
}
|
||||
|
||||
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
|
||||
|
||||
// First read.
|
||||
rr, err := r.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err := io.Copy(ioutil.Discard, rr)
|
||||
if err != nil {
|
||||
t.Fatalf("read #0: %v", err)
|
||||
}
|
||||
if want := int64(blockSize / 2); n != want {
|
||||
t.Fatalf("read #0: got %d bytes want %d", n, want)
|
||||
}
|
||||
|
||||
// Second read.
|
||||
rr, err = r.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err = io.Copy(ioutil.Discard, rr)
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
t.Fatalf("read #1: unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Third read.
|
||||
rr, err = r.Next()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err = io.Copy(ioutil.Discard, rr)
|
||||
if err != nil {
|
||||
t.Fatalf("read #2: %v", err)
|
||||
}
|
||||
if want := int64(blockSize-headerSize) + 2; n != want {
|
||||
t.Fatalf("read #2: got %d bytes want %d", n, want)
|
||||
}
|
||||
}
|
||||
|
||||
2
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/session_util.go
generated
vendored
@@ -22,7 +22,7 @@ type dropper struct {
|
||||
}
|
||||
|
||||
func (d dropper) Drop(err error) {
|
||||
if e, ok := err.(journal.DroppedError); ok {
|
||||
if e, ok := err.(journal.ErrCorrupted); ok {
|
||||
d.s.logf("journal@drop %s-%d S·%s %q", d.file.Type(), d.file.Num(), shortenb(e.Size), e.Reason)
|
||||
} else {
|
||||
d.s.logf("journal@drop %s-%d %q", d.file.Type(), d.file.Num(), err)
|
||||
|
||||
16
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table/reader.go
generated
vendored
@@ -13,6 +13,7 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.google.com/p/snappy-go/snappy"
|
||||
|
||||
@@ -528,6 +529,8 @@ type Reader struct {
|
||||
dataEnd int64
|
||||
indexBlock *block
|
||||
filterBlock *filterBlock
|
||||
|
||||
blockPool sync.Pool
|
||||
}
|
||||
|
||||
func verifyChecksum(data []byte) bool {
|
||||
@@ -538,7 +541,13 @@ func verifyChecksum(data []byte) bool {
|
||||
}
|
||||
|
||||
func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
|
||||
data := make([]byte, bh.length+blockTrailerLen)
|
||||
data, _ := r.blockPool.Get().([]byte) // data is either nil or a valid []byte from the pool
|
||||
if l := bh.length + blockTrailerLen; uint64(len(data)) >= l {
|
||||
data = data[:l]
|
||||
} else {
|
||||
r.blockPool.Put(data)
|
||||
data = make([]byte, l)
|
||||
}
|
||||
if _, err := r.reader.ReadAt(data, int64(bh.offset)); err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
@@ -552,10 +561,13 @@ func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
|
||||
data = data[:bh.length]
|
||||
case blockTypeSnappyCompression:
|
||||
var err error
|
||||
data, err = snappy.Decode(nil, data[:bh.length])
|
||||
decData, _ := r.blockPool.Get().([]byte)
|
||||
decData, err = snappy.Decode(decData, data[:bh.length])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.blockPool.Put(data[:cap(data)])
|
||||
data = decData
|
||||
default:
|
||||
return nil, fmt.Errorf("leveldb/table: Reader: unknown block compression type: %d", data[bh.length])
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1008 B After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 6.4 KiB |
BIN
assets/logo-text-128.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/logo-text-256.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
assets/logo-text-64.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 12 KiB |
1421
assets/logo.ai
1549
assets/logo.pdf
110
build.sh
@@ -4,7 +4,11 @@ export COPYFILE_DISABLE=true
|
||||
export GO386=387 # Don't use SSE on 32 bit builds
|
||||
|
||||
distFiles=(README.md LICENSE CONTRIBUTORS) # apart from the binary itself
|
||||
|
||||
# replace "...-12-g123abc" with "...+12-g123abc" to remain semver compatible-ish
|
||||
version=$(git describe --always --dirty)
|
||||
version=$(echo "$version" | sed 's/-\([0-9]\{1,3\}-g[0-9a-f]\{5,10\}\)/+\1/')
|
||||
|
||||
date=$(git show -s --format=%ct)
|
||||
user=$(whoami)
|
||||
host=$(hostname)
|
||||
@@ -93,7 +97,6 @@ setup() {
|
||||
go get -v code.google.com/p/go.tools/cmd/vet
|
||||
go get -v github.com/mattn/goveralls
|
||||
go get -v github.com/tools/godep
|
||||
GOPATH="$GOPATH:$(godep path)" go get -v -t ./...
|
||||
}
|
||||
|
||||
xdr() {
|
||||
@@ -111,11 +114,57 @@ translate() {
|
||||
|
||||
transifex() {
|
||||
pushd gui
|
||||
go run ../cmd/transifexdl/main.go > valid-langs.js
|
||||
go run ../cmd/transifexdl/main.go
|
||||
popd
|
||||
assets
|
||||
}
|
||||
|
||||
build-all() {
|
||||
rm -f *.tar.gz *.zip
|
||||
test -short || exit 1
|
||||
assets
|
||||
|
||||
rm -rf bin Godeps/_workspace/pkg $GOPATH/pkg/*/github.com/syncthing
|
||||
for os in darwin-amd64 freebsd-amd64 freebsd-386 linux-amd64 linux-386 windows-amd64 windows-386 solaris-amd64 ; do
|
||||
export GOOS=${os%-*}
|
||||
export GOARCH=${os#*-}
|
||||
|
||||
build $*
|
||||
|
||||
name="syncthing-${os/darwin/macosx}-$version"
|
||||
case $GOOS in
|
||||
windows)
|
||||
zipDist "$name"
|
||||
rm -f syncthing.exe
|
||||
;;
|
||||
*)
|
||||
tarDist "$name"
|
||||
rm -f syncthing
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
export GOOS=linux
|
||||
export GOARCH=arm
|
||||
|
||||
origldflags="$ldflags"
|
||||
|
||||
export GOARM=7
|
||||
ldflags="$origldflags -X main.GoArchExtra v7"
|
||||
build $*
|
||||
tarDist "syncthing-linux-armv7-$version"
|
||||
|
||||
export GOARM=6
|
||||
ldflags="$origldflags -X main.GoArchExtra v6"
|
||||
build $*
|
||||
tarDist "syncthing-linux-armv6-$version"
|
||||
|
||||
export GOARM=5
|
||||
ldflags="$origldflags -X main.GoArchExtra v5"
|
||||
build $*
|
||||
tarDist "syncthing-linux-armv5-$version"
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
"")
|
||||
shift
|
||||
@@ -123,6 +172,15 @@ case "$1" in
|
||||
godep go install $* -ldflags "$ldflags" ./cmd/...
|
||||
;;
|
||||
|
||||
clean)
|
||||
rm -rf bin Godeps/_workspace/pkg $GOPATH/pkg/*/github.com/syncthing
|
||||
;;
|
||||
|
||||
noupgrade)
|
||||
export GOBIN=$(pwd)/bin
|
||||
godep go install -tags noupgrade -ldflags "$ldflags" ./cmd/...
|
||||
;;
|
||||
|
||||
race)
|
||||
build -race
|
||||
;;
|
||||
@@ -153,49 +211,13 @@ case "$1" in
|
||||
;;
|
||||
|
||||
all)
|
||||
rm -f *.tar.gz *.zip
|
||||
test -short || exit 1
|
||||
assets
|
||||
|
||||
for os in darwin-amd64 linux-386 linux-amd64 freebsd-amd64 windows-amd64 windows-386 solaris-amd64 ; do
|
||||
export GOOS=${os%-*}
|
||||
export GOARCH=${os#*-}
|
||||
|
||||
build
|
||||
|
||||
name="syncthing-${os/darwin/macosx}-$version"
|
||||
case $GOOS in
|
||||
windows)
|
||||
zipDist "$name"
|
||||
rm -f syncthing.exe
|
||||
;;
|
||||
*)
|
||||
tarDist "$name"
|
||||
rm -f syncthing
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
export GOOS=linux
|
||||
export GOARCH=arm
|
||||
|
||||
origldflags="$ldflags"
|
||||
|
||||
export GOARM=7
|
||||
ldflags="$origldflags -X main.GoArchExtra v7"
|
||||
build
|
||||
tarDist "syncthing-linux-armv7-$version"
|
||||
|
||||
export GOARM=6
|
||||
ldflags="$origldflags -X main.GoArchExtra v6"
|
||||
build
|
||||
tarDist "syncthing-linux-armv6-$version"
|
||||
|
||||
export GOARM=5
|
||||
ldflags="$origldflags -X main.GoArchExtra v5"
|
||||
build
|
||||
tarDist "syncthing-linux-armv5-$version"
|
||||
shift
|
||||
build-all
|
||||
;;
|
||||
|
||||
all-noupgrade)
|
||||
shift
|
||||
build-all -tags noupgrade
|
||||
;;
|
||||
|
||||
upload)
|
||||
|
||||
@@ -32,7 +32,8 @@ func main() {
|
||||
|
||||
if *node == "" {
|
||||
log.Printf("*** Global index for repo %q", *repo)
|
||||
fs.WithGlobal(func(f protocol.FileInfo) bool {
|
||||
fs.WithGlobalTruncated(func(fi protocol.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfoTruncated)
|
||||
fmt.Println(f)
|
||||
fmt.Println("\t", fs.Availability(f.Name))
|
||||
return true
|
||||
@@ -43,7 +44,8 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("*** Have index for repo %q node %q", *repo, n)
|
||||
fs.WithHave(n, func(f protocol.FileInfo) bool {
|
||||
fs.WithHaveTruncated(n, func(fi protocol.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfoTruncated)
|
||||
fmt.Println(f)
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -57,7 +57,7 @@ const (
|
||||
|
||||
func init() {
|
||||
l.AddHandler(logger.LevelWarn, showGuiError)
|
||||
sub := events.Default.Subscribe(^events.EventType(events.ItemStarted | events.ItemCompleted))
|
||||
sub := events.Default.Subscribe(events.AllEvents)
|
||||
eventSub = events.NewBufferedSubscription(sub, 1000)
|
||||
}
|
||||
|
||||
@@ -126,6 +126,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
postRestMux.HandleFunc("/rest/restart", restPostRestart)
|
||||
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
|
||||
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
|
||||
postRestMux.HandleFunc("/rest/scan", withModel(m, restPostScan))
|
||||
|
||||
// A handler that splits requests between the two above and disables
|
||||
// caching
|
||||
@@ -278,6 +279,8 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
err := json.NewDecoder(r.Body).Decode(&newCfg)
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
} else {
|
||||
if newCfg.GUI.Password == "" {
|
||||
// Leave it empty
|
||||
@@ -287,6 +290,8 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
} else {
|
||||
newCfg.GUI.Password = string(hash)
|
||||
}
|
||||
@@ -384,7 +389,7 @@ func restGetSystem(w http.ResponseWriter, r *http.Request) {
|
||||
res["myID"] = myID.String()
|
||||
res["goroutines"] = runtime.NumGoroutine()
|
||||
res["alloc"] = m.Alloc
|
||||
res["sys"] = m.Sys
|
||||
res["sys"] = m.Sys - m.HeapReleased
|
||||
res["tilde"] = expandTilde("~")
|
||||
if cfg.Options.GlobalAnnEnabled && discoverer != nil {
|
||||
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
|
||||
@@ -502,6 +507,7 @@ func restGetLang(w http.ResponseWriter, r *http.Request) {
|
||||
langs = append(langs, l[:2])
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(langs)
|
||||
}
|
||||
|
||||
@@ -514,7 +520,7 @@ func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if upgrade.CompareVersions(rel.Tag, Version) == 1 {
|
||||
err = upgrade.UpgradeTo(rel)
|
||||
err = upgrade.UpgradeTo(rel, GoArchExtra)
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
@@ -525,9 +531,19 @@ func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func restPostScan(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
repo := qs.Get("repo")
|
||||
sub := qs.Get("sub")
|
||||
err := m.ScanRepoSub(repo, sub)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
|
||||
func getQR(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
text := r.FormValue("text")
|
||||
var qs = r.URL.Query()
|
||||
var text = qs.Get("text")
|
||||
code, err := qr.Encode(text, qr.M)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid", 500)
|
||||
|
||||
46
cmd/syncthing/heapprof.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build heapprof
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
go saveHeapProfiles()
|
||||
}
|
||||
|
||||
func saveHeapProfiles() {
|
||||
runtime.MemProfileRate = 1
|
||||
var memstats, prevMemstats runtime.MemStats
|
||||
|
||||
t0 := time.Now()
|
||||
for t := range time.NewTicker(250 * time.Millisecond).C {
|
||||
startms := int(t.Sub(t0).Seconds() * 1000)
|
||||
runtime.ReadMemStats(&memstats)
|
||||
if memstats.HeapInuse > prevMemstats.HeapInuse {
|
||||
fd, err := os.Create(fmt.Sprintf("heap-%05d-%07d.pprof", syscall.Getpid(), startms))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = pprof.WriteHeapProfile(fd)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = fd.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
prevMemstats = memstats
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ var (
|
||||
BuildHost = "unknown"
|
||||
BuildUser = "unknown"
|
||||
LongVersion string
|
||||
GoArchExtra string // "", "v5", "v6", "v7"
|
||||
)
|
||||
|
||||
var l = logger.DefaultLogger
|
||||
@@ -54,7 +55,7 @@ var l = logger.DefaultLogger
|
||||
func init() {
|
||||
if Version != "unknown-dev" {
|
||||
// If not a generic dev build, version string should come from git describe
|
||||
exp := regexp.MustCompile(`^v\d+\.\d+\.\d+(-beta\d+)?(-\d+-g[0-9a-f]+)?(-dirty)?$`)
|
||||
exp := regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z0-9]+)*(\+\d+-g[0-9a-f]+)?(-dirty)?$`)
|
||||
if !exp.MatchString(Version) {
|
||||
l.Fatalf("Invalid version string %q;\n\tdoes not match regexp %v", Version, exp)
|
||||
}
|
||||
@@ -135,12 +136,16 @@ func main() {
|
||||
var showVersion bool
|
||||
var doUpgrade bool
|
||||
var doUpgradeCheck bool
|
||||
var generateDir string
|
||||
var noBrowser bool
|
||||
flag.StringVar(&confDir, "home", getDefaultConfDir(), "Set configuration directory")
|
||||
flag.BoolVar(&reset, "reset", false, "Prepare to resync from cluster")
|
||||
flag.BoolVar(&showVersion, "version", false, "Show version")
|
||||
flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade")
|
||||
flag.BoolVar(&doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
|
||||
flag.BoolVar(&noBrowser, "no-browser", false, "Do not start browser")
|
||||
flag.IntVar(&logFlags, "logflags", logFlags, "Set log flags")
|
||||
flag.StringVar(&generateDir, "generate", "", "Generate key in specified dir")
|
||||
flag.Usage = usageFor(flag.CommandLine, usage, extraUsage)
|
||||
flag.Parse()
|
||||
|
||||
@@ -151,10 +156,29 @@ func main() {
|
||||
|
||||
l.SetFlags(logFlags)
|
||||
|
||||
var err error
|
||||
lockPort, err = getLockPort()
|
||||
if err != nil {
|
||||
l.Fatalln("Opening lock port:", err)
|
||||
if generateDir != "" {
|
||||
dir := expandTilde(generateDir)
|
||||
|
||||
info, err := os.Stat(dir)
|
||||
l.FatalErr(err)
|
||||
if !info.IsDir() {
|
||||
l.Fatalln(dir, "is not a directory")
|
||||
}
|
||||
|
||||
cert, err := loadCert(dir, "")
|
||||
if err == nil {
|
||||
l.Warnln("Key exists; will not overwrite.")
|
||||
l.Infoln("Node ID:", protocol.NewNodeID(cert.Certificate[0]))
|
||||
return
|
||||
}
|
||||
|
||||
newCertificate(dir, "")
|
||||
cert, err = loadCert(dir, "")
|
||||
l.FatalErr(err)
|
||||
if err == nil {
|
||||
l.Infoln("Node ID:", protocol.NewNodeID(cert.Certificate[0]))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if doUpgrade || doUpgradeCheck {
|
||||
@@ -171,7 +195,7 @@ func main() {
|
||||
l.Infof("Upgrade available (current %q < latest %q)", Version, rel.Tag)
|
||||
|
||||
if doUpgrade {
|
||||
err = upgrade.UpgradeTo(rel)
|
||||
err = upgrade.UpgradeTo(rel, GoArchExtra)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err) // exits 1
|
||||
}
|
||||
@@ -182,6 +206,12 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
lockPort, err = getLockPort()
|
||||
if err != nil {
|
||||
l.Fatalln("Opening lock port:", err)
|
||||
}
|
||||
|
||||
if len(os.Getenv("GOGC")) == 0 {
|
||||
debug.SetGCPercent(25)
|
||||
}
|
||||
@@ -393,12 +423,21 @@ nextRepo:
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot start GUI:", err)
|
||||
}
|
||||
if cfg.Options.StartBrowser && len(os.Getenv("STRESTART")) == 0 {
|
||||
if !noBrowser && cfg.Options.StartBrowser && len(os.Getenv("STRESTART")) == 0 {
|
||||
openURL(fmt.Sprintf("%s://%s:%d", proto, hostOpen, addr.Port))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear out old indexes for other nodes. Otherwise we'll start up and
|
||||
// start needing a bunch of files which are nowhere to be found. This
|
||||
// needs to be changed when we correctly do persistent indexes.
|
||||
for _, repoCfg := range cfg.Repositories {
|
||||
for _, node := range repoCfg.NodeIDs() {
|
||||
m.Index(node, repoCfg.ID, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the repository and update the local model before establishing any
|
||||
// connections to other nodes.
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build perfstats
|
||||
|
||||
package main
|
||||
@@ -40,6 +44,6 @@ func savePerfStats(file string) {
|
||||
|
||||
startms := int(t.Sub(t0).Seconds() * 1000)
|
||||
|
||||
fmt.Fprintf(fd, "%d\t%f\t%d\t%d\n", startms, cpuUsagePercent, memstats.Alloc, memstats.Sys)
|
||||
fmt.Fprintf(fd, "%d\t%f\t%d\t%d\n", startms, cpuUsagePercent, memstats.Alloc, memstats.Sys-memstats.HeapReleased)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func certSeed(bs []byte) int64 {
|
||||
}
|
||||
|
||||
func newCertificate(dir string, prefix string) {
|
||||
l.Infoln("Generating RSA certificate and key...")
|
||||
l.Infoln("Generating RSA key and certificate...")
|
||||
|
||||
priv, err := rsa.GenerateKey(rand.Reader, tlsRSABits)
|
||||
l.FatalErr(err)
|
||||
@@ -67,11 +67,9 @@ func newCertificate(dir string, prefix string) {
|
||||
l.FatalErr(err)
|
||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
certOut.Close()
|
||||
l.Okln("Created RSA certificate file")
|
||||
|
||||
keyOut, err := os.OpenFile(filepath.Join(dir, prefix+"key.pem"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
l.FatalErr(err)
|
||||
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
keyOut.Close()
|
||||
l.Okln("Created RSA key file")
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func reportData(m *model.Model) map[string]interface{} {
|
||||
|
||||
var mem runtime.MemStats
|
||||
runtime.ReadMemStats(&mem)
|
||||
res["memoryUsageMiB"] = mem.Sys / 1024 / 1024
|
||||
res["memoryUsageMiB"] = (mem.Sys - mem.HeapReleased) / 1024 / 1024
|
||||
|
||||
var perf float64
|
||||
for i := 0; i < 5; i++ {
|
||||
|
||||
@@ -9,10 +9,13 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type stat struct {
|
||||
@@ -31,6 +34,12 @@ func main() {
|
||||
log.Fatal("Need environment variables TRANSIFEX_USER and TRANSIFEX_PASS")
|
||||
}
|
||||
|
||||
curValidLangs := map[string]bool{}
|
||||
for _, lang := range loadValidLangs() {
|
||||
curValidLangs[lang] = true
|
||||
}
|
||||
log.Println(curValidLangs)
|
||||
|
||||
resp := req("https://www.transifex.com/api/2/project/syncthing/resource/gui/stats")
|
||||
|
||||
var stats map[string]stat
|
||||
@@ -43,10 +52,12 @@ func main() {
|
||||
var langs []string
|
||||
for code, stat := range stats {
|
||||
shortCode := code[:2]
|
||||
if pct := 100 * stat.Translated / (stat.Translated + stat.Untranslated); pct < 95 {
|
||||
log.Printf("Skipping language %q (too low completion ratio %d%%)", shortCode, pct)
|
||||
os.Remove("lang-" + shortCode + ".json")
|
||||
continue
|
||||
if !curValidLangs[shortCode] {
|
||||
if pct := 100 * stat.Translated / (stat.Translated + stat.Untranslated); pct < 95 {
|
||||
log.Printf("Skipping language %q (too low completion ratio %d%%)", shortCode, pct)
|
||||
os.Remove("lang-" + shortCode + ".json")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
langs = append(langs, shortCode)
|
||||
@@ -72,9 +83,18 @@ func main() {
|
||||
fd.Close()
|
||||
}
|
||||
|
||||
saveValidLangs(langs)
|
||||
}
|
||||
|
||||
func saveValidLangs(langs []string) {
|
||||
sort.Strings(langs)
|
||||
fmt.Print("var validLangs = ")
|
||||
json.NewEncoder(os.Stdout).Encode(langs)
|
||||
fd, err := os.Create("valid-langs.js")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Fprint(fd, "var validLangs = ")
|
||||
json.NewEncoder(fd).Encode(langs)
|
||||
fd.Close()
|
||||
}
|
||||
|
||||
func userPass() (string, string) {
|
||||
@@ -97,3 +117,27 @@ func req(url string) *http.Response {
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func loadValidLangs() []string {
|
||||
fd, err := os.Open("valid-langs.js")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer fd.Close()
|
||||
bs, err := ioutil.ReadAll(fd)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var langs []string
|
||||
exp := regexp.MustCompile(`\[([a-z",]+)\]`)
|
||||
if matches := exp.FindSubmatch(bs); len(matches) == 2 {
|
||||
langs = strings.Split(string(matches[1]), ",")
|
||||
for i := range langs {
|
||||
// Remove quotes
|
||||
langs[i] = langs[i][1 : len(langs[i])-1]
|
||||
}
|
||||
}
|
||||
|
||||
return langs
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.google.com/p/go.net/html"
|
||||
)
|
||||
|
||||
var trans = make(map[string]string)
|
||||
var attrRe = regexp.MustCompile(`\{\{'([^']+)'\s+\|\s+translate\}\}`)
|
||||
|
||||
func generalNode(n *html.Node) {
|
||||
translate := false
|
||||
@@ -24,6 +26,10 @@ func generalNode(n *html.Node) {
|
||||
if a.Key == "translate" {
|
||||
translate = true
|
||||
break
|
||||
} else {
|
||||
if matches := attrRe.FindStringSubmatch(a.Val); len(matches) == 2 {
|
||||
translation(matches[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if n.Type == html.TextNode {
|
||||
@@ -44,12 +50,7 @@ func generalNode(n *html.Node) {
|
||||
|
||||
func inTranslate(n *html.Node) {
|
||||
if n.Type == html.TextNode {
|
||||
v := strings.TrimSpace(n.Data)
|
||||
if _, ok := trans[v]; !ok {
|
||||
av := strings.Replace(v, "{%", "{{", -1)
|
||||
av = strings.Replace(av, "%}", "}}", -1)
|
||||
trans[v] = av
|
||||
}
|
||||
translation(n.Data)
|
||||
} else {
|
||||
log.Println("translate node with non-text child <")
|
||||
log.Println(n)
|
||||
@@ -60,6 +61,15 @@ func inTranslate(n *html.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
func translation(v string) {
|
||||
v = strings.TrimSpace(v)
|
||||
if _, ok := trans[v]; !ok {
|
||||
av := strings.Replace(v, "{%", "{{", -1)
|
||||
av = strings.Replace(av, "%}", "}}", -1)
|
||||
trans[v] = av
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
fd, err := os.Open(os.Args[1])
|
||||
if err != nil {
|
||||
|
||||
@@ -110,7 +110,6 @@ type OptionsConfiguration struct {
|
||||
MaxSendKbps int `xml:"maxSendKbps"`
|
||||
RescanIntervalS int `xml:"rescanIntervalS" default:"60"`
|
||||
ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60"`
|
||||
MaxChangeKbps int `xml:"maxChangeKbps" default:"10000"`
|
||||
StartBrowser bool `xml:"startBrowser" default:"true"`
|
||||
UPnPEnabled bool `xml:"upnpEnabled" default:"true"`
|
||||
URAccepted int `xml:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
|
||||
|
||||
@@ -34,7 +34,6 @@ func TestDefaultValues(t *testing.T) {
|
||||
MaxSendKbps: 0,
|
||||
RescanIntervalS: 60,
|
||||
ReconnectIntervalS: 60,
|
||||
MaxChangeKbps: 10000,
|
||||
StartBrowser: true,
|
||||
UPnPEnabled: true,
|
||||
}
|
||||
@@ -189,7 +188,6 @@ func TestOverriddenValues(t *testing.T) {
|
||||
<maxSendKbps>1234</maxSendKbps>
|
||||
<rescanIntervalS>600</rescanIntervalS>
|
||||
<reconnectionIntervalS>6000</reconnectionIntervalS>
|
||||
<maxChangeKbps>2345</maxChangeKbps>
|
||||
<startBrowser>false</startBrowser>
|
||||
<upnpEnabled>false</upnpEnabled>
|
||||
</options>
|
||||
@@ -206,7 +204,6 @@ func TestOverriddenValues(t *testing.T) {
|
||||
MaxSendKbps: 1234,
|
||||
RescanIntervalS: 600,
|
||||
ReconnectIntervalS: 6000,
|
||||
MaxChangeKbps: 2345,
|
||||
StartBrowser: false,
|
||||
UPnPEnabled: false,
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ const (
|
||||
LocalIndexUpdated
|
||||
RemoteIndexUpdated
|
||||
ItemStarted
|
||||
ItemCompleted
|
||||
StateChanged
|
||||
|
||||
AllEvents = ^EventType(0)
|
||||
|
||||
103
files/leveldb.go
@@ -2,6 +2,7 @@ package files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
@@ -118,9 +119,11 @@ func globalKeyName(key []byte) []byte {
|
||||
|
||||
type deletionHandler func(db dbReader, batch dbWriter, repo, node, name []byte, dbi iterator.Iterator) uint64
|
||||
|
||||
type fileIterator func(f protocol.FileInfo) bool
|
||||
type fileIterator func(f protocol.FileIntf) bool
|
||||
|
||||
func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo, deleteFn deletionHandler) uint64 {
|
||||
defer runtime.GC()
|
||||
|
||||
sort.Sort(fileList(fs)) // sort list on name, same as on disk
|
||||
|
||||
start := nodeKey(repo, node, nil) // before all repo/node files
|
||||
@@ -178,7 +181,7 @@ func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo
|
||||
|
||||
case moreFs && moreDb && cmp == 0:
|
||||
// File exists on both sides - compare versions.
|
||||
var ef protocol.FileInfo
|
||||
var ef protocol.FileInfoTruncated
|
||||
ef.UnmarshalXDR(dbi.Value())
|
||||
if fs[fsi].Version > ef.Version {
|
||||
if lv := ldbInsert(batch, repo, node, newName, fs[fsi]); lv > maxLocalVer {
|
||||
@@ -223,20 +226,23 @@ func ldbReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint6
|
||||
|
||||
func ldbReplaceWithDelete(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64 {
|
||||
return ldbGenericReplace(db, repo, node, fs, func(db dbReader, batch dbWriter, repo, node, name []byte, dbi iterator.Iterator) uint64 {
|
||||
var f protocol.FileInfo
|
||||
err := f.UnmarshalXDR(dbi.Value())
|
||||
var tf protocol.FileInfoTruncated
|
||||
err := tf.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !protocol.IsDeleted(f.Flags) {
|
||||
if !tf.IsDeleted() {
|
||||
if debug {
|
||||
l.Debugf("mark deleted; repo=%q node=%v name=%q", repo, protocol.NodeIDFromBytes(node), name)
|
||||
}
|
||||
ts := clock(f.LocalVersion)
|
||||
f.Blocks = nil
|
||||
f.Version = lamport.Default.Tick(f.Version)
|
||||
f.Flags |= protocol.FlagDeleted
|
||||
f.LocalVersion = ts
|
||||
ts := clock(tf.LocalVersion)
|
||||
f := protocol.FileInfo{
|
||||
Name: tf.Name,
|
||||
Version: lamport.Default.Tick(tf.Version),
|
||||
LocalVersion: ts,
|
||||
Flags: tf.Flags | protocol.FlagDeleted,
|
||||
Modified: tf.Modified,
|
||||
}
|
||||
batch.Put(dbi.Key(), f.MarshalXDR())
|
||||
ldbUpdateGlobal(db, batch, repo, node, nodeKeyName(dbi.Key()), f.Version)
|
||||
return ts
|
||||
@@ -246,6 +252,8 @@ func ldbReplaceWithDelete(db *leveldb.DB, repo, node []byte, fs []protocol.FileI
|
||||
}
|
||||
|
||||
func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64 {
|
||||
defer runtime.GC()
|
||||
|
||||
batch := new(leveldb.Batch)
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
@@ -266,7 +274,7 @@ func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64
|
||||
continue
|
||||
}
|
||||
|
||||
var ef protocol.FileInfo
|
||||
var ef protocol.FileInfoTruncated
|
||||
err = ef.UnmarshalXDR(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -390,7 +398,7 @@ func ldbRemoveFromGlobal(db dbReader, batch dbWriter, repo, node, file []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func ldbWithHave(db *leveldb.DB, repo, node []byte, fn fileIterator) {
|
||||
func ldbWithHave(db *leveldb.DB, repo, node []byte, truncate bool, fn fileIterator) {
|
||||
start := nodeKey(repo, node, nil) // before all repo/node files
|
||||
limit := nodeKey(repo, node, []byte{0xff, 0xff, 0xff, 0xff}) // after all repo/node files
|
||||
snap, err := db.GetSnapshot()
|
||||
@@ -402,8 +410,7 @@ func ldbWithHave(db *leveldb.DB, repo, node []byte, fn fileIterator) {
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
var f protocol.FileInfo
|
||||
err := f.UnmarshalXDR(dbi.Value())
|
||||
f, err := unmarshalTrunc(dbi.Value(), truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -413,7 +420,9 @@ func ldbWithHave(db *leveldb.DB, repo, node []byte, fn fileIterator) {
|
||||
}
|
||||
}
|
||||
|
||||
func ldbWithAllRepo(db *leveldb.DB, repo []byte, fn func(node []byte, f protocol.FileInfo) bool) {
|
||||
func ldbWithAllRepoTruncated(db *leveldb.DB, repo []byte, fn func(node []byte, f protocol.FileInfoTruncated) bool) {
|
||||
defer runtime.GC()
|
||||
|
||||
start := nodeKey(repo, nil, nil) // before all repo/node files
|
||||
limit := nodeKey(repo, protocol.LocalNodeID[:], []byte{0xff, 0xff, 0xff, 0xff}) // after all repo/node files
|
||||
snap, err := db.GetSnapshot()
|
||||
@@ -426,7 +435,7 @@ func ldbWithAllRepo(db *leveldb.DB, repo []byte, fn func(node []byte, f protocol
|
||||
|
||||
for dbi.Next() {
|
||||
node := nodeKeyNode(dbi.Key())
|
||||
var f protocol.FileInfo
|
||||
var f protocol.FileInfoTruncated
|
||||
err := f.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -437,40 +446,6 @@ func ldbWithAllRepo(db *leveldb.DB, repo []byte, fn func(node []byte, f protocol
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func ldbCheckGlobalConsistency(db *leveldb.DB, repo []byte) {
|
||||
l.Debugf("Checking global consistency for %q", repo)
|
||||
start := nodeKey(repo, nil, nil) // before all repo/node files
|
||||
limit := nodeKey(repo, protocol.LocalNodeID[:], []byte{0xff, 0xff, 0xff, 0xff}) // after all repo/node files
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer snap.Release()
|
||||
dbi := snap.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
batch := new(leveldb.Batch)
|
||||
i := 0
|
||||
for dbi.Next() {
|
||||
repo := nodeKeyRepo(dbi.Key())
|
||||
node := nodeKeyNode(dbi.Key())
|
||||
var f protocol.FileInfo
|
||||
err := f.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if ldbUpdateGlobal(snap, batch, repo, node, []byte(f.Name), f.Version) {
|
||||
var nodeID protocol.NodeID
|
||||
copy(nodeID[:], node)
|
||||
l.Debugf("fixed global for %q %s %q", repo, nodeID, f.Name)
|
||||
}
|
||||
i++
|
||||
}
|
||||
l.Debugln("Done", i)
|
||||
}
|
||||
*/
|
||||
|
||||
func ldbGet(db *leveldb.DB, repo, node, file []byte) protocol.FileInfo {
|
||||
nk := nodeKey(repo, node, file)
|
||||
bs, err := db.Get(nk, nil)
|
||||
@@ -529,7 +504,9 @@ func ldbGetGlobal(db *leveldb.DB, repo, file []byte) protocol.FileInfo {
|
||||
return f
|
||||
}
|
||||
|
||||
func ldbWithGlobal(db *leveldb.DB, repo []byte, fn fileIterator) {
|
||||
func ldbWithGlobal(db *leveldb.DB, repo []byte, truncate bool, fn fileIterator) {
|
||||
defer runtime.GC()
|
||||
|
||||
start := globalKey(repo, nil)
|
||||
limit := globalKey(repo, []byte{0xff, 0xff, 0xff, 0xff})
|
||||
snap, err := db.GetSnapshot()
|
||||
@@ -556,8 +533,7 @@ func ldbWithGlobal(db *leveldb.DB, repo []byte, fn fileIterator) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var f protocol.FileInfo
|
||||
err = f.UnmarshalXDR(bs)
|
||||
f, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -596,7 +572,9 @@ func ldbAvailability(db *leveldb.DB, repo, file []byte) []protocol.NodeID {
|
||||
return nodes
|
||||
}
|
||||
|
||||
func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
|
||||
func ldbWithNeed(db *leveldb.DB, repo, node []byte, truncate bool, fn fileIterator) {
|
||||
defer runtime.GC()
|
||||
|
||||
start := globalKey(repo, nil)
|
||||
limit := globalKey(repo, []byte{0xff, 0xff, 0xff, 0xff})
|
||||
snap, err := db.GetSnapshot()
|
||||
@@ -638,13 +616,12 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var gf protocol.FileInfo
|
||||
err = gf.UnmarshalXDR(bs)
|
||||
gf, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if protocol.IsDeleted(gf.Flags) && !have {
|
||||
if gf.IsDeleted() && !have {
|
||||
// We don't need deleted files that we don't have
|
||||
continue
|
||||
}
|
||||
@@ -659,3 +636,15 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalTrunc(bs []byte, truncate bool) (protocol.FileIntf, error) {
|
||||
if truncate {
|
||||
var tf protocol.FileInfoTruncated
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
return tf, err
|
||||
} else {
|
||||
var tf protocol.FileInfo
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
return tf, err
|
||||
}
|
||||
}
|
||||
|
||||
29
files/set.go
@@ -36,7 +36,7 @@ func NewSet(repo string, db *leveldb.DB) *Set {
|
||||
}
|
||||
|
||||
var nodeID protocol.NodeID
|
||||
ldbWithAllRepo(db, []byte(repo), func(node []byte, f protocol.FileInfo) bool {
|
||||
ldbWithAllRepoTruncated(db, []byte(repo), func(node []byte, f protocol.FileInfoTruncated) bool {
|
||||
copy(nodeID[:], node)
|
||||
if f.LocalVersion > s.localVersion[nodeID] {
|
||||
s.localVersion[nodeID] = f.LocalVersion
|
||||
@@ -87,21 +87,42 @@ func (s *Set) WithNeed(node protocol.NodeID, fn fileIterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithNeed(%v)", s.repo, node)
|
||||
}
|
||||
ldbWithNeed(s.db, []byte(s.repo), node[:], fn)
|
||||
ldbWithNeed(s.db, []byte(s.repo), node[:], false, fn)
|
||||
}
|
||||
|
||||
func (s *Set) WithNeedTruncated(node protocol.NodeID, fn fileIterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithNeedTruncated(%v)", s.repo, node)
|
||||
}
|
||||
ldbWithNeed(s.db, []byte(s.repo), node[:], true, fn)
|
||||
}
|
||||
|
||||
func (s *Set) WithHave(node protocol.NodeID, fn fileIterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithHave(%v)", s.repo, node)
|
||||
}
|
||||
ldbWithHave(s.db, []byte(s.repo), node[:], fn)
|
||||
ldbWithHave(s.db, []byte(s.repo), node[:], false, fn)
|
||||
}
|
||||
|
||||
func (s *Set) WithHaveTruncated(node protocol.NodeID, fn fileIterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithHaveTruncated(%v)", s.repo, node)
|
||||
}
|
||||
ldbWithHave(s.db, []byte(s.repo), node[:], true, fn)
|
||||
}
|
||||
|
||||
func (s *Set) WithGlobal(fn fileIterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithGlobal()", s.repo)
|
||||
}
|
||||
ldbWithGlobal(s.db, []byte(s.repo), fn)
|
||||
ldbWithGlobal(s.db, []byte(s.repo), false, fn)
|
||||
}
|
||||
|
||||
func (s *Set) WithGlobalTruncated(fn fileIterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithGlobalTruncated()", s.repo)
|
||||
}
|
||||
ldbWithGlobal(s.db, []byte(s.repo), true, fn)
|
||||
}
|
||||
|
||||
func (s *Set) Get(node protocol.NodeID, file string) protocol.FileInfo {
|
||||
|
||||
@@ -37,7 +37,8 @@ func genBlocks(n int) []protocol.BlockInfo {
|
||||
|
||||
func globalList(s *files.Set) []protocol.FileInfo {
|
||||
var fs []protocol.FileInfo
|
||||
s.WithGlobal(func(f protocol.FileInfo) bool {
|
||||
s.WithGlobal(func(fi protocol.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfo)
|
||||
fs = append(fs, f)
|
||||
return true
|
||||
})
|
||||
@@ -46,7 +47,8 @@ func globalList(s *files.Set) []protocol.FileInfo {
|
||||
|
||||
func haveList(s *files.Set, n protocol.NodeID) []protocol.FileInfo {
|
||||
var fs []protocol.FileInfo
|
||||
s.WithHave(n, func(f protocol.FileInfo) bool {
|
||||
s.WithHave(n, func(fi protocol.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfo)
|
||||
fs = append(fs, f)
|
||||
return true
|
||||
})
|
||||
@@ -55,7 +57,8 @@ func haveList(s *files.Set, n protocol.NodeID) []protocol.FileInfo {
|
||||
|
||||
func needList(s *files.Set, n protocol.NodeID) []protocol.FileInfo {
|
||||
var fs []protocol.FileInfo
|
||||
s.WithNeed(n, func(f protocol.FileInfo) bool {
|
||||
s.WithNeed(n, func(fi protocol.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfo)
|
||||
fs = append(fs, f)
|
||||
return true
|
||||
})
|
||||
@@ -592,3 +595,64 @@ func TestLocalVersion(t *testing.T) {
|
||||
t.Fatal("Local version number should be unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
var gf protocol.FileInfo
|
||||
|
||||
func TestStressGlobalVersion(t *testing.T) {
|
||||
dur := 15 * time.Second
|
||||
if testing.Short() {
|
||||
dur = 1 * time.Second
|
||||
}
|
||||
|
||||
set1 := []protocol.FileInfo{
|
||||
protocol.FileInfo{Name: "a", Version: 1000},
|
||||
protocol.FileInfo{Name: "b", Version: 1000},
|
||||
}
|
||||
set2 := []protocol.FileInfo{
|
||||
protocol.FileInfo{Name: "b", Version: 1001},
|
||||
protocol.FileInfo{Name: "c", Version: 1000},
|
||||
}
|
||||
|
||||
db, err := leveldb.OpenFile("testdata/global.db", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m := files.NewSet("test", db)
|
||||
|
||||
done := make(chan struct{})
|
||||
go stressWriter(m, remoteNode, set1, nil, done)
|
||||
go stressWriter(m, protocol.LocalNodeID, set2, nil, done)
|
||||
|
||||
t0 := time.Now()
|
||||
for time.Since(t0) < dur {
|
||||
m.WithGlobal(func(f protocol.FileInfo) bool {
|
||||
gf = f
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
close(done)
|
||||
}
|
||||
|
||||
func stressWriter(s *files.Set, id protocol.NodeID, set1, set2 []protocol.FileInfo, done chan struct{}) {
|
||||
one := true
|
||||
i := 0
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
|
||||
default:
|
||||
if one {
|
||||
s.Replace(id, set1)
|
||||
} else {
|
||||
s.Replace(id, set2)
|
||||
}
|
||||
one = !one
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
46
gui/app.js
@@ -128,7 +128,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
$scope.$on('UIOffline', function (event, arg) {
|
||||
console.log('UIOffline');
|
||||
if (!restarting) {
|
||||
$('#networkError').modal({backdrop: 'static', keyboard: false});
|
||||
$('#networkError').modal();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -168,6 +168,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
OutBytesTotal: 0,
|
||||
Address: arg.data.addr,
|
||||
};
|
||||
$scope.completion[arg.data.id] = {
|
||||
_total: 100,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -185,7 +188,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
|
||||
} else {
|
||||
if (+firstVisit < Date.now() - 4*3600*1000){
|
||||
$('#ur').modal({backdrop: 'static', keyboard: false});
|
||||
$('#ur').modal();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,6 +287,11 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
|
||||
|
||||
$scope.nodes = $scope.config.Nodes;
|
||||
$scope.nodes.forEach(function (nodeCfg) {
|
||||
$scope.completion[nodeCfg.NodeID] = {
|
||||
_total: 100,
|
||||
};
|
||||
});
|
||||
$scope.nodes.sort(nodeCompare);
|
||||
|
||||
$scope.repos = repoMap($scope.config.Repositories);
|
||||
@@ -463,7 +471,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
$scope.tmpOptions = angular.copy($scope.config.Options);
|
||||
$scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
|
||||
$scope.tmpGUI = angular.copy($scope.config.GUI);
|
||||
$('#settings').modal({backdrop: 'static', keyboard: true});
|
||||
$('#settings').modal();
|
||||
};
|
||||
|
||||
$scope.saveConfig = function() {
|
||||
@@ -506,9 +514,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
|
||||
$scope.restart = function () {
|
||||
restarting = true;
|
||||
$scope.restartingTitle = "Restarting"
|
||||
$scope.restartingBody = "Syncthing is restarting."
|
||||
$('#restarting').modal({backdrop: 'static', keyboard: false});
|
||||
$('#restarting').modal();
|
||||
$http.post(urlbase + '/restart');
|
||||
$scope.configInSync = true;
|
||||
|
||||
@@ -529,21 +535,20 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
};
|
||||
|
||||
$scope.upgrade = function () {
|
||||
$scope.restartingTitle = "Upgrading"
|
||||
$scope.restartingBody = "Syncthing is upgrading."
|
||||
$('#restarting').modal({backdrop: 'static', keyboard: false});
|
||||
restarting = true;
|
||||
$('#upgrading').modal();
|
||||
$http.post(urlbase + '/upgrade').success(function () {
|
||||
restarting = true;
|
||||
$scope.restartingBody = "Syncthing is restarting into the new version."
|
||||
$('#restarting').modal();
|
||||
$('#upgrading').modal('hide');
|
||||
}).error(function () {
|
||||
$('#restarting').modal('hide');
|
||||
$('#upgrading').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.shutdown = function () {
|
||||
restarting = true;
|
||||
$http.post(urlbase + '/shutdown').success(function () {
|
||||
$('#shutdown').modal({backdrop: 'static', keyboard: false});
|
||||
$('#shutdown').modal();
|
||||
});
|
||||
$scope.configInSync = true;
|
||||
};
|
||||
@@ -554,7 +559,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
$scope.editingSelf = (nodeCfg.NodeID == $scope.myID);
|
||||
$scope.currentNode.AddressesStr = nodeCfg.Addresses.join(', ');
|
||||
$scope.nodeEditor.$setPristine();
|
||||
$('#editNode').modal({backdrop: 'static', keyboard: true});
|
||||
$('#editNode').modal();
|
||||
};
|
||||
|
||||
$scope.idNode = function () {
|
||||
@@ -566,7 +571,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
$scope.editingExisting = false;
|
||||
$scope.editingSelf = false;
|
||||
$scope.nodeEditor.$setPristine();
|
||||
$('#editNode').modal({backdrop: 'static', keyboard: true});
|
||||
$('#editNode').modal();
|
||||
};
|
||||
|
||||
$scope.deleteNode = function () {
|
||||
@@ -674,14 +679,14 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
$scope.currentRepo.simpleKeep = $scope.currentRepo.simpleKeep || 5;
|
||||
$scope.editingExisting = true;
|
||||
$scope.repoEditor.$setPristine();
|
||||
$('#editRepo').modal({backdrop: 'static', keyboard: true});
|
||||
$('#editRepo').modal();
|
||||
};
|
||||
|
||||
$scope.addRepo = function () {
|
||||
$scope.currentRepo = {selectedNodes: {}};
|
||||
$scope.editingExisting = false;
|
||||
$scope.repoEditor.$setPristine();
|
||||
$('#editRepo').modal({backdrop: 'static', keyboard: true});
|
||||
$('#editRepo').modal();
|
||||
};
|
||||
|
||||
$scope.saveRepo = function () {
|
||||
@@ -758,7 +763,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
|
||||
$scope.showNeed = function (repo) {
|
||||
$scope.neededLoaded = false;
|
||||
$('#needed').modal({backdrop: 'static', keyboard: true});
|
||||
$('#needed').modal();
|
||||
$http.get(urlbase + "/need?repo=" + encodeURIComponent(repo)).success(function (data) {
|
||||
$scope.needed = data;
|
||||
$scope.neededLoaded = true;
|
||||
@@ -790,6 +795,10 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
$('#about').modal('show');
|
||||
};
|
||||
|
||||
$scope.showReportPreview = function () {
|
||||
$scope.reportPreview = true;
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
setInterval($scope.refresh, 10000);
|
||||
});
|
||||
@@ -1035,7 +1044,6 @@ syncthing.directive('validNodeid', function($http) {
|
||||
if (resp.error) {
|
||||
ctrl.$setValidity('validNodeid', false);
|
||||
} else {
|
||||
scope.currentNode.NodeID = resp.id;
|
||||
ctrl.$setValidity('validNodeid', true);
|
||||
}
|
||||
});
|
||||
|
||||
BIN
gui/favicon.png
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -15,12 +15,17 @@
|
||||
|
||||
<title>Syncthing | {{thisNodeName()}}</title>
|
||||
<link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="raleway.css" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
body {
|
||||
padding-bottom: 70px;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
ul+h5 {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
@@ -95,7 +100,8 @@
|
||||
|
||||
<nav class="navbar navbar-top navbar-default" role="navigation">
|
||||
<div class="container">
|
||||
<span class="navbar-brand"><img class="logo" src="st-logo-128.png" width="32" height="32" /> Syncthing<small class="hidden-xs"> <span class="text-muted">|</span> {{thisNodeName()}}</small></span>
|
||||
<span class="navbar-brand"><img class="logo" src="logo-text-64.png" height="32" width="117"/></span>
|
||||
<p class="navbar-text hidden-xs">{{thisNodeName()}}</p>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li ng-if="upgradeInfo.newer">
|
||||
<button type="button" class="btn navbar-btn btn-default" href="" ng-click="upgrade()">
|
||||
@@ -153,7 +159,7 @@
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}">
|
||||
<span class="glyphicon glyphicon-hdd"></span> {{repo.Directory | shortPath}}
|
||||
<span class="glyphicon glyphicon-hdd"></span> {{repo.ID}}
|
||||
<span class="pull-right hidden-xs" ng-switch="repoStatus(repo.ID)">
|
||||
<span translate ng-switch-when="unknown">Unknown</span>
|
||||
<span translate ng-switch-when="stopped">Stopped</span>
|
||||
@@ -189,16 +195,16 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-globe"></span> <span translate>Global Repository</span></th>
|
||||
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} <span translate>items</span>, {{model[repo.ID].globalBytes | binary}}B</td>
|
||||
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].globalBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-home"></span> <span translate>Local Repository</span></th>
|
||||
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} <span translate>items</span>, {{model[repo.ID].localBytes | binary}}B</td>
|
||||
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].localBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Out Of Sync</span></th>
|
||||
<td class="text-right">
|
||||
<a ng-if="model[repo.ID].needFiles > 0" ng-click="showNeed(repo.ID)" href="">{{model[repo.ID].needFiles | alwaysNumber}} <span translate>items</span>, {{model[repo.ID].needBytes | binary}}B</a>
|
||||
<a ng-if="model[repo.ID].needFiles > 0" ng-click="showNeed(repo.ID)" href="">{{model[repo.ID].needFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].needBytes | binary}}B</a>
|
||||
<span ng-if="model[repo.ID].needFiles == 0">0 <span translate>items</span>, 0 B</span>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -385,8 +391,14 @@
|
||||
|
||||
<!-- Restarting modal -->
|
||||
|
||||
<modal id="restarting" icon="refresh" title="{{restartingTitle}}" status="info">
|
||||
<p>{{restartingBody}} <span translate>Please wait</span>…</p>
|
||||
<modal id="restarting" icon="refresh" title="{{'Restarting' | translate}}" status="info">
|
||||
<p><span translate>Syncthing is restarting.</span> <span translate>Please wait</span>...</p>
|
||||
</modal>
|
||||
|
||||
<!-- Upgrading modal -->
|
||||
|
||||
<modal id="upgrading" icon="refresh" title="{{'Upgrading' | translate}}" status="info">
|
||||
<p><span translate>Syncthing is upgrading.</span> <span translate>Please wait</span>...</p>
|
||||
</modal>
|
||||
|
||||
<!-- Shutdown modal -->
|
||||
@@ -399,12 +411,12 @@
|
||||
|
||||
<modal id="idqr" large="yes" status="info" close="yes" icon="qrcode" title="{{'Node Identification' | translate}} — {{nodeName(thisNode())}}">
|
||||
<div class="well well-sm text-monospace text-center">{{myID}}</div>
|
||||
<img ng-if="myID" class="center-block img-thumbnail" src="qr/{{myID}}"/>
|
||||
<img ng-if="myID" class="center-block img-thumbnail" src="qr/?text={{myID}}"/>
|
||||
</modal>
|
||||
|
||||
<!-- Node editor modal -->
|
||||
|
||||
<div id="editNode" class="modal fade">
|
||||
<div id="editNode" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -421,7 +433,7 @@
|
||||
<span translate ng-if="nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine">The node ID to enter here can be found in the "Edit > Show ID" dialog on the other node. Spaces and dashes are optional (ignored).</span>
|
||||
<span translate ng-show="!editingExisting && (nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine)">When adding a new node, keep in mind that this node must be added on the other side too.</span>
|
||||
<span translate ng-if="nodeEditor.nodeID.$error.required && nodeEditor.nodeID.$dirty">The node ID cannot be blank.</span>
|
||||
<span translate ng-if="nodeEditor.nodeID.$error.validNodeid && nodeEditor.nodeID.$dirty">The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.</span>
|
||||
<span translate ng-if="nodeEditor.nodeID.$error.validNodeid && nodeEditor.nodeID.$dirty">The entered node 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.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -454,7 +466,7 @@
|
||||
|
||||
<!-- Repo editor modal -->
|
||||
|
||||
<div id="editRepo" class="modal fade">
|
||||
<div id="editRepo" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -548,7 +560,7 @@
|
||||
|
||||
<!-- Settings modal -->
|
||||
|
||||
<div id="settings" class="modal fade">
|
||||
<div id="settings" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -674,7 +686,7 @@
|
||||
|
||||
<!-- Usage report modal -->
|
||||
|
||||
<div id="ur" class="modal fade">
|
||||
<div id="ur" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-success">
|
||||
@@ -682,8 +694,8 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
|
||||
<p translate translate-value-url="https://data.syncthing.net">The aggregated statistics are publicly available at {{url}}</p>
|
||||
<button translate type="button" class="btn btn-default" ng-show="!reportPreview" ng-click="reportPreview = true">Preview Usage Report</button>
|
||||
<p translate translate-value-url="https://data.syncthing.net">The aggregated statistics are publicly available at {%url%}.</p>
|
||||
<button translate type="button" class="btn btn-default" ng-show="!reportPreview" ng-click="showReportPreview()">Preview Usage Report</button>
|
||||
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -709,7 +721,7 @@
|
||||
<!-- About modal -->
|
||||
|
||||
<modal id="about" large="yes" close="yes" status="info" title="About">
|
||||
<h1 class="text-center"><img src="st-logo-128.png" style="vertical-align: -16px" width="64" height="64"/> Syncthing<br/><small>{{version}}</small></h1>
|
||||
<h1 class="text-center"><img alt="Syncthing" title="Syncthing" src="logo-text-256.png" style="vertical-align: -16px" height="100" width="366"/><br/><small>{{version}}</small></h1>
|
||||
<hr/>
|
||||
|
||||
<p translate>Copyright © 2014 Jakob Borg and the following Contributors:</p>
|
||||
@@ -722,6 +734,7 @@
|
||||
<li>Audrius Butkevicius</li>
|
||||
<li>Ben Sidhom</li>
|
||||
<li>Brandon Philips</li>
|
||||
<li>Gilli Sigurdsson</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@@ -730,6 +743,7 @@
|
||||
<li>Jens Diemer</li>
|
||||
<li>Philippe Schommers</li>
|
||||
<li>Ryan Sullivan</li>
|
||||
<li>Tully Robinson</li>
|
||||
<li>Veeti Paananen</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
120
gui/lang-da.json
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"API Key": "API-nøgle",
|
||||
"About": "Om",
|
||||
"Add Node": "Tilføj node",
|
||||
"Add Repository": "Tilføj lager",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresser",
|
||||
"Allow Anonymous Usage Reporting?": "Tillad anonym brugerstatistik?",
|
||||
"Announce Server": "Opslagsserver",
|
||||
"Anonymous Usage Reporting": "Anonym brugerstatistik",
|
||||
"Bugs": "Fejl",
|
||||
"CPU Utilization": "CPU-forbrug",
|
||||
"Close": "Luk",
|
||||
"Connection Error": "Tilslutnings fejl",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg og følgende bidragsydere:",
|
||||
"Delete": "Slet",
|
||||
"Disconnected": "Ikke tilsluttet",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Downloadhastighed",
|
||||
"Edit": "Rediger",
|
||||
"Edit Node": "Rediger node",
|
||||
"Edit Repository": "Rediger lager",
|
||||
"Enable UPnP": "Anvend UPnP",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv kommaseparerat \"ip:port\"-adresser eller ordet \"dynamic\" for at benytte automatisk opslag.",
|
||||
"Error": "Fejl",
|
||||
"File Versioning": "Filversionering",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Filrettigheder tages der ikke hensyn til ved synkronisering. Anvend på FAT-filsystemer.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Filer flyttes til et datostemplet versionsnavn i et .stversions-bibliotek, når de bliver opdateret eller slettet af syncthing.",
|
||||
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Filer beskyttes mod ændringer fra andre noder, men ændringer på denne node bliver sendt til hele clusteret.",
|
||||
"Folder": "Bibliotek",
|
||||
"GUI Authentication Password": "GUI-kodeord",
|
||||
"GUI Authentication User": "GUI-brugernavn",
|
||||
"GUI Listen Addresses": "GUI-lytteadresse",
|
||||
"Generate": "Opret",
|
||||
"Global Discovery": "Globalt opslag",
|
||||
"Global Discovery Server": "Global opslagsserver",
|
||||
"Global Repository": "Global lagring",
|
||||
"Idle": "Inaktiv",
|
||||
"Ignore Permissions": "Ignorér filrettigheder",
|
||||
"Keep Versions": "Behold versioner",
|
||||
"Latest Release": "Seneste udgivelse",
|
||||
"Local Discovery": "Lokal opslag",
|
||||
"Local Discovery Port": "Lokal opslagsport",
|
||||
"Local Repository": "Lokal lagring",
|
||||
"Master Repo": "Hovedlagring",
|
||||
"Max File Change Rate (KiB/s)": "Højeste filændringshastighed (KiB/s)",
|
||||
"Max Outstanding Requests": "Parallelitet",
|
||||
"No": "Nej",
|
||||
"Node ID": "Node ID",
|
||||
"Node Identification": "Node identifikation",
|
||||
"Node Name": "Nodenavn",
|
||||
"Notice": "OBS",
|
||||
"OK": "OK",
|
||||
"Offline": "Offline",
|
||||
"Online": "Online",
|
||||
"Out Of Sync": "Ude af sync",
|
||||
"Outgoing Rate Limit (KiB/s)": "Udgående hastighedsbegrænsning (KiB/s)",
|
||||
"Override Changes": "Overskriv ændringer",
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sti til lagring på din lokale computer. Hvis biblioteket ikke findes vil det blive oprettet. Tegnet tilde (~) kan bruges som genvej til",
|
||||
"Please wait": "Vent venligst",
|
||||
"Preview Usage Report": "Forhåndsvisning af forbrugsrapport",
|
||||
"RAM Utilization": "RAM-forbrug",
|
||||
"Reconnect Interval (s)": "Gentilslutningsinterval (s)",
|
||||
"Repository ID": "Lagrings-ID",
|
||||
"Repository Master": "Hovedlagring",
|
||||
"Repository Path": "Sti til lagring",
|
||||
"Rescan Interval (s)": "Genscanningsinterval (s)",
|
||||
"Restart": "Genstart",
|
||||
"Restart Needed": "Programmet kræver genstart",
|
||||
"Restarting": "Genstarter",
|
||||
"Save": "Gem",
|
||||
"Scanning": "Opdaterer",
|
||||
"Select the nodes to share this repository with.": "Vælg hvilke noder denne lagring skal deles med.",
|
||||
"Settings": "Indstillinger",
|
||||
"Share With Nodes": "Del med noderne",
|
||||
"Shared With": "Delt med",
|
||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Kort identifikation for denne lagring. Skal være ens på alle noder i clusteret.",
|
||||
"Show ID": "Vis ID",
|
||||
"Shown instead of Node ID in the cluster status.": "Vises i stedet for node-ID under clusterstatus.",
|
||||
"Shutdown": "Luk ned",
|
||||
"Source Code": "Kildekode",
|
||||
"Start Browser": "Start browser",
|
||||
"Stopped": "Stoppet",
|
||||
"Support / Forum": "Support / Forum",
|
||||
"Sync Protocol Listen Addresses": "Lytteadresser for indgående forbindelser",
|
||||
"Synchronization": "Synkronisering",
|
||||
"Syncing": "Synkroniserer",
|
||||
"Syncthing has been shut down.": "Syncthing er blevet lukket ned.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing indeholder følgende software eller dele heraf:",
|
||||
"Syncthing is restarting.": "Syncthing genstarter",
|
||||
"Syncthing is upgrading.": "Syncthing opgradere",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ud til at være stoppet eller oplever problemer med din internetforbindels. Prøver igen...",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Samlet statistik er offentligt tilgængelig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen er gemt, men ikke aktiveret. Syncthing skal genstarte for at aktivere den nye konfiguration.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterede forbrugsrapport sendes dagligt. Den benyttes til at spore anvendte platforme, lagringsstørrelser og versioner. Hvis det typen af opsamlet data ændres på et senere tidspunkt, vil du blive spurgt om tilladelse igen.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det indtastede node ID ser ikke gyldigt ud. Det skal være en 52 tegn streng, bestående af tal og bogstaver, eventuelt med mellemrum og bindestreger.",
|
||||
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det indtastede node ID ser ikke gyldigt ud. Det skal være en 52 eller 56 tegn streng, bestående af tal og bogstaver, eventuelt med mellemrum og bindestreger.",
|
||||
"The node ID cannot be blank.": "Node-ID'et kan ikke være blankt.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Node-ID'et som skal bruges her, kan du finde i \"Rediger > Vis ID\"-dialogen på den anden node. Mellemrum og bindestreg er valgfri (ignoreres).",
|
||||
"The number of old versions to keep, per file.": "Antallet af gamle versioner som gemmes, per fil.",
|
||||
"The number of versions must be a number and cannot be blank.": "Antallet af versioner skal være et tal, og kan ikke være blankt.",
|
||||
"The repository ID cannot be blank.": "Lagrings-ID kan ikke være blankt.",
|
||||
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Lagrings-ID'et skal være en kort identificierende streng (64 karaktere eller mindre) bestående af bogstav-, tal-, punktum- (.), bindestreg- (-) og understregskaraktere (_).",
|
||||
"The repository ID must be unique.": "Lagrings-ID'et skal være unikt.",
|
||||
"The repository path cannot be blank.": "Lagringsstien kan ikke være blank.",
|
||||
"Unknown": "Ukendt",
|
||||
"Up to Date": "Fuldt opdateret",
|
||||
"Upgrade To {%version%}": "Opgradér til {{version}}",
|
||||
"Upgrading": "Opgradere",
|
||||
"Upload Rate": "Uploadhastighed",
|
||||
"Usage": "Forbrug",
|
||||
"Use Compression": "Anvend komprimering",
|
||||
"Use HTTPS for GUI": "Anvend HTTPS til GUI adgang",
|
||||
"Version": "Version",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Når du tilføjer en ny node skal du huske, at den også skal tilføjes på den anden side.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Når du tilføjer en ny node skal du huske, at lagrings-ID'et bliver brugt til at knytte noder sammen. De er følsomme for store og små bogstaver og skal matche på alle noder.",
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du skal beholde mindst én version.",
|
||||
"items": "poster"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"API Key": "API-Schlüssel",
|
||||
"About": "Über",
|
||||
"API Key": "API-Key",
|
||||
"About": "Über Syncthing",
|
||||
"Add Node": "Knoten hinzufügen",
|
||||
"Add Repository": "Verzeichnis hinzufügen",
|
||||
"Address": "Adresse",
|
||||
@@ -11,6 +11,7 @@
|
||||
"Bugs": "Fehler",
|
||||
"CPU Utilization": "Prozessorauslastung",
|
||||
"Close": "Schließen",
|
||||
"Connection Error": "Verbindungsfehler",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg und folgende Unterstützer:",
|
||||
"Delete": "Löschen",
|
||||
"Disconnected": "Verbindung getrennt",
|
||||
@@ -19,18 +20,18 @@
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit Node": "Knoten bearbeiten",
|
||||
"Edit Repository": "Verzeichnis ändern",
|
||||
"Enable UPnP": "Aktiviere UPnP",
|
||||
"Enable UPnP": "UPnP aktivieren",
|
||||
"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.",
|
||||
"Error": "Fehler",
|
||||
"File Versioning": "Dateiversionierung",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Dateizugriffsrechte beim Suchen nach Veränderungen ignorieren. Bei FAT-Dateisystemen verwenden.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Dateien werden beim Löschen oder Ersetzen als datierte Versionen in einen .stversions -Ordner verschoben.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Dateien werden, bevor syncthing sie löscht oder ersetzt, als datierte Versionen in einen Ordner names .stversions verschoben.",
|
||||
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Dateien sind vor Veränderung durch andere Knoten geschützt, auf diesem Knoten durchgeführte Veränderungen werden aber auf den Rest des Netzwerks übertragen.",
|
||||
"Folder": "Ordner",
|
||||
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
|
||||
"GUI Authentication User": "Nutzername für Zugang zur Benutzeroberfläche.",
|
||||
"GUI Listen Addresses": "Adresse(n) für die Benutzeroberfläche",
|
||||
"Generate": "Generiere",
|
||||
"Generate": "Generieren",
|
||||
"Global Discovery": "Globale Auffindung",
|
||||
"Global Discovery Server": "Globaler Auffindungsserver",
|
||||
"Global Repository": "Globales Verzeichnis",
|
||||
@@ -41,11 +42,12 @@
|
||||
"Local Discovery": "Lokale Auffindung",
|
||||
"Local Discovery Port": "Lokaler Aufindungsport",
|
||||
"Local Repository": "Lokales Verzeichnis",
|
||||
"Master Repo": "Keine Veränderungen zugelassen",
|
||||
"Master Repo": "Originalverzeichnis",
|
||||
"Max File Change Rate (KiB/s)": "Maximale Datenänderungsrate (KiB/s)",
|
||||
"Max Outstanding Requests": "Max. ausstehende Anfragen",
|
||||
"No": "Nein",
|
||||
"Node ID": "Knoten-ID",
|
||||
"Node Identification": "Knoten Identifikation",
|
||||
"Node Name": "Knoten-Name",
|
||||
"Notice": "Benachrichtigung",
|
||||
"OK": "Ok",
|
||||
@@ -60,13 +62,14 @@
|
||||
"RAM Utilization": "Verwendeter Arbeitsspeicher",
|
||||
"Reconnect Interval (s)": "Wiederverbindungsintervall (s)",
|
||||
"Repository ID": "Verzeichnis-ID",
|
||||
"Repository Master": "Keine Veränderungen zulassen",
|
||||
"Repository Master": "Originalverzeichnis",
|
||||
"Repository Path": "Pfad zum Verzeichnis",
|
||||
"Rescan Interval (s)": "Suchintervall (s)",
|
||||
"Restart": "Neustart",
|
||||
"Restart Needed": "Neustart notwendig",
|
||||
"Restarting": "Wird neu gestartet",
|
||||
"Save": "Speichern",
|
||||
"Scanning": "Überprüfe",
|
||||
"Scanning": "Sucht",
|
||||
"Select the nodes to share this repository with.": "Wähle die Knoten aus, mit denen du dieses Verzeichnis teilen willst.",
|
||||
"Settings": "Einstellungen",
|
||||
"Share With Nodes": "Teile mit diesen Knoten",
|
||||
@@ -75,7 +78,7 @@
|
||||
"Show ID": "ID anzeigen",
|
||||
"Shown instead of Node ID in the cluster status.": "Wird anstatt der Knoten-ID im Verbunds-Status angezeigt.",
|
||||
"Shutdown": "Herunterfahren",
|
||||
"Source Code": "Quellcode",
|
||||
"Source Code": "Sourcecode",
|
||||
"Start Browser": "Starte Browser",
|
||||
"Stopped": "Gestoppt",
|
||||
"Support / Forum": "Support / Forum",
|
||||
@@ -84,15 +87,18 @@
|
||||
"Syncing": "Synchronisiere",
|
||||
"Syncthing has been shut down.": "Syncthing wurde heruntergefahren.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing enthält die folgende Software oder Teile davon:",
|
||||
"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 Ihrer Internetverbindung. Versuche erneut...",
|
||||
"The aggregated statistics are publicly available at {{url}}": "Die aggregierten Statistiken sind öffentlich verfügbar unter {{url}}",
|
||||
"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 encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Der verschlüsselte Benutzungsbericht wird täglich gesendet. Er wird benutzt um Statistiken über verwendete Betriebssysteme, Verzeichnis-Größen und Programm-Versionen zu erstellen. Sobald der Bericht in Zukunft weitere Daten erfasst, wird dir dieses Fenster erneut angezeigt.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Die eingegebene Knoten-ID scheint nicht gültig zu sein. Sie sollte eine 52 Stellen lange Zeichenkette aus Buchstaben und Zahlen sein. Leerzeichen und Striche sind optional (werden ignoriert).",
|
||||
"The entered node 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 Knoten-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 node ID cannot be blank.": "Die Knoten-ID darf nicht leer sein.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Die hier einzutragende Knoten-ID kann im \"Bearbeiten > Zeige ID\"-Dialog auf dem anderen Knoten gefunden werden. Leerzeichen und Striche sind optional (werden ignoriert).",
|
||||
"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 sein und darf nicht leer sein.",
|
||||
"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 repository ID cannot be blank.": "Die Verzeichnis-ID darf nicht leer sein.",
|
||||
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis-ID muss eine kurze Kennung (64 Zeichen oder weniger) sein. Sie kann aus Buchstaben, Zahlen und den Punkt- (.), Strich- (-), und Unterstrich- (_) Zeichen bestehen.",
|
||||
"The repository ID must be unique.": "Die Verzeichnis-ID muss eindeutig sein.",
|
||||
@@ -100,6 +106,7 @@
|
||||
"Unknown": "Unbekannt",
|
||||
"Up to Date": "Aktuell",
|
||||
"Upgrade To {%version%}": "Upgrade auf {{version}}",
|
||||
"Upgrading": "Wird aktualisiert",
|
||||
"Upload Rate": "Uploadgeschwindigkeit",
|
||||
"Usage": "Benutzung",
|
||||
"Use Compression": "Benutze Komprimierung",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"API Key": "Κλειδί API",
|
||||
"About": "Σχετικά",
|
||||
"Add Node": "Πρόσθεσε Κόμβο",
|
||||
"Add Repository": "Πρόσθεσε Αποθετήριο",
|
||||
"Add Node": "Προσθήκη Κόμβου",
|
||||
"Add Repository": "Προσθήκη Αποθετηρίου",
|
||||
"Address": "Διεύθυνση",
|
||||
"Addresses": "Διευθύνσεις",
|
||||
"Allow Anonymous Usage Reporting?": "Να επιτρέπεται Ανώνυμη Αποστολή Αναφοράς Χρήσης?",
|
||||
@@ -11,6 +11,7 @@
|
||||
"Bugs": "Bugs",
|
||||
"CPU Utilization": "Χρήση CPU",
|
||||
"Close": "Τέλος",
|
||||
"Connection Error": "Σφάλμα Σύνδεσης",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg και οι παρακάτω Συνεισφορείς:",
|
||||
"Delete": "Διαγραφή",
|
||||
"Disconnected": "Αποσυνδεδεμένος",
|
||||
@@ -27,46 +28,48 @@
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Όταν τα αρχεία αντικατασταθούν ή διαγραφούν από το syncthing, μεταφέρονται σε φάκελο .stversions με χρονική σήμανση.",
|
||||
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Τα αρχεία προστατεύονται από αλλαγές που γίνονται σε άλλους κόμβους, αλλά όποιες αλλαγές γίνουν εδώ θα αποσταλούν στο όλο το cluster.",
|
||||
"Folder": "Κατάλογος",
|
||||
"GUI Authentication Password": "GUI Authentication Password",
|
||||
"GUI Authentication User": "GUI Authentication User",
|
||||
"GUI Listen Addresses": "GUI Listen Addresses",
|
||||
"GUI Authentication Password": "Κωδικός πιστοποίησης στο GUI",
|
||||
"GUI Authentication User": "Χρήστης πιστοποίησης στο GUI",
|
||||
"GUI Listen Addresses": "GUI Listen διευθύνσεις",
|
||||
"Generate": "Δημιουργία",
|
||||
"Global Discovery": "Global Discovery",
|
||||
"Global Discovery Server": "Global Discovery Server",
|
||||
"Global Discovery Server": "Διακομιστής Ανεύρεσης Κόμβου",
|
||||
"Global Repository": "Global Repository",
|
||||
"Idle": "Ανενεργός",
|
||||
"Ignore Permissions": "Ignore Permissions",
|
||||
"Keep Versions": "Keep Versions",
|
||||
"Idle": "Ανενεργό",
|
||||
"Ignore Permissions": "Αγνόησε Δικαιώματα",
|
||||
"Keep Versions": "Διατήρησε Εκδόσεις",
|
||||
"Latest Release": "Τελευταία Έκδοση",
|
||||
"Local Discovery": "Local Discovery",
|
||||
"Local Discovery Port": "Local Discovery Port",
|
||||
"Local Discovery": "Τοπική Ανεύρεση",
|
||||
"Local Discovery Port": "Port Τοπικής Ανεύρεσης",
|
||||
"Local Repository": "Τοπικό Αποθετήριο",
|
||||
"Master Repo": "Master Repo",
|
||||
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
|
||||
"Max Outstanding Requests": "Max Outstanding Requests",
|
||||
"No": "Αριθμός",
|
||||
"Node ID": "ID Κόμβου",
|
||||
"Node Identification": "Ταυτοποίηση Κόμβου",
|
||||
"Node Name": "Όνομα Κόμβου",
|
||||
"Notice": "Notice",
|
||||
"OK": "OK",
|
||||
"Offline": "Ανεργός",
|
||||
"Offline": "Ανεργό",
|
||||
"Online": "Ενεργός",
|
||||
"Out Of Sync": "Εκτός Συγχρονισμού",
|
||||
"Out Of Sync": "Μη Συγχρονισμένα",
|
||||
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
|
||||
"Override Changes": "Override Changes",
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Μονοπάτι του αποθετηρίου στον τοπικό υπολογιστή. Σε περίπτωση που δεν υπάρχει, θα δημιουργηθεί. Ο χαρακτήρας tilde (~) μπορεί να χρησιμοποιηθεί σαν συντόμευση για",
|
||||
"Please wait": "Παρακαλώ περιμένετε",
|
||||
"Preview Usage Report": "Preview Usage Report",
|
||||
"Preview Usage Report": "Προεπισκόπηση αναφοράς χρήσης",
|
||||
"RAM Utilization": "Χρήση RAM",
|
||||
"Reconnect Interval (s)": "Reconnect Interval (s)",
|
||||
"Reconnect Interval (s)": "Χρονικό διάστημα επανασύνδεσης (s)",
|
||||
"Repository ID": "ID Αποθετηρίου",
|
||||
"Repository Master": "Repository Master",
|
||||
"Repository Path": "Μονοπάτι Αποθετηρίου",
|
||||
"Rescan Interval (s)": "Rescan Interval (s)",
|
||||
"Rescan Interval (s)": "Χρονικό διάστημα Επανασάρρωσης (s)",
|
||||
"Restart": "Επανεκκίνηση",
|
||||
"Restart Needed": "Απαιτείται Επανεκκίνηση",
|
||||
"Restarting": "Επανεκκίνηση",
|
||||
"Save": "Αποθήκευση",
|
||||
"Scanning": "Scanning",
|
||||
"Scanning": "Σάρρωση",
|
||||
"Select the nodes to share this repository with.": "Επιλογή των κόμβων με τους οποίους θα διαμοιραστεί αυτό το αποθετήριο.",
|
||||
"Settings": "Ρυθμίσεις",
|
||||
"Share With Nodes": "Διαμοιρασμός με Κόμβους",
|
||||
@@ -76,36 +79,40 @@
|
||||
"Shown instead of Node ID in the cluster status.": "Εμφάνιση στη θέση του ID Αποθετηρίου, στην κατάσταση του cluster.",
|
||||
"Shutdown": "Απενεργοποίηση",
|
||||
"Source Code": "Πηγαίος Κώδικας",
|
||||
"Start Browser": "Start Browser",
|
||||
"Start Browser": "Έναρξη Φυλλομετρητή",
|
||||
"Stopped": "Απενεργοποιημένο",
|
||||
"Support / Forum": "Υποστήριξη / Forum",
|
||||
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
|
||||
"Synchronization": "Συγχρονισμός",
|
||||
"Syncing": "Syncing",
|
||||
"Syncthing has been shut down.": "Syncthing έχει απενεργοποιηθεί.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing includes the following software or portions thereof:",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
|
||||
"The aggregated statistics are publicly available at {{url}}": "The aggregated statistics are publicly available at {{url}}",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.",
|
||||
"The node ID cannot be blank.": "The node ID cannot be blank.",
|
||||
"Syncing": "Συγχρονισμός",
|
||||
"Syncthing has been shut down.": "Το Syncthing έχει απενεργοποιηθεί.",
|
||||
"Syncthing includes the following software or portions thereof:": "Το Syncthing συμπεριλαμβάνει τα παρακάτω λογισμικά ή μέρη αυτών:",
|
||||
"Syncthing is restarting.": "Το Syncthing επανεκκινεί.",
|
||||
"Syncthing is upgrading.": "Το Syncthing αναβαθμίζεται.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Το Syncthing φαίνεται πως είναι απενεργοποιημένο ή υπάρχει πρόβλημα στη σύνδεσή σας στο Internet. Προσπάθεια ξανά…",
|
||||
"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 encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Η κρυπτογραφημένη αναφοράς χρήσης στέλνεται καθημερινά. Χρησιμοποιείται ανίχνευση πληροφοριών πλατφόρμας, μεγέθους αποθετηρίων και εκδόσεων της εφαρμογής. Αν τα δεδομένα που αποστέλονται αλλάξουν, θα πληροφορηθείτε ξανά με αυτό το διάλογο.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Το ID Κόμβου που έχει εισαχθεί δεν είναι σωστό. Θα πρέπει να είναι αλφαριθμητικό 52 χαρακτήρων που να αποτελείται από γράμματα και αριθμούς, όπου τα κενά και οι παύλες είναι προαιρετικά.",
|
||||
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Το ID Κόμβου δεν είναι έγκυρο. Θα πρέπει να είναι αλφαριθμιτικό με 52 ή 56 χαρακτήρες και να αποτελείται από γράμματα και αριθμούς, που προαιρετικά χωρίζονται με κενά και παύλες.",
|
||||
"The node ID cannot be blank.": "Το ID Κόμβου δε μπορεί να είναι κενό.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Το ID Κόμβου μπορείτε να βρείτε στο μενού \"Επεξεργασία > Εμφάνιση ID\" του άλλου κόμβου. Κενά και παύλες είναι προαιρετικά (αγνοούνται).",
|
||||
"The number of old versions to keep, per file.": "The number of old versions to keep, per file.",
|
||||
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
|
||||
"The repository ID cannot be blank.": "The repository ID cannot be blank.",
|
||||
"The number of versions must be a number and cannot be blank.": "Ο αριθμός εκδόσεων πρέπει να είναι αριθμός και σίγουρα όχι κενό.",
|
||||
"The repository ID cannot be blank.": "Το ID Αποθετηρίου δε μπορεί να είναι κενό.",
|
||||
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.",
|
||||
"The repository ID must be unique.": "The repository ID must be unique.",
|
||||
"The repository path cannot be blank.": "The repository path cannot be blank.",
|
||||
"The repository ID must be unique.": "Το ID Αποθετηρίου πρέπει να είναι μοναδικό.",
|
||||
"The repository path cannot be blank.": "Το μονοπάτι του αποθετηρίου δε μπορεί να είναι κενό.",
|
||||
"Unknown": "Άγνωστο",
|
||||
"Up to Date": "Ενημερώμενο",
|
||||
"Up to Date": "Ενημερωμένος",
|
||||
"Upgrade To {%version%}": "Αναβάθμιση στην έκδοση {{version}}",
|
||||
"Upgrading": "Αναβάθμιση",
|
||||
"Upload Rate": "Upload Rate",
|
||||
"Usage": "Usage",
|
||||
"Use Compression": "Χρήση συμπίεσης",
|
||||
"Use HTTPS for GUI": "Use HTTPS for GUI",
|
||||
"Use HTTPS for GUI": "Χρήση HTTPS για το GUI",
|
||||
"Version": "Έκδοση",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "When adding a new node, keep in mind that this node must be added on the other side too.",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Προσθέτοντας έναν καινούργιο κόμβο, θυμηθείται πως θα πρέπει να προσθέσετε και τον παρόν κόμβο στην άλλη πλευρά.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Κατά την πρόσθεση νέου αποθετηρίου, να γνωρίζεται πως το ID Αποθετηρίου χρησιμοποιείται για να συνδέει Αποθετήρια μεταξύ κόμβων. Τα ID είναι case sensitive και θα πρέπει να είναι ταυτόσημα μεταξύ όλων των κόμβων.",
|
||||
"Yes": "Ναι",
|
||||
"You must keep at least one version.": "You must keep at least one version.",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"Bugs": "Bugs",
|
||||
"CPU Utilization": "CPU Utilization",
|
||||
"Close": "Close",
|
||||
"Connection Error": "Connection Error",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
|
||||
"Delete": "Delete",
|
||||
"Disconnected": "Disconnected",
|
||||
@@ -46,6 +47,7 @@
|
||||
"Max Outstanding Requests": "Max Outstanding Requests",
|
||||
"No": "No",
|
||||
"Node ID": "Node ID",
|
||||
"Node Identification": "Node Identification",
|
||||
"Node Name": "Node Name",
|
||||
"Notice": "Notice",
|
||||
"OK": "OK",
|
||||
@@ -65,6 +67,7 @@
|
||||
"Rescan Interval (s)": "Rescan Interval (s)",
|
||||
"Restart": "Restart",
|
||||
"Restart Needed": "Restart Needed",
|
||||
"Restarting": "Restarting",
|
||||
"Save": "Save",
|
||||
"Scanning": "Scanning",
|
||||
"Select the nodes to share this repository with.": "Select the nodes to share this repository with.",
|
||||
@@ -84,11 +87,14 @@
|
||||
"Syncing": "Syncing",
|
||||
"Syncthing has been shut down.": "Syncthing has been shut down.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing includes the following software or portions thereof:",
|
||||
"Syncthing is restarting.": "Syncthing is restarting.",
|
||||
"Syncthing is upgrading.": "Syncthing is upgrading.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
|
||||
"The aggregated statistics are publicly available at {{url}}": "The aggregated statistics are publicly available at {{url}}",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "The aggregated statistics are publicly available at {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.",
|
||||
"The entered node 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.": "The entered node 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.",
|
||||
"The node ID cannot be blank.": "The node ID cannot be blank.",
|
||||
"The node ID to enter here can be found in the \"Edit \u003e Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "The node ID to enter here can be found in the \"Edit \u003e Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).",
|
||||
"The number of old versions to keep, per file.": "The number of old versions to keep, per file.",
|
||||
@@ -100,6 +106,7 @@
|
||||
"Unknown": "Unknown",
|
||||
"Up to Date": "Up to Date",
|
||||
"Upgrade To {%version%}": "Upgrade To {{version}}",
|
||||
"Upgrading": "Upgrading",
|
||||
"Upload Rate": "Upload Rate",
|
||||
"Usage": "Usage",
|
||||
"Use Compression": "Use Compression",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"Bugs": "Errores",
|
||||
"CPU Utilization": "Uso de la CPU",
|
||||
"Close": "Cerrar",
|
||||
"Connection Error": "Connection Error",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Derechos de autor © 2014 Jakob Borg y los siguientes colaboradores:",
|
||||
"Delete": "Suprimir",
|
||||
"Disconnected": "Desconectado",
|
||||
@@ -46,6 +47,7 @@
|
||||
"Max Outstanding Requests": "Cantidad máxima de peticiones pendientes",
|
||||
"No": "No",
|
||||
"Node ID": "Nodo ID",
|
||||
"Node Identification": "Node Identification",
|
||||
"Node Name": "Nodo nombre",
|
||||
"Notice": "Aviso",
|
||||
"OK": "OK",
|
||||
@@ -65,6 +67,7 @@
|
||||
"Rescan Interval (s)": "Intervalo de reescaneo (s)",
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "Es necesario reiniciar",
|
||||
"Restarting": "Restarting",
|
||||
"Save": "Guardar",
|
||||
"Scanning": "Actualización",
|
||||
"Select the nodes to share this repository with.": "Seleccione los nodos con los cuales compartir el repositorio.",
|
||||
@@ -84,11 +87,14 @@
|
||||
"Syncing": "Sincronización",
|
||||
"Syncthing has been shut down.": "La sincronización esta apagada",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing incluye los siguientes softwares o partes de ellos:",
|
||||
"Syncthing is restarting.": "Syncthing is restarting.",
|
||||
"Syncthing is upgrading.": "Syncthing is upgrading.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece estar apagado, o hay un problema con su conexión de Internet. Reintentando...",
|
||||
"The aggregated statistics are publicly available at {{url}}": "Las estadísticas acumuladas están públicamente disponibles en {{url}}",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Las estadísticas acumuladas están públicamente disponibles en {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido guardada pero no activada.\nSyncthing debe reiniciarse para activar la nueva configuración.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "El reporte de uso se envía encriptado diariamente. Se utiliza para hacer un seguimiento de plataformas comunes, tamaño de repositorios y versión de aplicaciones. Si el conjunto de datos cambia sera notificado mediante este dialogo nuevamente.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "El ID de nodo ingresado no es valido. Debe ser una cadena de al menos 52 caracteres consistente en letras y números, con espacios y guiones opcionales.",
|
||||
"The entered node 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.": "El ID de nodo ingresado no es valido. Debe ser una cadena de 52 o de 56 caracteres consistente en letras y números, con espacios y guiones opcionales.",
|
||||
"The node ID cannot be blank.": "El ID de nodo no puede estar vacío.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "El ID de nodo a ingresar aquí puede verse en la opción de menú \"Edición > Mostrar ID\" del otro nodo. Espacios y guiones son opcionales (ignorados).",
|
||||
"The number of old versions to keep, per file.": "El numero de versiones anteriores a conservar, por archivo.",
|
||||
@@ -100,9 +106,10 @@
|
||||
"Unknown": "Desconocido",
|
||||
"Up to Date": "Actualizado",
|
||||
"Upgrade To {%version%}": "Actualizar a {{version}}",
|
||||
"Upgrading": "Upgrading",
|
||||
"Upload Rate": "Tasa de subida",
|
||||
"Usage": "Utilización",
|
||||
"Use Compression": "Use Compression",
|
||||
"Use Compression": "Usar compresión",
|
||||
"Use HTTPS for GUI": "Usar HTTPS para la GUI",
|
||||
"Version": "Versión",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Al agregar un nuevo nodo, recuerde que este nodo debe ser agregado en el otro lado también.",
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
"Add Repository": "Ajouter un répertoire",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresses",
|
||||
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation?",
|
||||
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation ?",
|
||||
"Announce Server": "Serveur d'annonce",
|
||||
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
|
||||
"Bugs": "Bugs",
|
||||
"CPU Utilization": "Utilisation du CPU",
|
||||
"Close": "Fermer",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants:",
|
||||
"Connection Error": "Erreur de connexion",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants :",
|
||||
"Delete": "Supprimer",
|
||||
"Disconnected": "Déconnecté",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Débit de téléchargement",
|
||||
"Download Rate": "Débit de réception",
|
||||
"Edit": "Éditer",
|
||||
"Edit Node": "Éditer le nœud",
|
||||
"Edit Repository": "Éditer le répertoire",
|
||||
@@ -23,8 +24,8 @@
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses \"ip:port\" séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
|
||||
"Error": "Erreur",
|
||||
"File Versioning": "Versions de fichier",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Les permissions de fichier sont ignorées lors de la recherche de changements. À utiliser sur les systèmes de fichiers en FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Les fichiers sont datés et déplacés dans le dossier .stversions lors de leurs remplacements ou suppressions par syncthing.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Les permissions de fichier sont ignorées lors de la recherche de changements. À utiliser sur les systèmes de fichiers de type FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Les fichiers sont datés et déplacés dans le dossier .stversions lors de leur remplacement ou suppression par syncthing.",
|
||||
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Les fichiers sont protégés des changements réalisés sur les autres nœuds, mais les changements réalisés sur ce nœud seront transférés au reste du cluster.",
|
||||
"Folder": "Dossier",
|
||||
"GUI Authentication Password": "Mot de passe d'authentification GUI",
|
||||
@@ -43,9 +44,10 @@
|
||||
"Local Repository": "Dossier local",
|
||||
"Master Repo": "Dossier maître",
|
||||
"Max File Change Rate (KiB/s)": "Débit maximum de changement de fichier (KiB/s)",
|
||||
"Max Outstanding Requests": "Nombre maximum de demandes conccurentes de blocs de fichier",
|
||||
"Max Outstanding Requests": "Nombre maximum de demandes concurrentes de blocs de fichier",
|
||||
"No": "Non",
|
||||
"Node ID": "ID du nœud",
|
||||
"Node Identification": "Identification du nœud",
|
||||
"Node Name": "Nom du nœud",
|
||||
"Notice": "Notification",
|
||||
"OK": "OK",
|
||||
@@ -65,9 +67,10 @@
|
||||
"Rescan Interval (s)": "Intervalle de rescan (s)",
|
||||
"Restart": "Redémarrer",
|
||||
"Restart Needed": "Redémarrage nécessaire",
|
||||
"Restarting": "Redémarrage",
|
||||
"Save": "Sauver",
|
||||
"Scanning": "Scanning",
|
||||
"Select the nodes to share this repository with.": "Sélectionner les nœuds qui partageront ce répertoire.",
|
||||
"Scanning": "En cours de scan",
|
||||
"Select the nodes to share this repository with.": "Sélectionner les nœuds qui partagent ce répertoire.",
|
||||
"Settings": "Configuration",
|
||||
"Share With Nodes": "Partager avec les nœuds",
|
||||
"Shared With": "Partagé avec",
|
||||
@@ -84,29 +87,33 @@
|
||||
"Syncing": "En cours de synchronisation",
|
||||
"Syncthing has been shut down.": "Syncthing a été éteint.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing inclut les logiciels, ou portion de ceux-ci, suivants:",
|
||||
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
|
||||
"Syncthing is upgrading.": "Syncthing est cours de mise à jour.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
|
||||
"The aggregated statistics are publicly available at {{url}}": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été sauvée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des répertoires et les versions de l'application. Si le jeu de données rapportées devait être changé, il vous sera demandé de le valider de nouveau via ce dialogue.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID du nœud ne semble pas être valide. Il devrait ressembler à une chaine de 52 caractères comprenant lettres et chiffres, avec des espaces et des traits d'union optionnels.",
|
||||
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID du nœud inséré ne semble pas être valide. Il devrait ressembler à une chaîne de 52 ou 56 comprenant lettres et chiffres, avec des espaces et des traits d'union optionnels.",
|
||||
"The node ID cannot be blank.": "L'ID du nœud ne peut être vide.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "L'ID du nœud à insérer peut être trouvé à travers le menu \"Éditer > Montrer l'ID\" des autres nœuds. Les espaces et les traits d'union sont optionnels (ils seront ignorés).",
|
||||
"The number of old versions to keep, per file.": "Le nombre d'anciennes versions à garder, par fichier.",
|
||||
"The number of versions must be a number and cannot be blank.": "Le nombre de version doit être un nombre et ne peut pas être vide.",
|
||||
"The repository ID cannot be blank.": "L'ID du répertoire ne peut être vide.",
|
||||
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "L'ID du répertoire doit un identifiant court (64 caractères ou moins) comprenant des lettres, nombres, points (.), trait d'union (-) et tiret bas (_).",
|
||||
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "L'ID du répertoire doit être un identifiant court (64 caractères ou moins) comprenant des lettres, nombres, points (.), trait d'union (-) et tiret bas (_).",
|
||||
"The repository ID must be unique.": "L'ID du répertoire doit être unique.",
|
||||
"The repository path cannot be blank.": "Le chemin du répertoire ne peut pas être vide.",
|
||||
"Unknown": "Inconnu",
|
||||
"Up to Date": "Synchronistation à jour",
|
||||
"Up to Date": "Synchronisation à jour",
|
||||
"Upgrade To {%version%}": "Upgrader vers {{version}}",
|
||||
"Upgrading": "Mise à jour de Syncthing",
|
||||
"Upload Rate": "Débit d'envoi",
|
||||
"Usage": "Utilisation",
|
||||
"Use Compression": "Utiliser la compression",
|
||||
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
|
||||
"Version": "Version",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Lorsqu'un nœud est ajouté, gardez à l'esprit que ce nœud doit aussi être ajouté de l'autre coté.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que l'ID du répertoire est utilisé pour lier les répertoires à travers les nœuds. Ils sont sensibles à la case et doivent être semblables à travers tous les nœuds.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que l'ID du répertoire est utilisé pour lier les répertoires à travers les nœuds. Ils sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
|
||||
"Yes": "Oui",
|
||||
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
||||
"items": "éléments"
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
"Address": "Indirizzo",
|
||||
"Addresses": "Indirizzi",
|
||||
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
|
||||
"Announce Server": "Server di Ricerca Globale dei Nodi",
|
||||
"Announce Server": "Server di Presenza Globale dei Nodi",
|
||||
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
|
||||
"Bugs": "Bug",
|
||||
"CPU Utilization": "Utilizzo della CPU",
|
||||
"CPU Utilization": "Utilizzo CPU",
|
||||
"Close": "Chiudi",
|
||||
"Connection Error": "Errore di Connessione",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg e i seguenti Collaboratori:",
|
||||
"Delete": "Elimina",
|
||||
"Disconnected": "Disconnesso",
|
||||
@@ -32,12 +33,12 @@
|
||||
"GUI Listen Addresses": "Indirizzi dell'Interfaccia Grafica",
|
||||
"Generate": "Genera",
|
||||
"Global Discovery": "Individuazione Globale",
|
||||
"Global Discovery Server": "Global Discovery Server",
|
||||
"Global Discovery Server": "Server di Ricerca Globale",
|
||||
"Global Repository": "Deposito Globale",
|
||||
"Idle": "Inattivo",
|
||||
"Ignore Permissions": "Ignora Permessi",
|
||||
"Keep Versions": "Mantieni le Versioni",
|
||||
"Latest Release": "Ultimo Rilascio",
|
||||
"Latest Release": "Ultima Versione",
|
||||
"Local Discovery": "Individuazione Locale",
|
||||
"Local Discovery Port": "Porta di Individuazione Locale",
|
||||
"Local Repository": "Deposito Locale",
|
||||
@@ -46,6 +47,7 @@
|
||||
"Max Outstanding Requests": "Numero Massimo di Richieste Simultanee per i Blocchi di File",
|
||||
"No": "No",
|
||||
"Node ID": "ID Nodo",
|
||||
"Node Identification": "Identificazione Nodo",
|
||||
"Node Name": "Nome Nodo",
|
||||
"Notice": "Avviso",
|
||||
"OK": "OK",
|
||||
@@ -57,7 +59,7 @@
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Percorso del deposito nel computer locale. Verrà creato se non esiste già. Il carattere tilde (~) può essere utilizzato come scorciatoia per",
|
||||
"Please wait": "Attendere prego",
|
||||
"Preview Usage Report": "Anteprima Statistiche di Utilizzo",
|
||||
"RAM Utilization": "Utilizzo della RAM",
|
||||
"RAM Utilization": "Utilizzo RAM",
|
||||
"Reconnect Interval (s)": "Intervallo di Riconnessione (s)",
|
||||
"Repository ID": "ID Deposito",
|
||||
"Repository Master": "Deposito Principale",
|
||||
@@ -65,6 +67,7 @@
|
||||
"Rescan Interval (s)": "Intervallo di Scansione dei File (s)",
|
||||
"Restart": "Riavvia",
|
||||
"Restart Needed": "Riavvio Necessario",
|
||||
"Restarting": "Riavvio",
|
||||
"Save": "Salva",
|
||||
"Scanning": "Scansione in corso",
|
||||
"Select the nodes to share this repository with.": "Seleziona i nodi con i quali vuoi condividere questo deposito.",
|
||||
@@ -84,13 +87,16 @@
|
||||
"Syncing": "Sincronizzazione in corso",
|
||||
"Syncthing has been shut down.": "Syncthing è stato arrestato.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing include i seguenti software o porzioni di questi:",
|
||||
"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…",
|
||||
"The aggregated statistics are publicly available at {{url}}": "Le statistiche aggregate sono disponibili pubblicamente su {{url}}",
|
||||
"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 encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Quotidianamente il software invia le statistiche di utilizzo in forma criptata. Questi dati riguardano i sistemi operativi utilizzati, le dimensioni dei depositi e le versioni del software. Se i dati riportati cambiano verrà mostrata di nuovo questa finestra di dialogo.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID del nodo inserito non sembra valido. Dovrebbe essere una stringa di 52 caratteri costituita da lettere e numeri, con spazi e trattini opzionali.",
|
||||
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID del nodo inserito non sembra valido. Dovrebbe essere una stringa di 52 o 56 caratteri costituita da lettere e numeri, con spazi e trattini opzionali.",
|
||||
"The node ID cannot be blank.": "L'ID del nodo non può essere vuoto.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Trova l'ID del nodo nella finestra di dialogo \"Modifica > Mostra ID\" dell'altro nodo e poi inseriscilo qui. Gli spazi e i trattini sono opzionali (ignorati).",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Trova l'ID nella finestra di dialogo \"Modifica > Mostra ID\" dell'altro nodo, poi inseriscilo qui. Gli spazi e i trattini sono opzionali (ignorati).",
|
||||
"The number of old versions to keep, per file.": "Il numero di vecchie versioni da mantenere, per file.",
|
||||
"The number of versions must be a number and cannot be blank.": "Il numero di versioni dev'essere un numero e non può essere vuoto.",
|
||||
"The repository ID cannot be blank.": "L'ID del deposito non può essere vuoto.",
|
||||
@@ -100,12 +106,13 @@
|
||||
"Unknown": "Sconosciuto",
|
||||
"Up to Date": "Sincronizzato",
|
||||
"Upgrade To {%version%}": "Aggiorna Alla {{version}}",
|
||||
"Upgrading": "Aggiornamento",
|
||||
"Upload Rate": "Velocità Upload",
|
||||
"Usage": "Utilizzo",
|
||||
"Use Compression": "Utilizza Compressione",
|
||||
"Use HTTPS for GUI": "Utilizza HTTPS per l'interfaccia grafica",
|
||||
"Version": "Versione",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quando aggiungi un nuovo nodo, ricordati di aggiungerlo anche dall'altro lato.",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Ora la stessa operazione deve essere eseguita anche nel nuovo nodo inserendo l'ID di questo nodo.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Quando aggiungi un nuovo deposito ricordati che gli ID vengono utilizzati per collegare i depositi nei nodi. Distinguono maiuscole e minuscole e devono corrispondere esattamente su tutti i nodi.",
|
||||
"Yes": "Sì",
|
||||
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
|
||||
|
||||
120
gui/lang-nl.json
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"API Key": "API-sleutel",
|
||||
"About": "Over",
|
||||
"Add Node": "Voeg node toe",
|
||||
"Add Repository": "Voeg repository toe",
|
||||
"Address": "Adres",
|
||||
"Addresses": "Adressen",
|
||||
"Allow Anonymous Usage Reporting?": "Bijhouden van anonieme gebruikers statistieken toestaan?",
|
||||
"Announce Server": "Aankondigings Server",
|
||||
"Anonymous Usage Reporting": "Bijhouden anonieme gebruikers statistieken",
|
||||
"Bugs": "Fouten",
|
||||
"CPU Utilization": "CPU Gebruik",
|
||||
"Close": "Sluiten",
|
||||
"Connection Error": "Verbindingsfout",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg en de onderstaande bijdragers:",
|
||||
"Delete": "Verwijderen",
|
||||
"Disconnected": "Niet Verbonden",
|
||||
"Documentation": "Documentatie",
|
||||
"Download Rate": "Downloadsnelheid",
|
||||
"Edit": "Bewerk",
|
||||
"Edit Node": "Bewerk node",
|
||||
"Edit Repository": "Repository Bijwerken",
|
||||
"Enable UPnP": "UPnP aanzetten",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Geef, gescheiden door komma's, \"ip:port\" adressen of \"dynamic\" voor het automatische vinden van de addressen.",
|
||||
"Error": "Fout",
|
||||
"File Versioning": "Versiebeheer",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Bestands permissiebits worden genegeerd wanneer naar veranderingen wordt gekeken. Gebruik dit op FAT bestandsystemen",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Bestanden worden naar de .stversions map verplaatst met een tijdsaanduiding, wanneer ze aangepast of verwijderd worden door syncthing.",
|
||||
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Bestanden zijn beschermd tegen verandering gedaan op andere nodes, maar veranderingen op deze node worden naar de rest van het cluster gestuurd",
|
||||
"Folder": "Map",
|
||||
"GUI Authentication Password": "GUI Authentificatie Wachtwoord",
|
||||
"GUI Authentication User": "GUI Authentificatie Gebruikersnaam",
|
||||
"GUI Listen Addresses": "GUI Inkomend adres",
|
||||
"Generate": "Genereer",
|
||||
"Global Discovery": "Globaal zoeken",
|
||||
"Global Discovery Server": "Globale zoekserver",
|
||||
"Global Repository": "Globale repository",
|
||||
"Idle": "Klaar",
|
||||
"Ignore Permissions": "Rechten negeren",
|
||||
"Keep Versions": "Versies behouden",
|
||||
"Latest Release": "Laatste uitgave",
|
||||
"Local Discovery": "Lokaal zoeken",
|
||||
"Local Discovery Port": "Lokaal zoeken-poort",
|
||||
"Local Repository": "Lokale repository",
|
||||
"Master Repo": "Tegen veranderingen beschermen",
|
||||
"Max File Change Rate (KiB/s)": "Maximale bestands uitwisselsnelheid (KiB/s)",
|
||||
"Max Outstanding Requests": "Maximaal aantal openstaande aanvragen",
|
||||
"No": "Nee",
|
||||
"Node ID": "Node ID",
|
||||
"Node Identification": "Node Identificatie",
|
||||
"Node Name": "Node naam",
|
||||
"Notice": "Notificatie",
|
||||
"OK": "OK",
|
||||
"Offline": "Offline",
|
||||
"Online": "Online",
|
||||
"Out Of Sync": "Niet gesynchroniseerd",
|
||||
"Outgoing Rate Limit (KiB/s)": "Uitgaande snelheidslimiet (KiB/s)",
|
||||
"Override Changes": "Veranderingen overschrijven",
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Pad naar de repository op de lokale computer. Word aangemaakt indien deze niet bestaat. Het tilde (~) karakter kan gebruikt worden als afkorting voor",
|
||||
"Please wait": "Even geduld",
|
||||
"Preview Usage Report": "Bekijk gebruikers statistieken",
|
||||
"RAM Utilization": "RAM gebruik",
|
||||
"Reconnect Interval (s)": "Herverbind-interval (s)",
|
||||
"Repository ID": "Repository ID",
|
||||
"Repository Master": "Hoofd repository",
|
||||
"Repository Path": "Pad van repository",
|
||||
"Rescan Interval (s)": "Herscan interval (s)",
|
||||
"Restart": "Herstart",
|
||||
"Restart Needed": "Herstart nodig",
|
||||
"Restarting": "Herstarten",
|
||||
"Save": "Bewaar",
|
||||
"Scanning": "Aan het zoeken",
|
||||
"Select the nodes to share this repository with.": "Selecteer de nodes om deze repository mee te delen",
|
||||
"Settings": "Instellingen",
|
||||
"Share With Nodes": "Deel met nodes",
|
||||
"Shared With": "Gedeeld met",
|
||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Korte naam voor de repository. Moet hetzelfde zijn op alle nodes in het cluster.",
|
||||
"Show ID": "Toon ID",
|
||||
"Shown instead of Node ID in the cluster status.": "Wordt weergegeven i.p.v. het node ID in de cluster status",
|
||||
"Shutdown": "Sluit af",
|
||||
"Source Code": "Broncode",
|
||||
"Start Browser": "Start browser",
|
||||
"Stopped": "Gestopt",
|
||||
"Support / Forum": "Support / Forum",
|
||||
"Sync Protocol Listen Addresses": "Synchronisatie protocol luister adres",
|
||||
"Synchronization": "Synchronisatie",
|
||||
"Syncing": "Aan het synchroniseren",
|
||||
"Syncthing has been shut down.": "Syncthing is afgesloten",
|
||||
"Syncthing includes the following software or portions thereof:": "De volgende software of delen daarvan zijn onderdeel van syncthing:",
|
||||
"Syncthing is restarting.": "Syncthing is aan het herstarten.",
|
||||
"Syncthing is upgrading.": "Syncthing is aan het upgraden.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing lijkt afgesloten te zijn, of er is een verbindingsprobleem met het internet. Nieuwe poging....",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "The verzamelde statistieken zijn publiek beschikbaar op {{url}}",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De configuratie is opslagen maar nog niet actief. Syncthing moet opnieuw opgestart worden om de nieuwe configuratie te activeren.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "De versleutelde gebruikers statistieken worden dagelijks verstuurd. Deze worden gebruikt om veelgebruikte platformen, repo groottes en app versies bij te houden. Als er nieuwe statistieken worden bijgehouden, wordt dit venster weer getoond.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Het ingevoerde node ID lijkt niet valide te zijn. Het moet een 52 karakter lange string zijn bestaande uit letters en cijfers, spaties en streepjes zijn optioneel.",
|
||||
"The entered node 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.": "Het ingevoerde node ID lijkt niet valide te zijn. Het moet een 52 of 56 karakter lange string zijn, bestaande uit letters en cijfers, spaties en streepjes zijn optioneel.",
|
||||
"The node ID cannot be blank.": "Er moet een node ID ingevoerd worden",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Het node ID dat ingevoerd moet worden kan op de andere node gevonden via \"Bewerk > Toon ID\". Spaties en streepjes zijn toegestaan (worden genegeerd).",
|
||||
"The number of old versions to keep, per file.": "Het aantal versies dat bewaard moet worden per file.",
|
||||
"The number of versions must be a number and cannot be blank.": "Het aantal nummers moet een getal zijn en mag niet leeg blijven.",
|
||||
"The repository ID cannot be blank.": "Er moet een repository ID ingevoerd worden.",
|
||||
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Het repository ID moet een korte naam zijn (met 64 karakters of minder) en mag alleen bestaan uit cijfers, letters, punten (.), streepjes (-) en liggende streepjes (_). ",
|
||||
"The repository ID must be unique.": "Het repository ID moet uniek zijn.",
|
||||
"The repository path cannot be blank.": "Het repository ID moet ingevuld worden.",
|
||||
"Unknown": "Onbekend",
|
||||
"Up to Date": "Gesynchroniseerd",
|
||||
"Upgrade To {%version%}": "Upgrade naar {{version}}",
|
||||
"Upgrading": "Bezig met upgrade",
|
||||
"Upload Rate": "Upload snelheid",
|
||||
"Usage": "Gebruik",
|
||||
"Use Compression": "Compressie gebruiken",
|
||||
"Use HTTPS for GUI": "Gebruik HTTPS voor de GUI",
|
||||
"Version": "Versie",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Bedenk bij het toevoegen van een nieuwe node dat deze node ook toegevoegd moet worden aan de kant van de nieuwe node.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Bedenk bij het toevoegen van een nieuwe repository dat het repository ID de repositores met elkaar verbindt tussen de nodes. Ze zijn hoofdlettergevoelig en moeten exact dezelfde naam hebben op alle nodes.",
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Minstens 1 versie moet bewaard blijven.",
|
||||
"items": "objecten"
|
||||
}
|
||||
119
gui/lang-pt.json
@@ -1,112 +1,119 @@
|
||||
{
|
||||
"API Key": "Chave API",
|
||||
"About": "Acerca de",
|
||||
"Add Node": "Adicionar Nó",
|
||||
"Add Repository": "Adicionar Repositório",
|
||||
"API Key": "Chave da API",
|
||||
"About": "Acerca da aplicação",
|
||||
"Add Node": "Adicionar nó",
|
||||
"Add Repository": "Adicionar repositório",
|
||||
"Address": "Endereço",
|
||||
"Addresses": "Endereços",
|
||||
"Allow Anonymous Usage Reporting?": "Permitir Envio de Relatórios Anónimos?",
|
||||
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
|
||||
"Announce Server": "Servidor de anúncios",
|
||||
"Anonymous Usage Reporting": "Envio de Relatórios Anónimos",
|
||||
"Anonymous Usage Reporting": "Enviar de relatórios anónimos de utilização",
|
||||
"Bugs": "Erros",
|
||||
"CPU Utilization": "Utilização da CPU",
|
||||
"Close": "Fechar",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Direitos Reservados © 2014 Jakob Borg e os seguintes Contribuidores:",
|
||||
"Delete": "Apagar",
|
||||
"Connection Error": "Erro de ligação",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Direitos reservados © 2014 Jakob Borg e os seguintes contribuidores:",
|
||||
"Delete": "Eliminar",
|
||||
"Disconnected": "Desconectado",
|
||||
"Documentation": "Documentação",
|
||||
"Download Rate": "Taxa de transferência",
|
||||
"Download Rate": "Velocidade de recepção",
|
||||
"Edit": "Editar",
|
||||
"Edit Node": "Editar Nó",
|
||||
"Edit Repository": "Editar Repositório",
|
||||
"Edit Node": "Editar nó",
|
||||
"Edit Repository": "Editar repositório",
|
||||
"Enable UPnP": "Activar UPnP",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços \"ip:porto\" separados por vírgulas ou \"dynamic\" para o descobrimento automático do endereço.",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços \"ip:porto\" separados por vírgulas ou \"dynamic\" para descobrir automaticamente o endereço.",
|
||||
"Error": "Erro",
|
||||
"File Versioning": "Gestão de versões",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "As permissões do ficheiro são ignoradas na pesquisa por mudanças. Utilize nos sistemas de ficheiros FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Os ficheiros são movidos para versões carimbadas com o tempo numa pasta .stversions quando substituídos ou apagados por syncthing.",
|
||||
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Os ficheiros são protegidos de mudanças feitas em outros nós, mas alterações feitas neste nó serão enviadas para o resto do agrupamento.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "As permissões do ficheiro são ignoradas ao procurar alterações. Utilize nos sistemas de ficheiros FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Os ficheiros são movidos para versões carimbadas com o tempo numa pasta .stversions quando substituídos ou apagados pelo syncthing.",
|
||||
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Os ficheiros são protegidos das alterações feitas noutros nós, mas alterações feitas neste nó serão enviadas para o resto do agrupamento.",
|
||||
"Folder": "Pasta",
|
||||
"GUI Authentication Password": "Senha de Autenticação GUI",
|
||||
"GUI Authentication User": "Utilizador de autenticação GUI",
|
||||
"GUI Listen Addresses": "Endereço de escuta GUI",
|
||||
"GUI Authentication Password": "Senha da autenticação na interface gráfica",
|
||||
"GUI Authentication User": "Utilizador da autenticação na interface gráfica",
|
||||
"GUI Listen Addresses": "Endereço de escuta da interface gráfica",
|
||||
"Generate": "Gerar",
|
||||
"Global Discovery": "Descoberta Global",
|
||||
"Global Discovery Server": "Servidor de Descoberta Global",
|
||||
"Global Repository": "Repositório Global",
|
||||
"Global Discovery": "Busca global",
|
||||
"Global Discovery Server": "Servidor da busca global",
|
||||
"Global Repository": "Repositório global",
|
||||
"Idle": "Em espera",
|
||||
"Ignore Permissions": "Ignorar Permissões",
|
||||
"Keep Versions": "Manter Versões",
|
||||
"Ignore Permissions": "Ignorar permissões",
|
||||
"Keep Versions": "Manter versões",
|
||||
"Latest Release": "Última versão",
|
||||
"Local Discovery": "Descoberta Local",
|
||||
"Local Discovery Port": "Porto de Descoberta Local",
|
||||
"Local Discovery": "Busca local",
|
||||
"Local Discovery Port": "Porto da busca local",
|
||||
"Local Repository": "Repositório local",
|
||||
"Master Repo": "Repositório Mestre",
|
||||
"Max File Change Rate (KiB/s)": "Taxa máxima de troca de ficheiros (KiB/s)",
|
||||
"Master Repo": "Repositório mestre",
|
||||
"Max File Change Rate (KiB/s)": "Velocidade máxima de alterações de ficheiros (KiB/s)",
|
||||
"Max Outstanding Requests": "Número máximo de pedidos pendentes",
|
||||
"No": "Não",
|
||||
"Node ID": "ID do Nó",
|
||||
"Node Name": "Nome do Nó",
|
||||
"Node ID": "ID do nó",
|
||||
"Node Identification": "Identificação do nó",
|
||||
"Node Name": "Nome do nó",
|
||||
"Notice": "Nota",
|
||||
"OK": "OK",
|
||||
"Offline": "Desconectado",
|
||||
"Online": "Conectado",
|
||||
"Out Of Sync": "Não sincronizado",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite da taxa de envio (KiB/s)",
|
||||
"Override Changes": "Sobrepor Mudanças",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite da velocidade de envio (KiB/s)",
|
||||
"Override Changes": "Sobrepor alterações",
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Caminho para o repositório no computador local. Será criado se não existir. O carácter (~) pode ser utilizado como atalho para",
|
||||
"Please wait": "Aguarde",
|
||||
"Preview Usage Report": "Visualização de Relatório",
|
||||
"Preview Usage Report": "Pré-visualizar relatório de utilização",
|
||||
"RAM Utilization": "Utilização da RAM",
|
||||
"Reconnect Interval (s)": "Intervalo de reestabelecimento de ligação (s)",
|
||||
"Repository ID": "ID do Repositório",
|
||||
"Repository Master": "Repositório Mestre",
|
||||
"Repository Path": "Caminho do Repositório",
|
||||
"Rescan Interval (s)": "Intervalo de monitorização (s)",
|
||||
"Repository ID": "ID do repositório",
|
||||
"Repository Master": "Repositório mestre",
|
||||
"Repository Path": "Caminho do repositório",
|
||||
"Rescan Interval (s)": "Intervalo entre varrimentos (s)",
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "É preciso reiniciar",
|
||||
"Restarting": "Reiniciando",
|
||||
"Save": "Gravar",
|
||||
"Scanning": "Varrendo",
|
||||
"Select the nodes to share this repository with.": "Seleccione os nós com os quais vai partilhar este repositório.",
|
||||
"Settings": "Configurações",
|
||||
"Share With Nodes": "Partilhar com Nós",
|
||||
"Shared With": "Partilhado Com",
|
||||
"Share With Nodes": "Partilhar com os nós",
|
||||
"Shared With": "Partilhado com",
|
||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identificador curto para o repositório. Tem que ser igual em todos os nós do agrupamento.",
|
||||
"Show ID": "Mostrar ID",
|
||||
"Shown instead of Node ID in the cluster status.": "Mostrado ao invés do ID do Nó no estado do agrupamento.",
|
||||
"Shown instead of Node ID in the cluster status.": "Apresentado ao invés do ID do nó no estado do agrupamento.",
|
||||
"Shutdown": "Desligar",
|
||||
"Source Code": "Código Fonte",
|
||||
"Start Browser": "Iniciar Navegador",
|
||||
"Source Code": "Código fonte",
|
||||
"Start Browser": "Iniciar navegador",
|
||||
"Stopped": "Parado",
|
||||
"Support / Forum": "Suporte / Fórum",
|
||||
"Sync Protocol Listen Addresses": "Endereços de escuta do protocolo de sincronização",
|
||||
"Synchronization": "Sincronização",
|
||||
"Syncing": "Sincronizando",
|
||||
"Syncthing has been shut down.": "Syncthing foi desligado.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing inclui as seguintes aplicações ou partes delas:",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece estar em baixo, ou então existe um problema com a sua ligação à Internet. Tentando novamente...",
|
||||
"The aggregated statistics are publicly available at {{url}}": "As estatísticas agrupadas estão disponíveis publicamente em {{url}}",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi gravada mas não activada. Syncthing tem que reiniciar para activar a nova configuração.",
|
||||
"Syncing": "A Sincronizar",
|
||||
"Syncthing has been shut down.": "O Syncthing foi desligado.",
|
||||
"Syncthing includes the following software or portions thereof:": "O Syncthing inclui as seguintes aplicações ou partes delas:",
|
||||
"Syncthing is restarting.": "O Syncthing está a reiniciar.",
|
||||
"Syncthing is upgrading.": "O Syncthing está a actualizar-se.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "O Syncthing parece estar em baixo, ou então existe um problema com a sua ligação à Internet. Tentando novamente...",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "As estatísticas agrupadas estão disponíveis publicamente em {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi gravada mas não activada. O Syncthing tem que reiniciar para activar a nova configuração.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "O relatório de utilização cifrado é enviado diariamente. É utilizado para seguir plataformas comuns, tamanhos de repositórios e versões da aplicação. Se o conjunto de dados do relatório for alterado, será notificado novamente através desta janela.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "O ID do nó indicado não parece ser válido. Deveria conter uma palavra de 52 caracteres constituída por letras e números, com espaços e traços opcionais. ",
|
||||
"The node ID cannot be blank.": "O ID do nó não pode ser vazio.",
|
||||
"The entered node 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.": "O ID do nó fornecido não parece ser válido. Deveria ser um texto com 52 ou 56 caracteres constituídos por letras e números, com espaços e traços opcionais.",
|
||||
"The node ID cannot be blank.": "O ID do nó não pode estar vazio.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "O ID do nó a introduzir pode ser encontrado no diálogo \"Editar > Mostrar ID\" no outro nó. Espaços e traços são opcionais (ignorados).",
|
||||
"The number of old versions to keep, per file.": "O número de versões antigas a manter, por ficheiro.",
|
||||
"The number of versions must be a number and cannot be blank.": "O número de versões tem que ser um número e não pode ser vazio.",
|
||||
"The repository ID cannot be blank.": "O ID do repositório não pode ser vazio.",
|
||||
"The number of versions must be a number and cannot be blank.": "O número de versões tem que ser um número e não pode estar vazio.",
|
||||
"The repository ID cannot be blank.": "O ID do repositório não pode estar vazio.",
|
||||
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "O ID do repositório tem que ser um identificador curto (64 caracteres ou menos) e consiste em letras, números e os caracteres ponto (.), traço (-) e (_).",
|
||||
"The repository ID must be unique.": "O ID do repositório tem que ser único.",
|
||||
"The repository path cannot be blank.": "O caminho do repositório não pode ser vazio.",
|
||||
"The repository path cannot be blank.": "O caminho do repositório não pode estar vazio.",
|
||||
"Unknown": "Desconhecido",
|
||||
"Up to Date": "Actualizado",
|
||||
"Upgrade To {%version%}": "Atualizar para {{version}}",
|
||||
"Upgrade To {%version%}": "Actualizar para {{version}}",
|
||||
"Upgrading": "Actualizando",
|
||||
"Upload Rate": "Taxa de envio",
|
||||
"Usage": "Utilização",
|
||||
"Use Compression": "Usar Compressão",
|
||||
"Use HTTPS for GUI": "Utilizar HTTPS para GUI",
|
||||
"Use Compression": "Usar compressão",
|
||||
"Use HTTPS for GUI": "Utilizar HTTPS na interface gráfica",
|
||||
"Version": "Versão",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quando adicionar um novo nó, lembre-se que este nó tem que ser adicionado do outro lado também.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Quando adicionar um novo repositório, lembre-se que o ID do Repositório é utilizado para juntar os repositórios entre nós. São sensíveis às maiúsculas e minúsculas e tem que corresponder exactamente entre todos os nós.",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quando adicionar um novo nó, lembre-se que este nó tem que ser adicionado no outro lado também.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Quando adicionar um novo repositório, lembre-se que o ID do repositório é utilizado para juntar os repositórios entre nós. Os ID's são sensíveis às maiúsculas e minúsculas e têm que corresponder exactamente entre todos os nós.",
|
||||
"Yes": "Sim",
|
||||
"You must keep at least one version.": "Tem que manter pelo menos uma versão.",
|
||||
"items": "itens"
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"Bugs": "Ошибки",
|
||||
"CPU Utilization": "Загрузка ЦПУ",
|
||||
"Close": "Закрыть",
|
||||
"Connection Error": "Ошибка подключения",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Все права защищены © 2014 Jakob Borg и следующие участники:",
|
||||
"Delete": "Удалить",
|
||||
"Disconnected": "Нет соединения",
|
||||
@@ -46,6 +47,7 @@
|
||||
"Max Outstanding Requests": "Максимальное количество исходящих запросов",
|
||||
"No": "Нет",
|
||||
"Node ID": "ID Узла",
|
||||
"Node Identification": "Идентификация узла",
|
||||
"Node Name": "Имя Узла",
|
||||
"Notice": "Внимание",
|
||||
"OK": "ОК",
|
||||
@@ -65,6 +67,7 @@
|
||||
"Rescan Interval (s)": "Интервал между сканированием (сек)",
|
||||
"Restart": "Перезапуск",
|
||||
"Restart Needed": "Требуется перезапуск",
|
||||
"Restarting": "Перезапуск",
|
||||
"Save": "Сохранить",
|
||||
"Scanning": "Сканирование",
|
||||
"Select the nodes to share this repository with.": "Выберите узлы для которых будет доступен данный репозиторий.",
|
||||
@@ -84,11 +87,14 @@
|
||||
"Syncing": "Синхронизация",
|
||||
"Syncthing has been shut down.": "Syncthing выключен.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing включает в себя следующее ПО или его части:",
|
||||
"Syncthing is restarting.": "Перезапуск Syncthing",
|
||||
"Syncthing is upgrading.": "Обновление Syncthing ",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",
|
||||
"The aggregated statistics are publicly available at {{url}}": "Суммарная статистика общедоступна на {{url}}",
|
||||
"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 encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Зашифрованные отчёты об использовании отправляются ежедневно. Они используются для отслеживания проблем, размеров репозиториев и версий Syncthing. Если набор данных в отчёте будет изменён, то вы получите уведомление об этом в этом диалоге.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введённый ID узла выглядит неправильно: ID должен быть строкой, длинной 52 символа, обязательно содержащей группы букв и цифр которые могут быть разделены пробелами или тире.",
|
||||
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введённый ID узла выглядит неправильно: ID должен быть строкой, длинной 52 или 56 символов, обязательно содержащей группы букв и цифр которые могут быть разделены пробелами или тире.",
|
||||
"The node ID cannot be blank.": "ID узла не может быть пустым.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "ID узла можно узнать в окне \"Редактировать > Показать ID\" на требуемом узле. Пробелы и тире используются для удобства и не обязательны (игнорируются).",
|
||||
"The number of old versions to keep, per file.": "Количество хранимых версий файла.",
|
||||
@@ -100,6 +106,7 @@
|
||||
"Unknown": "Неизвестно",
|
||||
"Up to Date": "Обновлено",
|
||||
"Upgrade To {%version%}": "Обновить до {{version}}",
|
||||
"Upgrading": "Обновление",
|
||||
"Upload Rate": "Скорость отдачи",
|
||||
"Usage": "Справка",
|
||||
"Use Compression": "Использовать сжатие",
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
"Announce Server": "Uppslagningsserver",
|
||||
"Anonymous Usage Reporting": "Anonym användarstatistik",
|
||||
"Bugs": "Buggar",
|
||||
"CPU Utilization": "CPU-utnyttjande",
|
||||
"CPU Utilization": "CPU-användning",
|
||||
"Close": "Stäng",
|
||||
"Connection Error": "Anslutningsproblem",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg och de följande medarbetarna:",
|
||||
"Delete": "Radera",
|
||||
"Disconnected": "Ej ansluten",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Nerladdningshastighet",
|
||||
"Download Rate": "Nedladdningshastighet",
|
||||
"Edit": "Redigera",
|
||||
"Edit Node": "Redigera nod",
|
||||
"Edit Repository": "Redigera lagring",
|
||||
@@ -29,7 +30,7 @@
|
||||
"Folder": "Katalog",
|
||||
"GUI Authentication Password": "GUI-lösenord",
|
||||
"GUI Authentication User": "GUI-användare",
|
||||
"GUI Listen Addresses": "GUI-address",
|
||||
"GUI Listen Addresses": "GUI-adress",
|
||||
"Generate": "Skapa",
|
||||
"Global Discovery": "Global uppslagning",
|
||||
"Global Discovery Server": "Global uppslagningsserver",
|
||||
@@ -46,6 +47,7 @@
|
||||
"Max Outstanding Requests": "Paralellitet",
|
||||
"No": "Nej",
|
||||
"Node ID": "Nod-ID",
|
||||
"Node Identification": "Nod-identifiering",
|
||||
"Node Name": "Nodnamn",
|
||||
"Notice": "OBS",
|
||||
"OK": "OK",
|
||||
@@ -57,7 +59,7 @@
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
||||
"Please wait": "Var god vänta",
|
||||
"Preview Usage Report": "Förhandsgranska statistik",
|
||||
"RAM Utilization": "Minnesutnyttjande",
|
||||
"RAM Utilization": "Minnesanvändning",
|
||||
"Reconnect Interval (s)": "Anslutningsintervall (s)",
|
||||
"Repository ID": "Lagrings-ID",
|
||||
"Repository Master": "Huvudlagring",
|
||||
@@ -65,6 +67,7 @@
|
||||
"Rescan Interval (s)": "Förnyelseintervall (s)",
|
||||
"Restart": "Starta om",
|
||||
"Restart Needed": "Omstart behövs",
|
||||
"Restarting": "Startar om",
|
||||
"Save": "Spara",
|
||||
"Scanning": "Uppdaterar",
|
||||
"Select the nodes to share this repository with.": "Välj vilka noder förvaringen ska delas med.",
|
||||
@@ -80,15 +83,18 @@
|
||||
"Stopped": "Stoppad",
|
||||
"Support / Forum": "Support / Forum",
|
||||
"Sync Protocol Listen Addresses": "Address för inkommande anslutningar",
|
||||
"Synchronization": "Synkronisation",
|
||||
"Synchronization": "Synkronisering",
|
||||
"Syncing": "Synkroniserar",
|
||||
"Syncthing has been shut down.": "Syncthing har stängts ner.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller de följande mjukvarupaketen eller delar därav:",
|
||||
"Syncthing is restarting.": "Syncthing startar om.",
|
||||
"Syncthing is upgrading.": "Syncthing uppgraderas.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller så är där ett problem med din Internetanslutning. Försöker igen...",
|
||||
"The aggregated statistics are publicly available at {{url}}": "Aggregerad statistik finns publikt tillgänglig på {{url}}",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Aggregerad statistik finns publikt tillgänglig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, lagringsstorlekar och versioner. Om datan som rapporteras ändras så kommer du att bli tillfrågad igen.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade nod-ID:t verkar inte korrekt. Det ska vara en 52 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
|
||||
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade nod-ID:t verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
|
||||
"The node ID cannot be blank.": "Nod-ID:t kan inte vara blankt.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Nod-ID:t som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra noden. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas, per fil.",
|
||||
@@ -100,6 +106,7 @@
|
||||
"Unknown": "Okänt",
|
||||
"Up to Date": "Helt uppdaterad",
|
||||
"Upgrade To {%version%}": "Uppgradera till {{version}}",
|
||||
"Upgrading": "Uppgraderar",
|
||||
"Upload Rate": "Uppladdningshastighet",
|
||||
"Usage": "Användande",
|
||||
"Use Compression": "Använd komprimering",
|
||||
|
||||
120
gui/lang-tr.json
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"API Key": "API Anahtarı",
|
||||
"About": "Hakkında",
|
||||
"Add Node": "Düğüm ekle",
|
||||
"Add Repository": "Depo ekle",
|
||||
"Address": "Adres",
|
||||
"Addresses": "Adresler",
|
||||
"Allow Anonymous Usage Reporting?": "Anonim kullanım raporlarına izin ver ?",
|
||||
"Announce Server": "Duyuru Sunucusu",
|
||||
"Anonymous Usage Reporting": "Anonim Kullanım Raporlama",
|
||||
"Bugs": "Hatalar",
|
||||
"CPU Utilization": "İşlemci Kullanımı",
|
||||
"Close": "Kapat",
|
||||
"Connection Error": "Bağlantı hatası",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Telif hakkı © 2014 Jakob Borg ve aşağıdaki katkıda bulunanlar",
|
||||
"Delete": "Sil",
|
||||
"Disconnected": "Bağlantı Kesik",
|
||||
"Documentation": "Dokümantasyon",
|
||||
"Download Rate": "İndirme Hızı",
|
||||
"Edit": "Seçenekler",
|
||||
"Edit Node": "Düğümü Düzenle",
|
||||
"Edit Repository": "Depoyu düzenle",
|
||||
"Enable UPnP": "UPnP Etkinleştir",
|
||||
"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.",
|
||||
"Error": "Hata",
|
||||
"File Versioning": "Dosya Sürümlendirme",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Değişim yoklarken dosya izin bilgilerini ihmal et. FAT dosya sisteminde 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 nodes, but changes made on this node will be sent to the rest of the cluster.": "Dosyalar diğer düğümlerde yapılan değişikliklerden korunur, ancak bu düğümdeki değişiklikler kümedeki diğer düğümlere gönderilir.",
|
||||
"Folder": "Dizin",
|
||||
"GUI Authentication Password": "Kullanıcı arayüzü şifresi",
|
||||
"GUI Authentication User": "Kullanıcı arayüzü kullanıcı ismi",
|
||||
"GUI Listen Addresses": "Kullanıcı arayüzü bağlantı adresi",
|
||||
"Generate": "Oluştur",
|
||||
"Global Discovery": "Küresel Keşif",
|
||||
"Global Discovery Server": "Küresel Keşif Sunucusu",
|
||||
"Global Repository": "Global Depo",
|
||||
"Idle": "Boşta",
|
||||
"Ignore Permissions": "İzinleri yoksay",
|
||||
"Keep Versions": "Sürüm tut",
|
||||
"Latest Release": "Son sürüm",
|
||||
"Local Discovery": "Yerel bulma",
|
||||
"Local Discovery Port": "Yerel bulma portları",
|
||||
"Local Repository": "Yerel Depo",
|
||||
"Master Repo": "Ana depo",
|
||||
"Max File Change Rate (KiB/s)": "Mak. Dosya değiştirme oranı (KB/sn)",
|
||||
"Max Outstanding Requests": "Maks Öncellikli İstekler",
|
||||
"No": "Hayır",
|
||||
"Node ID": "Düğüm ID",
|
||||
"Node Identification": "Düğüm Kimliği",
|
||||
"Node Name": "Düğüm İsmi",
|
||||
"Notice": "Uyarı",
|
||||
"OK": "Tamam",
|
||||
"Offline": "Çevrim dışı",
|
||||
"Online": "Çevrim içi",
|
||||
"Out Of Sync": "Senkronize edilmemiş",
|
||||
"Outgoing Rate Limit (KiB/s)": "Yükleme hız sınırı (KB/sn)",
|
||||
"Override Changes": "Değişiklikleri Geçersiz kıl",
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Yerel bilgisayardaki depoya ulaşım yolu. Dizin yoksa yaratılacak. (~) karakterinin kısayol olarak kullanılabileceği yol",
|
||||
"Please wait": "Lütfen Bekleyin",
|
||||
"Preview Usage Report": "Kullanım raporunu gözden geçir",
|
||||
"RAM Utilization": "RAM Kullanımı",
|
||||
"Reconnect Interval (s)": "Yeniden bağlanma süresi (sn)",
|
||||
"Repository ID": "Depo ID",
|
||||
"Repository Master": "Ana depo",
|
||||
"Repository Path": "Depo Yolu",
|
||||
"Rescan Interval (s)": "Yeni tarama süresi (sn)",
|
||||
"Restart": "Yeniden Başlat",
|
||||
"Restart Needed": "Yeniden başlatma gereklidir",
|
||||
"Restarting": "Yeniden başlatılıyor",
|
||||
"Save": "Kaydet",
|
||||
"Scanning": "Taranıyor",
|
||||
"Select the nodes to share this repository with.": "Bu deponun paylaşılacağı düğümleri seçin.",
|
||||
"Settings": "Ayarlar",
|
||||
"Share With Nodes": "Paylaşılan düğümler:",
|
||||
"Shared With": "Paylaşılan düğümler",
|
||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Depo için kısa tanımlayıcı. Kümedeki tüm düğümlerde aynı olmalı.",
|
||||
"Show ID": "ID Göster",
|
||||
"Shown instead of Node ID in the cluster status.": "Ana ekranda Düğüm ID yerine bunu göster.",
|
||||
"Shutdown": "Kapat",
|
||||
"Source Code": "Kaynak Kodu",
|
||||
"Start Browser": "Tarayıcıyı Başlat",
|
||||
"Stopped": "Durduruldu",
|
||||
"Support / Forum": "Destek / Forum",
|
||||
"Sync Protocol Listen Addresses": "Sync Protokol Dinleme Adresleri",
|
||||
"Synchronization": "Senkronizasyon",
|
||||
"Syncing": "Senkronize ediliyor",
|
||||
"Syncthing has been shut down.": "Syncthing durduruldu",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing aşağıdaki yazılımları veya bunların bölümlerini içermektedir:",
|
||||
"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....",
|
||||
"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 encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Şifrelenmiş kullanım bilgisi günlük olarak gönderilir. Platform, depo büyüklüğü ve uygulama sürümü hakkında bilgi toplanır. Toplanan bilgi çeşidi değişecek olursa, sizden tekrar onay istenecek.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Girilen düğüm ID'si geçerli gibi gözükmüyor. 52 karakter uzunluğunda, harf ve rakamlardan oluşmalı. Boşlukların ve kısa çizgilerin olup olmaması önemli değildir.",
|
||||
"The entered node 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.": "Girilen düğüm ID'si geçerli gibi gözükmüyor. 52 ya da 56 karakter uzunluğunda, harf ve rakamlardan oluşmalı. Boşlukların ve kısa çizgilerin olup olmaması önemli değildir.",
|
||||
"The node ID cannot be blank.": "Düğüm ID'si boş olamaz.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Buraya girilecek düğüm ID'si diğer düğümde \"Düzenle > ID Göster\" menüsünden bulunabilir. Boşluk ve kısa çizginin olup olmaması önemli değildir. (İhmal edilir)",
|
||||
"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 repository ID cannot be blank.": "Depo ID boş bırakılamaz.",
|
||||
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Depo ID uzun olmamalı (64 karakter ya da daha az). Sadece harf, rakam, nokta (.), kısa çizgi (-) ve alt çizgi (_) kullanabilirsiniz.",
|
||||
"The repository ID must be unique.": "Depo ID'si benzersiz olmalıdır.",
|
||||
"The repository path cannot be blank.": "Depo yolu boş bırakılamaz.",
|
||||
"Unknown": "Bilinmiyor",
|
||||
"Up to Date": "Güncel",
|
||||
"Upgrade To {%version%}": "{{version}} sürümüne yükselt",
|
||||
"Upgrading": "Yükseltiliyor",
|
||||
"Upload Rate": "Yükleme hızı",
|
||||
"Usage": "Kullanım",
|
||||
"Use Compression": "Sıkıştırma kullan",
|
||||
"Use HTTPS for GUI": "GUI için HTTPS kullan",
|
||||
"Version": "Sürüm",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Yeni bir düğüm eklendiğinde unutmayın ki; bu düğüm diğer tarafa da eklenmelidir.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Unutmayın ki; Depo ID, depoları düğümler arasında bağlamak için kullanılıyor. Büyük - küçük harf duyarlı, ve bütün düğümlerde aynı olmalı.",
|
||||
"Yes": "Evet",
|
||||
"You must keep at least one version.": "En az bir sürümü tutmalısınız.",
|
||||
"items": "öğe"
|
||||
}
|
||||
120
gui/lang-uk.json
Normal file
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"API Key": "API ключ",
|
||||
"About": "Про програму",
|
||||
"Add Node": "Додати вузол",
|
||||
"Add Repository": "Додати репозиторій",
|
||||
"Address": "Адреса",
|
||||
"Addresses": "Адреси",
|
||||
"Allow Anonymous Usage Reporting?": "Дозволити програмі збирати анонімну статистику викроистання?",
|
||||
"Announce Server": "Сервер анонсування",
|
||||
"Anonymous Usage Reporting": "Анонімна статистика використання",
|
||||
"Bugs": "Помилки",
|
||||
"CPU Utilization": "Навантаження CPU",
|
||||
"Close": "Закрити",
|
||||
"Connection Error": "Помилка з’єднання",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg та наступні контриб’ютори:",
|
||||
"Delete": "Видалити",
|
||||
"Disconnected": "З’єднання відсутнє",
|
||||
"Documentation": "Документація",
|
||||
"Download Rate": "Швидкість завантаження",
|
||||
"Edit": "Редагувати",
|
||||
"Edit Node": "Редагувати вузол",
|
||||
"Edit Repository": "Редагувати репозиторій",
|
||||
"Enable UPnP": "Увімкнути UPnP",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Уведіть адреси \"ip:port\" розділені комою, або слово \"dynamic\" для здійснення автоматичного виявлення адреси.",
|
||||
"Error": "Помилка",
|
||||
"File Versioning": "Керування версіями",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Біти прав доступу до файлів будуть проігноровані під час визначення змін. Використовуйте на файлових системах 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 nodes, but changes made on this node will be sent to the rest of the cluster.": "Файли захищені від змін на інших вузлах, але зміни на цьому вузлі будуть розіслані на інші вузли кластера.",
|
||||
"Folder": "Директорія",
|
||||
"GUI Authentication Password": "Пароль для доступу до панелі управління",
|
||||
"GUI Authentication User": "Логін користувача для доступу до панелі управління",
|
||||
"GUI Listen Addresses": "Адреса доступу до панелі управління",
|
||||
"Generate": "Згенерувати",
|
||||
"Global Discovery": "Глобальне виявлення",
|
||||
"Global Discovery Server": "Сервер для глобального виявлення",
|
||||
"Global Repository": "Глобальний репозиторій",
|
||||
"Idle": "Очікування",
|
||||
"Ignore Permissions": "Ігнорувати права доступу до файлів",
|
||||
"Keep Versions": "Зберігати версії",
|
||||
"Latest Release": "Останній реліз",
|
||||
"Local Discovery": "Локальне виявлення",
|
||||
"Local Discovery Port": "Локальний порт для виявлення",
|
||||
"Local Repository": "Локальний репозиторій",
|
||||
"Master Repo": "Центральний репозиторій",
|
||||
"Max File Change Rate (KiB/s)": "Максимальна швидкість змінення файлів (КіБ/с)",
|
||||
"Max Outstanding Requests": "Максимальна кількість вихідних запитів",
|
||||
"No": "Ні",
|
||||
"Node ID": "ID вузла",
|
||||
"Node Identification": "Ідентифікатор вузла",
|
||||
"Node Name": "Назва вузла",
|
||||
"Notice": "Повідомлення",
|
||||
"OK": "OK",
|
||||
"Offline": "Офлайн",
|
||||
"Online": "Онлайн",
|
||||
"Out Of Sync": "Не синхронізовано",
|
||||
"Outgoing Rate Limit (KiB/s)": "Ліміт швидкості віддачі (КіБ/с)",
|
||||
"Override Changes": "Перезаписати зміни",
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Шлях до репозиторія на локальному комп’ютері. Буде створений, якщо такий не існує. Символ тильди (~) може бути використаний як ярлик для",
|
||||
"Please wait": "Будь ласка, зачекайте",
|
||||
"Preview Usage Report": "Попередній перегляд статистичного звіту",
|
||||
"RAM Utilization": "Використання RAM",
|
||||
"Reconnect Interval (s)": "Інтервал повторного з’єднання (с)",
|
||||
"Repository ID": "ID репозиторія",
|
||||
"Repository Master": "Центральний репозиторій",
|
||||
"Repository Path": "Шлях до репозиторія",
|
||||
"Rescan Interval (s)": "Інтервал для повторного сканування (с)",
|
||||
"Restart": "Перезапуск",
|
||||
"Restart Needed": "Необхідний перезапуск",
|
||||
"Restarting": "Відбувається перезапуск",
|
||||
"Save": "Зберегти",
|
||||
"Scanning": "Сканування",
|
||||
"Select the nodes to share this repository with.": "Оберіть вузли із якими обміняти даний репозиторій.",
|
||||
"Settings": "Налаштування",
|
||||
"Share With Nodes": "Обмінювати із вузлами",
|
||||
"Shared With": "Доступно для",
|
||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Короткий ідентифікатор репозиторія. Повинен бути однаковим на всіх вузлах кластера.",
|
||||
"Show ID": "Показати ID",
|
||||
"Shown instead of Node ID in the cluster status.": "Показано замість ID вузла в статусі кластера.",
|
||||
"Shutdown": "Вимкнути",
|
||||
"Source Code": "Сирцевий код",
|
||||
"Start Browser": "Запустити браузер",
|
||||
"Stopped": "Зупинено",
|
||||
"Support / Forum": "Підтримка / Форум",
|
||||
"Sync Protocol Listen Addresses": "Адреса панелі управління",
|
||||
"Synchronization": "Синхронізація",
|
||||
"Syncing": "Синхронізація",
|
||||
"Syncthing has been shut down.": "Syncthing вимкнено (закрито).",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing містить наступне програмне забезпечення (або його частини):",
|
||||
"Syncthing is restarting.": "Syncthing перезавантажується.",
|
||||
"Syncthing is upgrading.": "Syncthing оновлюється.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Схоже на те, що 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 encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Зашифрована статистика використання відсилається щоденно. Вона використовується для того, щоб розробники розуміли, на яких платформах працює програма, розміри репозиторіїв та версії програми. Якщо набір даних, що збирається зазнає змін, ви обов’язково будете повідомлені через це діалогове вікно.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введений ID вузла невалідний. Ідентифікатор має вигляд строки довжиною 52 символи, що містить цифри та літери, із опціональними пробілами та тире.",
|
||||
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введений ID вузла невалідний. Ідентифікатор має вигляд строки довжиною 52 або 56 символів, що містить цифри та літери, із опціональними пробілами та тире.",
|
||||
"The node ID cannot be blank.": "ID вузла не може бути порожнім.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "ID вузла, який необхідно додати. Може бути знайдений у вікні \"Редагувати > Показати ID\" на іншому вузлі. Пробіли та тире опціональні (вони ігноруються програмою).",
|
||||
"The number of old versions to keep, per file.": "Кількість старих версій, яку необхідно зберігати для кожного файлу.",
|
||||
"The number of versions must be a number and cannot be blank.": "Кількість версій повинна бути цифрою та не може бути порожньою.",
|
||||
"The repository ID cannot be blank.": "ID репозиторія не може бути порожнім.",
|
||||
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "ID репозиторія повинен бути коротким ідентифікатором (64 символи або менше), що містить лише цифри та літери, знак крапки (.), тире (-) та нижнього підкреслення (_).",
|
||||
"The repository ID must be unique.": "ID репозиторія повинен бути унікальним.",
|
||||
"The repository path cannot be blank.": "Шлях до репозиторія не може бути порожнім.",
|
||||
"Unknown": "Невідомо",
|
||||
"Up to Date": "Актуальа версія",
|
||||
"Upgrade To {%version%}": "Оновити до {{version}}",
|
||||
"Upgrading": "Оновлення",
|
||||
"Upload Rate": "Швидкість віддачі",
|
||||
"Usage": "Допомога",
|
||||
"Use Compression": "Використовувати компресію",
|
||||
"Use HTTPS for GUI": "Використовувати HTTPS для доступу до панелі управління",
|
||||
"Version": "Версія",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Коли додаєте новий вузол, пам’ятайте, що цей вузол повинен бути доданий і на іншій стороні.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Коли додаєте новий репозиторій, пам’ятайте, що ID репозиторія використовується для того, щоб зв’язувати репозиторії разом між вузлами. Назви є чутливими до регістра та повинні співпадати точно між усіма вузлами.",
|
||||
"Yes": "Так",
|
||||
"You must keep at least one version.": "Ви повинні зберігати щонайменше одну версію.",
|
||||
"items": "елементи"
|
||||
}
|
||||
BIN
gui/logo-text-256.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
gui/logo-text-64.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -1,4 +1,4 @@
|
||||
<div class="modal fade">
|
||||
<div class="modal fade" tabindex="-1" ng-attr-data-backdrop="{{ close ? true : 'static' }}" ng-attr-data-keyboard="{{ Boolean(close) }}">
|
||||
<div class="modal-dialog" ng-class="{'modal-lg': large}">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-{{status}}">
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="modal-body" ng-transclude>
|
||||
</div>
|
||||
<div ng-if="close" class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> Close</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
BIN
gui/raleway-500.ttf
Normal file
6
gui/raleway.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: 'Raleway';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local('Raleway'), url(raleway-500.ttf) format('truetype');
|
||||
}
|
||||
|
Before Width: | Height: | Size: 17 KiB |
@@ -1 +1 @@
|
||||
var validLangs = ["de","el","en","es","fr","it","pt","ru","sv"]
|
||||
var validLangs = ["da","de","el","en","es","fr","it","nl","pt","ru","sv","tr","uk"]
|
||||
|
||||
@@ -3,5 +3,3 @@
|
||||
./test-http.sh || exit
|
||||
./test-merge.sh || exit
|
||||
./test-delupd.sh || exit
|
||||
# ./test-folders.sh || exit
|
||||
./test-reconnect.sh || exit
|
||||
|
||||
264
integration/common_test.go
Normal file
@@ -0,0 +1,264 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
mr "math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
type syncthingProcess struct {
|
||||
log string
|
||||
argv []string
|
||||
port int
|
||||
|
||||
cmd *exec.Cmd
|
||||
logfd *os.File
|
||||
}
|
||||
|
||||
func (p *syncthingProcess) start() error {
|
||||
if p.logfd == nil {
|
||||
logfd, err := os.Create(p.log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.logfd = logfd
|
||||
}
|
||||
|
||||
cmd := exec.Command("../bin/syncthing", p.argv...)
|
||||
cmd.Stdout = p.logfd
|
||||
cmd.Stderr = p.logfd
|
||||
cmd.Env = append(env, fmt.Sprintf("STPROFILER=:%d", p.port+1000))
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.cmd = cmd
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *syncthingProcess) stop() {
|
||||
p.cmd.Process.Kill()
|
||||
p.cmd.Wait()
|
||||
}
|
||||
|
||||
func (p *syncthingProcess) peerCompletion() (map[string]int, error) {
|
||||
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/rest/debug/peerCompletion", p.port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
comp := map[string]int{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&comp)
|
||||
return comp, err
|
||||
}
|
||||
|
||||
type fileGenerator struct {
|
||||
files int
|
||||
maxexp int
|
||||
srcname string
|
||||
}
|
||||
|
||||
func generateFiles(dir string, files, maxexp int, srcname string) error {
|
||||
fd, err := os.Open(srcname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < files; i++ {
|
||||
n := randomName()
|
||||
p0 := filepath.Join(dir, string(n[0]), n[0:2])
|
||||
err = os.MkdirAll(p0, 0755)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
s := 1 << uint(mr.Intn(maxexp))
|
||||
a := 128 * 1024
|
||||
if a > s {
|
||||
a = s
|
||||
}
|
||||
s += mr.Intn(a)
|
||||
|
||||
src := io.LimitReader(&inifiteReader{fd}, int64(s))
|
||||
|
||||
p1 := filepath.Join(p0, n)
|
||||
dst, err := os.Create(p1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dst.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.Chmod(p1, os.FileMode(mr.Intn(0777)|0400))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := time.Now().Add(-time.Duration(mr.Intn(30*86400)) * time.Second)
|
||||
err = os.Chtimes(p1, t, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomName() string {
|
||||
var b [16]byte
|
||||
rand.Reader.Read(b[:])
|
||||
return fmt.Sprintf("%x", b[:])
|
||||
}
|
||||
|
||||
type inifiteReader struct {
|
||||
rd io.ReadSeeker
|
||||
}
|
||||
|
||||
func (i *inifiteReader) Read(bs []byte) (int, error) {
|
||||
n, err := i.rd.Read(bs)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
i.rd.Seek(0, 0)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// rm -rf
|
||||
func removeAll(dirs ...string) error {
|
||||
for _, dir := range dirs {
|
||||
err := os.RemoveAll(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compare a number of directories. Returns nil if the contents are identical,
|
||||
// otherwise an error describing the first found difference.
|
||||
func compareDirectories(dirs ...string) error {
|
||||
chans := make([]chan fileInfo, len(dirs))
|
||||
for i := range chans {
|
||||
chans[i] = make(chan fileInfo)
|
||||
}
|
||||
abort := make(chan struct{})
|
||||
|
||||
for i := range dirs {
|
||||
startWalker(dirs[i], chans[i], abort)
|
||||
}
|
||||
|
||||
res := make([]fileInfo, len(dirs))
|
||||
for {
|
||||
numDone := 0
|
||||
for i := range chans {
|
||||
fi, ok := <-chans[i]
|
||||
if !ok {
|
||||
numDone++
|
||||
}
|
||||
res[i] = fi
|
||||
}
|
||||
|
||||
for i := 1; i < len(res); i++ {
|
||||
if res[i] != res[0] {
|
||||
close(abort)
|
||||
return fmt.Errorf("Mismatch; %#v (%s) != %#v (%s)", res[i], dirs[i], res[0], dirs[0])
|
||||
}
|
||||
}
|
||||
|
||||
if numDone == len(dirs) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
name string
|
||||
mode os.FileMode
|
||||
mod time.Time
|
||||
hash [16]byte
|
||||
}
|
||||
|
||||
func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) {
|
||||
walker := func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rn, _ := filepath.Rel(dir, path)
|
||||
if rn == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
var f fileInfo
|
||||
if info.IsDir() {
|
||||
f = fileInfo{
|
||||
name: rn,
|
||||
mode: info.Mode(),
|
||||
// hash and modtime zero for directories
|
||||
}
|
||||
} else {
|
||||
f = fileInfo{
|
||||
name: rn,
|
||||
mode: info.Mode(),
|
||||
mod: info.ModTime(),
|
||||
}
|
||||
sum, err := md5file(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.hash = sum
|
||||
}
|
||||
|
||||
select {
|
||||
case res <- f:
|
||||
return nil
|
||||
case <-abort:
|
||||
return errors.New("abort")
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
filepath.Walk(dir, walker)
|
||||
close(res)
|
||||
}()
|
||||
}
|
||||
|
||||
func md5file(fname string) (hash [16]byte, err error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := md5.New()
|
||||
io.Copy(h, f)
|
||||
hb := h.Sum(nil)
|
||||
copy(hash[:], hb)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
<reconnectionIntervalS>5</reconnectionIntervalS>
|
||||
<maxChangeKbps>10000</maxChangeKbps>
|
||||
<startBrowser>false</startBrowser>
|
||||
<upnpEnabled>true</upnpEnabled>
|
||||
<upnpEnabled>false</upnpEnabled>
|
||||
<urAccepted>-1</urAccepted>
|
||||
</options>
|
||||
</configuration>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<reconnectionIntervalS>5</reconnectionIntervalS>
|
||||
<maxChangeKbps>10000</maxChangeKbps>
|
||||
<startBrowser>false</startBrowser>
|
||||
<upnpEnabled>true</upnpEnabled>
|
||||
<upnpEnabled>false</upnpEnabled>
|
||||
<urAccepted>-1</urAccepted>
|
||||
</options>
|
||||
</configuration>
|
||||
|
||||
@@ -8,7 +8,9 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -27,16 +29,19 @@ var (
|
||||
)
|
||||
|
||||
var jsonEndpoints = []string{
|
||||
"/rest/model?repo=default",
|
||||
"/rest/model/version?repo=default",
|
||||
"/rest/need",
|
||||
"/rest/connections",
|
||||
"/rest/completion?node=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU&repo=default",
|
||||
"/rest/config",
|
||||
"/rest/config/sync",
|
||||
"/rest/system",
|
||||
"/rest/connections",
|
||||
"/rest/errors",
|
||||
// "/rest/discovery",
|
||||
"/rest/events",
|
||||
"/rest/lang",
|
||||
"/rest/model/version?repo=default",
|
||||
"/rest/model?repo=default",
|
||||
"/rest/need",
|
||||
"/rest/nodeid?id=I6KAH7666SLLLB5PFXSOAUFJCDZCYAOMLEKCP2GB32BV5RQST3PSROAU",
|
||||
"/rest/report",
|
||||
"/rest/system",
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -61,17 +66,23 @@ func main() {
|
||||
|
||||
var tests []testing.InternalTest
|
||||
tests = append(tests, testing.InternalTest{"TestGetIndex", TestGetIndex})
|
||||
tests = append(tests, testing.InternalTest{"TestGetVersion", TestGetVersion})
|
||||
tests = append(tests, testing.InternalTest{"TestGetVersionNoCSRF", TestGetVersion})
|
||||
tests = append(tests, testing.InternalTest{"TestJSONEndpoints", TestJSONEndpoints})
|
||||
if len(authUser) > 0 || len(apiKey) > 0 {
|
||||
tests = append(tests, testing.InternalTest{"TestPOSTNoCSRF", TestPOSTNoCSRF})
|
||||
|
||||
if len(authUser) > 0 {
|
||||
// If we expect authentication, verify that it fails with the wrong password and wrong API key
|
||||
tests = append(tests, testing.InternalTest{"TestJSONEndpointsNoAuth", TestJSONEndpointsNoAuth})
|
||||
tests = append(tests, testing.InternalTest{"TestJSONEndpointsIncorrectAuth", TestJSONEndpointsIncorrectAuth})
|
||||
}
|
||||
|
||||
if len(csrfToken) > 0 {
|
||||
tests = append(tests, testing.InternalTest{"TestJSONEndpointsNoCSRF", TestJSONEndpointsNoCSRF})
|
||||
// If we have a CSRF token, verify that POST succeeds with it
|
||||
tests = append(tests, testing.InternalTest{"TestPostWitchCSRF", TestPostWitchCSRF})
|
||||
tests = append(tests, testing.InternalTest{"TestGetPostConfigOK", TestGetPostConfigOK})
|
||||
tests = append(tests, testing.InternalTest{"TestGetPostConfigFail", TestGetPostConfigFail})
|
||||
}
|
||||
|
||||
fmt.Printf("Testing HTTP: CSRF=%v, API=%v, Auth=%v\n", len(csrfToken) > 0, len(apiKey) > 0, len(authUser) > 0)
|
||||
testing.Main(matcher, tests, nil, nil)
|
||||
}
|
||||
|
||||
@@ -145,33 +156,144 @@ func TestJSONEndpoints(t *testing.T) {
|
||||
for _, p := range jsonEndpoints {
|
||||
res, err := get(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Errorf("Status %d != 200 for %q", res.StatusCode, p)
|
||||
continue
|
||||
}
|
||||
if ct := res.Header.Get("Content-Type"); ct != "application/json; charset=utf-8" {
|
||||
t.Errorf("Content-Type %q != \"application/json\" for %q", ct, p)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONEndpointsNoCSRF(t *testing.T) {
|
||||
for _, p := range jsonEndpoints {
|
||||
r, err := http.NewRequest("GET", "http://"+target+p, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 403 && res.StatusCode != 401 {
|
||||
t.Fatalf("Status %d != 403/401 for %q", res.StatusCode, p)
|
||||
}
|
||||
func TestPOSTNoCSRF(t *testing.T) {
|
||||
r, err := http.NewRequest("POST", "http://"+target+"/rest/error/clear", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 403 && res.StatusCode != 401 {
|
||||
t.Fatalf("Status %d != 403/401 for POST", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostWitchCSRF(t *testing.T) {
|
||||
r, err := http.NewRequest("POST", "http://"+target+"/rest/error/clear", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("Status %d != 200 for POST", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPostConfigOK(t *testing.T) {
|
||||
// Get config
|
||||
r, err := http.NewRequest("GET", "http://"+target+"/rest/config", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("Status %d != 200 for POST", res.StatusCode)
|
||||
}
|
||||
bs, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
// Post same config back
|
||||
r, err = http.NewRequest("POST", "http://"+target+"/rest/config", bytes.NewBuffer(bs))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
res, err = http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("Status %d != 200 for POST", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPostConfigFail(t *testing.T) {
|
||||
// Get config
|
||||
r, err := http.NewRequest("GET", "http://"+target+"/rest/config", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("Status %d != 200 for POST", res.StatusCode)
|
||||
}
|
||||
bs, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
// Post same config back, with some characters missing to create a syntax error
|
||||
r, err = http.NewRequest("POST", "http://"+target+"/rest/config", bytes.NewBuffer(bs[2:]))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
res, err = http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.StatusCode != 500 {
|
||||
t.Fatalf("Status %d != 500 for POST", res.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,17 +301,20 @@ func TestJSONEndpointsNoAuth(t *testing.T) {
|
||||
for _, p := range jsonEndpoints {
|
||||
r, err := http.NewRequest("GET", "http://"+target+p, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
if res.StatusCode != 403 && res.StatusCode != 401 {
|
||||
t.Fatalf("Status %d != 403/401 for %q", res.StatusCode, p)
|
||||
t.Errorf("Status %d != 403/401 for %q", res.StatusCode, p)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,7 +323,8 @@ func TestJSONEndpointsIncorrectAuth(t *testing.T) {
|
||||
for _, p := range jsonEndpoints {
|
||||
r, err := http.NewRequest("GET", "http://"+target+p, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
@@ -206,10 +332,12 @@ func TestJSONEndpointsIncorrectAuth(t *testing.T) {
|
||||
r.SetBasicAuth("wronguser", "wrongpass")
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
if res.StatusCode != 403 && res.StatusCode != 401 {
|
||||
t.Fatalf("Status %d != 403/401 for %q", res.StatusCode, p)
|
||||
t.Errorf("Status %d != 403/401 for %q", res.StatusCode, p)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,9 +350,6 @@ func get(path string) (*http.Response, error) {
|
||||
if len(authUser) > 0 {
|
||||
r.SetBasicAuth(authUser, authPass)
|
||||
}
|
||||
if len(csrfToken) > 0 {
|
||||
r.Header.Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
if len(apiKey) > 0 {
|
||||
r.Header.Set("X-API-Key", apiKey)
|
||||
}
|
||||
|
||||
141
integration/reconnect_test.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build integration
|
||||
|
||||
package integration_test
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
apiKey = "abc123" // Used when talking to the processes under test
|
||||
id1 = "I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"
|
||||
id2 = "JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"
|
||||
)
|
||||
|
||||
var env = []string{
|
||||
"HOME=.",
|
||||
"STTRACE=model",
|
||||
}
|
||||
|
||||
func TestRestartBothDuringTransfer(t *testing.T) {
|
||||
// Give the receiver some time to rot with needed files but
|
||||
// without any peer. This triggers
|
||||
// https://github.com/syncthing/syncthing/issues/463
|
||||
testRestartDuringTransfer(t, true, true, 10*time.Second, 0)
|
||||
}
|
||||
|
||||
func TestRestartReceiverDuringTransfer(t *testing.T) {
|
||||
testRestartDuringTransfer(t, false, true, 0, 0)
|
||||
}
|
||||
|
||||
func TestRestartSenderDuringTransfer(t *testing.T) {
|
||||
testRestartDuringTransfer(t, true, false, 0, 0)
|
||||
}
|
||||
|
||||
func testRestartDuringTransfer(t *testing.T, restartSender, restartReceiver bool, senderDelay, receiverDelay time.Duration) {
|
||||
t.Log("Cleaning...")
|
||||
err := removeAll("s1", "s2", "f1/index", "f2/index")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log("Generating files...")
|
||||
err = generateFiles("s1", 1000, 20, "../bin/syncthing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log("Starting up...")
|
||||
sender := syncthingProcess{ // id1
|
||||
log: "1.out",
|
||||
argv: []string{"-home", "f1"},
|
||||
port: 8081,
|
||||
}
|
||||
err = sender.start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
receiver := syncthingProcess{ // id2
|
||||
log: "2.out",
|
||||
argv: []string{"-home", "f2"},
|
||||
port: 8082,
|
||||
}
|
||||
err = receiver.start()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Give them time to start up
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
var prevComp int
|
||||
for {
|
||||
comp, err := sender.peerCompletion()
|
||||
if err != nil {
|
||||
sender.stop()
|
||||
receiver.stop()
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
curComp := comp[id2]
|
||||
|
||||
if curComp == 100 {
|
||||
sender.stop()
|
||||
receiver.stop()
|
||||
break
|
||||
}
|
||||
|
||||
if curComp > prevComp {
|
||||
if restartReceiver {
|
||||
t.Logf("Stopping receiver...")
|
||||
receiver.stop()
|
||||
}
|
||||
|
||||
if restartSender {
|
||||
t.Logf("Stopping sender...")
|
||||
sender.stop()
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
if restartReceiver {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
time.Sleep(receiverDelay)
|
||||
t.Logf("Starting receiver...")
|
||||
receiver.start()
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
if restartSender {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
time.Sleep(senderDelay)
|
||||
t.Logf("Starting sender...")
|
||||
sender.start()
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
prevComp = curComp
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
t.Log("Comparing directories...")
|
||||
err = compareDirectories("s1", "s2")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,7 @@ alterFiles() {
|
||||
for i in 1 12-2 23-3 ; do
|
||||
# Delete some files
|
||||
pushd "s$i" >/dev/null
|
||||
chmod 755 ro-test
|
||||
nfiles=$(find . -type f | wc -l)
|
||||
if [[ $nfiles -ge 300 ]] ; then
|
||||
todelete=$(( $nfiles - 300 ))
|
||||
@@ -107,6 +108,10 @@ alterFiles() {
|
||||
../genfiles -maxexp 22 -files 200
|
||||
echo " $i: append to large file"
|
||||
dd if=large-$i bs=1024k count=4 >> large-$i 2>/dev/null
|
||||
echo " $i: new files in ro directory"
|
||||
uuidgen > ro-test/$(uuidgen)
|
||||
chmod 500 ro-test
|
||||
|
||||
../md5r -l | sort | grep -v .stversions > ../md5-$i
|
||||
popd >/dev/null
|
||||
done
|
||||
@@ -118,6 +123,7 @@ alterFiles() {
|
||||
}
|
||||
|
||||
rm -rf h?/*.idx.gz h?/index
|
||||
chmod -R u+w s? s??-?
|
||||
rm -rf s? s??-?
|
||||
mkdir s1 s2 s3 s12-1 s12-2 s23-2 s23-3
|
||||
|
||||
@@ -126,6 +132,10 @@ for i in 1 12-2 23-3; do
|
||||
pushd "s$i" >/dev/null
|
||||
echo " $i: random nonoverlapping"
|
||||
../genfiles -maxexp 22 -files 400
|
||||
echo " $i: ro directory"
|
||||
mkdir ro-test
|
||||
uuidgen > ro-test/$(uuidgen)
|
||||
chmod 500 ro-test
|
||||
popd >/dev/null
|
||||
done
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copyright (C) 2014 Jakob Borg and other contributors. All rights reserved.
|
||||
# Use of this source code is governed by an MIT-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
id1=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU
|
||||
id2=JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU
|
||||
|
||||
go build json.go
|
||||
go build md5r.go
|
||||
go build genfiles.go
|
||||
|
||||
start() {
|
||||
echo "Starting..."
|
||||
STTRACE=model,scanner STPROFILER=":9091" syncthing -home "f1" > 1.out 2>&1 &
|
||||
STTRACE=model,scanner STPROFILER=":9092" syncthing -home "f2" > 2.out 2>&1 &
|
||||
sleep 1
|
||||
}
|
||||
|
||||
stop() {
|
||||
echo "Stopping..."
|
||||
for i in 1 2 ; do
|
||||
curl -HX-API-Key:abc123 -X POST "http://localhost:808$i/rest/shutdown"
|
||||
done
|
||||
sleep 1
|
||||
}
|
||||
|
||||
setup() {
|
||||
echo "Setting up..."
|
||||
rm -rf s? s??-?
|
||||
rm -rf f?/*.idx.gz f?/index
|
||||
mkdir -p s1
|
||||
pushd s1 >/dev/null
|
||||
../genfiles
|
||||
../md5r > ../md5-1
|
||||
popd >/dev/null
|
||||
}
|
||||
|
||||
testConvergence() {
|
||||
torestart="$1"
|
||||
prevcomp=0
|
||||
|
||||
while true ; do
|
||||
sleep 5
|
||||
comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/debug/peerCompletion" | ./json "$id2")
|
||||
comp=${comp:-0}
|
||||
echo $comp / 100
|
||||
|
||||
if [[ $comp == 100 ]] ; then
|
||||
echo Done
|
||||
break
|
||||
fi
|
||||
|
||||
# Restart if the destination has made some progress
|
||||
if [[ $comp -gt $prevcomp ]] ; then
|
||||
prevcomp=$comp
|
||||
curl -HX-API-Key:abc123 -X POST "http://localhost:$torestart/rest/restart"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Verifying..."
|
||||
|
||||
pushd s2 >/dev/null
|
||||
../md5r | grep -v .stversions > ../md5-2
|
||||
popd >/dev/null
|
||||
|
||||
if ! cmp md5-1 md5-2 ; then
|
||||
echo Repos differ
|
||||
stop
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo Testing reconnects during pull where the source node restarts
|
||||
setup
|
||||
start
|
||||
testConvergence 8081
|
||||
stop
|
||||
|
||||
echo Testing reconnects during pull where the destination node restarts
|
||||
setup
|
||||
start
|
||||
testConvergence 8082
|
||||
stop
|
||||
|
||||
exit 0
|
||||
144
model/model.go
@@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -55,7 +56,12 @@ func (s repoState) String() string {
|
||||
const zeroEntrySize = 128
|
||||
|
||||
// How many files to send in each Index/IndexUpdate message.
|
||||
const indexBatchSize = 1000
|
||||
const (
|
||||
indexTargetSize = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed)
|
||||
indexPerFileSize = 250 // Each FileInfo is approximately this big, in bytes, excluding BlockInfos
|
||||
IndexPerBlockSize = 40 // Each BlockInfo is approximately this big
|
||||
indexBatchSize = 1000 // Either way, don't include more files than this
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
indexDir string
|
||||
@@ -65,12 +71,11 @@ type Model struct {
|
||||
clientName string
|
||||
clientVersion string
|
||||
|
||||
repoCfgs map[string]config.RepositoryConfiguration // repo -> cfg
|
||||
repoFiles map[string]*files.Set // repo -> files
|
||||
repoNodes map[string][]protocol.NodeID // repo -> nodeIDs
|
||||
nodeRepos map[protocol.NodeID][]string // nodeID -> repos
|
||||
suppressor map[string]*suppressor // repo -> suppressor
|
||||
rmut sync.RWMutex // protects the above
|
||||
repoCfgs map[string]config.RepositoryConfiguration // repo -> cfg
|
||||
repoFiles map[string]*files.Set // repo -> files
|
||||
repoNodes map[string][]protocol.NodeID // repo -> nodeIDs
|
||||
nodeRepos map[protocol.NodeID][]string // nodeID -> repos
|
||||
rmut sync.RWMutex // protects the above
|
||||
|
||||
repoState map[string]repoState // repo -> state
|
||||
repoStateChanged map[string]time.Time // repo -> time when state changed
|
||||
@@ -84,8 +89,6 @@ type Model struct {
|
||||
sentLocalVer map[protocol.NodeID]map[string]uint64
|
||||
slMut sync.Mutex
|
||||
|
||||
sup suppressor
|
||||
|
||||
addedRepo bool
|
||||
started bool
|
||||
}
|
||||
@@ -111,12 +114,10 @@ func NewModel(indexDir string, cfg *config.Configuration, clientName, clientVers
|
||||
nodeRepos: make(map[protocol.NodeID][]string),
|
||||
repoState: make(map[string]repoState),
|
||||
repoStateChanged: make(map[string]time.Time),
|
||||
suppressor: make(map[string]*suppressor),
|
||||
protoConn: make(map[protocol.NodeID]protocol.Connection),
|
||||
rawConn: make(map[protocol.NodeID]io.Closer),
|
||||
nodeVer: make(map[protocol.NodeID]string),
|
||||
sentLocalVer: make(map[protocol.NodeID]map[string]uint64),
|
||||
sup: suppressor{threshold: int64(cfg.Options.MaxChangeKbps)},
|
||||
}
|
||||
|
||||
var timeout = 20 * 60 // seconds
|
||||
@@ -199,34 +200,39 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
||||
// Returns the completion status, in percent, for the given node and repo.
|
||||
func (m *Model) Completion(node protocol.NodeID, repo string) float64 {
|
||||
var tot int64
|
||||
m.repoFiles[repo].WithGlobal(func(f protocol.FileInfo) bool {
|
||||
if !protocol.IsDeleted(f.Flags) {
|
||||
var size int64
|
||||
if protocol.IsDirectory(f.Flags) {
|
||||
size = zeroEntrySize
|
||||
} else {
|
||||
size = f.Size()
|
||||
}
|
||||
tot += size
|
||||
|
||||
m.rmut.RLock()
|
||||
rf, ok := m.repoFiles[repo]
|
||||
m.rmut.RUnlock()
|
||||
if !ok {
|
||||
return 0 // Repo doesn't exist, so we hardly have any of it
|
||||
}
|
||||
|
||||
rf.WithGlobalTruncated(func(f protocol.FileIntf) bool {
|
||||
if !f.IsDeleted() {
|
||||
tot += f.Size()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if tot == 0 {
|
||||
return 100 // Repo is empty, so we have all of it
|
||||
}
|
||||
|
||||
var need int64
|
||||
m.repoFiles[repo].WithNeed(node, func(f protocol.FileInfo) bool {
|
||||
if !protocol.IsDeleted(f.Flags) {
|
||||
var size int64
|
||||
if protocol.IsDirectory(f.Flags) {
|
||||
size = zeroEntrySize
|
||||
} else {
|
||||
size = f.Size()
|
||||
}
|
||||
need += size
|
||||
rf.WithNeedTruncated(node, func(f protocol.FileIntf) bool {
|
||||
if !f.IsDeleted() {
|
||||
need += f.Size()
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return 100 * (1 - float64(need)/float64(tot))
|
||||
res := 100 * (1 - float64(need)/float64(tot))
|
||||
if debug {
|
||||
l.Debugf("Completion(%s, %q): %f (%d / %d)", node, repo, res, need, tot)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func sizeOf(fs []protocol.FileInfo) (files, deleted int, bytes int64) {
|
||||
@@ -239,18 +245,13 @@ func sizeOf(fs []protocol.FileInfo) (files, deleted int, bytes int64) {
|
||||
return
|
||||
}
|
||||
|
||||
func sizeOfFile(f protocol.FileInfo) (files, deleted int, bytes int64) {
|
||||
if !protocol.IsDeleted(f.Flags) {
|
||||
func sizeOfFile(f protocol.FileIntf) (files, deleted int, bytes int64) {
|
||||
if !f.IsDeleted() {
|
||||
files++
|
||||
if !protocol.IsDirectory(f.Flags) {
|
||||
bytes += f.Size()
|
||||
} else {
|
||||
bytes += zeroEntrySize
|
||||
}
|
||||
} else {
|
||||
deleted++
|
||||
bytes += zeroEntrySize
|
||||
}
|
||||
bytes += f.Size()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -260,7 +261,7 @@ func (m *Model) GlobalSize(repo string) (files, deleted int, bytes int64) {
|
||||
m.rmut.RLock()
|
||||
defer m.rmut.RUnlock()
|
||||
if rf, ok := m.repoFiles[repo]; ok {
|
||||
rf.WithGlobal(func(f protocol.FileInfo) bool {
|
||||
rf.WithGlobalTruncated(func(f protocol.FileIntf) bool {
|
||||
fs, de, by := sizeOfFile(f)
|
||||
files += fs
|
||||
deleted += de
|
||||
@@ -277,7 +278,7 @@ func (m *Model) LocalSize(repo string) (files, deleted int, bytes int64) {
|
||||
m.rmut.RLock()
|
||||
defer m.rmut.RUnlock()
|
||||
if rf, ok := m.repoFiles[repo]; ok {
|
||||
rf.WithHave(protocol.LocalNodeID, func(f protocol.FileInfo) bool {
|
||||
rf.WithHaveTruncated(protocol.LocalNodeID, func(f protocol.FileIntf) bool {
|
||||
fs, de, by := sizeOfFile(f)
|
||||
files += fs
|
||||
deleted += de
|
||||
@@ -293,13 +294,16 @@ func (m *Model) NeedSize(repo string) (files int, bytes int64) {
|
||||
m.rmut.RLock()
|
||||
defer m.rmut.RUnlock()
|
||||
if rf, ok := m.repoFiles[repo]; ok {
|
||||
rf.WithNeed(protocol.LocalNodeID, func(f protocol.FileInfo) bool {
|
||||
rf.WithNeedTruncated(protocol.LocalNodeID, func(f protocol.FileIntf) bool {
|
||||
fs, de, by := sizeOfFile(f)
|
||||
files += fs + de
|
||||
bytes += by
|
||||
return true
|
||||
})
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("NeedSize(%q): %d %d", repo, files, bytes)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -309,8 +313,8 @@ func (m *Model) NeedFilesRepo(repo string) []protocol.FileInfo {
|
||||
defer m.rmut.RUnlock()
|
||||
if rf, ok := m.repoFiles[repo]; ok {
|
||||
fs := make([]protocol.FileInfo, 0, indexBatchSize)
|
||||
rf.WithNeed(protocol.LocalNodeID, func(f protocol.FileInfo) bool {
|
||||
fs = append(fs, f)
|
||||
rf.WithNeed(protocol.LocalNodeID, func(f protocol.FileIntf) bool {
|
||||
fs = append(fs, f.(protocol.FileInfo))
|
||||
return len(fs) < indexBatchSize
|
||||
})
|
||||
return fs
|
||||
@@ -359,7 +363,7 @@ func (m *Model) IndexUpdate(nodeID protocol.NodeID, repo string, fs []protocol.F
|
||||
}
|
||||
|
||||
if !m.repoSharedWith(repo, nodeID) {
|
||||
l.Warnf("Unexpected repository ID %q sent from node %q; ensure that the repository exists and that this node is selected under \"Share With\" in the repository configuration.", repo, nodeID)
|
||||
l.Infof("Update for unexpected repository ID %q sent from node %q; ensure that the repository exists and that this node is selected under \"Share With\" in the repository configuration.", repo, nodeID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -580,10 +584,12 @@ func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, rep
|
||||
nodeID := conn.ID()
|
||||
name := conn.Name()
|
||||
batch := make([]protocol.FileInfo, 0, indexBatchSize)
|
||||
currentBatchSize := 0
|
||||
maxLocalVer := uint64(0)
|
||||
var err error
|
||||
|
||||
fs.WithHave(protocol.LocalNodeID, func(f protocol.FileInfo) bool {
|
||||
fs.WithHave(protocol.LocalNodeID, func(fi protocol.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfo)
|
||||
if f.LocalVersion <= minLocalVer {
|
||||
return true
|
||||
}
|
||||
@@ -592,13 +598,13 @@ func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, rep
|
||||
maxLocalVer = f.LocalVersion
|
||||
}
|
||||
|
||||
if len(batch) == indexBatchSize {
|
||||
if len(batch) == indexBatchSize || currentBatchSize > indexTargetSize {
|
||||
if initial {
|
||||
if err = conn.Index(repo, batch); err != nil {
|
||||
return false
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("sendIndexes for %s-%s/%q: %d files (initial index)", nodeID, name, repo, len(batch))
|
||||
l.Debugf("sendIndexes for %s-%s/%q: %d files (<%d bytes) (initial index)", nodeID, name, repo, len(batch), currentBatchSize)
|
||||
}
|
||||
initial = false
|
||||
} else {
|
||||
@@ -606,14 +612,16 @@ func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, rep
|
||||
return false
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("sendIndexes for %s-%s/%q: %d files (batched update)", nodeID, name, repo, len(batch))
|
||||
l.Debugf("sendIndexes for %s-%s/%q: %d files (<%d bytes) (batched update)", nodeID, name, repo, len(batch), currentBatchSize)
|
||||
}
|
||||
}
|
||||
|
||||
batch = make([]protocol.FileInfo, 0, indexBatchSize)
|
||||
currentBatchSize = 0
|
||||
}
|
||||
|
||||
batch = append(batch, f)
|
||||
currentBatchSize += indexPerFileSize + len(f.Blocks)*IndexPerBlockSize
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -673,7 +681,6 @@ func (m *Model) AddRepo(cfg config.RepositoryConfiguration) {
|
||||
m.rmut.Lock()
|
||||
m.repoCfgs[cfg.ID] = cfg
|
||||
m.repoFiles[cfg.ID] = files.NewSet(cfg.ID, m.db)
|
||||
m.suppressor[cfg.ID] = &suppressor{threshold: int64(m.cfg.Options.MaxChangeKbps)}
|
||||
|
||||
m.repoNodes[cfg.ID] = make([]protocol.NodeID, len(cfg.Nodes))
|
||||
for i, node := range cfg.Nodes {
|
||||
@@ -732,20 +739,31 @@ func (m *Model) CleanRepos() {
|
||||
}
|
||||
|
||||
func (m *Model) ScanRepo(repo string) error {
|
||||
return m.ScanRepoSub(repo, "")
|
||||
}
|
||||
|
||||
func (m *Model) ScanRepoSub(repo, sub string) error {
|
||||
if p := filepath.Clean(filepath.Join(repo, sub)); !strings.HasPrefix(p, repo) {
|
||||
return errors.New("invalid subpath")
|
||||
}
|
||||
|
||||
m.rmut.RLock()
|
||||
fs := m.repoFiles[repo]
|
||||
fs, ok := m.repoFiles[repo]
|
||||
dir := m.repoCfgs[repo].Directory
|
||||
|
||||
w := &scanner.Walker{
|
||||
Dir: dir,
|
||||
Sub: sub,
|
||||
IgnoreFile: ".stignore",
|
||||
BlockSize: scanner.StandardBlockSize,
|
||||
TempNamer: defTempNamer,
|
||||
Suppressor: m.suppressor[repo],
|
||||
CurrentFiler: cFiler{m, repo},
|
||||
IgnorePerms: m.repoCfgs[repo].IgnorePerms,
|
||||
}
|
||||
m.rmut.RUnlock()
|
||||
if !ok {
|
||||
return errors.New("no such repo")
|
||||
}
|
||||
|
||||
m.setState(repo, RepoScanning)
|
||||
fchan, _, err := w.Walk()
|
||||
@@ -774,7 +792,14 @@ func (m *Model) ScanRepo(repo string) error {
|
||||
}
|
||||
|
||||
batch = batch[:0]
|
||||
fs.WithHave(protocol.LocalNodeID, func(f protocol.FileInfo) bool {
|
||||
// TODO: We should limit the Have scanning to start at sub
|
||||
seenPrefix := false
|
||||
fs.WithHaveTruncated(protocol.LocalNodeID, func(fi protocol.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfoTruncated)
|
||||
if !strings.HasPrefix(f.Name, sub) {
|
||||
return !seenPrefix
|
||||
}
|
||||
seenPrefix = true
|
||||
if !protocol.IsDeleted(f.Flags) {
|
||||
if len(batch) == batchSize {
|
||||
fs.Update(protocol.LocalNodeID, batch)
|
||||
@@ -782,10 +807,12 @@ func (m *Model) ScanRepo(repo string) error {
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, f.Name)); err != nil && os.IsNotExist(err) {
|
||||
// File has been deleted
|
||||
f.Blocks = nil
|
||||
f.Flags |= protocol.FlagDeleted
|
||||
f.Version = lamport.Default.Tick(f.Version)
|
||||
f.LocalVersion = 0
|
||||
nf := protocol.FileInfo{
|
||||
Name: f.Name,
|
||||
Flags: f.Flags | protocol.FlagDeleted,
|
||||
Modified: f.Modified,
|
||||
Version: lamport.Default.Tick(f.Version),
|
||||
}
|
||||
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
|
||||
"repo": repo,
|
||||
"name": f.Name,
|
||||
@@ -793,7 +820,7 @@ func (m *Model) ScanRepo(repo string) error {
|
||||
"flags": fmt.Sprintf("0%o", f.Flags),
|
||||
"size": f.Size(),
|
||||
})
|
||||
batch = append(batch, f)
|
||||
batch = append(batch, nf)
|
||||
}
|
||||
}
|
||||
return true
|
||||
@@ -866,7 +893,8 @@ func (m *Model) Override(repo string) {
|
||||
m.rmut.RUnlock()
|
||||
|
||||
batch := make([]protocol.FileInfo, 0, indexBatchSize)
|
||||
fs.WithNeed(protocol.LocalNodeID, func(need protocol.FileInfo) bool {
|
||||
fs.WithNeed(protocol.LocalNodeID, func(fi protocol.FileIntf) bool {
|
||||
need := fi.(protocol.FileInfo)
|
||||
if len(batch) == indexBatchSize {
|
||||
fs.Update(protocol.LocalNodeID, batch)
|
||||
batch = batch[:0]
|
||||
|
||||
108
model/puller.go
@@ -2,11 +2,31 @@
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
__ __ _ _
|
||||
\ \ / /_ _ _ __ _ __ (_)_ __ __ _| |
|
||||
\ \ /\ / / _` | '__| '_ \| | '_ \ / _` | |
|
||||
\ V V / (_| | | | | | | | | | | (_| |_|
|
||||
\_/\_/ \__,_|_| |_| |_|_|_| |_|\__, (_)
|
||||
|___/
|
||||
|
||||
The code in this file is a piece of crap. Don't base anything on it.
|
||||
Refactorin ongoing in new-puller branch.
|
||||
|
||||
__ __ _ _
|
||||
\ \ / /_ _ _ __ _ __ (_)_ __ __ _| |
|
||||
\ \ /\ / / _` | '__| '_ \| | '_ \ / _` | |
|
||||
\ V V / (_| | | | | | | | | | | (_| |_|
|
||||
\_/\_/ \__,_|_| |_| |_|_|_| |_|\__, (_)
|
||||
|___/
|
||||
*/
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -72,6 +92,7 @@ type puller struct {
|
||||
blocks chan bqBlock
|
||||
requestResults chan requestResult
|
||||
versioner versioner.Versioner
|
||||
errors int
|
||||
}
|
||||
|
||||
func newPuller(repoCfg config.RepositoryConfiguration, model *Model, slots int, cfg *config.Configuration) *puller {
|
||||
@@ -128,6 +149,7 @@ func (p *puller) run() {
|
||||
|
||||
prevVer, queued = p.queueNeededBlocks(prevVer)
|
||||
if queued > 0 {
|
||||
p.errors = 0
|
||||
|
||||
pull:
|
||||
for {
|
||||
@@ -145,6 +167,11 @@ func (p *puller) run() {
|
||||
if debug {
|
||||
l.Debugf("%q: pulling loop needs more blocks", p.repoCfg.ID)
|
||||
}
|
||||
|
||||
if p.errors > 0 && p.errors >= queued {
|
||||
break pull
|
||||
}
|
||||
|
||||
prevVer, _ = p.queueNeededBlocks(prevVer)
|
||||
b, ok = p.bq.get()
|
||||
}
|
||||
@@ -179,6 +206,12 @@ func (p *puller) run() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if p.errors > 0 && p.errors >= queued {
|
||||
l.Warnf("All remaining files failed to sync. Stopping repo %q.", p.repoCfg.ID)
|
||||
invalidateRepo(p.cfg, p.repoCfg.ID, errors.New("too many errors, check logs"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
@@ -365,7 +398,8 @@ func (p *puller) handleBlock(b bqBlock) bool {
|
||||
}
|
||||
err = os.MkdirAll(path, os.FileMode(f.Flags&0777))
|
||||
if err != nil {
|
||||
l.Warnf("Create folder: %q: %v", path, err)
|
||||
p.errors++
|
||||
l.Infof("mkdir: error: %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
} else if debug {
|
||||
@@ -384,13 +418,13 @@ func (p *puller) handleBlock(b bqBlock) bool {
|
||||
fp := filepath.Join(p.repoCfg.Directory, f.Name)
|
||||
t := time.Unix(f.Modified, 0)
|
||||
err := os.Chtimes(fp, t, t)
|
||||
if debug && err != nil {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
if err != nil {
|
||||
l.Infof("chtimes: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) {
|
||||
err = os.Chmod(fp, os.FileMode(f.Flags&0777))
|
||||
if debug && err != nil {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
if err != nil {
|
||||
l.Infof("chmod: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,16 +458,20 @@ func (p *puller) handleBlock(b bqBlock) bool {
|
||||
_, err := os.Stat(dirName)
|
||||
if err != nil {
|
||||
err = os.MkdirAll(dirName, 0777)
|
||||
} else {
|
||||
// We need to make sure the directory is writeable so we can create files in it
|
||||
if dirName != p.repoCfg.Directory {
|
||||
err = os.Chmod(dirName, 0777)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
l.Infof("mkdir: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
|
||||
of.file, of.err = os.Create(of.temp)
|
||||
if of.err != nil {
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
}
|
||||
p.errors++
|
||||
l.Infof("create: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
if !b.last {
|
||||
p.openFiles[f.Name] = of
|
||||
}
|
||||
@@ -482,9 +520,8 @@ func (p *puller) handleCopyBlock(b bqBlock) {
|
||||
var exfd *os.File
|
||||
exfd, of.err = os.Open(of.filepath)
|
||||
if of.err != nil {
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
}
|
||||
p.errors++
|
||||
l.Infof("open: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
of.file.Close()
|
||||
of.file = nil
|
||||
|
||||
@@ -500,9 +537,8 @@ func (p *puller) handleCopyBlock(b bqBlock) {
|
||||
_, of.err = of.file.WriteAt(bs, b.Offset)
|
||||
}
|
||||
if of.err != nil {
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
}
|
||||
p.errors++
|
||||
l.Infof("write: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
exfd.Close()
|
||||
of.file.Close()
|
||||
of.file = nil
|
||||
@@ -585,7 +621,13 @@ func (p *puller) handleEmptyBlock(b bqBlock) {
|
||||
l.Debugf("pull: delete %q", f.Name)
|
||||
}
|
||||
os.Remove(of.temp)
|
||||
|
||||
// Ensure the file and the directory it is in is writeable so we can remove the file
|
||||
dirName := filepath.Dir(of.filepath)
|
||||
os.Chmod(of.filepath, 0666)
|
||||
if dirName != p.repoCfg.Directory {
|
||||
os.Chmod(dirName, 0777)
|
||||
}
|
||||
if p.versioner != nil {
|
||||
if debug {
|
||||
l.Debugln("pull: deleting with versioner")
|
||||
@@ -630,10 +672,17 @@ func (p *puller) queueNeededBlocks(prevVer uint64) (uint64, int) {
|
||||
}
|
||||
|
||||
queued := 0
|
||||
files := make([]protocol.FileInfo, 0, indexBatchSize)
|
||||
for _, f := range p.model.NeedFilesRepo(p.repoCfg.ID) {
|
||||
if _, ok := p.openFiles[f.Name]; ok {
|
||||
continue
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
perm := rand.Perm(len(files))
|
||||
for _, idx := range perm {
|
||||
f := files[idx]
|
||||
lf := p.model.CurrentRepoFile(p.repoCfg.ID, f.Name)
|
||||
have, need := scanner.BlockDiff(lf.Blocks, f.Blocks)
|
||||
if debug {
|
||||
@@ -646,6 +695,7 @@ func (p *puller) queueNeededBlocks(prevVer uint64) (uint64, int) {
|
||||
need: need,
|
||||
})
|
||||
}
|
||||
|
||||
if debug && queued > 0 {
|
||||
l.Debugf("%q: queued %d items", p.repoCfg.ID, queued)
|
||||
}
|
||||
@@ -663,19 +713,22 @@ func (p *puller) closeFile(f protocol.FileInfo) {
|
||||
}
|
||||
|
||||
of := p.openFiles[f.Name]
|
||||
of.file.Close()
|
||||
err := of.file.Close()
|
||||
if err != nil {
|
||||
p.errors++
|
||||
l.Infof("close: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
defer os.Remove(of.temp)
|
||||
|
||||
delete(p.openFiles, f.Name)
|
||||
|
||||
fd, err := os.Open(of.temp)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
p.errors++
|
||||
l.Infof("open: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
return
|
||||
}
|
||||
hb, _ := scanner.Blocks(fd, scanner.StandardBlockSize)
|
||||
hb, _ := scanner.Blocks(fd, scanner.StandardBlockSize, f.Size())
|
||||
fd.Close()
|
||||
|
||||
if l0, l1 := len(hb), len(f.Blocks); l0 != l1 {
|
||||
@@ -687,20 +740,22 @@ func (p *puller) closeFile(f protocol.FileInfo) {
|
||||
|
||||
for i := range hb {
|
||||
if bytes.Compare(hb[i].Hash, f.Blocks[i].Hash) != 0 {
|
||||
l.Debugf("pull: %q / %q: block %d hash mismatch\n\thave: %x\n\twant: %x", p.repoCfg.ID, f.Name, i, hb[i].Hash, f.Blocks[i].Hash)
|
||||
if debug {
|
||||
l.Debugf("pull: %q / %q: block %d hash mismatch\n have: %x\n want: %x", p.repoCfg.ID, f.Name, i, hb[i].Hash, f.Blocks[i].Hash)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t := time.Unix(f.Modified, 0)
|
||||
err = os.Chtimes(of.temp, t, t)
|
||||
if debug && err != nil {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
if err != nil {
|
||||
l.Infof("chtimes: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) {
|
||||
err = os.Chmod(of.temp, os.FileMode(f.Flags&0777))
|
||||
if debug && err != nil {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
if err != nil {
|
||||
l.Infof("chmod: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,7 +777,8 @@ func (p *puller) closeFile(f protocol.FileInfo) {
|
||||
if err := osutil.Rename(of.temp, of.filepath); err == nil {
|
||||
p.model.updateLocal(p.repoCfg.ID, f)
|
||||
} else {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
p.errors++
|
||||
l.Infof("rename: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxChangeHistory = 4
|
||||
)
|
||||
|
||||
type change struct {
|
||||
size int64
|
||||
when time.Time
|
||||
}
|
||||
|
||||
type changeHistory struct {
|
||||
changes []change
|
||||
next int64
|
||||
prevSup bool
|
||||
}
|
||||
|
||||
type suppressor struct {
|
||||
sync.Mutex
|
||||
changes map[string]changeHistory
|
||||
threshold int64 // bytes/s
|
||||
}
|
||||
|
||||
func (h changeHistory) bandwidth(t time.Time) int64 {
|
||||
if len(h.changes) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var t0 = h.changes[0].when
|
||||
if t == t0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var bw float64
|
||||
for _, c := range h.changes {
|
||||
bw += float64(c.size)
|
||||
}
|
||||
return int64(bw / t.Sub(t0).Seconds())
|
||||
}
|
||||
|
||||
func (h *changeHistory) append(size int64, t time.Time) {
|
||||
c := change{size, t}
|
||||
if len(h.changes) == MaxChangeHistory {
|
||||
h.changes = h.changes[1:MaxChangeHistory]
|
||||
}
|
||||
h.changes = append(h.changes, c)
|
||||
}
|
||||
|
||||
func (s *suppressor) Suppress(name string, fi os.FileInfo) (cur, prev bool) {
|
||||
return s.suppress(name, fi.Size(), time.Now())
|
||||
}
|
||||
|
||||
func (s *suppressor) suppress(name string, size int64, t time.Time) (bool, bool) {
|
||||
s.Lock()
|
||||
|
||||
if s.changes == nil {
|
||||
s.changes = make(map[string]changeHistory)
|
||||
}
|
||||
h := s.changes[name]
|
||||
sup := h.bandwidth(t) > s.threshold
|
||||
prevSup := h.prevSup
|
||||
h.prevSup = sup
|
||||
if !sup {
|
||||
h.append(size, t)
|
||||
}
|
||||
s.changes[name] = h
|
||||
|
||||
s.Unlock()
|
||||
|
||||
return sup, prevSup
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSuppressor(t *testing.T) {
|
||||
s := suppressor{threshold: 10000}
|
||||
t0 := time.Now()
|
||||
|
||||
t1 := t0
|
||||
sup, prev := s.suppress("foo", 10000, t1)
|
||||
if sup {
|
||||
t.Fatal("Never suppress first change")
|
||||
}
|
||||
if prev {
|
||||
t.Fatal("Incorrect prev status")
|
||||
}
|
||||
|
||||
// bw is 10000 / 10 = 1000
|
||||
t1 = t0.Add(10 * time.Second)
|
||||
if bw := s.changes["foo"].bandwidth(t1); bw != 1000 {
|
||||
t.Errorf("Incorrect bw %d", bw)
|
||||
}
|
||||
sup, prev = s.suppress("foo", 10000, t1)
|
||||
if sup {
|
||||
t.Fatal("Should still be fine")
|
||||
}
|
||||
if prev {
|
||||
t.Fatal("Incorrect prev status")
|
||||
}
|
||||
|
||||
// bw is (10000 + 10000) / 11 = 1818
|
||||
t1 = t0.Add(11 * time.Second)
|
||||
if bw := s.changes["foo"].bandwidth(t1); bw != 1818 {
|
||||
t.Errorf("Incorrect bw %d", bw)
|
||||
}
|
||||
sup, prev = s.suppress("foo", 100500, t1)
|
||||
if sup {
|
||||
t.Fatal("Should still be fine")
|
||||
}
|
||||
if prev {
|
||||
t.Fatal("Incorrect prev status")
|
||||
}
|
||||
|
||||
// bw is (10000 + 10000 + 100500) / 12 = 10041
|
||||
t1 = t0.Add(12 * time.Second)
|
||||
if bw := s.changes["foo"].bandwidth(t1); bw != 10041 {
|
||||
t.Errorf("Incorrect bw %d", bw)
|
||||
}
|
||||
sup, prev = s.suppress("foo", 10000000, t1) // value will be ignored
|
||||
if !sup {
|
||||
t.Fatal("Should be over threshold")
|
||||
}
|
||||
if prev {
|
||||
t.Fatal("Incorrect prev status")
|
||||
}
|
||||
|
||||
// bw is (10000 + 10000 + 100500) / 15 = 8033
|
||||
t1 = t0.Add(15 * time.Second)
|
||||
if bw := s.changes["foo"].bandwidth(t1); bw != 8033 {
|
||||
t.Errorf("Incorrect bw %d", bw)
|
||||
}
|
||||
sup, prev = s.suppress("foo", 10000000, t1)
|
||||
if sup {
|
||||
t.Fatal("Should be Ok")
|
||||
}
|
||||
if !prev {
|
||||
t.Fatal("Incorrect prev status")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHistory(t *testing.T) {
|
||||
h := changeHistory{}
|
||||
|
||||
t0 := time.Now()
|
||||
h.append(40, t0)
|
||||
|
||||
if l := len(h.changes); l != 1 {
|
||||
t.Errorf("Incorrect history length %d", l)
|
||||
}
|
||||
if s := h.changes[0].size; s != 40 {
|
||||
t.Errorf("Incorrect first record size %d", s)
|
||||
}
|
||||
|
||||
for i := 1; i < MaxChangeHistory; i++ {
|
||||
h.append(int64(40+i), t0.Add(time.Duration(i)*time.Second))
|
||||
}
|
||||
|
||||
if l := len(h.changes); l != MaxChangeHistory {
|
||||
t.Errorf("Incorrect history length %d", l)
|
||||
}
|
||||
if s := h.changes[0].size; s != 40 {
|
||||
t.Errorf("Incorrect first record size %d", s)
|
||||
}
|
||||
if s := h.changes[MaxChangeHistory-1].size; s != 40+MaxChangeHistory-1 {
|
||||
t.Errorf("Incorrect last record size %d", s)
|
||||
}
|
||||
|
||||
h.append(999, t0.Add(time.Duration(999)*time.Second))
|
||||
|
||||
if l := len(h.changes); l != MaxChangeHistory {
|
||||
t.Errorf("Incorrect history length %d", l)
|
||||
}
|
||||
if s := h.changes[0].size; s != 41 {
|
||||
t.Errorf("Incorrect first record size %d", s)
|
||||
}
|
||||
if s := h.changes[MaxChangeHistory-1].size; s != 999 {
|
||||
t.Errorf("Incorrect last record size %d", s)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,12 +26,50 @@ func (f FileInfo) String() string {
|
||||
}
|
||||
|
||||
func (f FileInfo) Size() (bytes int64) {
|
||||
if IsDeleted(f.Flags) || IsDirectory(f.Flags) {
|
||||
return 128
|
||||
}
|
||||
for _, b := range f.Blocks {
|
||||
bytes += int64(b.Size)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f FileInfo) IsDeleted() bool {
|
||||
return IsDeleted(f.Flags)
|
||||
}
|
||||
|
||||
// Used for unmarshalling a FileInfo structure but skipping the actual block list
|
||||
type FileInfoTruncated struct {
|
||||
Name string // max:1024
|
||||
Flags uint32
|
||||
Modified int64
|
||||
Version uint64
|
||||
LocalVersion uint64
|
||||
NumBlocks uint32
|
||||
}
|
||||
|
||||
// Returns a statistical guess on the size, not the exact figure
|
||||
func (f FileInfoTruncated) Size() int64 {
|
||||
if IsDeleted(f.Flags) || IsDirectory(f.Flags) {
|
||||
return 128
|
||||
}
|
||||
if f.NumBlocks < 2 {
|
||||
return BlockSize / 2
|
||||
} else {
|
||||
return int64(f.NumBlocks-1)*BlockSize + BlockSize/2
|
||||
}
|
||||
}
|
||||
|
||||
func (f FileInfoTruncated) IsDeleted() bool {
|
||||
return IsDeleted(f.Flags)
|
||||
}
|
||||
|
||||
type FileIntf interface {
|
||||
Size() int64
|
||||
IsDeleted() bool
|
||||
}
|
||||
|
||||
type BlockInfo struct {
|
||||
Offset int64 // noencode (cache only)
|
||||
Size uint32
|
||||
|
||||
@@ -199,6 +199,98 @@ func (o *FileInfo) decodeXDR(xr *xdr.Reader) error {
|
||||
|
||||
/*
|
||||
|
||||
FileInfoTruncated Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of Name |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Name (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Flags |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
+ Modified (64 bits) +
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
+ Version (64 bits) +
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
+ Local Version (64 bits) +
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Num Blocks |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct FileInfoTruncated {
|
||||
string Name<1024>;
|
||||
unsigned int Flags;
|
||||
hyper Modified;
|
||||
unsigned hyper Version;
|
||||
unsigned hyper LocalVersion;
|
||||
unsigned int NumBlocks;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (o FileInfoTruncated) EncodeXDR(w io.Writer) (int, error) {
|
||||
var xw = xdr.NewWriter(w)
|
||||
return o.encodeXDR(xw)
|
||||
}
|
||||
|
||||
func (o FileInfoTruncated) MarshalXDR() []byte {
|
||||
return o.AppendXDR(make([]byte, 0, 128))
|
||||
}
|
||||
|
||||
func (o FileInfoTruncated) AppendXDR(bs []byte) []byte {
|
||||
var aw = xdr.AppendWriter(bs)
|
||||
var xw = xdr.NewWriter(&aw)
|
||||
o.encodeXDR(xw)
|
||||
return []byte(aw)
|
||||
}
|
||||
|
||||
func (o FileInfoTruncated) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
if len(o.Name) > 1024 {
|
||||
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||
}
|
||||
xw.WriteString(o.Name)
|
||||
xw.WriteUint32(o.Flags)
|
||||
xw.WriteUint64(uint64(o.Modified))
|
||||
xw.WriteUint64(o.Version)
|
||||
xw.WriteUint64(o.LocalVersion)
|
||||
xw.WriteUint32(o.NumBlocks)
|
||||
return xw.Tot(), xw.Error()
|
||||
}
|
||||
|
||||
func (o *FileInfoTruncated) DecodeXDR(r io.Reader) error {
|
||||
xr := xdr.NewReader(r)
|
||||
return o.decodeXDR(xr)
|
||||
}
|
||||
|
||||
func (o *FileInfoTruncated) UnmarshalXDR(bs []byte) error {
|
||||
var br = bytes.NewReader(bs)
|
||||
var xr = xdr.NewReader(br)
|
||||
return o.decodeXDR(xr)
|
||||
}
|
||||
|
||||
func (o *FileInfoTruncated) decodeXDR(xr *xdr.Reader) error {
|
||||
o.Name = xr.ReadStringMax(1024)
|
||||
o.Flags = xr.ReadUint32()
|
||||
o.Modified = int64(xr.ReadUint64())
|
||||
o.Version = xr.ReadUint64()
|
||||
o.LocalVersion = xr.ReadUint64()
|
||||
o.NumBlocks = xr.ReadUint32()
|
||||
return xr.Error()
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
BlockInfo Structure:
|
||||
|
||||
0 1 2 3
|
||||
|
||||
@@ -7,6 +7,7 @@ package protocol
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -17,7 +18,6 @@ import (
|
||||
"testing/quick"
|
||||
|
||||
"github.com/calmh/xdr"
|
||||
pretty "github.com/tonnerre/golang-pretty"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -346,12 +346,10 @@ func testMarshal(t *testing.T, prefix string, m1, m2 message) bool {
|
||||
var buf bytes.Buffer
|
||||
|
||||
failed := func(bc []byte) {
|
||||
f, _ := os.Create(prefix + "-1.txt")
|
||||
pretty.Fprintf(f, "%# v", m1)
|
||||
f.Close()
|
||||
f, _ = os.Create(prefix + "-2.txt")
|
||||
pretty.Fprintf(f, "%# v", m2)
|
||||
f.Close()
|
||||
bs, _ := json.MarshalIndent(m1, "", " ")
|
||||
ioutil.WriteFile(prefix+"-1.txt", bs, 0644)
|
||||
bs, _ = json.MarshalIndent(m2, "", " ")
|
||||
ioutil.WriteFile(prefix+"-2.txt", bs, 0644)
|
||||
if len(bc) > 0 {
|
||||
f, _ := os.Create(prefix + "-data.txt")
|
||||
fmt.Fprint(f, hex.Dump(bc))
|
||||
|
||||
@@ -49,7 +49,15 @@ func hashFile(dir string, blockSize int, outbox, inbox chan protocol.FileInfo) {
|
||||
continue
|
||||
}
|
||||
|
||||
blocks, err := Blocks(fd, blockSize)
|
||||
fi, err := fd.Stat()
|
||||
if err != nil {
|
||||
fd.Close()
|
||||
if debug {
|
||||
l.Debugln("stat:", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
blocks, err := Blocks(fd, blockSize, fi.Size())
|
||||
fd.Close()
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -17,12 +17,15 @@ const StandardBlockSize = 128 * 1024
|
||||
var sha256OfNothing = []uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}
|
||||
|
||||
// Blocks returns the blockwise hash of the reader.
|
||||
func Blocks(r io.Reader, blocksize int) ([]protocol.BlockInfo, error) {
|
||||
func Blocks(r io.Reader, blocksize int, sizehint int64) ([]protocol.BlockInfo, error) {
|
||||
var blocks []protocol.BlockInfo
|
||||
if sizehint > 0 {
|
||||
blocks = make([]protocol.BlockInfo, 0, int(sizehint/int64(blocksize)))
|
||||
}
|
||||
var offset int64
|
||||
hf := sha256.New()
|
||||
for {
|
||||
lr := &io.LimitedReader{R: r, N: int64(blocksize)}
|
||||
hf := sha256.New()
|
||||
n, err := io.Copy(hf, lr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -39,6 +42,8 @@ func Blocks(r io.Reader, blocksize int) ([]protocol.BlockInfo, error) {
|
||||
}
|
||||
blocks = append(blocks, b)
|
||||
offset += int64(n)
|
||||
|
||||
hf.Reset()
|
||||
}
|
||||
|
||||
if len(blocks) == 0 {
|
||||
|
||||
@@ -49,7 +49,7 @@ var blocksTestData = []struct {
|
||||
func TestBlocks(t *testing.T) {
|
||||
for _, test := range blocksTestData {
|
||||
buf := bytes.NewBuffer(test.data)
|
||||
blocks, err := Blocks(buf, test.blocksize)
|
||||
blocks, err := Blocks(buf, test.blocksize, 0)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -103,8 +103,8 @@ var diffTestData = []struct {
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
for i, test := range diffTestData {
|
||||
a, _ := Blocks(bytes.NewBufferString(test.a), test.s)
|
||||
b, _ := Blocks(bytes.NewBufferString(test.b), test.s)
|
||||
a, _ := Blocks(bytes.NewBufferString(test.a), test.s, 0)
|
||||
b, _ := Blocks(bytes.NewBufferString(test.b), test.s, 0)
|
||||
_, d := BlockDiff(a, b)
|
||||
if len(d) != len(test.d) {
|
||||
t.Fatalf("Incorrect length for diff %d; %d != %d", i, len(d), len(test.d))
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
type Walker struct {
|
||||
// Dir is the base directory for the walk
|
||||
Dir string
|
||||
// Limit walking to this path within Dir, or no limit if Sub is blank
|
||||
Sub string
|
||||
// BlockSize controls the size of the block used when hashing.
|
||||
BlockSize int
|
||||
// If IgnoreFile is not empty, it is the name used for the file that holds ignore patterns.
|
||||
@@ -30,10 +32,6 @@ type Walker struct {
|
||||
TempNamer TempNamer
|
||||
// If CurrentFiler is not nil, it is queried for the current file before rescanning.
|
||||
CurrentFiler CurrentFiler
|
||||
// If Suppressor is not nil, it is queried for supression of modified files.
|
||||
// Suppressed files will be returned with empty metadata and the Suppressed flag set.
|
||||
// Requires CurrentFiler to be set.
|
||||
Suppressor Suppressor
|
||||
// If IgnorePerms is true, changes to permission bits will not be
|
||||
// detected. Scanned files will get zero permission bits and the
|
||||
// NoPermissionBits flag set.
|
||||
@@ -47,11 +45,6 @@ type TempNamer interface {
|
||||
IsTemporary(path string) bool
|
||||
}
|
||||
|
||||
type Suppressor interface {
|
||||
// Supress returns true if the update to the named file should be ignored.
|
||||
Suppress(name string, fi os.FileInfo) (bool, bool)
|
||||
}
|
||||
|
||||
type CurrentFiler interface {
|
||||
// CurrentFile returns the file as seen at last scan.
|
||||
CurrentFile(name string) protocol.FileInfo
|
||||
@@ -61,7 +54,7 @@ type CurrentFiler interface {
|
||||
// file system. Files are blockwise hashed.
|
||||
func (w *Walker) Walk() (chan protocol.FileInfo, map[string][]string, error) {
|
||||
if debug {
|
||||
l.Debugln("Walk", w.Dir, w.BlockSize, w.IgnoreFile)
|
||||
l.Debugln("Walk", w.Dir, w.Sub, w.BlockSize, w.IgnoreFile)
|
||||
}
|
||||
|
||||
err := checkDir(w.Dir)
|
||||
@@ -77,7 +70,7 @@ func (w *Walker) Walk() (chan protocol.FileInfo, map[string][]string, error) {
|
||||
|
||||
go func() {
|
||||
filepath.Walk(w.Dir, w.loadIgnoreFiles(w.Dir, ignore))
|
||||
filepath.Walk(w.Dir, hashFiles)
|
||||
filepath.Walk(filepath.Join(w.Dir, w.Sub), hashFiles)
|
||||
close(files)
|
||||
}()
|
||||
|
||||
@@ -199,22 +192,6 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo, ign map[string][
|
||||
return nil
|
||||
}
|
||||
|
||||
if w.Suppressor != nil {
|
||||
if cur, prev := w.Suppressor.Suppress(rn, info); cur && !prev {
|
||||
l.Infof("Changes to %q are being temporarily suppressed because it changes too frequently.", p)
|
||||
cf.Flags |= protocol.FlagInvalid
|
||||
cf.Version = lamport.Default.Tick(cf.Version)
|
||||
cf.LocalVersion = 0
|
||||
if debug {
|
||||
l.Debugln("suppressed:", cf)
|
||||
}
|
||||
fchan <- cf
|
||||
return nil
|
||||
} else if prev && !cur {
|
||||
l.Infof("Changes to %q are no longer suppressed.", p)
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("rescan:", cf, info.ModTime().Unix(), info.Mode()&os.ModePerm)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,30 @@ var correctIgnores = map[string][]string{
|
||||
".": {".*", "quux"},
|
||||
}
|
||||
|
||||
func TestWalkSub(t *testing.T) {
|
||||
w := Walker{
|
||||
Dir: "testdata",
|
||||
Sub: "foo",
|
||||
BlockSize: 128 * 1024,
|
||||
IgnoreFile: ".stignore",
|
||||
}
|
||||
fchan, _, err := w.Walk()
|
||||
var files []protocol.FileInfo
|
||||
for f := range fchan {
|
||||
files = append(files, f)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(files) != 1 {
|
||||
t.Fatalf("Incorrect length %d != 1", len(files))
|
||||
}
|
||||
if files[0].Name != "foo" {
|
||||
t.Errorf("Incorrect file %v != foo", files[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalk(t *testing.T) {
|
||||
w := Walker{
|
||||
Dir: "testdata",
|
||||
|
||||
17
upgrade/debug.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = strings.Contains(os.Getenv("STTRACE"), "upgrade") || os.Getenv("STTRACE") == "all"
|
||||
l = logger.DefaultLogger
|
||||
)
|
||||
@@ -114,7 +114,8 @@ func CompareVersions(a, b string) int {
|
||||
// Split a version into parts.
|
||||
// "1.2.3-beta.2" -> []int{1, 2, 3}, []interface{}{"beta", 2}
|
||||
func versionParts(v string) ([]int, []interface{}) {
|
||||
parts := strings.SplitN(v, "-", 2)
|
||||
parts := strings.SplitN(v, "+", 2)
|
||||
parts = strings.SplitN(parts[0], "-", 2)
|
||||
fields := strings.Split(parts[0], ".")
|
||||
|
||||
release := make([]int, len(fields))
|
||||
|
||||
@@ -23,10 +23,8 @@ import (
|
||||
"bitbucket.org/kardianos/osext"
|
||||
)
|
||||
|
||||
var GoArchExtra string // "", "v5", "v6", "v7"
|
||||
|
||||
// Upgrade to the given release, saving the previous binary with a ".old" extension.
|
||||
func UpgradeTo(rel Release) error {
|
||||
func UpgradeTo(rel Release, archExtra string) error {
|
||||
path, err := osext.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -38,8 +36,14 @@ func UpgradeTo(rel Release) error {
|
||||
// sense for people downloading them
|
||||
osName = "macosx"
|
||||
}
|
||||
expectedRelease := fmt.Sprintf("syncthing-%s-%s%s-%s.", osName, runtime.GOARCH, GoArchExtra, rel.Tag)
|
||||
expectedRelease := fmt.Sprintf("syncthing-%s-%s%s-%s.", osName, runtime.GOARCH, archExtra, rel.Tag)
|
||||
if debug {
|
||||
l.Debugf("expected release asset %q", expectedRelease)
|
||||
}
|
||||
for _, asset := range rel.Assets {
|
||||
if debug {
|
||||
l.Debugln("considering release", asset)
|
||||
}
|
||||
if strings.HasPrefix(asset.Name, expectedRelease) {
|
||||
if strings.HasSuffix(asset.Name, ".tar.gz") {
|
||||
fname, err := readTarGZ(asset.URL, filepath.Dir(path))
|
||||
@@ -97,6 +101,10 @@ func LatestRelease(prerelease bool) (Release, error) {
|
||||
}
|
||||
|
||||
func readTarGZ(url string, dir string) (string, error) {
|
||||
if debug {
|
||||
l.Debugf("loading %q", url)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -129,6 +137,9 @@ func readTarGZ(url string, dir string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("considering file %q", hdr.Name)
|
||||
}
|
||||
|
||||
if path.Base(hdr.Name) == "syncthing" {
|
||||
of, err := ioutil.TempFile(dir, "syncthing")
|
||||
|
||||
@@ -28,6 +28,9 @@ var testcases = []struct {
|
||||
{"1.0.0-beta.2", "1.0.0-beta.11", -1},
|
||||
{"1.0.0-beta.11", "1.0.0-rc.1", -1},
|
||||
{"1.0.0-rc.1", "1.0.0", -1},
|
||||
{"1.0.0+45", "1.0.0+23-dev-foo", 0},
|
||||
{"1.0.0-beta.23+45", "1.0.0-beta.23+23-dev-foo", 0},
|
||||
{"1.0.0-beta.3+99", "1.0.0-beta.24+0", -1},
|
||||
}
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows solaris noupgrade
|
||||
// +build solaris noupgrade
|
||||
|
||||
package upgrade
|
||||
|
||||
func UpgradeTo(rel Release) error {
|
||||
func UpgradeTo(rel Release, extra string) error {
|
||||
return ErrUpgradeUnsupported
|
||||
}
|
||||
|
||||
|
||||
166
upgrade/upgrade_windows.go
Executable file
@@ -0,0 +1,166 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows,!noupgrade
|
||||
|
||||
package upgrade
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"bitbucket.org/kardianos/osext"
|
||||
)
|
||||
|
||||
// Upgrade to the given release, saving the previous binary with a ".old" extension.
|
||||
func UpgradeTo(rel Release, archExtra string) error {
|
||||
path, err := osext.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedRelease := fmt.Sprintf("syncthing-%s-%s%s-%s.", runtime.GOOS, runtime.GOARCH, archExtra, rel.Tag)
|
||||
if debug {
|
||||
l.Debugf("expected release asset %q", expectedRelease)
|
||||
}
|
||||
for _, asset := range rel.Assets {
|
||||
if debug {
|
||||
l.Debugln("considering release", asset)
|
||||
}
|
||||
if strings.HasPrefix(asset.Name, expectedRelease) {
|
||||
if strings.HasSuffix(asset.Name, ".zip") {
|
||||
fname, err := readZip(asset.URL, filepath.Dir(path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
old := path + ".old"
|
||||
|
||||
os.Remove(old)
|
||||
err = os.Rename(path, old)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Rename(fname, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrVersionUnknown
|
||||
}
|
||||
|
||||
// Returns the latest release, including prereleases or not depending on the argument
|
||||
func LatestRelease(prerelease bool) (Release, error) {
|
||||
resp, err := http.Get("https://api.github.com/repos/syncthing/syncthing/releases?per_page=10")
|
||||
if err != nil {
|
||||
return Release{}, err
|
||||
}
|
||||
if resp.StatusCode > 299 {
|
||||
return Release{}, fmt.Errorf("API call returned HTTP error: %s", resp.Status)
|
||||
}
|
||||
|
||||
var rels []Release
|
||||
json.NewDecoder(resp.Body).Decode(&rels)
|
||||
resp.Body.Close()
|
||||
|
||||
if len(rels) == 0 {
|
||||
return Release{}, ErrVersionUnknown
|
||||
}
|
||||
|
||||
if prerelease {
|
||||
// We are a beta version. Use the latest.
|
||||
return rels[0], nil
|
||||
} else {
|
||||
// We are a regular release. Only consider non-prerelease versions for upgrade.
|
||||
for _, rel := range rels {
|
||||
if !rel.Prerelease {
|
||||
return rel, nil
|
||||
}
|
||||
}
|
||||
return Release{}, ErrVersionUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func readZip(url, dir string) (string, error) {
|
||||
if debug {
|
||||
l.Debugf("loading %q", url)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Add("Accept", "application/octet-stream")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
archive, err := zip.NewReader(bytes.NewReader(body), resp.ContentLength)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Iterate through the files in the archive.
|
||||
for _, file := range archive.File {
|
||||
|
||||
if debug {
|
||||
l.Debugf("considering file %q", file.Name)
|
||||
}
|
||||
|
||||
if path.Base(file.Name) == "syncthing.exe" {
|
||||
infile, err := file.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
outfile, err := ioutil.TempFile(dir, "syncthing")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outfile, infile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = infile.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = outfile.Close()
|
||||
if err != nil {
|
||||
os.Remove(outfile.Name())
|
||||
return "", err
|
||||
}
|
||||
|
||||
os.Chmod(outfile.Name(), file.Mode())
|
||||
return outfile.Name(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("No upgrade found")
|
||||
}
|
||||