Compare commits
319
Commits
v0.9.1
...
v0.10.0-beta1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99427d649e | ||
|
|
7bc4589d4d | ||
|
|
9af586d4ac | ||
|
|
87e68cac6c | ||
|
|
7d5a98409b | ||
|
|
14817e31f6 | ||
|
|
fbdbd722b1 | ||
|
|
b34102cd11 | ||
|
|
3c51cd6626 | ||
|
|
b0b34236e3 | ||
|
|
a502836002 | ||
|
|
09417d4b83 | ||
|
|
83ef2fa84c | ||
|
|
e596a45e9f | ||
|
|
24e5000c37 | ||
|
|
e3bcfa17f8 | ||
|
|
3b512676b7 | ||
|
|
928198bbfe | ||
|
|
1fb56f0ad2 | ||
|
|
9797f62cb8 | ||
|
|
4ddd87e773 | ||
|
|
7fd2e4d2db | ||
|
|
55c7d86205 | ||
|
|
737a28050c | ||
|
|
434ecdac6b | ||
|
|
709570afcc | ||
|
|
b084b4faaf | ||
|
|
d96ce23451 | ||
|
|
760a9d6d35 | ||
|
|
8e624cedb1 | ||
|
|
39cf269d6b | ||
|
|
6a00b5a79e | ||
|
|
2ce674e3fd | ||
|
|
0fcc25d7c9 | ||
|
|
63bd0136fb | ||
|
|
80a2a934dd | ||
|
|
e13976a3b3 | ||
|
|
5144330807 | ||
|
|
bb29639183 | ||
|
|
4667cb9de9 | ||
|
|
c34f3defe1 | ||
|
|
eb0d742672 | ||
|
|
d9b0a73787 | ||
|
|
4810879b2f | ||
|
|
f4b6704aad | ||
|
|
b1a31d3b30 | ||
|
|
bf909db3f9 | ||
|
|
9c68be4d5e | ||
|
|
d7956dd495 | ||
|
|
37a473e7d6 | ||
|
|
5a1c885e8f | ||
|
|
0b1136ad82 | ||
|
|
45af549897 | ||
|
|
97844603fc | ||
|
|
c07b39e58b | ||
|
|
384c543ab9 | ||
|
|
592b13d7db | ||
|
|
6fdba3c02e | ||
|
|
cbf758ead9 | ||
|
|
d1ad778a64 | ||
|
|
ce5ad296ae | ||
|
|
797e105786 | ||
|
|
d17d80747e | ||
|
|
55ea207a55 | ||
|
|
6384d1e5a3 | ||
|
|
aba01cdace | ||
|
|
517b7a14b4 | ||
|
|
2927de7cf9 | ||
|
|
6471ba70e4 | ||
|
|
9f9de01c51 | ||
|
|
3662decb8b | ||
|
|
583bcfb3c7 | ||
|
|
c45e3fa4d5 | ||
|
|
24cbcef620 | ||
|
|
e2a520ff49 | ||
|
|
a5e3317e28 | ||
|
|
5638c4ba87 | ||
|
|
bf7a128142 | ||
|
|
c5243cd4d5 | ||
|
|
db868ed29d | ||
|
|
450c7d80f8 | ||
|
|
abbb001975 | ||
|
|
f35d83ae48 | ||
|
|
a2315dc95e | ||
|
|
4e2feb6fbc | ||
|
|
13602b6769 | ||
|
|
85dba25246 | ||
|
|
66432672b3 | ||
|
|
e6d96e4c18 | ||
|
|
9812305bb9 | ||
|
|
f680a63a1f | ||
|
|
781d63cb2a | ||
|
|
9ff04ee3d8 | ||
|
|
5d85a24977 | ||
|
|
1e51fca0b0 | ||
|
|
5537d53f9a | ||
|
|
50a4170541 | ||
|
|
3a8255bda1 | ||
|
|
a617846f0f | ||
|
|
5772588c29 | ||
|
|
9d0dc45f74 | ||
|
|
c6aefbc9a0 | ||
|
|
dbbafb0cc9 | ||
|
|
6e8272f78f | ||
|
|
baf8a63121 | ||
|
|
fc4a76ee50 | ||
|
|
2117d1d035 | ||
|
|
0a70e0b7b6 | ||
|
|
64ffac5671 | ||
|
|
ac384e8a9c | ||
|
|
f97c8222c7 | ||
|
|
728289ee3a | ||
|
|
5faa16f9ee | ||
|
|
4e608b116a | ||
|
|
521b49166e | ||
|
|
8f32decf2d | ||
|
|
0d51f83d2d | ||
|
|
78c6a68db9 | ||
|
|
2949ab73e2 | ||
|
|
223741820d | ||
|
|
4b57821f52 | ||
|
|
74271a479f | ||
|
|
c377177108 | ||
|
|
84eb729bd4 | ||
|
|
14aea365c5 | ||
|
|
97cb3fa5a5 | ||
|
|
b5368db704 | ||
|
|
8c442b72f3 | ||
|
|
f8f6791d39 | ||
|
|
0c09f077aa | ||
|
|
af2831d7b6 | ||
|
|
64d5d4aec7 | ||
|
|
619a6b2adb | ||
|
|
33a26bc0cf | ||
|
|
b445a7c4d3 | ||
|
|
e6892d0c3e | ||
|
|
33e9a88b56 | ||
|
|
df00a2251e | ||
|
|
92c44c8abe | ||
|
|
8e4f7bbd3e | ||
|
|
a40217cf07 | ||
|
|
e586fda5f2 | ||
|
|
a58564ff88 | ||
|
|
89885b9fb9 | ||
|
|
5c7d977ae0 | ||
|
|
2cd3ee9698 | ||
|
|
dd3080e018 | ||
|
|
5915e8e86a | ||
|
|
3c67c06654 | ||
|
|
76232ca573 | ||
|
|
5235e82bda | ||
|
|
10f0713257 | ||
|
|
e9c7970ea4 | ||
|
|
1a6ac4aeb1 | ||
|
|
f633bdddf0 | ||
|
|
de0b91d157 | ||
|
|
2e77e498f5 | ||
|
|
4ac67eb1f9 | ||
|
|
2b536de37f | ||
|
|
2ffa92ba1b | ||
|
|
6ecddd8388 | ||
|
|
bd2772ea4c | ||
|
|
92bf79d53b | ||
|
|
eebe0eeb71 | ||
|
|
1068eaa0b9 | ||
|
|
faac3e7d7c | ||
|
|
dab4340207 | ||
|
|
fd2567748f | ||
|
|
c2daedbd11 | ||
|
|
7c604beb73 | ||
|
|
8c42aea827 | ||
|
|
cf1bfdfb61 | ||
|
|
75b26513e1 | ||
|
|
6c09a77a97 | ||
|
|
67389c39fb | ||
|
|
c326103e6e | ||
|
|
c2120a16da | ||
|
|
258ad4352e | ||
|
|
435d3958f4 | ||
|
|
b0408ef5c6 | ||
|
|
1c41b0bc2f | ||
|
|
aa827f3042 | ||
|
|
f44f5964bb | ||
|
|
91ba93bd7a | ||
|
|
0abe4cefb4 | ||
|
|
bccd460f3b | ||
|
|
d1023004e1 | ||
|
|
04a5f9cb04 | ||
|
|
9818e2b550 | ||
|
|
fe43e3b89d | ||
|
|
e1f1ae041f | ||
|
|
5bcf26e324 | ||
|
|
5f47a8149f | ||
|
|
00b662b53a | ||
|
|
faf519ab1b | ||
|
|
fce73f6f17 | ||
|
|
887890baf5 | ||
|
|
c66b24feeb | ||
|
|
84c6f147ad | ||
|
|
0cdb0daa8c | ||
|
|
eee702f299 | ||
|
|
df65247325 | ||
|
|
1a174e75d3 | ||
|
|
9e1fd3454f | ||
|
|
3b1603cadf | ||
|
|
8803bac708 | ||
|
|
3a01eaa4a6 | ||
|
|
9f84c1c448 | ||
|
|
dda0390156 | ||
|
|
c74509dd5f | ||
|
|
f61bbb2ff4 | ||
|
|
e7f60161a3 | ||
|
|
ebec4fbc24 | ||
|
|
1d4105ae3d | ||
|
|
586d49f0c3 | ||
|
|
5b0fab0697 | ||
|
|
2b3359dff3 | ||
|
|
63203aa14c | ||
|
|
716a8329c2 | ||
|
|
dab0aec85e | ||
|
|
1f1ab017c0 | ||
|
|
b6912ef95e | ||
|
|
db54dca694 | ||
|
|
0e751b983c | ||
|
|
997b20a975 | ||
|
|
386f9c42c2 | ||
|
|
cfae06db65 | ||
|
|
44260b7b5c | ||
|
|
13063b957f | ||
|
|
ee05e12480 | ||
|
|
5538545fb0 | ||
|
|
bc1167c2c5 | ||
|
|
c57656e4c3 | ||
|
|
264400a984 | ||
|
|
408db4eb1d | ||
|
|
9347f223ef | ||
|
|
518aa30c9c | ||
|
|
6bbf1f9355 | ||
|
|
b221e4d445 | ||
|
|
580fccbfca | ||
|
|
045916efcc | ||
|
|
4f92482294 | ||
|
|
2f055a75a0 | ||
|
|
f0621207e3 | ||
|
|
d657bc4e3d | ||
|
|
a1fd07b27c | ||
|
|
52219c5f3f | ||
|
|
1a66461e07 | ||
|
|
d20df12168 | ||
|
|
668b429615 | ||
|
|
7db528be39 | ||
|
|
60f760ee49 | ||
|
|
884aaab751 | ||
|
|
e968560ea4 | ||
|
|
07caaa96e4 | ||
|
|
e8a679c280 | ||
|
|
bc885f1d08 | ||
|
|
f2f051d6de | ||
|
|
49a0bfccba | ||
|
|
0c1e60894f | ||
|
|
ace87ad7bb | ||
|
|
50f0097843 | ||
|
|
32a9466277 | ||
|
|
1ee3407946 | ||
|
|
f1120d7aa9 | ||
|
|
2e7d6b2f99 | ||
|
|
dfef929187 | ||
|
|
e78d9ad592 | ||
|
|
9f2948f595 | ||
|
|
198da910ed | ||
|
|
5f1bf9d9d6 | ||
|
|
798c4aef9a | ||
|
|
f80f5b3bda | ||
|
|
cbb07b0d67 | ||
|
|
7cc9921615 | ||
|
|
7555fe065e | ||
|
|
d977f4278e | ||
|
|
870e3ca893 | ||
|
|
213acaee3b | ||
|
|
58381496a2 | ||
|
|
5981e42aed | ||
|
|
3c9165d295 | ||
|
|
60d0ef93ac | ||
|
|
f45d5b0066 | ||
|
|
b71306480f | ||
|
|
0c7771ccc5 | ||
|
|
dc9df0a79a | ||
|
|
17cd49fbdc | ||
|
|
ad273adb78 | ||
|
|
150e7daf2d | ||
|
|
b004155e8f | ||
|
|
92eed3b33b | ||
|
|
fe7b77198c | ||
|
|
f51b775698 | ||
|
|
939dd5cb31 | ||
|
|
adcbe13ecd | ||
|
|
8976e53998 | ||
|
|
97dda6a4bb | ||
|
|
9e395eb883 | ||
|
|
60da59623e | ||
|
|
9752ea9ac3 | ||
|
|
279693078a | ||
|
|
19b93045a4 | ||
|
|
5231a09820 | ||
|
|
ab952e6103 | ||
|
|
a418771c04 | ||
|
|
b41590ce38 | ||
|
|
c7dde9499f | ||
|
|
528cbf62ec | ||
|
|
1be4b8bb5d | ||
|
|
c832fc9917 | ||
|
|
4797a94689 | ||
|
|
6948903084 | ||
|
|
94164611ae | ||
|
|
ae298e8902 | ||
|
|
3d8771ecb0 | ||
|
|
28db264e90 | ||
|
|
6af9fa4b81 | ||
|
|
c45b18cc75 |
@@ -9,3 +9,4 @@ coverage.out
|
|||||||
files/pidx
|
files/pidx
|
||||||
bin
|
bin
|
||||||
perfstats*.csv
|
perfstats*.csv
|
||||||
|
coverage.xml
|
||||||
|
|||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- tip
|
|
||||||
|
|
||||||
install:
|
|
||||||
- export PATH=$PATH:$HOME/gopath/bin
|
|
||||||
- ./build.sh setup
|
|
||||||
|
|
||||||
script:
|
|
||||||
- ./build.sh test-cov
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- goveralls -coverprofile=coverage.out -service=travis-ci -package=syncthing/syncthing -repotoken="$COVERALLS_TOKEN"
|
|
||||||
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
secure: "TSPJDsokGCQhKLjgG3c58qHn8Qxhh4zEkWFf0XIOOY2nlDVzdgXDsC+Nq0YaP4106Ee4FgkSefsUTQV5lq/IyYW8elgqlgghjOtOi6RJa14eIS9Yy5Bkx6MXn0QfZX/lG+sy42pKSNk43y9GWx/qrt4nkfTtTvI5cXgwDGYdmX8="
|
|
||||||
+15
-1
@@ -34,7 +34,21 @@ latest info on Transifex.
|
|||||||
Please do contribute! If you want to contribute but are unsure where to
|
Please do contribute! If you want to contribute but are unsure where to
|
||||||
start, the [Contributions Needed
|
start, the [Contributions Needed
|
||||||
topic](http://discourse.syncthing.net/t/49) lists areas in need of
|
topic](http://discourse.syncthing.net/t/49) lists areas in need of
|
||||||
attention. In general, any open issues are fair game!
|
attention. In general, any open issues are fair game! Be prepared for a
|
||||||
|
[certain amount of
|
||||||
|
review](https://discourse.syncthing.net/t/733); it's all in the name of
|
||||||
|
quality. :)
|
||||||
|
|
||||||
|
You may be asked to "rebase" or "squash" your pull request before it's
|
||||||
|
accepted. This means to make sure that the pull request:
|
||||||
|
|
||||||
|
- Is based on the latest commit on `master`.
|
||||||
|
|
||||||
|
- Only contains a single commit (exceptions to be made where
|
||||||
|
appropriate).
|
||||||
|
|
||||||
|
- Has a descriptive commit message, ending in `(fixes #123)` if it
|
||||||
|
resolves an existing issue.
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -1,12 +1,16 @@
|
|||||||
Aaron Bieber <qbit@deftly.net>
|
Aaron Bieber <qbit@deftly.net>
|
||||||
|
Alexander Graf <register-github@alex-graf.de>
|
||||||
Andrew Dunham <andrew@du.nham.ca>
|
Andrew Dunham <andrew@du.nham.ca>
|
||||||
Audrius Butkevicius <audrius.butkevicius@gmail.com>
|
Audrius Butkevicius <audrius.butkevicius@gmail.com>
|
||||||
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com>
|
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com> <frioux@gmail.com>
|
||||||
Ben Sidhom <bsidhom@gmail.com>
|
Ben Sidhom <bsidhom@gmail.com>
|
||||||
Brandon Philips <brandon@ifup.org>
|
Brandon Philips <brandon@ifup.org>
|
||||||
Gilli Sigurdsson <gilli@vx.is>
|
Gilli Sigurdsson <gilli@vx.is>
|
||||||
James Patterson <jamespatterson@operamail.com>
|
James Patterson <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
||||||
Jens Diemer <github.com@jensdiemer.de>
|
Jens Diemer <github.com@jensdiemer.de> <git@jensdiemer.de>
|
||||||
|
Lode Hoste <zillode@zillode.be>
|
||||||
|
Marcin Dziadus <dziadus.marcin@gmail.com>
|
||||||
|
Michael Tilli <pyfisch@gmail.com>
|
||||||
Philippe Schommers <philippe@schommers.be>
|
Philippe Schommers <philippe@schommers.be>
|
||||||
Ryan Sullivan <kayoticsully@gmail.com>
|
Ryan Sullivan <kayoticsully@gmail.com>
|
||||||
Tully Robinson <tully@tojr.org>
|
Tully Robinson <tully@tojr.org>
|
||||||
|
|||||||
Generated
+12
-12
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ImportPath": "github.com/syncthing/syncthing",
|
"ImportPath": "github.com/syncthing/syncthing",
|
||||||
"GoVersion": "go1.3",
|
"GoVersion": "go1.3.1",
|
||||||
"Packages": [
|
"Packages": [
|
||||||
"./cmd/..."
|
"./cmd/..."
|
||||||
],
|
],
|
||||||
@@ -12,23 +12,23 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "code.google.com/p/go.crypto/bcrypt",
|
"ImportPath": "code.google.com/p/go.crypto/bcrypt",
|
||||||
"Comment": "null-213",
|
"Comment": "null-216",
|
||||||
"Rev": "aa2644fe4aa50e3b38d75187b4799b1f0c9ddcef"
|
"Rev": "41cd4647fccc72b0b79ef1bd1fe6735e718257cd"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "code.google.com/p/go.crypto/blowfish",
|
"ImportPath": "code.google.com/p/go.crypto/blowfish",
|
||||||
"Comment": "null-213",
|
"Comment": "null-216",
|
||||||
"Rev": "aa2644fe4aa50e3b38d75187b4799b1f0c9ddcef"
|
"Rev": "41cd4647fccc72b0b79ef1bd1fe6735e718257cd"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "code.google.com/p/go.text/transform",
|
"ImportPath": "code.google.com/p/go.text/transform",
|
||||||
"Comment": "null-89",
|
"Comment": "null-90",
|
||||||
"Rev": "df15baaf13e3f62b6b7a901e74caa3818a7c0a7c"
|
"Rev": "d65bffbc88a153d23a6d2a864531e6e7c2cde59b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "code.google.com/p/go.text/unicode/norm",
|
"ImportPath": "code.google.com/p/go.text/unicode/norm",
|
||||||
"Comment": "null-89",
|
"Comment": "null-90",
|
||||||
"Rev": "df15baaf13e3f62b6b7a901e74caa3818a7c0a7c"
|
"Rev": "d65bffbc88a153d23a6d2a864531e6e7c2cde59b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "code.google.com/p/snappy-go/snappy",
|
"ImportPath": "code.google.com/p/snappy-go/snappy",
|
||||||
@@ -37,11 +37,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/bkaradzic/go-lz4",
|
"ImportPath": "github.com/bkaradzic/go-lz4",
|
||||||
"Rev": "77e2ba877bde9da31213bec75dbbe197fa507c21"
|
"Rev": "93a831dcee242be64a9cc9803dda84af25932de7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/calmh/xdr",
|
"ImportPath": "github.com/calmh/xdr",
|
||||||
"Rev": "694859acb207675085232438780db923ceb43e96"
|
"Rev": "a597b63b87d6140f79084c8aab214b4d533833a1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/juju/ratelimit",
|
"ImportPath": "github.com/juju/ratelimit",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||||
"Rev": "c5955912e3287376475731c5bc59c79a5a799105"
|
"Rev": "9bca75c48d6c31becfbb127702b425e7226052e3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "github.com/vitrun/qart/coding",
|
"ImportPath": "github.com/vitrun/qart/coding",
|
||||||
|
|||||||
+32
-63
@@ -4,6 +4,22 @@
|
|||||||
|
|
||||||
package blowfish
|
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
|
// ExpandKey performs a key expansion on the given *Cipher. Specifically, it
|
||||||
// performs the Blowfish algorithm's key schedule which sets up the *Cipher's
|
// 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,
|
// pi and substitution tables for calls to Encrypt. This is used, primarily,
|
||||||
@@ -12,6 +28,7 @@ package blowfish
|
|||||||
func ExpandKey(key []byte, c *Cipher) {
|
func ExpandKey(key []byte, c *Cipher) {
|
||||||
j := 0
|
j := 0
|
||||||
for i := 0; i < 18; i++ {
|
for i := 0; i < 18; i++ {
|
||||||
|
// Using inlined getNextWord for performance.
|
||||||
var d uint32
|
var d uint32
|
||||||
for k := 0; k < 4; k++ {
|
for k := 0; k < 4; k++ {
|
||||||
d = d<<8 | uint32(key[j])
|
d = d<<8 | uint32(key[j])
|
||||||
@@ -54,86 +71,44 @@ func ExpandKey(key []byte, c *Cipher) {
|
|||||||
func expandKeyWithSalt(key []byte, salt []byte, c *Cipher) {
|
func expandKeyWithSalt(key []byte, salt []byte, c *Cipher) {
|
||||||
j := 0
|
j := 0
|
||||||
for i := 0; i < 18; i++ {
|
for i := 0; i < 18; i++ {
|
||||||
var d uint32
|
c.p[i] ^= getNextWord(key, &j)
|
||||||
for k := 0; k < 4; k++ {
|
|
||||||
d = d<<8 | uint32(key[j])
|
|
||||||
j++
|
|
||||||
if j >= len(key) {
|
|
||||||
j = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.p[i] ^= d
|
|
||||||
}
|
}
|
||||||
|
|
||||||
j = 0
|
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
|
var l, r uint32
|
||||||
for i := 0; i < 18; i += 2 {
|
for i := 0; i < 18; i += 2 {
|
||||||
l ^= expandedSalt[i&2]
|
l ^= getNextWord(salt, &j)
|
||||||
r ^= expandedSalt[(i&2)+1]
|
r ^= getNextWord(salt, &j)
|
||||||
l, r = encryptBlock(l, r, c)
|
l, r = encryptBlock(l, r, c)
|
||||||
c.p[i], c.p[i+1] = l, r
|
c.p[i], c.p[i+1] = l, r
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 256; i += 4 {
|
for i := 0; i < 256; i += 2 {
|
||||||
l ^= expandedSalt[2]
|
l ^= getNextWord(salt, &j)
|
||||||
r ^= expandedSalt[3]
|
r ^= getNextWord(salt, &j)
|
||||||
l, r = encryptBlock(l, r, c)
|
l, r = encryptBlock(l, r, c)
|
||||||
c.s0[i], c.s0[i+1] = l, r
|
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 {
|
for i := 0; i < 256; i += 2 {
|
||||||
l ^= expandedSalt[2]
|
l ^= getNextWord(salt, &j)
|
||||||
r ^= expandedSalt[3]
|
r ^= getNextWord(salt, &j)
|
||||||
l, r = encryptBlock(l, r, c)
|
l, r = encryptBlock(l, r, c)
|
||||||
c.s1[i], c.s1[i+1] = l, r
|
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 {
|
for i := 0; i < 256; i += 2 {
|
||||||
l ^= expandedSalt[2]
|
l ^= getNextWord(salt, &j)
|
||||||
r ^= expandedSalt[3]
|
r ^= getNextWord(salt, &j)
|
||||||
l, r = encryptBlock(l, r, c)
|
l, r = encryptBlock(l, r, c)
|
||||||
c.s2[i], c.s2[i+1] = l, r
|
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 {
|
for i := 0; i < 256; i += 2 {
|
||||||
l ^= expandedSalt[2]
|
l ^= getNextWord(salt, &j)
|
||||||
r ^= expandedSalt[3]
|
r ^= getNextWord(salt, &j)
|
||||||
l, r = encryptBlock(l, r, c)
|
l, r = encryptBlock(l, r, c)
|
||||||
c.s3[i], c.s3[i+1] = l, r
|
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]
|
xr ^= c.p[0]
|
||||||
return xr, xl
|
return xr, xl
|
||||||
}
|
}
|
||||||
|
|
||||||
func zero(x []uint32) {
|
|
||||||
for i := range x {
|
|
||||||
x[i] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+73
-3
@@ -4,9 +4,7 @@
|
|||||||
|
|
||||||
package blowfish
|
package blowfish
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CryptTest struct {
|
type CryptTest struct {
|
||||||
key []byte
|
key []byte
|
||||||
@@ -202,3 +200,75 @@ func TestSaltedCipherKeyLength(t *testing.T) {
|
|||||||
t.Errorf("NewSaltedCipher with long key, gave error %#v", err)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+4
-1
@@ -40,8 +40,11 @@ func NewCipher(key []byte) (*Cipher, error) {
|
|||||||
// NewSaltedCipher creates a returns a Cipher that folds a salt into its key
|
// NewSaltedCipher creates a returns a Cipher that folds a salt into its key
|
||||||
// schedule. For most purposes, NewCipher, instead of NewSaltedCipher, is
|
// schedule. For most purposes, NewCipher, instead of NewSaltedCipher, is
|
||||||
// sufficient and desirable. For bcrypt compatiblity, the key can be over 56
|
// 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) {
|
func NewSaltedCipher(key, salt []byte) (*Cipher, error) {
|
||||||
|
if len(salt) == 0 {
|
||||||
|
return NewCipher(key)
|
||||||
|
}
|
||||||
var result Cipher
|
var result Cipher
|
||||||
if k := len(key); k < 1 {
|
if k := len(key); k < 1 {
|
||||||
return nil, KeySizeError(k)
|
return nil, KeySizeError(k)
|
||||||
|
|||||||
+10
-2
@@ -72,10 +72,18 @@ func main() {
|
|||||||
|
|
||||||
if *decompress {
|
if *decompress {
|
||||||
data, _ = ioutil.ReadAll(input)
|
data, _ = ioutil.ReadAll(input)
|
||||||
data, _ = lz4.Decode(nil, data)
|
data, err = lz4.Decode(nil, data)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to decode:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
data, _ = ioutil.ReadAll(input)
|
data, _ = ioutil.ReadAll(input)
|
||||||
data, _ = lz4.Encode(nil, data)
|
data, err = lz4.Encode(nil, data)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to encode:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ioutil.WriteFile(args[1], data, 0644)
|
err = ioutil.WriteFile(args[1], data, 0644)
|
||||||
|
|||||||
+2
-2
@@ -121,7 +121,7 @@ func Encode(dst, src []byte) ([]byte, error) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if int(e.pos)+4 >= len(e.src) {
|
if int(e.pos)+12 >= len(e.src) {
|
||||||
e.writeLiterals(uint32(len(e.src))-e.anchor, 0, e.anchor)
|
e.writeLiterals(uint32(len(e.src))-e.anchor, 0, e.anchor)
|
||||||
return e.dst[:e.dpos], nil
|
return e.dst[:e.dpos], nil
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ func Encode(dst, src []byte) ([]byte, error) {
|
|||||||
ref += minMatch
|
ref += minMatch
|
||||||
e.anchor = e.pos
|
e.anchor = e.pos
|
||||||
|
|
||||||
for int(e.pos) < len(e.src) && e.src[e.pos] == e.src[ref] {
|
for int(e.pos) < len(e.src)-5 && e.src[e.pos] == e.src[ref] {
|
||||||
e.pos++
|
e.pos++
|
||||||
ref++
|
ref++
|
||||||
}
|
}
|
||||||
|
|||||||
+25
@@ -7,6 +7,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/calmh/xdr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type XDRBenchStruct struct {
|
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 {
|
type repeatReader struct {
|
||||||
data []byte
|
data []byte
|
||||||
}
|
}
|
||||||
@@ -86,3 +98,16 @@ func BenchmarkThisDecode(b *testing.B) {
|
|||||||
rr.Reset(e)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+12
-17
@@ -7,6 +7,8 @@ package xdr
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrElementSizeExceeded = errors.New("element size exceeded")
|
var ErrElementSizeExceeded = errors.New("element size exceeded")
|
||||||
@@ -15,7 +17,6 @@ type Reader struct {
|
|||||||
r io.Reader
|
r io.Reader
|
||||||
err error
|
err error
|
||||||
b [8]byte
|
b [8]byte
|
||||||
sb []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReader(r io.Reader) *Reader {
|
func NewReader(r io.Reader) *Reader {
|
||||||
@@ -35,23 +36,17 @@ func (r *Reader) ReadRaw(bs []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) ReadString() string {
|
func (r *Reader) ReadString() string {
|
||||||
if r.sb == nil {
|
return r.ReadStringMax(0)
|
||||||
r.sb = make([]byte, 64)
|
|
||||||
} else {
|
|
||||||
r.sb = r.sb[:cap(r.sb)]
|
|
||||||
}
|
|
||||||
r.sb = r.ReadBytesInto(r.sb)
|
|
||||||
return string(r.sb)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) ReadStringMax(max int) string {
|
func (r *Reader) ReadStringMax(max int) string {
|
||||||
if r.sb == nil {
|
buf := r.ReadBytesMaxInto(max, nil)
|
||||||
r.sb = make([]byte, 64)
|
bh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
|
||||||
} else {
|
sh := reflect.StringHeader{
|
||||||
r.sb = r.sb[:cap(r.sb)]
|
Data: bh.Data,
|
||||||
|
Len: bh.Len,
|
||||||
}
|
}
|
||||||
r.sb = r.ReadBytesMaxInto(max, r.sb)
|
return *((*string)(unsafe.Pointer(&sh)))
|
||||||
return string(r.sb)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) ReadBytes() []byte {
|
func (r *Reader) ReadBytes() []byte {
|
||||||
@@ -80,10 +75,10 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if l+pad(l) > len(dst) {
|
if fullLen := l + pad(l); fullLen > len(dst) {
|
||||||
dst = make([]byte, l+pad(l))
|
dst = make([]byte, fullLen)
|
||||||
} else {
|
} else {
|
||||||
dst = dst[:l+pad(l)]
|
dst = dst[:fullLen]
|
||||||
}
|
}
|
||||||
|
|
||||||
var n int
|
var n int
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ func (r *Reader) ReadUint8() uint8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
dl.Printf("rd uint8=%d (0x%08x)", r.b[0], r.b[0])
|
dl.Printf("rd uint8=%d (0x%02x)", r.b[0], r.b[0])
|
||||||
}
|
}
|
||||||
return r.b[0]
|
return r.b[0]
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ func (r *Reader) ReadUint16() uint16 {
|
|||||||
v := uint16(r.b[1]) | uint16(r.b[0])<<8
|
v := uint16(r.b[1]) | uint16(r.b[0])<<8
|
||||||
|
|
||||||
if debug {
|
if debug {
|
||||||
dl.Printf("rd uint16=%d (0x%08x)", v, v)
|
dl.Printf("rd uint16=%d (0x%04x)", v, v)
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -3,7 +3,11 @@
|
|||||||
|
|
||||||
package xdr
|
package xdr
|
||||||
|
|
||||||
import "io"
|
import (
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
var padBytes = []byte{0, 0, 0}
|
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) {
|
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) {
|
func (w *Writer) WriteBytes(bs []byte) (int, error) {
|
||||||
|
|||||||
+9
-6
@@ -170,7 +170,7 @@ func (p *dbBench) writes(perBatch int) {
|
|||||||
b.SetBytes(116)
|
b.SetBytes(116)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *dbBench) drop() {
|
func (p *dbBench) gc() {
|
||||||
p.keys, p.values = nil, nil
|
p.keys, p.values = nil, nil
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
}
|
}
|
||||||
@@ -249,6 +249,9 @@ func (p *dbBench) newIter() iterator.Iterator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *dbBench) close() {
|
func (p *dbBench) close() {
|
||||||
|
if bp, err := p.db.GetProperty("leveldb.blockpool"); err == nil {
|
||||||
|
p.b.Log("Block pool stats: ", bp)
|
||||||
|
}
|
||||||
p.db.Close()
|
p.db.Close()
|
||||||
p.stor.Close()
|
p.stor.Close()
|
||||||
os.RemoveAll(benchDB)
|
os.RemoveAll(benchDB)
|
||||||
@@ -331,7 +334,7 @@ func BenchmarkDBRead(b *testing.B) {
|
|||||||
p := openDBBench(b, false)
|
p := openDBBench(b, false)
|
||||||
p.populate(b.N)
|
p.populate(b.N)
|
||||||
p.fill()
|
p.fill()
|
||||||
p.drop()
|
p.gc()
|
||||||
|
|
||||||
iter := p.newIter()
|
iter := p.newIter()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
@@ -362,7 +365,7 @@ func BenchmarkDBReadUncompressed(b *testing.B) {
|
|||||||
p := openDBBench(b, true)
|
p := openDBBench(b, true)
|
||||||
p.populate(b.N)
|
p.populate(b.N)
|
||||||
p.fill()
|
p.fill()
|
||||||
p.drop()
|
p.gc()
|
||||||
|
|
||||||
iter := p.newIter()
|
iter := p.newIter()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
@@ -379,7 +382,7 @@ func BenchmarkDBReadTable(b *testing.B) {
|
|||||||
p.populate(b.N)
|
p.populate(b.N)
|
||||||
p.fill()
|
p.fill()
|
||||||
p.reopen()
|
p.reopen()
|
||||||
p.drop()
|
p.gc()
|
||||||
|
|
||||||
iter := p.newIter()
|
iter := p.newIter()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
@@ -395,7 +398,7 @@ func BenchmarkDBReadReverse(b *testing.B) {
|
|||||||
p := openDBBench(b, false)
|
p := openDBBench(b, false)
|
||||||
p.populate(b.N)
|
p.populate(b.N)
|
||||||
p.fill()
|
p.fill()
|
||||||
p.drop()
|
p.gc()
|
||||||
|
|
||||||
iter := p.newIter()
|
iter := p.newIter()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
@@ -413,7 +416,7 @@ func BenchmarkDBReadReverseTable(b *testing.B) {
|
|||||||
p.populate(b.N)
|
p.populate(b.N)
|
||||||
p.fill()
|
p.fill()
|
||||||
p.reopen()
|
p.reopen()
|
||||||
p.drop()
|
p.gc()
|
||||||
|
|
||||||
iter := p.newIter()
|
iter := p.newIter()
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
|
|||||||
+91
-57
@@ -11,115 +11,149 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetFunc used by Namespace.Get method to create a cache object. SetFunc
|
// SetFunc is the function that will be called by Namespace.Get to create
|
||||||
// may return ok false, in that case the cache object will not be created.
|
// a cache object, if charge is less than one than the cache object will
|
||||||
type SetFunc func() (ok bool, value interface{}, charge int, fin SetFin)
|
// not be registered to cache tree, if value is nil then the cache object
|
||||||
|
// will not be created.
|
||||||
|
type SetFunc func() (charge int, value interface{})
|
||||||
|
|
||||||
// SetFin will be called when corresponding cache object are released.
|
// DelFin is the function that will be called as the result of a delete operation.
|
||||||
type SetFin func()
|
// Exist == true is indication that the object is exist, and pending == true is
|
||||||
|
// indication of deletion already happen but haven't done yet (wait for all handles
|
||||||
|
// to be released). And exist == false means the object doesn't exist.
|
||||||
|
type DelFin func(exist, pending bool)
|
||||||
|
|
||||||
// DelFin will be called when corresponding cache object are released.
|
// PurgeFin is the function that will be called as the result of a purge operation.
|
||||||
// DelFin will be called after SetFin. The exist is true if the corresponding
|
type PurgeFin func(ns, key uint64)
|
||||||
// cache object is actually exist in the cache tree.
|
|
||||||
type DelFin func(exist bool)
|
|
||||||
|
|
||||||
// PurgeFin will be called when corresponding cache object are released.
|
|
||||||
// PurgeFin will be called after SetFin. If PurgeFin present DelFin will
|
|
||||||
// not be executed but passed to the PurgeFin, it is up to the caller
|
|
||||||
// to call it or not.
|
|
||||||
type PurgeFin func(ns, key uint64, delfin DelFin)
|
|
||||||
|
|
||||||
// Cache is a cache tree. A cache instance must be goroutine-safe.
|
// Cache is a cache tree. A cache instance must be goroutine-safe.
|
||||||
type Cache interface {
|
type Cache interface {
|
||||||
// SetCapacity sets cache capacity.
|
// SetCapacity sets cache tree capacity.
|
||||||
SetCapacity(capacity int)
|
SetCapacity(capacity int)
|
||||||
|
|
||||||
// GetNamespace gets or creates a cache namespace for the given id.
|
// Capacity returns cache tree capacity.
|
||||||
|
Capacity() int
|
||||||
|
|
||||||
|
// Used returns used cache tree capacity.
|
||||||
|
Used() int
|
||||||
|
|
||||||
|
// Size returns entire alive cache objects size.
|
||||||
|
Size() int
|
||||||
|
|
||||||
|
// NumObjects returns number of alive objects.
|
||||||
|
NumObjects() int
|
||||||
|
|
||||||
|
// GetNamespace gets cache namespace with the given id.
|
||||||
|
// GetNamespace is never return nil.
|
||||||
GetNamespace(id uint64) Namespace
|
GetNamespace(id uint64) Namespace
|
||||||
|
|
||||||
// Purge purges all cache namespaces, read Namespace.Purge method documentation.
|
// PurgeNamespace purges cache namespace with the given id from this cache tree.
|
||||||
|
// Also read Namespace.Purge.
|
||||||
|
PurgeNamespace(id uint64, fin PurgeFin)
|
||||||
|
|
||||||
|
// ZapNamespace detaches cache namespace with the given id from this cache tree.
|
||||||
|
// Also read Namespace.Zap.
|
||||||
|
ZapNamespace(id uint64)
|
||||||
|
|
||||||
|
// Purge purges all cache namespace from this cache tree.
|
||||||
|
// This is behave the same as calling Namespace.Purge method on all cache namespace.
|
||||||
Purge(fin PurgeFin)
|
Purge(fin PurgeFin)
|
||||||
|
|
||||||
// Zap zaps all cache namespaces, read Namespace.Zap method documentation.
|
// Zap detaches all cache namespace from this cache tree.
|
||||||
Zap(closed bool)
|
// This is behave the same as calling Namespace.Zap method on all cache namespace.
|
||||||
|
Zap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Namespace is a cache namespace. A namespace instance must be goroutine-safe.
|
// Namespace is a cache namespace. A namespace instance must be goroutine-safe.
|
||||||
type Namespace interface {
|
type Namespace interface {
|
||||||
// Get gets cache object for the given key. The given SetFunc (if not nil) will
|
// Get gets cache object with the given key.
|
||||||
// be called if the given key does not exist.
|
// If cache object is not found and setf is not nil, Get will atomically creates
|
||||||
// If the given key does not exist, SetFunc is nil or SetFunc return ok false, Get
|
// the cache object by calling setf. Otherwise Get will returns nil.
|
||||||
// will return ok false.
|
|
||||||
Get(key uint64, setf SetFunc) (obj Object, ok bool)
|
|
||||||
|
|
||||||
// Get deletes cache object for the given key. If exist the cache object will
|
|
||||||
// be deleted later when all of its handles have been released (i.e. no one use
|
|
||||||
// it anymore) and the given DelFin (if not nil) will finally be executed. If
|
|
||||||
// such cache object does not exist the given DelFin will be executed anyway.
|
|
||||||
//
|
//
|
||||||
// Delete returns true if such cache object exist.
|
// The returned cache handle should be released after use by calling Release
|
||||||
|
// method.
|
||||||
|
Get(key uint64, setf SetFunc) Handle
|
||||||
|
|
||||||
|
// Delete removes cache object with the given key from cache tree.
|
||||||
|
// A deleted cache object will be released as soon as all of its handles have
|
||||||
|
// been released.
|
||||||
|
// Delete only happen once, subsequent delete will consider cache object doesn't
|
||||||
|
// exist, even if the cache object ins't released yet.
|
||||||
|
//
|
||||||
|
// If not nil, fin will be called if the cache object doesn't exist or when
|
||||||
|
// finally be released.
|
||||||
|
//
|
||||||
|
// Delete returns true if such cache object exist and never been deleted.
|
||||||
Delete(key uint64, fin DelFin) bool
|
Delete(key uint64, fin DelFin) bool
|
||||||
|
|
||||||
// Purge deletes all cache objects, read Delete method documentation.
|
// Purge removes all cache objects within this namespace from cache tree.
|
||||||
|
// This is the same as doing delete on all cache objects.
|
||||||
|
//
|
||||||
|
// If not nil, fin will be called on all cache objects when its finally be
|
||||||
|
// released.
|
||||||
Purge(fin PurgeFin)
|
Purge(fin PurgeFin)
|
||||||
|
|
||||||
// Zap detaches the namespace from the cache tree and delete all its cache
|
// Zap detaches namespace from cache tree and release all its cache objects.
|
||||||
// objects. The cache objects deletion and finalizers execution are happen
|
// A zapped namespace can never be filled again.
|
||||||
// immediately, even if its existing handles haven't yet been released.
|
// Calling Get on zapped namespace will always return nil.
|
||||||
// A zapped namespace can't never be filled again.
|
Zap()
|
||||||
// If closed is false then the Get function will always call the given SetFunc
|
|
||||||
// if it is not nil, but resultant of the SetFunc will not be cached.
|
|
||||||
Zap(closed bool)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Object is a cache object.
|
// Handle is a cache handle.
|
||||||
type Object interface {
|
type Handle interface {
|
||||||
// Release releases the cache object. Other methods should not be called
|
// Release releases this cache handle. This method can be safely called mutiple
|
||||||
// after the cache object has been released.
|
// times.
|
||||||
Release()
|
Release()
|
||||||
|
|
||||||
// Value returns value of the cache object.
|
// Value returns value of this cache handle.
|
||||||
|
// Value will returns nil after this cache handle have be released.
|
||||||
Value() interface{}
|
Value() interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
DelNotExist = iota
|
||||||
|
DelExist
|
||||||
|
DelPendig
|
||||||
|
)
|
||||||
|
|
||||||
// Namespace state.
|
// Namespace state.
|
||||||
type nsState int
|
type nsState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
nsEffective nsState = iota
|
nsEffective nsState = iota
|
||||||
nsZapped
|
nsZapped
|
||||||
nsClosed
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Node state.
|
// Node state.
|
||||||
type nodeState int
|
type nodeState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
nodeEffective nodeState = iota
|
nodeZero nodeState = iota
|
||||||
|
nodeEffective
|
||||||
nodeEvicted
|
nodeEvicted
|
||||||
nodeRemoved
|
nodeDeleted
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fake object.
|
// Fake handle.
|
||||||
type fakeObject struct {
|
type fakeHandle struct {
|
||||||
value interface{}
|
value interface{}
|
||||||
fin func()
|
fin func()
|
||||||
once uint32
|
once uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *fakeObject) Value() interface{} {
|
func (h *fakeHandle) Value() interface{} {
|
||||||
if atomic.LoadUint32(&o.once) == 0 {
|
if atomic.LoadUint32(&h.once) == 0 {
|
||||||
return o.value
|
return h.value
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *fakeObject) Release() {
|
func (h *fakeHandle) Release() {
|
||||||
if !atomic.CompareAndSwapUint32(&o.once, 0, 1) {
|
if !atomic.CompareAndSwapUint32(&h.once, 0, 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if o.fin != nil {
|
if h.fin != nil {
|
||||||
o.fin()
|
h.fin()
|
||||||
o.fin = nil
|
h.fin = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+428
-67
@@ -7,15 +7,35 @@
|
|||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func set(ns Namespace, key uint64, value interface{}, charge int, fin func()) Object {
|
type releaserFunc struct {
|
||||||
obj, _ := ns.Get(key, func() (bool, interface{}, int, SetFin) {
|
fn func()
|
||||||
return true, value, charge, fin
|
value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r releaserFunc) Release() {
|
||||||
|
if r.fn != nil {
|
||||||
|
r.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(ns Namespace, key uint64, value interface{}, charge int, relf func()) Handle {
|
||||||
|
return ns.Get(key, func() (int, interface{}) {
|
||||||
|
if relf != nil {
|
||||||
|
return charge, releaserFunc{relf, value}
|
||||||
|
} else {
|
||||||
|
return charge, value
|
||||||
|
}
|
||||||
})
|
})
|
||||||
return obj
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCache_HitMiss(t *testing.T) {
|
func TestCache_HitMiss(t *testing.T) {
|
||||||
@@ -43,29 +63,31 @@ func TestCache_HitMiss(t *testing.T) {
|
|||||||
setfin++
|
setfin++
|
||||||
}).Release()
|
}).Release()
|
||||||
for j, y := range cases {
|
for j, y := range cases {
|
||||||
r, ok := ns.Get(y.key, nil)
|
h := ns.Get(y.key, nil)
|
||||||
if j <= i {
|
if j <= i {
|
||||||
// should hit
|
// should hit
|
||||||
if !ok {
|
if h == nil {
|
||||||
t.Errorf("case '%d' iteration '%d' is miss", i, j)
|
t.Errorf("case '%d' iteration '%d' is miss", i, j)
|
||||||
} else if r.Value().(string) != y.value {
|
} else {
|
||||||
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, r.Value().(string), y.value)
|
if x := h.Value().(releaserFunc).value.(string); x != y.value {
|
||||||
|
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, x, y.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// should miss
|
// should miss
|
||||||
if ok {
|
if h != nil {
|
||||||
t.Errorf("case '%d' iteration '%d' is hit , value '%s'", i, j, r.Value().(string))
|
t.Errorf("case '%d' iteration '%d' is hit , value '%s'", i, j, h.Value().(releaserFunc).value.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ok {
|
if h != nil {
|
||||||
r.Release()
|
h.Release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, x := range cases {
|
for i, x := range cases {
|
||||||
finalizerOk := false
|
finalizerOk := false
|
||||||
ns.Delete(x.key, func(exist bool) {
|
ns.Delete(x.key, func(exist, pending bool) {
|
||||||
finalizerOk = true
|
finalizerOk = true
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -74,22 +96,24 @@ func TestCache_HitMiss(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for j, y := range cases {
|
for j, y := range cases {
|
||||||
r, ok := ns.Get(y.key, nil)
|
h := ns.Get(y.key, nil)
|
||||||
if j > i {
|
if j > i {
|
||||||
// should hit
|
// should hit
|
||||||
if !ok {
|
if h == nil {
|
||||||
t.Errorf("case '%d' iteration '%d' is miss", i, j)
|
t.Errorf("case '%d' iteration '%d' is miss", i, j)
|
||||||
} else if r.Value().(string) != y.value {
|
} else {
|
||||||
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, r.Value().(string), y.value)
|
if x := h.Value().(releaserFunc).value.(string); x != y.value {
|
||||||
|
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, x, y.value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// should miss
|
// should miss
|
||||||
if ok {
|
if h != nil {
|
||||||
t.Errorf("case '%d' iteration '%d' is hit, value '%s'", i, j, r.Value().(string))
|
t.Errorf("case '%d' iteration '%d' is hit, value '%s'", i, j, h.Value().(releaserFunc).value.(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ok {
|
if h != nil {
|
||||||
r.Release()
|
h.Release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,42 +131,42 @@ func TestLRUCache_Eviction(t *testing.T) {
|
|||||||
set(ns, 3, 3, 1, nil).Release()
|
set(ns, 3, 3, 1, nil).Release()
|
||||||
set(ns, 4, 4, 1, nil).Release()
|
set(ns, 4, 4, 1, nil).Release()
|
||||||
set(ns, 5, 5, 1, nil).Release()
|
set(ns, 5, 5, 1, nil).Release()
|
||||||
if r, ok := ns.Get(2, nil); ok { // 1,3,4,5,2
|
if h := ns.Get(2, nil); h != nil { // 1,3,4,5,2
|
||||||
r.Release()
|
h.Release()
|
||||||
}
|
}
|
||||||
set(ns, 9, 9, 10, nil).Release() // 5,2,9
|
set(ns, 9, 9, 10, nil).Release() // 5,2,9
|
||||||
|
|
||||||
for _, x := range []uint64{9, 2, 5, 1} {
|
for _, key := range []uint64{9, 2, 5, 1} {
|
||||||
r, ok := ns.Get(x, nil)
|
h := ns.Get(key, nil)
|
||||||
if !ok {
|
if h == nil {
|
||||||
t.Errorf("miss for key '%d'", x)
|
t.Errorf("miss for key '%d'", key)
|
||||||
} else {
|
} else {
|
||||||
if r.Value().(int) != int(x) {
|
if x := h.Value().(int); x != int(key) {
|
||||||
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
|
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
|
||||||
}
|
}
|
||||||
r.Release()
|
h.Release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
o1.Release()
|
o1.Release()
|
||||||
for _, x := range []uint64{1, 2, 5} {
|
for _, key := range []uint64{1, 2, 5} {
|
||||||
r, ok := ns.Get(x, nil)
|
h := ns.Get(key, nil)
|
||||||
if !ok {
|
if h == nil {
|
||||||
t.Errorf("miss for key '%d'", x)
|
t.Errorf("miss for key '%d'", key)
|
||||||
} else {
|
} else {
|
||||||
if r.Value().(int) != int(x) {
|
if x := h.Value().(int); x != int(key) {
|
||||||
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
|
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
|
||||||
}
|
}
|
||||||
r.Release()
|
h.Release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, x := range []uint64{3, 4, 9} {
|
for _, key := range []uint64{3, 4, 9} {
|
||||||
r, ok := ns.Get(x, nil)
|
h := ns.Get(key, nil)
|
||||||
if ok {
|
if h != nil {
|
||||||
t.Errorf("hit for key '%d'", x)
|
t.Errorf("hit for key '%d'", key)
|
||||||
if r.Value().(int) != int(x) {
|
if x := h.Value().(int); x != int(key) {
|
||||||
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
|
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
|
||||||
}
|
}
|
||||||
r.Release()
|
h.Release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,16 +177,15 @@ func TestLRUCache_SetGet(t *testing.T) {
|
|||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
n := uint64(rand.Intn(99999) % 20)
|
n := uint64(rand.Intn(99999) % 20)
|
||||||
set(ns, n, n, 1, nil).Release()
|
set(ns, n, n, 1, nil).Release()
|
||||||
if p, ok := ns.Get(n, nil); ok {
|
if h := ns.Get(n, nil); h != nil {
|
||||||
if p.Value() == nil {
|
if h.Value() == nil {
|
||||||
t.Errorf("key '%d' contains nil value", n)
|
t.Errorf("key '%d' contains nil value", n)
|
||||||
} else {
|
} else {
|
||||||
got := p.Value().(uint64)
|
if x := h.Value().(uint64); x != n {
|
||||||
if got != n {
|
t.Errorf("invalid value for key '%d' want '%d', got '%d'", n, n, x)
|
||||||
t.Errorf("invalid value for key '%d' want '%d', got '%d'", n, n, got)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.Release()
|
h.Release()
|
||||||
} else {
|
} else {
|
||||||
t.Errorf("key '%d' doesn't exist", n)
|
t.Errorf("key '%d' doesn't exist", n)
|
||||||
}
|
}
|
||||||
@@ -176,31 +199,369 @@ func TestLRUCache_Purge(t *testing.T) {
|
|||||||
o2 := set(ns1, 2, 2, 1, nil)
|
o2 := set(ns1, 2, 2, 1, nil)
|
||||||
ns1.Purge(nil)
|
ns1.Purge(nil)
|
||||||
set(ns1, 3, 3, 1, nil).Release()
|
set(ns1, 3, 3, 1, nil).Release()
|
||||||
for _, x := range []uint64{1, 2, 3} {
|
for _, key := range []uint64{1, 2, 3} {
|
||||||
r, ok := ns1.Get(x, nil)
|
h := ns1.Get(key, nil)
|
||||||
if !ok {
|
if h == nil {
|
||||||
t.Errorf("miss for key '%d'", x)
|
t.Errorf("miss for key '%d'", key)
|
||||||
} else {
|
} else {
|
||||||
if r.Value().(int) != int(x) {
|
if x := h.Value().(int); x != int(key) {
|
||||||
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
|
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
|
||||||
}
|
}
|
||||||
r.Release()
|
h.Release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
o1.Release()
|
o1.Release()
|
||||||
o2.Release()
|
o2.Release()
|
||||||
for _, x := range []uint64{1, 2} {
|
for _, key := range []uint64{1, 2} {
|
||||||
r, ok := ns1.Get(x, nil)
|
h := ns1.Get(key, nil)
|
||||||
if ok {
|
if h != nil {
|
||||||
t.Errorf("hit for key '%d'", x)
|
t.Errorf("hit for key '%d'", key)
|
||||||
if r.Value().(int) != int(x) {
|
if x := h.Value().(int); x != int(key) {
|
||||||
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
|
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
|
||||||
}
|
}
|
||||||
r.Release()
|
h.Release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testingCacheObjectCounter struct {
|
||||||
|
created uint32
|
||||||
|
released uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testingCacheObjectCounter) createOne() {
|
||||||
|
atomic.AddUint32(&c.created, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testingCacheObjectCounter) releaseOne() {
|
||||||
|
atomic.AddUint32(&c.released, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type testingCacheObject struct {
|
||||||
|
t *testing.T
|
||||||
|
cnt *testingCacheObjectCounter
|
||||||
|
|
||||||
|
ns, key uint64
|
||||||
|
|
||||||
|
releaseCalled uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *testingCacheObject) Release() {
|
||||||
|
if atomic.CompareAndSwapUint32(&x.releaseCalled, 0, 1) {
|
||||||
|
x.cnt.releaseOne()
|
||||||
|
} else {
|
||||||
|
x.t.Errorf("duplicate setfin NS#%d KEY#%s", x.ns, x.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLRUCache_Finalizer(t *testing.T) {
|
||||||
|
const (
|
||||||
|
capacity = 100
|
||||||
|
goroutines = 100
|
||||||
|
iterations = 10000
|
||||||
|
keymax = 8000
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
defer runtime.GOMAXPROCS(1)
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
cnt := &testingCacheObjectCounter{}
|
||||||
|
|
||||||
|
c := NewLRUCache(capacity)
|
||||||
|
|
||||||
|
type instance struct {
|
||||||
|
seed int64
|
||||||
|
rnd *rand.Rand
|
||||||
|
ns uint64
|
||||||
|
effective int32
|
||||||
|
handles []Handle
|
||||||
|
handlesMap map[uint64]int
|
||||||
|
|
||||||
|
delete bool
|
||||||
|
purge bool
|
||||||
|
zap bool
|
||||||
|
wantDel int32
|
||||||
|
delfinCalledAll int32
|
||||||
|
delfinCalledEff int32
|
||||||
|
purgefinCalled int32
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceGet := func(p *instance, ns Namespace, key uint64) {
|
||||||
|
h := ns.Get(key, func() (charge int, value interface{}) {
|
||||||
|
to := &testingCacheObject{
|
||||||
|
t: t, cnt: cnt,
|
||||||
|
ns: p.ns,
|
||||||
|
key: key,
|
||||||
|
}
|
||||||
|
atomic.AddInt32(&p.effective, 1)
|
||||||
|
cnt.createOne()
|
||||||
|
return 1, releaserFunc{func() {
|
||||||
|
to.Release()
|
||||||
|
atomic.AddInt32(&p.effective, -1)
|
||||||
|
}, to}
|
||||||
|
})
|
||||||
|
p.handles = append(p.handles, h)
|
||||||
|
p.handlesMap[key] = p.handlesMap[key] + 1
|
||||||
|
}
|
||||||
|
instanceRelease := func(p *instance, ns Namespace, i int) {
|
||||||
|
h := p.handles[i]
|
||||||
|
key := h.Value().(releaserFunc).value.(*testingCacheObject).key
|
||||||
|
if n := p.handlesMap[key]; n == 0 {
|
||||||
|
t.Fatal("key ref == 0")
|
||||||
|
} else if n > 1 {
|
||||||
|
p.handlesMap[key] = n - 1
|
||||||
|
} else {
|
||||||
|
delete(p.handlesMap, key)
|
||||||
|
}
|
||||||
|
h.Release()
|
||||||
|
p.handles = append(p.handles[:i], p.handles[i+1:]...)
|
||||||
|
p.handles[len(p.handles) : len(p.handles)+1][0] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
seeds := make([]int64, goroutines)
|
||||||
|
instances := make([]instance, goroutines)
|
||||||
|
for i := range instances {
|
||||||
|
p := &instances[i]
|
||||||
|
p.handlesMap = make(map[uint64]int)
|
||||||
|
if seeds[i] == 0 {
|
||||||
|
seeds[i] = time.Now().UnixNano()
|
||||||
|
}
|
||||||
|
p.seed = seeds[i]
|
||||||
|
p.rnd = rand.New(rand.NewSource(p.seed))
|
||||||
|
p.ns = uint64(i)
|
||||||
|
p.delete = i%6 == 0
|
||||||
|
p.purge = i%8 == 0
|
||||||
|
p.zap = i%12 == 0 || i%3 == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
seedsStr := make([]string, len(seeds))
|
||||||
|
for i, seed := range seeds {
|
||||||
|
seedsStr[i] = fmt.Sprint(seed)
|
||||||
|
}
|
||||||
|
t.Logf("seeds := []int64{%s}", strings.Join(seedsStr, ", "))
|
||||||
|
|
||||||
|
// Get and release.
|
||||||
|
for i := range instances {
|
||||||
|
p := &instances[i]
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(p *instance) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
ns := c.GetNamespace(p.ns)
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
if len(p.handles) == 0 || p.rnd.Int()%2 == 0 {
|
||||||
|
instanceGet(p, ns, uint64(p.rnd.Intn(keymax)))
|
||||||
|
} else {
|
||||||
|
instanceRelease(p, ns, p.rnd.Intn(len(p.handles)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(p)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if used, cap := c.Used(), c.Capacity(); used > cap {
|
||||||
|
t.Errorf("Used > capacity, used=%d cap=%d", used, cap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check effective objects.
|
||||||
|
for i := range instances {
|
||||||
|
p := &instances[i]
|
||||||
|
if int(p.effective) < len(p.handlesMap) {
|
||||||
|
t.Errorf("#%d effective objects < acquired handle, eo=%d ah=%d", i, p.effective, len(p.handlesMap))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if want := int(cnt.created - cnt.released); c.Size() != want {
|
||||||
|
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete and purge.
|
||||||
|
for i := range instances {
|
||||||
|
p := &instances[i]
|
||||||
|
p.wantDel = p.effective
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(p *instance) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
ns := c.GetNamespace(p.ns)
|
||||||
|
|
||||||
|
if p.delete {
|
||||||
|
for key := uint64(0); key < keymax; key++ {
|
||||||
|
_, wantExist := p.handlesMap[key]
|
||||||
|
gotExist := ns.Delete(key, func(exist, pending bool) {
|
||||||
|
atomic.AddInt32(&p.delfinCalledAll, 1)
|
||||||
|
if exist {
|
||||||
|
atomic.AddInt32(&p.delfinCalledEff, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if !gotExist && wantExist {
|
||||||
|
t.Errorf("delete on NS#%d KEY#%d not found", p.ns, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var delfinCalled int
|
||||||
|
for key := uint64(0); key < keymax; key++ {
|
||||||
|
func(key uint64) {
|
||||||
|
gotExist := ns.Delete(key, func(exist, pending bool) {
|
||||||
|
if exist && !pending {
|
||||||
|
t.Errorf("delete fin on NS#%d KEY#%d exist and not pending for deletion", p.ns, key)
|
||||||
|
}
|
||||||
|
delfinCalled++
|
||||||
|
})
|
||||||
|
if gotExist {
|
||||||
|
t.Errorf("delete on NS#%d KEY#%d found", p.ns, key)
|
||||||
|
}
|
||||||
|
}(key)
|
||||||
|
}
|
||||||
|
if delfinCalled != keymax {
|
||||||
|
t.Errorf("(2) #%d not all delete fin called, diff=%d", p.ns, keymax-delfinCalled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.purge {
|
||||||
|
ns.Purge(func(ns, key uint64) {
|
||||||
|
atomic.AddInt32(&p.purgefinCalled, 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}(p)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if want := int(cnt.created - cnt.released); c.Size() != want {
|
||||||
|
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release.
|
||||||
|
for i := range instances {
|
||||||
|
p := &instances[i]
|
||||||
|
|
||||||
|
if !p.zap {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(p *instance) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
ns := c.GetNamespace(p.ns)
|
||||||
|
for i := len(p.handles) - 1; i >= 0; i-- {
|
||||||
|
instanceRelease(p, ns, i)
|
||||||
|
}
|
||||||
|
}(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if want := int(cnt.created - cnt.released); c.Size() != want {
|
||||||
|
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zap.
|
||||||
|
for i := range instances {
|
||||||
|
p := &instances[i]
|
||||||
|
|
||||||
|
if p.zap {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(p *instance) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
ns := c.GetNamespace(p.ns)
|
||||||
|
ns.Zap()
|
||||||
|
|
||||||
|
p.handles = nil
|
||||||
|
p.handlesMap = nil
|
||||||
|
}(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if want := int(cnt.created - cnt.released); c.Size() != want {
|
||||||
|
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
if notrel, used := int(cnt.created-cnt.released), c.Used(); notrel != used {
|
||||||
|
t.Errorf("Invalid used value, want=%d got=%d", notrel, used)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Purge(nil)
|
||||||
|
|
||||||
|
for i := range instances {
|
||||||
|
p := &instances[i]
|
||||||
|
|
||||||
|
if p.delete {
|
||||||
|
if p.delfinCalledAll != keymax {
|
||||||
|
t.Errorf("#%d not all delete fin called, purge=%v zap=%v diff=%d", p.ns, p.purge, p.zap, keymax-p.delfinCalledAll)
|
||||||
|
}
|
||||||
|
if p.delfinCalledEff != p.wantDel {
|
||||||
|
t.Errorf("#%d not all effective delete fin called, diff=%d", p.ns, p.wantDel-p.delfinCalledEff)
|
||||||
|
}
|
||||||
|
if p.purge && p.purgefinCalled > 0 {
|
||||||
|
t.Errorf("#%d some purge fin called, delete=%v zap=%v n=%d", p.ns, p.delete, p.zap, p.purgefinCalled)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if p.purge {
|
||||||
|
if p.purgefinCalled != p.wantDel {
|
||||||
|
t.Errorf("#%d not all purge fin called, delete=%v zap=%v diff=%d", p.ns, p.delete, p.zap, p.wantDel-p.purgefinCalled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cnt.created != cnt.released {
|
||||||
|
t.Errorf("Some cache object weren't released, created=%d released=%d", cnt.created, cnt.released)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLRUCache_Set(b *testing.B) {
|
||||||
|
c := NewLRUCache(0)
|
||||||
|
ns := c.GetNamespace(0)
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := uint64(0); i < uint64(b.N); i++ {
|
||||||
|
set(ns, i, "", 1, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLRUCache_Get(b *testing.B) {
|
||||||
|
c := NewLRUCache(0)
|
||||||
|
ns := c.GetNamespace(0)
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := uint64(0); i < uint64(b.N); i++ {
|
||||||
|
set(ns, i, "", 1, nil)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := uint64(0); i < uint64(b.N); i++ {
|
||||||
|
ns.Get(i, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLRUCache_Get2(b *testing.B) {
|
||||||
|
c := NewLRUCache(0)
|
||||||
|
ns := c.GetNamespace(0)
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := uint64(0); i < uint64(b.N); i++ {
|
||||||
|
set(ns, i, "", 1, nil)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := uint64(0); i < uint64(b.N); i++ {
|
||||||
|
ns.Get(i, func() (charge int, value interface{}) {
|
||||||
|
return 0, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkLRUCache_Release(b *testing.B) {
|
||||||
|
c := NewLRUCache(0)
|
||||||
|
ns := c.GetNamespace(0)
|
||||||
|
handles := make([]Handle, b.N)
|
||||||
|
for i := uint64(0); i < uint64(b.N); i++ {
|
||||||
|
handles[i] = set(ns, i, "", 1, nil)
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for _, h := range handles {
|
||||||
|
h.Release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkLRUCache_SetRelease(b *testing.B) {
|
func BenchmarkLRUCache_SetRelease(b *testing.B) {
|
||||||
capacity := b.N / 100
|
capacity := b.N / 100
|
||||||
if capacity <= 0 {
|
if capacity <= 0 {
|
||||||
@@ -210,7 +571,7 @@ func BenchmarkLRUCache_SetRelease(b *testing.B) {
|
|||||||
ns := c.GetNamespace(0)
|
ns := c.GetNamespace(0)
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := uint64(0); i < uint64(b.N); i++ {
|
for i := uint64(0); i < uint64(b.N); i++ {
|
||||||
set(ns, i, nil, 1, nil).Release()
|
set(ns, i, "", 1, nil).Release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,10 +588,10 @@ func BenchmarkLRUCache_SetReleaseTwice(b *testing.B) {
|
|||||||
nb := b.N - na
|
nb := b.N - na
|
||||||
|
|
||||||
for i := uint64(0); i < uint64(na); i++ {
|
for i := uint64(0); i < uint64(na); i++ {
|
||||||
set(ns, i, nil, 1, nil).Release()
|
set(ns, i, "", 1, nil).Release()
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := uint64(0); i < uint64(nb); i++ {
|
for i := uint64(0); i < uint64(nb); i++ {
|
||||||
set(ns, i, nil, 1, nil).Release()
|
set(ns, i, "", 1, nil).Release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-246
@@ -1,246 +0,0 @@
|
|||||||
// Copyright (c) 2013, Suryandaru Triandana <syndtr@gmail.com>
|
|
||||||
// All rights reserved.
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style license that can be
|
|
||||||
// found in the LICENSE file.
|
|
||||||
|
|
||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
type emptyCache struct {
|
|
||||||
sync.Mutex
|
|
||||||
table map[uint64]*emptyNS
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEmptyCache creates a new initialized empty cache.
|
|
||||||
func NewEmptyCache() Cache {
|
|
||||||
return &emptyCache{
|
|
||||||
table: make(map[uint64]*emptyNS),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *emptyCache) GetNamespace(id uint64) Namespace {
|
|
||||||
c.Lock()
|
|
||||||
defer c.Unlock()
|
|
||||||
|
|
||||||
if ns, ok := c.table[id]; ok {
|
|
||||||
return ns
|
|
||||||
}
|
|
||||||
|
|
||||||
ns := &emptyNS{
|
|
||||||
cache: c,
|
|
||||||
id: id,
|
|
||||||
table: make(map[uint64]*emptyNode),
|
|
||||||
}
|
|
||||||
c.table[id] = ns
|
|
||||||
return ns
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *emptyCache) Purge(fin PurgeFin) {
|
|
||||||
c.Lock()
|
|
||||||
for _, ns := range c.table {
|
|
||||||
ns.purgeNB(fin)
|
|
||||||
}
|
|
||||||
c.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *emptyCache) Zap(closed bool) {
|
|
||||||
c.Lock()
|
|
||||||
for _, ns := range c.table {
|
|
||||||
ns.zapNB(closed)
|
|
||||||
}
|
|
||||||
c.table = make(map[uint64]*emptyNS)
|
|
||||||
c.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*emptyCache) SetCapacity(capacity int) {}
|
|
||||||
|
|
||||||
type emptyNS struct {
|
|
||||||
cache *emptyCache
|
|
||||||
id uint64
|
|
||||||
table map[uint64]*emptyNode
|
|
||||||
state nsState
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *emptyNS) Get(key uint64, setf SetFunc) (o Object, ok bool) {
|
|
||||||
ns.cache.Lock()
|
|
||||||
|
|
||||||
switch ns.state {
|
|
||||||
case nsZapped:
|
|
||||||
ns.cache.Unlock()
|
|
||||||
if setf == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var value interface{}
|
|
||||||
var fin func()
|
|
||||||
ok, value, _, fin = setf()
|
|
||||||
if ok {
|
|
||||||
o = &fakeObject{
|
|
||||||
value: value,
|
|
||||||
fin: fin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case nsClosed:
|
|
||||||
ns.cache.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
n, ok := ns.table[key]
|
|
||||||
if ok {
|
|
||||||
n.ref++
|
|
||||||
} else {
|
|
||||||
if setf == nil {
|
|
||||||
ns.cache.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var value interface{}
|
|
||||||
var fin func()
|
|
||||||
ok, value, _, fin = setf()
|
|
||||||
if !ok {
|
|
||||||
ns.cache.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
n = &emptyNode{
|
|
||||||
ns: ns,
|
|
||||||
key: key,
|
|
||||||
value: value,
|
|
||||||
setfin: fin,
|
|
||||||
ref: 1,
|
|
||||||
}
|
|
||||||
ns.table[key] = n
|
|
||||||
}
|
|
||||||
|
|
||||||
ns.cache.Unlock()
|
|
||||||
o = &emptyObject{node: n}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *emptyNS) Delete(key uint64, fin DelFin) bool {
|
|
||||||
ns.cache.Lock()
|
|
||||||
|
|
||||||
if ns.state != nsEffective {
|
|
||||||
ns.cache.Unlock()
|
|
||||||
if fin != nil {
|
|
||||||
fin(false)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
n, ok := ns.table[key]
|
|
||||||
if !ok {
|
|
||||||
ns.cache.Unlock()
|
|
||||||
if fin != nil {
|
|
||||||
fin(false)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
n.delfin = fin
|
|
||||||
ns.cache.Unlock()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *emptyNS) purgeNB(fin PurgeFin) {
|
|
||||||
if ns.state != nsEffective {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, n := range ns.table {
|
|
||||||
n.purgefin = fin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *emptyNS) Purge(fin PurgeFin) {
|
|
||||||
ns.cache.Lock()
|
|
||||||
ns.purgeNB(fin)
|
|
||||||
ns.cache.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *emptyNS) zapNB(closed bool) {
|
|
||||||
if ns.state != nsEffective {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, n := range ns.table {
|
|
||||||
n.execFin()
|
|
||||||
}
|
|
||||||
if closed {
|
|
||||||
ns.state = nsClosed
|
|
||||||
} else {
|
|
||||||
ns.state = nsZapped
|
|
||||||
}
|
|
||||||
ns.table = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ns *emptyNS) Zap(closed bool) {
|
|
||||||
ns.cache.Lock()
|
|
||||||
ns.zapNB(closed)
|
|
||||||
delete(ns.cache.table, ns.id)
|
|
||||||
ns.cache.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
type emptyNode struct {
|
|
||||||
ns *emptyNS
|
|
||||||
key uint64
|
|
||||||
value interface{}
|
|
||||||
ref int
|
|
||||||
setfin SetFin
|
|
||||||
delfin DelFin
|
|
||||||
purgefin PurgeFin
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *emptyNode) execFin() {
|
|
||||||
if n.setfin != nil {
|
|
||||||
n.setfin()
|
|
||||||
n.setfin = nil
|
|
||||||
}
|
|
||||||
if n.purgefin != nil {
|
|
||||||
n.purgefin(n.ns.id, n.key, n.delfin)
|
|
||||||
n.delfin = nil
|
|
||||||
n.purgefin = nil
|
|
||||||
} else if n.delfin != nil {
|
|
||||||
n.delfin(true)
|
|
||||||
n.delfin = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *emptyNode) evict() {
|
|
||||||
n.ns.cache.Lock()
|
|
||||||
n.ref--
|
|
||||||
if n.ref == 0 {
|
|
||||||
if n.ns.state == nsEffective {
|
|
||||||
// Remove elem.
|
|
||||||
delete(n.ns.table, n.key)
|
|
||||||
// Execute finalizer.
|
|
||||||
n.execFin()
|
|
||||||
}
|
|
||||||
} else if n.ref < 0 {
|
|
||||||
panic("leveldb/cache: emptyNode: negative node reference")
|
|
||||||
}
|
|
||||||
n.ns.cache.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
type emptyObject struct {
|
|
||||||
node *emptyNode
|
|
||||||
once uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *emptyObject) Value() interface{} {
|
|
||||||
if atomic.LoadUint32(&o.once) == 0 {
|
|
||||||
return o.node.value
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *emptyObject) Release() {
|
|
||||||
if !atomic.CompareAndSwapUint32(&o.once, 0, 1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
o.node.evict()
|
|
||||||
o.node = nil
|
|
||||||
}
|
|
||||||
+437
-178
@@ -9,16 +9,24 @@ package cache
|
|||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/syndtr/goleveldb/leveldb/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The LLRB implementation were taken from https://github.com/petar/GoLLRB.
|
||||||
|
// Which contains the following header:
|
||||||
|
//
|
||||||
|
// Copyright 2010 Petar Maymounkov. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// lruCache represent a LRU cache state.
|
// lruCache represent a LRU cache state.
|
||||||
type lruCache struct {
|
type lruCache struct {
|
||||||
sync.Mutex
|
mu sync.Mutex
|
||||||
|
recent lruNode
|
||||||
recent lruNode
|
table map[uint64]*lruNs
|
||||||
table map[uint64]*lruNs
|
capacity int
|
||||||
capacity int
|
used, size, alive int
|
||||||
size int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLRUCache creates a new initialized LRU cache with the given capacity.
|
// NewLRUCache creates a new initialized LRU cache with the given capacity.
|
||||||
@@ -32,245 +40,408 @@ func NewLRUCache(capacity int) Cache {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *lruCache) Capacity() int {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lruCache) Used() int {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.used
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lruCache) Size() int {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lruCache) NumObjects() int {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.alive
|
||||||
|
}
|
||||||
|
|
||||||
// SetCapacity set cache capacity.
|
// SetCapacity set cache capacity.
|
||||||
func (c *lruCache) SetCapacity(capacity int) {
|
func (c *lruCache) SetCapacity(capacity int) {
|
||||||
c.Lock()
|
c.mu.Lock()
|
||||||
c.capacity = capacity
|
c.capacity = capacity
|
||||||
c.evict()
|
c.evict()
|
||||||
c.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNamespace return namespace object for given id.
|
// GetNamespace return namespace object for given id.
|
||||||
func (c *lruCache) GetNamespace(id uint64) Namespace {
|
func (c *lruCache) GetNamespace(id uint64) Namespace {
|
||||||
c.Lock()
|
c.mu.Lock()
|
||||||
defer c.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
if p, ok := c.table[id]; ok {
|
if ns, ok := c.table[id]; ok {
|
||||||
return p
|
return ns
|
||||||
}
|
}
|
||||||
|
|
||||||
p := &lruNs{
|
ns := &lruNs{lru: c, id: id}
|
||||||
lru: c,
|
c.table[id] = ns
|
||||||
id: id,
|
return ns
|
||||||
table: make(map[uint64]*lruNode),
|
}
|
||||||
|
|
||||||
|
func (c *lruCache) ZapNamespace(id uint64) {
|
||||||
|
c.mu.Lock()
|
||||||
|
if ns, exist := c.table[id]; exist {
|
||||||
|
ns.zapNB()
|
||||||
|
delete(c.table, id)
|
||||||
}
|
}
|
||||||
c.table[id] = p
|
c.mu.Unlock()
|
||||||
return p
|
}
|
||||||
|
|
||||||
|
func (c *lruCache) PurgeNamespace(id uint64, fin PurgeFin) {
|
||||||
|
c.mu.Lock()
|
||||||
|
if ns, exist := c.table[id]; exist {
|
||||||
|
ns.purgeNB(fin)
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Purge purge entire cache.
|
// Purge purge entire cache.
|
||||||
func (c *lruCache) Purge(fin PurgeFin) {
|
func (c *lruCache) Purge(fin PurgeFin) {
|
||||||
c.Lock()
|
c.mu.Lock()
|
||||||
for _, ns := range c.table {
|
for _, ns := range c.table {
|
||||||
ns.purgeNB(fin)
|
ns.purgeNB(fin)
|
||||||
}
|
}
|
||||||
c.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *lruCache) Zap(closed bool) {
|
func (c *lruCache) Zap() {
|
||||||
c.Lock()
|
c.mu.Lock()
|
||||||
for _, ns := range c.table {
|
for _, ns := range c.table {
|
||||||
ns.zapNB(closed)
|
ns.zapNB()
|
||||||
}
|
}
|
||||||
c.table = make(map[uint64]*lruNs)
|
c.table = make(map[uint64]*lruNs)
|
||||||
c.Unlock()
|
c.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *lruCache) evict() {
|
func (c *lruCache) evict() {
|
||||||
top := &c.recent
|
top := &c.recent
|
||||||
for n := c.recent.rPrev; c.size > c.capacity && n != top; {
|
for n := c.recent.rPrev; c.used > c.capacity && n != top; {
|
||||||
n.state = nodeEvicted
|
n.state = nodeEvicted
|
||||||
n.rRemove()
|
n.rRemove()
|
||||||
n.evictNB()
|
n.derefNB()
|
||||||
c.size -= n.charge
|
c.used -= n.charge
|
||||||
n = c.recent.rPrev
|
n = c.recent.rPrev
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type lruNs struct {
|
type lruNs struct {
|
||||||
lru *lruCache
|
lru *lruCache
|
||||||
id uint64
|
id uint64
|
||||||
table map[uint64]*lruNode
|
rbRoot *lruNode
|
||||||
state nsState
|
state nsState
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *lruNs) Get(key uint64, setf SetFunc) (o Object, ok bool) {
|
func (ns *lruNs) rbGetOrCreateNode(h *lruNode, key uint64) (hn, n *lruNode) {
|
||||||
lru := ns.lru
|
if h == nil {
|
||||||
lru.Lock()
|
n = &lruNode{ns: ns, key: key}
|
||||||
|
return n, n
|
||||||
switch ns.state {
|
|
||||||
case nsZapped:
|
|
||||||
lru.Unlock()
|
|
||||||
if setf == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var value interface{}
|
|
||||||
var fin func()
|
|
||||||
ok, value, _, fin = setf()
|
|
||||||
if ok {
|
|
||||||
o = &fakeObject{
|
|
||||||
value: value,
|
|
||||||
fin: fin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case nsClosed:
|
|
||||||
lru.Unlock()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
n, ok := ns.table[key]
|
if key < h.key {
|
||||||
if ok {
|
hn, n = ns.rbGetOrCreateNode(h.rbLeft, key)
|
||||||
switch n.state {
|
if hn != nil {
|
||||||
case nodeEvicted:
|
h.rbLeft = hn
|
||||||
// Insert to recent list.
|
} else {
|
||||||
n.state = nodeEffective
|
return nil, n
|
||||||
n.ref++
|
}
|
||||||
lru.size += n.charge
|
} else if key > h.key {
|
||||||
lru.evict()
|
hn, n = ns.rbGetOrCreateNode(h.rbRight, key)
|
||||||
fallthrough
|
if hn != nil {
|
||||||
case nodeEffective:
|
h.rbRight = hn
|
||||||
// Bump to front
|
} else {
|
||||||
n.rRemove()
|
return nil, n
|
||||||
n.rInsert(&lru.recent)
|
|
||||||
}
|
}
|
||||||
n.ref++
|
|
||||||
} else {
|
} else {
|
||||||
if setf == nil {
|
return nil, h
|
||||||
lru.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var value interface{}
|
|
||||||
var charge int
|
|
||||||
var fin func()
|
|
||||||
ok, value, charge, fin = setf()
|
|
||||||
if !ok {
|
|
||||||
lru.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
n = &lruNode{
|
|
||||||
ns: ns,
|
|
||||||
key: key,
|
|
||||||
value: value,
|
|
||||||
charge: charge,
|
|
||||||
setfin: fin,
|
|
||||||
ref: 2,
|
|
||||||
}
|
|
||||||
ns.table[key] = n
|
|
||||||
n.rInsert(&lru.recent)
|
|
||||||
|
|
||||||
lru.size += charge
|
|
||||||
lru.evict()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lru.Unlock()
|
if rbIsRed(h.rbRight) && !rbIsRed(h.rbLeft) {
|
||||||
o = &lruObject{node: n}
|
h = rbRotLeft(h)
|
||||||
return
|
}
|
||||||
|
if rbIsRed(h.rbLeft) && rbIsRed(h.rbLeft.rbLeft) {
|
||||||
|
h = rbRotRight(h)
|
||||||
|
}
|
||||||
|
if rbIsRed(h.rbLeft) && rbIsRed(h.rbRight) {
|
||||||
|
rbFlip(h)
|
||||||
|
}
|
||||||
|
return h, n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *lruNs) getOrCreateNode(key uint64) *lruNode {
|
||||||
|
hn, n := ns.rbGetOrCreateNode(ns.rbRoot, key)
|
||||||
|
if hn != nil {
|
||||||
|
ns.rbRoot = hn
|
||||||
|
ns.rbRoot.rbBlack = true
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *lruNs) rbGetNode(key uint64) *lruNode {
|
||||||
|
h := ns.rbRoot
|
||||||
|
for h != nil {
|
||||||
|
switch {
|
||||||
|
case key < h.key:
|
||||||
|
h = h.rbLeft
|
||||||
|
case key > h.key:
|
||||||
|
h = h.rbRight
|
||||||
|
default:
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *lruNs) getNode(key uint64) *lruNode {
|
||||||
|
return ns.rbGetNode(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *lruNs) rbDeleteNode(h *lruNode, key uint64) *lruNode {
|
||||||
|
if h == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if key < h.key {
|
||||||
|
if h.rbLeft == nil { // key not present. Nothing to delete
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
if !rbIsRed(h.rbLeft) && !rbIsRed(h.rbLeft.rbLeft) {
|
||||||
|
h = rbMoveLeft(h)
|
||||||
|
}
|
||||||
|
h.rbLeft = ns.rbDeleteNode(h.rbLeft, key)
|
||||||
|
} else {
|
||||||
|
if rbIsRed(h.rbLeft) {
|
||||||
|
h = rbRotRight(h)
|
||||||
|
}
|
||||||
|
// If @key equals @h.key and no right children at @h
|
||||||
|
if h.key == key && h.rbRight == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if h.rbRight != nil && !rbIsRed(h.rbRight) && !rbIsRed(h.rbRight.rbLeft) {
|
||||||
|
h = rbMoveRight(h)
|
||||||
|
}
|
||||||
|
// If @key equals @h.key, and (from above) 'h.Right != nil'
|
||||||
|
if h.key == key {
|
||||||
|
var x *lruNode
|
||||||
|
h.rbRight, x = rbDeleteMin(h.rbRight)
|
||||||
|
if x == nil {
|
||||||
|
panic("logic")
|
||||||
|
}
|
||||||
|
x.rbLeft, h.rbLeft = h.rbLeft, nil
|
||||||
|
x.rbRight, h.rbRight = h.rbRight, nil
|
||||||
|
x.rbBlack = h.rbBlack
|
||||||
|
h = x
|
||||||
|
} else { // Else, @key is bigger than @h.key
|
||||||
|
h.rbRight = ns.rbDeleteNode(h.rbRight, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rbFixup(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *lruNs) deleteNode(key uint64) {
|
||||||
|
ns.rbRoot = ns.rbDeleteNode(ns.rbRoot, key)
|
||||||
|
if ns.rbRoot != nil {
|
||||||
|
ns.rbRoot.rbBlack = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *lruNs) rbIterateNodes(h *lruNode, pivot uint64, iter func(n *lruNode) bool) bool {
|
||||||
|
if h == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if h.key >= pivot {
|
||||||
|
if !ns.rbIterateNodes(h.rbLeft, pivot, iter) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !iter(h) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ns.rbIterateNodes(h.rbRight, pivot, iter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *lruNs) iterateNodes(iter func(n *lruNode) bool) {
|
||||||
|
ns.rbIterateNodes(ns.rbRoot, 0, iter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ns *lruNs) Get(key uint64, setf SetFunc) Handle {
|
||||||
|
ns.lru.mu.Lock()
|
||||||
|
defer ns.lru.mu.Unlock()
|
||||||
|
|
||||||
|
if ns.state != nsEffective {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var n *lruNode
|
||||||
|
if setf == nil {
|
||||||
|
n = ns.getNode(key)
|
||||||
|
if n == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
n = ns.getOrCreateNode(key)
|
||||||
|
}
|
||||||
|
switch n.state {
|
||||||
|
case nodeZero:
|
||||||
|
charge, value := setf()
|
||||||
|
if value == nil {
|
||||||
|
ns.deleteNode(key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if charge < 0 {
|
||||||
|
charge = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
n.value = value
|
||||||
|
n.charge = charge
|
||||||
|
n.state = nodeEvicted
|
||||||
|
|
||||||
|
ns.lru.size += charge
|
||||||
|
ns.lru.alive++
|
||||||
|
|
||||||
|
fallthrough
|
||||||
|
case nodeEvicted:
|
||||||
|
if n.charge == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert to recent list.
|
||||||
|
n.state = nodeEffective
|
||||||
|
n.ref++
|
||||||
|
ns.lru.used += n.charge
|
||||||
|
ns.lru.evict()
|
||||||
|
|
||||||
|
fallthrough
|
||||||
|
case nodeEffective:
|
||||||
|
// Bump to front.
|
||||||
|
n.rRemove()
|
||||||
|
n.rInsert(&ns.lru.recent)
|
||||||
|
}
|
||||||
|
n.ref++
|
||||||
|
|
||||||
|
return &lruHandle{node: n}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *lruNs) Delete(key uint64, fin DelFin) bool {
|
func (ns *lruNs) Delete(key uint64, fin DelFin) bool {
|
||||||
lru := ns.lru
|
ns.lru.mu.Lock()
|
||||||
lru.Lock()
|
defer ns.lru.mu.Unlock()
|
||||||
|
|
||||||
if ns.state != nsEffective {
|
if ns.state != nsEffective {
|
||||||
lru.Unlock()
|
|
||||||
if fin != nil {
|
if fin != nil {
|
||||||
fin(false)
|
fin(false, false)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
n, ok := ns.table[key]
|
n := ns.getNode(key)
|
||||||
if !ok {
|
if n == nil {
|
||||||
lru.Unlock()
|
|
||||||
if fin != nil {
|
if fin != nil {
|
||||||
fin(false)
|
fin(false, false)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
n.delfin = fin
|
|
||||||
switch n.state {
|
switch n.state {
|
||||||
case nodeRemoved:
|
|
||||||
lru.Unlock()
|
|
||||||
return false
|
|
||||||
case nodeEffective:
|
case nodeEffective:
|
||||||
lru.size -= n.charge
|
ns.lru.used -= n.charge
|
||||||
|
n.state = nodeDeleted
|
||||||
|
n.delfin = fin
|
||||||
n.rRemove()
|
n.rRemove()
|
||||||
n.evictNB()
|
n.derefNB()
|
||||||
|
case nodeEvicted:
|
||||||
|
n.state = nodeDeleted
|
||||||
|
n.delfin = fin
|
||||||
|
case nodeDeleted:
|
||||||
|
if fin != nil {
|
||||||
|
fin(true, true)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
panic("invalid state")
|
||||||
}
|
}
|
||||||
n.state = nodeRemoved
|
|
||||||
|
|
||||||
lru.Unlock()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *lruNs) purgeNB(fin PurgeFin) {
|
func (ns *lruNs) purgeNB(fin PurgeFin) {
|
||||||
lru := ns.lru
|
if ns.state == nsEffective {
|
||||||
if ns.state != nsEffective {
|
var nodes []*lruNode
|
||||||
return
|
ns.iterateNodes(func(n *lruNode) bool {
|
||||||
}
|
nodes = append(nodes, n)
|
||||||
|
return true
|
||||||
for _, n := range ns.table {
|
})
|
||||||
n.purgefin = fin
|
for _, n := range nodes {
|
||||||
if n.state == nodeEffective {
|
switch n.state {
|
||||||
lru.size -= n.charge
|
case nodeEffective:
|
||||||
n.rRemove()
|
ns.lru.used -= n.charge
|
||||||
n.evictNB()
|
n.state = nodeDeleted
|
||||||
|
n.purgefin = fin
|
||||||
|
n.rRemove()
|
||||||
|
n.derefNB()
|
||||||
|
case nodeEvicted:
|
||||||
|
n.state = nodeDeleted
|
||||||
|
n.purgefin = fin
|
||||||
|
case nodeDeleted:
|
||||||
|
default:
|
||||||
|
panic("invalid state")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
n.state = nodeRemoved
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *lruNs) Purge(fin PurgeFin) {
|
func (ns *lruNs) Purge(fin PurgeFin) {
|
||||||
ns.lru.Lock()
|
ns.lru.mu.Lock()
|
||||||
ns.purgeNB(fin)
|
ns.purgeNB(fin)
|
||||||
ns.lru.Unlock()
|
ns.lru.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *lruNs) zapNB(closed bool) {
|
func (ns *lruNs) zapNB() {
|
||||||
lru := ns.lru
|
if ns.state == nsEffective {
|
||||||
if ns.state != nsEffective {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if closed {
|
|
||||||
ns.state = nsClosed
|
|
||||||
} else {
|
|
||||||
ns.state = nsZapped
|
ns.state = nsZapped
|
||||||
|
|
||||||
|
ns.iterateNodes(func(n *lruNode) bool {
|
||||||
|
if n.state == nodeEffective {
|
||||||
|
ns.lru.used -= n.charge
|
||||||
|
n.rRemove()
|
||||||
|
}
|
||||||
|
ns.lru.size -= n.charge
|
||||||
|
n.state = nodeDeleted
|
||||||
|
n.fin()
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
ns.rbRoot = nil
|
||||||
}
|
}
|
||||||
for _, n := range ns.table {
|
|
||||||
if n.state == nodeEffective {
|
|
||||||
lru.size -= n.charge
|
|
||||||
n.rRemove()
|
|
||||||
}
|
|
||||||
n.state = nodeRemoved
|
|
||||||
n.execFin()
|
|
||||||
}
|
|
||||||
ns.table = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ns *lruNs) Zap(closed bool) {
|
func (ns *lruNs) Zap() {
|
||||||
ns.lru.Lock()
|
ns.lru.mu.Lock()
|
||||||
ns.zapNB(closed)
|
ns.zapNB()
|
||||||
delete(ns.lru.table, ns.id)
|
delete(ns.lru.table, ns.id)
|
||||||
ns.lru.Unlock()
|
ns.lru.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
type lruNode struct {
|
type lruNode struct {
|
||||||
ns *lruNs
|
ns *lruNs
|
||||||
|
|
||||||
rNext, rPrev *lruNode
|
rNext, rPrev *lruNode
|
||||||
|
rbLeft, rbRight *lruNode
|
||||||
|
rbBlack bool
|
||||||
|
|
||||||
key uint64
|
key uint64
|
||||||
value interface{}
|
value interface{}
|
||||||
charge int
|
charge int
|
||||||
ref int
|
ref int
|
||||||
state nodeState
|
state nodeState
|
||||||
setfin SetFin
|
|
||||||
delfin DelFin
|
delfin DelFin
|
||||||
purgefin PurgeFin
|
purgefin PurgeFin
|
||||||
}
|
}
|
||||||
@@ -284,7 +455,6 @@ func (n *lruNode) rInsert(at *lruNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *lruNode) rRemove() bool {
|
func (n *lruNode) rRemove() bool {
|
||||||
// only remove if not already removed
|
|
||||||
if n.rPrev == nil {
|
if n.rPrev == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -297,58 +467,147 @@ func (n *lruNode) rRemove() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *lruNode) execFin() {
|
func (n *lruNode) fin() {
|
||||||
if n.setfin != nil {
|
if r, ok := n.value.(util.Releaser); ok {
|
||||||
n.setfin()
|
r.Release()
|
||||||
n.setfin = nil
|
|
||||||
}
|
}
|
||||||
if n.purgefin != nil {
|
if n.purgefin != nil {
|
||||||
n.purgefin(n.ns.id, n.key, n.delfin)
|
n.purgefin(n.ns.id, n.key)
|
||||||
n.delfin = nil
|
n.delfin = nil
|
||||||
n.purgefin = nil
|
n.purgefin = nil
|
||||||
} else if n.delfin != nil {
|
} else if n.delfin != nil {
|
||||||
n.delfin(true)
|
n.delfin(true, false)
|
||||||
n.delfin = nil
|
n.delfin = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *lruNode) evictNB() {
|
func (n *lruNode) derefNB() {
|
||||||
n.ref--
|
n.ref--
|
||||||
if n.ref == 0 {
|
if n.ref == 0 {
|
||||||
if n.ns.state == nsEffective {
|
if n.ns.state == nsEffective {
|
||||||
// remove elem
|
// Remove elemement.
|
||||||
delete(n.ns.table, n.key)
|
n.ns.deleteNode(n.key)
|
||||||
// execute finalizer
|
n.ns.lru.size -= n.charge
|
||||||
n.execFin()
|
n.ns.lru.alive--
|
||||||
|
n.fin()
|
||||||
}
|
}
|
||||||
|
n.value = nil
|
||||||
} else if n.ref < 0 {
|
} else if n.ref < 0 {
|
||||||
panic("leveldb/cache: lruCache: negative node reference")
|
panic("leveldb/cache: lruCache: negative node reference")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *lruNode) evict() {
|
func (n *lruNode) deref() {
|
||||||
n.ns.lru.Lock()
|
n.ns.lru.mu.Lock()
|
||||||
n.evictNB()
|
n.derefNB()
|
||||||
n.ns.lru.Unlock()
|
n.ns.lru.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
type lruObject struct {
|
type lruHandle struct {
|
||||||
node *lruNode
|
node *lruNode
|
||||||
once uint32
|
once uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *lruObject) Value() interface{} {
|
func (h *lruHandle) Value() interface{} {
|
||||||
if atomic.LoadUint32(&o.once) == 0 {
|
if atomic.LoadUint32(&h.once) == 0 {
|
||||||
return o.node.value
|
return h.node.value
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *lruObject) Release() {
|
func (h *lruHandle) Release() {
|
||||||
if !atomic.CompareAndSwapUint32(&o.once, 0, 1) {
|
if !atomic.CompareAndSwapUint32(&h.once, 0, 1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
h.node.deref()
|
||||||
|
h.node = nil
|
||||||
|
}
|
||||||
|
|
||||||
o.node.evict()
|
func rbIsRed(h *lruNode) bool {
|
||||||
o.node = nil
|
if h == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !h.rbBlack
|
||||||
|
}
|
||||||
|
|
||||||
|
func rbRotLeft(h *lruNode) *lruNode {
|
||||||
|
x := h.rbRight
|
||||||
|
if x.rbBlack {
|
||||||
|
panic("rotating a black link")
|
||||||
|
}
|
||||||
|
h.rbRight = x.rbLeft
|
||||||
|
x.rbLeft = h
|
||||||
|
x.rbBlack = h.rbBlack
|
||||||
|
h.rbBlack = false
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
func rbRotRight(h *lruNode) *lruNode {
|
||||||
|
x := h.rbLeft
|
||||||
|
if x.rbBlack {
|
||||||
|
panic("rotating a black link")
|
||||||
|
}
|
||||||
|
h.rbLeft = x.rbRight
|
||||||
|
x.rbRight = h
|
||||||
|
x.rbBlack = h.rbBlack
|
||||||
|
h.rbBlack = false
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
func rbFlip(h *lruNode) {
|
||||||
|
h.rbBlack = !h.rbBlack
|
||||||
|
h.rbLeft.rbBlack = !h.rbLeft.rbBlack
|
||||||
|
h.rbRight.rbBlack = !h.rbRight.rbBlack
|
||||||
|
}
|
||||||
|
|
||||||
|
func rbMoveLeft(h *lruNode) *lruNode {
|
||||||
|
rbFlip(h)
|
||||||
|
if rbIsRed(h.rbRight.rbLeft) {
|
||||||
|
h.rbRight = rbRotRight(h.rbRight)
|
||||||
|
h = rbRotLeft(h)
|
||||||
|
rbFlip(h)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func rbMoveRight(h *lruNode) *lruNode {
|
||||||
|
rbFlip(h)
|
||||||
|
if rbIsRed(h.rbLeft.rbLeft) {
|
||||||
|
h = rbRotRight(h)
|
||||||
|
rbFlip(h)
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func rbFixup(h *lruNode) *lruNode {
|
||||||
|
if rbIsRed(h.rbRight) {
|
||||||
|
h = rbRotLeft(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rbIsRed(h.rbLeft) && rbIsRed(h.rbLeft.rbLeft) {
|
||||||
|
h = rbRotRight(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rbIsRed(h.rbLeft) && rbIsRed(h.rbRight) {
|
||||||
|
rbFlip(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func rbDeleteMin(h *lruNode) (hn, n *lruNode) {
|
||||||
|
if h == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if h.rbLeft == nil {
|
||||||
|
return nil, h
|
||||||
|
}
|
||||||
|
|
||||||
|
if !rbIsRed(h.rbLeft) && !rbIsRed(h.rbLeft.rbLeft) {
|
||||||
|
h = rbMoveLeft(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.rbLeft, n = rbDeleteMin(h.rbLeft)
|
||||||
|
|
||||||
|
return rbFixup(h), n
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-10
@@ -14,6 +14,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||||
@@ -35,8 +36,8 @@ type DB struct {
|
|||||||
|
|
||||||
// MemDB.
|
// MemDB.
|
||||||
memMu sync.RWMutex
|
memMu sync.RWMutex
|
||||||
mem *memdb.DB
|
memPool chan *memdb.DB
|
||||||
frozenMem *memdb.DB
|
mem, frozenMem *memDB
|
||||||
journal *journal.Writer
|
journal *journal.Writer
|
||||||
journalWriter storage.Writer
|
journalWriter storage.Writer
|
||||||
journalFile storage.File
|
journalFile storage.File
|
||||||
@@ -47,6 +48,9 @@ type DB struct {
|
|||||||
snapsMu sync.Mutex
|
snapsMu sync.Mutex
|
||||||
snapsRoot snapshotElement
|
snapsRoot snapshotElement
|
||||||
|
|
||||||
|
// Stats.
|
||||||
|
aliveSnaps, aliveIters int32
|
||||||
|
|
||||||
// Write.
|
// Write.
|
||||||
writeC chan *Batch
|
writeC chan *Batch
|
||||||
writeMergedC chan bool
|
writeMergedC chan bool
|
||||||
@@ -79,6 +83,8 @@ func openDB(s *session) (*DB, error) {
|
|||||||
s: s,
|
s: s,
|
||||||
// Initial sequence
|
// Initial sequence
|
||||||
seq: s.stSeq,
|
seq: s.stSeq,
|
||||||
|
// MemDB
|
||||||
|
memPool: make(chan *memdb.DB, 1),
|
||||||
// Write
|
// Write
|
||||||
writeC: make(chan *Batch),
|
writeC: make(chan *Batch),
|
||||||
writeMergedC: make(chan bool),
|
writeMergedC: make(chan bool),
|
||||||
@@ -120,6 +126,7 @@ func openDB(s *session) (*DB, error) {
|
|||||||
go db.tCompaction()
|
go db.tCompaction()
|
||||||
go db.mCompaction()
|
go db.mCompaction()
|
||||||
go db.jWriter()
|
go db.jWriter()
|
||||||
|
go db.mpoolDrain()
|
||||||
|
|
||||||
s.logf("db@open done T·%v", time.Since(start))
|
s.logf("db@open done T·%v", time.Since(start))
|
||||||
|
|
||||||
@@ -257,6 +264,7 @@ func recoverTable(s *session, o *opt.Options) error {
|
|||||||
var mSeq uint64
|
var mSeq uint64
|
||||||
var good, corrupted int
|
var good, corrupted int
|
||||||
rec := new(sessionRecord)
|
rec := new(sessionRecord)
|
||||||
|
bpool := util.NewBufferPool(o.GetBlockSize() + 5)
|
||||||
buildTable := func(iter iterator.Iterator) (tmp storage.File, size int64, err error) {
|
buildTable := func(iter iterator.Iterator) (tmp storage.File, size int64, err error) {
|
||||||
tmp = s.newTemp()
|
tmp = s.newTemp()
|
||||||
writer, err := tmp.Create()
|
writer, err := tmp.Create()
|
||||||
@@ -314,7 +322,7 @@ func recoverTable(s *session, o *opt.Options) error {
|
|||||||
var tSeq uint64
|
var tSeq uint64
|
||||||
var tgood, tcorrupted, blockerr int
|
var tgood, tcorrupted, blockerr int
|
||||||
var imin, imax []byte
|
var imin, imax []byte
|
||||||
tr := table.NewReader(reader, size, nil, o)
|
tr := table.NewReader(reader, size, nil, bpool, o)
|
||||||
iter := tr.NewIterator(nil, nil)
|
iter := tr.NewIterator(nil, nil)
|
||||||
iter.(iterator.ErrorCallbackSetter).SetErrorCallback(func(err error) {
|
iter.(iterator.ErrorCallbackSetter).SetErrorCallback(func(err error) {
|
||||||
s.logf("table@recovery found error @%d %q", file.Num(), err)
|
s.logf("table@recovery found error @%d %q", file.Num(), err)
|
||||||
@@ -481,10 +489,11 @@ func (db *DB) recoverJournal() error {
|
|||||||
|
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
if _, err := buf.ReadFrom(r); err != nil {
|
if _, err := buf.ReadFrom(r); err != nil {
|
||||||
if strict {
|
if err == io.ErrUnexpectedEOF {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if err := batch.decode(buf.Bytes()); err != nil {
|
if err := batch.decode(buf.Bytes()); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -558,19 +567,20 @@ func (db *DB) get(key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, er
|
|||||||
ikey := newIKey(key, seq, tSeek)
|
ikey := newIKey(key, seq, tSeek)
|
||||||
|
|
||||||
em, fm := db.getMems()
|
em, fm := db.getMems()
|
||||||
for _, m := range [...]*memdb.DB{em, fm} {
|
for _, m := range [...]*memDB{em, fm} {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
defer m.decref()
|
||||||
|
|
||||||
mk, mv, me := m.Find(ikey)
|
mk, mv, me := m.mdb.Find(ikey)
|
||||||
if me == nil {
|
if me == nil {
|
||||||
ukey, _, t, ok := parseIkey(mk)
|
ukey, _, t, ok := parseIkey(mk)
|
||||||
if ok && db.s.icmp.uCompare(ukey, key) == 0 {
|
if ok && db.s.icmp.uCompare(ukey, key) == 0 {
|
||||||
if t == tDel {
|
if t == tDel {
|
||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
return mv, nil
|
return append([]byte{}, mv...), nil
|
||||||
}
|
}
|
||||||
} else if me != ErrNotFound {
|
} else if me != ErrNotFound {
|
||||||
return nil, me
|
return nil, me
|
||||||
@@ -590,8 +600,9 @@ func (db *DB) get(key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, er
|
|||||||
// Get gets the value for the given key. It returns ErrNotFound if the
|
// Get gets the value for the given key. It returns ErrNotFound if the
|
||||||
// DB does not contain the key.
|
// DB does not contain the key.
|
||||||
//
|
//
|
||||||
// The caller should not modify the contents of the returned slice, but
|
// The returned slice is its own copy, it is safe to modify the contents
|
||||||
// it is safe to modify the contents of the argument after Get returns.
|
// of the returned slice.
|
||||||
|
// It is safe to modify the contents of the argument after Get returns.
|
||||||
func (db *DB) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
|
func (db *DB) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
|
||||||
err = db.ok()
|
err = db.ok()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -649,6 +660,16 @@ func (db *DB) GetSnapshot() (*Snapshot, error) {
|
|||||||
// Returns statistics of the underlying DB.
|
// Returns statistics of the underlying DB.
|
||||||
// leveldb.sstables
|
// leveldb.sstables
|
||||||
// Returns sstables list for each level.
|
// Returns sstables list for each level.
|
||||||
|
// leveldb.blockpool
|
||||||
|
// Returns block pool stats.
|
||||||
|
// leveldb.cachedblock
|
||||||
|
// Returns size of cached block.
|
||||||
|
// leveldb.openedtables
|
||||||
|
// Returns number of opened tables.
|
||||||
|
// leveldb.alivesnaps
|
||||||
|
// Returns number of alive snapshots.
|
||||||
|
// leveldb.aliveiters
|
||||||
|
// Returns number of alive iterators.
|
||||||
func (db *DB) GetProperty(name string) (value string, err error) {
|
func (db *DB) GetProperty(name string) (value string, err error) {
|
||||||
err = db.ok()
|
err = db.ok()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -694,6 +715,20 @@ func (db *DB) GetProperty(name string) (value string, err error) {
|
|||||||
value += fmt.Sprintf("%d:%d[%q .. %q]\n", t.file.Num(), t.size, t.imin, t.imax)
|
value += fmt.Sprintf("%d:%d[%q .. %q]\n", t.file.Num(), t.size, t.imin, t.imax)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case p == "blockpool":
|
||||||
|
value = fmt.Sprintf("%v", db.s.tops.bpool)
|
||||||
|
case p == "cachedblock":
|
||||||
|
if bc := db.s.o.GetBlockCache(); bc != nil {
|
||||||
|
value = fmt.Sprintf("%d", bc.Size())
|
||||||
|
} else {
|
||||||
|
value = "<nil>"
|
||||||
|
}
|
||||||
|
case p == "openedtables":
|
||||||
|
value = fmt.Sprintf("%d", db.s.tops.cache.Size())
|
||||||
|
case p == "alivesnaps":
|
||||||
|
value = fmt.Sprintf("%d", atomic.LoadInt32(&db.aliveSnaps))
|
||||||
|
case p == "aliveiters":
|
||||||
|
value = fmt.Sprintf("%d", atomic.LoadInt32(&db.aliveIters))
|
||||||
default:
|
default:
|
||||||
err = errors.New("leveldb: GetProperty: unknown property: " + name)
|
err = errors.New("leveldb: GetProperty: unknown property: " + name)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -216,14 +216,15 @@ func (db *DB) memCompaction() {
|
|||||||
if mem == nil {
|
if mem == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer mem.decref()
|
||||||
|
|
||||||
c := newCMem(db.s)
|
c := newCMem(db.s)
|
||||||
stats := new(cStatsStaging)
|
stats := new(cStatsStaging)
|
||||||
|
|
||||||
db.logf("mem@flush N·%d S·%s", mem.Len(), shortenb(mem.Size()))
|
db.logf("mem@flush N·%d S·%s", mem.mdb.Len(), shortenb(mem.mdb.Size()))
|
||||||
|
|
||||||
// Don't compact empty memdb.
|
// Don't compact empty memdb.
|
||||||
if mem.Len() == 0 {
|
if mem.mdb.Len() == 0 {
|
||||||
db.logf("mem@flush skipping")
|
db.logf("mem@flush skipping")
|
||||||
// drop frozen mem
|
// drop frozen mem
|
||||||
db.dropFrozenMem()
|
db.dropFrozenMem()
|
||||||
@@ -241,7 +242,7 @@ func (db *DB) memCompaction() {
|
|||||||
db.compactionTransact("mem@flush", func(cnt *compactionTransactCounter) (err error) {
|
db.compactionTransact("mem@flush", func(cnt *compactionTransactCounter) (err error) {
|
||||||
stats.startTimer()
|
stats.startTimer()
|
||||||
defer stats.stopTimer()
|
defer stats.stopTimer()
|
||||||
return c.flush(mem, -1)
|
return c.flush(mem.mdb, -1)
|
||||||
}, func() error {
|
}, func() error {
|
||||||
for _, r := range c.rec.addedTables {
|
for _, r := range c.rec.addedTables {
|
||||||
db.logf("mem@flush rollback @%d", r.num)
|
db.logf("mem@flush rollback @%d", r.num)
|
||||||
|
|||||||
+25
-2
@@ -9,6 +9,8 @@ package leveldb
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||||
@@ -19,6 +21,17 @@ var (
|
|||||||
errInvalidIkey = errors.New("leveldb: Iterator: invalid internal key")
|
errInvalidIkey = errors.New("leveldb: Iterator: invalid internal key")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type memdbReleaser struct {
|
||||||
|
once sync.Once
|
||||||
|
m *memDB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *memdbReleaser) Release() {
|
||||||
|
mr.once.Do(func() {
|
||||||
|
mr.m.decref()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (db *DB) newRawIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
|
func (db *DB) newRawIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
|
||||||
em, fm := db.getMems()
|
em, fm := db.getMems()
|
||||||
v := db.s.version()
|
v := db.s.version()
|
||||||
@@ -26,9 +39,13 @@ func (db *DB) newRawIterator(slice *util.Range, ro *opt.ReadOptions) iterator.It
|
|||||||
ti := v.getIterators(slice, ro)
|
ti := v.getIterators(slice, ro)
|
||||||
n := len(ti) + 2
|
n := len(ti) + 2
|
||||||
i := make([]iterator.Iterator, 0, n)
|
i := make([]iterator.Iterator, 0, n)
|
||||||
i = append(i, em.NewIterator(slice))
|
emi := em.mdb.NewIterator(slice)
|
||||||
|
emi.SetReleaser(&memdbReleaser{m: em})
|
||||||
|
i = append(i, emi)
|
||||||
if fm != nil {
|
if fm != nil {
|
||||||
i = append(i, fm.NewIterator(slice))
|
fmi := fm.mdb.NewIterator(slice)
|
||||||
|
fmi.SetReleaser(&memdbReleaser{m: fm})
|
||||||
|
i = append(i, fmi)
|
||||||
}
|
}
|
||||||
i = append(i, ti...)
|
i = append(i, ti...)
|
||||||
strict := db.s.o.GetStrict(opt.StrictIterator) || ro.GetStrict(opt.StrictIterator)
|
strict := db.s.o.GetStrict(opt.StrictIterator) || ro.GetStrict(opt.StrictIterator)
|
||||||
@@ -50,6 +67,7 @@ func (db *DB) newIterator(seq uint64, slice *util.Range, ro *opt.ReadOptions) *d
|
|||||||
}
|
}
|
||||||
rawIter := db.newRawIterator(islice, ro)
|
rawIter := db.newRawIterator(islice, ro)
|
||||||
iter := &dbIter{
|
iter := &dbIter{
|
||||||
|
db: db,
|
||||||
icmp: db.s.icmp,
|
icmp: db.s.icmp,
|
||||||
iter: rawIter,
|
iter: rawIter,
|
||||||
seq: seq,
|
seq: seq,
|
||||||
@@ -57,6 +75,7 @@ func (db *DB) newIterator(seq uint64, slice *util.Range, ro *opt.ReadOptions) *d
|
|||||||
key: make([]byte, 0),
|
key: make([]byte, 0),
|
||||||
value: make([]byte, 0),
|
value: make([]byte, 0),
|
||||||
}
|
}
|
||||||
|
atomic.AddInt32(&db.aliveIters, 1)
|
||||||
runtime.SetFinalizer(iter, (*dbIter).Release)
|
runtime.SetFinalizer(iter, (*dbIter).Release)
|
||||||
return iter
|
return iter
|
||||||
}
|
}
|
||||||
@@ -73,6 +92,7 @@ const (
|
|||||||
|
|
||||||
// dbIter represent an interator states over a database session.
|
// dbIter represent an interator states over a database session.
|
||||||
type dbIter struct {
|
type dbIter struct {
|
||||||
|
db *DB
|
||||||
icmp *iComparer
|
icmp *iComparer
|
||||||
iter iterator.Iterator
|
iter iterator.Iterator
|
||||||
seq uint64
|
seq uint64
|
||||||
@@ -287,6 +307,7 @@ func (i *dbIter) Release() {
|
|||||||
|
|
||||||
if i.releaser != nil {
|
if i.releaser != nil {
|
||||||
i.releaser.Release()
|
i.releaser.Release()
|
||||||
|
i.releaser = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
i.dir = dirReleased
|
i.dir = dirReleased
|
||||||
@@ -294,6 +315,8 @@ func (i *dbIter) Release() {
|
|||||||
i.value = nil
|
i.value = nil
|
||||||
i.iter.Release()
|
i.iter.Release()
|
||||||
i.iter = nil
|
i.iter = nil
|
||||||
|
atomic.AddInt32(&i.db.aliveIters, -1)
|
||||||
|
i.db = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -9,6 +9,7 @@ package leveldb
|
|||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||||
@@ -81,7 +82,7 @@ func (db *DB) minSeq() uint64 {
|
|||||||
type Snapshot struct {
|
type Snapshot struct {
|
||||||
db *DB
|
db *DB
|
||||||
elem *snapshotElement
|
elem *snapshotElement
|
||||||
mu sync.Mutex
|
mu sync.RWMutex
|
||||||
released bool
|
released bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +92,7 @@ func (db *DB) newSnapshot() *Snapshot {
|
|||||||
db: db,
|
db: db,
|
||||||
elem: db.acquireSnapshot(),
|
elem: db.acquireSnapshot(),
|
||||||
}
|
}
|
||||||
|
atomic.AddInt32(&db.aliveSnaps, 1)
|
||||||
runtime.SetFinalizer(snap, (*Snapshot).Release)
|
runtime.SetFinalizer(snap, (*Snapshot).Release)
|
||||||
return snap
|
return snap
|
||||||
}
|
}
|
||||||
@@ -105,8 +107,8 @@ func (snap *Snapshot) Get(key []byte, ro *opt.ReadOptions) (value []byte, err er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
snap.mu.Lock()
|
snap.mu.RLock()
|
||||||
defer snap.mu.Unlock()
|
defer snap.mu.RUnlock()
|
||||||
if snap.released {
|
if snap.released {
|
||||||
err = ErrSnapshotReleased
|
err = ErrSnapshotReleased
|
||||||
return
|
return
|
||||||
@@ -160,6 +162,7 @@ func (snap *Snapshot) Release() {
|
|||||||
|
|
||||||
snap.released = true
|
snap.released = true
|
||||||
snap.db.releaseSnapshot(snap.elem)
|
snap.db.releaseSnapshot(snap.elem)
|
||||||
|
atomic.AddInt32(&snap.db.aliveSnaps, -1)
|
||||||
snap.db = nil
|
snap.db = nil
|
||||||
snap.elem = nil
|
snap.elem = nil
|
||||||
}
|
}
|
||||||
|
|||||||
+93
-6
@@ -8,11 +8,36 @@ package leveldb
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/syndtr/goleveldb/leveldb/journal"
|
"github.com/syndtr/goleveldb/leveldb/journal"
|
||||||
"github.com/syndtr/goleveldb/leveldb/memdb"
|
"github.com/syndtr/goleveldb/leveldb/memdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type memDB struct {
|
||||||
|
db *DB
|
||||||
|
mdb *memdb.DB
|
||||||
|
ref int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memDB) incref() {
|
||||||
|
atomic.AddInt32(&m.ref, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memDB) decref() {
|
||||||
|
if ref := atomic.AddInt32(&m.ref, -1); ref == 0 {
|
||||||
|
// Only put back memdb with std capacity.
|
||||||
|
if m.mdb.Capacity() == m.db.s.o.GetWriteBuffer() {
|
||||||
|
m.mdb.Reset()
|
||||||
|
m.db.mpoolPut(m.mdb)
|
||||||
|
}
|
||||||
|
m.db = nil
|
||||||
|
m.mdb = nil
|
||||||
|
} else if ref < 0 {
|
||||||
|
panic("negative memdb ref")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get latest sequence number.
|
// Get latest sequence number.
|
||||||
func (db *DB) getSeq() uint64 {
|
func (db *DB) getSeq() uint64 {
|
||||||
return atomic.LoadUint64(&db.seq)
|
return atomic.LoadUint64(&db.seq)
|
||||||
@@ -23,9 +48,44 @@ func (db *DB) addSeq(delta uint64) {
|
|||||||
atomic.AddUint64(&db.seq, delta)
|
atomic.AddUint64(&db.seq, delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) mpoolPut(mem *memdb.DB) {
|
||||||
|
defer func() {
|
||||||
|
recover()
|
||||||
|
}()
|
||||||
|
select {
|
||||||
|
case db.memPool <- mem:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) mpoolGet() *memdb.DB {
|
||||||
|
select {
|
||||||
|
case mem := <-db.memPool:
|
||||||
|
return mem
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) mpoolDrain() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
select {
|
||||||
|
case <-db.memPool:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
case _, _ = <-db.closeC:
|
||||||
|
close(db.memPool)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create new memdb and froze the old one; need external synchronization.
|
// Create new memdb and froze the old one; need external synchronization.
|
||||||
// newMem only called synchronously by the writer.
|
// newMem only called synchronously by the writer.
|
||||||
func (db *DB) newMem(n int) (mem *memdb.DB, err error) {
|
func (db *DB) newMem(n int) (mem *memDB, err error) {
|
||||||
num := db.s.allocFileNum()
|
num := db.s.allocFileNum()
|
||||||
file := db.s.getJournalFile(num)
|
file := db.s.getJournalFile(num)
|
||||||
w, err := file.Create()
|
w, err := file.Create()
|
||||||
@@ -37,6 +97,10 @@ func (db *DB) newMem(n int) (mem *memdb.DB, err error) {
|
|||||||
db.memMu.Lock()
|
db.memMu.Lock()
|
||||||
defer db.memMu.Unlock()
|
defer db.memMu.Unlock()
|
||||||
|
|
||||||
|
if db.frozenMem != nil {
|
||||||
|
panic("still has frozen mem")
|
||||||
|
}
|
||||||
|
|
||||||
if db.journal == nil {
|
if db.journal == nil {
|
||||||
db.journal = journal.NewWriter(w)
|
db.journal = journal.NewWriter(w)
|
||||||
} else {
|
} else {
|
||||||
@@ -47,8 +111,16 @@ func (db *DB) newMem(n int) (mem *memdb.DB, err error) {
|
|||||||
db.journalWriter = w
|
db.journalWriter = w
|
||||||
db.journalFile = file
|
db.journalFile = file
|
||||||
db.frozenMem = db.mem
|
db.frozenMem = db.mem
|
||||||
db.mem = memdb.New(db.s.icmp, maxInt(db.s.o.GetWriteBuffer(), n))
|
mdb := db.mpoolGet()
|
||||||
mem = db.mem
|
if mdb == nil || mdb.Capacity() < n {
|
||||||
|
mdb = memdb.New(db.s.icmp, maxInt(db.s.o.GetWriteBuffer(), n))
|
||||||
|
}
|
||||||
|
mem = &memDB{
|
||||||
|
db: db,
|
||||||
|
mdb: mdb,
|
||||||
|
ref: 2,
|
||||||
|
}
|
||||||
|
db.mem = mem
|
||||||
// The seq only incremented by the writer. And whoever called newMem
|
// The seq only incremented by the writer. And whoever called newMem
|
||||||
// should hold write lock, so no need additional synchronization here.
|
// should hold write lock, so no need additional synchronization here.
|
||||||
db.frozenSeq = db.seq
|
db.frozenSeq = db.seq
|
||||||
@@ -56,16 +128,27 @@ func (db *DB) newMem(n int) (mem *memdb.DB, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all memdbs.
|
// Get all memdbs.
|
||||||
func (db *DB) getMems() (e *memdb.DB, f *memdb.DB) {
|
func (db *DB) getMems() (e, f *memDB) {
|
||||||
db.memMu.RLock()
|
db.memMu.RLock()
|
||||||
defer db.memMu.RUnlock()
|
defer db.memMu.RUnlock()
|
||||||
|
if db.mem == nil {
|
||||||
|
panic("nil effective mem")
|
||||||
|
}
|
||||||
|
db.mem.incref()
|
||||||
|
if db.frozenMem != nil {
|
||||||
|
db.frozenMem.incref()
|
||||||
|
}
|
||||||
return db.mem, db.frozenMem
|
return db.mem, db.frozenMem
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get frozen memdb.
|
// Get frozen memdb.
|
||||||
func (db *DB) getEffectiveMem() *memdb.DB {
|
func (db *DB) getEffectiveMem() *memDB {
|
||||||
db.memMu.RLock()
|
db.memMu.RLock()
|
||||||
defer db.memMu.RUnlock()
|
defer db.memMu.RUnlock()
|
||||||
|
if db.mem == nil {
|
||||||
|
panic("nil effective mem")
|
||||||
|
}
|
||||||
|
db.mem.incref()
|
||||||
return db.mem
|
return db.mem
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,9 +160,12 @@ func (db *DB) hasFrozenMem() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get frozen memdb.
|
// Get frozen memdb.
|
||||||
func (db *DB) getFrozenMem() *memdb.DB {
|
func (db *DB) getFrozenMem() *memDB {
|
||||||
db.memMu.RLock()
|
db.memMu.RLock()
|
||||||
defer db.memMu.RUnlock()
|
defer db.memMu.RUnlock()
|
||||||
|
if db.frozenMem != nil {
|
||||||
|
db.frozenMem.incref()
|
||||||
|
}
|
||||||
return db.frozenMem
|
return db.frozenMem
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +178,7 @@ func (db *DB) dropFrozenMem() {
|
|||||||
db.logf("journal@remove removed @%d", db.frozenJournalFile.Num())
|
db.logf("journal@remove removed @%d", db.frozenJournalFile.Num())
|
||||||
}
|
}
|
||||||
db.frozenJournalFile = nil
|
db.frozenJournalFile = nil
|
||||||
|
db.frozenMem.decref()
|
||||||
db.frozenMem = nil
|
db.frozenMem = nil
|
||||||
db.memMu.Unlock()
|
db.memMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -1210,7 +1210,7 @@ func TestDb_DeletionMarkers2(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDb_CompactionTableOpenError(t *testing.T) {
|
func TestDb_CompactionTableOpenError(t *testing.T) {
|
||||||
h := newDbHarnessWopt(t, &opt.Options{MaxOpenFiles: 0})
|
h := newDbHarnessWopt(t, &opt.Options{CachedOpenFiles: -1})
|
||||||
defer h.close()
|
defer h.close()
|
||||||
|
|
||||||
im := 10
|
im := 10
|
||||||
@@ -1577,7 +1577,11 @@ func TestDb_BloomFilter(t *testing.T) {
|
|||||||
return fmt.Sprintf("key%06d", i)
|
return fmt.Sprintf("key%06d", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
n := 10000
|
const (
|
||||||
|
n = 10000
|
||||||
|
indexOverhead = 19898
|
||||||
|
filterOverhead = 19799
|
||||||
|
)
|
||||||
|
|
||||||
// Populate multiple layers
|
// Populate multiple layers
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
@@ -1601,7 +1605,7 @@ func TestDb_BloomFilter(t *testing.T) {
|
|||||||
cnt := int(h.stor.ReadCounter())
|
cnt := int(h.stor.ReadCounter())
|
||||||
t.Logf("lookup of %d present keys yield %d sstable I/O reads", n, cnt)
|
t.Logf("lookup of %d present keys yield %d sstable I/O reads", n, cnt)
|
||||||
|
|
||||||
if min, max := n, n+2*n/100; cnt < min || cnt > max {
|
if min, max := n+indexOverhead+filterOverhead, n+indexOverhead+filterOverhead+2*n/100; cnt < min || cnt > max {
|
||||||
t.Errorf("num of sstable I/O reads of present keys not in range of %d - %d, got %d", min, max, cnt)
|
t.Errorf("num of sstable I/O reads of present keys not in range of %d - %d, got %d", min, max, cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1612,7 +1616,7 @@ func TestDb_BloomFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
cnt = int(h.stor.ReadCounter())
|
cnt = int(h.stor.ReadCounter())
|
||||||
t.Logf("lookup of %d missing keys yield %d sstable I/O reads", n, cnt)
|
t.Logf("lookup of %d missing keys yield %d sstable I/O reads", n, cnt)
|
||||||
if max := 3 * n / 100; cnt > max {
|
if max := 3*n/100 + indexOverhead + filterOverhead; cnt > max {
|
||||||
t.Errorf("num of sstable I/O reads of missing keys was more than %d, got %d", max, cnt)
|
t.Errorf("num of sstable I/O reads of missing keys was more than %d, got %d", max, cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-12
@@ -45,7 +45,7 @@ func (db *DB) jWriter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) rotateMem(n int) (mem *memdb.DB, err error) {
|
func (db *DB) rotateMem(n int) (mem *memDB, err error) {
|
||||||
// Wait for pending memdb compaction.
|
// Wait for pending memdb compaction.
|
||||||
err = db.compSendIdle(db.mcompCmdC)
|
err = db.compSendIdle(db.mcompCmdC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,13 +63,19 @@ func (db *DB) rotateMem(n int) (mem *memdb.DB, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) flush(n int) (mem *memdb.DB, nn int, err error) {
|
func (db *DB) flush(n int) (mem *memDB, nn int, err error) {
|
||||||
delayed := false
|
delayed := false
|
||||||
flush := func() bool {
|
flush := func() (retry bool) {
|
||||||
v := db.s.version()
|
v := db.s.version()
|
||||||
defer v.release()
|
defer v.release()
|
||||||
mem = db.getEffectiveMem()
|
mem = db.getEffectiveMem()
|
||||||
nn = mem.Free()
|
defer func() {
|
||||||
|
if retry {
|
||||||
|
mem.decref()
|
||||||
|
mem = nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
nn = mem.mdb.Free()
|
||||||
switch {
|
switch {
|
||||||
case v.tLen(0) >= kL0_SlowdownWritesTrigger && !delayed:
|
case v.tLen(0) >= kL0_SlowdownWritesTrigger && !delayed:
|
||||||
delayed = true
|
delayed = true
|
||||||
@@ -84,12 +90,17 @@ func (db *DB) flush(n int) (mem *memdb.DB, nn int, err error) {
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Allow memdb to grow if it has no entry.
|
// Allow memdb to grow if it has no entry.
|
||||||
if mem.Len() == 0 {
|
if mem.mdb.Len() == 0 {
|
||||||
nn = n
|
nn = n
|
||||||
return false
|
} else {
|
||||||
|
mem.decref()
|
||||||
|
mem, err = db.rotateMem(n)
|
||||||
|
if err == nil {
|
||||||
|
nn = mem.mdb.Free()
|
||||||
|
} else {
|
||||||
|
nn = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mem, err = db.rotateMem(n)
|
|
||||||
nn = mem.Free()
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -140,6 +151,7 @@ retry:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer mem.decref()
|
||||||
|
|
||||||
// Calculate maximum size of the batch.
|
// Calculate maximum size of the batch.
|
||||||
m := 1 << 20
|
m := 1 << 20
|
||||||
@@ -178,7 +190,7 @@ drain:
|
|||||||
return
|
return
|
||||||
case db.journalC <- b:
|
case db.journalC <- b:
|
||||||
// Write into memdb
|
// Write into memdb
|
||||||
b.memReplay(mem)
|
b.memReplay(mem.mdb)
|
||||||
}
|
}
|
||||||
// Wait for journal writer
|
// Wait for journal writer
|
||||||
select {
|
select {
|
||||||
@@ -188,7 +200,7 @@ drain:
|
|||||||
case err = <-db.journalAckC:
|
case err = <-db.journalAckC:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Revert memdb if error detected
|
// Revert memdb if error detected
|
||||||
b.revertMemReplay(mem)
|
b.revertMemReplay(mem.mdb)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +209,7 @@ drain:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
b.memReplay(mem)
|
b.memReplay(mem.mdb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set last seq number.
|
// Set last seq number.
|
||||||
@@ -258,7 +270,8 @@ func (db *DB) CompactRange(r util.Range) error {
|
|||||||
|
|
||||||
// Check for overlaps in memdb.
|
// Check for overlaps in memdb.
|
||||||
mem := db.getEffectiveMem()
|
mem := db.getEffectiveMem()
|
||||||
if isMemOverlaps(db.s.icmp, mem, r.Start, r.Limit) {
|
defer mem.decref()
|
||||||
|
if isMemOverlaps(db.s.icmp, mem.mdb, r.Start, r.Limit) {
|
||||||
// Memdb compaction.
|
// Memdb compaction.
|
||||||
if _, err := db.rotateMem(0); err != nil {
|
if _, err := db.rotateMem(0); err != nil {
|
||||||
<-db.writeLockC
|
<-db.writeLockC
|
||||||
|
|||||||
+10
@@ -37,6 +37,16 @@
|
|||||||
// err = iter.Error()
|
// err = iter.Error()
|
||||||
// ...
|
// ...
|
||||||
//
|
//
|
||||||
|
// Iterate over subset of database content with a particular prefix:
|
||||||
|
// iter := db.NewIterator(util.BytesPrefix([]byte("foo-")), nil)
|
||||||
|
// for iter.Next() {
|
||||||
|
// // Use key/value.
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
// iter.Release()
|
||||||
|
// err = iter.Error()
|
||||||
|
// ...
|
||||||
|
//
|
||||||
// Seek-then-Iterate:
|
// Seek-then-Iterate:
|
||||||
//
|
//
|
||||||
// iter := db.NewIterator(nil, nil)
|
// iter := db.NewIterator(nil, nil)
|
||||||
|
|||||||
+4
-5
@@ -21,7 +21,7 @@ var _ = testutil.Defer(func() {
|
|||||||
BlockRestartInterval: 5,
|
BlockRestartInterval: 5,
|
||||||
BlockSize: 50,
|
BlockSize: 50,
|
||||||
Compression: opt.NoCompression,
|
Compression: opt.NoCompression,
|
||||||
MaxOpenFiles: 0,
|
CachedOpenFiles: -1,
|
||||||
Strict: opt.StrictAll,
|
Strict: opt.StrictAll,
|
||||||
WriteBuffer: 1000,
|
WriteBuffer: 1000,
|
||||||
}
|
}
|
||||||
@@ -40,18 +40,17 @@ var _ = testutil.Defer(func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Describe("read test", func() {
|
Describe("read test", func() {
|
||||||
testutil.AllKeyValueTesting(nil, func(kv testutil.KeyValue) testutil.DB {
|
testutil.AllKeyValueTesting(nil, nil, func(kv testutil.KeyValue) testutil.DB {
|
||||||
// Building the DB.
|
// Building the DB.
|
||||||
db := newTestingDB(o, nil, nil)
|
db := newTestingDB(o, nil, nil)
|
||||||
kv.IterateShuffled(nil, func(i int, key, value []byte) {
|
kv.IterateShuffled(nil, func(i int, key, value []byte) {
|
||||||
err := db.TestPut(key, value)
|
err := db.TestPut(key, value)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
})
|
})
|
||||||
testutil.Defer("teardown", func() {
|
|
||||||
db.TestClose()
|
|
||||||
})
|
|
||||||
|
|
||||||
return db
|
return db
|
||||||
|
}, func(db testutil.DB) {
|
||||||
|
db.(*testingDB).TestClose()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build go1.3
|
||||||
|
|
||||||
|
package leveldb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkDBReadConcurrent(b *testing.B) {
|
||||||
|
p := openDBBench(b, false)
|
||||||
|
p.populate(b.N)
|
||||||
|
p.fill()
|
||||||
|
p.gc()
|
||||||
|
defer p.close()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.SetBytes(116)
|
||||||
|
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
iter := p.newIter()
|
||||||
|
defer iter.Release()
|
||||||
|
for pb.Next() && iter.Next() {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDBReadConcurrent2(b *testing.B) {
|
||||||
|
p := openDBBench(b, false)
|
||||||
|
p.populate(b.N)
|
||||||
|
p.fill()
|
||||||
|
p.gc()
|
||||||
|
defer p.close()
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.SetBytes(116)
|
||||||
|
|
||||||
|
var dir uint32
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
iter := p.newIter()
|
||||||
|
defer iter.Release()
|
||||||
|
if atomic.AddUint32(&dir, 1)%2 == 0 {
|
||||||
|
for pb.Next() && iter.Next() {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if pb.Next() && iter.Last() {
|
||||||
|
for pb.Next() && iter.Prev() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
+64
-57
@@ -103,18 +103,18 @@ type flusher interface {
|
|||||||
Flush() error
|
Flush() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// DroppedError is the error type that passed to Dropper.Drop method.
|
// ErrCorrupted is the error type that generated by corrupted block or chunk.
|
||||||
type DroppedError struct {
|
type ErrCorrupted struct {
|
||||||
Size int
|
Size int
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e DroppedError) Error() string {
|
func (e ErrCorrupted) Error() string {
|
||||||
return fmt.Sprintf("leveldb/journal: dropped %d bytes: %s", e.Size, e.Reason)
|
return fmt.Sprintf("leveldb/journal: block/chunk corrupted: %s (%d bytes)", e.Reason, e.Size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dropper is the interface that wrap simple Drop method. The Drop
|
// 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 {
|
type Dropper interface {
|
||||||
Drop(err error)
|
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
|
// nextChunk sets r.buf[r.i:r.j] to hold the next chunk's payload, reading the
|
||||||
// next block into the buffer if necessary.
|
// next block into the buffer if necessary.
|
||||||
func (r *Reader) nextChunk(wantFirst, skip bool) error {
|
func (r *Reader) nextChunk(first bool) error {
|
||||||
for {
|
for {
|
||||||
if r.j+headerSize <= r.n {
|
if r.j+headerSize <= r.n {
|
||||||
checksum := binary.LittleEndian.Uint32(r.buf[r.j+0 : r.j+4])
|
checksum := binary.LittleEndian.Uint32(r.buf[r.j+0 : r.j+4])
|
||||||
length := binary.LittleEndian.Uint16(r.buf[r.j+4 : r.j+6])
|
length := binary.LittleEndian.Uint16(r.buf[r.j+4 : r.j+6])
|
||||||
chunkType := r.buf[r.j+6]
|
chunkType := r.buf[r.j+6]
|
||||||
|
|
||||||
var err error
|
|
||||||
if checksum == 0 && length == 0 && chunkType == 0 {
|
if checksum == 0 && length == 0 && chunkType == 0 {
|
||||||
// Drop entire block.
|
// Drop entire block.
|
||||||
err = DroppedError{r.n - r.j, "zero header"}
|
m := r.n - r.j
|
||||||
r.i = r.n
|
r.i = r.n
|
||||||
r.j = r.n
|
r.j = r.n
|
||||||
|
return r.corrupt(m, "zero header", false)
|
||||||
} else {
|
} else {
|
||||||
m := r.n - r.j
|
m := r.n - r.j
|
||||||
r.i = r.j + headerSize
|
r.i = r.j + headerSize
|
||||||
r.j = r.j + headerSize + int(length)
|
r.j = r.j + headerSize + int(length)
|
||||||
if r.j > r.n {
|
if r.j > r.n {
|
||||||
// Drop entire block.
|
// Drop entire block.
|
||||||
err = DroppedError{m, "chunk length overflows block"}
|
|
||||||
r.i = r.n
|
r.i = r.n
|
||||||
r.j = 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() {
|
} else if r.checksum && checksum != util.NewCRC(r.buf[r.i-1:r.j]).Value() {
|
||||||
// Drop entire block.
|
// Drop entire block.
|
||||||
err = DroppedError{m, "checksum mismatch"}
|
|
||||||
r.i = r.n
|
r.i = r.n
|
||||||
r.j = r.n
|
r.j = r.n
|
||||||
|
return r.corrupt(m, "checksum mismatch", false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if wantFirst && err == nil && chunkType != fullChunkType && chunkType != firstChunkType {
|
if first && chunkType != fullChunkType && chunkType != firstChunkType {
|
||||||
if skip {
|
m := r.j - r.i
|
||||||
// The chunk are intentionally skipped.
|
r.i = r.j
|
||||||
if chunkType == lastChunkType {
|
// Report the error, but skip it.
|
||||||
skip = false
|
return r.corrupt(m+headerSize, "orphan chunk", true)
|
||||||
}
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
// Drop the chunk.
|
|
||||||
err = DroppedError{r.j - r.i + headerSize, "orphan chunk"}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err == nil {
|
r.last = chunkType == fullChunkType || chunkType == lastChunkType
|
||||||
r.last = chunkType == fullChunkType || chunkType == lastChunkType
|
return nil
|
||||||
} else {
|
}
|
||||||
if r.dropper != nil {
|
|
||||||
r.dropper.Drop(err)
|
// The last block.
|
||||||
}
|
if r.n < blockSize && r.n > 0 {
|
||||||
if r.strict {
|
if !first {
|
||||||
r.err = err
|
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
|
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 n == 0 {
|
||||||
|
if !first {
|
||||||
|
return r.corrupt(0, "missing chunk part", false)
|
||||||
|
}
|
||||||
r.err = io.EOF
|
r.err = io.EOF
|
||||||
return r.err
|
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
|
// 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,
|
// 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) {
|
func (r *Reader) Next() (io.Reader, error) {
|
||||||
r.seq++
|
r.seq++
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
return nil, r.err
|
return nil, r.err
|
||||||
}
|
}
|
||||||
skip := !r.last
|
r.i = r.j
|
||||||
for {
|
for {
|
||||||
r.i = r.j
|
if err := r.nextChunk(true); err == nil {
|
||||||
if r.nextChunk(true, skip) != nil {
|
|
||||||
// So that 'orphan chunk' drop will be reported.
|
|
||||||
skip = false
|
|
||||||
} else {
|
|
||||||
break
|
break
|
||||||
}
|
} else if err != errSkip {
|
||||||
if r.err != nil {
|
return nil, err
|
||||||
return nil, r.err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &singleReader{r, r.seq, nil}, nil
|
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 {
|
func (r *Reader) Reset(reader io.Reader, dropper Dropper, strict, checksum bool) error {
|
||||||
r.seq++
|
r.seq++
|
||||||
err := r.err
|
err := r.err
|
||||||
@@ -296,7 +295,11 @@ func (x *singleReader) Read(p []byte) (int, error) {
|
|||||||
if r.last {
|
if r.last {
|
||||||
return 0, io.EOF
|
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
|
return 0, x.err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -320,7 +323,11 @@ func (x *singleReader) ReadByte() (byte, error) {
|
|||||||
if r.last {
|
if r.last {
|
||||||
return 0, io.EOF
|
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
|
return 0, x.err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
Vendored
+490
@@ -12,6 +12,7 @@ package journal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@@ -326,3 +327,492 @@ func TestStaleWriter(t *testing.T) {
|
|||||||
t.Fatalf("stale write #1: unexpected error: %v", err)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.Next(); err != io.EOF {
|
||||||
|
t.Fatalf("last next: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrupt_CorruptedFirstBlock(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 #0.
|
||||||
|
for i := 0; i < 1024; i++ {
|
||||||
|
b[i] = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
|
||||||
|
|
||||||
|
// First read (third record).
|
||||||
|
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-headerSize) + 1; n != want {
|
||||||
|
t.Fatalf("read #0: got %d bytes want %d", n, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second read (fourth record).
|
||||||
|
rr, err = r.Next()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
n, err = io.Copy(ioutil.Discard, rr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read #1: %v", err)
|
||||||
|
}
|
||||||
|
if want := int64(blockSize-headerSize) + 2; n != want {
|
||||||
|
t.Fatalf("read #1: got %d bytes want %d", n, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.Next(); err != io.EOF {
|
||||||
|
t.Fatalf("last next: 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 (first record).
|
||||||
|
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 (second record).
|
||||||
|
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 (fourth record).
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.Next(); err != io.EOF {
|
||||||
|
t.Fatalf("last next: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrupt_CorruptedLastBlock(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 #3.
|
||||||
|
for i := len(b) - 1; i > len(b)-1024; i-- {
|
||||||
|
b[i] = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
|
||||||
|
|
||||||
|
// First read (first record).
|
||||||
|
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 (second record).
|
||||||
|
rr, err = r.Next()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
n, err = io.Copy(ioutil.Discard, rr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read #1: %v", err)
|
||||||
|
}
|
||||||
|
if want := int64(blockSize - headerSize); n != want {
|
||||||
|
t.Fatalf("read #1: got %d bytes want %d", n, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third read (third record).
|
||||||
|
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) + 1; n != want {
|
||||||
|
t.Fatalf("read #2: got %d bytes want %d", n, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fourth read (fourth record).
|
||||||
|
rr, err = r.Next()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
n, err = io.Copy(ioutil.Discard, rr)
|
||||||
|
if err != io.ErrUnexpectedEOF {
|
||||||
|
t.Fatalf("read #3: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.Next(); err != io.EOF {
|
||||||
|
t.Fatalf("last next: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrupt_FirstChuckLengthOverflow(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := buf.Bytes()
|
||||||
|
// Corrupting record #1.
|
||||||
|
x := blockSize
|
||||||
|
binary.LittleEndian.PutUint16(b[x+4:], 0xffff)
|
||||||
|
|
||||||
|
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
|
||||||
|
|
||||||
|
// First read (first record).
|
||||||
|
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 (second record).
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.Next(); err != io.EOF {
|
||||||
|
t.Fatalf("last next: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrupt_MiddleChuckLengthOverflow(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := buf.Bytes()
|
||||||
|
// Corrupting record #1.
|
||||||
|
x := blockSize/2 + headerSize
|
||||||
|
binary.LittleEndian.PutUint16(b[x+4:], 0xffff)
|
||||||
|
|
||||||
|
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
|
||||||
|
|
||||||
|
// First read (first record).
|
||||||
|
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 (third record).
|
||||||
|
rr, err = r.Next()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
n, err = io.Copy(ioutil.Discard, rr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read #1: %v", err)
|
||||||
|
}
|
||||||
|
if want := int64(blockSize-headerSize) + 1; n != want {
|
||||||
|
t.Fatalf("read #1: got %d bytes want %d", n, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := r.Next(); err != io.EOF {
|
||||||
|
t.Fatalf("last next: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -129,7 +129,7 @@ var _ = testutil.Defer(func() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return db
|
return db
|
||||||
})
|
}, nil, nil)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+27
-19
@@ -24,16 +24,22 @@ const (
|
|||||||
DefaultBlockRestartInterval = 16
|
DefaultBlockRestartInterval = 16
|
||||||
DefaultBlockSize = 4 * KiB
|
DefaultBlockSize = 4 * KiB
|
||||||
DefaultCompressionType = SnappyCompression
|
DefaultCompressionType = SnappyCompression
|
||||||
DefaultMaxOpenFiles = 1000
|
DefaultCachedOpenFiles = 500
|
||||||
DefaultWriteBuffer = 4 * MiB
|
DefaultWriteBuffer = 4 * MiB
|
||||||
)
|
)
|
||||||
|
|
||||||
type noCache struct{}
|
type noCache struct{}
|
||||||
|
|
||||||
func (noCache) SetCapacity(capacity int) {}
|
func (noCache) SetCapacity(capacity int) {}
|
||||||
func (noCache) GetNamespace(id uint64) cache.Namespace { return nil }
|
func (noCache) Capacity() int { return 0 }
|
||||||
func (noCache) Purge(fin cache.PurgeFin) {}
|
func (noCache) Used() int { return 0 }
|
||||||
func (noCache) Zap(closed bool) {}
|
func (noCache) Size() int { return 0 }
|
||||||
|
func (noCache) NumObjects() int { return 0 }
|
||||||
|
func (noCache) GetNamespace(id uint64) cache.Namespace { return nil }
|
||||||
|
func (noCache) PurgeNamespace(id uint64, fin cache.PurgeFin) {}
|
||||||
|
func (noCache) ZapNamespace(id uint64) {}
|
||||||
|
func (noCache) Purge(fin cache.PurgeFin) {}
|
||||||
|
func (noCache) Zap() {}
|
||||||
|
|
||||||
var NoCache cache.Cache = noCache{}
|
var NoCache cache.Cache = noCache{}
|
||||||
|
|
||||||
@@ -119,6 +125,13 @@ type Options struct {
|
|||||||
// The default value is 4KiB.
|
// The default value is 4KiB.
|
||||||
BlockSize int
|
BlockSize int
|
||||||
|
|
||||||
|
// CachedOpenFiles defines number of open files to kept around when not
|
||||||
|
// in-use, the counting includes still in-use files.
|
||||||
|
// Set this to negative value to disable caching.
|
||||||
|
//
|
||||||
|
// The default value is 500.
|
||||||
|
CachedOpenFiles int
|
||||||
|
|
||||||
// Comparer defines a total ordering over the space of []byte keys: a 'less
|
// Comparer defines a total ordering over the space of []byte keys: a 'less
|
||||||
// than' relationship. The same comparison algorithm must be used for reads
|
// than' relationship. The same comparison algorithm must be used for reads
|
||||||
// and writes over the lifetime of the DB.
|
// and writes over the lifetime of the DB.
|
||||||
@@ -159,13 +172,6 @@ type Options struct {
|
|||||||
// The default value is nil.
|
// The default value is nil.
|
||||||
Filter filter.Filter
|
Filter filter.Filter
|
||||||
|
|
||||||
// MaxOpenFiles defines maximum number of open files to kept around
|
|
||||||
// (cached). This is not an hard limit, actual open files may exceed
|
|
||||||
// the defined value.
|
|
||||||
//
|
|
||||||
// The default value is 1000.
|
|
||||||
MaxOpenFiles int
|
|
||||||
|
|
||||||
// Strict defines the DB strict level.
|
// Strict defines the DB strict level.
|
||||||
Strict Strict
|
Strict Strict
|
||||||
|
|
||||||
@@ -207,6 +213,15 @@ func (o *Options) GetBlockSize() int {
|
|||||||
return o.BlockSize
|
return o.BlockSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *Options) GetCachedOpenFiles() int {
|
||||||
|
if o == nil || o.CachedOpenFiles == 0 {
|
||||||
|
return DefaultCachedOpenFiles
|
||||||
|
} else if o.CachedOpenFiles < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return o.CachedOpenFiles
|
||||||
|
}
|
||||||
|
|
||||||
func (o *Options) GetComparer() comparer.Comparer {
|
func (o *Options) GetComparer() comparer.Comparer {
|
||||||
if o == nil || o.Comparer == nil {
|
if o == nil || o.Comparer == nil {
|
||||||
return comparer.DefaultComparer
|
return comparer.DefaultComparer
|
||||||
@@ -242,13 +257,6 @@ func (o *Options) GetFilter() filter.Filter {
|
|||||||
return o.Filter
|
return o.Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Options) GetMaxOpenFiles() int {
|
|
||||||
if o == nil || o.MaxOpenFiles <= 0 {
|
|
||||||
return DefaultMaxOpenFiles
|
|
||||||
}
|
|
||||||
return o.MaxOpenFiles
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Options) GetStrict(strict Strict) bool {
|
func (o *Options) GetStrict(strict Strict) bool {
|
||||||
if o == nil || o.Strict == 0 {
|
if o == nil || o.Strict == 0 {
|
||||||
return DefaultStrict&strict != 0
|
return DefaultStrict&strict != 0
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@ func newSession(stor storage.Storage, o *opt.Options) (s *session, err error) {
|
|||||||
storLock: storLock,
|
storLock: storLock,
|
||||||
}
|
}
|
||||||
s.setOptions(o)
|
s.setOptions(o)
|
||||||
s.tops = newTableOps(s, s.o.GetMaxOpenFiles())
|
s.tops = newTableOps(s, s.o.GetCachedOpenFiles())
|
||||||
s.setVersion(&version{s: s})
|
s.setVersion(&version{s: s})
|
||||||
s.log("log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock D·DeletedEntry L·Level Q·SeqNum T·TimeElapsed")
|
s.log("log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock D·DeletedEntry L·Level Q·SeqNum T·TimeElapsed")
|
||||||
return
|
return
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ type dropper struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d dropper) Drop(err error) {
|
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)
|
d.s.logf("journal@drop %s-%d S·%s %q", d.file.Type(), d.file.Num(), shortenb(e.Size), e.Reason)
|
||||||
} else {
|
} else {
|
||||||
d.s.logf("journal@drop %s-%d %q", d.file.Type(), d.file.Num(), err)
|
d.s.logf("journal@drop %s-%d %q", d.file.Type(), d.file.Num(), err)
|
||||||
|
|||||||
+39
-41
@@ -275,6 +275,7 @@ type tOps struct {
|
|||||||
s *session
|
s *session
|
||||||
cache cache.Cache
|
cache cache.Cache
|
||||||
cacheNS cache.Namespace
|
cacheNS cache.Namespace
|
||||||
|
bpool *util.BufferPool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates an empty table and returns table writer.
|
// Creates an empty table and returns table writer.
|
||||||
@@ -296,7 +297,7 @@ func (t *tOps) create() (*tWriter, error) {
|
|||||||
func (t *tOps) createFrom(src iterator.Iterator) (f *tFile, n int, err error) {
|
func (t *tOps) createFrom(src iterator.Iterator) (f *tFile, n int, err error) {
|
||||||
w, err := t.create()
|
w, err := t.create()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return f, n, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -321,33 +322,24 @@ func (t *tOps) createFrom(src iterator.Iterator) (f *tFile, n int, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opens table. It returns a cache object, which should
|
// Opens table. It returns a cache handle, which should
|
||||||
// be released after use.
|
// be released after use.
|
||||||
func (t *tOps) open(f *tFile) (c cache.Object, err error) {
|
func (t *tOps) open(f *tFile) (ch cache.Handle, err error) {
|
||||||
num := f.file.Num()
|
num := f.file.Num()
|
||||||
c, ok := t.cacheNS.Get(num, func() (ok bool, value interface{}, charge int, fin cache.SetFin) {
|
ch = t.cacheNS.Get(num, func() (charge int, value interface{}) {
|
||||||
var r storage.Reader
|
var r storage.Reader
|
||||||
r, err = f.file.Open()
|
r, err = f.file.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
o := t.s.o
|
var bcacheNS cache.Namespace
|
||||||
|
if bc := t.s.o.GetBlockCache(); bc != nil {
|
||||||
var cacheNS cache.Namespace
|
bcacheNS = bc.GetNamespace(num)
|
||||||
if bc := o.GetBlockCache(); bc != nil {
|
|
||||||
cacheNS = bc.GetNamespace(num)
|
|
||||||
}
|
}
|
||||||
|
return 1, table.NewReader(r, int64(f.size), bcacheNS, t.bpool, t.s.o)
|
||||||
ok = true
|
|
||||||
value = table.NewReader(r, int64(f.size), cacheNS, o)
|
|
||||||
charge = 1
|
|
||||||
fin = func() {
|
|
||||||
r.Close()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
})
|
})
|
||||||
if !ok && err == nil {
|
if ch == nil && err == nil {
|
||||||
err = ErrClosed
|
err = ErrClosed
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -356,34 +348,33 @@ func (t *tOps) open(f *tFile) (c cache.Object, err error) {
|
|||||||
// Finds key/value pair whose key is greater than or equal to the
|
// Finds key/value pair whose key is greater than or equal to the
|
||||||
// given key.
|
// given key.
|
||||||
func (t *tOps) find(f *tFile, key []byte, ro *opt.ReadOptions) (rkey, rvalue []byte, err error) {
|
func (t *tOps) find(f *tFile, key []byte, ro *opt.ReadOptions) (rkey, rvalue []byte, err error) {
|
||||||
c, err := t.open(f)
|
ch, err := t.open(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
defer c.Release()
|
defer ch.Release()
|
||||||
return c.Value().(*table.Reader).Find(key, ro)
|
return ch.Value().(*table.Reader).Find(key, ro)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns approximate offset of the given key.
|
// Returns approximate offset of the given key.
|
||||||
func (t *tOps) offsetOf(f *tFile, key []byte) (offset uint64, err error) {
|
func (t *tOps) offsetOf(f *tFile, key []byte) (offset uint64, err error) {
|
||||||
c, err := t.open(f)
|
ch, err := t.open(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_offset, err := c.Value().(*table.Reader).OffsetOf(key)
|
defer ch.Release()
|
||||||
offset = uint64(_offset)
|
offset_, err := ch.Value().(*table.Reader).OffsetOf(key)
|
||||||
c.Release()
|
return uint64(offset_), err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates an iterator from the given table.
|
// Creates an iterator from the given table.
|
||||||
func (t *tOps) newIterator(f *tFile, slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
|
func (t *tOps) newIterator(f *tFile, slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
|
||||||
c, err := t.open(f)
|
ch, err := t.open(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return iterator.NewEmptyIterator(err)
|
return iterator.NewEmptyIterator(err)
|
||||||
}
|
}
|
||||||
iter := c.Value().(*table.Reader).NewIterator(slice, ro)
|
iter := ch.Value().(*table.Reader).NewIterator(slice, ro)
|
||||||
iter.SetReleaser(c)
|
iter.SetReleaser(ch)
|
||||||
return iter
|
return iter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,14 +382,16 @@ func (t *tOps) newIterator(f *tFile, slice *util.Range, ro *opt.ReadOptions) ite
|
|||||||
// no one use the the table.
|
// no one use the the table.
|
||||||
func (t *tOps) remove(f *tFile) {
|
func (t *tOps) remove(f *tFile) {
|
||||||
num := f.file.Num()
|
num := f.file.Num()
|
||||||
t.cacheNS.Delete(num, func(exist bool) {
|
t.cacheNS.Delete(num, func(exist, pending bool) {
|
||||||
if err := f.file.Remove(); err != nil {
|
if !pending {
|
||||||
t.s.logf("table@remove removing @%d %q", num, err)
|
if err := f.file.Remove(); err != nil {
|
||||||
} else {
|
t.s.logf("table@remove removing @%d %q", num, err)
|
||||||
t.s.logf("table@remove removed @%d", num)
|
} else {
|
||||||
}
|
t.s.logf("table@remove removed @%d", num)
|
||||||
if bc := t.s.o.GetBlockCache(); bc != nil {
|
}
|
||||||
bc.GetNamespace(num).Zap(false)
|
if bc := t.s.o.GetBlockCache(); bc != nil {
|
||||||
|
bc.ZapNamespace(num)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -406,14 +399,19 @@ func (t *tOps) remove(f *tFile) {
|
|||||||
// Closes the table ops instance. It will close all tables,
|
// Closes the table ops instance. It will close all tables,
|
||||||
// regadless still used or not.
|
// regadless still used or not.
|
||||||
func (t *tOps) close() {
|
func (t *tOps) close() {
|
||||||
t.cache.Zap(true)
|
t.cache.Zap()
|
||||||
|
t.bpool.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates new initialized table ops instance.
|
// Creates new initialized table ops instance.
|
||||||
func newTableOps(s *session, cacheCap int) *tOps {
|
func newTableOps(s *session, cacheCap int) *tOps {
|
||||||
c := cache.NewLRUCache(cacheCap)
|
c := cache.NewLRUCache(cacheCap)
|
||||||
ns := c.GetNamespace(0)
|
return &tOps{
|
||||||
return &tOps{s, c, ns}
|
s: s,
|
||||||
|
cache: c,
|
||||||
|
cacheNS: c.GetNamespace(0),
|
||||||
|
bpool: util.NewBufferPool(s.o.GetBlockSize() + 5),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tWriter wraps the table writer. It keep track of file descriptor
|
// tWriter wraps the table writer. It keep track of file descriptor
|
||||||
|
|||||||
+2
-2
@@ -40,7 +40,7 @@ var _ = testutil.Defer(func() {
|
|||||||
data := bw.buf.Bytes()
|
data := bw.buf.Bytes()
|
||||||
restartsLen := int(binary.LittleEndian.Uint32(data[len(data)-4:]))
|
restartsLen := int(binary.LittleEndian.Uint32(data[len(data)-4:]))
|
||||||
return &block{
|
return &block{
|
||||||
cmp: comparer.DefaultComparer,
|
tr: &Reader{cmp: comparer.DefaultComparer},
|
||||||
data: data,
|
data: data,
|
||||||
restartsLen: restartsLen,
|
restartsLen: restartsLen,
|
||||||
restartsOffset: len(data) - (restartsLen+1)*4,
|
restartsOffset: len(data) - (restartsLen+1)*4,
|
||||||
@@ -59,7 +59,7 @@ var _ = testutil.Defer(func() {
|
|||||||
// Make block.
|
// Make block.
|
||||||
br := Build(kv, restartInterval)
|
br := Build(kv, restartInterval)
|
||||||
// Do testing.
|
// Do testing.
|
||||||
testutil.KeyValueTesting(nil, br, kv.Clone())
|
testutil.KeyValueTesting(nil, kv.Clone(), br, nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
Describe(Text(), Test)
|
Describe(Text(), Test)
|
||||||
|
|||||||
+181
-97
@@ -37,7 +37,7 @@ func max(x, y int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type block struct {
|
type block struct {
|
||||||
cmp comparer.BasicComparer
|
tr *Reader
|
||||||
data []byte
|
data []byte
|
||||||
restartsLen int
|
restartsLen int
|
||||||
restartsOffset int
|
restartsOffset int
|
||||||
@@ -46,31 +46,25 @@ type block struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *block) seek(rstart, rlimit int, key []byte) (index, offset int, err error) {
|
func (b *block) seek(rstart, rlimit int, key []byte) (index, offset int, err error) {
|
||||||
n := b.restartsOffset
|
|
||||||
data := b.data
|
|
||||||
cmp := b.cmp
|
|
||||||
|
|
||||||
index = sort.Search(b.restartsLen-rstart-(b.restartsLen-rlimit), func(i int) bool {
|
index = sort.Search(b.restartsLen-rstart-(b.restartsLen-rlimit), func(i int) bool {
|
||||||
offset := int(binary.LittleEndian.Uint32(data[n+4*(rstart+i):]))
|
offset := int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*(rstart+i):]))
|
||||||
offset += 1 // shared always zero, since this is a restart point
|
offset += 1 // shared always zero, since this is a restart point
|
||||||
v1, n1 := binary.Uvarint(data[offset:]) // key length
|
v1, n1 := binary.Uvarint(b.data[offset:]) // key length
|
||||||
_, n2 := binary.Uvarint(data[offset+n1:]) // value length
|
_, n2 := binary.Uvarint(b.data[offset+n1:]) // value length
|
||||||
m := offset + n1 + n2
|
m := offset + n1 + n2
|
||||||
return cmp.Compare(data[m:m+int(v1)], key) > 0
|
return b.tr.cmp.Compare(b.data[m:m+int(v1)], key) > 0
|
||||||
}) + rstart - 1
|
}) + rstart - 1
|
||||||
if index < rstart {
|
if index < rstart {
|
||||||
// The smallest key is greater-than key sought.
|
// The smallest key is greater-than key sought.
|
||||||
index = rstart
|
index = rstart
|
||||||
}
|
}
|
||||||
offset = int(binary.LittleEndian.Uint32(data[n+4*index:]))
|
offset = int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*index:]))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *block) restartIndex(rstart, rlimit, offset int) int {
|
func (b *block) restartIndex(rstart, rlimit, offset int) int {
|
||||||
n := b.restartsOffset
|
|
||||||
data := b.data
|
|
||||||
return sort.Search(b.restartsLen-rstart-(b.restartsLen-rlimit), func(i int) bool {
|
return sort.Search(b.restartsLen-rstart-(b.restartsLen-rlimit), func(i int) bool {
|
||||||
return int(binary.LittleEndian.Uint32(data[n+4*(rstart+i):])) > offset
|
return int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*(rstart+i):])) > offset
|
||||||
}) + rstart - 1
|
}) + rstart - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +133,14 @@ func (b *block) newIterator(slice *util.Range, inclLimit bool, cache util.Releas
|
|||||||
return bi
|
return bi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *block) Release() {
|
||||||
|
if b.tr.bpool != nil {
|
||||||
|
b.tr.bpool.Put(b.data)
|
||||||
|
}
|
||||||
|
b.tr = nil
|
||||||
|
b.data = nil
|
||||||
|
}
|
||||||
|
|
||||||
type dir int
|
type dir int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -261,7 +263,7 @@ func (i *blockIter) Seek(key []byte) bool {
|
|||||||
i.dir = dirForward
|
i.dir = dirForward
|
||||||
}
|
}
|
||||||
for i.Next() {
|
for i.Next() {
|
||||||
if i.block.cmp.Compare(i.key, key) >= 0 {
|
if i.block.tr.cmp.Compare(i.key, key) >= 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,18 +439,21 @@ func (i *blockIter) Value() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *blockIter) Release() {
|
func (i *blockIter) Release() {
|
||||||
i.prevNode = nil
|
if i.dir > dirReleased {
|
||||||
i.prevKeys = nil
|
i.block = nil
|
||||||
i.key = nil
|
i.prevNode = nil
|
||||||
i.value = nil
|
i.prevKeys = nil
|
||||||
i.dir = dirReleased
|
i.key = nil
|
||||||
if i.cache != nil {
|
i.value = nil
|
||||||
i.cache.Release()
|
i.dir = dirReleased
|
||||||
i.cache = nil
|
if i.cache != nil {
|
||||||
}
|
i.cache.Release()
|
||||||
if i.releaser != nil {
|
i.cache = nil
|
||||||
i.releaser.Release()
|
}
|
||||||
i.releaser = nil
|
if i.releaser != nil {
|
||||||
|
i.releaser.Release()
|
||||||
|
i.releaser = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +472,7 @@ func (i *blockIter) Error() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type filterBlock struct {
|
type filterBlock struct {
|
||||||
filter filter.Filter
|
tr *Reader
|
||||||
data []byte
|
data []byte
|
||||||
oOffset int
|
oOffset int
|
||||||
baseLg uint
|
baseLg uint
|
||||||
@@ -481,7 +486,7 @@ func (b *filterBlock) contains(offset uint64, key []byte) bool {
|
|||||||
n := int(binary.LittleEndian.Uint32(o))
|
n := int(binary.LittleEndian.Uint32(o))
|
||||||
m := int(binary.LittleEndian.Uint32(o[4:]))
|
m := int(binary.LittleEndian.Uint32(o[4:]))
|
||||||
if n < m && m <= b.oOffset {
|
if n < m && m <= b.oOffset {
|
||||||
return b.filter.Contains(b.data[n:m], key)
|
return b.tr.filter.Contains(b.data[n:m], key)
|
||||||
} else if n == m {
|
} else if n == m {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -489,10 +494,17 @@ func (b *filterBlock) contains(offset uint64, key []byte) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *filterBlock) Release() {
|
||||||
|
if b.tr.bpool != nil {
|
||||||
|
b.tr.bpool.Put(b.data)
|
||||||
|
}
|
||||||
|
b.tr = nil
|
||||||
|
b.data = nil
|
||||||
|
}
|
||||||
|
|
||||||
type indexIter struct {
|
type indexIter struct {
|
||||||
blockIter
|
*blockIter
|
||||||
tableReader *Reader
|
slice *util.Range
|
||||||
slice *util.Range
|
|
||||||
// Options
|
// Options
|
||||||
checksum bool
|
checksum bool
|
||||||
fillCache bool
|
fillCache bool
|
||||||
@@ -511,7 +523,7 @@ func (i *indexIter) Get() iterator.Iterator {
|
|||||||
if i.slice != nil && (i.blockIter.isFirst() || i.blockIter.isLast()) {
|
if i.slice != nil && (i.blockIter.isFirst() || i.blockIter.isLast()) {
|
||||||
slice = i.slice
|
slice = i.slice
|
||||||
}
|
}
|
||||||
return i.tableReader.getDataIter(dataBH, slice, i.checksum, i.fillCache)
|
return i.blockIter.block.tr.getDataIter(dataBH, slice, i.checksum, i.fillCache)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reader is a table reader.
|
// Reader is a table reader.
|
||||||
@@ -519,15 +531,15 @@ type Reader struct {
|
|||||||
reader io.ReaderAt
|
reader io.ReaderAt
|
||||||
cache cache.Namespace
|
cache cache.Namespace
|
||||||
err error
|
err error
|
||||||
|
bpool *util.BufferPool
|
||||||
// Options
|
// Options
|
||||||
cmp comparer.Comparer
|
cmp comparer.Comparer
|
||||||
filter filter.Filter
|
filter filter.Filter
|
||||||
checksum bool
|
checksum bool
|
||||||
strictIter bool
|
strictIter bool
|
||||||
|
|
||||||
dataEnd int64
|
dataEnd int64
|
||||||
indexBlock *block
|
indexBH, filterBH blockHandle
|
||||||
filterBlock *filterBlock
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyChecksum(data []byte) bool {
|
func verifyChecksum(data []byte) bool {
|
||||||
@@ -538,12 +550,13 @@ func verifyChecksum(data []byte) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
|
func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
|
||||||
data := make([]byte, bh.length+blockTrailerLen)
|
data := r.bpool.Get(int(bh.length + blockTrailerLen))
|
||||||
if _, err := r.reader.ReadAt(data, int64(bh.offset)); err != nil && err != io.EOF {
|
if _, err := r.reader.ReadAt(data, int64(bh.offset)); err != nil && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if checksum || r.checksum {
|
if checksum || r.checksum {
|
||||||
if !verifyChecksum(data) {
|
if !verifyChecksum(data) {
|
||||||
|
r.bpool.Put(data)
|
||||||
return nil, errors.New("leveldb/table: Reader: invalid block (checksum mismatch)")
|
return nil, errors.New("leveldb/table: Reader: invalid block (checksum mismatch)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -551,12 +564,18 @@ func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
|
|||||||
case blockTypeNoCompression:
|
case blockTypeNoCompression:
|
||||||
data = data[:bh.length]
|
data = data[:bh.length]
|
||||||
case blockTypeSnappyCompression:
|
case blockTypeSnappyCompression:
|
||||||
var err error
|
decLen, err := snappy.DecodedLen(data[:bh.length])
|
||||||
data, err = snappy.Decode(nil, data[:bh.length])
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tmp := data
|
||||||
|
data, err = snappy.Decode(r.bpool.Get(decLen), tmp[:bh.length])
|
||||||
|
r.bpool.Put(tmp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
r.bpool.Put(data)
|
||||||
return nil, fmt.Errorf("leveldb/table: Reader: unknown block compression type: %d", data[bh.length])
|
return nil, fmt.Errorf("leveldb/table: Reader: unknown block compression type: %d", data[bh.length])
|
||||||
}
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
@@ -569,7 +588,7 @@ func (r *Reader) readBlock(bh blockHandle, checksum bool) (*block, error) {
|
|||||||
}
|
}
|
||||||
restartsLen := int(binary.LittleEndian.Uint32(data[len(data)-4:]))
|
restartsLen := int(binary.LittleEndian.Uint32(data[len(data)-4:]))
|
||||||
b := &block{
|
b := &block{
|
||||||
cmp: r.cmp,
|
tr: r,
|
||||||
data: data,
|
data: data,
|
||||||
restartsLen: restartsLen,
|
restartsLen: restartsLen,
|
||||||
restartsOffset: len(data) - (restartsLen+1)*4,
|
restartsOffset: len(data) - (restartsLen+1)*4,
|
||||||
@@ -578,7 +597,44 @@ func (r *Reader) readBlock(bh blockHandle, checksum bool) (*block, error) {
|
|||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterBlock, error) {
|
func (r *Reader) readBlockCached(bh blockHandle, checksum, fillCache bool) (*block, util.Releaser, error) {
|
||||||
|
if r.cache != nil {
|
||||||
|
var err error
|
||||||
|
ch := r.cache.Get(bh.offset, func() (charge int, value interface{}) {
|
||||||
|
if !fillCache {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
var b *block
|
||||||
|
b, err = r.readBlock(bh, checksum)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return cap(b.data), b
|
||||||
|
})
|
||||||
|
if ch != nil {
|
||||||
|
b, ok := ch.Value().(*block)
|
||||||
|
if !ok {
|
||||||
|
ch.Release()
|
||||||
|
return nil, nil, errors.New("leveldb/table: Reader: inconsistent block type")
|
||||||
|
}
|
||||||
|
if !b.checksum && (r.checksum || checksum) {
|
||||||
|
if !verifyChecksum(b.data) {
|
||||||
|
ch.Release()
|
||||||
|
return nil, nil, errors.New("leveldb/table: Reader: invalid block (checksum mismatch)")
|
||||||
|
}
|
||||||
|
b.checksum = true
|
||||||
|
}
|
||||||
|
return b, ch, err
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := r.readBlock(bh, checksum)
|
||||||
|
return b, b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) readFilterBlock(bh blockHandle) (*filterBlock, error) {
|
||||||
data, err := r.readRawBlock(bh, true)
|
data, err := r.readRawBlock(bh, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -593,7 +649,7 @@ func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterB
|
|||||||
return nil, errors.New("leveldb/table: Reader: invalid filter block (invalid offset)")
|
return nil, errors.New("leveldb/table: Reader: invalid filter block (invalid offset)")
|
||||||
}
|
}
|
||||||
b := &filterBlock{
|
b := &filterBlock{
|
||||||
filter: filter,
|
tr: r,
|
||||||
data: data,
|
data: data,
|
||||||
oOffset: oOffset,
|
oOffset: oOffset,
|
||||||
baseLg: uint(data[n-1]),
|
baseLg: uint(data[n-1]),
|
||||||
@@ -602,44 +658,42 @@ func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterB
|
|||||||
return b, nil
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fillCache bool) iterator.Iterator {
|
func (r *Reader) readFilterBlockCached(bh blockHandle, fillCache bool) (*filterBlock, util.Releaser, error) {
|
||||||
if r.cache != nil {
|
if r.cache != nil {
|
||||||
// Get/set block cache.
|
|
||||||
var err error
|
var err error
|
||||||
cache, ok := r.cache.Get(dataBH.offset, func() (ok bool, value interface{}, charge int, fin cache.SetFin) {
|
ch := r.cache.Get(bh.offset, func() (charge int, value interface{}) {
|
||||||
if !fillCache {
|
if !fillCache {
|
||||||
return
|
return 0, nil
|
||||||
}
|
}
|
||||||
var dataBlock *block
|
var b *filterBlock
|
||||||
dataBlock, err = r.readBlock(dataBH, checksum)
|
b, err = r.readFilterBlock(bh)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
ok = true
|
return 0, nil
|
||||||
value = dataBlock
|
|
||||||
charge = int(dataBH.length)
|
|
||||||
}
|
}
|
||||||
return
|
return cap(b.data), b
|
||||||
})
|
})
|
||||||
if err != nil {
|
if ch != nil {
|
||||||
return iterator.NewEmptyIterator(err)
|
b, ok := ch.Value().(*filterBlock)
|
||||||
}
|
if !ok {
|
||||||
if ok {
|
ch.Release()
|
||||||
dataBlock := cache.Value().(*block)
|
return nil, nil, errors.New("leveldb/table: Reader: inconsistent block type")
|
||||||
if !dataBlock.checksum && (r.checksum || checksum) {
|
|
||||||
if !verifyChecksum(dataBlock.data) {
|
|
||||||
return iterator.NewEmptyIterator(errors.New("leveldb/table: Reader: invalid block (checksum mismatch)"))
|
|
||||||
}
|
|
||||||
dataBlock.checksum = true
|
|
||||||
}
|
}
|
||||||
iter := dataBlock.newIterator(slice, false, cache)
|
return b, ch, err
|
||||||
return iter
|
} else if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataBlock, err := r.readBlock(dataBH, checksum)
|
|
||||||
|
b, err := r.readFilterBlock(bh)
|
||||||
|
return b, b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fillCache bool) iterator.Iterator {
|
||||||
|
b, rel, err := r.readBlockCached(dataBH, checksum, fillCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return iterator.NewEmptyIterator(err)
|
return iterator.NewEmptyIterator(err)
|
||||||
}
|
}
|
||||||
iter := dataBlock.newIterator(slice, false, nil)
|
return b.newIterator(slice, false, rel)
|
||||||
return iter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIterator creates an iterator from the table.
|
// NewIterator creates an iterator from the table.
|
||||||
@@ -653,18 +707,21 @@ func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fi
|
|||||||
// when not used.
|
// when not used.
|
||||||
//
|
//
|
||||||
// Also read Iterator documentation of the leveldb/iterator package.
|
// Also read Iterator documentation of the leveldb/iterator package.
|
||||||
|
|
||||||
func (r *Reader) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
|
func (r *Reader) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
|
||||||
if r.err != nil {
|
if r.err != nil {
|
||||||
return iterator.NewEmptyIterator(r.err)
|
return iterator.NewEmptyIterator(r.err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fillCache := !ro.GetDontFillCache()
|
||||||
|
b, rel, err := r.readBlockCached(r.indexBH, true, fillCache)
|
||||||
|
if err != nil {
|
||||||
|
return iterator.NewEmptyIterator(err)
|
||||||
|
}
|
||||||
index := &indexIter{
|
index := &indexIter{
|
||||||
blockIter: *r.indexBlock.newIterator(slice, true, nil),
|
blockIter: b.newIterator(slice, true, rel),
|
||||||
tableReader: r,
|
slice: slice,
|
||||||
slice: slice,
|
checksum: ro.GetStrict(opt.StrictBlockChecksum),
|
||||||
checksum: ro.GetStrict(opt.StrictBlockChecksum),
|
fillCache: !ro.GetDontFillCache(),
|
||||||
fillCache: !ro.GetDontFillCache(),
|
|
||||||
}
|
}
|
||||||
return iterator.NewIndexedIterator(index, r.strictIter || ro.GetStrict(opt.StrictIterator), false)
|
return iterator.NewIndexedIterator(index, r.strictIter || ro.GetStrict(opt.StrictIterator), false)
|
||||||
}
|
}
|
||||||
@@ -681,7 +738,13 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
index := r.indexBlock.newIterator(nil, true, nil)
|
indexBlock, rel, err := r.readBlockCached(r.indexBH, true, true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rel.Release()
|
||||||
|
|
||||||
|
index := indexBlock.newIterator(nil, true, nil)
|
||||||
defer index.Release()
|
defer index.Release()
|
||||||
if !index.Seek(key) {
|
if !index.Seek(key) {
|
||||||
err = index.Error()
|
err = index.Error()
|
||||||
@@ -695,9 +758,15 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
|
|||||||
err = errors.New("leveldb/table: Reader: invalid table (bad data block handle)")
|
err = errors.New("leveldb/table: Reader: invalid table (bad data block handle)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.filterBlock != nil && !r.filterBlock.contains(dataBH.offset, key) {
|
if r.filter != nil {
|
||||||
err = ErrNotFound
|
filterBlock, rel, ferr := r.readFilterBlockCached(r.filterBH, true)
|
||||||
return
|
if ferr == nil {
|
||||||
|
if !filterBlock.contains(dataBH.offset, key) {
|
||||||
|
rel.Release()
|
||||||
|
return nil, nil, ErrNotFound
|
||||||
|
}
|
||||||
|
rel.Release()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data := r.getDataIter(dataBH, nil, ro.GetStrict(opt.StrictBlockChecksum), !ro.GetDontFillCache())
|
data := r.getDataIter(dataBH, nil, ro.GetStrict(opt.StrictBlockChecksum), !ro.GetDontFillCache())
|
||||||
defer data.Release()
|
defer data.Release()
|
||||||
@@ -708,8 +777,11 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Don't use block buffer, no need to copy the buffer.
|
||||||
rkey = data.Key()
|
rkey = data.Key()
|
||||||
value = data.Value()
|
// Use block buffer, and since the buffer will be recycled, the buffer
|
||||||
|
// need to be copied.
|
||||||
|
value = append([]byte{}, data.Value()...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,7 +813,13 @@ func (r *Reader) OffsetOf(key []byte) (offset int64, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
index := r.indexBlock.newIterator(nil, true, nil)
|
indexBlock, rel, err := r.readBlockCached(r.indexBH, true, true)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rel.Release()
|
||||||
|
|
||||||
|
index := indexBlock.newIterator(nil, true, nil)
|
||||||
defer index.Release()
|
defer index.Release()
|
||||||
if index.Seek(key) {
|
if index.Seek(key) {
|
||||||
dataBH, n := decodeBlockHandle(index.Value())
|
dataBH, n := decodeBlockHandle(index.Value())
|
||||||
@@ -759,14 +837,29 @@ func (r *Reader) OffsetOf(key []byte) (offset int64, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release implements util.Releaser.
|
||||||
|
// It also close the file if it is an io.Closer.
|
||||||
|
func (r *Reader) Release() {
|
||||||
|
if closer, ok := r.reader.(io.Closer); ok {
|
||||||
|
closer.Close()
|
||||||
|
}
|
||||||
|
r.reader = nil
|
||||||
|
r.cache = nil
|
||||||
|
r.bpool = nil
|
||||||
|
}
|
||||||
|
|
||||||
// NewReader creates a new initialized table reader for the file.
|
// NewReader creates a new initialized table reader for the file.
|
||||||
// The cache is optional and can be nil.
|
// The cache and bpool is optional and can be nil.
|
||||||
//
|
//
|
||||||
// The returned table reader instance is goroutine-safe.
|
// The returned table reader instance is goroutine-safe.
|
||||||
func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, o *opt.Options) *Reader {
|
func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, bpool *util.BufferPool, o *opt.Options) *Reader {
|
||||||
|
if bpool == nil {
|
||||||
|
bpool = util.NewBufferPool(o.GetBlockSize() + blockTrailerLen)
|
||||||
|
}
|
||||||
r := &Reader{
|
r := &Reader{
|
||||||
reader: f,
|
reader: f,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
|
bpool: bpool,
|
||||||
cmp: o.GetComparer(),
|
cmp: o.GetComparer(),
|
||||||
checksum: o.GetStrict(opt.StrictBlockChecksum),
|
checksum: o.GetStrict(opt.StrictBlockChecksum),
|
||||||
strictIter: o.GetStrict(opt.StrictIterator),
|
strictIter: o.GetStrict(opt.StrictIterator),
|
||||||
@@ -794,16 +887,11 @@ func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, o *opt.Options)
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
// Decode the index block handle.
|
// Decode the index block handle.
|
||||||
indexBH, n := decodeBlockHandle(footer[n:])
|
r.indexBH, n = decodeBlockHandle(footer[n:])
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
r.err = errors.New("leveldb/table: Reader: invalid table (bad index block handle)")
|
r.err = errors.New("leveldb/table: Reader: invalid table (bad index block handle)")
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
// Read index block.
|
|
||||||
r.indexBlock, r.err = r.readBlock(indexBH, true)
|
|
||||||
if r.err != nil {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
// Read metaindex block.
|
// Read metaindex block.
|
||||||
metaBlock, err := r.readBlock(metaBH, true)
|
metaBlock, err := r.readBlock(metaBH, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -819,32 +907,28 @@ func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, o *opt.Options)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fn := key[7:]
|
fn := key[7:]
|
||||||
var filter filter.Filter
|
|
||||||
if f0 := o.GetFilter(); f0 != nil && f0.Name() == fn {
|
if f0 := o.GetFilter(); f0 != nil && f0.Name() == fn {
|
||||||
filter = f0
|
r.filter = f0
|
||||||
} else {
|
} else {
|
||||||
for _, f0 := range o.GetAltFilters() {
|
for _, f0 := range o.GetAltFilters() {
|
||||||
if f0.Name() == fn {
|
if f0.Name() == fn {
|
||||||
filter = f0
|
r.filter = f0
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if filter != nil {
|
if r.filter != nil {
|
||||||
filterBH, n := decodeBlockHandle(metaIter.Value())
|
filterBH, n := decodeBlockHandle(metaIter.Value())
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
r.filterBH = filterBH
|
||||||
// Update data end.
|
// Update data end.
|
||||||
r.dataEnd = int64(filterBH.offset)
|
r.dataEnd = int64(filterBH.offset)
|
||||||
filterBlock, err := r.readFilterBlock(filterBH, filter)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
r.filterBlock = filterBlock
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
metaIter.Release()
|
metaIter.Release()
|
||||||
|
metaBlock.Release()
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-5
@@ -59,7 +59,7 @@ var _ = testutil.Defer(func() {
|
|||||||
It("Should be able to approximate offset of a key correctly", func() {
|
It("Should be able to approximate offset of a key correctly", func() {
|
||||||
Expect(err).ShouldNot(HaveOccurred())
|
Expect(err).ShouldNot(HaveOccurred())
|
||||||
|
|
||||||
tr := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()), nil, o)
|
tr := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()), nil, nil, o)
|
||||||
CheckOffset := func(key string, expect, threshold int) {
|
CheckOffset := func(key string, expect, threshold int) {
|
||||||
offset, err := tr.OffsetOf([]byte(key))
|
offset, err := tr.OffsetOf([]byte(key))
|
||||||
Expect(err).ShouldNot(HaveOccurred())
|
Expect(err).ShouldNot(HaveOccurred())
|
||||||
@@ -95,7 +95,7 @@ var _ = testutil.Defer(func() {
|
|||||||
tw.Close()
|
tw.Close()
|
||||||
|
|
||||||
// Opening the table.
|
// Opening the table.
|
||||||
tr := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()), nil, o)
|
tr := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()), nil, nil, o)
|
||||||
return tableWrapper{tr}
|
return tableWrapper{tr}
|
||||||
}
|
}
|
||||||
Test := func(kv *testutil.KeyValue, body func(r *Reader)) func() {
|
Test := func(kv *testutil.KeyValue, body func(r *Reader)) func() {
|
||||||
@@ -104,14 +104,16 @@ var _ = testutil.Defer(func() {
|
|||||||
if body != nil {
|
if body != nil {
|
||||||
body(db.(tableWrapper).Reader)
|
body(db.(tableWrapper).Reader)
|
||||||
}
|
}
|
||||||
testutil.KeyValueTesting(nil, db, *kv)
|
testutil.KeyValueTesting(nil, *kv, db, nil, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testutil.AllKeyValueTesting(nil, Build)
|
testutil.AllKeyValueTesting(nil, Build, nil, nil)
|
||||||
Describe("with one key per block", Test(testutil.KeyValue_Generate(nil, 9, 1, 10, 512, 512), func(r *Reader) {
|
Describe("with one key per block", Test(testutil.KeyValue_Generate(nil, 9, 1, 10, 512, 512), func(r *Reader) {
|
||||||
It("should have correct blocks number", func() {
|
It("should have correct blocks number", func() {
|
||||||
Expect(r.indexBlock.restartsLen).Should(Equal(9))
|
indexBlock, err := r.readBlock(r.indexBH, true)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(indexBlock.restartsLen).Should(Equal(9))
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|||||||
+57
-43
@@ -16,13 +16,22 @@ import (
|
|||||||
"github.com/syndtr/goleveldb/leveldb/util"
|
"github.com/syndtr/goleveldb/leveldb/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
|
func KeyValueTesting(rnd *rand.Rand, kv KeyValue, p DB, setup func(KeyValue) DB, teardown func(DB)) {
|
||||||
if rnd == nil {
|
if rnd == nil {
|
||||||
rnd = NewRand()
|
rnd = NewRand()
|
||||||
}
|
}
|
||||||
|
|
||||||
if db, ok := p.(Find); ok {
|
if p == nil {
|
||||||
It("Should find all keys with Find", func() {
|
BeforeEach(func() {
|
||||||
|
p = setup(kv)
|
||||||
|
})
|
||||||
|
AfterEach(func() {
|
||||||
|
teardown(p)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
It("Should find all keys with Find", func() {
|
||||||
|
if db, ok := p.(Find); ok {
|
||||||
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
|
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
|
||||||
key_, key, value := kv.IndexInexact(i)
|
key_, key, value := kv.IndexInexact(i)
|
||||||
|
|
||||||
@@ -38,9 +47,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
|
|||||||
Expect(rkey).Should(Equal(key))
|
Expect(rkey).Should(Equal(key))
|
||||||
Expect(rvalue).Should(Equal(value), "Value for key %q (%q)", key_, key)
|
Expect(rvalue).Should(Equal(value), "Value for key %q (%q)", key_, key)
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
|
||||||
It("Should return error if the key is not present", func() {
|
It("Should return error if the key is not present", func() {
|
||||||
|
if db, ok := p.(Find); ok {
|
||||||
var key []byte
|
var key []byte
|
||||||
if kv.Len() > 0 {
|
if kv.Len() > 0 {
|
||||||
key_, _ := kv.Index(kv.Len() - 1)
|
key_, _ := kv.Index(kv.Len() - 1)
|
||||||
@@ -49,11 +60,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
|
|||||||
rkey, _, err := db.TestFind(key)
|
rkey, _, err := db.TestFind(key)
|
||||||
Expect(err).Should(HaveOccurred(), "Find for key %q yield key %q", key, rkey)
|
Expect(err).Should(HaveOccurred(), "Find for key %q yield key %q", key, rkey)
|
||||||
Expect(err).Should(Equal(util.ErrNotFound))
|
Expect(err).Should(Equal(util.ErrNotFound))
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
if db, ok := p.(Get); ok {
|
It("Should only find exact key with Get", func() {
|
||||||
It("Should only find exact key with Get", func() {
|
if db, ok := p.(Get); ok {
|
||||||
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
|
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
|
||||||
key_, key, value := kv.IndexInexact(i)
|
key_, key, value := kv.IndexInexact(i)
|
||||||
|
|
||||||
@@ -69,11 +80,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
|
|||||||
Expect(err).Should(Equal(util.ErrNotFound))
|
Expect(err).Should(Equal(util.ErrNotFound))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
if db, ok := p.(NewIterator); ok {
|
TestIter := func(r *util.Range, _kv KeyValue) {
|
||||||
TestIter := func(r *util.Range, _kv KeyValue) {
|
if db, ok := p.(NewIterator); ok {
|
||||||
iter := db.TestNewIterator(r)
|
iter := db.TestNewIterator(r)
|
||||||
Expect(iter.Error()).ShouldNot(HaveOccurred())
|
Expect(iter.Error()).ShouldNot(HaveOccurred())
|
||||||
|
|
||||||
@@ -84,45 +95,48 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
|
|||||||
|
|
||||||
DoIteratorTesting(&t)
|
DoIteratorTesting(&t)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
It("Should iterates and seeks correctly", func(done Done) {
|
It("Should iterates and seeks correctly", func(done Done) {
|
||||||
TestIter(nil, kv.Clone())
|
TestIter(nil, kv.Clone())
|
||||||
done <- true
|
done <- true
|
||||||
}, 3.0)
|
}, 3.0)
|
||||||
|
|
||||||
RandomIndex(rnd, kv.Len(), kv.Len(), func(i int) {
|
RandomIndex(rnd, kv.Len(), kv.Len(), func(i int) {
|
||||||
type slice struct {
|
type slice struct {
|
||||||
r *util.Range
|
r *util.Range
|
||||||
start, limit int
|
start, limit int
|
||||||
}
|
}
|
||||||
|
|
||||||
key_, _, _ := kv.IndexInexact(i)
|
key_, _, _ := kv.IndexInexact(i)
|
||||||
for _, x := range []slice{
|
for _, x := range []slice{
|
||||||
{&util.Range{Start: key_, Limit: nil}, i, kv.Len()},
|
{&util.Range{Start: key_, Limit: nil}, i, kv.Len()},
|
||||||
{&util.Range{Start: nil, Limit: key_}, 0, i},
|
{&util.Range{Start: nil, Limit: key_}, 0, i},
|
||||||
} {
|
} {
|
||||||
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", x.start, x.limit), func(done Done) {
|
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", x.start, x.limit), func(done Done) {
|
||||||
TestIter(x.r, kv.Slice(x.start, x.limit))
|
TestIter(x.r, kv.Slice(x.start, x.limit))
|
||||||
done <- true
|
|
||||||
}, 3.0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
RandomRange(rnd, kv.Len(), kv.Len(), func(start, limit int) {
|
|
||||||
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", start, limit), func(done Done) {
|
|
||||||
r := kv.Range(start, limit)
|
|
||||||
TestIter(&r, kv.Slice(start, limit))
|
|
||||||
done <- true
|
done <- true
|
||||||
}, 3.0)
|
}, 3.0)
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
|
RandomRange(rnd, kv.Len(), kv.Len(), func(start, limit int) {
|
||||||
|
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", start, limit), func(done Done) {
|
||||||
|
r := kv.Range(start, limit)
|
||||||
|
TestIter(&r, kv.Slice(start, limit))
|
||||||
|
done <- true
|
||||||
|
}, 3.0)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func AllKeyValueTesting(rnd *rand.Rand, body func(kv KeyValue) DB) {
|
func AllKeyValueTesting(rnd *rand.Rand, body, setup func(KeyValue) DB, teardown func(DB)) {
|
||||||
Test := func(kv *KeyValue) func() {
|
Test := func(kv *KeyValue) func() {
|
||||||
return func() {
|
return func() {
|
||||||
db := body(*kv)
|
var p DB
|
||||||
KeyValueTesting(rnd, db, *kv)
|
if body != nil {
|
||||||
|
p = body(*kv)
|
||||||
|
}
|
||||||
|
KeyValueTesting(rnd, *kv, p, setup, teardown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+205
@@ -0,0 +1,205 @@
|
|||||||
|
// Copyright (c) 2014, Suryandaru Triandana <syndtr@gmail.com>
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type buffer struct {
|
||||||
|
b []byte
|
||||||
|
miss int
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferPool is a 'buffer pool'.
|
||||||
|
type BufferPool struct {
|
||||||
|
pool [6]chan []byte
|
||||||
|
size [5]uint32
|
||||||
|
sizeMiss [5]uint32
|
||||||
|
sizeHalf [5]uint32
|
||||||
|
baseline [4]int
|
||||||
|
baselinex0 int
|
||||||
|
baselinex1 int
|
||||||
|
baseline0 int
|
||||||
|
baseline1 int
|
||||||
|
baseline2 int
|
||||||
|
close chan struct{}
|
||||||
|
|
||||||
|
get uint32
|
||||||
|
put uint32
|
||||||
|
half uint32
|
||||||
|
less uint32
|
||||||
|
equal uint32
|
||||||
|
greater uint32
|
||||||
|
miss uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BufferPool) poolNum(n int) int {
|
||||||
|
if n <= p.baseline0 && n > p.baseline0/2 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for i, x := range p.baseline {
|
||||||
|
if n <= x {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(p.baseline) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns buffer with length of n.
|
||||||
|
func (p *BufferPool) Get(n int) []byte {
|
||||||
|
atomic.AddUint32(&p.get, 1)
|
||||||
|
|
||||||
|
poolNum := p.poolNum(n)
|
||||||
|
pool := p.pool[poolNum]
|
||||||
|
if poolNum == 0 {
|
||||||
|
// Fast path.
|
||||||
|
select {
|
||||||
|
case b := <-pool:
|
||||||
|
switch {
|
||||||
|
case cap(b) > n:
|
||||||
|
if cap(b)-n >= n {
|
||||||
|
atomic.AddUint32(&p.half, 1)
|
||||||
|
select {
|
||||||
|
case pool <- b:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return make([]byte, n)
|
||||||
|
} else {
|
||||||
|
atomic.AddUint32(&p.less, 1)
|
||||||
|
return b[:n]
|
||||||
|
}
|
||||||
|
case cap(b) == n:
|
||||||
|
atomic.AddUint32(&p.equal, 1)
|
||||||
|
return b[:n]
|
||||||
|
default:
|
||||||
|
atomic.AddUint32(&p.greater, 1)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
atomic.AddUint32(&p.miss, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return make([]byte, n, p.baseline0)
|
||||||
|
} else {
|
||||||
|
sizePtr := &p.size[poolNum-1]
|
||||||
|
|
||||||
|
select {
|
||||||
|
case b := <-pool:
|
||||||
|
switch {
|
||||||
|
case cap(b) > n:
|
||||||
|
if cap(b)-n >= n {
|
||||||
|
atomic.AddUint32(&p.half, 1)
|
||||||
|
sizeHalfPtr := &p.sizeHalf[poolNum-1]
|
||||||
|
if atomic.AddUint32(sizeHalfPtr, 1) == 20 {
|
||||||
|
atomic.StoreUint32(sizePtr, uint32(cap(b)/2))
|
||||||
|
atomic.StoreUint32(sizeHalfPtr, 0)
|
||||||
|
} else {
|
||||||
|
select {
|
||||||
|
case pool <- b:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return make([]byte, n)
|
||||||
|
} else {
|
||||||
|
atomic.AddUint32(&p.less, 1)
|
||||||
|
return b[:n]
|
||||||
|
}
|
||||||
|
case cap(b) == n:
|
||||||
|
atomic.AddUint32(&p.equal, 1)
|
||||||
|
return b[:n]
|
||||||
|
default:
|
||||||
|
atomic.AddUint32(&p.greater, 1)
|
||||||
|
if uint32(cap(b)) >= atomic.LoadUint32(sizePtr) {
|
||||||
|
select {
|
||||||
|
case pool <- b:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
atomic.AddUint32(&p.miss, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if size := atomic.LoadUint32(sizePtr); uint32(n) > size {
|
||||||
|
if size == 0 {
|
||||||
|
atomic.CompareAndSwapUint32(sizePtr, 0, uint32(n))
|
||||||
|
} else {
|
||||||
|
sizeMissPtr := &p.sizeMiss[poolNum-1]
|
||||||
|
if atomic.AddUint32(sizeMissPtr, 1) == 20 {
|
||||||
|
atomic.StoreUint32(sizePtr, uint32(n))
|
||||||
|
atomic.StoreUint32(sizeMissPtr, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return make([]byte, n)
|
||||||
|
} else {
|
||||||
|
return make([]byte, n, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put adds given buffer to the pool.
|
||||||
|
func (p *BufferPool) Put(b []byte) {
|
||||||
|
atomic.AddUint32(&p.put, 1)
|
||||||
|
|
||||||
|
pool := p.pool[p.poolNum(cap(b))]
|
||||||
|
select {
|
||||||
|
case pool <- b:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BufferPool) Close() {
|
||||||
|
select {
|
||||||
|
case p.close <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BufferPool) String() string {
|
||||||
|
return fmt.Sprintf("BufferPool{B·%d Z·%v Zm·%v Zh·%v G·%d P·%d H·%d <·%d =·%d >·%d M·%d}",
|
||||||
|
p.baseline0, p.size, p.sizeMiss, p.sizeHalf, p.get, p.put, p.half, p.less, p.equal, p.greater, p.miss)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *BufferPool) drain() {
|
||||||
|
ticker := time.NewTicker(2 * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
for _, ch := range p.pool {
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case <-p.close:
|
||||||
|
for _, ch := range p.pool {
|
||||||
|
close(ch)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferPool creates a new initialized 'buffer pool'.
|
||||||
|
func NewBufferPool(baseline int) *BufferPool {
|
||||||
|
if baseline <= 0 {
|
||||||
|
panic("baseline can't be <= 0")
|
||||||
|
}
|
||||||
|
p := &BufferPool{
|
||||||
|
baseline0: baseline,
|
||||||
|
baseline: [...]int{baseline / 4, baseline / 2, baseline * 2, baseline * 4},
|
||||||
|
close: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
for i, cap := range []int{2, 2, 4, 4, 2, 1} {
|
||||||
|
p.pool[i] = make(chan []byte, cap)
|
||||||
|
}
|
||||||
|
go p.drain()
|
||||||
|
return p
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
// Copyright (c) 2014, Suryandaru Triandana <syndtr@gmail.com>
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build go1.3
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Pool struct {
|
||||||
|
sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPool(cap int) *Pool {
|
||||||
|
return &Pool{}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (c) 2014, Suryandaru Triandana <syndtr@gmail.com>
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !go1.3
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
type Pool struct {
|
||||||
|
pool chan interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) Get() interface{} {
|
||||||
|
select {
|
||||||
|
case x := <-p.pool:
|
||||||
|
return x
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) Put(x interface{}) {
|
||||||
|
select {
|
||||||
|
case p.pool <- x:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPool(cap int) *Pool {
|
||||||
|
return &Pool{pool: make(chan interface{}, cap)}
|
||||||
|
}
|
||||||
+16
@@ -14,3 +14,19 @@ type Range struct {
|
|||||||
// Limit of the key range, not include in the range.
|
// Limit of the key range, not include in the range.
|
||||||
Limit []byte
|
Limit []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BytesPrefix returns key range that satisfy the given prefix.
|
||||||
|
// This only applicable for the standard 'bytes comparer'.
|
||||||
|
func BytesPrefix(prefix []byte) *Range {
|
||||||
|
var limit []byte
|
||||||
|
for i := len(prefix) - 1; i >= 0; i-- {
|
||||||
|
c := prefix[i]
|
||||||
|
if c < 0xff {
|
||||||
|
limit = make([]byte, i+1)
|
||||||
|
copy(limit, prefix)
|
||||||
|
limit[i] = c + 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &Range{prefix, limit}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
syncthing
|
syncthing
|
||||||
=========
|
=========
|
||||||
|
|
||||||
[](https://travis-ci.org/syncthing/syncthing)
|
[](http://build.syncthing.net/job/syncthing/lastBuild/)
|
||||||
[](https://coveralls.io/r/syncthing/syncthing?branch=master)
|
[](http://godoc.org/github.com/syncthing/syncthing)
|
||||||
[](http://godoc.org/github.com/syncthing/syncthing)
|
[](http://opensource.org/licenses/MIT)
|
||||||
[](http://opensource.org/licenses/MIT)
|
|
||||||
|
|
||||||
This is the `syncthing` project. The following are the project goals:
|
This is the `syncthing` project. The following are the project goals:
|
||||||
|
|
||||||
@@ -26,14 +25,22 @@ for incompatible changes.
|
|||||||
Getting Started
|
Getting Started
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
Take a look at the [getting started guide](http://discourse.syncthing.net/t/getting-started/46).
|
Take a look at the [getting started guide](http://discourse.syncthing.net/t/46).
|
||||||
|
|
||||||
|
Building
|
||||||
|
--------
|
||||||
|
|
||||||
|
Building Syncthing from source is easy, and there's a
|
||||||
|
[guide](http://discourse.syncthing.net/t/44)
|
||||||
|
that describes it for both Unix and Windows.
|
||||||
|
|
||||||
Signed Releases
|
Signed Releases
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
As of v0.7.0 and onwards, git tags and release binaries are GPG signed with
|
As of v0.7.0 and onwards, git tags and release binaries are GPG signed with
|
||||||
the key BCE524C7 (http://nym.se/gpg.txt). The signature is included in the
|
the key BCE524C7 (http://nym.se/gpg.txt). For release binaries, MD5 and
|
||||||
normal release bundle as `syncthing.asc` or `syncthing.exe.asc`.
|
SHA1 checksums are calculated and signed, available in the
|
||||||
|
md5sum.txt.asc and sha1sum.txt.asc files.
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
=============
|
=============
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
// Package auto contains auto generated files for web assets.
|
|
||||||
package auto
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,500 @@
|
|||||||
|
// 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 ignore
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
|
||||||
|
goarch string
|
||||||
|
goos string
|
||||||
|
noupgrade bool
|
||||||
|
)
|
||||||
|
|
||||||
|
const minGoVersion = 1.3
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.SetOutput(os.Stdout)
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
if os.Getenv("GOPATH") == "" {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
gopath := filepath.Clean(filepath.Join(cwd, "../../../../"))
|
||||||
|
log.Println("GOPATH is", gopath)
|
||||||
|
os.Setenv("GOPATH", gopath)
|
||||||
|
}
|
||||||
|
os.Setenv("PATH", fmt.Sprintf("%s%cbin%c%s", os.Getenv("GOPATH"), os.PathSeparator, os.PathListSeparator, os.Getenv("PATH")))
|
||||||
|
|
||||||
|
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
|
||||||
|
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
|
||||||
|
flag.BoolVar(&noupgrade, "no-upgrade", false, "Disable upgrade functionality")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
switch goarch {
|
||||||
|
case "386", "amd64", "armv5", "armv6", "armv7":
|
||||||
|
break
|
||||||
|
case "arm":
|
||||||
|
log.Println("Invalid goarch \"arm\". Use one of \"armv5\", \"armv6\", \"armv7\".")
|
||||||
|
log.Fatalln("Note that producing a correct \"armv5\" binary requires a rebuilt stdlib.")
|
||||||
|
default:
|
||||||
|
log.Printf("Unknown goarch %q; proceed with caution!", goarch)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkRequiredGoVersion()
|
||||||
|
|
||||||
|
if check() != nil {
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
if flag.NArg() == 0 {
|
||||||
|
install("./cmd/...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch flag.Arg(0) {
|
||||||
|
case "install":
|
||||||
|
pkg := "./cmd/..."
|
||||||
|
if flag.NArg() > 2 {
|
||||||
|
pkg = flag.Arg(1)
|
||||||
|
}
|
||||||
|
install(pkg)
|
||||||
|
|
||||||
|
case "build":
|
||||||
|
pkg := "./cmd/syncthing"
|
||||||
|
if flag.NArg() > 2 {
|
||||||
|
pkg = flag.Arg(1)
|
||||||
|
}
|
||||||
|
var tags []string
|
||||||
|
if noupgrade {
|
||||||
|
tags = []string{"noupgrade"}
|
||||||
|
}
|
||||||
|
build(pkg, tags)
|
||||||
|
|
||||||
|
case "test":
|
||||||
|
pkg := "./..."
|
||||||
|
if flag.NArg() > 2 {
|
||||||
|
pkg = flag.Arg(1)
|
||||||
|
}
|
||||||
|
test(pkg)
|
||||||
|
|
||||||
|
case "assets":
|
||||||
|
assets()
|
||||||
|
|
||||||
|
case "xdr":
|
||||||
|
xdr()
|
||||||
|
|
||||||
|
case "translate":
|
||||||
|
translate()
|
||||||
|
|
||||||
|
case "transifex":
|
||||||
|
transifex()
|
||||||
|
|
||||||
|
case "deps":
|
||||||
|
deps()
|
||||||
|
|
||||||
|
case "tar":
|
||||||
|
buildTar()
|
||||||
|
|
||||||
|
case "zip":
|
||||||
|
buildZip()
|
||||||
|
|
||||||
|
case "clean":
|
||||||
|
clean()
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown command %q", flag.Arg(0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func check() error {
|
||||||
|
_, err := exec.LookPath("godep")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkRequiredGoVersion() {
|
||||||
|
ver := run("go", "version")
|
||||||
|
re := regexp.MustCompile(`go version go(\d+\.\d+)`)
|
||||||
|
if m := re.FindSubmatch(ver); len(m) == 2 {
|
||||||
|
vs := string(m[1])
|
||||||
|
// This is a standard go build. Verify that it's new enough.
|
||||||
|
f, err := strconv.ParseFloat(vs, 64)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("*** Could parse Go version out of %q.\n*** This isn't known to work, proceed on your own risk.", vs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if f < minGoVersion {
|
||||||
|
log.Fatalf("*** Go version %.01f is less than required %.01f.\n*** This is known not to work, not proceeding.", f, minGoVersion)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("*** Unknown Go version %q.\n*** This isn't known to work, proceed on your own risk.", ver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
runPrint("go", "get", "-v", "code.google.com/p/go.tools/cmd/cover")
|
||||||
|
runPrint("go", "get", "-v", "code.google.com/p/go.tools/cmd/vet")
|
||||||
|
runPrint("go", "get", "-v", "code.google.com/p/go.net/html")
|
||||||
|
runPrint("go", "get", "-v", "github.com/tools/godep")
|
||||||
|
}
|
||||||
|
|
||||||
|
func test(pkg string) {
|
||||||
|
runPrint("godep", "go", "test", "-short", "-timeout", "10s", pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func install(pkg string) {
|
||||||
|
os.Setenv("GOBIN", "./bin")
|
||||||
|
setBuildEnv()
|
||||||
|
runPrint("godep", "go", "install", "-ldflags", ldflags(), pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func build(pkg string, tags []string) {
|
||||||
|
rmr("syncthing", "syncthing.exe")
|
||||||
|
args := []string{"go", "build", "-ldflags", ldflags()}
|
||||||
|
if len(tags) > 0 {
|
||||||
|
args = append(args, "-tags", strings.Join(tags, ","))
|
||||||
|
}
|
||||||
|
args = append(args, pkg)
|
||||||
|
setBuildEnv()
|
||||||
|
runPrint("godep", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTar() {
|
||||||
|
name := archiveName()
|
||||||
|
var tags []string
|
||||||
|
if noupgrade {
|
||||||
|
tags = []string{"noupgrade"}
|
||||||
|
name += "-noupgrade"
|
||||||
|
}
|
||||||
|
build("./cmd/syncthing", tags)
|
||||||
|
filename := name + ".tar.gz"
|
||||||
|
tarGz(filename, []archiveFile{
|
||||||
|
{"README.md", name + "/README.txt"},
|
||||||
|
{"LICENSE", name + "/LICENSE.txt"},
|
||||||
|
{"CONTRIBUTORS", name + "/CONTRIBUTORS.txt"},
|
||||||
|
{"syncthing", name + "/syncthing"},
|
||||||
|
})
|
||||||
|
log.Println(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildZip() {
|
||||||
|
name := archiveName()
|
||||||
|
var tags []string
|
||||||
|
if noupgrade {
|
||||||
|
tags = []string{"noupgrade"}
|
||||||
|
name += "-noupgrade"
|
||||||
|
}
|
||||||
|
build("./cmd/syncthing", tags)
|
||||||
|
filename := name + ".zip"
|
||||||
|
zipFile(filename, []archiveFile{
|
||||||
|
{"README.md", name + "/README.txt"},
|
||||||
|
{"LICENSE", name + "/LICENSE.txt"},
|
||||||
|
{"CONTRIBUTORS", name + "/CONTRIBUTORS.txt"},
|
||||||
|
{"syncthing.exe", name + "/syncthing.exe"},
|
||||||
|
})
|
||||||
|
log.Println(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setBuildEnv() {
|
||||||
|
os.Setenv("GOOS", goos)
|
||||||
|
if strings.HasPrefix(goarch, "arm") {
|
||||||
|
os.Setenv("GOARCH", "arm")
|
||||||
|
os.Setenv("GOARM", goarch[4:])
|
||||||
|
} else {
|
||||||
|
os.Setenv("GOARCH", goarch)
|
||||||
|
}
|
||||||
|
if goarch == "386" {
|
||||||
|
os.Setenv("GO386", "387")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assets() {
|
||||||
|
runPipe("internal/auto/gui.files.go", "godep", "go", "run", "cmd/genassets/main.go", "gui")
|
||||||
|
}
|
||||||
|
|
||||||
|
func xdr() {
|
||||||
|
for _, f := range []string{"internal/discover/packets", "internal/files/leveldb", "internal/protocol/message"} {
|
||||||
|
runPipe(f+"_xdr.go", "go", "run", "./Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go", "--", f+".go")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func translate() {
|
||||||
|
os.Chdir("gui/lang")
|
||||||
|
runPipe("lang-en-new.json", "go", "run", "../../cmd/translate/main.go", "lang-en.json", "../index.html")
|
||||||
|
os.Remove("lang-en.json")
|
||||||
|
err := os.Rename("lang-en-new.json", "lang-en.json")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
os.Chdir("../..")
|
||||||
|
}
|
||||||
|
|
||||||
|
func transifex() {
|
||||||
|
os.Chdir("gui/lang")
|
||||||
|
runPrint("go", "run", "../../cmd/transifexdl/main.go")
|
||||||
|
os.Chdir("../..")
|
||||||
|
assets()
|
||||||
|
}
|
||||||
|
|
||||||
|
func deps() {
|
||||||
|
rmr("Godeps")
|
||||||
|
runPrint("godep", "save", "./cmd/...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func clean() {
|
||||||
|
rmr("bin", "Godeps/_workspace/pkg", "Godeps/_workspace/bin")
|
||||||
|
rmr(filepath.Join(os.Getenv("GOPATH"), fmt.Sprintf("pkg/%s_%s/github.com/syncthing", goos, goarch)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ldflags() string {
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.WriteString("-w")
|
||||||
|
b.WriteString(fmt.Sprintf(" -X main.Version %s", version()))
|
||||||
|
b.WriteString(fmt.Sprintf(" -X main.BuildStamp %d", buildStamp()))
|
||||||
|
b.WriteString(fmt.Sprintf(" -X main.BuildUser %s", buildUser()))
|
||||||
|
b.WriteString(fmt.Sprintf(" -X main.BuildHost %s", buildHost()))
|
||||||
|
b.WriteString(fmt.Sprintf(" -X main.BuildEnv %s", buildEnvironment()))
|
||||||
|
if strings.HasPrefix(goarch, "arm") {
|
||||||
|
b.WriteString(fmt.Sprintf(" -X main.GoArchExtra %s", goarch[3:]))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func rmr(paths ...string) {
|
||||||
|
for _, path := range paths {
|
||||||
|
log.Println("rm -r", path)
|
||||||
|
os.RemoveAll(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func version() string {
|
||||||
|
v := run("git", "describe", "--always", "--dirty")
|
||||||
|
v = versionRe.ReplaceAllFunc(v, func(s []byte) []byte {
|
||||||
|
s[0] = '+'
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
return string(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildStamp() int64 {
|
||||||
|
bs := run("git", "show", "-s", "--format=%ct")
|
||||||
|
s, _ := strconv.ParseInt(string(bs), 10, 64)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildUser() string {
|
||||||
|
u, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "unknown-user"
|
||||||
|
}
|
||||||
|
return strings.Replace(u.Username, " ", "-", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildHost() string {
|
||||||
|
h, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return "unknown-host"
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildEnvironment() string {
|
||||||
|
if v := os.Getenv("ENVIRONMENT"); len(v) > 0 {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildArch() string {
|
||||||
|
os := goos
|
||||||
|
if os == "darwin" {
|
||||||
|
os = "macosx"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s-%s", os, goarch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func archiveName() string {
|
||||||
|
return fmt.Sprintf("syncthing-%s-%s", buildArch(), version())
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(cmd string, args ...string) []byte {
|
||||||
|
ecmd := exec.Command(cmd, args...)
|
||||||
|
bs, err := ecmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Println(cmd, strings.Join(args, " "))
|
||||||
|
log.Println(string(bs))
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
return bytes.TrimSpace(bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPrint(cmd string, args ...string) {
|
||||||
|
log.Println(cmd, strings.Join(args, " "))
|
||||||
|
ecmd := exec.Command(cmd, args...)
|
||||||
|
ecmd.Stdout = os.Stdout
|
||||||
|
ecmd.Stderr = os.Stderr
|
||||||
|
err := ecmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPipe(file, cmd string, args ...string) {
|
||||||
|
log.Println(cmd, strings.Join(args, " "), ">", file)
|
||||||
|
fd, err := os.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
ecmd := exec.Command(cmd, args...)
|
||||||
|
ecmd.Stdout = fd
|
||||||
|
ecmd.Stderr = os.Stderr
|
||||||
|
err = ecmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fd.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
type archiveFile struct {
|
||||||
|
src string
|
||||||
|
dst string
|
||||||
|
}
|
||||||
|
|
||||||
|
func tarGz(out string, files []archiveFile) {
|
||||||
|
fd, err := os.Create(out)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gw := gzip.NewWriter(fd)
|
||||||
|
tw := tar.NewWriter(gw)
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
sf, err := os.Open(f.src)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := sf.Stat()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
h := &tar.Header{
|
||||||
|
Name: f.dst,
|
||||||
|
Size: info.Size(),
|
||||||
|
Mode: int64(info.Mode()),
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tw.WriteHeader(h)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = io.Copy(tw, sf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
sf.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tw.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = gw.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = fd.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func zipFile(out string, files []archiveFile) {
|
||||||
|
fd, err := os.Create(out)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
zw := zip.NewWriter(fd)
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
sf, err := os.Open(f.src)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := sf.Stat()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fh, err := zip.FileInfoHeader(info)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fh.Name = f.dst
|
||||||
|
fh.Method = zip.Deflate
|
||||||
|
|
||||||
|
if strings.HasSuffix(f.dst, ".txt") {
|
||||||
|
// Text file. Read it and convert line endings.
|
||||||
|
bs, err := ioutil.ReadAll(sf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
bs = bytes.Replace(bs, []byte{'\n'}, []byte{'\n', '\r'}, -1)
|
||||||
|
fh.UncompressedSize = uint32(len(bs))
|
||||||
|
fh.UncompressedSize64 = uint64(len(bs))
|
||||||
|
|
||||||
|
of, err := zw.CreateHeader(fh)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
of.Write(bs)
|
||||||
|
} else {
|
||||||
|
// Binary file. Copy verbatim.
|
||||||
|
of, err := zw.CreateHeader(fh)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = io.Copy(of, sf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = zw.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = fd.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,239 +1,105 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
export COPYFILE_DISABLE=true
|
case "${1:-default}" in
|
||||||
export GO386=387 # Don't use SSE on 32 bit builds
|
default)
|
||||||
|
go run build.go
|
||||||
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)
|
|
||||||
host=${host%%.*}
|
|
||||||
bldenv=${ENVIRONMENT:-default}
|
|
||||||
ldflags="-w -X main.Version $version -X main.BuildStamp $date -X main.BuildUser $user -X main.BuildHost $host -X main.BuildEnv $bldenv"
|
|
||||||
|
|
||||||
check() {
|
|
||||||
if ! command -v godep >/dev/null ; then
|
|
||||||
echo "Error: no godep. Try \"$0 setup\"."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
build() {
|
|
||||||
check
|
|
||||||
godep go build $* -ldflags "$ldflags" ./cmd/syncthing
|
|
||||||
}
|
|
||||||
|
|
||||||
assets() {
|
|
||||||
check
|
|
||||||
godep go run cmd/genassets/main.go gui > auto/gui.files.go
|
|
||||||
}
|
|
||||||
|
|
||||||
test-cov() {
|
|
||||||
echo "mode: set" > coverage.out
|
|
||||||
fail=0
|
|
||||||
|
|
||||||
for dir in $(go list ./...) ; do
|
|
||||||
godep go test -coverprofile=profile.out $dir || fail=1
|
|
||||||
if [ -f profile.out ] ; then
|
|
||||||
grep -v "mode: set" profile.out >> coverage.out
|
|
||||||
rm profile.out
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
exit $fail
|
|
||||||
}
|
|
||||||
|
|
||||||
test() {
|
|
||||||
check
|
|
||||||
go vet ./...
|
|
||||||
godep go test -cpu=1,2,4 $* ./...
|
|
||||||
}
|
|
||||||
|
|
||||||
sign() {
|
|
||||||
if git describe --exact-match 2>/dev/null >/dev/null ; then
|
|
||||||
# HEAD is a tag
|
|
||||||
id=BCE524C7
|
|
||||||
if gpg --list-keys "$id" >/dev/null 2>&1 ; then
|
|
||||||
gpg -ab -u "$id" "$1"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
tarDist() {
|
|
||||||
name="$1"
|
|
||||||
rm -rf "$name"
|
|
||||||
mkdir -p "$name"
|
|
||||||
cp syncthing "${distFiles[@]}" "$name"
|
|
||||||
sign "$name/syncthing"
|
|
||||||
tar zcvf "$name.tar.gz" "$name"
|
|
||||||
rm -rf "$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
zipDist() {
|
|
||||||
name="$1"
|
|
||||||
rm -rf "$name"
|
|
||||||
mkdir -p "$name"
|
|
||||||
for f in "${distFiles[@]}" ; do
|
|
||||||
GOARCH="" GOOS="" go run cmd/todos/main.go < "$f" > "$name/$f.txt"
|
|
||||||
done
|
|
||||||
cp syncthing.exe "$name"
|
|
||||||
sign "$name/syncthing.exe"
|
|
||||||
zip -r "$name.zip" "$name"
|
|
||||||
rm -rf "$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
deps() {
|
|
||||||
check
|
|
||||||
godep save ./cmd/...
|
|
||||||
}
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
go get -v code.google.com/p/go.tools/cmd/cover
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
xdr() {
|
|
||||||
for f in discover/packets files/leveldb protocol/message ; do
|
|
||||||
go run "$(godep path)/src/github.com/calmh/xdr/cmd/genxdr/main.go" -- "${f}.go" > "${f}_xdr.go"
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
translate() {
|
|
||||||
pushd gui
|
|
||||||
go run ../cmd/translate/main.go lang-en.json < index.html > lang-en-new.json
|
|
||||||
mv lang-en-new.json lang-en.json
|
|
||||||
popd
|
|
||||||
}
|
|
||||||
|
|
||||||
transifex() {
|
|
||||||
pushd gui
|
|
||||||
go run ../cmd/transifexdl/main.go
|
|
||||||
popd
|
|
||||||
assets
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
"")
|
|
||||||
shift
|
|
||||||
export GOBIN=$(pwd)/bin
|
|
||||||
godep go install $* -ldflags "$ldflags" ./cmd/...
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
race)
|
clean)
|
||||||
build -race
|
go run build.go "$1"
|
||||||
;;
|
|
||||||
|
|
||||||
guidev)
|
|
||||||
echo "Syncthing is already built for GUI developments. Try:"
|
|
||||||
echo " STGUIASSETS=~/someDir/gui syncthing"
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
test)
|
test)
|
||||||
test -short
|
ulimit -t 60 &>/dev/null || true
|
||||||
;;
|
ulimit -d 512000 &>/dev/null || true
|
||||||
|
ulimit -m 512000 &>/dev/null || true
|
||||||
|
|
||||||
test-cov)
|
go run build.go "$1"
|
||||||
test-cov
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
tar)
|
tar)
|
||||||
rm -f *.tar.gz *.zip
|
go run build.go "$1"
|
||||||
test -short || exit 1
|
|
||||||
assets
|
|
||||||
build
|
|
||||||
|
|
||||||
eval $(go env)
|
|
||||||
name="syncthing-${GOOS/darwin/macosx}-$GOARCH-$version"
|
|
||||||
|
|
||||||
tarDist "$name"
|
|
||||||
;;
|
|
||||||
|
|
||||||
all)
|
|
||||||
rm -f *.tar.gz *.zip
|
|
||||||
test -short || exit 1
|
|
||||||
assets
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
;;
|
|
||||||
|
|
||||||
upload)
|
|
||||||
tag=$(git describe)
|
|
||||||
shopt -s nullglob
|
|
||||||
for f in *.tar.gz *.zip *.asc ; do
|
|
||||||
relup syncthing/syncthing "$tag" "$f"
|
|
||||||
done
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
deps)
|
deps)
|
||||||
deps
|
go run build.go "$1"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
assets)
|
assets)
|
||||||
assets
|
go run build.go "$1"
|
||||||
;;
|
|
||||||
|
|
||||||
setup)
|
|
||||||
setup
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
xdr)
|
xdr)
|
||||||
xdr
|
go run build.go "$1"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
translate)
|
translate)
|
||||||
translate
|
go run build.go "$1"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
transifex)
|
transifex)
|
||||||
transifex
|
go run build.go "$1"
|
||||||
|
;;
|
||||||
|
|
||||||
|
noupgrade)
|
||||||
|
go run build.go -no-upgrade tar
|
||||||
|
;;
|
||||||
|
|
||||||
|
all)
|
||||||
|
go run build.go -goos linux -goarch amd64 tar
|
||||||
|
go run build.go -goos linux -goarch 386 tar
|
||||||
|
go run build.go -goos linux -goarch armv5 tar
|
||||||
|
go run build.go -goos linux -goarch armv6 tar
|
||||||
|
go run build.go -goos linux -goarch armv7 tar
|
||||||
|
|
||||||
|
go run build.go -goos freebsd -goarch amd64 tar
|
||||||
|
go run build.go -goos freebsd -goarch 386 tar
|
||||||
|
|
||||||
|
go run build.go -goos darwin -goarch amd64 tar
|
||||||
|
|
||||||
|
go run build.go -goos windows -goarch amd64 zip
|
||||||
|
go run build.go -goos windows -goarch 386 zip
|
||||||
|
;;
|
||||||
|
|
||||||
|
setup)
|
||||||
|
echo "Don't worry, just build."
|
||||||
|
;;
|
||||||
|
|
||||||
|
test-cov)
|
||||||
|
ulimit -t 60 &>/dev/null || true
|
||||||
|
ulimit -d 512000 &>/dev/null || true
|
||||||
|
ulimit -m 512000 &>/dev/null || true
|
||||||
|
|
||||||
|
go get github.com/axw/gocov/gocov
|
||||||
|
go get github.com/AlekSi/gocov-xml
|
||||||
|
|
||||||
|
echo "mode: set" > coverage.out
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
# For every package in the repo
|
||||||
|
for dir in $(go list ./...) ; do
|
||||||
|
# run the tests
|
||||||
|
godep go test -coverprofile=profile.out $dir
|
||||||
|
if [ -f profile.out ] ; then
|
||||||
|
# and if there was test output, append it to coverage.out
|
||||||
|
grep -v "mode: set" profile.out >> coverage.out
|
||||||
|
rm profile.out
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
gocov convert coverage.out | gocov-xml > coverage.xml
|
||||||
|
|
||||||
|
# This is usually run from within Jenkins. If it is, we need to
|
||||||
|
# tweak the paths in coverage.xml so cobertura finds the
|
||||||
|
# source.
|
||||||
|
if [[ "${WORKSPACE:-default}" != "default" ]] ; then
|
||||||
|
sed "s#$WORKSPACE##g" < coverage.xml > coverage.xml.new && mv coverage.xml.new coverage.xml
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
*)
|
*)
|
||||||
echo "Unknown build parameter $1"
|
echo "Unknown build command $1"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
missing-contribs() {
|
||||||
|
for email in $(git log --format=%ae master | grep -v jakob@nym.se | sort | uniq) ; do
|
||||||
|
grep -q "$email" CONTRIBUTORS || echo $email
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
no-docs-typos() {
|
||||||
|
# Commits that are known to not change code
|
||||||
|
grep -v 63bd0136fb40a91efaa279cb4b4159d82e8e6904 |\
|
||||||
|
grep -v 4e2feb6fbc791bb8a2daf0ab8efb10775d66343e |\
|
||||||
|
grep -v f2459ef3319b2f060dbcdacd0c35a1788a94b8bd |\
|
||||||
|
grep -v b61f418bf2d1f7d5a9d7088a20a2a448e5e66801 |\
|
||||||
|
grep -v f0621207e3953711f9ab86d99724f1d0faac45b1 |\
|
||||||
|
grep -v f1120d7aa936c0658429edef0037792520b46334
|
||||||
|
}
|
||||||
|
|
||||||
|
print-missing-contribs() {
|
||||||
|
for email in $(missing-contribs) ; do
|
||||||
|
git log --author="$email" --format="%H %ae %s" | no-docs-typos
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
print-missing-copyright() {
|
||||||
|
find . -name \*.go | xargs grep -L 'Copyright (C)' | grep -v Godeps
|
||||||
|
}
|
||||||
|
|
||||||
|
print-missing-contribs
|
||||||
|
print-missing-copyright
|
||||||
|
|
||||||
+11
-11
@@ -9,8 +9,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"encoding/base64"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"go/format"
|
"go/format"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -23,27 +23,27 @@ var tpl = template.Must(template.New("assets").Parse(`package auto
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"encoding/hex"
|
"encoding/base64"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Assets = make(map[string][]byte)
|
func Assets() map[string][]byte {
|
||||||
|
var assets = make(map[string][]byte, {{.assets | len}})
|
||||||
func init() {
|
|
||||||
var bs []byte
|
var bs []byte
|
||||||
var gr *gzip.Reader
|
var gr *gzip.Reader
|
||||||
{{range $asset := .assets}}
|
{{range $asset := .assets}}
|
||||||
bs, _ = hex.DecodeString("{{$asset.HexData}}")
|
bs, _ = base64.StdEncoding.DecodeString("{{$asset.Data}}")
|
||||||
gr, _ = gzip.NewReader(bytes.NewBuffer(bs))
|
gr, _ = gzip.NewReader(bytes.NewBuffer(bs))
|
||||||
bs, _ = ioutil.ReadAll(gr)
|
bs, _ = ioutil.ReadAll(gr)
|
||||||
Assets["{{$asset.Name}}"] = bs
|
assets["{{$asset.Name}}"] = bs
|
||||||
{{end}}
|
{{end}}
|
||||||
|
return assets
|
||||||
}
|
}
|
||||||
`))
|
`))
|
||||||
|
|
||||||
type asset struct {
|
type asset struct {
|
||||||
Name string
|
Name string
|
||||||
HexData string
|
Data string
|
||||||
}
|
}
|
||||||
|
|
||||||
var assets []asset
|
var assets []asset
|
||||||
@@ -69,8 +69,8 @@ func walkerFor(basePath string) filepath.WalkFunc {
|
|||||||
|
|
||||||
name, _ = filepath.Rel(basePath, name)
|
name, _ = filepath.Rel(basePath, name)
|
||||||
assets = append(assets, asset{
|
assets = append(assets, asset{
|
||||||
Name: filepath.ToSlash(name),
|
Name: filepath.ToSlash(name),
|
||||||
HexData: fmt.Sprintf("%x", buf.Bytes()),
|
Data: base64.StdEncoding.EncodeToString(buf.Bytes()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -10,8 +10,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/files"
|
"github.com/syncthing/syncthing/internal/files"
|
||||||
"github.com/syncthing/syncthing/protocol"
|
"github.com/syncthing/syncthing/internal/protocol"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +32,8 @@ func main() {
|
|||||||
|
|
||||||
if *node == "" {
|
if *node == "" {
|
||||||
log.Printf("*** Global index for repo %q", *repo)
|
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(f)
|
||||||
fmt.Println("\t", fs.Availability(f.Name))
|
fmt.Println("\t", fs.Availability(f.Name))
|
||||||
return true
|
return true
|
||||||
@@ -43,7 +44,8 @@ func main() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
log.Printf("*** Have index for repo %q node %q", *repo, n)
|
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)
|
fmt.Println(f)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|||||||
+205
-176
@@ -5,34 +5,29 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"crypto/tls"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"mime"
|
"mime"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"crypto/tls"
|
|
||||||
"code.google.com/p/go.crypto/bcrypt"
|
"code.google.com/p/go.crypto/bcrypt"
|
||||||
"github.com/syncthing/syncthing/auto"
|
"github.com/syncthing/syncthing/internal/auto"
|
||||||
"github.com/syncthing/syncthing/config"
|
"github.com/syncthing/syncthing/internal/config"
|
||||||
"github.com/syncthing/syncthing/events"
|
"github.com/syncthing/syncthing/internal/events"
|
||||||
"github.com/syncthing/syncthing/logger"
|
"github.com/syncthing/syncthing/internal/logger"
|
||||||
"github.com/syncthing/syncthing/model"
|
"github.com/syncthing/syncthing/internal/model"
|
||||||
"github.com/syncthing/syncthing/protocol"
|
"github.com/syncthing/syncthing/internal/protocol"
|
||||||
"github.com/syncthing/syncthing/upgrade"
|
"github.com/syncthing/syncthing/internal/upgrade"
|
||||||
"github.com/vitrun/qart/qr"
|
"github.com/vitrun/qart/qr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,16 +40,10 @@ var (
|
|||||||
configInSync = true
|
configInSync = true
|
||||||
guiErrors = []guiError{}
|
guiErrors = []guiError{}
|
||||||
guiErrorsMut sync.Mutex
|
guiErrorsMut sync.Mutex
|
||||||
static func(http.ResponseWriter, *http.Request, *log.Logger)
|
|
||||||
apiKey string
|
|
||||||
modt = time.Now().UTC().Format(http.TimeFormat)
|
modt = time.Now().UTC().Format(http.TimeFormat)
|
||||||
eventSub *events.BufferedSubscription
|
eventSub *events.BufferedSubscription
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
unchangedPassword = "--password-unchanged--"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
l.AddHandler(logger.LevelWarn, showGuiError)
|
l.AddHandler(logger.LevelWarn, showGuiError)
|
||||||
sub := events.Default.Subscribe(events.AllEvents)
|
sub := events.Default.Subscribe(events.AllEvents)
|
||||||
@@ -62,39 +51,32 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
|
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
|
||||||
var listener net.Listener
|
|
||||||
var err error
|
var err error
|
||||||
if cfg.UseTLS {
|
|
||||||
cert, err := loadCert(confDir, "https-")
|
cert, err := loadCert(confDir, "https-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Infoln("Loading HTTPS certificate:", err)
|
l.Infoln("Loading HTTPS certificate:", err)
|
||||||
l.Infoln("Creating new HTTPS certificate")
|
l.Infoln("Creating new HTTPS certificate")
|
||||||
newCertificate(confDir, "https-")
|
newCertificate(confDir, "https-")
|
||||||
cert, err = loadCert(confDir, "https-")
|
cert, err = loadCert(confDir, "https-")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tlsCfg := &tls.Config{
|
tlsCfg := &tls.Config{
|
||||||
Certificates: []tls.Certificate{cert},
|
Certificates: []tls.Certificate{cert},
|
||||||
ServerName: "syncthing",
|
ServerName: "syncthing",
|
||||||
}
|
|
||||||
listener, err = tls.Listen("tcp", cfg.Address, tlsCfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
listener, err = net.Listen("tcp", cfg.Address)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKey = cfg.APIKey
|
rawListener, err := net.Listen("tcp", cfg.Address)
|
||||||
loadCsrfTokens()
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
listener := &DowngradingListener{rawListener, tlsCfg}
|
||||||
|
|
||||||
// The GET handlers
|
// The GET handlers
|
||||||
getRestMux := http.NewServeMux()
|
getRestMux := http.NewServeMux()
|
||||||
|
getRestMux.HandleFunc("/rest/ping", restPing)
|
||||||
getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion))
|
getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion))
|
||||||
getRestMux.HandleFunc("/rest/config", restGetConfig)
|
getRestMux.HandleFunc("/rest/config", restGetConfig)
|
||||||
getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync)
|
getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync)
|
||||||
@@ -102,30 +84,34 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
|||||||
getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
|
getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
|
||||||
getRestMux.HandleFunc("/rest/errors", restGetErrors)
|
getRestMux.HandleFunc("/rest/errors", restGetErrors)
|
||||||
getRestMux.HandleFunc("/rest/events", restGetEvents)
|
getRestMux.HandleFunc("/rest/events", restGetEvents)
|
||||||
|
getRestMux.HandleFunc("/rest/ignores", withModel(m, restGetIgnores))
|
||||||
getRestMux.HandleFunc("/rest/lang", restGetLang)
|
getRestMux.HandleFunc("/rest/lang", restGetLang)
|
||||||
getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
|
getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
|
||||||
getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion))
|
|
||||||
getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed))
|
getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed))
|
||||||
getRestMux.HandleFunc("/rest/nodeid", restGetNodeID)
|
getRestMux.HandleFunc("/rest/nodeid", restGetNodeID)
|
||||||
getRestMux.HandleFunc("/rest/report", withModel(m, restGetReport))
|
getRestMux.HandleFunc("/rest/report", withModel(m, restGetReport))
|
||||||
getRestMux.HandleFunc("/rest/system", restGetSystem)
|
getRestMux.HandleFunc("/rest/system", restGetSystem)
|
||||||
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
|
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
|
||||||
getRestMux.HandleFunc("/rest/version", restGetVersion)
|
getRestMux.HandleFunc("/rest/version", restGetVersion)
|
||||||
|
getRestMux.HandleFunc("/rest/stats/node", withModel(m, restGetNodeStats))
|
||||||
|
|
||||||
// Debug endpoints, not for general use
|
// Debug endpoints, not for general use
|
||||||
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
|
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
|
||||||
|
|
||||||
// The POST handlers
|
// The POST handlers
|
||||||
postRestMux := http.NewServeMux()
|
postRestMux := http.NewServeMux()
|
||||||
|
postRestMux.HandleFunc("/rest/ping", restPing)
|
||||||
postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig))
|
postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig))
|
||||||
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
|
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
|
||||||
postRestMux.HandleFunc("/rest/error", restPostError)
|
postRestMux.HandleFunc("/rest/error", restPostError)
|
||||||
postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
|
postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
|
||||||
|
postRestMux.HandleFunc("/rest/ignores", withModel(m, restPostIgnores))
|
||||||
postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride))
|
postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride))
|
||||||
postRestMux.HandleFunc("/rest/reset", restPostReset)
|
postRestMux.HandleFunc("/rest/reset", restPostReset)
|
||||||
postRestMux.HandleFunc("/rest/restart", restPostRestart)
|
postRestMux.HandleFunc("/rest/restart", restPostRestart)
|
||||||
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
|
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
|
||||||
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
|
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
|
||||||
|
postRestMux.HandleFunc("/rest/scan", withModel(m, restPostScan))
|
||||||
|
|
||||||
// A handler that splits requests between the two above and disables
|
// A handler that splits requests between the two above and disables
|
||||||
// caching
|
// caching
|
||||||
@@ -141,14 +127,27 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
|||||||
|
|
||||||
// Wrap everything in CSRF protection. The /rest prefix should be
|
// Wrap everything in CSRF protection. The /rest prefix should be
|
||||||
// protected, other requests will grant cookies.
|
// protected, other requests will grant cookies.
|
||||||
handler := csrfMiddleware("/rest", mux)
|
handler := csrfMiddleware("/rest", cfg.APIKey, mux)
|
||||||
|
|
||||||
|
// Add our version as a header to responses
|
||||||
|
handler = withVersionMiddleware(handler)
|
||||||
|
|
||||||
// Wrap everything in basic auth, if user/password is set.
|
// Wrap everything in basic auth, if user/password is set.
|
||||||
if len(cfg.User) > 0 {
|
if len(cfg.User) > 0 && len(cfg.Password) > 0 {
|
||||||
handler = basicAuthMiddleware(cfg.User, cfg.Password, handler)
|
handler = basicAuthAndSessionMiddleware(cfg, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
go http.Serve(listener, handler)
|
// Redirect to HTTPS if we are supposed to
|
||||||
|
if cfg.UseTLS {
|
||||||
|
handler = redirectToHTTPSMiddleware(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := http.Serve(listener, handler)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +164,23 @@ func getPostHandler(get, post http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func redirectToHTTPSMiddleware(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Add a generous access-control-allow-origin header since we may be
|
||||||
|
// redirecting REST requests over protocols
|
||||||
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
if r.TLS == nil {
|
||||||
|
// Redirect HTTP requests to HTTPS
|
||||||
|
r.URL.Host = r.Host
|
||||||
|
r.URL.Scheme = "https"
|
||||||
|
http.Redirect(w, r, r.URL.String(), http.StatusFound)
|
||||||
|
} else {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func noCacheMiddleware(h http.Handler) http.Handler {
|
func noCacheMiddleware(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
@@ -172,14 +188,34 @@ func noCacheMiddleware(h http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withVersionMiddleware(h http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("X-Syncthing-Version", Version)
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func withModel(m *model.Model, h func(m *model.Model, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
func withModel(m *model.Model, h func(m *model.Model, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
h(m, w, r)
|
h(m, w, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restPing(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"ping": "pong",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func restGetVersion(w http.ResponseWriter, r *http.Request) {
|
func restGetVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte(Version))
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"version": Version,
|
||||||
|
"longVersion": LongVersion,
|
||||||
|
"os": runtime.GOOS,
|
||||||
|
"arch": runtime.GOARCH,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func restGetCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
func restGetCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -201,17 +237,6 @@ func restGetCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(res)
|
json.NewEncoder(w).Encode(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restGetModelVersion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
||||||
var qs = r.URL.Query()
|
|
||||||
var repo = qs.Get("repo")
|
|
||||||
var res = make(map[string]interface{})
|
|
||||||
|
|
||||||
res["version"] = m.LocalVersion(repo)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
json.NewEncoder(w).Encode(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
var qs = r.URL.Query()
|
var qs = r.URL.Query()
|
||||||
var repo = qs.Get("repo")
|
var repo = qs.Get("repo")
|
||||||
@@ -236,7 +261,7 @@ func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|||||||
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
|
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
|
||||||
|
|
||||||
res["state"], res["stateChanged"] = m.State(repo)
|
res["state"], res["stateChanged"] = m.State(repo)
|
||||||
res["version"] = m.LocalVersion(repo)
|
res["version"] = m.CurrentLocalVersion(repo) + m.RemoteLocalVersion(repo)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(res)
|
json.NewEncoder(w).Encode(res)
|
||||||
@@ -245,14 +270,14 @@ func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|||||||
func restPostOverride(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
func restPostOverride(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
var qs = r.URL.Query()
|
var qs = r.URL.Query()
|
||||||
var repo = qs.Get("repo")
|
var repo = qs.Get("repo")
|
||||||
m.Override(repo)
|
go m.Override(repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
var qs = r.URL.Query()
|
var qs = r.URL.Query()
|
||||||
var repo = qs.Get("repo")
|
var repo = qs.Get("repo")
|
||||||
|
|
||||||
files := m.NeedFilesRepo(repo)
|
files := m.NeedFilesRepoLimited(repo, 100, 2500) // max 100 files or 2500 blocks
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(files)
|
json.NewEncoder(w).Encode(files)
|
||||||
@@ -264,62 +289,39 @@ func restGetConnections(m *model.Model, w http.ResponseWriter, r *http.Request)
|
|||||||
json.NewEncoder(w).Encode(res)
|
json.NewEncoder(w).Encode(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restGetConfig(w http.ResponseWriter, r *http.Request) {
|
func restGetNodeStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
encCfg := cfg
|
var res = m.NodeStatistics()
|
||||||
if encCfg.GUI.Password != "" {
|
|
||||||
encCfg.GUI.Password = unchangedPassword
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(encCfg)
|
json.NewEncoder(w).Encode(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func restGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
json.NewEncoder(w).Encode(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
var newCfg config.Configuration
|
var newCfg config.Configuration
|
||||||
err := json.NewDecoder(r.Body).Decode(&newCfg)
|
err := json.NewDecoder(r.Body).Decode(&newCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warnln(err)
|
l.Warnln("decoding posted config:", err)
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
if newCfg.GUI.Password == "" {
|
if newCfg.GUI.Password != cfg.GUI.Password {
|
||||||
// Leave it empty
|
if newCfg.GUI.Password != "" {
|
||||||
} else if newCfg.GUI.Password == unchangedPassword {
|
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
|
||||||
newCfg.GUI.Password = cfg.GUI.Password
|
if err != nil {
|
||||||
} else {
|
l.Warnln("bcrypting password:", err)
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
|
http.Error(w, err.Error(), 500)
|
||||||
if err != nil {
|
return
|
||||||
l.Warnln(err)
|
} else {
|
||||||
} else {
|
newCfg.GUI.Password = string(hash)
|
||||||
newCfg.GUI.Password = string(hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out if any changes require a restart
|
|
||||||
|
|
||||||
if len(cfg.Repositories) != len(newCfg.Repositories) {
|
|
||||||
configInSync = false
|
|
||||||
} else {
|
|
||||||
om := cfg.RepoMap()
|
|
||||||
nm := newCfg.RepoMap()
|
|
||||||
for id := range om {
|
|
||||||
if !reflect.DeepEqual(om[id], nm[id]) {
|
|
||||||
configInSync = false
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.Nodes) != len(newCfg.Nodes) {
|
// Start or stop usage reporting as appropriate
|
||||||
configInSync = false
|
|
||||||
} else {
|
|
||||||
om := cfg.NodeMap()
|
|
||||||
nm := newCfg.NodeMap()
|
|
||||||
for k := range om {
|
|
||||||
if _, ok := nm[k]; !ok {
|
|
||||||
// A node was removed and another added
|
|
||||||
configInSync = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if newCfg.Options.URAccepted > cfg.Options.URAccepted {
|
if newCfg.Options.URAccepted > cfg.Options.URAccepted {
|
||||||
// UR was enabled
|
// UR was enabled
|
||||||
@@ -335,14 +337,12 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|||||||
stopUsageReporting()
|
stopUsageReporting()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(cfg.Options, newCfg.Options) || !reflect.DeepEqual(cfg.GUI, newCfg.GUI) {
|
|
||||||
configInSync = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activate and save
|
// Activate and save
|
||||||
|
|
||||||
|
configInSync = !config.ChangeRequiresRestart(cfg, newCfg)
|
||||||
|
newCfg.Location = cfg.Location
|
||||||
|
newCfg.Save()
|
||||||
cfg = newCfg
|
cfg = newCfg
|
||||||
saveConfig()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ func restGetSystem(w http.ResponseWriter, r *http.Request) {
|
|||||||
func restGetErrors(w http.ResponseWriter, r *http.Request) {
|
func restGetErrors(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
guiErrorsMut.Lock()
|
guiErrorsMut.Lock()
|
||||||
json.NewEncoder(w).Encode(guiErrors)
|
json.NewEncoder(w).Encode(map[string][]guiError{"errors": guiErrors})
|
||||||
guiErrorsMut.Unlock()
|
guiErrorsMut.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,6 +447,41 @@ func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(reportData(m))
|
json.NewEncoder(w).Encode(reportData(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restGetIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
|
qs := r.URL.Query()
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
ignores, err := m.GetIgnores(qs.Get("repo"))
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(map[string][]string{
|
||||||
|
"ignore": ignores,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func restPostIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||||
|
qs := r.URL.Query()
|
||||||
|
|
||||||
|
var data map[string][]string
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
|
r.Body.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.SetIgnores(qs.Get("repo"), data["ignore"])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
restGetIgnores(m, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
func restGetEvents(w http.ResponseWriter, r *http.Request) {
|
func restGetEvents(w http.ResponseWriter, r *http.Request) {
|
||||||
qs := r.URL.Query()
|
qs := r.URL.Query()
|
||||||
sinceStr := qs.Get("since")
|
sinceStr := qs.Get("since")
|
||||||
@@ -454,12 +489,18 @@ func restGetEvents(w http.ResponseWriter, r *http.Request) {
|
|||||||
since, _ := strconv.Atoi(sinceStr)
|
since, _ := strconv.Atoi(sinceStr)
|
||||||
limit, _ := strconv.Atoi(limitStr)
|
limit, _ := strconv.Atoi(limitStr)
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
// Flush before blocking, to indicate that we've received the request
|
||||||
|
// and that it should not be retried.
|
||||||
|
f := w.(http.Flusher)
|
||||||
|
f.Flush()
|
||||||
|
|
||||||
evs := eventSub.Since(since, nil)
|
evs := eventSub.Since(since, nil)
|
||||||
if 0 < limit && limit < len(evs) {
|
if 0 < limit && limit < len(evs) {
|
||||||
evs = evs[len(evs)-limit:]
|
evs = evs[len(evs)-limit:]
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
||||||
json.NewEncoder(w).Encode(evs)
|
json.NewEncoder(w).Encode(evs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,9 +539,8 @@ func restGetLang(w http.ResponseWriter, r *http.Request) {
|
|||||||
lang := r.Header.Get("Accept-Language")
|
lang := r.Header.Get("Accept-Language")
|
||||||
var langs []string
|
var langs []string
|
||||||
for _, l := range strings.Split(lang, ",") {
|
for _, l := range strings.Split(lang, ",") {
|
||||||
if len(l) >= 2 {
|
parts := strings.SplitN(l, ";", 2)
|
||||||
langs = append(langs, l[:2])
|
langs = append(langs, strings.ToLower(strings.TrimSpace(parts[0])))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
json.NewEncoder(w).Encode(langs)
|
json.NewEncoder(w).Encode(langs)
|
||||||
@@ -509,20 +549,32 @@ func restGetLang(w http.ResponseWriter, r *http.Request) {
|
|||||||
func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
|
func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||||
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
|
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warnln(err)
|
l.Warnln("getting latest release:", err)
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if upgrade.CompareVersions(rel.Tag, Version) == 1 {
|
if upgrade.CompareVersions(rel.Tag, Version) == 1 {
|
||||||
err = upgrade.UpgradeTo(rel)
|
err = upgrade.UpgradeTo(rel, GoArchExtra)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warnln(err)
|
l.Warnln("upgrading:", err)
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
restPostRestart(w, r)
|
flushResponse(`{"ok": "restarting"}`, w)
|
||||||
|
l.Infoln("Upgrading")
|
||||||
|
stop <- exitUpgrading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,57 +616,9 @@ func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Reques
|
|||||||
json.NewEncoder(w).Encode(comp)
|
json.NewEncoder(w).Encode(comp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func basicAuthMiddleware(username string, passhash string, next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if validAPIKey(r.Header.Get("X-API-Key")) {
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
error := func() {
|
|
||||||
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
|
|
||||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
|
|
||||||
http.Error(w, "Not Authorized", http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
hdr := r.Header.Get("Authorization")
|
|
||||||
if !strings.HasPrefix(hdr, "Basic ") {
|
|
||||||
error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
hdr = hdr[6:]
|
|
||||||
bs, err := base64.StdEncoding.DecodeString(hdr)
|
|
||||||
if err != nil {
|
|
||||||
error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := bytes.SplitN(bs, []byte(":"), 2)
|
|
||||||
if len(fields) != 2 {
|
|
||||||
error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(fields[0]) != username {
|
|
||||||
error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(passhash), fields[1]); err != nil {
|
|
||||||
error()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func validAPIKey(k string) bool {
|
|
||||||
return len(apiKey) > 0 && k == apiKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func embeddedStatic(assetDir string) http.Handler {
|
func embeddedStatic(assetDir string) http.Handler {
|
||||||
|
assets := auto.Assets()
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
file := r.URL.Path
|
file := r.URL.Path
|
||||||
|
|
||||||
@@ -635,13 +639,13 @@ func embeddedStatic(assetDir string) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bs, ok := auto.Assets[file]
|
bs, ok := assets[file]
|
||||||
if !ok {
|
if !ok {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mtype := mime.TypeByExtension(filepath.Ext(r.URL.Path))
|
mtype := mimeTypeForFile(file)
|
||||||
if len(mtype) != 0 {
|
if len(mtype) != 0 {
|
||||||
w.Header().Set("Content-Type", mtype)
|
w.Header().Set("Content-Type", mtype)
|
||||||
}
|
}
|
||||||
@@ -651,3 +655,28 @@ func embeddedStatic(assetDir string) http.Handler {
|
|||||||
w.Write(bs)
|
w.Write(bs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mimeTypeForFile(file string) string {
|
||||||
|
// We use a built in table of the common types since the system
|
||||||
|
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||||
|
// to the system.
|
||||||
|
ext := filepath.Ext(file)
|
||||||
|
switch ext {
|
||||||
|
case ".htm", ".html":
|
||||||
|
return "text/html"
|
||||||
|
case ".css":
|
||||||
|
return "text/css"
|
||||||
|
case ".js":
|
||||||
|
return "application/javascript"
|
||||||
|
case ".json":
|
||||||
|
return "application/json"
|
||||||
|
case ".png":
|
||||||
|
return "image/png"
|
||||||
|
case ".ttf":
|
||||||
|
return "application/x-font-ttf"
|
||||||
|
case ".woff":
|
||||||
|
return "application/x-font-woff"
|
||||||
|
default:
|
||||||
|
return mime.TypeByExtension(ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Executable
+90
@@ -0,0 +1,90 @@
|
|||||||
|
// 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.google.com/p/go.crypto/bcrypt"
|
||||||
|
"github.com/syncthing/syncthing/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
sessions = make(map[string]bool)
|
||||||
|
sessionsMut sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func basicAuthAndSessionMiddleware(cfg config.GUIConfiguration, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if cfg.APIKey != "" && r.Header.Get("X-API-Key") == cfg.APIKey {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cookie, err := r.Cookie("sessionid")
|
||||||
|
if err == nil && cookie != nil {
|
||||||
|
sessionsMut.Lock()
|
||||||
|
_, ok := sessions[cookie.Value]
|
||||||
|
sessionsMut.Unlock()
|
||||||
|
if ok {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error := func() {
|
||||||
|
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
|
||||||
|
w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
|
||||||
|
http.Error(w, "Not Authorized", http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr := r.Header.Get("Authorization")
|
||||||
|
if !strings.HasPrefix(hdr, "Basic ") {
|
||||||
|
error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr = hdr[6:]
|
||||||
|
bs, err := base64.StdEncoding.DecodeString(hdr)
|
||||||
|
if err != nil {
|
||||||
|
error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := bytes.SplitN(bs, []byte(":"), 2)
|
||||||
|
if len(fields) != 2 {
|
||||||
|
error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(fields[0]) != cfg.User {
|
||||||
|
error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(cfg.Password), fields[1]); err != nil {
|
||||||
|
error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionid := randomString(32)
|
||||||
|
sessionsMut.Lock()
|
||||||
|
sessions[sessionid] = true
|
||||||
|
sessionsMut.Unlock()
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "sessionid",
|
||||||
|
Value: sessionid,
|
||||||
|
MaxAge: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
+19
-10
@@ -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.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -12,7 +16,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/osutil"
|
"github.com/syncthing/syncthing/internal/osutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var csrfTokens []string
|
var csrfTokens []string
|
||||||
@@ -21,10 +25,11 @@ var csrfMut sync.Mutex
|
|||||||
// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
|
// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
|
||||||
// the request with 403. For / and /index.html, set a new CSRF cookie if none
|
// the request with 403. For / and /index.html, set a new CSRF cookie if none
|
||||||
// is currently set.
|
// is currently set.
|
||||||
func csrfMiddleware(prefix string, next http.Handler) http.Handler {
|
func csrfMiddleware(prefix, apiKey string, next http.Handler) http.Handler {
|
||||||
|
loadCsrfTokens()
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Allow requests carrying a valid API key
|
// Allow requests carrying a valid API key
|
||||||
if validAPIKey(r.Header.Get("X-API-Key")) {
|
if apiKey != "" && r.Header.Get("X-API-Key") == apiKey {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -72,13 +77,7 @@ func validCsrfToken(token string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newCsrfToken() string {
|
func newCsrfToken() string {
|
||||||
bs := make([]byte, 30)
|
token := randomString(30)
|
||||||
_, err := rand.Reader.Read(bs)
|
|
||||||
if err != nil {
|
|
||||||
l.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
token := base64.StdEncoding.EncodeToString(bs)
|
|
||||||
|
|
||||||
csrfMut.Lock()
|
csrfMut.Lock()
|
||||||
csrfTokens = append(csrfTokens, token)
|
csrfTokens = append(csrfTokens, token)
|
||||||
@@ -130,3 +129,13 @@ func loadCsrfTokens() {
|
|||||||
csrfTokens = append(csrfTokens, s.Text())
|
csrfTokens = append(csrfTokens, s.Text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func randomString(len int) string {
|
||||||
|
bs := make([]byte, len)
|
||||||
|
_, err := rand.Reader.Read(bs)
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(bs)
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func trackCPUUsage() {
|
|||||||
for _ = range time.NewTicker(time.Second).C {
|
for _ = range time.NewTicker(time.Second).C {
|
||||||
err := solarisPrusage(pid, &rusage)
|
err := solarisPrusage(pid, &rusage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warnln(err)
|
l.Warnln("getting prusage:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
curTime := time.Now().UnixNano()
|
curTime := time.Now().UnixNano()
|
||||||
|
|||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
// 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
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
go trackCPUUsage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func trackCPUUsage() {
|
||||||
|
handle, err := syscall.GetCurrentProcess()
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Cannot track CPU usage:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctime, etime, ktime, utime syscall.Filetime
|
||||||
|
err = syscall.GetProcessTimes(handle, &ctime, &etime, &ktime, &utime)
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Cannot track CPU usage:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
prevTime := ctime.Nanoseconds()
|
||||||
|
prevUsage := ktime.Nanoseconds() + utime.Nanoseconds() // Always overflows
|
||||||
|
|
||||||
|
for _ = range time.NewTicker(time.Second).C {
|
||||||
|
err := syscall.GetProcessTimes(handle, &ctime, &etime, &ktime, &utime)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
curTime := time.Now().UnixNano()
|
||||||
|
timeDiff := curTime - prevTime
|
||||||
|
curUsage := ktime.Nanoseconds() + utime.Nanoseconds()
|
||||||
|
usageDiff := curUsage - prevUsage
|
||||||
|
cpuUsageLock.Lock()
|
||||||
|
copy(cpuUsagePercent[1:], cpuUsagePercent[0:])
|
||||||
|
cpuUsagePercent[0] = 100 * float64(usageDiff) / float64(timeDiff)
|
||||||
|
cpuUsageLock.Unlock()
|
||||||
|
prevTime = curTime
|
||||||
|
prevUsage = curUsage
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"runtime/pprof"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if innerProcess && os.Getenv("STHEAPPROFILE") != "" {
|
||||||
|
l.Debugln("Starting heap profiling")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/juju/ratelimit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type limitedReader struct {
|
||||||
|
r io.Reader
|
||||||
|
bucket *ratelimit.Bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *limitedReader) Read(buf []byte) (int, error) {
|
||||||
|
n, err := r.r.Read(buf)
|
||||||
|
if r.bucket != nil {
|
||||||
|
r.bucket.Wait(int64(n))
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
+349
-220
@@ -16,7 +16,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -26,17 +25,19 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.google.com/p/go.crypto/bcrypt"
|
||||||
"github.com/juju/ratelimit"
|
"github.com/juju/ratelimit"
|
||||||
"github.com/syncthing/syncthing/config"
|
"github.com/syncthing/syncthing/internal/config"
|
||||||
"github.com/syncthing/syncthing/discover"
|
"github.com/syncthing/syncthing/internal/discover"
|
||||||
"github.com/syncthing/syncthing/events"
|
"github.com/syncthing/syncthing/internal/events"
|
||||||
"github.com/syncthing/syncthing/logger"
|
"github.com/syncthing/syncthing/internal/files"
|
||||||
"github.com/syncthing/syncthing/model"
|
"github.com/syncthing/syncthing/internal/logger"
|
||||||
"github.com/syncthing/syncthing/osutil"
|
"github.com/syncthing/syncthing/internal/model"
|
||||||
"github.com/syncthing/syncthing/protocol"
|
"github.com/syncthing/syncthing/internal/protocol"
|
||||||
"github.com/syncthing/syncthing/upgrade"
|
"github.com/syncthing/syncthing/internal/upgrade"
|
||||||
"github.com/syncthing/syncthing/upnp"
|
"github.com/syncthing/syncthing/internal/upnp"
|
||||||
"github.com/syndtr/goleveldb/leveldb"
|
"github.com/syndtr/goleveldb/leveldb"
|
||||||
|
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -47,9 +48,19 @@ var (
|
|||||||
BuildHost = "unknown"
|
BuildHost = "unknown"
|
||||||
BuildUser = "unknown"
|
BuildUser = "unknown"
|
||||||
LongVersion string
|
LongVersion string
|
||||||
|
GoArchExtra string // "", "v5", "v6", "v7"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
exitSuccess = 0
|
||||||
|
exitError = 1
|
||||||
|
exitNoUpgradeAvailable = 2
|
||||||
|
exitRestarting = 3
|
||||||
|
exitUpgrading = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
var l = logger.DefaultLogger
|
var l = logger.DefaultLogger
|
||||||
|
var innerProcess = os.Getenv("STNORESTART") != ""
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if Version != "unknown-dev" {
|
if Version != "unknown-dev" {
|
||||||
@@ -72,15 +83,16 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfg config.Configuration
|
cfg config.Configuration
|
||||||
myID protocol.NodeID
|
myID protocol.NodeID
|
||||||
confDir string
|
confDir string
|
||||||
logFlags int = log.Ltime
|
logFlags int = log.Ltime
|
||||||
rateBucket *ratelimit.Bucket
|
writeRateLimit *ratelimit.Bucket
|
||||||
stop = make(chan bool)
|
readRateLimit *ratelimit.Bucket
|
||||||
discoverer *discover.Discoverer
|
stop = make(chan int)
|
||||||
lockConn *net.TCPListener
|
discoverer *discover.Discoverer
|
||||||
lockPort int
|
externalPort int
|
||||||
|
cert tls.Certificate
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -99,13 +111,19 @@ show time only (2).
|
|||||||
|
|
||||||
The following enviroment variables are interpreted by syncthing:
|
The following enviroment variables are interpreted by syncthing:
|
||||||
|
|
||||||
|
STGUIADDRESS Override GUI listen address set in config. Expects protocol type
|
||||||
|
followed by hostname or an IP address, followed by a port, such
|
||||||
|
as "https://127.0.0.1:8888".
|
||||||
|
|
||||||
|
STGUIAUTH Override GUI authentication credentials set in config. Expects
|
||||||
|
a colon separated username and password, such as "admin:secret".
|
||||||
|
|
||||||
|
STGUIAPIKEY Override GUI API key set in config.
|
||||||
|
|
||||||
STNORESTART Do not attempt to restart when requested to, instead just exit.
|
STNORESTART Do not attempt to restart when requested to, instead just exit.
|
||||||
Set this variable when running under a service manager such as
|
Set this variable when running under a service manager such as
|
||||||
runit, launchd, etc.
|
runit, launchd, etc.
|
||||||
|
|
||||||
STPROFILER Set to a listen address such as "127.0.0.1:9090" to start the
|
|
||||||
profiler with HTTP access.
|
|
||||||
|
|
||||||
STTRACE A comma separated string of facilities to trace. The valid
|
STTRACE A comma separated string of facilities to trace. The valid
|
||||||
facility strings:
|
facility strings:
|
||||||
- "beacon" (the beacon package)
|
- "beacon" (the beacon package)
|
||||||
@@ -115,36 +133,57 @@ The following enviroment variables are interpreted by syncthing:
|
|||||||
- "net" (the main package; connections & network messages)
|
- "net" (the main package; connections & network messages)
|
||||||
- "model" (the model package)
|
- "model" (the model package)
|
||||||
- "scanner" (the scanner package)
|
- "scanner" (the scanner package)
|
||||||
|
- "stats" (the stats package)
|
||||||
- "upnp" (the upnp package)
|
- "upnp" (the upnp package)
|
||||||
- "xdr" (the xdr package)
|
- "xdr" (the xdr package)
|
||||||
- "all" (all of the above)
|
- "all" (all of the above)
|
||||||
|
|
||||||
STCPUPROFILE Write CPU profile to the specified file.
|
|
||||||
|
|
||||||
STGUIASSETS Directory to load GUI assets from. Overrides compiled in assets.
|
STGUIASSETS Directory to load GUI assets from. Overrides compiled in assets.
|
||||||
|
|
||||||
STDEADLOCKTIMEOUT Alter deadlock detection timeout (seconds; default 1200).`
|
STPROFILER Set to a listen address such as "127.0.0.1:9090" to start the
|
||||||
|
profiler with HTTP access.
|
||||||
|
|
||||||
|
STCPUPROFILE Write a CPU profile to cpu-$pid.pprof on exit.
|
||||||
|
|
||||||
|
STHEAPPROFILE Write heap profiles to heap-$pid-$timestamp.pprof each time
|
||||||
|
heap usage increases.
|
||||||
|
|
||||||
|
STPERFSTATS Write running performance statistics to perf-$pid.csv. Not
|
||||||
|
supported on Windows.
|
||||||
|
|
||||||
|
GOMAXPROCS Set the maximum number of CPU cores to use. Defaults to all
|
||||||
|
available CPU cores.`
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Command line options
|
||||||
|
var (
|
||||||
|
reset bool
|
||||||
|
showVersion bool
|
||||||
|
doUpgrade bool
|
||||||
|
doUpgradeCheck bool
|
||||||
|
noBrowser bool
|
||||||
|
generateDir string
|
||||||
|
guiAddress string
|
||||||
|
guiAuthentication string
|
||||||
|
guiAPIKey string
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var reset bool
|
|
||||||
var showVersion bool
|
|
||||||
var doUpgrade bool
|
|
||||||
var doUpgradeCheck bool
|
|
||||||
var generateDir string
|
|
||||||
var noBrowser bool
|
|
||||||
flag.StringVar(&confDir, "home", getDefaultConfDir(), "Set configuration directory")
|
flag.StringVar(&confDir, "home", getDefaultConfDir(), "Set configuration directory")
|
||||||
flag.BoolVar(&reset, "reset", false, "Prepare to resync from cluster")
|
flag.BoolVar(&reset, "reset", false, "Prepare to resync from cluster")
|
||||||
flag.BoolVar(&showVersion, "version", false, "Show version")
|
flag.BoolVar(&showVersion, "version", false, "Show version")
|
||||||
flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade")
|
flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade")
|
||||||
flag.BoolVar(&doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
|
flag.BoolVar(&doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
|
||||||
flag.BoolVar(&noBrowser, "no-browser", false, "Do not start browser")
|
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.StringVar(&generateDir, "generate", "", "Generate key in specified dir")
|
||||||
|
flag.StringVar(&guiAddress, "gui-address", "", "Override GUI address")
|
||||||
|
flag.StringVar(&guiAuthentication, "gui-authentication", "", "Override GUI authentication. Expects 'username:password'")
|
||||||
|
flag.StringVar(&guiAPIKey, "gui-apikey", "", "Override GUI API key")
|
||||||
|
flag.IntVar(&logFlags, "logflags", logFlags, "Set log flags")
|
||||||
flag.Usage = usageFor(flag.CommandLine, usage, extraUsage)
|
flag.Usage = usageFor(flag.CommandLine, usage, extraUsage)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -159,7 +198,9 @@ func main() {
|
|||||||
dir := expandTilde(generateDir)
|
dir := expandTilde(generateDir)
|
||||||
|
|
||||||
info, err := os.Stat(dir)
|
info, err := os.Stat(dir)
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
|
l.Fatalln("generate:", err)
|
||||||
|
}
|
||||||
if !info.IsDir() {
|
if !info.IsDir() {
|
||||||
l.Fatalln(dir, "is not a directory")
|
l.Fatalln(dir, "is not a directory")
|
||||||
}
|
}
|
||||||
@@ -173,13 +214,24 @@ func main() {
|
|||||||
|
|
||||||
newCertificate(dir, "")
|
newCertificate(dir, "")
|
||||||
cert, err = loadCert(dir, "")
|
cert, err = loadCert(dir, "")
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
|
l.Fatalln("load cert:", err)
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
l.Infoln("Node ID:", protocol.NewNodeID(cert.Certificate[0]))
|
l.Infoln("Node ID:", protocol.NewNodeID(cert.Certificate[0]))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
confDir = expandTilde(confDir)
|
||||||
|
|
||||||
|
if info, err := os.Stat(confDir); err == nil && !info.IsDir() {
|
||||||
|
l.Fatalln("Config directory", confDir, "is not a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that our home directory exists.
|
||||||
|
ensureDir(confDir, 0700)
|
||||||
|
|
||||||
if doUpgrade || doUpgradeCheck {
|
if doUpgrade || doUpgradeCheck {
|
||||||
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
|
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -188,13 +240,19 @@ func main() {
|
|||||||
|
|
||||||
if upgrade.CompareVersions(rel.Tag, Version) <= 0 {
|
if upgrade.CompareVersions(rel.Tag, Version) <= 0 {
|
||||||
l.Infof("No upgrade available (current %q >= latest %q).", Version, rel.Tag)
|
l.Infof("No upgrade available (current %q >= latest %q).", Version, rel.Tag)
|
||||||
os.Exit(2)
|
os.Exit(exitNoUpgradeAvailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Infof("Upgrade available (current %q < latest %q)", Version, rel.Tag)
|
l.Infof("Upgrade available (current %q < latest %q)", Version, rel.Tag)
|
||||||
|
|
||||||
if doUpgrade {
|
if doUpgrade {
|
||||||
err = upgrade.UpgradeTo(rel)
|
// Use leveldb database locks to protect against concurrent upgrades
|
||||||
|
_, err = leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{CachedOpenFiles: 100})
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("Cannot upgrade, database seems to be locked. Is another copy of Syncthing already running?")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = upgrade.UpgradeTo(rel, GoArchExtra)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("Upgrade:", err) // exits 1
|
l.Fatalln("Upgrade:", err) // exits 1
|
||||||
}
|
}
|
||||||
@@ -205,12 +263,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
if reset {
|
||||||
lockPort, err = getLockPort()
|
resetRepositories()
|
||||||
if err != nil {
|
return
|
||||||
l.Fatalln("Opening lock port:", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("STNORESTART") != "" {
|
||||||
|
syncthingMain()
|
||||||
|
} else {
|
||||||
|
monitorMain()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncthingMain() {
|
||||||
|
var err error
|
||||||
|
|
||||||
if len(os.Getenv("GOGC")) == 0 {
|
if len(os.Getenv("GOGC")) == 0 {
|
||||||
debug.SetGCPercent(25)
|
debug.SetGCPercent(25)
|
||||||
}
|
}
|
||||||
@@ -219,11 +286,9 @@ func main() {
|
|||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
}
|
}
|
||||||
|
|
||||||
confDir = expandTilde(confDir)
|
|
||||||
|
|
||||||
events.Default.Log(events.Starting, map[string]string{"home": confDir})
|
events.Default.Log(events.Starting, map[string]string{"home": confDir})
|
||||||
|
|
||||||
if _, err := os.Stat(confDir); err != nil && confDir == getDefaultConfDir() {
|
if _, err = os.Stat(confDir); err != nil && confDir == getDefaultConfDir() {
|
||||||
// We are supposed to use the default configuration directory. It
|
// We are supposed to use the default configuration directory. It
|
||||||
// doesn't exist. In the past our default has been ~/.syncthing, so if
|
// doesn't exist. In the past our default has been ~/.syncthing, so if
|
||||||
// that directory exists we move it to the new default location and
|
// that directory exists we move it to the new default location and
|
||||||
@@ -244,14 +309,14 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that our home directory exists and that we have a certificate and key.
|
// Ensure that that we have a certificate and key.
|
||||||
|
cert, err = loadCert(confDir, "")
|
||||||
ensureDir(confDir, 0700)
|
|
||||||
cert, err := loadCert(confDir, "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
newCertificate(confDir, "")
|
newCertificate(confDir, "")
|
||||||
cert, err = loadCert(confDir, "")
|
cert, err = loadCert(confDir, "")
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
|
l.Fatalln("load cert:", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
myID = protocol.NewNodeID(cert.Certificate[0])
|
myID = protocol.NewNodeID(cert.Certificate[0])
|
||||||
@@ -263,62 +328,58 @@ func main() {
|
|||||||
// Prepare to be able to save configuration
|
// Prepare to be able to save configuration
|
||||||
|
|
||||||
cfgFile := filepath.Join(confDir, "config.xml")
|
cfgFile := filepath.Join(confDir, "config.xml")
|
||||||
go saveConfigLoop(cfgFile)
|
|
||||||
|
var myName string
|
||||||
|
|
||||||
// Load the configuration file, if it exists.
|
// Load the configuration file, if it exists.
|
||||||
// If it does not, create a template.
|
// If it does not, create a template.
|
||||||
|
|
||||||
cf, err := os.Open(cfgFile)
|
cfg, err = config.Load(cfgFile, myID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Read config.xml
|
myCfg := cfg.GetNodeConfiguration(myID)
|
||||||
cfg, err = config.Load(cf, myID)
|
if myCfg == nil || myCfg.Name == "" {
|
||||||
if err != nil {
|
myName, _ = os.Hostname()
|
||||||
l.Fatalln(err)
|
} else {
|
||||||
|
myName = myCfg.Name
|
||||||
}
|
}
|
||||||
cf.Close()
|
|
||||||
} else {
|
} else {
|
||||||
l.Infoln("No config file; starting with empty defaults")
|
l.Infoln("No config file; starting with empty defaults")
|
||||||
name, _ := os.Hostname()
|
myName, _ = os.Hostname()
|
||||||
defaultRepo := filepath.Join(getHomeDir(), "Sync")
|
defaultRepo := filepath.Join(getHomeDir(), "Sync")
|
||||||
ensureDir(defaultRepo, 0755)
|
|
||||||
|
|
||||||
cfg, err = config.Load(nil, myID)
|
cfg = config.New(cfgFile, myID)
|
||||||
cfg.Repositories = []config.RepositoryConfiguration{
|
cfg.Repositories = []config.RepositoryConfiguration{
|
||||||
{
|
{
|
||||||
ID: "default",
|
ID: "default",
|
||||||
Directory: defaultRepo,
|
Directory: defaultRepo,
|
||||||
Nodes: []config.NodeConfiguration{{NodeID: myID}},
|
RescanIntervalS: 60,
|
||||||
|
Nodes: []config.RepositoryNodeConfiguration{{NodeID: myID}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cfg.Nodes = []config.NodeConfiguration{
|
cfg.Nodes = []config.NodeConfiguration{
|
||||||
{
|
{
|
||||||
NodeID: myID,
|
NodeID: myID,
|
||||||
Addresses: []string{"dynamic"},
|
Addresses: []string{"dynamic"},
|
||||||
Name: name,
|
Name: myName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
port, err := getFreePort("127.0.0.1", 8080)
|
port, err := getFreePort("127.0.0.1", 8080)
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
|
l.Fatalln("get free port (GUI):", err)
|
||||||
|
}
|
||||||
cfg.GUI.Address = fmt.Sprintf("127.0.0.1:%d", port)
|
cfg.GUI.Address = fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
|
|
||||||
port, err = getFreePort("0.0.0.0", 22000)
|
port, err = getFreePort("0.0.0.0", 22000)
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
|
l.Fatalln("get free port (BEP):", err)
|
||||||
|
}
|
||||||
cfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)}
|
cfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)}
|
||||||
|
|
||||||
saveConfig()
|
cfg.Save()
|
||||||
l.Infof("Edit %s to taste or use the GUI\n", cfgFile)
|
l.Infof("Edit %s to taste or use the GUI\n", cfgFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if reset {
|
|
||||||
resetRepositories()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(os.Getenv("STRESTART")) > 0 {
|
|
||||||
waitForParentExit()
|
|
||||||
}
|
|
||||||
|
|
||||||
if profiler := os.Getenv("STPROFILER"); len(profiler) > 0 {
|
if profiler := os.Getenv("STPROFILER"); len(profiler) > 0 {
|
||||||
go func() {
|
go func() {
|
||||||
l.Debugln("Starting profiler on", profiler)
|
l.Debugln("Starting profiler on", profiler)
|
||||||
@@ -343,61 +404,77 @@ func main() {
|
|||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the write rate should be limited, set up a rate limiter for it.
|
// If the read or write rate should be limited, set up a rate limiter for it.
|
||||||
// This will be used on connections created in the connect and listen routines.
|
// This will be used on connections created in the connect and listen routines.
|
||||||
|
|
||||||
if cfg.Options.MaxSendKbps > 0 {
|
if cfg.Options.MaxSendKbps > 0 {
|
||||||
rateBucket = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps))
|
writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps))
|
||||||
|
}
|
||||||
|
if cfg.Options.MaxRecvKbps > 0 {
|
||||||
|
readRateLimit = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxRecvKbps), int64(5*1000*cfg.Options.MaxRecvKbps))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is the first time the user runs v0.9, archive the old indexes and config.
|
// If this is the first time the user runs v0.9, archive the old indexes and config.
|
||||||
archiveLegacyConfig()
|
archiveLegacyConfig()
|
||||||
|
|
||||||
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), nil)
|
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{CachedOpenFiles: 100})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("leveldb.OpenFile():", err)
|
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
|
||||||
}
|
}
|
||||||
m := model.NewModel(confDir, &cfg, "syncthing", Version, db)
|
|
||||||
|
// Remove database entries for repos that no longer exist in the config
|
||||||
|
repoMap := cfg.RepoMap()
|
||||||
|
for _, repo := range files.ListRepos(db) {
|
||||||
|
if _, ok := repoMap[repo]; !ok {
|
||||||
|
l.Infof("Cleaning data for dropped repo %q", repo)
|
||||||
|
files.DropRepo(db, repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m := model.NewModel(confDir, &cfg, myName, "syncthing", Version, db)
|
||||||
|
|
||||||
nextRepo:
|
nextRepo:
|
||||||
for i, repo := range cfg.Repositories {
|
for i, repo := range cfg.Repositories {
|
||||||
if repo.Invalid != "" {
|
if repo.Invalid != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
repo.Directory = expandTilde(repo.Directory)
|
repo.Directory = expandTilde(repo.Directory)
|
||||||
|
m.AddRepo(repo)
|
||||||
|
|
||||||
fi, err := os.Stat(repo.Directory)
|
fi, err := os.Stat(repo.Directory)
|
||||||
if m.LocalVersion(repo.ID) > 0 {
|
if m.CurrentLocalVersion(repo.ID) > 0 {
|
||||||
// Safety check. If the cached index contains files but the
|
// Safety check. If the cached index contains files but the
|
||||||
// repository doesn't exist, we have a problem. We would assume
|
// repository doesn't exist, we have a problem. We would assume
|
||||||
// that all files have been deleted which might not be the case,
|
// that all files have been deleted which might not be the case,
|
||||||
// so mark it as invalid instead.
|
// so mark it as invalid instead.
|
||||||
if err != nil || !fi.IsDir() {
|
if err != nil || !fi.IsDir() {
|
||||||
|
l.Warnf("Stopping repository %q - directory missing, but has files in index", repo.ID)
|
||||||
cfg.Repositories[i].Invalid = "repo directory missing"
|
cfg.Repositories[i].Invalid = "repo directory missing"
|
||||||
continue nextRepo
|
continue nextRepo
|
||||||
}
|
}
|
||||||
} else if os.IsNotExist(err) {
|
} else if os.IsNotExist(err) {
|
||||||
// If we don't have ny files in the index, and the directory does
|
// If we don't have any files in the index, and the directory
|
||||||
// exist, try creating it.
|
// doesn't exist, try creating it.
|
||||||
err = os.MkdirAll(repo.Directory, 0700)
|
err = os.MkdirAll(repo.Directory, 0700)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If there was another error or we could not create the
|
// If there was another error or we could not create the
|
||||||
// directory, the repository is invalid.
|
// directory, the repository is invalid.
|
||||||
|
l.Warnf("Stopping repository %q - %v", err)
|
||||||
cfg.Repositories[i].Invalid = err.Error()
|
cfg.Repositories[i].Invalid = err.Error()
|
||||||
continue nextRepo
|
continue nextRepo
|
||||||
}
|
}
|
||||||
|
|
||||||
m.AddRepo(repo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GUI
|
// GUI
|
||||||
if cfg.GUI.Enabled && cfg.GUI.Address != "" {
|
|
||||||
addr, err := net.ResolveTCPAddr("tcp", cfg.GUI.Address)
|
guiCfg := overrideGUIConfig(cfg.GUI, guiAddress, guiAuthentication, guiAPIKey)
|
||||||
|
|
||||||
|
if guiCfg.Enabled && guiCfg.Address != "" {
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", guiCfg.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalf("Cannot start GUI on %q: %v", cfg.GUI.Address, err)
|
l.Fatalf("Cannot start GUI on %q: %v", guiCfg.Address, err)
|
||||||
} else {
|
} else {
|
||||||
var hostOpen, hostShow string
|
var hostOpen, hostShow string
|
||||||
switch {
|
switch {
|
||||||
@@ -413,17 +490,19 @@ nextRepo:
|
|||||||
}
|
}
|
||||||
|
|
||||||
var proto = "http"
|
var proto = "http"
|
||||||
if cfg.GUI.UseTLS {
|
if guiCfg.UseTLS {
|
||||||
proto = "https"
|
proto = "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Infof("Starting web GUI on %s://%s:%d/", proto, hostShow, addr.Port)
|
urlShow := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
|
||||||
err := startGUI(cfg.GUI, os.Getenv("STGUIASSETS"), m)
|
l.Infoln("Starting web GUI on", urlShow)
|
||||||
|
err := startGUI(guiCfg, os.Getenv("STGUIASSETS"), m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("Cannot start GUI:", err)
|
l.Fatalln("Cannot start GUI:", err)
|
||||||
}
|
}
|
||||||
if !noBrowser && 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))
|
urlOpen := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostOpen, strconv.Itoa(addr.Port)))
|
||||||
|
openURL(urlOpen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,7 +511,13 @@ nextRepo:
|
|||||||
// start needing a bunch of files which are nowhere to be found. This
|
// start needing a bunch of files which are nowhere to be found. This
|
||||||
// needs to be changed when we correctly do persistent indexes.
|
// needs to be changed when we correctly do persistent indexes.
|
||||||
for _, repoCfg := range cfg.Repositories {
|
for _, repoCfg := range cfg.Repositories {
|
||||||
|
if repoCfg.Invalid != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
for _, node := range repoCfg.NodeIDs() {
|
for _, node := range repoCfg.NodeIDs() {
|
||||||
|
if node == myID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
m.Index(node, repoCfg.ID, nil)
|
m.Index(node, repoCfg.ID, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,13 +552,18 @@ nextRepo:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The default port we announce, possibly modified by setupUPnP next.
|
||||||
|
|
||||||
|
addr, err := net.ResolveTCPAddr("tcp", cfg.Options.ListenAddress[0])
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("Bad listen address:", err)
|
||||||
|
}
|
||||||
|
externalPort = addr.Port
|
||||||
|
|
||||||
// UPnP
|
// UPnP
|
||||||
|
|
||||||
var externalPort = 0
|
|
||||||
if cfg.Options.UPnPEnabled {
|
if cfg.Options.UPnPEnabled {
|
||||||
// We seed the random number generator with the node ID to get a
|
setupUPnP()
|
||||||
// repeatable sequence of random external ports.
|
|
||||||
externalPort = setupUPnP(rand.NewSource(certSeed(cert.Certificate[0])))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Routine to connect out to configured nodes
|
// Routine to connect out to configured nodes
|
||||||
@@ -492,12 +582,12 @@ nextRepo:
|
|||||||
m.StartRepoRO(repo.ID)
|
m.StartRepoRO(repo.ID)
|
||||||
} else {
|
} else {
|
||||||
l.Okf("Ready to synchronize %s (read-write)", repo.ID)
|
l.Okf("Ready to synchronize %s (read-write)", repo.ID)
|
||||||
m.StartRepoRW(repo.ID, cfg.Options.ParallelRequests)
|
m.StartRepoRW(repo.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cpuprof := os.Getenv("STCPUPROFILE"); len(cpuprof) > 0 {
|
if cpuprof := os.Getenv("STCPUPROFILE"); len(cpuprof) > 0 {
|
||||||
f, err := os.Create(cpuprof)
|
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -526,12 +616,17 @@ nextRepo:
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Options.RestartOnWakeup {
|
||||||
|
go standbyMonitor()
|
||||||
|
}
|
||||||
|
|
||||||
events.Default.Log(events.StartupComplete, nil)
|
events.Default.Log(events.StartupComplete, nil)
|
||||||
go generateEvents()
|
go generateEvents()
|
||||||
|
|
||||||
<-stop
|
code := <-stop
|
||||||
|
|
||||||
l.Okln("Exiting")
|
l.Okln("Exiting")
|
||||||
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateEvents() {
|
func generateEvents() {
|
||||||
@@ -541,27 +636,7 @@ func generateEvents() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitForParentExit() {
|
func setupUPnP() {
|
||||||
l.Infoln("Waiting for parent to exit...")
|
|
||||||
lockPortStr := os.Getenv("STRESTART")
|
|
||||||
lockPort, err := strconv.Atoi(lockPortStr)
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("Invalid lock port %q: %v", lockPortStr, err)
|
|
||||||
}
|
|
||||||
// Wait for the listen address to become free, indicating that the parent has exited.
|
|
||||||
for {
|
|
||||||
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", lockPort))
|
|
||||||
if err == nil {
|
|
||||||
ln.Close()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(250 * time.Millisecond)
|
|
||||||
}
|
|
||||||
l.Infoln("Continuing")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupUPnP(r rand.Source) int {
|
|
||||||
var externalPort = 0
|
|
||||||
if len(cfg.Options.ListenAddress) == 1 {
|
if len(cfg.Options.ListenAddress) == 1 {
|
||||||
_, portStr, err := net.SplitHostPort(cfg.Options.ListenAddress[0])
|
_, portStr, err := net.SplitHostPort(cfg.Options.ListenAddress[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -571,17 +646,11 @@ func setupUPnP(r rand.Source) int {
|
|||||||
port, _ := strconv.Atoi(portStr)
|
port, _ := strconv.Atoi(portStr)
|
||||||
igd, err := upnp.Discover()
|
igd, err := upnp.Discover()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for i := 0; i < 10; i++ {
|
externalPort = setupExternalPort(igd, port)
|
||||||
r := 1024 + int(r.Int63()%(65535-1024))
|
|
||||||
err := igd.AddPortMapping(upnp.TCP, r, port, "syncthing", 0)
|
|
||||||
if err == nil {
|
|
||||||
externalPort = r
|
|
||||||
l.Infoln("Created UPnP port mapping - external port", externalPort)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if externalPort == 0 {
|
if externalPort == 0 {
|
||||||
l.Warnln("Failed to create UPnP port mapping")
|
l.Warnln("Failed to create UPnP port mapping")
|
||||||
|
} else {
|
||||||
|
l.Infoln("Created UPnP port mapping - external port", externalPort)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
l.Infof("No UPnP gateway detected")
|
l.Infof("No UPnP gateway detected")
|
||||||
@@ -589,11 +658,60 @@ func setupUPnP(r rand.Source) int {
|
|||||||
l.Debugf("UPnP: %v", err)
|
l.Debugf("UPnP: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if cfg.Options.UPnPRenewal > 0 {
|
||||||
|
go renewUPnP(port)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
l.Warnln("Multiple listening addresses; not attempting UPnP port mapping")
|
l.Warnln("Multiple listening addresses; not attempting UPnP port mapping")
|
||||||
}
|
}
|
||||||
return externalPort
|
}
|
||||||
|
|
||||||
|
func setupExternalPort(igd *upnp.IGD, port int) int {
|
||||||
|
// We seed the random number generator with the node ID to get a
|
||||||
|
// repeatable sequence of random external ports.
|
||||||
|
rnd := rand.NewSource(certSeed(cert.Certificate[0]))
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
r := 1024 + int(rnd.Int63()%(65535-1024))
|
||||||
|
err := igd.AddPortMapping(upnp.TCP, r, port, "syncthing", cfg.Options.UPnPLease*60)
|
||||||
|
if err == nil {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func renewUPnP(port int) {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Duration(cfg.Options.UPnPRenewal) * time.Minute)
|
||||||
|
|
||||||
|
igd, err := upnp.Discover()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just renew the same port that we already have
|
||||||
|
if externalPort != 0 {
|
||||||
|
err = igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", cfg.Options.UPnPLease*60)
|
||||||
|
if err == nil {
|
||||||
|
l.Infoln("Renewed UPnP port mapping - external port", externalPort)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Something strange has happened. We didn't have an external port before?
|
||||||
|
// Or perhaps the gateway has changed?
|
||||||
|
// Retry the same port sequence from the beginning.
|
||||||
|
r := setupExternalPort(igd, port)
|
||||||
|
if r != 0 {
|
||||||
|
externalPort = r
|
||||||
|
l.Infoln("Updated UPnP port mapping - external port", externalPort)
|
||||||
|
discoverer.StopGlobal()
|
||||||
|
discoverer.StartGlobal(cfg.Options.GlobalAnnServer, uint16(r))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.Warnln("Failed to update UPnP port mapping - external port", externalPort)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetRepositories() {
|
func resetRepositories() {
|
||||||
@@ -638,7 +756,7 @@ func archiveLegacyConfig() {
|
|||||||
l.Warnf("Cannot archive config:", err)
|
l.Warnf("Cannot archive config:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer src.Close()
|
defer dst.Close()
|
||||||
|
|
||||||
l.Infoln("Archiving config.xml")
|
l.Infoln("Archiving config.xml")
|
||||||
io.Copy(dst, src)
|
io.Copy(dst, src)
|
||||||
@@ -647,74 +765,12 @@ func archiveLegacyConfig() {
|
|||||||
|
|
||||||
func restart() {
|
func restart() {
|
||||||
l.Infoln("Restarting")
|
l.Infoln("Restarting")
|
||||||
if os.Getenv("SMF_FMRI") != "" || os.Getenv("STNORESTART") != "" {
|
stop <- exitRestarting
|
||||||
// Solaris SMF
|
|
||||||
l.Infoln("Service manager detected; exit instead of restart")
|
|
||||||
stop <- true
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
env := os.Environ()
|
|
||||||
newEnv := make([]string, 0, len(env))
|
|
||||||
for _, s := range env {
|
|
||||||
if !strings.HasPrefix(s, "STRESTART=") {
|
|
||||||
newEnv = append(newEnv, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newEnv = append(newEnv, fmt.Sprintf("STRESTART=%d", lockPort))
|
|
||||||
|
|
||||||
pgm, err := exec.LookPath(os.Args[0])
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("Cannot restart:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
proc, err := os.StartProcess(pgm, os.Args, &os.ProcAttr{
|
|
||||||
Env: newEnv,
|
|
||||||
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
l.Fatalln(err)
|
|
||||||
}
|
|
||||||
proc.Release()
|
|
||||||
stop <- true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func shutdown() {
|
func shutdown() {
|
||||||
stop <- true
|
l.Infoln("Shutting down")
|
||||||
}
|
stop <- exitSuccess
|
||||||
|
|
||||||
var saveConfigCh = make(chan struct{})
|
|
||||||
|
|
||||||
func saveConfigLoop(cfgFile string) {
|
|
||||||
for _ = range saveConfigCh {
|
|
||||||
fd, err := os.Create(cfgFile + ".tmp")
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("Saving config:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = config.Save(fd, cfg)
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("Saving config:", err)
|
|
||||||
fd.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = fd.Close()
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("Saving config:", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err = osutil.Rename(cfgFile+".tmp", cfgFile)
|
|
||||||
if err != nil {
|
|
||||||
l.Warnln("Saving config:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveConfig() {
|
|
||||||
saveConfigCh <- struct{}{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func listenConnect(myID protocol.NodeID, m *model.Model, tlsCfg *tls.Config) {
|
func listenConnect(myID protocol.NodeID, m *model.Model, tlsCfg *tls.Config) {
|
||||||
@@ -770,15 +826,20 @@ next:
|
|||||||
continue next
|
continue next
|
||||||
}
|
}
|
||||||
|
|
||||||
// If rate limiting is set, we wrap the write side of the
|
// If rate limiting is set, we wrap the connection in a
|
||||||
// connection in a limiter.
|
// limiter.
|
||||||
var wr io.Writer = conn
|
var wr io.Writer = conn
|
||||||
if rateBucket != nil {
|
if writeRateLimit != nil {
|
||||||
wr = &limitedWriter{conn, rateBucket}
|
wr = &limitedWriter{conn, writeRateLimit}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rd io.Reader = conn
|
||||||
|
if readRateLimit != nil {
|
||||||
|
rd = &limitedReader{conn, readRateLimit}
|
||||||
}
|
}
|
||||||
|
|
||||||
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
|
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
|
||||||
protoConn := protocol.NewConnection(remoteID, conn, wr, m, name, nodeCfg.Compression)
|
protoConn := protocol.NewConnection(remoteID, rd, wr, m, name, nodeCfg.Compression)
|
||||||
|
|
||||||
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
||||||
if debugNet {
|
if debugNet {
|
||||||
@@ -794,6 +855,10 @@ next:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
events.Default.Log(events.NodeRejected, map[string]string{
|
||||||
|
"node": remoteID.String(),
|
||||||
|
"address": conn.RemoteAddr().String(),
|
||||||
|
})
|
||||||
l.Infof("Connection from %s with unknown node ID %s; ignoring", conn.RemoteAddr(), remoteID)
|
l.Infof("Connection from %s with unknown node ID %s; ignoring", conn.RemoteAddr(), remoteID)
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
@@ -805,9 +870,13 @@ func listenTLS(conns chan *tls.Conn, addr string, tlsCfg *tls.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tcaddr, err := net.ResolveTCPAddr("tcp", addr)
|
tcaddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
|
l.Fatalln("listen (BEP):", err)
|
||||||
|
}
|
||||||
listener, err := net.ListenTCP("tcp", tcaddr)
|
listener, err := net.ListenTCP("tcp", tcaddr)
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
|
l.Fatalln("listen (BEP):", err)
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
@@ -933,19 +1002,15 @@ func setTCPOptions(conn *net.TCPConn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func discovery(extPort int) *discover.Discoverer {
|
func discovery(extPort int) *discover.Discoverer {
|
||||||
disc, err := discover.NewDiscoverer(myID, cfg.Options.ListenAddress, cfg.Options.LocalAnnPort)
|
disc := discover.NewDiscoverer(myID, cfg.Options.ListenAddress)
|
||||||
if err != nil {
|
|
||||||
l.Warnf("No discovery possible (%v)", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Options.LocalAnnEnabled {
|
if cfg.Options.LocalAnnEnabled {
|
||||||
l.Infoln("Sending local discovery announcements")
|
l.Infoln("Starting local discovery announcements")
|
||||||
disc.StartLocal()
|
disc.StartLocal(cfg.Options.LocalAnnPort, cfg.Options.LocalAnnMCAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Options.GlobalAnnEnabled {
|
if cfg.Options.GlobalAnnEnabled {
|
||||||
l.Infoln("Sending global discovery announcements")
|
l.Infoln("Starting global discovery announcements")
|
||||||
disc.StartGlobal(cfg.Options.GlobalAnnServer, uint16(extPort))
|
disc.StartGlobal(cfg.Options.GlobalAnnServer, uint16(extPort))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -956,10 +1021,15 @@ func ensureDir(dir string, mode int) {
|
|||||||
fi, err := os.Stat(dir)
|
fi, err := os.Stat(dir)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
err := os.MkdirAll(dir, 0700)
|
err := os.MkdirAll(dir, 0700)
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
|
l.Fatalln(err)
|
||||||
|
}
|
||||||
} else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
|
} else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
|
||||||
err := os.Chmod(dir, os.FileMode(mode))
|
err := os.Chmod(dir, os.FileMode(mode))
|
||||||
l.FatalErr(err)
|
// This can fail on crappy filesystems, nothing we can do about it.
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1034,12 +1104,71 @@ func getFreePort(host string, ports ...int) (int, error) {
|
|||||||
return addr.Port, nil
|
return addr.Port, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLockPort() (int, error) {
|
func overrideGUIConfig(originalCfg config.GUIConfiguration, address, authentication, apikey string) config.GUIConfiguration {
|
||||||
var err error
|
// Make a copy of the config
|
||||||
lockConn, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.IP{127, 0, 0, 1}})
|
cfg := originalCfg
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
if address == "" {
|
||||||
|
address = os.Getenv("STGUIADDRESS")
|
||||||
|
}
|
||||||
|
|
||||||
|
if address != "" {
|
||||||
|
cfg.Enabled = true
|
||||||
|
|
||||||
|
addressParts := strings.SplitN(address, "://", 2)
|
||||||
|
switch addressParts[0] {
|
||||||
|
case "http":
|
||||||
|
cfg.UseTLS = false
|
||||||
|
case "https":
|
||||||
|
cfg.UseTLS = true
|
||||||
|
default:
|
||||||
|
l.Fatalln("Unidentified protocol", addressParts[0])
|
||||||
|
}
|
||||||
|
cfg.Address = addressParts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if authentication == "" {
|
||||||
|
authentication = os.Getenv("STGUIAUTH")
|
||||||
|
}
|
||||||
|
|
||||||
|
if authentication != "" {
|
||||||
|
authenticationParts := strings.SplitN(authentication, ":", 2)
|
||||||
|
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(authenticationParts[1]), 0)
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("Invalid GUI password:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.User = authenticationParts[0]
|
||||||
|
cfg.Password = string(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if apikey == "" {
|
||||||
|
apikey = os.Getenv("STGUIAPIKEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
if apikey != "" {
|
||||||
|
cfg.APIKey = apikey
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func standbyMonitor() {
|
||||||
|
restartDelay := time.Duration(60 * time.Second)
|
||||||
|
now := time.Now()
|
||||||
|
for {
|
||||||
|
time.Sleep(10 * time.Second)
|
||||||
|
if time.Since(now) > 2*time.Minute {
|
||||||
|
l.Infof("Paused state detected, possibly woke up from standby. Restarting in %v.", restartDelay)
|
||||||
|
|
||||||
|
// We most likely just woke from standby. If we restart
|
||||||
|
// immediately chances are we won't have networking ready. Give
|
||||||
|
// things a moment to stabilize.
|
||||||
|
time.Sleep(restartDelay)
|
||||||
|
|
||||||
|
restart()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now = time.Now()
|
||||||
}
|
}
|
||||||
addr := lockConn.Addr().(*net.TCPAddr)
|
|
||||||
return addr.Port, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +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.
|
||||||
|
|
||||||
|
package main_test
|
||||||
|
|
||||||
|
// Empty test file to generate 0% coverage rather than no coverage
|
||||||
@@ -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.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -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 solaris
|
// +build solaris
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|||||||
@@ -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 freebsd
|
// +build freebsd
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
// 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
stdoutFirstLines []string // The first 10 lines of stdout
|
||||||
|
stdoutLastLines []string // The last 50 lines of stdout
|
||||||
|
stdoutMut sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
countRestarts = 5
|
||||||
|
loopThreshold = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func monitorMain() {
|
||||||
|
os.Setenv("STNORESTART", "yes")
|
||||||
|
l.SetPrefix("[monitor] ")
|
||||||
|
|
||||||
|
args := os.Args
|
||||||
|
var restarts [countRestarts]time.Time
|
||||||
|
|
||||||
|
sign := make(chan os.Signal, 1)
|
||||||
|
sigTerm := syscall.Signal(0xf)
|
||||||
|
signal.Notify(sign, os.Interrupt, sigTerm, os.Kill)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if t := time.Since(restarts[0]); t < loopThreshold {
|
||||||
|
l.Warnf("%d restarts in %v; not retrying further", countRestarts, t)
|
||||||
|
os.Exit(exitError)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(restarts[0:], restarts[1:])
|
||||||
|
restarts[len(restarts)-1] = time.Now()
|
||||||
|
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
|
||||||
|
stderr, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Infoln("Starting syncthing")
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdoutMut.Lock()
|
||||||
|
stdoutFirstLines = make([]string, 0, 10)
|
||||||
|
stdoutLastLines = make([]string, 0, 50)
|
||||||
|
stdoutMut.Unlock()
|
||||||
|
|
||||||
|
go copyStderr(stderr)
|
||||||
|
go copyStdout(stdout)
|
||||||
|
|
||||||
|
exit := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
exit <- cmd.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case s := <-sign:
|
||||||
|
l.Infof("Signal %d received; exiting", s)
|
||||||
|
cmd.Process.Kill()
|
||||||
|
<-exit
|
||||||
|
return
|
||||||
|
|
||||||
|
case err = <-exit:
|
||||||
|
if err == nil {
|
||||||
|
// Successfull exit indicates an intentional shutdown
|
||||||
|
return
|
||||||
|
} else if exiterr, ok := err.(*exec.ExitError); ok {
|
||||||
|
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||||
|
switch status.ExitStatus() {
|
||||||
|
case exitUpgrading:
|
||||||
|
// Restart the monitor process to release the .old
|
||||||
|
// binary as part of the upgrade process.
|
||||||
|
l.Infoln("Restarting monitor...")
|
||||||
|
os.Setenv("STNORESTART", "")
|
||||||
|
err := exec.Command(args[0], args[1:]...).Start()
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("restart:", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Infoln("Syncthing exited:", err)
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
// Let the next child process know that this is not the first time
|
||||||
|
// it's starting up.
|
||||||
|
os.Setenv("STRESTART", "yes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyStderr(stderr io.ReadCloser) {
|
||||||
|
br := bufio.NewReader(stderr)
|
||||||
|
|
||||||
|
var panicFd *os.File
|
||||||
|
for {
|
||||||
|
line, err := br.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if panicFd == nil {
|
||||||
|
os.Stderr.WriteString(line)
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") {
|
||||||
|
panicFd, err = os.Create(filepath.Join(confDir, time.Now().Format("panic-20060102-150405.log")))
|
||||||
|
if err != nil {
|
||||||
|
l.Warnln("Create panic log:", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Warnf("Panic detected, writing to \"%s\"", panicFd.Name())
|
||||||
|
l.Warnln("Please create an issue at https://github.com/syncthing/syncthing/issues/ with the panic log attached")
|
||||||
|
|
||||||
|
panicFd.WriteString("Panic at " + time.Now().Format(time.RFC1123) + "\n")
|
||||||
|
stdoutMut.Lock()
|
||||||
|
for _, line := range stdoutFirstLines {
|
||||||
|
panicFd.WriteString(line)
|
||||||
|
}
|
||||||
|
panicFd.WriteString("...\n")
|
||||||
|
for _, line := range stdoutLastLines {
|
||||||
|
panicFd.WriteString(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if panicFd != nil {
|
||||||
|
panicFd.WriteString(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyStdout(stderr io.ReadCloser) {
|
||||||
|
br := bufio.NewReader(stderr)
|
||||||
|
for {
|
||||||
|
line, err := br.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stdoutMut.Lock()
|
||||||
|
if len(stdoutFirstLines) < cap(stdoutFirstLines) {
|
||||||
|
stdoutFirstLines = append(stdoutFirstLines, line)
|
||||||
|
}
|
||||||
|
if l := len(stdoutLastLines); l == cap(stdoutLastLines) {
|
||||||
|
stdoutLastLines = stdoutLastLines[:l-1]
|
||||||
|
}
|
||||||
|
stdoutLastLines = append(stdoutLastLines, line)
|
||||||
|
stdoutMut.Unlock()
|
||||||
|
|
||||||
|
os.Stdout.WriteString(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// +build perfstats
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func savePerfStats(file string) {
|
|
||||||
fd, err := os.Create(file)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var prevUsage int64
|
|
||||||
var prevTime int64
|
|
||||||
var rusage syscall.Rusage
|
|
||||||
var memstats runtime.MemStats
|
|
||||||
|
|
||||||
t0 := time.Now()
|
|
||||||
for t := range time.NewTicker(250 * time.Millisecond).C {
|
|
||||||
syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
|
|
||||||
curTime := time.Now().UnixNano()
|
|
||||||
timeDiff := curTime - prevTime
|
|
||||||
curUsage := rusage.Utime.Nano() + rusage.Stime.Nano()
|
|
||||||
usageDiff := curUsage - prevUsage
|
|
||||||
cpuUsagePercent := 100 * float64(usageDiff) / float64(timeDiff)
|
|
||||||
prevTime = curTime
|
|
||||||
prevUsage = curUsage
|
|
||||||
|
|
||||||
runtime.ReadMemStats(&memstats)
|
|
||||||
|
|
||||||
startms := int(t.Sub(t0).Seconds() * 1000)
|
|
||||||
|
|
||||||
fmt.Fprintf(fd, "%d\t%f\t%d\t%d\n", startms, cpuUsagePercent, memstats.Alloc, memstats.Sys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// 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 !solaris,!windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/internal/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if innerProcess && os.Getenv("STPERFSTATS") != "" {
|
||||||
|
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func savePerfStats(file string) {
|
||||||
|
fd, err := os.Create(file)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevUsage int64
|
||||||
|
var prevTime int64
|
||||||
|
var rusage syscall.Rusage
|
||||||
|
var memstats runtime.MemStats
|
||||||
|
var prevIn, prevOut uint64
|
||||||
|
|
||||||
|
t0 := time.Now()
|
||||||
|
for t := range time.NewTicker(250 * time.Millisecond).C {
|
||||||
|
syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
|
||||||
|
curTime := time.Now().UnixNano()
|
||||||
|
timeDiff := curTime - prevTime
|
||||||
|
curUsage := rusage.Utime.Nano() + rusage.Stime.Nano()
|
||||||
|
usageDiff := curUsage - prevUsage
|
||||||
|
cpuUsagePercent := 100 * float64(usageDiff) / float64(timeDiff)
|
||||||
|
prevTime = curTime
|
||||||
|
prevUsage = curUsage
|
||||||
|
in, out := protocol.TotalInOut()
|
||||||
|
var inRate, outRate float64
|
||||||
|
if timeDiff > 0 {
|
||||||
|
inRate = float64(in-prevIn) / (float64(timeDiff) / 1e9) // bytes per second
|
||||||
|
outRate = float64(out-prevOut) / (float64(timeDiff) / 1e9) // bytes per second
|
||||||
|
}
|
||||||
|
prevIn, prevOut = in, out
|
||||||
|
|
||||||
|
runtime.ReadMemStats(&memstats)
|
||||||
|
|
||||||
|
startms := int(t.Sub(t0).Seconds() * 1000)
|
||||||
|
|
||||||
|
fmt.Fprintf(fd, "%d\t%f\t%d\t%d\t%.0f\t%.0f\n", startms, cpuUsagePercent, memstats.Alloc, memstats.Sys-memstats.HeapReleased, inRate, outRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
+69
-8
@@ -5,6 +5,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@@ -13,8 +14,10 @@ import (
|
|||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
mr "math/rand"
|
mr "math/rand"
|
||||||
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -42,7 +45,9 @@ func newCertificate(dir string, prefix string) {
|
|||||||
l.Infoln("Generating RSA key and certificate...")
|
l.Infoln("Generating RSA key and certificate...")
|
||||||
|
|
||||||
priv, err := rsa.GenerateKey(rand.Reader, tlsRSABits)
|
priv, err := rsa.GenerateKey(rand.Reader, tlsRSABits)
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
|
l.Fatalln("generate key:", err)
|
||||||
|
}
|
||||||
|
|
||||||
notBefore := time.Now()
|
notBefore := time.Now()
|
||||||
notAfter := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
|
notAfter := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
|
||||||
@@ -61,15 +66,71 @@ func newCertificate(dir string, prefix string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
|
l.Fatalln("create cert:", err)
|
||||||
|
}
|
||||||
|
|
||||||
certOut, err := os.Create(filepath.Join(dir, prefix+"cert.pem"))
|
certOut, err := os.Create(filepath.Join(dir, prefix+"cert.pem"))
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
l.Fatalln("save cert:", err)
|
||||||
certOut.Close()
|
}
|
||||||
|
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("save cert:", err)
|
||||||
|
}
|
||||||
|
err = certOut.Close()
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("save cert:", err)
|
||||||
|
}
|
||||||
|
|
||||||
keyOut, err := os.OpenFile(filepath.Join(dir, prefix+"key.pem"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
keyOut, err := os.OpenFile(filepath.Join(dir, prefix+"key.pem"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
l.FatalErr(err)
|
if err != nil {
|
||||||
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
l.Fatalln("save key:", err)
|
||||||
keyOut.Close()
|
}
|
||||||
|
err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("save key:", err)
|
||||||
|
}
|
||||||
|
err = keyOut.Close()
|
||||||
|
if err != nil {
|
||||||
|
l.Fatalln("save key:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DowngradingListener struct {
|
||||||
|
net.Listener
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type WrappedConnection struct {
|
||||||
|
io.Reader
|
||||||
|
net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *DowngradingListener) Accept() (net.Conn, error) {
|
||||||
|
conn, err := l.Listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
br := bufio.NewReader(conn)
|
||||||
|
bs, err := br.Peek(1)
|
||||||
|
if err != nil {
|
||||||
|
// We hit a read error here, but the Accept() call succeeded so we must not return an error.
|
||||||
|
// We return the connection as is and let whoever tries to use it deal with the error.
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper := &WrappedConnection{br, conn}
|
||||||
|
|
||||||
|
// 0x16 is the first byte of a TLS handshake
|
||||||
|
if bs[0] == 0x16 {
|
||||||
|
return tls.Server(wrapper, l.TLSConfig), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WrappedConnection) Read(b []byte) (n int, err error) {
|
||||||
|
return c.Reader.Read(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,7 +15,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/model"
|
"github.com/syncthing/syncthing/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Current version number of the usage report, for acceptance purposes. If
|
// Current version number of the usage report, for acceptance purposes. If
|
||||||
|
|||||||
@@ -51,21 +51,21 @@ func main() {
|
|||||||
|
|
||||||
var langs []string
|
var langs []string
|
||||||
for code, stat := range stats {
|
for code, stat := range stats {
|
||||||
shortCode := code[:2]
|
code = strings.Replace(code, "_", "-", 1)
|
||||||
if !curValidLangs[shortCode] {
|
if !curValidLangs[code] {
|
||||||
if pct := 100 * stat.Translated / (stat.Translated + stat.Untranslated); pct < 95 {
|
if pct := 100 * stat.Translated / (stat.Translated + stat.Untranslated); pct < 95 {
|
||||||
log.Printf("Skipping language %q (too low completion ratio %d%%)", shortCode, pct)
|
log.Printf("Skipping language %q (too low completion ratio %d%%)", code, pct)
|
||||||
os.Remove("lang-" + shortCode + ".json")
|
os.Remove("lang-" + code + ".json")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
langs = append(langs, shortCode)
|
langs = append(langs, code)
|
||||||
if shortCode == "en" {
|
if code == "en" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Updating language %q", shortCode)
|
log.Printf("Updating language %q", code)
|
||||||
|
|
||||||
resp := req("https://www.transifex.com/api/2/project/syncthing/resource/gui/translation/" + code)
|
resp := req("https://www.transifex.com/api/2/project/syncthing/resource/gui/translation/" + code)
|
||||||
var t translation
|
var t translation
|
||||||
@@ -75,7 +75,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
|
||||||
fd, err := os.Create("lang-" + shortCode + ".json")
|
fd, err := os.Create("lang-" + code + ".json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ func loadValidLangs() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var langs []string
|
var langs []string
|
||||||
exp := regexp.MustCompile(`\[([a-z",]+)\]`)
|
exp := regexp.MustCompile(`\[([a-zA-Z",-]+)\]`)
|
||||||
if matches := exp.FindSubmatch(bs); len(matches) == 2 {
|
if matches := exp.FindSubmatch(bs); len(matches) == 2 {
|
||||||
langs = strings.Split(string(matches[1]), ",")
|
langs = strings.Split(string(matches[1]), ",")
|
||||||
for i := range langs {
|
for i := range langs {
|
||||||
|
|||||||
@@ -81,10 +81,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
fd.Close()
|
fd.Close()
|
||||||
|
|
||||||
doc, err := html.Parse(os.Stdin)
|
fd, err = os.Open(os.Args[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
doc, err := html.Parse(fd)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fd.Close()
|
||||||
|
|
||||||
generalNode(doc)
|
generalNode(doc)
|
||||||
bs, err := json.MarshalIndent(trans, "", " ")
|
bs, err := json.MarshalIndent(trans, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,353 +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 config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/protocol"
|
|
||||||
)
|
|
||||||
|
|
||||||
var node1, node2, node3, node4 protocol.NodeID
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
node1, _ = protocol.NodeIDFromString("AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ")
|
|
||||||
node2, _ = protocol.NodeIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
|
|
||||||
node3, _ = protocol.NodeIDFromString("LGFPDIT-7SKNNJL-VJZA4FC-7QNCRKA-CE753K7-2BW5QDK-2FOZ7FR-FEP57QJ")
|
|
||||||
node4, _ = protocol.NodeIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultValues(t *testing.T) {
|
|
||||||
expected := OptionsConfiguration{
|
|
||||||
ListenAddress: []string{"0.0.0.0:22000"},
|
|
||||||
GlobalAnnServer: "announce.syncthing.net:22026",
|
|
||||||
GlobalAnnEnabled: true,
|
|
||||||
LocalAnnEnabled: true,
|
|
||||||
LocalAnnPort: 21025,
|
|
||||||
ParallelRequests: 16,
|
|
||||||
MaxSendKbps: 0,
|
|
||||||
RescanIntervalS: 60,
|
|
||||||
ReconnectIntervalS: 60,
|
|
||||||
MaxChangeKbps: 10000,
|
|
||||||
StartBrowser: true,
|
|
||||||
UPnPEnabled: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := Load(bytes.NewReader(nil), node1)
|
|
||||||
if err != io.EOF {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(cfg.Options, expected) {
|
|
||||||
t.Errorf("Default config differs;\n E: %#v\n A: %#v", expected, cfg.Options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNodeConfig(t *testing.T) {
|
|
||||||
v1data := []byte(`
|
|
||||||
<configuration version="1">
|
|
||||||
<repository id="test" directory="~/Sync">
|
|
||||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
|
|
||||||
<address>a</address>
|
|
||||||
</node>
|
|
||||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
|
|
||||||
<address>b</address>
|
|
||||||
</node>
|
|
||||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
|
|
||||||
<address>a</address>
|
|
||||||
</node>
|
|
||||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
|
|
||||||
<address>b</address>
|
|
||||||
</node>
|
|
||||||
</repository>
|
|
||||||
<options>
|
|
||||||
<readOnly>true</readOnly>
|
|
||||||
</options>
|
|
||||||
</configuration>
|
|
||||||
`)
|
|
||||||
|
|
||||||
v2data := []byte(`
|
|
||||||
<configuration version="2">
|
|
||||||
<repository id="test" directory="~/Sync" ro="true">
|
|
||||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
|
|
||||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
|
|
||||||
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
|
|
||||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
|
|
||||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
|
|
||||||
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
|
|
||||||
</repository>
|
|
||||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
|
|
||||||
<address>a</address>
|
|
||||||
</node>
|
|
||||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
|
|
||||||
<address>b</address>
|
|
||||||
</node>
|
|
||||||
</configuration>
|
|
||||||
`)
|
|
||||||
|
|
||||||
v3data := []byte(`
|
|
||||||
<configuration version="3">
|
|
||||||
<repository id="test" directory="~/Sync" ro="true" ignorePerms="false">
|
|
||||||
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" compression="false"></node>
|
|
||||||
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" compression="false"></node>
|
|
||||||
</repository>
|
|
||||||
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="true">
|
|
||||||
<address>a</address>
|
|
||||||
</node>
|
|
||||||
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="true">
|
|
||||||
<address>b</address>
|
|
||||||
</node>
|
|
||||||
</configuration>`)
|
|
||||||
|
|
||||||
for i, data := range [][]byte{v1data, v2data, v3data} {
|
|
||||||
cfg, err := Load(bytes.NewReader(data), node1)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedRepos := []RepositoryConfiguration{
|
|
||||||
{
|
|
||||||
ID: "test",
|
|
||||||
Directory: "~/Sync",
|
|
||||||
Nodes: []NodeConfiguration{{NodeID: node1}, {NodeID: node4}},
|
|
||||||
ReadOnly: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
expectedNodes := []NodeConfiguration{
|
|
||||||
{
|
|
||||||
NodeID: node1,
|
|
||||||
Name: "node one",
|
|
||||||
Addresses: []string{"a"},
|
|
||||||
Compression: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
NodeID: node4,
|
|
||||||
Name: "node two",
|
|
||||||
Addresses: []string{"b"},
|
|
||||||
Compression: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
expectedNodeIDs := []protocol.NodeID{node1, node4}
|
|
||||||
|
|
||||||
if cfg.Version != 3 {
|
|
||||||
t.Errorf("%d: Incorrect version %d != 3", i, cfg.Version)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(cfg.Repositories, expectedRepos) {
|
|
||||||
t.Errorf("%d: Incorrect Repositories\n A: %#v\n E: %#v", i, cfg.Repositories, expectedRepos)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(cfg.Nodes, expectedNodes) {
|
|
||||||
t.Errorf("%d: Incorrect Nodes\n A: %#v\n E: %#v", i, cfg.Nodes, expectedNodes)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(cfg.Repositories[0].NodeIDs(), expectedNodeIDs) {
|
|
||||||
t.Errorf("%d: Incorrect NodeIDs\n A: %#v\n E: %#v", i, cfg.Repositories[0].NodeIDs(), expectedNodeIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.NodeMap()) != len(expectedNodes) {
|
|
||||||
t.Errorf("Unexpected number of NodeMap() entries")
|
|
||||||
}
|
|
||||||
if len(cfg.RepoMap()) != len(expectedRepos) {
|
|
||||||
t.Errorf("Unexpected number of RepoMap() entries")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoListenAddress(t *testing.T) {
|
|
||||||
data := []byte(`<configuration version="1">
|
|
||||||
<options>
|
|
||||||
<listenAddress></listenAddress>
|
|
||||||
</options>
|
|
||||||
</configuration>
|
|
||||||
`)
|
|
||||||
|
|
||||||
cfg, err := Load(bytes.NewReader(data), node1)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := []string{""}
|
|
||||||
if !reflect.DeepEqual(cfg.Options.ListenAddress, expected) {
|
|
||||||
t.Errorf("Unexpected ListenAddress %#v", cfg.Options.ListenAddress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOverriddenValues(t *testing.T) {
|
|
||||||
data := []byte(`<configuration version="2">
|
|
||||||
<options>
|
|
||||||
<listenAddress>:23000</listenAddress>
|
|
||||||
<allowDelete>false</allowDelete>
|
|
||||||
<globalAnnounceServer>syncthing.nym.se:22026</globalAnnounceServer>
|
|
||||||
<globalAnnounceEnabled>false</globalAnnounceEnabled>
|
|
||||||
<localAnnounceEnabled>false</localAnnounceEnabled>
|
|
||||||
<localAnnouncePort>42123</localAnnouncePort>
|
|
||||||
<parallelRequests>32</parallelRequests>
|
|
||||||
<maxSendKbps>1234</maxSendKbps>
|
|
||||||
<rescanIntervalS>600</rescanIntervalS>
|
|
||||||
<reconnectionIntervalS>6000</reconnectionIntervalS>
|
|
||||||
<maxChangeKbps>2345</maxChangeKbps>
|
|
||||||
<startBrowser>false</startBrowser>
|
|
||||||
<upnpEnabled>false</upnpEnabled>
|
|
||||||
</options>
|
|
||||||
</configuration>
|
|
||||||
`)
|
|
||||||
|
|
||||||
expected := OptionsConfiguration{
|
|
||||||
ListenAddress: []string{":23000"},
|
|
||||||
GlobalAnnServer: "syncthing.nym.se:22026",
|
|
||||||
GlobalAnnEnabled: false,
|
|
||||||
LocalAnnEnabled: false,
|
|
||||||
LocalAnnPort: 42123,
|
|
||||||
ParallelRequests: 32,
|
|
||||||
MaxSendKbps: 1234,
|
|
||||||
RescanIntervalS: 600,
|
|
||||||
ReconnectIntervalS: 6000,
|
|
||||||
MaxChangeKbps: 2345,
|
|
||||||
StartBrowser: false,
|
|
||||||
UPnPEnabled: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := Load(bytes.NewReader(data), node1)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(cfg.Options, expected) {
|
|
||||||
t.Errorf("Overridden config differs;\n E: %#v\n A: %#v", expected, cfg.Options)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNodeAddressesDynamic(t *testing.T) {
|
|
||||||
data := []byte(`
|
|
||||||
<configuration version="2">
|
|
||||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
|
|
||||||
<address></address>
|
|
||||||
</node>
|
|
||||||
<node id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
|
|
||||||
</node>
|
|
||||||
<node id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
|
|
||||||
<address>dynamic</address>
|
|
||||||
</node>
|
|
||||||
</configuration>
|
|
||||||
`)
|
|
||||||
|
|
||||||
name, _ := os.Hostname()
|
|
||||||
expected := []NodeConfiguration{
|
|
||||||
{
|
|
||||||
NodeID: node1,
|
|
||||||
Addresses: []string{"dynamic"},
|
|
||||||
Compression: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
NodeID: node2,
|
|
||||||
Addresses: []string{"dynamic"},
|
|
||||||
Compression: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
NodeID: node3,
|
|
||||||
Addresses: []string{"dynamic"},
|
|
||||||
Compression: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
NodeID: node4,
|
|
||||||
Name: name, // Set when auto created
|
|
||||||
Addresses: []string{"dynamic"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := Load(bytes.NewReader(data), node4)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(cfg.Nodes, expected) {
|
|
||||||
t.Errorf("Nodes differ;\n E: %#v\n A: %#v", expected, cfg.Nodes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNodeAddressesStatic(t *testing.T) {
|
|
||||||
data := []byte(`
|
|
||||||
<configuration version="3">
|
|
||||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
|
|
||||||
<address>192.0.2.1</address>
|
|
||||||
<address>192.0.2.2</address>
|
|
||||||
</node>
|
|
||||||
<node id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
|
|
||||||
<address>192.0.2.3:6070</address>
|
|
||||||
<address>[2001:db8::42]:4242</address>
|
|
||||||
</node>
|
|
||||||
<node id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
|
|
||||||
<address>[2001:db8::44]:4444</address>
|
|
||||||
<address>192.0.2.4:6090</address>
|
|
||||||
</node>
|
|
||||||
</configuration>
|
|
||||||
`)
|
|
||||||
|
|
||||||
name, _ := os.Hostname()
|
|
||||||
expected := []NodeConfiguration{
|
|
||||||
{
|
|
||||||
NodeID: node1,
|
|
||||||
Addresses: []string{"192.0.2.1", "192.0.2.2"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
NodeID: node2,
|
|
||||||
Addresses: []string{"192.0.2.3:6070", "[2001:db8::42]:4242"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
NodeID: node3,
|
|
||||||
Addresses: []string{"[2001:db8::44]:4444", "192.0.2.4:6090"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
NodeID: node4,
|
|
||||||
Name: name, // Set when auto created
|
|
||||||
Addresses: []string{"dynamic"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := Load(bytes.NewReader(data), node4)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(cfg.Nodes, expected) {
|
|
||||||
t.Errorf("Nodes differ;\n E: %#v\n A: %#v", expected, cfg.Nodes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVersioningConfig(t *testing.T) {
|
|
||||||
data := []byte(`
|
|
||||||
<configuration version="2">
|
|
||||||
<repository id="test" directory="~/Sync" ro="true">
|
|
||||||
<versioning type="simple">
|
|
||||||
<param key="foo" val="bar"/>
|
|
||||||
<param key="baz" val="quux"/>
|
|
||||||
</versioning>
|
|
||||||
</repository>
|
|
||||||
</configuration>
|
|
||||||
`)
|
|
||||||
|
|
||||||
cfg, err := Load(bytes.NewReader(data), node4)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
vc := cfg.Repositories[0].Versioning
|
|
||||||
if vc.Type != "simple" {
|
|
||||||
t.Errorf(`vc.Type %q != "simple"`, vc.Type)
|
|
||||||
}
|
|
||||||
if l := len(vc.Params); l != 2 {
|
|
||||||
t.Errorf("len(vc.Params) %d != 2", l)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
"baz": "quux",
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(vc.Params, expected) {
|
|
||||||
t.Errorf("vc.Params differ;\n E: %#v\n A: %#v", expected, vc.Params)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+294
-167
@@ -3,7 +3,7 @@
|
|||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
/*jslint browser: true, continue: true, plusplus: true */
|
/*jslint browser: true, continue: true, plusplus: true */
|
||||||
/*global $: false, angular: false */
|
/*global $: false, angular: false, console: false, validLangs: false */
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -15,48 +15,52 @@ syncthing.config(function ($httpProvider, $translateProvider) {
|
|||||||
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token';
|
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token';
|
||||||
|
|
||||||
$translateProvider.useStaticFilesLoader({
|
$translateProvider.useStaticFilesLoader({
|
||||||
prefix: 'lang-',
|
prefix: 'lang/lang-',
|
||||||
suffix: '.json'
|
suffix: '.json'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
syncthing.controller('EventCtrl', function ($scope, $http) {
|
syncthing.controller('EventCtrl', function ($scope, $http) {
|
||||||
$scope.lastEvent = null;
|
$scope.lastEvent = null;
|
||||||
var online = false;
|
|
||||||
var lastID = 0;
|
var lastID = 0;
|
||||||
|
|
||||||
var successFn = function (data) {
|
var successFn = function (data) {
|
||||||
if (!online) {
|
// When Syncthing restarts while the long polling connection is in
|
||||||
$scope.$emit('UIOnline');
|
// progress the browser on some platforms returns a 200 (since the
|
||||||
online = true;
|
// headers has been flushed with the return code 200), with no data.
|
||||||
|
// This basically means that the connection has been reset, and the call
|
||||||
|
// was not actually sucessful.
|
||||||
|
if (!data) {
|
||||||
|
errorFn(data);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.$emit('UIOnline');
|
||||||
|
|
||||||
if (lastID > 0) {
|
if (lastID > 0) {
|
||||||
data.forEach(function (event) {
|
data.forEach(function (event) {
|
||||||
console.log("event", event.id, event.type, event.data);
|
console.log("event", event.id, event.type, event.data);
|
||||||
$scope.$emit(event.type, event);
|
$scope.$emit(event.type, event);
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
$scope.lastEvent = data[data.length - 1];
|
$scope.lastEvent = data[data.length - 1];
|
||||||
lastID = $scope.lastEvent.id;
|
lastID = $scope.lastEvent.id;
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
$http.get(urlbase + '/events?since=' + lastID)
|
$http.get(urlbase + '/events?since=' + lastID)
|
||||||
.success(successFn)
|
.success(successFn)
|
||||||
.error(errorFn);
|
.error(errorFn);
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
var errorFn = function (data) {
|
var errorFn = function (data) {
|
||||||
if (online) {
|
$scope.$emit('UIOffline');
|
||||||
$scope.$emit('UIOffline');
|
|
||||||
online = false;
|
|
||||||
}
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
$http.get(urlbase + '/events?limit=1')
|
$http.get(urlbase + '/events?limit=1')
|
||||||
.success(successFn)
|
.success(successFn)
|
||||||
.error(errorFn);
|
.error(errorFn);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,6 +72,8 @@ syncthing.controller('EventCtrl', function ($scope, $http) {
|
|||||||
syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $location) {
|
syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $location) {
|
||||||
var prevDate = 0;
|
var prevDate = 0;
|
||||||
var getOK = true;
|
var getOK = true;
|
||||||
|
var navigatingAway = false;
|
||||||
|
var online = false;
|
||||||
var restarting = false;
|
var restarting = false;
|
||||||
|
|
||||||
$scope.completion = {};
|
$scope.completion = {};
|
||||||
@@ -84,17 +90,43 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
$scope.repos = {};
|
$scope.repos = {};
|
||||||
$scope.seenError = '';
|
$scope.seenError = '';
|
||||||
$scope.upgradeInfo = {};
|
$scope.upgradeInfo = {};
|
||||||
|
$scope.stats = {};
|
||||||
|
|
||||||
$http.get(urlbase+"/lang").success(function (langs) {
|
$http.get(urlbase + "/lang").success(function (langs) {
|
||||||
var lang;
|
// Find the first language in the list provided by the user's browser
|
||||||
|
// that is a prefix of a language we have available. That is, "en"
|
||||||
|
// sent by the browser will match "en" or "en-US", while "zh-TW" will
|
||||||
|
// match only "zh-TW" and not "zh-CN".
|
||||||
|
|
||||||
|
var lang, matching;
|
||||||
for (var i = 0; i < langs.length; i++) {
|
for (var i = 0; i < langs.length; i++) {
|
||||||
lang = langs[i];
|
lang = langs[i];
|
||||||
if (validLangs.indexOf(lang) >= 0) {
|
if (lang.length < 2) {
|
||||||
$translate.use(lang);
|
continue;
|
||||||
break;
|
}
|
||||||
|
matching = validLangs.filter(function (possibleLang) {
|
||||||
|
// The langs returned by the /rest/langs call will be in lower
|
||||||
|
// case. We compare to the lowercase version of the language
|
||||||
|
// code we have as well.
|
||||||
|
possibleLang = possibleLang.toLowerCase();
|
||||||
|
if (possibleLang.length > lang.length) {
|
||||||
|
return possibleLang.indexOf(lang) === 0;
|
||||||
|
} else {
|
||||||
|
return lang.indexOf(possibleLang) === 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (matching.length >= 1) {
|
||||||
|
$translate.use(matching[0]);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
// Fallback if nothing matched
|
||||||
|
$translate.use("en");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(window).bind('beforeunload', function () {
|
||||||
|
navigatingAway = true;
|
||||||
|
});
|
||||||
|
|
||||||
$scope.$on("$locationChangeSuccess", function () {
|
$scope.$on("$locationChangeSuccess", function () {
|
||||||
var lang = $location.search().lang;
|
var lang = $location.search().lang;
|
||||||
@@ -108,27 +140,41 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
'rmdir': 'Del (dir)',
|
'rmdir': 'Del (dir)',
|
||||||
'sync': 'Sync',
|
'sync': 'Sync',
|
||||||
'touch': 'Update',
|
'touch': 'Update',
|
||||||
}
|
};
|
||||||
$scope.needIcons = {
|
$scope.needIcons = {
|
||||||
'rm': 'remove',
|
'rm': 'remove',
|
||||||
'rmdir': 'remove',
|
'rmdir': 'remove',
|
||||||
'sync': 'download',
|
'sync': 'download',
|
||||||
'touch': 'asterisk',
|
'touch': 'asterisk',
|
||||||
}
|
};
|
||||||
|
|
||||||
$scope.$on('UIOnline', function (event, arg) {
|
$scope.$on('UIOnline', function (event, arg) {
|
||||||
console.log('UIOnline');
|
if (online && !restarting) {
|
||||||
$scope.init();
|
return;
|
||||||
restarting = false;
|
}
|
||||||
$('#networkError').modal('hide');
|
|
||||||
$('#restarting').modal('hide');
|
if (restarting) {
|
||||||
$('#shutdown').modal('hide');
|
document.location.reload(true);
|
||||||
|
} else {
|
||||||
|
console.log('UIOnline');
|
||||||
|
$scope.init();
|
||||||
|
online = true;
|
||||||
|
restarting = false;
|
||||||
|
$('#networkError').modal('hide');
|
||||||
|
$('#restarting').modal('hide');
|
||||||
|
$('#shutdown').modal('hide');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.$on('UIOffline', function (event, arg) {
|
$scope.$on('UIOffline', function (event, arg) {
|
||||||
|
if (navigatingAway || !online) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('UIOffline');
|
console.log('UIOffline');
|
||||||
|
online = false;
|
||||||
if (!restarting) {
|
if (!restarting) {
|
||||||
$('#networkError').modal({backdrop: 'static', keyboard: false});
|
$('#networkError').modal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,6 +203,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
|
|
||||||
$scope.$on('NodeDisconnected', function (event, arg) {
|
$scope.$on('NodeDisconnected', function (event, arg) {
|
||||||
delete $scope.connections[arg.data.id];
|
delete $scope.connections[arg.data.id];
|
||||||
|
refreshNodeStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.$on('NodeConnected', function (event, arg) {
|
$scope.$on('NodeConnected', function (event, arg) {
|
||||||
@@ -175,7 +222,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
});
|
});
|
||||||
|
|
||||||
$scope.$on('ConfigLoaded', function (event) {
|
$scope.$on('ConfigLoaded', function (event) {
|
||||||
if ($scope.config.Options.URAccepted == 0) {
|
if ($scope.config.Options.URAccepted === 0) {
|
||||||
// If usage reporting has been neither accepted nor declined,
|
// If usage reporting has been neither accepted nor declined,
|
||||||
// we want to ask the user to make a choice. But we don't want
|
// we want to ask the user to make a choice. But we don't want
|
||||||
// to bug them during initial setup, so we set a cookie with
|
// to bug them during initial setup, so we set a cookie with
|
||||||
@@ -185,14 +232,22 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
|
|
||||||
var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
|
var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
|
||||||
if (!firstVisit) {
|
if (!firstVisit) {
|
||||||
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
|
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
|
||||||
} else {
|
} else {
|
||||||
if (+firstVisit < Date.now() - 4*3600*1000){
|
if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
|
||||||
$('#ur').modal({backdrop: 'static', keyboard: false});
|
$('#ur').modal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
$scope.$on('ConfigSaved', function (event, arg) {
|
||||||
|
updateLocalConfig(arg.data);
|
||||||
|
|
||||||
|
$http.get(urlbase + '/config/sync').success(function (data) {
|
||||||
|
$scope.configInSync = data.configInSync;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var debouncedFuncs = {};
|
var debouncedFuncs = {};
|
||||||
|
|
||||||
@@ -209,6 +264,33 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
debouncedFuncs[key]();
|
debouncedFuncs[key]();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateLocalConfig(config) {
|
||||||
|
var hasConfig = !isEmptyObject($scope.config);
|
||||||
|
|
||||||
|
$scope.config = config;
|
||||||
|
$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);
|
||||||
|
Object.keys($scope.repos).forEach(function (repo) {
|
||||||
|
refreshRepo(repo);
|
||||||
|
$scope.repos[repo].Nodes.forEach(function (nodeCfg) {
|
||||||
|
refreshCompletion(nodeCfg.NodeID, repo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasConfig) {
|
||||||
|
$scope.$emit('ConfigLoaded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function refreshSystem() {
|
function refreshSystem() {
|
||||||
$http.get(urlbase + '/system').success(function (data) {
|
$http.get(urlbase + '/system').success(function (data) {
|
||||||
$scope.myID = data.myID;
|
$scope.myID = data.myID;
|
||||||
@@ -219,7 +301,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
|
|
||||||
function refreshCompletion(node, repo) {
|
function refreshCompletion(node, repo) {
|
||||||
if (node === $scope.myID) {
|
if (node === $scope.myID) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = "refreshCompletion" + node + repo;
|
var key = "refreshCompletion" + node + repo;
|
||||||
@@ -231,7 +313,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
}
|
}
|
||||||
$scope.completion[node][repo] = data.completion;
|
$scope.completion[node][repo] = data.completion;
|
||||||
|
|
||||||
var tot = 0, cnt = 0;
|
var tot = 0,
|
||||||
|
cnt = 0;
|
||||||
for (var cmp in $scope.completion[node]) {
|
for (var cmp in $scope.completion[node]) {
|
||||||
if (cmp === "_total") {
|
if (cmp === "_total") {
|
||||||
continue;
|
continue;
|
||||||
@@ -251,8 +334,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
function refreshConnectionStats() {
|
function refreshConnectionStats() {
|
||||||
$http.get(urlbase + '/connections').success(function (data) {
|
$http.get(urlbase + '/connections').success(function (data) {
|
||||||
var now = Date.now(),
|
var now = Date.now(),
|
||||||
td = (now - prevDate) / 1000,
|
td = (now - prevDate) / 1000,
|
||||||
id;
|
id;
|
||||||
|
|
||||||
prevDate = now;
|
prevDate = now;
|
||||||
for (id in data) {
|
for (id in data) {
|
||||||
@@ -274,38 +357,14 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
|
|
||||||
function refreshErrors() {
|
function refreshErrors() {
|
||||||
$http.get(urlbase + '/errors').success(function (data) {
|
$http.get(urlbase + '/errors').success(function (data) {
|
||||||
$scope.errors = data;
|
$scope.errors = data.errors;
|
||||||
console.log("refreshErrors", data);
|
console.log("refreshErrors", data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshConfig() {
|
function refreshConfig() {
|
||||||
$http.get(urlbase + '/config').success(function (data) {
|
$http.get(urlbase + '/config').success(function (data) {
|
||||||
var hasConfig = !isEmptyObject($scope.config);
|
updateLocalConfig(data);
|
||||||
|
|
||||||
$scope.config = data;
|
|
||||||
$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);
|
|
||||||
Object.keys($scope.repos).forEach(function (repo) {
|
|
||||||
refreshRepo(repo);
|
|
||||||
$scope.repos[repo].Nodes.forEach(function (nodeCfg) {
|
|
||||||
refreshCompletion(nodeCfg.NodeID, repo);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasConfig) {
|
|
||||||
$scope.$emit('ConfigLoaded');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("refreshConfig", data);
|
console.log("refreshConfig", data);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -314,13 +373,25 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.init = function() {
|
var refreshNodeStats = debounce(function () {
|
||||||
|
$http.get(urlbase + "/stats/node").success(function (data) {
|
||||||
|
$scope.stats = data;
|
||||||
|
for (var node in $scope.stats) {
|
||||||
|
$scope.stats[node].LastSeen = new Date($scope.stats[node].LastSeen);
|
||||||
|
$scope.stats[node].LastSeenDays = (new Date() - $scope.stats[node].LastSeen) / 1000 / 86400;
|
||||||
|
}
|
||||||
|
console.log("refreshNodeStats", data);
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
$scope.init = function () {
|
||||||
refreshSystem();
|
refreshSystem();
|
||||||
refreshConfig();
|
refreshConfig();
|
||||||
refreshConnectionStats();
|
refreshConnectionStats();
|
||||||
|
refreshNodeStats();
|
||||||
|
|
||||||
$http.get(urlbase + '/version').success(function (data) {
|
$http.get(urlbase + '/version').success(function (data) {
|
||||||
$scope.version = data;
|
$scope.version = data.version;
|
||||||
});
|
});
|
||||||
|
|
||||||
$http.get(urlbase + '/report').success(function (data) {
|
$http.get(urlbase + '/report').success(function (data) {
|
||||||
@@ -426,19 +497,10 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.nodeVer = function (nodeCfg) {
|
|
||||||
if (nodeCfg.NodeID === $scope.myID) {
|
|
||||||
return $scope.version;
|
|
||||||
}
|
|
||||||
var conn = $scope.connections[nodeCfg.NodeID];
|
|
||||||
if (conn) {
|
|
||||||
return conn.ClientVersion;
|
|
||||||
}
|
|
||||||
return '?';
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.findNode = function (nodeID) {
|
$scope.findNode = function (nodeID) {
|
||||||
var matches = $scope.nodes.filter(function (n) { return n.NodeID == nodeID; });
|
var matches = $scope.nodes.filter(function (n) {
|
||||||
|
return n.NodeID == nodeID;
|
||||||
|
});
|
||||||
if (matches.length != 1) {
|
if (matches.length != 1) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -470,13 +532,18 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
// Make a working copy
|
// Make a working copy
|
||||||
$scope.tmpOptions = angular.copy($scope.config.Options);
|
$scope.tmpOptions = angular.copy($scope.config.Options);
|
||||||
$scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
|
$scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
|
||||||
|
$scope.tmpOptions.NodeName = $scope.thisNode().Name;
|
||||||
$scope.tmpGUI = angular.copy($scope.config.GUI);
|
$scope.tmpGUI = angular.copy($scope.config.GUI);
|
||||||
$('#settings').modal({backdrop: 'static', keyboard: true});
|
$('#settings').modal();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.saveConfig = function() {
|
$scope.saveConfig = function () {
|
||||||
var cfg = JSON.stringify($scope.config);
|
var cfg = JSON.stringify($scope.config);
|
||||||
var opts = {headers: {'Content-Type': 'application/json'}};
|
var opts = {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
};
|
||||||
$http.post(urlbase + '/config', cfg, opts).success(function () {
|
$http.post(urlbase + '/config', cfg, opts).success(function () {
|
||||||
$http.get(urlbase + '/config/sync').success(function (data) {
|
$http.get(urlbase + '/config/sync').success(function (data) {
|
||||||
$scope.configInSync = data.configInSync;
|
$scope.configInSync = data.configInSync;
|
||||||
@@ -487,24 +554,27 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
$scope.saveSettings = function () {
|
$scope.saveSettings = function () {
|
||||||
// Make sure something changed
|
// Make sure something changed
|
||||||
var changed = !angular.equals($scope.config.Options, $scope.tmpOptions) ||
|
var changed = !angular.equals($scope.config.Options, $scope.tmpOptions) ||
|
||||||
!angular.equals($scope.config.GUI, $scope.tmpGUI);
|
!angular.equals($scope.config.GUI, $scope.tmpGUI);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
// Check if usage reporting has been enabled or disabled
|
// Check if usage reporting has been enabled or disabled
|
||||||
if ($scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted <= 0) {
|
if ($scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted <= 0) {
|
||||||
$scope.tmpOptions.URAccepted = 1000;
|
$scope.tmpOptions.URAccepted = 1000;
|
||||||
} else if (!$scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted > 0){
|
} else if (!$scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted > 0) {
|
||||||
$scope.tmpOptions.URAccepted = -1;
|
$scope.tmpOptions.URAccepted = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if protocol will need to be changed on restart
|
// Check if protocol will need to be changed on restart
|
||||||
if($scope.config.GUI.UseTLS !== $scope.tmpGUI.UseTLS){
|
if ($scope.config.GUI.UseTLS !== $scope.tmpGUI.UseTLS) {
|
||||||
$scope.protocolChanged = true;
|
$scope.protocolChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply new settings locally
|
// Apply new settings locally
|
||||||
|
$scope.thisNode().Name = $scope.tmpOptions.NodeName;
|
||||||
$scope.config.Options = angular.copy($scope.tmpOptions);
|
$scope.config.Options = angular.copy($scope.tmpOptions);
|
||||||
$scope.config.GUI = angular.copy($scope.tmpGUI);
|
$scope.config.GUI = angular.copy($scope.tmpGUI);
|
||||||
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
|
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) {
|
||||||
|
return x.trim();
|
||||||
|
});
|
||||||
|
|
||||||
$scope.saveConfig();
|
$scope.saveConfig();
|
||||||
}
|
}
|
||||||
@@ -514,21 +584,21 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
|
|
||||||
$scope.restart = function () {
|
$scope.restart = function () {
|
||||||
restarting = true;
|
restarting = true;
|
||||||
$('#restarting').modal({backdrop: 'static', keyboard: false});
|
$('#restarting').modal();
|
||||||
$http.post(urlbase + '/restart');
|
$http.post(urlbase + '/restart');
|
||||||
$scope.configInSync = true;
|
$scope.configInSync = true;
|
||||||
|
|
||||||
// Switch webpage protocol if needed
|
// Switch webpage protocol if needed
|
||||||
if($scope.protocolChanged){
|
if ($scope.protocolChanged) {
|
||||||
var protocol = 'http';
|
var protocol = 'http';
|
||||||
|
|
||||||
if($scope.config.GUI.UseTLS){
|
if ($scope.config.GUI.UseTLS) {
|
||||||
protocol = 'https';
|
protocol = 'https';
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(function(){
|
setTimeout(function () {
|
||||||
window.location.protocol = protocol;
|
window.location.protocol = protocol;
|
||||||
}, 1000);
|
}, 2500);
|
||||||
|
|
||||||
$scope.protocolChanged = false;
|
$scope.protocolChanged = false;
|
||||||
}
|
}
|
||||||
@@ -536,9 +606,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
|
|
||||||
$scope.upgrade = function () {
|
$scope.upgrade = function () {
|
||||||
restarting = true;
|
restarting = true;
|
||||||
$('#upgrading').modal({backdrop: 'static', keyboard: false});
|
$('#upgrading').modal();
|
||||||
$http.post(urlbase + '/upgrade').success(function () {
|
$http.post(urlbase + '/upgrade').success(function () {
|
||||||
$('#restarting').modal({backdrop: 'static', keyboard: false});
|
$('#restarting').modal();
|
||||||
$('#upgrading').modal('hide');
|
$('#upgrading').modal('hide');
|
||||||
}).error(function () {
|
}).error(function () {
|
||||||
$('#upgrading').modal('hide');
|
$('#upgrading').modal('hide');
|
||||||
@@ -548,7 +618,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
$scope.shutdown = function () {
|
$scope.shutdown = function () {
|
||||||
restarting = true;
|
restarting = true;
|
||||||
$http.post(urlbase + '/shutdown').success(function () {
|
$http.post(urlbase + '/shutdown').success(function () {
|
||||||
$('#shutdown').modal({backdrop: 'static', keyboard: false});
|
$('#shutdown').modal();
|
||||||
});
|
});
|
||||||
$scope.configInSync = true;
|
$scope.configInSync = true;
|
||||||
};
|
};
|
||||||
@@ -559,7 +629,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
$scope.editingSelf = (nodeCfg.NodeID == $scope.myID);
|
$scope.editingSelf = (nodeCfg.NodeID == $scope.myID);
|
||||||
$scope.currentNode.AddressesStr = nodeCfg.Addresses.join(', ');
|
$scope.currentNode.AddressesStr = nodeCfg.Addresses.join(', ');
|
||||||
$scope.nodeEditor.$setPristine();
|
$scope.nodeEditor.$setPristine();
|
||||||
$('#editNode').modal({backdrop: 'static', keyboard: true});
|
$('#editNode').modal();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.idNode = function () {
|
$scope.idNode = function () {
|
||||||
@@ -567,11 +637,15 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.addNode = function () {
|
$scope.addNode = function () {
|
||||||
$scope.currentNode = {AddressesStr: 'dynamic', Compression: true};
|
$scope.currentNode = {
|
||||||
|
AddressesStr: 'dynamic',
|
||||||
|
Compression: true,
|
||||||
|
Introducer: true
|
||||||
|
};
|
||||||
$scope.editingExisting = false;
|
$scope.editingExisting = false;
|
||||||
$scope.editingSelf = false;
|
$scope.editingSelf = false;
|
||||||
$scope.nodeEditor.$setPristine();
|
$scope.nodeEditor.$setPristine();
|
||||||
$('#editNode').modal({backdrop: 'static', keyboard: true});
|
$('#editNode').modal();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.deleteNode = function () {
|
$scope.deleteNode = function () {
|
||||||
@@ -599,7 +673,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
|
|
||||||
$('#editNode').modal('hide');
|
$('#editNode').modal('hide');
|
||||||
nodeCfg = $scope.currentNode;
|
nodeCfg = $scope.currentNode;
|
||||||
nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) { return x.trim(); });
|
nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) {
|
||||||
|
return x.trim();
|
||||||
|
});
|
||||||
|
|
||||||
done = false;
|
done = false;
|
||||||
for (i = 0; i < $scope.nodes.length; i++) {
|
for (i = 0; i < $scope.nodes.length; i++) {
|
||||||
@@ -621,7 +697,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.otherNodes = function () {
|
$scope.otherNodes = function () {
|
||||||
return $scope.nodes.filter(function (n){
|
return $scope.nodes.filter(function (n) {
|
||||||
return n.NodeID !== $scope.myID;
|
return n.NodeID !== $scope.myID;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -674,19 +750,46 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
});
|
});
|
||||||
if ($scope.currentRepo.Versioning && $scope.currentRepo.Versioning.Type === "simple") {
|
if ($scope.currentRepo.Versioning && $scope.currentRepo.Versioning.Type === "simple") {
|
||||||
$scope.currentRepo.simpleFileVersioning = true;
|
$scope.currentRepo.simpleFileVersioning = true;
|
||||||
|
$scope.currentRepo.FileVersioningSelector = "simple";
|
||||||
$scope.currentRepo.simpleKeep = +$scope.currentRepo.Versioning.Params.keep;
|
$scope.currentRepo.simpleKeep = +$scope.currentRepo.Versioning.Params.keep;
|
||||||
|
} else if ($scope.currentRepo.Versioning && $scope.currentRepo.Versioning.Type === "staggered") {
|
||||||
|
$scope.currentRepo.staggeredFileVersioning = true;
|
||||||
|
$scope.currentRepo.FileVersioningSelector = "staggered";
|
||||||
|
$scope.currentRepo.staggeredMaxAge = Math.floor(+$scope.currentRepo.Versioning.Params.maxAge / 86400);
|
||||||
|
$scope.currentRepo.staggeredCleanInterval = +$scope.currentRepo.Versioning.Params.cleanInterval;
|
||||||
|
$scope.currentRepo.staggeredVersionsPath = $scope.currentRepo.Versioning.Params.versionsPath;
|
||||||
|
} else {
|
||||||
|
$scope.currentRepo.FileVersioningSelector = "none";
|
||||||
}
|
}
|
||||||
$scope.currentRepo.simpleKeep = $scope.currentRepo.simpleKeep || 5;
|
$scope.currentRepo.simpleKeep = $scope.currentRepo.simpleKeep || 5;
|
||||||
|
$scope.currentRepo.staggeredCleanInterval = $scope.currentRepo.staggeredCleanInterval || 3600;
|
||||||
|
$scope.currentRepo.staggeredVersionsPath = $scope.currentRepo.staggeredVersionsPath || "";
|
||||||
|
|
||||||
|
// staggeredMaxAge can validly be zero, which we should not replace
|
||||||
|
// with the default value of 365. So only set the default if it's
|
||||||
|
// actually undefined.
|
||||||
|
if (typeof $scope.currentRepo.staggeredMaxAge === 'undefined') {
|
||||||
|
$scope.currentRepo.staggeredMaxAge = 365;
|
||||||
|
}
|
||||||
|
|
||||||
$scope.editingExisting = true;
|
$scope.editingExisting = true;
|
||||||
$scope.repoEditor.$setPristine();
|
$scope.repoEditor.$setPristine();
|
||||||
$('#editRepo').modal({backdrop: 'static', keyboard: true});
|
$('#editRepo').modal();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.addRepo = function () {
|
$scope.addRepo = function () {
|
||||||
$scope.currentRepo = {selectedNodes: {}};
|
$scope.currentRepo = {
|
||||||
|
selectedNodes: {}
|
||||||
|
};
|
||||||
|
$scope.currentRepo.RescanIntervalS = 60;
|
||||||
|
$scope.currentRepo.FileVersioningSelector = "none";
|
||||||
|
$scope.currentRepo.simpleKeep = 5;
|
||||||
|
$scope.currentRepo.staggeredMaxAge = 365;
|
||||||
|
$scope.currentRepo.staggeredCleanInterval = 3600;
|
||||||
|
$scope.currentRepo.staggeredVersionsPath = "";
|
||||||
$scope.editingExisting = false;
|
$scope.editingExisting = false;
|
||||||
$scope.repoEditor.$setPristine();
|
$scope.repoEditor.$setPristine();
|
||||||
$('#editRepo').modal({backdrop: 'static', keyboard: true});
|
$('#editRepo').modal();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.saveRepo = function () {
|
$scope.saveRepo = function () {
|
||||||
@@ -698,12 +801,14 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
repoCfg.selectedNodes[$scope.myID] = true;
|
repoCfg.selectedNodes[$scope.myID] = true;
|
||||||
for (var nodeID in repoCfg.selectedNodes) {
|
for (var nodeID in repoCfg.selectedNodes) {
|
||||||
if (repoCfg.selectedNodes[nodeID] === true) {
|
if (repoCfg.selectedNodes[nodeID] === true) {
|
||||||
repoCfg.Nodes.push({NodeID: nodeID});
|
repoCfg.Nodes.push({
|
||||||
|
NodeID: nodeID
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete repoCfg.selectedNodes;
|
delete repoCfg.selectedNodes;
|
||||||
|
|
||||||
if (repoCfg.simpleFileVersioning) {
|
if (repoCfg.FileVersioningSelector === "simple") {
|
||||||
repoCfg.Versioning = {
|
repoCfg.Versioning = {
|
||||||
'Type': 'simple',
|
'Type': 'simple',
|
||||||
'Params': {
|
'Params': {
|
||||||
@@ -712,6 +817,20 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
};
|
};
|
||||||
delete repoCfg.simpleFileVersioning;
|
delete repoCfg.simpleFileVersioning;
|
||||||
delete repoCfg.simpleKeep;
|
delete repoCfg.simpleKeep;
|
||||||
|
} else if (repoCfg.FileVersioningSelector === "staggered") {
|
||||||
|
repoCfg.Versioning = {
|
||||||
|
'Type': 'staggered',
|
||||||
|
'Params': {
|
||||||
|
'maxAge': '' + (repoCfg.staggeredMaxAge * 86400),
|
||||||
|
'cleanInterval': '' + repoCfg.staggeredCleanInterval,
|
||||||
|
'versionsPath': '' + repoCfg.staggeredVersionsPath,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
delete repoCfg.staggeredFileVersioning;
|
||||||
|
delete repoCfg.staggeredMaxAge;
|
||||||
|
delete repoCfg.staggeredCleanInterval;
|
||||||
|
delete repoCfg.staggeredVersionsPath;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
delete repoCfg.Versioning;
|
delete repoCfg.Versioning;
|
||||||
}
|
}
|
||||||
@@ -722,7 +841,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
$scope.saveConfig();
|
$scope.saveConfig();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.sharesRepo = function(repoCfg) {
|
$scope.sharesRepo = function (repoCfg) {
|
||||||
var names = [];
|
var names = [];
|
||||||
repoCfg.Nodes.forEach(function (node) {
|
repoCfg.Nodes.forEach(function (node) {
|
||||||
names.push($scope.nodeName($scope.findNode(node.NodeID)));
|
names.push($scope.nodeName($scope.findNode(node.NodeID)));
|
||||||
@@ -743,11 +862,54 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
$scope.saveConfig();
|
$scope.saveConfig();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.editIgnores = function () {
|
||||||
|
if (!$scope.editingExisting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#editIgnoresButton').attr('disabled', 'disabled');
|
||||||
|
$http.get(urlbase + '/ignores?repo=' + encodeURIComponent($scope.currentRepo.ID))
|
||||||
|
.success(function (data) {
|
||||||
|
data.ignore = data.ignore || [];
|
||||||
|
|
||||||
|
$('#editRepo').modal('hide');
|
||||||
|
var textArea = $('#editIgnores textarea');
|
||||||
|
|
||||||
|
textArea.val(data.ignore.join('\n'));
|
||||||
|
|
||||||
|
$('#editIgnores').modal()
|
||||||
|
.on('hidden.bs.modal', function () {
|
||||||
|
$('#editRepo').modal();
|
||||||
|
})
|
||||||
|
.on('shown.bs.modal', function () {
|
||||||
|
textArea.focus();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
$('#editIgnoresButton').removeAttr('disabled');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.saveIgnores = function () {
|
||||||
|
if (!$scope.editingExisting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$http.post(urlbase + '/ignores?repo=' + encodeURIComponent($scope.currentRepo.ID), {
|
||||||
|
ignore: $('#editIgnores textarea').val().split('\n')
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.setAPIKey = function (cfg) {
|
$scope.setAPIKey = function (cfg) {
|
||||||
cfg.APIKey = randomString(30, 32);
|
cfg.APIKey = randomString(30, 32);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.showURPreview = function () {
|
||||||
|
$('#settings').modal('hide');
|
||||||
|
$('#urPreview').modal().on('hidden.bs.modal', function () {
|
||||||
|
$('#settings').modal();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
$scope.acceptUR = function () {
|
$scope.acceptUR = function () {
|
||||||
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
|
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
|
||||||
@@ -763,7 +925,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
|
|
||||||
$scope.showNeed = function (repo) {
|
$scope.showNeed = function (repo) {
|
||||||
$scope.neededLoaded = false;
|
$scope.neededLoaded = false;
|
||||||
$('#needed').modal({backdrop: 'static', keyboard: true});
|
$('#needed').modal();
|
||||||
$http.get(urlbase + "/need?repo=" + encodeURIComponent(repo)).success(function (data) {
|
$http.get(urlbase + "/need?repo=" + encodeURIComponent(repo)).success(function (data) {
|
||||||
$scope.needed = data;
|
$scope.needed = data;
|
||||||
$scope.neededLoaded = true;
|
$scope.neededLoaded = true;
|
||||||
@@ -774,7 +936,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
var fDelete = 4096;
|
var fDelete = 4096;
|
||||||
var fDirectory = 16384;
|
var fDirectory = 16384;
|
||||||
|
|
||||||
if ((file.Flags & (fDelete+fDirectory)) === fDelete+fDirectory) {
|
if ((file.Flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
|
||||||
return 'rmdir';
|
return 'rmdir';
|
||||||
} else if ((file.Flags & fDelete) === fDelete) {
|
} else if ((file.Flags & fDelete) === fDelete) {
|
||||||
return 'rm';
|
return 'rm';
|
||||||
@@ -786,9 +948,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.override = function (repo) {
|
$scope.override = function (repo) {
|
||||||
$http.post(urlbase + "/model/override?repo=" + encodeURIComponent(repo)).success(function () {
|
$http.post(urlbase + "/model/override?repo=" + encodeURIComponent(repo));
|
||||||
$scope.refresh();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.about = function () {
|
$scope.about = function () {
|
||||||
@@ -799,6 +959,10 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
|||||||
$scope.reportPreview = true;
|
$scope.reportPreview = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.rescanRepo = function (repo) {
|
||||||
|
$http.post(urlbase + "/scan?repo=" + encodeURIComponent(repo));
|
||||||
|
};
|
||||||
|
|
||||||
$scope.init();
|
$scope.init();
|
||||||
setInterval($scope.refresh, 10000);
|
setInterval($scope.refresh, 10000);
|
||||||
});
|
});
|
||||||
@@ -816,10 +980,10 @@ function nodeCompare(a, b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function repoCompare(a, b) {
|
function repoCompare(a, b) {
|
||||||
if (a.Directory < b.Directory) {
|
if (a.ID < b.ID) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return a.Directory > b.Directory;
|
return a.ID > b.ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
function repoMap(l) {
|
function repoMap(l) {
|
||||||
@@ -851,12 +1015,11 @@ function decimals(val, num) {
|
|||||||
return decs;
|
return decs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function randomString(len, bits)
|
function randomString(len, bits) {
|
||||||
{
|
|
||||||
bits = bits || 36;
|
bits = bits || 36;
|
||||||
var outStr = "", newStr;
|
var outStr = "",
|
||||||
while (outStr.length < len)
|
newStr;
|
||||||
{
|
while (outStr.length < len) {
|
||||||
newStr = Math.random().toString(bits).slice(2);
|
newStr = Math.random().toString(bits).slice(2);
|
||||||
outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
|
outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
|
||||||
}
|
}
|
||||||
@@ -874,21 +1037,21 @@ function isEmptyObject(obj) {
|
|||||||
function debounce(func, wait) {
|
function debounce(func, wait) {
|
||||||
var timeout, args, context, timestamp, result, again;
|
var timeout, args, context, timestamp, result, again;
|
||||||
|
|
||||||
var later = function() {
|
var later = function () {
|
||||||
var last = Date.now() - timestamp;
|
var last = Date.now() - timestamp;
|
||||||
if (last < wait) {
|
if (last < wait) {
|
||||||
timeout = setTimeout(later, wait - last);
|
timeout = setTimeout(later, wait - last);
|
||||||
} else {
|
} else {
|
||||||
timeout = null;
|
timeout = null;
|
||||||
if (again) {
|
if (again) {
|
||||||
|
again = false;
|
||||||
result = func.apply(context, args);
|
result = func.apply(context, args);
|
||||||
context = args = null;
|
context = args = null;
|
||||||
again = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return function() {
|
return function () {
|
||||||
context = this;
|
context = this;
|
||||||
args = arguments;
|
args = arguments;
|
||||||
timestamp = Date.now();
|
timestamp = Date.now();
|
||||||
@@ -953,12 +1116,6 @@ syncthing.filter('metric', function () {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
syncthing.filter('short', function () {
|
|
||||||
return function (input) {
|
|
||||||
return input.substr(0, 6);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
syncthing.filter('alwaysNumber', function () {
|
syncthing.filter('alwaysNumber', function () {
|
||||||
return function (input) {
|
return function (input) {
|
||||||
if (input === undefined) {
|
if (input === undefined) {
|
||||||
@@ -968,18 +1125,6 @@ syncthing.filter('alwaysNumber', function () {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
syncthing.filter('shortPath', function () {
|
|
||||||
return function (input) {
|
|
||||||
if (input === undefined)
|
|
||||||
return "";
|
|
||||||
var parts = input.split(/[\/\\]/);
|
|
||||||
if (!parts || parts.length <= 3) {
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
return ".../" + parts.slice(parts.length-2).join("/");
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
syncthing.filter('basename', function () {
|
syncthing.filter('basename', function () {
|
||||||
return function (input) {
|
return function (input) {
|
||||||
if (input === undefined)
|
if (input === undefined)
|
||||||
@@ -988,33 +1133,15 @@ syncthing.filter('basename', function () {
|
|||||||
if (!parts || parts.length < 1) {
|
if (!parts || parts.length < 1) {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
return parts[parts.length-1];
|
return parts[parts.length - 1];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
syncthing.filter('clean', function () {
|
syncthing.directive('uniqueRepo', function () {
|
||||||
return function (input) {
|
|
||||||
return encodeURIComponent(input).replace(/%/g, '');
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
syncthing.directive('optionEditor', function () {
|
|
||||||
return {
|
|
||||||
restrict: 'C',
|
|
||||||
replace: true,
|
|
||||||
transclude: true,
|
|
||||||
scope: {
|
|
||||||
setting: '=setting',
|
|
||||||
},
|
|
||||||
template: '<input type="text" ng-model="config.Options[setting.id]"></input>',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
syncthing.directive('uniqueRepo', function() {
|
|
||||||
return {
|
return {
|
||||||
require: 'ngModel',
|
require: 'ngModel',
|
||||||
link: function(scope, elm, attrs, ctrl) {
|
link: function (scope, elm, attrs, ctrl) {
|
||||||
ctrl.$parsers.unshift(function(viewValue) {
|
ctrl.$parsers.unshift(function (viewValue) {
|
||||||
if (scope.editingExisting) {
|
if (scope.editingExisting) {
|
||||||
// we shouldn't validate
|
// we shouldn't validate
|
||||||
ctrl.$setValidity('uniqueRepo', true);
|
ctrl.$setValidity('uniqueRepo', true);
|
||||||
@@ -1031,16 +1158,16 @@ syncthing.directive('uniqueRepo', function() {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
syncthing.directive('validNodeid', function($http) {
|
syncthing.directive('validNodeid', function ($http) {
|
||||||
return {
|
return {
|
||||||
require: 'ngModel',
|
require: 'ngModel',
|
||||||
link: function(scope, elm, attrs, ctrl) {
|
link: function (scope, elm, attrs, ctrl) {
|
||||||
ctrl.$parsers.unshift(function(viewValue) {
|
ctrl.$parsers.unshift(function (viewValue) {
|
||||||
if (scope.editingExisting) {
|
if (scope.editingExisting) {
|
||||||
// we shouldn't validate
|
// we shouldn't validate
|
||||||
ctrl.$setValidity('validNodeid', true);
|
ctrl.$setValidity('validNodeid', true);
|
||||||
} else {
|
} else {
|
||||||
$http.get(urlbase + '/nodeid?id='+viewValue).success(function (resp) {
|
$http.get(urlbase + '/nodeid?id=' + viewValue).success(function (resp) {
|
||||||
if (resp.error) {
|
if (resp.error) {
|
||||||
ctrl.$setValidity('validNodeid', false);
|
ctrl.$setValidity('validNodeid', false);
|
||||||
} else {
|
} else {
|
||||||
@@ -1067,5 +1194,5 @@ syncthing.directive('modal', function () {
|
|||||||
close: '@',
|
close: '@',
|
||||||
large: '@',
|
large: '@',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Raleway';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: local('Raleway'), url(raleway-500.woff) format('woff');
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
+334
-303
@@ -11,86 +11,12 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="">
|
<meta name="description" content="">
|
||||||
<meta name="author" content="">
|
<meta name="author" content="">
|
||||||
<link rel="shortcut icon" href="favicon.png">
|
<link rel="shortcut icon" href="img/favicon.png">
|
||||||
|
|
||||||
<title>Syncthing | {{thisNodeName()}}</title>
|
<title>Syncthing | {{thisNodeName()}}</title>
|
||||||
<link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
<link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="raleway.css" rel="stylesheet">
|
<link href="font/raleway.css" rel="stylesheet">
|
||||||
<style type="text/css">
|
<link href="overrides.css" rel="stylesheet">
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-monospace {
|
|
||||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-condensed>thead>tr>th, .table-condensed>tbody>tr>th, .table-condensed>tfoot>tr>th, .table-condensed>thead>tr>td, .table-condensed>tbody>tr>td, .table-condensed>tfoot>tr>td {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
top: -5px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-no-bullet {
|
|
||||||
list-style-type: none
|
|
||||||
}
|
|
||||||
|
|
||||||
.li-column {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 7em;
|
|
||||||
margin-right: 1em;
|
|
||||||
background-color: rgb(236, 240, 241);
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 1px 4px;
|
|
||||||
margin: 2px 2px;
|
|
||||||
}
|
|
||||||
.li-column span.data {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
min-width: 10em;
|
|
||||||
text-align: right;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ng-cloak {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table th {
|
|
||||||
white-space: nowrap;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table td {
|
|
||||||
padding-left: 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table td.small-data {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width:767px) {
|
|
||||||
.table-responsive>.table>tbody>tr>td {
|
|
||||||
/* revert a bootstrap setting e.g.:
|
|
||||||
* for mobile phones to allow linebreaks in long repro folder/shared with
|
|
||||||
* columns. */
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -100,21 +26,18 @@
|
|||||||
|
|
||||||
<nav class="navbar navbar-top navbar-default" role="navigation">
|
<nav class="navbar navbar-top navbar-default" role="navigation">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<span class="navbar-brand"><img class="logo" src="logo-text-64.png" height="32" width="117"/></span>
|
<span class="navbar-brand"><img class="logo" src="img/logo-text-64.png" height="32" width="117"/></span>
|
||||||
<p class="navbar-text hidden-xs">{{thisNodeName()}}</p>
|
<p class="navbar-text hidden-xs">{{thisNodeName()}}</p>
|
||||||
<ul class="nav navbar-nav navbar-right">
|
<ul class="nav navbar-nav navbar-right">
|
||||||
<li ng-if="upgradeInfo.newer">
|
<li ng-if="upgradeInfo.newer">
|
||||||
<button type="button" class="btn navbar-btn btn-default" href="" ng-click="upgrade()">
|
<button type="button" class="btn navbar-btn btn-primary btn-sm" href="" ng-click="upgrade()">
|
||||||
<span class="glyphicon glyphicon-chevron-up"></span> 
|
<span class="glyphicon glyphicon-chevron-up"></span> 
|
||||||
<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
|
<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span translate>Edit</spanq <b class="caret"></b></a>
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-cog"></span></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="" ng-click="addRepo()"><span class="glyphicon glyphicon-hdd"></span> <span translate>Add Repository</span></a></li>
|
|
||||||
<li><a href="" ng-click="addNode()"><span class="glyphicon glyphicon-retweet"></span> <span translate>Add Node</span></a></li>
|
|
||||||
<li class="divider"></li>
|
|
||||||
<li><a href="" ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span> <span translate>Settings</span></a></li>
|
<li><a href="" ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span> <span translate>Settings</span></a></li>
|
||||||
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span> <span translate>Show ID</span></a></li>
|
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span> <span translate>Show ID</span></a></li>
|
||||||
<li class="divider"></li>
|
<li class="divider"></li>
|
||||||
@@ -156,195 +79,217 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel-group" id="repositories">
|
<div class="panel-group" id="repositories">
|
||||||
<div class="panel panel-{{repoClass(repo.ID)}}" ng-repeat="repo in repoList()">
|
<div class="panel panel-{{repoClass(repo.ID)}}" ng-repeat="repo in repoList()">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading" data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}" style="cursor: pointer">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}">
|
<span class="glyphicon glyphicon-hdd"></span> {{repo.ID}}
|
||||||
<span class="glyphicon glyphicon-hdd"></span> {{repo.ID}}
|
<span class="pull-right hidden-xs" ng-switch="repoStatus(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="unknown">Unknown</span>
|
<span translate ng-switch-when="stopped">Stopped</span>
|
||||||
<span translate ng-switch-when="stopped">Stopped</span>
|
<span translate ng-switch-when="scanning">Scanning</span>
|
||||||
<span translate ng-switch-when="scanning">Scanning</span>
|
<span ng-switch-when="syncing">
|
||||||
<span ng-switch-when="syncing">
|
<span translate>Syncing</span>
|
||||||
<span translate>Syncing</span>
|
({{syncPercentage(repo.ID)}}%)
|
||||||
({{syncPercentage(repo.ID)}}%)
|
|
||||||
</span>
|
|
||||||
<span ng-switch-when="idle">
|
|
||||||
<span translate>Idle</span>
|
|
||||||
({{syncPercentage(repo.ID)}}%)
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
<span ng-switch-when="idle">
|
||||||
|
<span translate>Idle</span>
|
||||||
|
({{syncPercentage(repo.ID)}}%)
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="repo-{{$index}}" class="panel-collapse collapse">
|
<div id="repo-{{$index}}" class="panel-collapse collapse" ng-class="{in: $index === 0}">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="table-responsive">
|
<table class="table table-condensed table-striped">
|
||||||
<table class="table table-condensed table-striped">
|
<tbody>
|
||||||
<tbody>
|
<tr>
|
||||||
<tr>
|
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Repository ID</span></th>
|
||||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Repository ID</span></th>
|
<td class="text-right">{{repo.ID}}</td>
|
||||||
<td class="text-right">{{repo.ID}}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th><span class="glyphicon glyphicon-folder-open"></span> <span translate>Folder</span></th>
|
||||||
<th><span class="glyphicon glyphicon-folder-open"></span> <span translate>Folder</span></th>
|
<td class="text-right">{{repo.Directory}}</td>
|
||||||
<td class="text-right">{{repo.Directory}}</td>
|
</tr>
|
||||||
</tr>
|
<tr ng-if="model[repo.ID].invalid">
|
||||||
<tr ng-if="model[repo.ID].invalid">
|
<th><span class="glyphicon glyphicon-warning-sign"></span> <span translate>Error</span></th>
|
||||||
<th><span class="glyphicon glyphicon-warning-sign"></span> <span translate>Error</span></th>
|
<td class="text-right">{{model[repo.ID].invalid}}</td>
|
||||||
<td class="text-right">{{model[repo.ID].invalid}}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th><span class="glyphicon glyphicon-globe"></span> <span translate>Global Repository</span></th>
|
||||||
<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>
|
<tr>
|
||||||
<tr>
|
<th><span class="glyphicon glyphicon-home"></span> <span translate>Local Repository</span></th>
|
||||||
<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>
|
<tr>
|
||||||
<tr>
|
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Out Of Sync</span></th>
|
||||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Out Of Sync</span></th>
|
<td class="text-right">
|
||||||
<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>
|
<span ng-if="model[repo.ID].needFiles == 0">0 <span translate>items</span>, 0 B</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><span class="glyphicon glyphicon-lock"></span> <span translate>Master Repo</span></th>
|
<th><span class="glyphicon glyphicon-lock"></span> <span translate>Master Repo</span></th>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span translate ng-if="repo.ReadOnly">Yes</span>
|
<span translate ng-if="repo.ReadOnly">Yes</span>
|
||||||
<span translate ng-if="!repo.ReadOnly">No</span>
|
<span translate ng-if="!repo.ReadOnly">No</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><span class="glyphicon glyphicon-unchecked"></span> <span translate>Ignore Permissions</span></th>
|
<th><span class="glyphicon glyphicon-unchecked"></span> <span translate>Ignore Permissions</span></th>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span translate ng-if="repo.IgnorePerms">Yes</span>
|
<span translate ng-if="repo.IgnorePerms">Yes</span>
|
||||||
<span translate ng-if="!repo.IgnorePerms">No</span>
|
<span translate ng-if="!repo.IgnorePerms">No</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th><span class="glyphicon glyphicon-share-alt"></span> <span translate>Shared With</span></th>
|
<th><span class="glyphicon glyphicon-refresh"></span> <span translate>Rescan Interval</span></th>
|
||||||
<td class="text-right">{{sharesRepo(repo)}}</td>
|
<td class="text-right">{{repo.RescanIntervalS}} s</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
<tr>
|
||||||
</table>
|
<th><span class="glyphicon glyphicon-share-alt"></span> <span translate>Shared With</span></th>
|
||||||
</div>
|
<td class="text-right">{{sharesRepo(repo)}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">
|
||||||
|
<button class="btn btn-sm btn-danger" ng-if="repo.ReadOnly && model[repo.ID].needFiles > 0" ng-click="override(repo.ID)" href=""><span class="glyphicon glyphicon-upload"></span> <span translate>Override Changes</span></button>
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
<a class="btn btn-sm btn-primary" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span> <span translate>Edit</span></a>
|
<button class="btn btn-sm btn-default" href="" ng-show="repoStatus(repo.ID) == 'idle'" ng-click="rescanRepo(repo.ID)"><span class="glyphicon glyphicon-refresh"></span> <span translate>Rescan</span></button>
|
||||||
<a class="btn btn-sm btn-danger" ng-if="repo.ReadOnly && model[repo.ID].needFiles > 0" ng-click="override(repo.ID)" href=""><span class="glyphicon glyphicon-upload"></span> <span translate>Override Changes</span></a>
|
<button class="btn btn-sm btn-default" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span> <span translate>Edit</span></button>
|
||||||
</span>
|
</span>
|
||||||
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-sm btn-default pull-right" ng-click="addRepo()"><span class="glyphicon glyphicon-plus"></span> <span translate>Add Repository</span></button>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
|
<hr class="visible-sm"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Node list (top right) -->
|
<!-- Node list (top right) -->
|
||||||
|
|
||||||
|
<!-- This node -->
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="panel-group" id="nodes">
|
<div class="panel panel-default" ng-repeat="nodeCfg in [thisNode()]">
|
||||||
<div class="panel panel-default" ng-repeat="nodeCfg in [thisNode()]">
|
<div class="panel-heading" data-toggle="collapse" href="#node-this" style="cursor: pointer">
|
||||||
<div class="panel-heading">
|
<h3 class="panel-title">
|
||||||
<h3 class="panel-title">
|
<span class="glyphicon glyphicon-home"></span> {{nodeName(nodeCfg)}}
|
||||||
<a data-toggle="collapse" data-parent="#nodes" href="#node-this"><span class="glyphicon glyphicon-home"></span> {{nodeName(nodeCfg)}}</a>
|
</h3>
|
||||||
</h3>
|
</div>
|
||||||
</div>
|
<div id="node-this" class="panel-collapse collapse in">
|
||||||
<div id="node-this" class="panel-collapse collapse in">
|
<div class="panel-body">
|
||||||
<div class="panel-body">
|
<table class="table table-condensed table-striped">
|
||||||
<div class="table-responsive">
|
<tbody>
|
||||||
<table class="table table-condensed table-striped">
|
<tr>
|
||||||
<tbody>
|
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Download Rate</span></th>
|
||||||
<tr>
|
<td class="text-right">{{connections['total'].inbps | metric}}bps ({{connections['total'].InBytesTotal | binary}}B)</td>
|
||||||
<th><span class="glyphicon glyphicon-th"></span> <span translate>RAM Utilization</span></th>
|
</tr>
|
||||||
<td class="text-right">{{system.sys | binary}}B</td>
|
<tr>
|
||||||
</tr>
|
<th><span class="glyphicon glyphicon-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
||||||
<tr>
|
<td class="text-right">{{connections['total'].outbps | metric}}bps ({{connections['total'].OutBytesTotal | binary}}B)</td>
|
||||||
<th><span class="glyphicon glyphicon-tasks"></span> <span translate>CPU Utilization</span></th>
|
</tr>
|
||||||
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
<tr>
|
||||||
</tr>
|
<th><span class="glyphicon glyphicon-th"></span> <span translate>RAM Utilization</span></th>
|
||||||
<tr>
|
<td class="text-right">{{system.sys | binary}}B</td>
|
||||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Download Rate</span></th>
|
</tr>
|
||||||
<td class="text-right">{{connections['total'].inbps | metric}}bps ({{connections['total'].InBytesTotal | binary}}B)</td>
|
<tr>
|
||||||
</tr>
|
<th><span class="glyphicon glyphicon-dashboard"></span> <span translate>CPU Utilization</span></th>
|
||||||
<tr>
|
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
||||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
</tr>
|
||||||
<td class="text-right">{{connections['total'].outbps | metric}}bps ({{connections['total'].OutBytesTotal | binary}}B)</td>
|
<tr ng-if="system.extAnnounceOK != undefined">
|
||||||
</tr>
|
<th><span class="glyphicon glyphicon-bullhorn"></span> <span translate>Global Discovery Server</span></th>
|
||||||
<tr ng-if="system.extAnnounceOK != undefined">
|
<td class="text-right">
|
||||||
<th><span class="glyphicon glyphicon-bullhorn"></span> <span translate>Announce Server</span></th>
|
<span class="data text-success" ng-if="system.extAnnounceOK"><span translate>Online</span></span>
|
||||||
<td class="text-right">
|
<span class="data text-danger" ng-if="!system.extAnnounceOK"><span translate>Offline</span></span>
|
||||||
<span class="data text-success" ng-if="system.extAnnounceOK"><span translate>Online</span></span>
|
</td>
|
||||||
<span class="data text-danger" ng-if="!system.extAnnounceOK"><span translate>Offline</span></span>
|
</tr>
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
||||||
<tr>
|
<td class="text-right">{{version}}</td>
|
||||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
</tr>
|
||||||
<td class="text-right">{{version}}</td>
|
</tbody>
|
||||||
</tr>
|
</table>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span> <span translate>Edit</span></a></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remote nodes -->
|
||||||
|
|
||||||
|
<div class="panel-group" id="nodes">
|
||||||
<div class="panel panel-{{nodeClass(nodeCfg)}}" ng-repeat="nodeCfg in otherNodes()">
|
<div class="panel panel-{{nodeClass(nodeCfg)}}" ng-repeat="nodeCfg in otherNodes()">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading" data-toggle="collapse" data-parent="#nodes" href="#node-{{$index}}" style="cursor: pointer">
|
||||||
<h3 class="panel-title">
|
<h3 class="panel-title">
|
||||||
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{$index}}">
|
<span class="glyphicon glyphicon-retweet"></span> {{nodeName(nodeCfg)}}
|
||||||
<span class="glyphicon glyphicon-retweet"></span>
|
<span class="pull-right hidden-xs">
|
||||||
{{nodeName(nodeCfg)}}
|
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total == 100">
|
||||||
<span class="pull-right hidden-xs">
|
<span translate>Up to Date</span> (100%)
|
||||||
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total == 100">
|
|
||||||
<span translate>Up to Date</span> (100%)
|
|
||||||
</span>
|
|
||||||
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total < 100">
|
|
||||||
<span translate>Syncing</span> ({{completion[nodeCfg.NodeID]._total | number:0}}%)
|
|
||||||
</span>
|
|
||||||
<span translate ng-if="!connections[nodeCfg.NodeID]">Disconnected</span>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total < 100">
|
||||||
|
<span translate>Syncing</span> ({{completion[nodeCfg.NodeID]._total | number:0}}%)
|
||||||
|
</span>
|
||||||
|
<span translate ng-if="!connections[nodeCfg.NodeID]">Disconnected</span>
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div id="node-{{$index}}" class="panel-collapse collapse">
|
<div id="node-{{$index}}" class="panel-collapse collapse">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="table-responsive">
|
<table class="table table-condensed table-striped">
|
||||||
<table class="table table-condensed table-striped">
|
<tbody>
|
||||||
<tbody>
|
<tr ng-if="connections[nodeCfg.NodeID]">
|
||||||
<tr>
|
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Download Rate</span></th>
|
||||||
<th><span class="glyphicon glyphicon-link"></span> <span translate>Address</span></th>
|
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps ({{connections[nodeCfg.NodeID].InBytesTotal | binary}}B)</td>
|
||||||
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
|
</tr>
|
||||||
</tr>
|
<tr ng-if="connections[nodeCfg.NodeID]">
|
||||||
<tr>
|
<th><span class="glyphicon glyphicon-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
||||||
<th><span class="glyphicon glyphicon-comment"></span> <span translate>Synchronization</span></th>
|
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps ({{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B)</td>
|
||||||
<td class="text-right">{{completion[nodeCfg.NodeID]._total | alwaysNumber | number:0}}%</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th><span class="glyphicon glyphicon-link"></span> <span translate>Address</span></th>
|
||||||
<th><span class="glyphicon glyphicon-compressed"></span> <span translate>Use Compression</span></th>
|
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
|
||||||
<td translate ng-if="nodeCfg.Compression" class="text-right">Yes</td>
|
</tr>
|
||||||
<td translate ng-if="!nodeCfg.Compression" class="text-right">No</td>
|
<tr ng-if="connections[nodeCfg.NodeID]">
|
||||||
</tr>
|
<th><span class="glyphicon glyphicon-comment"></span> <span translate>Synchronization</span></th>
|
||||||
<tr>
|
<td class="text-right">{{completion[nodeCfg.NodeID]._total | alwaysNumber | number:0}}%</td>
|
||||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Download Rate</span></th>
|
</tr>
|
||||||
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps ({{connections[nodeCfg.NodeID].InBytesTotal | binary}}B)</td>
|
<tr>
|
||||||
</tr>
|
<th><span class="glyphicon glyphicon-compressed"></span> <span translate>Use Compression</span></th>
|
||||||
<tr>
|
<td translate ng-if="nodeCfg.Compression" class="text-right">Yes</td>
|
||||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
<td translate ng-if="!nodeCfg.Compression" class="text-right">No</td>
|
||||||
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps ({{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B)</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th><span class="glyphicon glyphicon-thumbs-up"></span> <span translate>Introducer</span></th>
|
||||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
<td translate ng-if="nodeCfg.Introducer" class="text-right">Yes</td>
|
||||||
<td class="text-right">{{nodeVer(nodeCfg)}}</td>
|
<td translate ng-if="!nodeCfg.Introducer" class="text-right">No</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
<tr ng-if="connections[nodeCfg.NodeID]">
|
||||||
</table>
|
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
||||||
</div>
|
<td class="text-right">{{connections[nodeCfg.NodeID].ClientVersion}}</td>
|
||||||
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span> <span translate>Edit</span></a></span>
|
</tr>
|
||||||
|
<tr ng-if="!connections[nodeCfg.NodeID]">
|
||||||
|
<th><span class="glyphicon glyphicon-eye-open"></span> <span translate>Last seen</span></th>
|
||||||
|
<td translate ng-if="!stats[nodeCfg.NodeID].LastSeenDays || stats[nodeCfg.NodeID].LastSeenDays >= 365" class="text-right">Never</td>
|
||||||
|
<td ng-if="stats[nodeCfg.NodeID].LastSeenDays < 365" class="text-right">{{stats[nodeCfg.NodeID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer">
|
||||||
|
<span class="pull-right"><a class="btn btn-sm btn-default" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span> <span translate>Edit</span></a></span>
|
||||||
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-sm btn-default pull-right" ng-click="addNode()"><span class="glyphicon glyphicon-plus"></span> <span translate>Add Node</span></button>
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- /row -->
|
</div> <!-- /row -->
|
||||||
|
|
||||||
@@ -395,6 +340,8 @@
|
|||||||
<p><span translate>Syncthing is restarting.</span> <span translate>Please wait</span>...</p>
|
<p><span translate>Syncthing is restarting.</span> <span translate>Please wait</span>...</p>
|
||||||
</modal>
|
</modal>
|
||||||
|
|
||||||
|
<!-- Upgrading modal -->
|
||||||
|
|
||||||
<modal id="upgrading" icon="refresh" title="{{'Upgrading' | translate}}" status="info">
|
<modal id="upgrading" icon="refresh" title="{{'Upgrading' | translate}}" status="info">
|
||||||
<p><span translate>Syncthing is upgrading.</span> <span translate>Please wait</span>...</p>
|
<p><span translate>Syncthing is upgrading.</span> <span translate>Please wait</span>...</p>
|
||||||
</modal>
|
</modal>
|
||||||
@@ -414,7 +361,7 @@
|
|||||||
|
|
||||||
<!-- Node editor 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-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -437,7 +384,8 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate for="name">Node Name</label>
|
<label translate for="name">Node Name</label>
|
||||||
<input placeholder="Home Server" id="name" class="form-control" type="text" ng-model="currentNode.Name"></input>
|
<input placeholder="Home Server" id="name" class="form-control" type="text" ng-model="currentNode.Name"></input>
|
||||||
<p translate class="help-block">Shown instead of Node ID in the cluster status.</p>
|
<p translate ng-if="currentNode.NodeID == myID" class="help-block">Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.</p>
|
||||||
|
<p translate ng-if="currentNode.NodeID != myID" class="help-block">Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate for="addresses">Addresses</label>
|
<label translate for="addresses">Addresses</label>
|
||||||
@@ -449,14 +397,23 @@
|
|||||||
<label>
|
<label>
|
||||||
<input type="checkbox" ng-model="currentNode.Compression"> <span translate>Use Compression</span>
|
<input type="checkbox" ng-model="currentNode.Compression"> <span translate>Use Compression</span>
|
||||||
</label>
|
</label>
|
||||||
|
<p translate class="help-block">Compression is recommended in most setups.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ng-if="!editingSelf" class="form-group">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ng-model="currentNode.Introducer"> <span translate>Introducer</span>
|
||||||
|
</label>
|
||||||
|
<p translate class="help-block">Any nodes configured on an introducer node will be added to this node as well.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-primary" ng-click="saveNode()" ng-disabled="nodeEditor.$invalid"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
<button type="button" class="btn btn-primary btn-sm" ng-click="saveNode()" ng-disabled="nodeEditor.$invalid"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
||||||
<button ng-if="editingExisting && !editingSelf" type="button" class="btn btn-danger pull-left" ng-click="deleteNode()"><span class="glyphicon glyphicon-minus"></span> <span translate>Delete</span></button>
|
<button ng-if="editingExisting && !editingSelf" type="button" class="btn btn-danger pull-left btn-sm" ng-click="deleteNode()"><span class="glyphicon glyphicon-minus"></span> <span translate>Delete</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -464,7 +421,7 @@
|
|||||||
|
|
||||||
<!-- Repo editor modal -->
|
<!-- 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-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -482,17 +439,24 @@
|
|||||||
<span translate ng-if="repoEditor.repoID.$valid || repoEditor.repoID.$pristine">Short identifier for the repository. Must be the same on all cluster nodes.</span>
|
<span translate ng-if="repoEditor.repoID.$valid || repoEditor.repoID.$pristine">Short identifier for the repository. Must be the same on all cluster nodes.</span>
|
||||||
<span translate ng-if="repoEditor.repoID.$error.uniqueRepo">The repository ID must be unique.</span>
|
<span translate ng-if="repoEditor.repoID.$error.uniqueRepo">The repository ID must be unique.</span>
|
||||||
<span translate ng-if="repoEditor.repoID.$error.required && repoEditor.repoID.$dirty">The repository ID cannot be blank.</span>
|
<span translate ng-if="repoEditor.repoID.$error.required && repoEditor.repoID.$dirty">The repository ID cannot be blank.</span>
|
||||||
<span translate ng-if="repoEditor.repoID.$error.pattern && repoEditor.repoID.$dirty">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.</span>
|
<span translate ng-if="repoEditor.repoID.$error.pattern && repoEditor.repoID.$dirty">The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-class="{'has-error': repoEditor.repoPath.$invalid && repoEditor.repoPath.$dirty}">
|
<div class="form-group" ng-class="{'has-error': repoEditor.repoPath.$invalid && repoEditor.repoPath.$dirty}">
|
||||||
<label translate for="repoPath">Repository Path</label>
|
<label translate for="repoPath">Repository Path</label>
|
||||||
<input name="repoPath" placeholder="~/Documents" id="repoPath" class="form-control" type="text" ng-model="currentRepo.Directory" required></input>
|
<input name="repoPath" placeholder="~/Documents" ng-disabled="editingExisting" id="repoPath" class="form-control" type="text" ng-model="currentRepo.Directory" required></input>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
<span translate ng-if="repoEditor.repoPath.$valid || repoEditor.repoPath.$pristine">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</span> <code>{{system.tilde}}</code>.
|
<span translate ng-if="repoEditor.repoPath.$valid || repoEditor.repoPath.$pristine">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</span> <code>{{system.tilde}}</code>.
|
||||||
<span translate ng-if="repoEditor.repoPath.$error.required && repoEditor.repoPath.$dirty">The repository path cannot be blank.</span>
|
<span translate ng-if="repoEditor.repoPath.$error.required && repoEditor.repoPath.$dirty">The repository path cannot be blank.</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" ng-class="{'has-error': repoEditor.rescanIntervalS.$invalid && repoEditor.rescanIntervalS.$dirty}">
|
||||||
|
<label for="rescanIntervalS"><span translate>Rescan Interval</span> (s)</label>
|
||||||
|
<input name="rescanIntervalS" placeholder="60" id="rescanIntervalS" class="form-control" type="number" ng-model="currentRepo.RescanIntervalS" required min="5"></input>
|
||||||
|
<p class="help-block">
|
||||||
|
<span translate ng-if="!repoEditor.rescanIntervalS.$valid && repoEditor.rescanIntervalS.$dirty">The rescan interval must be at least 5 seconds.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -525,14 +489,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<label translate>File Versioning</label>
|
||||||
|
<div class="radio">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" ng-model="currentRepo.simpleFileVersioning"> <span translate>File Versioning</span>
|
<input type="radio" ng-model="currentRepo.FileVersioningSelector" value="none"> <span translate>No File Versioning</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" ng-model="currentRepo.FileVersioningSelector" value="simple"> <span translate>Simple File Versioning</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" ng-model="currentRepo.FileVersioningSelector" value="staggered"> <span translate>Staggered File Versioning</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p translate class="help-block">Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" ng-if="currentRepo.simpleFileVersioning" ng-class="{'has-error': repoEditor.simpleKeep.$invalid && repoEditor.simpleKeep.$dirty}">
|
<div class="form-group" ng-if="currentRepo.FileVersioningSelector=='simple'" ng-class="{'has-error': repoEditor.simpleKeep.$invalid && repoEditor.simpleKeep.$dirty}">
|
||||||
|
<p translate class="help-block">Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.</p>
|
||||||
<label translate for="simpleKeep">Keep Versions</label>
|
<label translate for="simpleKeep">Keep Versions</label>
|
||||||
<input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentRepo.simpleKeep" required min="1"></input>
|
<input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentRepo.simpleKeep" required min="1"></input>
|
||||||
<p class="help-block">
|
<p class="help-block">
|
||||||
@@ -541,16 +516,62 @@
|
|||||||
<span translate ng-if="repoEditor.simpleKeep.$error.min && repoEditor.simpleKeep.$dirty">You must keep at least one version.</span>
|
<span translate ng-if="repoEditor.simpleKeep.$error.min && repoEditor.simpleKeep.$dirty">You must keep at least one version.</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" ng-if="currentRepo.FileVersioningSelector=='staggered'" ng-class="{'has-error': repoEditor.staggeredMaxAge.$invalid && repoEditor.staggeredMaxAge.$dirty}">
|
||||||
|
<p class="help-block"><span translate>Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.</span> <span translate>Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.</span></p>
|
||||||
|
<p translate class="help-block">The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.</p>
|
||||||
|
<label translate for="staggeredMaxAge">Maximum Age</label>
|
||||||
|
<input name="staggeredMaxAge" id="staggeredMaxAge" class="form-control" type="number" ng-model="currentRepo.staggeredMaxAge" required></input>
|
||||||
|
<p class="help-block">
|
||||||
|
<span translate ng-if="repoEditor.staggeredMaxAge.$valid || repoEditor.staggeredMaxAge.$pristine">The maximum time to keep a version (in days, set to 0 to keep versions forever).</span>
|
||||||
|
<span translate ng-if="repoEditor.staggeredMaxAge.$error.required && repoEditor.staggeredMaxAge.$dirty">The maximum age must be a number and cannot be blank.</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-if="currentRepo.FileVersioningSelector == 'staggered'">
|
||||||
|
<label translate for="staggeredVersionsPath">Versions Path</label>
|
||||||
|
<input name="staggeredVersionsPath" placeholder="" id="staggeredVersionsPath" class="form-control" type="text" ng-model="currentRepo.staggeredVersionsPath"></input>
|
||||||
|
<p translate class="help-block">Path where versions should be stored (leave empty for the default .stversions folder in the repository).</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div translate ng-show="!editingExisting">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.</div>
|
<div translate ng-show="!editingExisting">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.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-primary" ng-click="saveRepo()" ng-disabled="repoEditor.$invalid"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
<button type="button" class="btn btn-primary btn-sm" ng-click="saveRepo()" ng-disabled="repoEditor.$invalid"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
||||||
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left" ng-click="deleteRepo()"><span class="glyphicon glyphicon-minus"></span> <span translate>Delete</span></button>
|
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left btn-sm" ng-click="deleteRepo()"><span class="glyphicon glyphicon-minus"></span> <span translate>Delete</span></button>
|
||||||
|
<button id="editIgnoresButton" ng-if="editingExisting" type="button" class="btn btn-default pull-left btn-sm" ng-click="editIgnores()"><span class="glyphicon glyphicon-eye-close"></span> <span translate>Ignore Patterns</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Ignores editor modal -->
|
||||||
|
|
||||||
|
<div id="editIgnores" class="modal fade" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title" translate>Ignore Patterns</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p translate>Enter ignore patterns, one per line.</p>
|
||||||
|
<textarea class="form-control" rows="15"></textarea>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<p class="small"><span translate>Quick guide to supported patterns</span> (<a href="https://discourse.syncthing.net/t/80" translate>full documentation</a>):</p>
|
||||||
|
<dl class="dl-horizontal dl-narrow small">
|
||||||
|
<dt><code>!</code></dt> <dd><span translate>Inversion of the given condition (i.e. do not exclude)</span></dd>
|
||||||
|
<dt><code>*</code></dt> <dd><span translate>Single level wildcard (matches within a directory only)</span></dd>
|
||||||
|
<dt><code>**</code></dt> <dd><span translate>Multi level wildcard (matches multiple directory levels)</span></dd>
|
||||||
|
<dt><code>//</code></dt> <dd><span translate>Comment, when used at the start of a line</span></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="pull-left"><span translate>Editing</span> <code>{{currentRepo.Directory}}/.stignore</code></div>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" data-dismiss="modal" ng-click="saveIgnores()"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
||||||
|
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -558,7 +579,7 @@
|
|||||||
|
|
||||||
<!-- Settings modal -->
|
<!-- 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-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -569,32 +590,22 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label translate for="NodeName">Node Name</label>
|
||||||
|
<input id="NodeName" class="form-control" type="text" ng-model="tmpOptions.NodeName">
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate for="ListenStr">Sync Protocol Listen Addresses</label>
|
<label translate for="ListenStr">Sync Protocol Listen Addresses</label>
|
||||||
<input id="ListenStr" class="form-control" type="text" ng-model="tmpOptions.ListenStr">
|
<input id="ListenStr" class="form-control" type="text" ng-model="tmpOptions.ListenStr">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
|
||||||
|
<input id="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.MaxRecvKbps">
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
|
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
|
||||||
<input id="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.MaxSendKbps">
|
<input id="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.MaxSendKbps">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label translate for="RescanIntervalS">Rescan Interval (s)</label>
|
|
||||||
<input id="RescanIntervalS" class="form-control" type="number" ng-model="tmpOptions.RescanIntervalS">
|
|
||||||
</div>
|
|
||||||
<!--
|
|
||||||
<div class="form-group">
|
|
||||||
<label translate for="ReconnectIntervalS">Reconnect Interval (s)</label>
|
|
||||||
<input id="ReconnectIntervalS" class="form-control" type="number" ng-model="tmpOptions.ReconnectIntervalS">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label translate for="ParallelRequests">Max Outstanding Requests</label>
|
|
||||||
<input id="ParallelRequests" class="form-control" type="number" ng-model="tmpOptions.ParallelRequests">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label translate for="MaxChangeKbps">Max File Change Rate (KiB/s)</label>
|
|
||||||
<input id="MaxChangeKbps" class="form-control" type="number" ng-model="tmpOptions.MaxChangeKbps">
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
@@ -610,10 +621,6 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label translate for="LocalAnnPort">Local Discovery Port</label>
|
|
||||||
<input ng-disabled="!tmpOptions.LocalAnnEnabled" id="LocalAnnPort" class="form-control" type="number" ng-model="tmpOptions.LocalAnnPort">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
@@ -643,7 +650,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<span translate>Use HTTPS for GUI</span> <input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS">
|
<span translate>Use HTTPS for GUI</span> <input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -657,7 +664,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<span translate>Anonymous Usage Reporting</span> <input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled">
|
<span translate>Anonymous Usage Reporting</span> <input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled"> (<a translate ng-click="showURPreview()" href="#">Preview</a>)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -675,8 +682,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-primary" ng-click="saveSettings()"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -684,21 +691,41 @@
|
|||||||
|
|
||||||
<!-- Usage report modal -->
|
<!-- 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-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header alert alert-success">
|
<div class="modal-header alert alert-success">
|
||||||
<h4 translate class="modal-title">Allow Anonymous Usage Reporting?</h4>
|
<h4 translate class="modal-title">Allow Anonymous Usage Reporting?</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<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>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>
|
<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>
|
<button translate type="button" class="btn btn-default btn-sm" ng-show="!reportPreview" ng-click="showReportPreview()">Preview Usage Report</button>
|
||||||
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
|
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-success" ng-click="acceptUR()"><span class="glyphicon glyphicon-ok"></span> <span translate>Yes</span></button>
|
<button type="button" class="btn btn-success btn-sm" ng-click="acceptUR()"><span class="glyphicon glyphicon-ok"></span> <span translate>Yes</span></button>
|
||||||
<button type="button" class="btn btn-danger" ng-click="declineUR()"><span class="glyphicon glyphicon-remove"></span> <span translate>No</span></button>
|
<button type="button" class="btn btn-danger btn-sm" ng-click="declineUR()"><span class="glyphicon glyphicon-remove"></span> <span translate>No</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Usage report preview modal -->
|
||||||
|
|
||||||
|
<div id="urPreview" class="modal fade" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header alert alert-success">
|
||||||
|
<h4 translate class="modal-title">Anonymous Usage Reporting</h4>
|
||||||
|
</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>
|
||||||
|
<pre><small>{{reportData | json}}</small></pre>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-success btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-ok"></span> <span translate>OK</span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -719,7 +746,7 @@
|
|||||||
<!-- About modal -->
|
<!-- About modal -->
|
||||||
|
|
||||||
<modal id="about" large="yes" close="yes" status="info" title="About">
|
<modal id="about" large="yes" close="yes" status="info" title="About">
|
||||||
<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>
|
<h1 class="text-center"><img alt="Syncthing" title="Syncthing" src="img/logo-text-256.png" style="vertical-align: -16px" height="100" width="366"/><br/><small>{{version}}</small></h1>
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
<p translate>Copyright © 2014 Jakob Borg and the following Contributors:</p>
|
<p translate>Copyright © 2014 Jakob Borg and the following Contributors:</p>
|
||||||
@@ -728,29 +755,33 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>Aaron Bieber</li>
|
<li>Aaron Bieber</li>
|
||||||
<li>Andrew Dunham</li>
|
<li>Andrew Dunham</li>
|
||||||
|
<li>Alexander Graf</li>
|
||||||
<li>Arthur Axel fREW Schmidt</li>
|
<li>Arthur Axel fREW Schmidt</li>
|
||||||
<li>Audrius Butkevicius</li>
|
<li>Audrius Butkevicius</li>
|
||||||
<li>Ben Sidhom</li>
|
<li>Ben Sidhom</li>
|
||||||
<li>Brandon Philips</li>
|
<li>Brandon Philips</li>
|
||||||
<li>Gilli Sigurdsson</li>
|
<li>Gilli Sigurdsson</li>
|
||||||
|
<li>James Patterson</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<ul>
|
<ul>
|
||||||
<li>James Patterson</li>
|
|
||||||
<li>Jens Diemer</li>
|
<li>Jens Diemer</li>
|
||||||
|
<li>Lode Hoste</li>
|
||||||
|
<li>Marcin Dziadus</li>
|
||||||
|
<li>Michael Tilli</li>
|
||||||
<li>Philippe Schommers</li>
|
<li>Philippe Schommers</li>
|
||||||
<li>Ryan Sullivan</li>
|
<li>Ryan Sullivan</li>
|
||||||
<li>Tully Robinson</li>
|
<li>Tully Robinson</li>
|
||||||
<li>Veeti Paananen</li>
|
<li>Veeti Paananen</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
<p translate>Syncthing includes the following software or portions thereof:</p>
|
<p translate>Syncthing includes the following software or portions thereof:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="http://golang.org/">The Go Programming Languange</a>, Copyright © 2012 The Go Authors.</li>
|
<li><a href="http://golang.org/">The Go Programming Language</a>, Copyright © 2012 The Go Authors.</li>
|
||||||
<li><a href="https://bitbucket.org/kardianos/osext">kardianos/osext</a>, Copyright © 2012 Daniel Theophanes.</li>
|
<li><a href="https://bitbucket.org/kardianos/osext">kardianos/osext</a>, Copyright © 2012 Daniel Theophanes.</li>
|
||||||
<li><a href="https://code.google.com/p/snappy-go/">snappy-go</a>, Copyright © 2011 The Snappy-Go Authors.</li>
|
<li><a href="https://code.google.com/p/snappy-go/">snappy-go</a>, Copyright © 2011 The Snappy-Go Authors.</li>
|
||||||
<li><a href="https://github.com/golang/groupcache">groupcache/lru</a>, Copyright © 2013 Google Inc.</li>
|
<li><a href="https://github.com/golang/groupcache">groupcache/lru</a>, Copyright © 2013 Google Inc.</li>
|
||||||
@@ -763,12 +794,12 @@
|
|||||||
</modal>
|
</modal>
|
||||||
|
|
||||||
|
|
||||||
<script src="angular.min.js"></script>
|
<script src="angular/angular.min.js"></script>
|
||||||
<script src="angular-translate.min.js"></script>
|
<script src="angular/angular-translate.min.js"></script>
|
||||||
<script src="angular-translate-loader.min.js"></script>
|
<script src="angular/angular-translate-loader.min.js"></script>
|
||||||
<script src="jquery-2.0.3.min.js"></script>
|
<script src="jquery/jquery-2.0.3.min.js"></script>
|
||||||
<script src="bootstrap/js/bootstrap.min.js"></script>
|
<script src="bootstrap/js/bootstrap.min.js"></script>
|
||||||
<script src="valid-langs.js"></script>
|
<script src="lang/valid-langs.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
All files in this directory are auto generated. Do not change any of
|
||||||
|
them. To contribute translations, please head over to
|
||||||
|
|
||||||
|
https://www.transifex.com/projects/p/syncthing/
|
||||||
|
|
||||||
|
Any updates made on Transifex will be automatically pulled into these
|
||||||
|
files.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"API Key": "API Ключ",
|
||||||
|
"About": "За Програмата",
|
||||||
|
"Add Node": "Добави Машина",
|
||||||
|
"Add Repository": "Добави Папка",
|
||||||
|
"Address": "Адрес",
|
||||||
|
"Addresses": "Адреси",
|
||||||
|
"Allow Anonymous Usage Reporting?": "Разреши анонимен доклад за ползване на програмата?",
|
||||||
|
"Announce Server": "Announce Server",
|
||||||
|
"Anonymous Usage Reporting": "Анонимен Доклад",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
|
"Bugs": "Бъгове",
|
||||||
|
"CPU Utilization": "Натоварване на Процесора",
|
||||||
|
"Close": "Затвори",
|
||||||
|
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
|
"Connection Error": "Грешка при Свързването",
|
||||||
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Правата запазени © 2014 Jakob Borg и следните Сътрудници:",
|
||||||
|
"Delete": "Изтрий",
|
||||||
|
"Disconnected": "Прекрати Връзката",
|
||||||
|
"Documentation": "Документация",
|
||||||
|
"Download Rate": "Скорост на Теглене",
|
||||||
|
"Edit": "Промени",
|
||||||
|
"Edit Node": "Промени Машината",
|
||||||
|
"Edit Repository": "Промени Папката",
|
||||||
|
"Editing": "Editing",
|
||||||
|
"Enable UPnP": "Включи UPnP",
|
||||||
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Въведи \"ip:port\" адреси разделени със запетая или \"dynamic\", за да извършиш автоматична връзка на адреси.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
|
"Error": "Грешка",
|
||||||
|
"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.": "Когато 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 Patterns": "Ignore Patterns",
|
||||||
|
"Ignore Permissions": "Игнорирай Права за Достъп",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Входящ Лимит на Скороста(KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
|
"Keep Versions": "Пази Версии",
|
||||||
|
"Last seen": "Последно видян",
|
||||||
|
"Latest Release": "Най-новата Версия",
|
||||||
|
"Local Discovery": "Локално Откриване",
|
||||||
|
"Local Discovery Port": "Порт за Локално Откриване",
|
||||||
|
"Local Repository": "Локална Папка",
|
||||||
|
"Master Repo": "Главна Папка",
|
||||||
|
"Max File Change Rate (KiB/s)": "Макс. Скорост на Промяна (KiB/s)",
|
||||||
|
"Max Outstanding Requests": "Макс. Неизпълени Заявки",
|
||||||
|
"Maximum Age": "Максимална Възраст",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
|
"Never": "Никога",
|
||||||
|
"No": "Не",
|
||||||
|
"No File Versioning": "Няма Файлови Версии",
|
||||||
|
"Node ID": "Код на Машината",
|
||||||
|
"Node Identification": "Идентификация на Машината",
|
||||||
|
"Node Name": "Име на Машината",
|
||||||
|
"Notice": "Известие",
|
||||||
|
"OK": "ОК",
|
||||||
|
"Offline": "Не е на линия",
|
||||||
|
"Online": "На линия",
|
||||||
|
"Out Of Sync": "Не Синхронизиран",
|
||||||
|
"Outgoing Rate Limit (KiB/s)": "Лимит на Изходящата Скорост (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": "Пътят до папката на този компютър. Ще бъде създадена ако не съществува. Символът тилда (~) може да бъде използван като заместител на",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Пътят, където версиите да бъдат складирани(остави празно за папката .stversions).",
|
||||||
|
"Please wait": "Моля изчакай",
|
||||||
|
"Preview": "Preview",
|
||||||
|
"Preview Usage Report": "Разгледай Доклада за Използване",
|
||||||
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
|
"RAM Utilization": "RAM Натоварване",
|
||||||
|
"Reconnect Interval (s)": "Интервал(и) на Свързване",
|
||||||
|
"Repository ID": "Идентификатор на Папката",
|
||||||
|
"Repository Master": "Главна Папка",
|
||||||
|
"Repository Path": "Път до Папката",
|
||||||
|
"Rescan": "Повторно Сканиране",
|
||||||
|
"Rescan Interval": "Интервал за Повторно Сканиране",
|
||||||
|
"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": "Покажи Идентификатора",
|
||||||
|
"Shown instead of Node ID in the cluster status.": "Покажи вмест ID-то на Компютъра в статус на клъстъра.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Покажи вмест ID-то на Компютъра в статус на клъстъра. Ще бъде предлагано на други комютри като име по подразбиране.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Покажи вмест ID-то на Компютъра в статус на клъстъра. Ще бъде обновено с името по подразбиране изпратено от другия компютър.",
|
||||||
|
"Shutdown": "Спри Програмата",
|
||||||
|
"Simple File Versioning": "Просто Файлови Версии",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
|
"Source Code": "Сорс Код",
|
||||||
|
"Staggered File Versioning": "Наслагващи се Файлови Версии",
|
||||||
|
"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.": "Въведни код на машината не е валиден. Трябва да бъде 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.": "Въведни код на машината не е валиден. Трябва да бъде 52 или 56 символа и да се състои от букви, цифри като интервалите и тиретата са пожелание.",
|
||||||
|
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Използва се следния интервал: за първия час се пази версия всеки 30 секунди, за първия ден се пази версия всеки час, за първите 30 дена се пази версия всеки ден, до максимума се пази една версия всяка седмица.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "Максималната възраст трябва да е число и не може д ае празна.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максималното време да се пазят весрсии (в дни, сложи 0, за да пазиш версии завинаги).",
|
||||||
|
"The node ID cannot be blank.": "Кодът на машината не може да бъде празен.",
|
||||||
|
"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).": "Кодът на машината, който си въвел може да бъде намерен в \"Промени > Покажи Идентификатора\". Интервалите и тиретата са пожелание(биват прескачани).",
|
||||||
|
"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.": "Полето идентификатор на папка не може д абъде празно.",
|
||||||
|
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and 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 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.": "Идентификаторът на папка трябва да бъде къс(64 символа или по-малко) състоящ се само от букви, цифри, точка(.), тире(-) и подчерта (_).",
|
||||||
|
"The repository ID must be unique.": "Идентификаторът на папката тряба да бъде уникален.",
|
||||||
|
"The repository path cannot be blank.": "Пътят до папката не може да бъде празен.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "Интервала за повторно сканиране трябва да бъде поне 5 секунди.",
|
||||||
|
"Unknown": "Неясен",
|
||||||
|
"Up to Date": "Актуален",
|
||||||
|
"Upgrade To {%version%}": "Обновен До {{version}}",
|
||||||
|
"Upgrading": "Обновяване",
|
||||||
|
"Upload Rate": "Скорост на Качване",
|
||||||
|
"Usage": "Употреба",
|
||||||
|
"Use Compression": "Използвай Компресиране",
|
||||||
|
"Use HTTPS for GUI": "Използвай HTTPS за Потребителския Интерфейс",
|
||||||
|
"Version": "Версия",
|
||||||
|
"Versions Path": "Път до Версиите",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версиите биват изтривани автоматично, когато са по-стари от максималната възраст или надминават броя файлове разрешени в даден интервал.",
|
||||||
|
"When adding a new 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.": "Когато добавяш нов идентификатор на папка помни, че той се използва за свързване на папките на различни машини. Главни/малки букви са от значение и трябва да са еднакви на всички машини.",
|
||||||
|
"Yes": "Да",
|
||||||
|
"You must keep at least one version.": "Трябва да пазиш поне една версия.",
|
||||||
|
"full documentation": "full documentation",
|
||||||
|
"items": "артикула"
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"API Key": "Clau API",
|
||||||
|
"About": "Sobre",
|
||||||
|
"Add Node": "Afegir Node",
|
||||||
|
"Add Repository": "Afegir Repositori",
|
||||||
|
"Address": "Adreça",
|
||||||
|
"Addresses": "Adreces",
|
||||||
|
"Allow Anonymous Usage Reporting?": "Permetre l'enviament anònim d'informes d'ús?",
|
||||||
|
"Announce Server": "Servidor d'anunciament",
|
||||||
|
"Anonymous Usage Reporting": "Informe anònim d'ús",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
|
"Bugs": "Bugs",
|
||||||
|
"CPU Utilization": "Utilització del CPU",
|
||||||
|
"Close": "Tancar",
|
||||||
|
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
|
"Connection Error": "Error de connexió",
|
||||||
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg i els següents contribuïdors",
|
||||||
|
"Delete": "Esborrar",
|
||||||
|
"Disconnected": "Desconnectat",
|
||||||
|
"Documentation": "Documentació",
|
||||||
|
"Download Rate": "Tasa de descarrega",
|
||||||
|
"Edit": "Editar",
|
||||||
|
"Edit Node": "Editar Node",
|
||||||
|
"Edit Repository": "Editar Repositori",
|
||||||
|
"Editing": "Editing",
|
||||||
|
"Enable UPnP": "Habilitat UPnP",
|
||||||
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduir, separat per comes, adreces \"ip:port\" o \"dynamic\" per descobrir automàticament les adreces.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
|
"Error": "Error",
|
||||||
|
"File Versioning": "Versionat de Fitxers",
|
||||||
|
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Els bits de permisos dels fitxers son ignorats quan es cerquen canvis. Utilitzar en sistemes de fitxers FAT.",
|
||||||
|
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Els fitxers es mouen amb l'estampat de la data a la carpeta .stversions quan son substituïts o esborrats per 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.": "Els fitxers estan protegits de canvis fets per altres nodes, però els canvis fets en aquest node seran enviats a la resta del cluster.",
|
||||||
|
"Folder": "Carpeta",
|
||||||
|
"GUI Authentication Password": "Contrasenya d'autenticació GUI",
|
||||||
|
"GUI Authentication User": "Usuari d'autenticació GUI",
|
||||||
|
"GUI Listen Addresses": "Adreça d'escolta del GUI",
|
||||||
|
"Generate": "Generar",
|
||||||
|
"Global Discovery": "Descobriment Global",
|
||||||
|
"Global Discovery Server": "Servidor de Descobriment Global",
|
||||||
|
"Global Repository": "Repositori Global",
|
||||||
|
"Idle": "Inactiu",
|
||||||
|
"Ignore Patterns": "Ignore Patterns",
|
||||||
|
"Ignore Permissions": "Ignora Permisos",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
|
"Keep Versions": "Mantenir Versions",
|
||||||
|
"Last seen": "Vist per última vegada",
|
||||||
|
"Latest Release": "Última publicació",
|
||||||
|
"Local Discovery": "Descobriment Local",
|
||||||
|
"Local Discovery Port": "Port de Descobriment Local",
|
||||||
|
"Local Repository": "Repositori Local",
|
||||||
|
"Master Repo": "Rep Master",
|
||||||
|
"Max File Change Rate (KiB/s)": "Tasa Màxima d'intercanvi de fitxer (KiB/s)",
|
||||||
|
"Max Outstanding Requests": "Màxim de Peticions Pendents",
|
||||||
|
"Maximum Age": "Antiguitat Màxima",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
|
"Never": "Mai",
|
||||||
|
"No": "No",
|
||||||
|
"No File Versioning": "Sense Versionat de Fitxer",
|
||||||
|
"Node ID": "ID del Node",
|
||||||
|
"Node Identification": "Identificació del Node",
|
||||||
|
"Node Name": "Nom Del Node",
|
||||||
|
"Notice": "Avís",
|
||||||
|
"OK": "OK",
|
||||||
|
"Offline": "Desconnectat",
|
||||||
|
"Online": "Connectat",
|
||||||
|
"Out Of Sync": "Fora de la Sincronització",
|
||||||
|
"Outgoing Rate Limit (KiB/s)": "Tasa Límit de Sortida (KiB/s)",
|
||||||
|
"Override Changes": "Sobreescriure Canvis",
|
||||||
|
"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": "Ruta del repositori a l'equip local. Si no existeix serà creada. El caràcter (~) es pot fer servir com a drecera de",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Ruta on les versions s'haurien de guardar (deixa-ho buit per fer servir el directori .stversions per defecte al repositori)",
|
||||||
|
"Please wait": "Si-us-plau espera",
|
||||||
|
"Preview": "Preview",
|
||||||
|
"Preview Usage Report": "Vista Prèvia de l'Informe d'Ús",
|
||||||
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
|
"RAM Utilization": "Utilització de la RAM",
|
||||||
|
"Reconnect Interval (s)": "Interval de Reconnexió (s)",
|
||||||
|
"Repository ID": "ID del Repositori",
|
||||||
|
"Repository Master": "Repositori Mestre",
|
||||||
|
"Repository Path": "Ruta del Repositori",
|
||||||
|
"Rescan": "Re-escanejar",
|
||||||
|
"Rescan Interval": "Interval de re-escaneig",
|
||||||
|
"Rescan Interval (s)": "Interval de re-escaneig (s)",
|
||||||
|
"Restart": "Reiniciar",
|
||||||
|
"Restart Needed": "És Necessari Reiniciar",
|
||||||
|
"Restarting": "Reiniciant",
|
||||||
|
"Save": "Guardar",
|
||||||
|
"Scanning": "Escanejant",
|
||||||
|
"Select the nodes to share this repository with.": "Seleccionar els nodes amb els que es comparteix el repositori.",
|
||||||
|
"Settings": "Preferències",
|
||||||
|
"Share With Nodes": "Compartir Amb Els Nodes",
|
||||||
|
"Shared With": "Compartir Amb",
|
||||||
|
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identificador curt pel repositori. Ha de ser el mateix per tots els nodes del cluster.",
|
||||||
|
"Show ID": "Mostrar ID",
|
||||||
|
"Shown instead of Node ID in the cluster status.": "Mostrat en comptes del ID del Node en l'estat del cluster.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Mostrat en comptes del ID del Node en l'estat del cluster. Serà advertit als altres nodes com un nom opcional per defecte.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Mostrat en comptes del ID del Node en l'estat del cluster. S'actualitzara al nom del node si es deixa buit.",
|
||||||
|
"Shutdown": "Apagar",
|
||||||
|
"Simple File Versioning": "Versionat de Fitxers Senzill",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
|
"Source Code": "Codi Font",
|
||||||
|
"Staggered File Versioning": "Versionat de Fitxers Esglaonat",
|
||||||
|
"Start Browser": "Arrancar Navegador",
|
||||||
|
"Stopped": "Aturat",
|
||||||
|
"Support / Forum": "Suport / Fòrum",
|
||||||
|
"Sync Protocol Listen Addresses": "Adreça d'escolta del Protocol Sync",
|
||||||
|
"Synchronization": "Sincronització",
|
||||||
|
"Syncing": "Synthing",
|
||||||
|
"Syncthing has been shut down.": "S'ha aturat el synthing.",
|
||||||
|
"Syncthing includes the following software or portions thereof:": "Syncthing inclou el següent programari o parts dels mateixos:",
|
||||||
|
"Syncthing is restarting.": "Reiniciant syncthing.",
|
||||||
|
"Syncthing is upgrading.": "Actualitzant syncthing.",
|
||||||
|
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Synthing sembla parat, o hi ha algun problema amb la connexió a Internet. Reintentant...",
|
||||||
|
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan públicament disponibles a {{url}}.",
|
||||||
|
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració s'ha guardar però no s'ha activat. S'ha de reiniciar el synthing per activar la nova configuració.",
|
||||||
|
"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.": "L'informe d'ús encriptat s'envia diàriament. Es fa servir per rastrejar plataformes habituals, mides de repositoris i versions de l'aplicació. Si es canvia el conjunt de dades reportades es demanarà amb aquest diàleg de nou.",
|
||||||
|
"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 del Node introduït no sembla vàlid. Hauria de tenir 52 caràcters amb lletres i números, els espais i les barres son opcionals.",
|
||||||
|
"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 del Node introduït no sembla vàlid. Hauria de tenir 52 o 56 caràcters amb lletres i números, els espais i les barres son opcionals.",
|
||||||
|
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Es fan servir els següents intervals: per la primera hora es manté una versió cada 30 segons, pel primer dia es manté una versió cada hora, pel primer cada 30 dies es manté una versió cada dia, fins el màxim d'antiguitat es manté una versió cada setmana.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "La màxima antiguitat ha de ser un número i no pot estar en blanc.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Temps màxim en mantenir una versió (en dies, si es deixa en 0 es mantenen les versions per sempre).",
|
||||||
|
"The node ID cannot be blank.": "El ID del node no pot estar en blanc.",
|
||||||
|
"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 del node per introduir aquí es pot trobar al diàleg \"Editar > Mostrar ID\" en l'altre node. Els espais i les barres son opcionals (s'ignoren).",
|
||||||
|
"The number of old versions to keep, per file.": "El nombre de versions antigues que es mantenen per fitxer.",
|
||||||
|
"The number of versions must be a number and cannot be blank.": "El nombre de versions ha de ser un número i no es pot deixar en blanc.",
|
||||||
|
"The repository ID cannot be blank.": "El ID del repositori no pot estar en blanc.",
|
||||||
|
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and 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 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.": "El ID del repositori ha de ser un identificador curt (64 caràcters o menys) format només per lletres, nombres i el punt (.), barra (-) i barra baixa (_).",
|
||||||
|
"The repository ID must be unique.": "El ID del repositori ha de ser únic",
|
||||||
|
"The repository path cannot be blank.": "La carpeta del repositori no pot estar en blanc.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "El interval de re-escaneig ha de ser com a mínim de 5 segons.",
|
||||||
|
"Unknown": "Desconegut",
|
||||||
|
"Up to Date": "Actualitzat",
|
||||||
|
"Upgrade To {%version%}": "Actualitzar a {{version}}",
|
||||||
|
"Upgrading": "Actualitzant",
|
||||||
|
"Upload Rate": "Tasa de Pujada",
|
||||||
|
"Usage": "Ús",
|
||||||
|
"Use Compression": "Utilitza compressió",
|
||||||
|
"Use HTTPS for GUI": "Utilitzar HTTPS pel GUI",
|
||||||
|
"Version": "Versió",
|
||||||
|
"Versions Path": "Carpeta de les Versions",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions son automàticament eliminades si son més antigues que el màxim d'antiguitat o si excedeixen del nombre de fitxers permesos en un interval.",
|
||||||
|
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quan s'afegeix un nou node recorda que aquest node s'ha d'afegir tambe a l'altre banda.",
|
||||||
|
"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.": "Quan s'afegeix un nou repositori recorda que el ID del repositori s'utilitza per lligar repositoris entre nodes. Es distingeix entre majúscules i minúscules i ha de ser exactament iguals entre tots els nodes.",
|
||||||
|
"Yes": "Si",
|
||||||
|
"You must keep at least one version.": "Has de mantenir com a mínim una versió.",
|
||||||
|
"full documentation": "full documentation",
|
||||||
|
"items": "Elements"
|
||||||
|
}
|
||||||
@@ -8,9 +8,12 @@
|
|||||||
"Allow Anonymous Usage Reporting?": "Tillad anonym brugerstatistik?",
|
"Allow Anonymous Usage Reporting?": "Tillad anonym brugerstatistik?",
|
||||||
"Announce Server": "Opslagsserver",
|
"Announce Server": "Opslagsserver",
|
||||||
"Anonymous Usage Reporting": "Anonym brugerstatistik",
|
"Anonymous Usage Reporting": "Anonym brugerstatistik",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
"Bugs": "Fejl",
|
"Bugs": "Fejl",
|
||||||
"CPU Utilization": "CPU-forbrug",
|
"CPU Utilization": "CPU-forbrug",
|
||||||
"Close": "Luk",
|
"Close": "Luk",
|
||||||
|
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
"Connection Error": "Tilslutnings fejl",
|
"Connection Error": "Tilslutnings fejl",
|
||||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg og følgende bidragsydere:",
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg og følgende bidragsydere:",
|
||||||
"Delete": "Slet",
|
"Delete": "Slet",
|
||||||
@@ -20,8 +23,10 @@
|
|||||||
"Edit": "Rediger",
|
"Edit": "Rediger",
|
||||||
"Edit Node": "Rediger node",
|
"Edit Node": "Rediger node",
|
||||||
"Edit Repository": "Rediger lager",
|
"Edit Repository": "Rediger lager",
|
||||||
|
"Editing": "Editing",
|
||||||
"Enable UPnP": "Anvend UPnP",
|
"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.",
|
"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.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
"Error": "Fejl",
|
"Error": "Fejl",
|
||||||
"File Versioning": "Filversionering",
|
"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.",
|
"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.",
|
||||||
@@ -36,8 +41,13 @@
|
|||||||
"Global Discovery Server": "Global opslagsserver",
|
"Global Discovery Server": "Global opslagsserver",
|
||||||
"Global Repository": "Global lagring",
|
"Global Repository": "Global lagring",
|
||||||
"Idle": "Inaktiv",
|
"Idle": "Inaktiv",
|
||||||
|
"Ignore Patterns": "Ignore Patterns",
|
||||||
"Ignore Permissions": "Ignorér filrettigheder",
|
"Ignore Permissions": "Ignorér filrettigheder",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
"Keep Versions": "Behold versioner",
|
"Keep Versions": "Behold versioner",
|
||||||
|
"Last seen": "Last seen",
|
||||||
"Latest Release": "Seneste udgivelse",
|
"Latest Release": "Seneste udgivelse",
|
||||||
"Local Discovery": "Lokal opslag",
|
"Local Discovery": "Lokal opslag",
|
||||||
"Local Discovery Port": "Lokal opslagsport",
|
"Local Discovery Port": "Lokal opslagsport",
|
||||||
@@ -45,7 +55,11 @@
|
|||||||
"Master Repo": "Hovedlagring",
|
"Master Repo": "Hovedlagring",
|
||||||
"Max File Change Rate (KiB/s)": "Højeste filændringshastighed (KiB/s)",
|
"Max File Change Rate (KiB/s)": "Højeste filændringshastighed (KiB/s)",
|
||||||
"Max Outstanding Requests": "Parallelitet",
|
"Max Outstanding Requests": "Parallelitet",
|
||||||
|
"Maximum Age": "Maximum Age",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
|
"Never": "Never",
|
||||||
"No": "Nej",
|
"No": "Nej",
|
||||||
|
"No File Versioning": "No File Versioning",
|
||||||
"Node ID": "Node ID",
|
"Node ID": "Node ID",
|
||||||
"Node Identification": "Node identifikation",
|
"Node Identification": "Node identifikation",
|
||||||
"Node Name": "Nodenavn",
|
"Node Name": "Nodenavn",
|
||||||
@@ -57,13 +71,18 @@
|
|||||||
"Outgoing Rate Limit (KiB/s)": "Udgående hastighedsbegrænsning (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Udgående hastighedsbegrænsning (KiB/s)",
|
||||||
"Override Changes": "Overskriv ændringer",
|
"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",
|
"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",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Path where versions should be stored (leave empty for the default .stversions folder in the repository).",
|
||||||
"Please wait": "Vent venligst",
|
"Please wait": "Vent venligst",
|
||||||
|
"Preview": "Preview",
|
||||||
"Preview Usage Report": "Forhåndsvisning af forbrugsrapport",
|
"Preview Usage Report": "Forhåndsvisning af forbrugsrapport",
|
||||||
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
"RAM Utilization": "RAM-forbrug",
|
"RAM Utilization": "RAM-forbrug",
|
||||||
"Reconnect Interval (s)": "Gentilslutningsinterval (s)",
|
"Reconnect Interval (s)": "Gentilslutningsinterval (s)",
|
||||||
"Repository ID": "Lagrings-ID",
|
"Repository ID": "Lagrings-ID",
|
||||||
"Repository Master": "Hovedlagring",
|
"Repository Master": "Hovedlagring",
|
||||||
"Repository Path": "Sti til lagring",
|
"Repository Path": "Sti til lagring",
|
||||||
|
"Rescan": "Rescan",
|
||||||
|
"Rescan Interval": "Rescan Interval",
|
||||||
"Rescan Interval (s)": "Genscanningsinterval (s)",
|
"Rescan Interval (s)": "Genscanningsinterval (s)",
|
||||||
"Restart": "Genstart",
|
"Restart": "Genstart",
|
||||||
"Restart Needed": "Programmet kræver genstart",
|
"Restart Needed": "Programmet kræver genstart",
|
||||||
@@ -77,8 +96,13 @@
|
|||||||
"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.",
|
"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",
|
"Show ID": "Vis ID",
|
||||||
"Shown instead of Node ID in the cluster status.": "Vises i stedet for node-ID under clusterstatus.",
|
"Shown instead of Node ID in the cluster status.": "Vises i stedet for node-ID under clusterstatus.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.",
|
||||||
"Shutdown": "Luk ned",
|
"Shutdown": "Luk ned",
|
||||||
|
"Simple File Versioning": "Simple File Versioning",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
"Source Code": "Kildekode",
|
"Source Code": "Kildekode",
|
||||||
|
"Staggered File Versioning": "Staggered File Versioning",
|
||||||
"Start Browser": "Start browser",
|
"Start Browser": "Start browser",
|
||||||
"Stopped": "Stoppet",
|
"Stopped": "Stoppet",
|
||||||
"Support / Forum": "Support / Forum",
|
"Support / Forum": "Support / Forum",
|
||||||
@@ -95,14 +119,19 @@
|
|||||||
"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 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 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 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
|
||||||
"The node ID cannot be blank.": "Node-ID'et kan ikke være blankt.",
|
"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 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 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 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 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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and 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.": "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 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 ID must be unique.": "Lagrings-ID'et skal være unikt.",
|
||||||
"The repository path cannot be blank.": "Lagringsstien kan ikke være blank.",
|
"The repository path cannot be blank.": "Lagringsstien kan ikke være blank.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "The rescan interval must be at least 5 seconds.",
|
||||||
"Unknown": "Ukendt",
|
"Unknown": "Ukendt",
|
||||||
"Up to Date": "Fuldt opdateret",
|
"Up to Date": "Fuldt opdateret",
|
||||||
"Upgrade To {%version%}": "Opgradér til {{version}}",
|
"Upgrade To {%version%}": "Opgradér til {{version}}",
|
||||||
@@ -112,9 +141,12 @@
|
|||||||
"Use Compression": "Anvend komprimering",
|
"Use Compression": "Anvend komprimering",
|
||||||
"Use HTTPS for GUI": "Anvend HTTPS til GUI adgang",
|
"Use HTTPS for GUI": "Anvend HTTPS til GUI adgang",
|
||||||
"Version": "Version",
|
"Version": "Version",
|
||||||
|
"Versions Path": "Versions Path",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.",
|
||||||
"When adding a new 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 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.",
|
"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",
|
"Yes": "Ja",
|
||||||
"You must keep at least one version.": "Du skal beholde mindst én version.",
|
"You must keep at least one version.": "Du skal beholde mindst én version.",
|
||||||
|
"full documentation": "full documentation",
|
||||||
"items": "poster"
|
"items": "poster"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"API Key": "API-Schlüssel",
|
"API Key": "API-Key",
|
||||||
"About": "Über",
|
"About": "Über Syncthing",
|
||||||
"Add Node": "Knoten hinzufügen",
|
"Add Node": "Knoten hinzufügen",
|
||||||
"Add Repository": "Verzeichnis hinzufügen",
|
"Add Repository": "Verzeichnis hinzufügen",
|
||||||
"Address": "Adresse",
|
"Address": "Adresse",
|
||||||
@@ -8,44 +8,58 @@
|
|||||||
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsstatistiken erlauben?",
|
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsstatistiken erlauben?",
|
||||||
"Announce Server": "Ankündigungs-Server",
|
"Announce Server": "Ankündigungs-Server",
|
||||||
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
|
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
"Bugs": "Fehler",
|
"Bugs": "Fehler",
|
||||||
"CPU Utilization": "Prozessorauslastung",
|
"CPU Utilization": "Prozessorauslastung",
|
||||||
"Close": "Schließen",
|
"Close": "Schließen",
|
||||||
|
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
"Connection Error": "Verbindungsfehler",
|
"Connection Error": "Verbindungsfehler",
|
||||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg und folgende Unterstützer:",
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg und folgende Unterstützer:",
|
||||||
"Delete": "Löschen",
|
"Delete": "Löschen",
|
||||||
"Disconnected": "Verbindung getrennt",
|
"Disconnected": "Verbindung getrennt",
|
||||||
"Documentation": "Dokumentation",
|
"Documentation": "Dokumentation",
|
||||||
"Download Rate": "Downloadgeschwindigkeit",
|
"Download Rate": "Downloadgeschwindigkeit",
|
||||||
"Edit": "Bearbeiten",
|
"Edit": "Einstellungen bearbeiten",
|
||||||
"Edit Node": "Knoten bearbeiten",
|
"Edit Node": "Knoten bearbeiten",
|
||||||
"Edit Repository": "Verzeichnis ändern",
|
"Edit Repository": "Verzeichnis ändern",
|
||||||
"Enable UPnP": "Aktiviere UPnP",
|
"Editing": "Bearbeitung",
|
||||||
|
"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.",
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Trage durch ein Komma getrennte \"IP:Port\" Adressen oder \"dynamic\" ein um automatische Adresserkennung durchzuführen.",
|
||||||
|
"Enter ignore patterns, one per line.": "Geben Sie Ignoriermuster ein, eines pro Zeile.",
|
||||||
"Error": "Fehler",
|
"Error": "Fehler",
|
||||||
"File Versioning": "Dateiversionierung",
|
"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.",
|
"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.",
|
"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",
|
"Folder": "Ordner",
|
||||||
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
|
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
|
||||||
"GUI Authentication User": "Nutzername 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",
|
"GUI Listen Addresses": "Adresse(n) für die Benutzeroberfläche",
|
||||||
"Generate": "Generiere",
|
"Generate": "Generieren",
|
||||||
"Global Discovery": "Globale Auffindung",
|
"Global Discovery": "Globale Auffindung",
|
||||||
"Global Discovery Server": "Globaler Auffindungsserver",
|
"Global Discovery Server": "Globaler Auffindungsserver",
|
||||||
"Global Repository": "Globales Verzeichnis",
|
"Global Repository": "Globales Verzeichnis",
|
||||||
"Idle": "Untätig",
|
"Idle": "Untätig",
|
||||||
|
"Ignore Patterns": "Ignoriermuster",
|
||||||
"Ignore Permissions": "Berechtigungen ignorieren",
|
"Ignore Permissions": "Berechtigungen ignorieren",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Eingehendes Datenratelimit (KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Umkehrung der angegebenen Bedingung (z.B. schließe nicht aus)",
|
||||||
"Keep Versions": "Versionen erhalten",
|
"Keep Versions": "Versionen erhalten",
|
||||||
|
"Last seen": "Zuletzt online",
|
||||||
"Latest Release": "Letzte Veröffentlichung",
|
"Latest Release": "Letzte Veröffentlichung",
|
||||||
"Local Discovery": "Lokale Auffindung",
|
"Local Discovery": "Lokale Auffindung",
|
||||||
"Local Discovery Port": "Lokaler Aufindungsport",
|
"Local Discovery Port": "Lokaler Aufindungsport",
|
||||||
"Local Repository": "Lokales Verzeichnis",
|
"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 File Change Rate (KiB/s)": "Maximale Datenänderungsrate (KiB/s)",
|
||||||
"Max Outstanding Requests": "Max. ausstehende Anfragen",
|
"Max Outstanding Requests": "Max. ausstehende Anfragen",
|
||||||
|
"Maximum Age": "Höchstalter",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Verschachteltes Maskenzeichen (wird für verschachtelte Verzeichnisse verwendet)",
|
||||||
|
"Never": "Nie",
|
||||||
"No": "Nein",
|
"No": "Nein",
|
||||||
|
"No File Versioning": "Keine Dateiversionierung",
|
||||||
"Node ID": "Knoten-ID",
|
"Node ID": "Knoten-ID",
|
||||||
"Node Identification": "Knoten Identifikation",
|
"Node Identification": "Knoten Identifikation",
|
||||||
"Node Name": "Knoten-Name",
|
"Node Name": "Knoten-Name",
|
||||||
@@ -57,19 +71,24 @@
|
|||||||
"Outgoing Rate Limit (KiB/s)": "Ausgehendes Datenratelimit (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Ausgehendes Datenratelimit (KiB/s)",
|
||||||
"Override Changes": "Änderungen überschreiben",
|
"Override Changes": "Änderungen überschreiben",
|
||||||
"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": "Pfad zum Verzeichnis auf dem lokalen Rechner. Wird erzeugt, wenn es nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
|
"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": "Pfad zum Verzeichnis auf dem lokalen Rechner. Wird erzeugt, wenn es nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Pfad in dem die Versionen gespeichert werden sollen (ohne Angabe wird der Ordner .stversions im Verzeichnis verwendet).",
|
||||||
"Please wait": "Bitte warten",
|
"Please wait": "Bitte warten",
|
||||||
|
"Preview": "Vorschau",
|
||||||
"Preview Usage Report": "Vorschau des Nutzungsberichts",
|
"Preview Usage Report": "Vorschau des Nutzungsberichts",
|
||||||
|
"Quick guide to supported patterns": "Schnellanleitung zu den unterstützten Suchstrukturen",
|
||||||
"RAM Utilization": "Verwendeter Arbeitsspeicher",
|
"RAM Utilization": "Verwendeter Arbeitsspeicher",
|
||||||
"Reconnect Interval (s)": "Wiederverbindungsintervall (s)",
|
"Reconnect Interval (s)": "Wiederverbindungsintervall (s)",
|
||||||
"Repository ID": "Verzeichnis-ID",
|
"Repository ID": "Verzeichnis-ID",
|
||||||
"Repository Master": "Keine Veränderungen zulassen",
|
"Repository Master": "Originalverzeichnis",
|
||||||
"Repository Path": "Pfad zum Verzeichnis",
|
"Repository Path": "Pfad zum Verzeichnis",
|
||||||
|
"Rescan": "Überprüfen",
|
||||||
|
"Rescan Interval": "Suchintervall",
|
||||||
"Rescan Interval (s)": "Suchintervall (s)",
|
"Rescan Interval (s)": "Suchintervall (s)",
|
||||||
"Restart": "Neustart",
|
"Restart": "Neustart",
|
||||||
"Restart Needed": "Neustart notwendig",
|
"Restart Needed": "Neustart notwendig",
|
||||||
"Restarting": "Wird neu gestartet",
|
"Restarting": "Wird neu gestartet",
|
||||||
"Save": "Speichern",
|
"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.",
|
"Select the nodes to share this repository with.": "Wähle die Knoten aus, mit denen du dieses Verzeichnis teilen willst.",
|
||||||
"Settings": "Einstellungen",
|
"Settings": "Einstellungen",
|
||||||
"Share With Nodes": "Teile mit diesen Knoten",
|
"Share With Nodes": "Teile mit diesen Knoten",
|
||||||
@@ -77,8 +96,13 @@
|
|||||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Kurze ID für das Verzeichnis. Muss auf allen Verbunds-Knoten gleich sein.",
|
"Short identifier for the repository. Must be the same on all cluster nodes.": "Kurze ID für das Verzeichnis. Muss auf allen Verbunds-Knoten gleich sein.",
|
||||||
"Show ID": "ID anzeigen",
|
"Show ID": "ID anzeigen",
|
||||||
"Shown instead of Node ID in the cluster status.": "Wird anstatt der Knoten-ID im Verbunds-Status angezeigt.",
|
"Shown instead of Node ID in the cluster status.": "Wird anstatt der Knoten-ID im Verbunds-Status angezeigt.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Wird anstatt der Knoten-ID im Verbunds-Status angezeigt. Wird als optionaler Standardname an andere Knoten bekannt gegeben.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Wird anstatt der Knoten-ID im Verbunds-Status angezeigt. Wird auf den Namen aktualisiert, den der Knoten angibt.",
|
||||||
"Shutdown": "Herunterfahren",
|
"Shutdown": "Herunterfahren",
|
||||||
"Source Code": "Quellcode",
|
"Simple File Versioning": "Einfache Dateiversionierung",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Einzelnes Maskenzeichen (wird für ein einzelnes Verzeichnis verwendet)",
|
||||||
|
"Source Code": "Sourcecode",
|
||||||
|
"Staggered File Versioning": "Stufenweise Dateiversionierung",
|
||||||
"Start Browser": "Starte Browser",
|
"Start Browser": "Starte Browser",
|
||||||
"Stopped": "Gestoppt",
|
"Stopped": "Gestoppt",
|
||||||
"Support / Forum": "Support / Forum",
|
"Support / Forum": "Support / Forum",
|
||||||
@@ -90,19 +114,24 @@
|
|||||||
"Syncthing is restarting.": "Syncthing wird neu gestartet",
|
"Syncthing is restarting.": "Syncthing wird neu gestartet",
|
||||||
"Syncthing is upgrading.": "Syncthing wird aktualisiert",
|
"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...",
|
"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 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 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 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 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Es wird in folgenden Abständen versioniert: in der ersten Stunde wird alle 30 Sekunden eine Version behalten, am ersten Tag eine jede Stunde, in den ersten 30 Tagen eine jeden Tag, danach wird bis zum Höchstalter eine Version pro Woche beibehalten.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "Das Höchstalter muss angegeben werden und eine Zahl sein.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Die längste Zeit, die alte Versionen vorgehalten werden (in Tagen, 0 bedeutet, alte Versionen für immer zu behalten).",
|
||||||
"The node ID cannot be blank.": "Die Knoten-ID darf nicht leer sein.",
|
"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 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 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 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 dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis-ID muss eine kurze Kennung (64 Zeichen oder weniger) sein. Sie kann nur aus Buchstaben, Zahlen und den Punkt- (.), Strich- (-), und Unterstrich- (_) Zeichen bestehen.",
|
||||||
"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 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.",
|
"The repository ID must be unique.": "Die Verzeichnis-ID muss eindeutig sein.",
|
||||||
"The repository path cannot be blank.": "Der Verzeichnis-Pfad kann nicht leer sein",
|
"The repository path cannot be blank.": "Der Verzeichnis-Pfad kann nicht leer sein",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "Das Suchintervall muss mindestens 5 Sekunden betragen.",
|
||||||
"Unknown": "Unbekannt",
|
"Unknown": "Unbekannt",
|
||||||
"Up to Date": "Aktuell",
|
"Up to Date": "Aktuell",
|
||||||
"Upgrade To {%version%}": "Upgrade auf {{version}}",
|
"Upgrade To {%version%}": "Upgrade auf {{version}}",
|
||||||
@@ -112,9 +141,12 @@
|
|||||||
"Use Compression": "Benutze Komprimierung",
|
"Use Compression": "Benutze Komprimierung",
|
||||||
"Use HTTPS for GUI": "Benutze HTTPS für Benutzeroberfläche",
|
"Use HTTPS for GUI": "Benutze HTTPS für Benutzeroberfläche",
|
||||||
"Version": "Version",
|
"Version": "Version",
|
||||||
|
"Versions Path": "Versionierungspfad",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Alte Versionen werden automatisch gelöscht wenn sie älter als das angegebene Höchstalter sind oder die Höchstzahl der Dateien pro Zeitabschnitt überschritten wird.",
|
||||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Knotens, dass dieser Knoten auch auf der Gegenseite hinzugefügt werden muss.",
|
"When adding a new node, keep in mind that this node must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Knotens, dass dieser Knoten auch auf der Gegenseite hinzugefügt werden muss.",
|
||||||
"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.": "Beim Hinzufügen eines neuen Verzeichnisses, beachte dass die Verzeichnis-ID dazu verwendet wird, Verzeichnisse zwischen Knoten zu verbinden. Die ID muss also auf allen Knoten gleich sein, Groß- und Kleinschreibung muss dabei beachtet werden.",
|
"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.": "Beim Hinzufügen eines neuen Verzeichnisses, beachte dass die Verzeichnis-ID dazu verwendet wird, Verzeichnisse zwischen Knoten zu verbinden. Die ID muss also auf allen Knoten gleich sein, Groß- und Kleinschreibung muss dabei beachtet werden.",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"You must keep at least one version.": "Du musst mindestens eine Version behalten.",
|
"You must keep at least one version.": "Du musst mindestens eine Version behalten.",
|
||||||
|
"full documentation": "Komplette Dokumentation",
|
||||||
"items": "Einträge"
|
"items": "Einträge"
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
{
|
{
|
||||||
"API Key": "Κλειδί API",
|
"API Key": "Κλειδί API",
|
||||||
"About": "Σχετικά",
|
"About": "Σχετικά",
|
||||||
"Add Node": "Πρόσθεσε Κόμβο",
|
"Add Node": "Προσθήκη Κόμβου",
|
||||||
"Add Repository": "Πρόσθεσε Αποθετήριο",
|
"Add Repository": "Προσθήκη Αποθετηρίου",
|
||||||
"Address": "Διεύθυνση",
|
"Address": "Διεύθυνση",
|
||||||
"Addresses": "Διευθύνσεις",
|
"Addresses": "Διευθύνσεις",
|
||||||
"Allow Anonymous Usage Reporting?": "Να επιτρέπεται Ανώνυμη Αποστολή Αναφοράς Χρήσης?",
|
"Allow Anonymous Usage Reporting?": "Να επιτρέπεται Ανώνυμη Αποστολή Αναφοράς Χρήσης?",
|
||||||
"Announce Server": "Διακομιστής Αναγγελίας",
|
"Announce Server": "Διακομιστής Αναγγελίας",
|
||||||
"Anonymous Usage Reporting": "Ανώνυμη Αναφορά Χρήσης",
|
"Anonymous Usage Reporting": "Ανώνυμη Αναφορά Χρήσης",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
"Bugs": "Bugs",
|
"Bugs": "Bugs",
|
||||||
"CPU Utilization": "Χρήση CPU",
|
"CPU Utilization": "Χρήση CPU",
|
||||||
"Close": "Τέλος",
|
"Close": "Τέλος",
|
||||||
|
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
"Connection Error": "Σφάλμα Σύνδεσης",
|
"Connection Error": "Σφάλμα Σύνδεσης",
|
||||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg και οι παρακάτω Συνεισφορείς:",
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg και οι παρακάτω Συνεισφορείς:",
|
||||||
"Delete": "Διαγραφή",
|
"Delete": "Διαγραφή",
|
||||||
@@ -20,51 +23,67 @@
|
|||||||
"Edit": "Επεξεργασία",
|
"Edit": "Επεξεργασία",
|
||||||
"Edit Node": "Επεξεργασία Κόμβου",
|
"Edit Node": "Επεξεργασία Κόμβου",
|
||||||
"Edit Repository": "Επεξεργασία Αποθετηρίου",
|
"Edit Repository": "Επεξεργασία Αποθετηρίου",
|
||||||
|
"Editing": "Editing",
|
||||||
"Enable UPnP": "Ενεργοποίηση UPnP",
|
"Enable UPnP": "Ενεργοποίηση UPnP",
|
||||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Εισήγαγε διαχωρισμένα με κόμμα \"ip:port\" διευθύνσεις ή \"dynamic\", για να πραγματοποιηθεί η αυτόματη εξεύρεση διευθύνσεων.",
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Εισήγαγε διαχωρισμένα με κόμμα \"ip:port\" διευθύνσεις ή \"dynamic\", για να πραγματοποιηθεί η αυτόματη εξεύρεση διευθύνσεων.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
"Error": "Σφάλμα",
|
"Error": "Σφάλμα",
|
||||||
"File Versioning": "File Versioning",
|
"File Versioning": "File Versioning",
|
||||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Τα permission bits των αρχείων αγνοούνται όταν γίνεται αναζήτηση για αλλαγές. Χρήση σε FAT filesystems.",
|
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Τα permission bits των αρχείων αγνοούνται όταν γίνεται αναζήτηση για αλλαγές. Χρήση σε FAT filesystems.",
|
||||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Όταν τα αρχεία αντικατασταθούν ή διαγραφούν από το syncthing, μεταφέρονται σε φάκελο .stversions με χρονική σήμανση.",
|
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Όταν τα αρχεία αντικατασταθούν ή διαγραφούν από το 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.",
|
"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": "Κατάλογος",
|
"Folder": "Κατάλογος",
|
||||||
"GUI Authentication Password": "GUI Authentication Password",
|
"GUI Authentication Password": "Κωδικός πιστοποίησης στο GUI",
|
||||||
"GUI Authentication User": "GUI Authentication User",
|
"GUI Authentication User": "Χρήστης πιστοποίησης στο GUI",
|
||||||
"GUI Listen Addresses": "GUI Listen Addresses",
|
"GUI Listen Addresses": "GUI Listen διευθύνσεις",
|
||||||
"Generate": "Δημιουργία",
|
"Generate": "Δημιουργία",
|
||||||
"Global Discovery": "Global Discovery",
|
"Global Discovery": "Global Discovery",
|
||||||
"Global Discovery Server": "Διακομιστής Ανεύρεσης Κόμβου",
|
"Global Discovery Server": "Διακομιστής Ανεύρεσης Κόμβου",
|
||||||
"Global Repository": "Global Repository",
|
"Global Repository": "Global Repository",
|
||||||
"Idle": "Ανενεργός",
|
"Idle": "Ανενεργό",
|
||||||
"Ignore Permissions": "Ignore Permissions",
|
"Ignore Patterns": "Ignore Patterns",
|
||||||
"Keep Versions": "Keep Versions",
|
"Ignore Permissions": "Αγνόησε Δικαιώματα",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
|
"Keep Versions": "Διατήρησε Εκδόσεις",
|
||||||
|
"Last seen": "Last seen",
|
||||||
"Latest Release": "Τελευταία Έκδοση",
|
"Latest Release": "Τελευταία Έκδοση",
|
||||||
"Local Discovery": "Local Discovery",
|
"Local Discovery": "Τοπική Ανεύρεση",
|
||||||
"Local Discovery Port": "Local Discovery Port",
|
"Local Discovery Port": "Port Τοπικής Ανεύρεσης",
|
||||||
"Local Repository": "Τοπικό Αποθετήριο",
|
"Local Repository": "Τοπικό Αποθετήριο",
|
||||||
"Master Repo": "Master Repo",
|
"Master Repo": "Master Repo",
|
||||||
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
|
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
|
||||||
"Max Outstanding Requests": "Max Outstanding Requests",
|
"Max Outstanding Requests": "Max Outstanding Requests",
|
||||||
|
"Maximum Age": "Maximum Age",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
|
"Never": "Never",
|
||||||
"No": "Αριθμός",
|
"No": "Αριθμός",
|
||||||
|
"No File Versioning": "No File Versioning",
|
||||||
"Node ID": "ID Κόμβου",
|
"Node ID": "ID Κόμβου",
|
||||||
"Node Identification": "Ταυτοποίηση Κόμβου",
|
"Node Identification": "Ταυτοποίηση Κόμβου",
|
||||||
"Node Name": "Όνομα Κόμβου",
|
"Node Name": "Όνομα Κόμβου",
|
||||||
"Notice": "Notice",
|
"Notice": "Notice",
|
||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
"Offline": "Ανεργός",
|
"Offline": "Ανεργό",
|
||||||
"Online": "Ενεργός",
|
"Online": "Ενεργός",
|
||||||
"Out Of Sync": "Εκτός Συγχρονισμού",
|
"Out Of Sync": "Μη Συγχρονισμένα",
|
||||||
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
|
||||||
"Override Changes": "Override Changes",
|
"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 (~) μπορεί να χρησιμοποιηθεί σαν συντόμευση για",
|
"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 (~) μπορεί να χρησιμοποιηθεί σαν συντόμευση για",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Path where versions should be stored (leave empty for the default .stversions folder in the repository).",
|
||||||
"Please wait": "Παρακαλώ περιμένετε",
|
"Please wait": "Παρακαλώ περιμένετε",
|
||||||
"Preview Usage Report": "Preview Usage Report",
|
"Preview": "Preview",
|
||||||
|
"Preview Usage Report": "Προεπισκόπηση αναφοράς χρήσης",
|
||||||
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
"RAM Utilization": "Χρήση RAM",
|
"RAM Utilization": "Χρήση RAM",
|
||||||
"Reconnect Interval (s)": "Reconnect Interval (s)",
|
"Reconnect Interval (s)": "Χρονικό διάστημα επανασύνδεσης (s)",
|
||||||
"Repository ID": "ID Αποθετηρίου",
|
"Repository ID": "ID Αποθετηρίου",
|
||||||
"Repository Master": "Repository Master",
|
"Repository Master": "Repository Master",
|
||||||
"Repository Path": "Μονοπάτι Αποθετηρίου",
|
"Repository Path": "Μονοπάτι Αποθετηρίου",
|
||||||
"Rescan Interval (s)": "Rescan Interval (s)",
|
"Rescan": "Rescan",
|
||||||
|
"Rescan Interval": "Rescan Interval",
|
||||||
|
"Rescan Interval (s)": "Χρονικό διάστημα Επανασάρρωσης (s)",
|
||||||
"Restart": "Επανεκκίνηση",
|
"Restart": "Επανεκκίνηση",
|
||||||
"Restart Needed": "Απαιτείται Επανεκκίνηση",
|
"Restart Needed": "Απαιτείται Επανεκκίνηση",
|
||||||
"Restarting": "Επανεκκίνηση",
|
"Restarting": "Επανεκκίνηση",
|
||||||
@@ -77,44 +96,57 @@
|
|||||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Σύντομη περιγραφή του αποθετηρίου. Θα πρέπει να είναι το ίδιο σε όλους τους κόμβους του cluster.",
|
"Short identifier for the repository. Must be the same on all cluster nodes.": "Σύντομη περιγραφή του αποθετηρίου. Θα πρέπει να είναι το ίδιο σε όλους τους κόμβους του cluster.",
|
||||||
"Show ID": "Εμφάνιση ID",
|
"Show ID": "Εμφάνιση ID",
|
||||||
"Shown instead of Node ID in the cluster status.": "Εμφάνιση στη θέση του ID Αποθετηρίου, στην κατάσταση του cluster.",
|
"Shown instead of Node ID in the cluster status.": "Εμφάνιση στη θέση του ID Αποθετηρίου, στην κατάσταση του cluster.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.",
|
||||||
"Shutdown": "Απενεργοποίηση",
|
"Shutdown": "Απενεργοποίηση",
|
||||||
|
"Simple File Versioning": "Simple File Versioning",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
"Source Code": "Πηγαίος Κώδικας",
|
"Source Code": "Πηγαίος Κώδικας",
|
||||||
"Start Browser": "Start Browser",
|
"Staggered File Versioning": "Staggered File Versioning",
|
||||||
|
"Start Browser": "Έναρξη Φυλλομετρητή",
|
||||||
"Stopped": "Απενεργοποιημένο",
|
"Stopped": "Απενεργοποιημένο",
|
||||||
"Support / Forum": "Υποστήριξη / Forum",
|
"Support / Forum": "Υποστήριξη / Forum",
|
||||||
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
|
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
|
||||||
"Synchronization": "Συγχρονισμός",
|
"Synchronization": "Συγχρονισμός",
|
||||||
"Syncing": "Συγχρονισμός",
|
"Syncing": "Συγχρονισμός",
|
||||||
"Syncthing has been shut down.": "Syncthing έχει απενεργοποιηθεί.",
|
"Syncthing has been shut down.": "Το Syncthing έχει απενεργοποιηθεί.",
|
||||||
"Syncthing includes the following software or portions thereof:": "Syncthing includes the following software or portions thereof:",
|
"Syncthing includes the following software or portions thereof:": "Το Syncthing συμπεριλαμβάνει τα παρακάτω λογισμικά ή μέρη αυτών:",
|
||||||
"Syncthing is restarting.": "Το Syncthing επανεκκινεί.",
|
"Syncthing is restarting.": "Το Syncthing επανεκκινεί.",
|
||||||
"Syncthing is upgrading.": "Το Syncthing αναβαθμίζεται.",
|
"Syncthing is upgrading.": "Το Syncthing αναβαθμίζεται.",
|
||||||
"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…",
|
"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 aggregated statistics are publicly available at {%url%}.": "Τα στατιστικά που έχουν συλλεγεί είναι διαθέσιμα στο κοινό, στο {{url}}.",
|
||||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The 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.": "Οι ρυθμίσεις έχουν αποθηκευτεί αλλά δεν έχουν ενεργοποιηθεί. Πρέπει να επανεκκινήσετε το 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 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 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 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.": "The node ID cannot be blank.",
|
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
|
||||||
|
"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 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 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 number of versions must be a number and cannot be blank.": "Ο αριθμός εκδόσεων πρέπει να είναι αριθμός και σίγουρα όχι κενό.",
|
||||||
"The repository ID cannot be blank.": "The repository ID 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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and 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 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 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 ID must be unique.": "Το ID Αποθετηρίου πρέπει να είναι μοναδικό.",
|
||||||
"The repository path cannot be blank.": "The repository path cannot be blank.",
|
"The repository path cannot be blank.": "Το μονοπάτι του αποθετηρίου δε μπορεί να είναι κενό.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "The rescan interval must be at least 5 seconds.",
|
||||||
"Unknown": "Άγνωστο",
|
"Unknown": "Άγνωστο",
|
||||||
"Up to Date": "Ενημερώμενο",
|
"Up to Date": "Ενημερωμένος",
|
||||||
"Upgrade To {%version%}": "Αναβάθμιση στην έκδοση {{version}}",
|
"Upgrade To {%version%}": "Αναβάθμιση στην έκδοση {{version}}",
|
||||||
"Upgrading": "Αναβάθμιση",
|
"Upgrading": "Αναβάθμιση",
|
||||||
"Upload Rate": "Upload Rate",
|
"Upload Rate": "Upload Rate",
|
||||||
"Usage": "Usage",
|
"Usage": "Usage",
|
||||||
"Use Compression": "Χρήση συμπίεσης",
|
"Use Compression": "Χρήση συμπίεσης",
|
||||||
"Use HTTPS for GUI": "Use HTTPS for GUI",
|
"Use HTTPS for GUI": "Χρήση HTTPS για το GUI",
|
||||||
"Version": "Έκδοση",
|
"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.",
|
"Versions Path": "Versions Path",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.",
|
||||||
|
"When adding a new 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 και θα πρέπει να είναι ταυτόσημα μεταξύ όλων των κόμβων.",
|
"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": "Ναι",
|
"Yes": "Ναι",
|
||||||
"You must keep at least one version.": "You must keep at least one version.",
|
"You must keep at least one version.": "You must keep at least one version.",
|
||||||
|
"full documentation": "full documentation",
|
||||||
"items": "items"
|
"items": "items"
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,12 @@
|
|||||||
"Allow Anonymous Usage Reporting?": "Allow Anonymous Usage Reporting?",
|
"Allow Anonymous Usage Reporting?": "Allow Anonymous Usage Reporting?",
|
||||||
"Announce Server": "Announce Server",
|
"Announce Server": "Announce Server",
|
||||||
"Anonymous Usage Reporting": "Anonymous Usage Reporting",
|
"Anonymous Usage Reporting": "Anonymous Usage Reporting",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
"Bugs": "Bugs",
|
"Bugs": "Bugs",
|
||||||
"CPU Utilization": "CPU Utilization",
|
"CPU Utilization": "CPU Utilization",
|
||||||
"Close": "Close",
|
"Close": "Close",
|
||||||
|
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
"Connection Error": "Connection Error",
|
"Connection Error": "Connection Error",
|
||||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
|
||||||
"Delete": "Delete",
|
"Delete": "Delete",
|
||||||
@@ -20,8 +23,10 @@
|
|||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"Edit Node": "Edit Node",
|
"Edit Node": "Edit Node",
|
||||||
"Edit Repository": "Edit Repository",
|
"Edit Repository": "Edit Repository",
|
||||||
|
"Editing": "Editing",
|
||||||
"Enable UPnP": "Enable UPnP",
|
"Enable UPnP": "Enable UPnP",
|
||||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.",
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"File Versioning": "File Versioning",
|
"File Versioning": "File Versioning",
|
||||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "File permission bits are ignored when looking for changes. Use on FAT filesystems.",
|
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "File permission bits are ignored when looking for changes. Use on FAT filesystems.",
|
||||||
@@ -36,8 +41,13 @@
|
|||||||
"Global Discovery Server": "Global Discovery Server",
|
"Global Discovery Server": "Global Discovery Server",
|
||||||
"Global Repository": "Global Repository",
|
"Global Repository": "Global Repository",
|
||||||
"Idle": "Idle",
|
"Idle": "Idle",
|
||||||
|
"Ignore Patterns": "Ignore Patterns",
|
||||||
"Ignore Permissions": "Ignore Permissions",
|
"Ignore Permissions": "Ignore Permissions",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
"Keep Versions": "Keep Versions",
|
"Keep Versions": "Keep Versions",
|
||||||
|
"Last seen": "Last seen",
|
||||||
"Latest Release": "Latest Release",
|
"Latest Release": "Latest Release",
|
||||||
"Local Discovery": "Local Discovery",
|
"Local Discovery": "Local Discovery",
|
||||||
"Local Discovery Port": "Local Discovery Port",
|
"Local Discovery Port": "Local Discovery Port",
|
||||||
@@ -45,7 +55,11 @@
|
|||||||
"Master Repo": "Master Repo",
|
"Master Repo": "Master Repo",
|
||||||
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
|
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
|
||||||
"Max Outstanding Requests": "Max Outstanding Requests",
|
"Max Outstanding Requests": "Max Outstanding Requests",
|
||||||
|
"Maximum Age": "Maximum Age",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
|
"Never": "Never",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
|
"No File Versioning": "No File Versioning",
|
||||||
"Node ID": "Node ID",
|
"Node ID": "Node ID",
|
||||||
"Node Identification": "Node Identification",
|
"Node Identification": "Node Identification",
|
||||||
"Node Name": "Node Name",
|
"Node Name": "Node Name",
|
||||||
@@ -57,13 +71,18 @@
|
|||||||
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
|
||||||
"Override Changes": "Override Changes",
|
"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": "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",
|
"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": "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",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Path where versions should be stored (leave empty for the default .stversions folder in the repository).",
|
||||||
"Please wait": "Please wait",
|
"Please wait": "Please wait",
|
||||||
|
"Preview": "Preview",
|
||||||
"Preview Usage Report": "Preview Usage Report",
|
"Preview Usage Report": "Preview Usage Report",
|
||||||
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
"RAM Utilization": "RAM Utilization",
|
"RAM Utilization": "RAM Utilization",
|
||||||
"Reconnect Interval (s)": "Reconnect Interval (s)",
|
"Reconnect Interval (s)": "Reconnect Interval (s)",
|
||||||
"Repository ID": "Repository ID",
|
"Repository ID": "Repository ID",
|
||||||
"Repository Master": "Repository Master",
|
"Repository Master": "Repository Master",
|
||||||
"Repository Path": "Repository Path",
|
"Repository Path": "Repository Path",
|
||||||
|
"Rescan": "Rescan",
|
||||||
|
"Rescan Interval": "Rescan Interval",
|
||||||
"Rescan Interval (s)": "Rescan Interval (s)",
|
"Rescan Interval (s)": "Rescan Interval (s)",
|
||||||
"Restart": "Restart",
|
"Restart": "Restart",
|
||||||
"Restart Needed": "Restart Needed",
|
"Restart Needed": "Restart Needed",
|
||||||
@@ -77,8 +96,13 @@
|
|||||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Short identifier for the repository. Must be the same on all cluster nodes.",
|
"Short identifier for the repository. Must be the same on all cluster nodes.": "Short identifier for the repository. Must be the same on all cluster nodes.",
|
||||||
"Show ID": "Show ID",
|
"Show ID": "Show ID",
|
||||||
"Shown instead of Node ID in the cluster status.": "Shown instead of Node ID in the cluster status.",
|
"Shown instead of Node ID in the cluster status.": "Shown instead of Node ID in the cluster status.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.",
|
||||||
"Shutdown": "Shutdown",
|
"Shutdown": "Shutdown",
|
||||||
|
"Simple File Versioning": "Simple File Versioning",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
"Source Code": "Source Code",
|
"Source Code": "Source Code",
|
||||||
|
"Staggered File Versioning": "Staggered File Versioning",
|
||||||
"Start Browser": "Start Browser",
|
"Start Browser": "Start Browser",
|
||||||
"Stopped": "Stopped",
|
"Stopped": "Stopped",
|
||||||
"Support / Forum": "Support / Forum",
|
"Support / Forum": "Support / Forum",
|
||||||
@@ -95,14 +119,19 @@
|
|||||||
"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 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 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 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
|
||||||
"The node ID cannot be blank.": "The node ID cannot be blank.",
|
"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 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.",
|
"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 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 repository ID cannot be blank.": "The repository ID cannot be blank.",
|
||||||
|
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and 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 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 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 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 ID must be unique.": "The repository ID must be unique.",
|
||||||
"The repository path cannot be blank.": "The repository path cannot be blank.",
|
"The repository path cannot be blank.": "The repository path cannot be blank.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "The rescan interval must be at least 5 seconds.",
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
"Up to Date": "Up to Date",
|
"Up to Date": "Up to Date",
|
||||||
"Upgrade To {%version%}": "Upgrade To {{version}}",
|
"Upgrade To {%version%}": "Upgrade To {{version}}",
|
||||||
@@ -112,9 +141,12 @@
|
|||||||
"Use Compression": "Use Compression",
|
"Use Compression": "Use Compression",
|
||||||
"Use HTTPS for GUI": "Use HTTPS for GUI",
|
"Use HTTPS for GUI": "Use HTTPS for GUI",
|
||||||
"Version": "Version",
|
"Version": "Version",
|
||||||
|
"Versions Path": "Versions Path",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.",
|
||||||
"When adding a new 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 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.": "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.",
|
"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.": "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.",
|
||||||
"Yes": "Yes",
|
"Yes": "Yes",
|
||||||
"You must keep at least one version.": "You must keep at least one version.",
|
"You must keep at least one version.": "You must keep at least one version.",
|
||||||
|
"full documentation": "full documentation",
|
||||||
"items": "items"
|
"items": "items"
|
||||||
}
|
}
|
||||||
@@ -8,10 +8,13 @@
|
|||||||
"Allow Anonymous Usage Reporting?": "Permitir reporte anónimo de uso?",
|
"Allow Anonymous Usage Reporting?": "Permitir reporte anónimo de uso?",
|
||||||
"Announce Server": "Anunciar servidor",
|
"Announce Server": "Anunciar servidor",
|
||||||
"Anonymous Usage Reporting": "Reporte anónimo de uso",
|
"Anonymous Usage Reporting": "Reporte anónimo de uso",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
"Bugs": "Errores",
|
"Bugs": "Errores",
|
||||||
"CPU Utilization": "Uso de la CPU",
|
"CPU Utilization": "Uso de la CPU",
|
||||||
"Close": "Cerrar",
|
"Close": "Cerrar",
|
||||||
"Connection Error": "Connection Error",
|
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
|
"Connection Error": "Error de conexión",
|
||||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Derechos de autor © 2014 Jakob Borg y los siguientes colaboradores:",
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Derechos de autor © 2014 Jakob Borg y los siguientes colaboradores:",
|
||||||
"Delete": "Suprimir",
|
"Delete": "Suprimir",
|
||||||
"Disconnected": "Desconectado",
|
"Disconnected": "Desconectado",
|
||||||
@@ -20,8 +23,10 @@
|
|||||||
"Edit": "Editar",
|
"Edit": "Editar",
|
||||||
"Edit Node": "Editar nodo",
|
"Edit Node": "Editar nodo",
|
||||||
"Edit Repository": "Editar repositorio",
|
"Edit Repository": "Editar repositorio",
|
||||||
|
"Editing": "Editing",
|
||||||
"Enable UPnP": "Permitir UPnP",
|
"Enable UPnP": "Permitir UPnP",
|
||||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Ingrese las direcciones \"ip:port\" separadas por coma o \"dynamic\" para descubrir automáticamente las direcciones.",
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Ingrese las direcciones \"ip:port\" separadas por coma o \"dynamic\" para descubrir automáticamente las direcciones.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
"Error": "Error",
|
"Error": "Error",
|
||||||
"File Versioning": "Control de versiones",
|
"File Versioning": "Control de versiones",
|
||||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Los permisos de archivo son ignorados al buscar cambios. Usar en sistemas de archivos FAT.",
|
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Los permisos de archivo son ignorados al buscar cambios. Usar en sistemas de archivos FAT.",
|
||||||
@@ -33,11 +38,16 @@
|
|||||||
"GUI Listen Addresses": "Direcciones de escucha para la GUI.",
|
"GUI Listen Addresses": "Direcciones de escucha para la GUI.",
|
||||||
"Generate": "Generar",
|
"Generate": "Generar",
|
||||||
"Global Discovery": "Búsqueda en internet",
|
"Global Discovery": "Búsqueda en internet",
|
||||||
"Global Discovery Server": "Global Discovery Server",
|
"Global Discovery Server": "Servidor global de identificación",
|
||||||
"Global Repository": "Repositorio global",
|
"Global Repository": "Repositorio global",
|
||||||
"Idle": "Inactivo",
|
"Idle": "Inactivo",
|
||||||
|
"Ignore Patterns": "Ignore Patterns",
|
||||||
"Ignore Permissions": "Ignorar permisos",
|
"Ignore Permissions": "Ignorar permisos",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Límite de velocidad de entrada (KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
"Keep Versions": "Conservar versiones",
|
"Keep Versions": "Conservar versiones",
|
||||||
|
"Last seen": "Visto por ultima vez",
|
||||||
"Latest Release": "Última versión",
|
"Latest Release": "Última versión",
|
||||||
"Local Discovery": "Búsqueda en red local",
|
"Local Discovery": "Búsqueda en red local",
|
||||||
"Local Discovery Port": "Puerto de búsqueda de red local",
|
"Local Discovery Port": "Puerto de búsqueda de red local",
|
||||||
@@ -45,9 +55,13 @@
|
|||||||
"Master Repo": "Repositorio maestro",
|
"Master Repo": "Repositorio maestro",
|
||||||
"Max File Change Rate (KiB/s)": "Tasa máxima de cambios (KiB/s)",
|
"Max File Change Rate (KiB/s)": "Tasa máxima de cambios (KiB/s)",
|
||||||
"Max Outstanding Requests": "Cantidad máxima de peticiones pendientes",
|
"Max Outstanding Requests": "Cantidad máxima de peticiones pendientes",
|
||||||
|
"Maximum Age": "Edad máxima",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
|
"Never": "Nunca",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
|
"No File Versioning": "Sin control de versiones de archivos",
|
||||||
"Node ID": "Nodo ID",
|
"Node ID": "Nodo ID",
|
||||||
"Node Identification": "Node Identification",
|
"Node Identification": "Identificador del nodo",
|
||||||
"Node Name": "Nodo nombre",
|
"Node Name": "Nodo nombre",
|
||||||
"Notice": "Aviso",
|
"Notice": "Aviso",
|
||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
@@ -57,17 +71,22 @@
|
|||||||
"Outgoing Rate Limit (KiB/s)": "Tasa máxima de envío (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Tasa máxima de envío (KiB/s)",
|
||||||
"Override Changes": "Reemplazar los cambios",
|
"Override Changes": "Reemplazar los cambios",
|
||||||
"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": "Ruta del repositorio en el equipo local. La carpeta sera creada si no existe. El carácter tilde (~) puede ser utilizado como atajo de ",
|
"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": "Ruta del repositorio en el equipo local. La carpeta sera creada si no existe. El carácter tilde (~) puede ser utilizado como atajo de ",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Ruta donde se deben almacenar versiones (dejar vacío para la carpeta .stversions predeterminada en el repositorio).",
|
||||||
"Please wait": "Aguarde por favor",
|
"Please wait": "Aguarde por favor",
|
||||||
|
"Preview": "Vista previa",
|
||||||
"Preview Usage Report": "Ver reporte de uso",
|
"Preview Usage Report": "Ver reporte de uso",
|
||||||
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
"RAM Utilization": "Utilización de RAM",
|
"RAM Utilization": "Utilización de RAM",
|
||||||
"Reconnect Interval (s)": "Intervalo de reconexión (s)",
|
"Reconnect Interval (s)": "Intervalo de reconexión (s)",
|
||||||
"Repository ID": "ID de repositorio",
|
"Repository ID": "ID de repositorio",
|
||||||
"Repository Master": "Repositorio maestro",
|
"Repository Master": "Repositorio maestro",
|
||||||
"Repository Path": "Ruta del repositorio",
|
"Repository Path": "Ruta del repositorio",
|
||||||
|
"Rescan": "Rescan",
|
||||||
|
"Rescan Interval": "Rescan Interval",
|
||||||
"Rescan Interval (s)": "Intervalo de reescaneo (s)",
|
"Rescan Interval (s)": "Intervalo de reescaneo (s)",
|
||||||
"Restart": "Reiniciar",
|
"Restart": "Reiniciar",
|
||||||
"Restart Needed": "Es necesario reiniciar",
|
"Restart Needed": "Es necesario reiniciar",
|
||||||
"Restarting": "Restarting",
|
"Restarting": "Reiniciando",
|
||||||
"Save": "Guardar",
|
"Save": "Guardar",
|
||||||
"Scanning": "Actualización",
|
"Scanning": "Actualización",
|
||||||
"Select the nodes to share this repository with.": "Seleccione los nodos con los cuales compartir el repositorio.",
|
"Select the nodes to share this repository with.": "Seleccione los nodos con los cuales compartir el repositorio.",
|
||||||
@@ -77,8 +96,13 @@
|
|||||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identificador corto para el repositorio. Debe ser el mismo en todos los nodos del clúster.",
|
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identificador corto para el repositorio. Debe ser el mismo en todos los nodos del clúster.",
|
||||||
"Show ID": "Mostrar ID",
|
"Show ID": "Mostrar ID",
|
||||||
"Shown instead of Node ID in the cluster status.": "Mostrar en lugar de ID de nodo en estado de cluster.",
|
"Shown instead of Node ID in the cluster status.": "Mostrar en lugar de ID de nodo en estado de cluster.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.",
|
||||||
"Shutdown": "Apagar",
|
"Shutdown": "Apagar",
|
||||||
|
"Simple File Versioning": "Versiones simple de archivos",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
"Source Code": "Código fuente",
|
"Source Code": "Código fuente",
|
||||||
|
"Staggered File Versioning": "Versiones del archivo escalonado",
|
||||||
"Start Browser": "Iniciar navegador",
|
"Start Browser": "Iniciar navegador",
|
||||||
"Stopped": "Parado",
|
"Stopped": "Parado",
|
||||||
"Support / Forum": "Soporte / Foro",
|
"Support / Forum": "Soporte / Foro",
|
||||||
@@ -87,34 +111,42 @@
|
|||||||
"Syncing": "Sincronización",
|
"Syncing": "Sincronización",
|
||||||
"Syncthing has been shut down.": "La sincronización esta apagada",
|
"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 includes the following software or portions thereof:": "Syncthing incluye los siguientes softwares o partes de ellos:",
|
||||||
"Syncthing is restarting.": "Syncthing is restarting.",
|
"Syncthing is restarting.": "Syncthing está reiniciando.",
|
||||||
"Syncthing is upgrading.": "Syncthing is upgrading.",
|
"Syncthing is upgrading.": "Syncthing se está actualizando.",
|
||||||
"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...",
|
"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 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 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 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 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Los siguientes intervalos se utilizan: para la primera hora una versión se mantiene cada 30 segundos, para el primer día de una versión se mantiene cada hora, durante los primeros 30 días de la versión se mantiene todos los días, hasta que la edad máxima de una versión se mantiene cada semana.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar en blanco.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El tiempo máximo para mantener una versión (en días, establece en 0 para mantener versiones para siempre).",
|
||||||
"The node ID cannot be blank.": "El ID de nodo no puede estar vacío.",
|
"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 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.",
|
"The number of old versions to keep, per file.": "El numero de versiones anteriores a conservar, por archivo.",
|
||||||
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
|
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
|
||||||
"The repository ID cannot be blank.": "El ID de repositorio no puede estar vacio.",
|
"The repository ID cannot be blank.": "El ID de repositorio no puede estar vacio.",
|
||||||
|
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and 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 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.": "El ID de repositorio debe ser una cadena corta (64 caracteres o menos) consistente solamente en letras, números, punto (.), guion (-) y guion bajo (_).",
|
"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.": "El ID de repositorio debe ser una cadena corta (64 caracteres o menos) consistente solamente en letras, números, punto (.), guion (-) y guion bajo (_).",
|
||||||
"The repository ID must be unique.": "El ID de repositorio debe ser único.",
|
"The repository ID must be unique.": "El ID de repositorio debe ser único.",
|
||||||
"The repository path cannot be blank.": "La ruta del repositorio no puede estar vacía.",
|
"The repository path cannot be blank.": "La ruta del repositorio no puede estar vacía.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "The rescan interval must be at least 5 seconds.",
|
||||||
"Unknown": "Desconocido",
|
"Unknown": "Desconocido",
|
||||||
"Up to Date": "Actualizado",
|
"Up to Date": "Actualizado",
|
||||||
"Upgrade To {%version%}": "Actualizar a {{version}}",
|
"Upgrade To {%version%}": "Actualizar a {{version}}",
|
||||||
"Upgrading": "Upgrading",
|
"Upgrading": "Actualizando",
|
||||||
"Upload Rate": "Tasa de subida",
|
"Upload Rate": "Tasa de subida",
|
||||||
"Usage": "Utilización",
|
"Usage": "Utilización",
|
||||||
"Use Compression": "Usar compresión",
|
"Use Compression": "Usar compresión",
|
||||||
"Use HTTPS for GUI": "Usar HTTPS para la GUI",
|
"Use HTTPS for GUI": "Usar HTTPS para la GUI",
|
||||||
"Version": "Versión",
|
"Version": "Versión",
|
||||||
|
"Versions Path": "Ruta de versiones",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Las versiones se eliminan automáticamente si son mayores de la edad máxima o mayor que el número de archivos permitidos en un intervalo.",
|
||||||
"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.",
|
"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.",
|
||||||
"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.": "Al agregar un nuevo repositorio, tenga en mente que el ID de repositorio se utiliza para ligar los repositorios entre nodos. Distingue mayúsculas y minúsculas y debe ser exactamente igual en todos los nodos.",
|
"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.": "Al agregar un nuevo repositorio, tenga en mente que el ID de repositorio se utiliza para ligar los repositorios entre nodos. Distingue mayúsculas y minúsculas y debe ser exactamente igual en todos los nodos.",
|
||||||
"Yes": "Si",
|
"Yes": "Si",
|
||||||
"You must keep at least one version.": "Debe mantener al menos una versión",
|
"You must keep at least one version.": "Debe mantener al menos una versión",
|
||||||
"items": "items"
|
"full documentation": "full documentation",
|
||||||
|
"items": "Articulos"
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,12 @@
|
|||||||
"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",
|
"Announce Server": "Serveur d'annonce",
|
||||||
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
|
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
"Bugs": "Bugs",
|
"Bugs": "Bugs",
|
||||||
"CPU Utilization": "Utilisation du CPU",
|
"CPU Utilization": "Utilisation du CPU",
|
||||||
"Close": "Fermer",
|
"Close": "Fermer",
|
||||||
|
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
"Connection Error": "Erreur de connexion",
|
"Connection Error": "Erreur de connexion",
|
||||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants :",
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants :",
|
||||||
"Delete": "Supprimer",
|
"Delete": "Supprimer",
|
||||||
@@ -20,8 +23,10 @@
|
|||||||
"Edit": "Éditer",
|
"Edit": "Éditer",
|
||||||
"Edit Node": "Éditer le nœud",
|
"Edit Node": "Éditer le nœud",
|
||||||
"Edit Repository": "Éditer le répertoire",
|
"Edit Repository": "Éditer le répertoire",
|
||||||
|
"Editing": "Editing",
|
||||||
"Enable UPnP": "Activer l'UPnP",
|
"Enable UPnP": "Activer l'UPnP",
|
||||||
"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.",
|
"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.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
"Error": "Erreur",
|
"Error": "Erreur",
|
||||||
"File Versioning": "Versions de fichier",
|
"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 de type FAT.",
|
"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.",
|
||||||
@@ -36,8 +41,13 @@
|
|||||||
"Global Discovery Server": "Serveur global de recherche",
|
"Global Discovery Server": "Serveur global de recherche",
|
||||||
"Global Repository": "Répertoire global",
|
"Global Repository": "Répertoire global",
|
||||||
"Idle": "Au repos",
|
"Idle": "Au repos",
|
||||||
|
"Ignore Patterns": "Ignore Patterns",
|
||||||
"Ignore Permissions": "Ignorer les permissions",
|
"Ignore Permissions": "Ignorer les permissions",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Limite du débit entrant (KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
"Keep Versions": "Conserver les versions",
|
"Keep Versions": "Conserver les versions",
|
||||||
|
"Last seen": "Dernière apparition",
|
||||||
"Latest Release": "Dernière version",
|
"Latest Release": "Dernière version",
|
||||||
"Local Discovery": "Recherche locale",
|
"Local Discovery": "Recherche locale",
|
||||||
"Local Discovery Port": "Port de recherche locale",
|
"Local Discovery Port": "Port de recherche locale",
|
||||||
@@ -45,7 +55,11 @@
|
|||||||
"Master Repo": "Dossier maître",
|
"Master Repo": "Dossier maître",
|
||||||
"Max File Change Rate (KiB/s)": "Débit maximum de changement de fichier (KiB/s)",
|
"Max File Change Rate (KiB/s)": "Débit maximum de changement de fichier (KiB/s)",
|
||||||
"Max Outstanding Requests": "Nombre maximum de demandes concurrentes de blocs de fichier",
|
"Max Outstanding Requests": "Nombre maximum de demandes concurrentes de blocs de fichier",
|
||||||
|
"Maximum Age": "Ancienneté maximum",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
|
"Never": "Jamais",
|
||||||
"No": "Non",
|
"No": "Non",
|
||||||
|
"No File Versioning": "Pas de version de fichier",
|
||||||
"Node ID": "ID du nœud",
|
"Node ID": "ID du nœud",
|
||||||
"Node Identification": "Identification du nœud",
|
"Node Identification": "Identification du nœud",
|
||||||
"Node Name": "Nom du nœud",
|
"Node Name": "Nom du nœud",
|
||||||
@@ -57,28 +71,38 @@
|
|||||||
"Outgoing Rate Limit (KiB/s)": "Limite du débit sortant (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Limite du débit sortant (KiB/s)",
|
||||||
"Override Changes": "Écraser les changements",
|
"Override Changes": "Écraser les changements",
|
||||||
"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": "Chemin du répertoire sur l'ordinateur local. Il sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
|
"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": "Chemin du répertoire sur l'ordinateur local. Il sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
|
||||||
"Please wait": "Merci de patienter",
|
"Please wait": "Merci de patienter",
|
||||||
|
"Preview": "Aperçu",
|
||||||
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
|
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
|
||||||
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
"RAM Utilization": "Utilisation de la RAM",
|
"RAM Utilization": "Utilisation de la RAM",
|
||||||
"Reconnect Interval (s)": "Intervalle de reconnexion (s)",
|
"Reconnect Interval (s)": "Intervalle de reconnexion (s)",
|
||||||
"Repository ID": "ID du répertoire",
|
"Repository ID": "ID du répertoire",
|
||||||
"Repository Master": "Répertoire maître",
|
"Repository Master": "Répertoire maître",
|
||||||
"Repository Path": "Chemin du répertoire",
|
"Repository Path": "Chemin du répertoire",
|
||||||
|
"Rescan": "Rescanner",
|
||||||
|
"Rescan Interval": "Intervalle de scan",
|
||||||
"Rescan Interval (s)": "Intervalle de rescan (s)",
|
"Rescan Interval (s)": "Intervalle de rescan (s)",
|
||||||
"Restart": "Redémarrer",
|
"Restart": "Redémarrer",
|
||||||
"Restart Needed": "Redémarrage nécessaire",
|
"Restart Needed": "Redémarrage nécessaire",
|
||||||
"Restarting": "Redémarrage",
|
"Restarting": "Redémarrage",
|
||||||
"Save": "Sauver",
|
"Save": "Sauver",
|
||||||
"Scanning": "En cours de scan",
|
"Scanning": "En cours de scan",
|
||||||
"Select the nodes to share this repository with.": "Sélectionner les nœuds qui partageront ce répertoire.",
|
"Select the nodes to share this repository with.": "Sélectionner les nœuds qui partagent ce répertoire.",
|
||||||
"Settings": "Configuration",
|
"Settings": "Configuration",
|
||||||
"Share With Nodes": "Partager avec les nœuds",
|
"Share With Nodes": "Partager avec les nœuds",
|
||||||
"Shared With": "Partagé avec",
|
"Shared With": "Partagé avec",
|
||||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identifiant court pour le répertoire. Il doit être le même sur l'ensemble des nœuds du cluster.",
|
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identifiant court pour le répertoire. Il doit être le même sur l'ensemble des nœuds du cluster.",
|
||||||
"Show ID": "Montrer l'ID",
|
"Show ID": "Montrer l'ID",
|
||||||
"Shown instead of Node ID in the cluster status.": "Affiché à la place de l'ID du nœud au sein du cluster.",
|
"Shown instead of Node ID in the cluster status.": "Affiché à la place de l'ID du nœud au sein du cluster.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Affiché à la place de l'ID du nœud dans le statut du cluster. Sera annoncé aux autres nœuds comme un nom par défaut optionnel.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Affiché à la place de l'ID du nœud dans le statut du cluster. Sera mis à jour par le nom que le nœud annonce si laissé vide.",
|
||||||
"Shutdown": "Éteindre",
|
"Shutdown": "Éteindre",
|
||||||
|
"Simple File Versioning": "Versions simples de fichier",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
"Source Code": "Code source",
|
"Source Code": "Code source",
|
||||||
|
"Staggered File Versioning": "Versions échelonnées de fichier",
|
||||||
"Start Browser": "Démarrer le navigateur web",
|
"Start Browser": "Démarrer le navigateur web",
|
||||||
"Stopped": "Arrêté",
|
"Stopped": "Arrêté",
|
||||||
"Support / Forum": "Aide / Forum",
|
"Support / Forum": "Aide / Forum",
|
||||||
@@ -86,7 +110,7 @@
|
|||||||
"Synchronization": "Synchronisation",
|
"Synchronization": "Synchronisation",
|
||||||
"Syncing": "En cours de synchronisation",
|
"Syncing": "En cours de synchronisation",
|
||||||
"Syncthing has been shut down.": "Syncthing a été éteint.",
|
"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 includes the following software or portions thereof:": "Syncthing intègre les logiciels suivants (ou des éléments provenant de ces logiciels) :",
|
||||||
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
|
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
|
||||||
"Syncthing is upgrading.": "Syncthing est cours de mise à jour.",
|
"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 ...",
|
"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 ...",
|
||||||
@@ -95,26 +119,34 @@
|
|||||||
"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 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 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 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Les intervalles suivant sont utilisés: la première heure une version est conservée chaque 30 secondes, le premier jour une version est conservée chaque heure, les premiers 30 jours une version est conservée chaque jour, jusqu'à la limite d'âge maximum une version est conservée chaque semaine.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "L'ancienneté maximum doit être un nombre et ne peut être vide.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Le temps maximum de conservation d'une version (en jours, mettre à 0 pour conserver les versions pour toujours)",
|
||||||
"The node ID cannot be blank.": "L'ID du nœud ne peut être vide.",
|
"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 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 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 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 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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and 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.": "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 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 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.",
|
"The repository path cannot be blank.": "Le chemin du répertoire ne peut pas être vide.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "L'intervalle de scan doit être d'au minimum 5 secondes.",
|
||||||
"Unknown": "Inconnu",
|
"Unknown": "Inconnu",
|
||||||
"Up to Date": "Synchronisation à jour",
|
"Up to Date": "Synchronisation à jour",
|
||||||
"Upgrade To {%version%}": "Upgrader vers {{version}}",
|
"Upgrade To {%version%}": "Mettre à jour vers {{version}}",
|
||||||
"Upgrading": "Mise à jour de Syncthing",
|
"Upgrading": "Mise à jour de Syncthing",
|
||||||
"Upload Rate": "Débit d'envoi",
|
"Upload Rate": "Débit d'envoi",
|
||||||
"Usage": "Utilisation",
|
"Usage": "Utilisation",
|
||||||
"Use Compression": "Utiliser la compression",
|
"Use Compression": "Utiliser la compression",
|
||||||
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
|
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
|
||||||
"Version": "Version",
|
"Version": "Version",
|
||||||
|
"Versions Path": "Emplacement des versions",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions sont supprimées automatiquement si celles-ci sont plus anciennes que l'ancienneté maximum ou que leur nombre est supérieur au nombre autorisé dans une intervale.",
|
||||||
"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 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 casse et doivent être identiques à 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",
|
"Yes": "Oui",
|
||||||
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
||||||
|
"full documentation": "full documentation",
|
||||||
"items": "éléments"
|
"items": "éléments"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"API Key": "API kulcs",
|
||||||
|
"About": "Névjegy",
|
||||||
|
"Add Node": "Csomópont hozzáadása",
|
||||||
|
"Add Repository": "Tároló hozzáadása",
|
||||||
|
"Address": "Cím",
|
||||||
|
"Addresses": "Címek",
|
||||||
|
"Allow Anonymous Usage Reporting?": "Engedélyezed a névtelen felhasználási statisztikai adatok küldését?",
|
||||||
|
"Announce Server": "Cím hirdető szerver",
|
||||||
|
"Anonymous Usage Reporting": "Névtelen felhasználási statisztikák küldése",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
|
"Bugs": "Hibák",
|
||||||
|
"CPU Utilization": "Processzor használat",
|
||||||
|
"Close": "Bezárás",
|
||||||
|
"Comment, when used at the start of a line": "Megjegyzés, a sor elején használva",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
|
"Connection Error": "Kapcsolódási hiba",
|
||||||
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg és a következő Közreműködők",
|
||||||
|
"Delete": "Törlés",
|
||||||
|
"Disconnected": "Kapcsolat bontása",
|
||||||
|
"Documentation": "Dokumentáció",
|
||||||
|
"Download Rate": "Letöltési sebesség",
|
||||||
|
"Edit": "Szerkesztés",
|
||||||
|
"Edit Node": "Csomópont szerkesztése",
|
||||||
|
"Edit Repository": "Tároló szerkesztése",
|
||||||
|
"Editing": "Szerkesztés",
|
||||||
|
"Enable UPnP": "UPnP engedélyezése",
|
||||||
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Add meg kettősponttal elválasztva \"ip:port\" a címet vagy add meg a \"dynamic\" szót az a cím automatikus észleléséhez.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
|
"Error": "Hiba",
|
||||||
|
"File Versioning": "File verziózás",
|
||||||
|
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "A file jogosultásgok figyelmen kívül hagyása. FAT file-rendszernél haszálatos.",
|
||||||
|
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "A file-ok időpecsételt verziói a .stversions mappában kerülnek áthelyezésre, amikor felülírásra vagy törlésre kerülnek a Síncthing által.",
|
||||||
|
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "A file-ok védve lesznek, a többi csomóponton történt változástól, de ezen a csomóponton történt változás el lesz küldve a többi csomópontra.",
|
||||||
|
"Folder": "Mappa",
|
||||||
|
"GUI Authentication Password": "GUI jelszó",
|
||||||
|
"GUI Authentication User": "GUI felhastnáló",
|
||||||
|
"GUI Listen Addresses": "GUI port",
|
||||||
|
"Generate": "Generálás",
|
||||||
|
"Global Discovery": "Globális csomópont keresés",
|
||||||
|
"Global Discovery Server": "Globális csomópont kereséshez használt szerver",
|
||||||
|
"Global Repository": "Globális tároló",
|
||||||
|
"Idle": "Tétlen",
|
||||||
|
"Ignore Patterns": "Ignore Patterns",
|
||||||
|
"Ignore Permissions": "Jogosultságok figyelmen kívül hagyása",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Bejövő sebesség korlát (KIB/mp)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
|
"Keep Versions": "Verziók megtartása",
|
||||||
|
"Last seen": "Utoljára látva",
|
||||||
|
"Latest Release": "Utolsó kiadás",
|
||||||
|
"Local Discovery": "Helyi csomópont keresés",
|
||||||
|
"Local Discovery Port": "Helyi csomópont keresés port-ja",
|
||||||
|
"Local Repository": "Helyi tároló",
|
||||||
|
"Master Repo": "Központi tároló",
|
||||||
|
"Max File Change Rate (KiB/s)": "Maximális file változás sebessége (KiB/mp)",
|
||||||
|
"Max Outstanding Requests": "Maximális kimenő kérés",
|
||||||
|
"Maximum Age": "Maximális kor",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Több szintű joker (több könyvtár szintre érvényesül)",
|
||||||
|
"Never": "Soha",
|
||||||
|
"No": "Nem",
|
||||||
|
"No File Versioning": "Nincs file verziózás",
|
||||||
|
"Node ID": "Csomópont azonosító",
|
||||||
|
"Node Identification": "Csomópont azonosítás",
|
||||||
|
"Node Name": "Csomópont név",
|
||||||
|
"Notice": "Megjegyzés",
|
||||||
|
"OK": "Rendben",
|
||||||
|
"Offline": "Nincs kpcsolat",
|
||||||
|
"Online": "Kapcsolódva",
|
||||||
|
"Out Of Sync": "Nincs szinkronban",
|
||||||
|
"Outgoing Rate Limit (KiB/s)": "Kimenő sávszélesség (KiB/mp)",
|
||||||
|
"Override Changes": "Változtatások felülbírálása",
|
||||||
|
"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": "A tároló útvonala ezen a számítógépen. Amennyiben nem létezik automatikusan létrehozzuk. A hullámvonal (~) karakter használható a rövidítésre",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Az útvonal ahol a régi verziókat tároljuk (ha üresen hagyod akkor .stversions mappa lesz a neve).",
|
||||||
|
"Please wait": "Kérem várj",
|
||||||
|
"Preview": "Előnézet",
|
||||||
|
"Preview Usage Report": "Felhasználási adatok átnézése",
|
||||||
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
|
"RAM Utilization": "Memória használat",
|
||||||
|
"Reconnect Interval (s)": "Újracsatlakozási intervallum (mp)",
|
||||||
|
"Repository ID": "Tároló azonosító",
|
||||||
|
"Repository Master": "Központi tároló",
|
||||||
|
"Repository Path": "Tároló útvonala",
|
||||||
|
"Rescan": "Újraátvizsgálás",
|
||||||
|
"Rescan Interval": "Átnézési intervallum",
|
||||||
|
"Rescan Interval (s)": "Átnézési intervallum (mp)",
|
||||||
|
"Restart": "Újraindítás",
|
||||||
|
"Restart Needed": "Újraindítás szükséges",
|
||||||
|
"Restarting": "Újraindulás",
|
||||||
|
"Save": "Mentés",
|
||||||
|
"Scanning": "Átnézés",
|
||||||
|
"Select the nodes to share this repository with.": "Válaszd ki a csomópontokat amikkel a tárolót megosszuk",
|
||||||
|
"Settings": "Beállítások",
|
||||||
|
"Share With Nodes": "Megosztás a csomópontokkal",
|
||||||
|
"Shared With": "Megosztva velük",
|
||||||
|
"Short identifier for the repository. Must be the same on all cluster nodes.": "A tároló rövid azonosítója. Ugyanannak kell lennie minden fürtbeli csomóponton.",
|
||||||
|
"Show ID": "Azonosító mutatáas",
|
||||||
|
"Shown instead of Node ID in the cluster status.": "A csomópont azonosító helyett jelenik meg a fürt állapotánál.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "A csomópont azonosító helyett jelenik meg a fürtben a státusznál. A csomópontoknak ez is hirdetve lesz, mint opcionális alapértelmezett név.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "A csomópont azonosító helyett jelenik meg a fürtben a státusznál. A csomópont neve a hirdetettre lesz llítva amennyiben az üresen van hagyva.",
|
||||||
|
"Shutdown": "Leállítás",
|
||||||
|
"Simple File Versioning": "Egyszerű file verziózás",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
|
"Source Code": "Forráskód",
|
||||||
|
"Staggered File Versioning": "Többszintű file verziózás",
|
||||||
|
"Start Browser": "Böngésző indítása",
|
||||||
|
"Stopped": "Leállítva",
|
||||||
|
"Support / Forum": "Támogatás / Fórum",
|
||||||
|
"Sync Protocol Listen Addresses": "Szinkronizációs protokoll hallgatózási címe",
|
||||||
|
"Synchronization": "Szinkronizálás",
|
||||||
|
"Syncing": "Syncthing",
|
||||||
|
"Syncthing has been shut down.": "Syncthing leállítva",
|
||||||
|
"Syncthing includes the following software or portions thereof:": "Syncthing a következő programokat, vagy programkomponenseket tartalmazza.",
|
||||||
|
"Syncthing is restarting.": "Syncthing újraindul",
|
||||||
|
"Syncthing is upgrading.": "Syncthing frissül",
|
||||||
|
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "A Syncthing úgy tűnik, hogy nem működik, vagy valami probléma van az hálózati kapcsolattal. Újra próbálom...",
|
||||||
|
"The aggregated statistics are publicly available at {%url%}.": "Az összevont statisztikák nyilvánosan elérhetők a {{url}} címen.",
|
||||||
|
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A beállítások ok elmentésre kerültek, de nem lettek aktiválva. Indítsad újra a Syncthing-et, hogy aktíváld őket.",
|
||||||
|
"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.": "A titkosított felhasználási jelentés naponta kerül elküldésre. Arra használjuk, hogy a futtató platformot, tároló méreteket illetve program verziókat kövessük nyomon. Amennyiben ez változik akkor újra meg fog jelenni ez az ablak.",
|
||||||
|
"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.": "A beírt csomópont azonosító nem tűnik megfelelőnek. 52 karakter hosszúnak kell lennie és csak számokat illetve betűket tartalmazhat, amit szóközök illetve kötőjelek tagolhatnak.",
|
||||||
|
"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.": "A beírt csomópont azonosító nem tűnik megfelelőnek. 52 vagy 56 karakteresnek kell lennie csak számot és betűt kell tartalmaznia és szóközökkel vagy kötőjelekkel lehet tagolva.",
|
||||||
|
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "A következő intervallumokat használjuk: egy-egy régi verziót őrzünk meg az első órában minden 30 másodpercben, az első nap minden órában, az első 30 napban minden nap, egészen addig amíg el nem érjük a maximálisan megtartható verziók számát minden héten.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "A maximális kornak számnak kell lenni és nem lehet üres",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "A verziók megtartásának maximális ideje (napokban, ha 0-t adsz meg örökre megmaradnak).",
|
||||||
|
"The node ID cannot be blank.": "A csomópont azonosító nem lehet üres",
|
||||||
|
"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).": "A csomópont azonosító a túloldalon a \"Beállítások > Azonosító mutatása\" alatt található. A szóközök illetve a kötőjelek opcionálisak (kihagyhatóak). ",
|
||||||
|
"The number of old versions to keep, per file.": "Mennyi régi változatot tartsunk meg a file-okból",
|
||||||
|
"The number of versions must be a number and cannot be blank.": "A megtartott változatok száma nem lehet üres",
|
||||||
|
"The repository ID cannot be blank.": "A tároló azonosító nem lehet üres",
|
||||||
|
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "A tároló azonosító egy rövid (maximálisan 64 karakteres) csak számokat, betűket, pontot (.), kötőjelet (-) illetve aláhúzást (_) tartalmazó karakterlánc.",
|
||||||
|
"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.": "A tároló azonosító egy rövid (maximálisan 64 karakteres) csak számokat, betűket, pontot (.), kötőjelet (-) illetve aláhúzást (_) tartalmazó karakterlánc",
|
||||||
|
"The repository ID must be unique.": "A tároló azonosítónak egyedinek kell lennie",
|
||||||
|
"The repository path cannot be blank.": "A tároló útvonala nem lehet üres",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "Az átnézési intervallumbak legalább 5 másodpercnek kell lennie.",
|
||||||
|
"Unknown": "Ismeretlen",
|
||||||
|
"Up to Date": "Friss",
|
||||||
|
"Upgrade To {%version%}": "Frissítés a {{version}} verzióra",
|
||||||
|
"Upgrading": "Frissítés",
|
||||||
|
"Upload Rate": "Feltöltési sebesség",
|
||||||
|
"Usage": "Használat",
|
||||||
|
"Use Compression": "Tömörítés használata",
|
||||||
|
"Use HTTPS for GUI": "HTTPS használata a GUI-hoz",
|
||||||
|
"Version": "Verzió",
|
||||||
|
"Versions Path": "Verziók útvonala",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "A régi verziók automatikusan törlődnek, amennyiben öregebbek mint a maximum kor, vagy már több van belőlük mint az adott intervallumban megtartható maximum.",
|
||||||
|
"When adding a new node, keep in mind that this node must be added on the other side too.": "Amikor új csomópontot adsz hozzá, tartsd észben, hogy azt a túloldalhoz is hozzá kell majd adni.",
|
||||||
|
"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.": "Amikor hozzáadod a tárolót, tartsad észben, hogy a Tároló azonosító az ami összeköti a csomópontokat. Kis-nagybetű érzékeny, és pontosan ugyan úgy kell azokat megadni mindegyik csomóponton.",
|
||||||
|
"Yes": "Igen",
|
||||||
|
"You must keep at least one version.": "Legalább egy verziót meg kell tartanod",
|
||||||
|
"full documentation": "teljes dokumentáció",
|
||||||
|
"items": "tételek"
|
||||||
|
}
|
||||||
@@ -8,10 +8,13 @@
|
|||||||
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
|
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
|
||||||
"Announce Server": "Server di Presenza Globale dei Nodi",
|
"Announce Server": "Server di Presenza Globale dei Nodi",
|
||||||
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
|
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
"Bugs": "Bug",
|
"Bugs": "Bug",
|
||||||
"CPU Utilization": "Utilizzo della CPU",
|
"CPU Utilization": "Utilizzo CPU",
|
||||||
"Close": "Chiudi",
|
"Close": "Chiudi",
|
||||||
"Connection Error": "Connection Error",
|
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
|
"Connection Error": "Errore di Connessione",
|
||||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg e i seguenti Collaboratori:",
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg e i seguenti Collaboratori:",
|
||||||
"Delete": "Elimina",
|
"Delete": "Elimina",
|
||||||
"Disconnected": "Disconnesso",
|
"Disconnected": "Disconnesso",
|
||||||
@@ -20,8 +23,10 @@
|
|||||||
"Edit": "Modifica",
|
"Edit": "Modifica",
|
||||||
"Edit Node": "Modifica Nodo",
|
"Edit Node": "Modifica Nodo",
|
||||||
"Edit Repository": "Modifica Deposito",
|
"Edit Repository": "Modifica Deposito",
|
||||||
|
"Editing": "Editing",
|
||||||
"Enable UPnP": "Attiva UPnP",
|
"Enable UPnP": "Attiva UPnP",
|
||||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserisci gli indirizzi \"ip:porta\" separati da una virgola, altrimenti inserisci \"dynamic\" per effettuare il rilevamento automatico dell'indirizzo.",
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserisci gli indirizzi \"ip:porta\" separati da una virgola, altrimenti inserisci \"dynamic\" per effettuare il rilevamento automatico dell'indirizzo.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
"Error": "Errore",
|
"Error": "Errore",
|
||||||
"File Versioning": "Controllo Versione dei File",
|
"File Versioning": "Controllo Versione dei File",
|
||||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Il software evita i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
|
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Il software evita i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
|
||||||
@@ -36,18 +41,27 @@
|
|||||||
"Global Discovery Server": "Server di Ricerca Globale",
|
"Global Discovery Server": "Server di Ricerca Globale",
|
||||||
"Global Repository": "Deposito Globale",
|
"Global Repository": "Deposito Globale",
|
||||||
"Idle": "Inattivo",
|
"Idle": "Inattivo",
|
||||||
|
"Ignore Patterns": "Ignore Patterns",
|
||||||
"Ignore Permissions": "Ignora Permessi",
|
"Ignore Permissions": "Ignora Permessi",
|
||||||
"Keep Versions": "Mantieni le Versioni",
|
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||||
"Latest Release": "Ultimo Rilascio",
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
|
"Keep Versions": "Versioni Mantenute",
|
||||||
|
"Last seen": "Last seen",
|
||||||
|
"Latest Release": "Ultima Versione",
|
||||||
"Local Discovery": "Individuazione Locale",
|
"Local Discovery": "Individuazione Locale",
|
||||||
"Local Discovery Port": "Porta di Individuazione Locale",
|
"Local Discovery Port": "Porta di Individuazione Locale",
|
||||||
"Local Repository": "Deposito Locale",
|
"Local Repository": "Deposito Locale",
|
||||||
"Master Repo": "Deposito Principale",
|
"Master Repo": "Deposito Principale",
|
||||||
"Max File Change Rate (KiB/s)": "Tasso Massimo di Cambiamento dei File (KiB/s)",
|
"Max File Change Rate (KiB/s)": "Tasso Massimo di Cambiamento dei File (KiB/s)",
|
||||||
"Max Outstanding Requests": "Numero Massimo di Richieste Simultanee per i Blocchi di File",
|
"Max Outstanding Requests": "Numero Massimo di Richieste Simultanee per i Blocchi di File",
|
||||||
|
"Maximum Age": "Durata Massima",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
|
"Never": "Never",
|
||||||
"No": "No",
|
"No": "No",
|
||||||
|
"No File Versioning": "Nessun Controllo Versione",
|
||||||
"Node ID": "ID Nodo",
|
"Node ID": "ID Nodo",
|
||||||
"Node Identification": "Node Identification",
|
"Node Identification": "Identificazione Nodo",
|
||||||
"Node Name": "Nome Nodo",
|
"Node Name": "Nome Nodo",
|
||||||
"Notice": "Avviso",
|
"Notice": "Avviso",
|
||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
@@ -57,17 +71,22 @@
|
|||||||
"Outgoing Rate Limit (KiB/s)": "Limite di Velocità in Uscita (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Limite di Velocità in Uscita (KiB/s)",
|
||||||
"Override Changes": "Ignora Modifiche",
|
"Override Changes": "Ignora Modifiche",
|
||||||
"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",
|
"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",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Percorso di salvataggio delle versioni (lasciare vuoto per utilizzare la cartella predefinita .stversions nel deposito).",
|
||||||
"Please wait": "Attendere prego",
|
"Please wait": "Attendere prego",
|
||||||
|
"Preview": "Preview",
|
||||||
"Preview Usage Report": "Anteprima Statistiche di Utilizzo",
|
"Preview Usage Report": "Anteprima Statistiche di Utilizzo",
|
||||||
"RAM Utilization": "Utilizzo della RAM",
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
|
"RAM Utilization": "Utilizzo RAM",
|
||||||
"Reconnect Interval (s)": "Intervallo di Riconnessione (s)",
|
"Reconnect Interval (s)": "Intervallo di Riconnessione (s)",
|
||||||
"Repository ID": "ID Deposito",
|
"Repository ID": "ID Deposito",
|
||||||
"Repository Master": "Deposito Principale",
|
"Repository Master": "Deposito Principale",
|
||||||
"Repository Path": "Percorso del Deposito",
|
"Repository Path": "Percorso del Deposito",
|
||||||
|
"Rescan": "Riscansiona",
|
||||||
|
"Rescan Interval": "Intervallo di Scansione",
|
||||||
"Rescan Interval (s)": "Intervallo di Scansione dei File (s)",
|
"Rescan Interval (s)": "Intervallo di Scansione dei File (s)",
|
||||||
"Restart": "Riavvia",
|
"Restart": "Riavvia",
|
||||||
"Restart Needed": "Riavvio Necessario",
|
"Restart Needed": "Riavvio Necessario",
|
||||||
"Restarting": "Restarting",
|
"Restarting": "Riavvio",
|
||||||
"Save": "Salva",
|
"Save": "Salva",
|
||||||
"Scanning": "Scansione in corso",
|
"Scanning": "Scansione in corso",
|
||||||
"Select the nodes to share this repository with.": "Seleziona i nodi con i quali vuoi condividere questo deposito.",
|
"Select the nodes to share this repository with.": "Seleziona i nodi con i quali vuoi condividere questo deposito.",
|
||||||
@@ -77,8 +96,13 @@
|
|||||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Breve identificatore del deposito. Deve essere lo stesso su tutti i nodi del cluster.",
|
"Short identifier for the repository. Must be the same on all cluster nodes.": "Breve identificatore del deposito. Deve essere lo stesso su tutti i nodi del cluster.",
|
||||||
"Show ID": "Mostra ID",
|
"Show ID": "Mostra ID",
|
||||||
"Shown instead of Node ID in the cluster status.": "Visibile al posto dell'ID Nodo nello stato del cluster.",
|
"Shown instead of Node ID in the cluster status.": "Visibile al posto dell'ID Nodo nello stato del cluster.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.",
|
||||||
"Shutdown": "Arresta",
|
"Shutdown": "Arresta",
|
||||||
|
"Simple File Versioning": "Controllo Versione Semplice",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
"Source Code": "Codice Sorgente",
|
"Source Code": "Codice Sorgente",
|
||||||
|
"Staggered File Versioning": "Controllo Versione Cadenzato",
|
||||||
"Start Browser": "Avvia Browser",
|
"Start Browser": "Avvia Browser",
|
||||||
"Stopped": "Fermato",
|
"Stopped": "Fermato",
|
||||||
"Support / Forum": "Supporto / Forum",
|
"Support / Forum": "Supporto / Forum",
|
||||||
@@ -87,34 +111,42 @@
|
|||||||
"Syncing": "Sincronizzazione in corso",
|
"Syncing": "Sincronizzazione in corso",
|
||||||
"Syncthing has been shut down.": "Syncthing è stato arrestato.",
|
"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 includes the following software or portions thereof:": "Syncthing include i seguenti software o porzioni di questi:",
|
||||||
"Syncthing is restarting.": "Syncthing is restarting.",
|
"Syncthing is restarting.": "Riavvio di Syncthing in corso.",
|
||||||
"Syncthing is upgrading.": "Syncthing is upgrading.",
|
"Syncthing is upgrading.": "Aggiornamento di Syncthing in corso.",
|
||||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing sembra inattivo, oppure c'è un problema con la tua connessione a Internet. Nuovo tentativo…",
|
"Syncthing seems to be 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 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 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 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 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "La durata massima dev'essere un numero e non può essere vuoto.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La durata massima di una versione (in giorni, imposta a 0 per mantenere le versioni per sempre).",
|
||||||
"The node ID cannot be blank.": "L'ID del nodo non può essere vuoto.",
|
"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 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 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.",
|
"The repository ID cannot be blank.": "L'ID del deposito non può essere vuoto.",
|
||||||
|
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and 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 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.": "L'ID del deposito dev'essere un identificatore breve (64 caratteri o meno) costituito solamente da lettere, numeri, punti (.), trattini (-) e trattini bassi (_).",
|
"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 del deposito dev'essere un identificatore breve (64 caratteri o meno) costituito solamente da lettere, numeri, punti (.), trattini (-) e trattini bassi (_).",
|
||||||
"The repository ID must be unique.": "L'ID del deposito dev'essere unico.",
|
"The repository ID must be unique.": "L'ID del deposito dev'essere unico.",
|
||||||
"The repository path cannot be blank.": "Il percorso del deposito non può essere vuoto.",
|
"The repository path cannot be blank.": "Il percorso del deposito non può essere vuoto.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "L'intervallo di scansione non può essere inferiore a 5 secondi.",
|
||||||
"Unknown": "Sconosciuto",
|
"Unknown": "Sconosciuto",
|
||||||
"Up to Date": "Sincronizzato",
|
"Up to Date": "Sincronizzato",
|
||||||
"Upgrade To {%version%}": "Aggiorna Alla {{version}}",
|
"Upgrade To {%version%}": "Aggiorna Alla {{version}}",
|
||||||
"Upgrading": "Upgrading",
|
"Upgrading": "Aggiornamento",
|
||||||
"Upload Rate": "Velocità Upload",
|
"Upload Rate": "Velocità Upload",
|
||||||
"Usage": "Utilizzo",
|
"Usage": "Utilizzo",
|
||||||
"Use Compression": "Utilizza Compressione",
|
"Use Compression": "Utilizza Compressione",
|
||||||
"Use HTTPS for GUI": "Utilizza HTTPS per l'interfaccia grafica",
|
"Use HTTPS for GUI": "Utilizza HTTPS per l'interfaccia grafica",
|
||||||
"Version": "Versione",
|
"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.",
|
"Versions Path": "Percorso Cartella delle Versioni",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Le versioni vengono eliminate automaticamente se superano la durata massima o il numero di file permessi in un determinato intervallo temporale.",
|
||||||
|
"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.",
|
"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ì",
|
"Yes": "Sì",
|
||||||
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
|
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
|
||||||
|
"full documentation": "full documentation",
|
||||||
"items": "elementi"
|
"items": "elementi"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
{
|
||||||
|
"API Key": "API raktas",
|
||||||
|
"About": "Apie programą",
|
||||||
|
"Add Node": "Pridėti mazgą",
|
||||||
|
"Add Repository": "Pridėti saugyklą",
|
||||||
|
"Address": "Adresas",
|
||||||
|
"Addresses": "Adresai",
|
||||||
|
"Allow Anonymous Usage Reporting?": "Siųsti anonimišką vartojimo ataskaitą?",
|
||||||
|
"Announce Server": "Aptikimų serveris",
|
||||||
|
"Anonymous Usage Reporting": "Anoniminė vartojimo ataskaita",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
|
"Bugs": "Klaidos",
|
||||||
|
"CPU Utilization": "CAP sunaudojimas",
|
||||||
|
"Close": "Uždaryti",
|
||||||
|
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
|
"Connection Error": "Susijungimo klaida",
|
||||||
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Visos teisės saugomos © 2014 Jakob Borg ir šių bendraautorių:",
|
||||||
|
"Delete": "Trinti",
|
||||||
|
"Disconnected": "Atsijungęs",
|
||||||
|
"Documentation": "Aprašymas",
|
||||||
|
"Download Rate": "Parsisiuntimo greitis",
|
||||||
|
"Edit": "Redaguoti",
|
||||||
|
"Edit Node": "Redaguoti mazgą",
|
||||||
|
"Edit Repository": "Redaguoti saugyklą",
|
||||||
|
"Editing": "Redagavimas",
|
||||||
|
"Enable UPnP": "Įjungti UPnP",
|
||||||
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Įveskite dvitaškiu atskirtą \"ip:port\" adresą arba žodį \"dynamic\" norėdami gauti adresą automatiškai",
|
||||||
|
"Enter ignore patterns, one per line.": "Suveskite nepaisomus šablonus, kiekvieną naujoje eilutėje.",
|
||||||
|
"Error": "Klaida",
|
||||||
|
"File Versioning": "Versijų valdymas",
|
||||||
|
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Nekreipti dėmesio į failų naudojimosi leidimus.\nTaikyti FAT sistemose.",
|
||||||
|
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Syncthing programa talpina senesnes versijas .stversions aplanke.",
|
||||||
|
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Failai apsaugoti nuo pakeitimų kituose mazguose, bet pakeitimai padaryti šiame mazge bus išsiųsti visai grupei.",
|
||||||
|
"Folder": "Aplankas",
|
||||||
|
"GUI Authentication Password": "Valdymo skydelio slaptažodis",
|
||||||
|
"GUI Authentication User": "Valdymo skydelio vartotojo vardas",
|
||||||
|
"GUI Listen Addresses": "Valdymo skydelio adresas",
|
||||||
|
"Generate": "Sukurti",
|
||||||
|
"Global Discovery": "Visuotinis matomumas",
|
||||||
|
"Global Discovery Server": "Visuotinio matomumo serveris",
|
||||||
|
"Global Repository": "Visuotinė saugykla",
|
||||||
|
"Idle": "Laisvas",
|
||||||
|
"Ignore Patterns": "Nepaisyti šablonų",
|
||||||
|
"Ignore Permissions": "Nepaisyti failų prieigos leidimų",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Įeinančio srauto maksimalus greitis (KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Apversti sąlygas (pvz.: nenustoti naudoti)",
|
||||||
|
"Keep Versions": "Saugojamų versijų kiekis",
|
||||||
|
"Last seen": "Paskutinį kartą matytas",
|
||||||
|
"Latest Release": "Paskutinė versija",
|
||||||
|
"Local Discovery": "Vietinis matomumas",
|
||||||
|
"Local Discovery Port": "Vietinio matomumo jungtis",
|
||||||
|
"Local Repository": "Vietinė saugykla",
|
||||||
|
"Master Repo": "Pagrindinė saugykla",
|
||||||
|
"Max File Change Rate (KiB/s)": "Maksimalus failų apsikeitimo greitis (KiB/s)",
|
||||||
|
"Max Outstanding Requests": "Maksimalus išeinančių užklausų skaičius",
|
||||||
|
"Maximum Age": "Maksimalus amžius",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Keletos lygių pakaitos (atitinka keletą direktorijų lygių)",
|
||||||
|
"Never": "Niekada",
|
||||||
|
"No": "Ne",
|
||||||
|
"No File Versioning": "Nėra versijų valdymo",
|
||||||
|
"Node ID": "Mazgo vardas",
|
||||||
|
"Node Identification": "Mazgo tapatybė",
|
||||||
|
"Node Name": "Mazgo vardas",
|
||||||
|
"Notice": "Įspėjimas",
|
||||||
|
"OK": "Gerai",
|
||||||
|
"Offline": "Atsijungęs",
|
||||||
|
"Online": "Prisijungęs",
|
||||||
|
"Out Of Sync": "Nesutikrinta",
|
||||||
|
"Outgoing Rate Limit (KiB/s)": "Išeinančio srauto maksimalus greitis (KiB/s)",
|
||||||
|
"Override Changes": "Perrašyti pakeitimus",
|
||||||
|
"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": "Kelias iki vietinės saugyklos. Tildės ženklas (~) gali būti naudojamas nuorodai",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Kelias iki papkės kur saugomos senesnės versijos (palikite tuščią norėdami naudoti .stversions)",
|
||||||
|
"Please wait": "Prašome palaukti",
|
||||||
|
"Preview": "Peržiūra",
|
||||||
|
"Preview Usage Report": "Vartojimo statistikos peržiūra",
|
||||||
|
"Quick guide to supported patterns": "Trumpas leistinų šablonų vadovas",
|
||||||
|
"RAM Utilization": "LKA panaudojimas",
|
||||||
|
"Reconnect Interval (s)": "Pertrauka tarp susijungimų (s)",
|
||||||
|
"Repository ID": "Saugyklos vardas",
|
||||||
|
"Repository Master": "Pagrindinė saugykla",
|
||||||
|
"Repository Path": "Kelias iki saugyklos",
|
||||||
|
"Rescan": "Nuskaityti iš naujo",
|
||||||
|
"Rescan Interval": "Pertrauka tarp nuskaitymų",
|
||||||
|
"Rescan Interval (s)": "Pertrauka tarp nuskaitymų (s)",
|
||||||
|
"Restart": "Perleisti",
|
||||||
|
"Restart Needed": "Reikalingas perleidimas",
|
||||||
|
"Restarting": "Persileidžia",
|
||||||
|
"Save": "Išsaugoti",
|
||||||
|
"Scanning": "Skenuojama",
|
||||||
|
"Select the nodes to share this repository with.": "Pasirinkite mazgus su kuriais norite dalintis šia saugykla",
|
||||||
|
"Settings": "Nustatymai",
|
||||||
|
"Share With Nodes": "Dalinamasi su šiais mazgais",
|
||||||
|
"Shared With": "Dalinamasi su",
|
||||||
|
"Short identifier for the repository. Must be the same on all cluster nodes.": "Trumpas saugyklos identifikavimas. Turi būti vienodas visuose grupės mazguose.",
|
||||||
|
"Show ID": "Rodyti vardą",
|
||||||
|
"Shown instead of Node ID in the cluster status.": "Grupės būsenoje rodomas vietoje mazgo vardo.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Grupės būsenoje rodomas vietoje mazgo vardo. Kiti mazgai matys kaip pasirinktinį vardą.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Grupės būsenoje rodomas vietoje mazgo vardo. Bus atnaujintas į mazgo vardą jei nieko neįrašysite.",
|
||||||
|
"Shutdown": "Išjungti",
|
||||||
|
"Simple File Versioning": "Supaprastintas versijų valdymas",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Vieno lygio pakaitos (atitinka tik vieną direktorijos lygį)",
|
||||||
|
"Source Code": "Išeities kodas",
|
||||||
|
"Staggered File Versioning": "Pakopinis versijų valdymas",
|
||||||
|
"Start Browser": "Paleisti naršyklę",
|
||||||
|
"Stopped": "Sustabdyta",
|
||||||
|
"Support / Forum": "Palaikymas / Diskusijos",
|
||||||
|
"Sync Protocol Listen Addresses": "Sutapatinimo taisyklių adresas",
|
||||||
|
"Synchronization": "Sutapatinimas",
|
||||||
|
"Syncing": "Sutapatinama",
|
||||||
|
"Syncthing has been shut down.": "Syncthing išjungtas",
|
||||||
|
"Syncthing includes the following software or portions thereof:": "Syncthing naudoja šias programas ar jų dalis:",
|
||||||
|
"Syncthing is restarting.": "Syncthing perleidžiamas",
|
||||||
|
"Syncthing is upgrading.": "Syncthing atsinaujina.",
|
||||||
|
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing išjungta arba problemos su Interneto ryšių. Bandoma iš naujo...",
|
||||||
|
"The aggregated statistics are publicly available at {%url%}.": "Naudojimosi ataskaitą galite peržiūrėti adresu: {{url}}.",
|
||||||
|
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Nauji nustatymai išsaugoti, bet neaktyvuoti. Perleiskite Syncthing programą iš naujo norėdami įgalinti naujus nustatymus.",
|
||||||
|
"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.": "Šifruota naudojimosi ataskaita siunčiama kasdien. Šiuo metu, ataskaitoje matosi tik operacinė sistema, saugyklos dydis ir programos versija. Jeigu ateityje bus siunčiama daugiau duomenų, jums vėl bus parodyta ši pranešimas.",
|
||||||
|
"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.": "Įvestas neteisingas mazgo vardas. Turi būti 52 simbolių eilutė su raidėmis ir skaičiais kuriuos galima atskirti tarpu arba brūkšneliu.",
|
||||||
|
"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.": "Įvestas neteisingas mazgo vardas. Turi būti 52 ar 56 simbolių eilutė su raidėmis ir skaičiais kuriuos galima atskirti tarpu arba brūkšneliu.",
|
||||||
|
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Šie pertraukų nustatymai naudojami: pirmą valandą versijos laikomos 30 sekundžių, pirmą dieną versijos laikomos valandą, pirmas 30 dienų versijos laikomos parą, kol nebus viršytas nustatytas maksimalus amžius.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "Maksimalus amžius turi būti skaitmuo ir negali būti tuščias laukelis.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimalus laikas kurį bus saugojama versija (dienomis, nustatykite 0 norėdami saugoti amžinai).",
|
||||||
|
"The node ID cannot be blank.": "Mazgo vardas negali būti tuščias.",
|
||||||
|
"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).": "Mazgo vardas visada galima sužinoti apsilankius \"Edit > Show ID\". Tarpai ir brūkšneliai nebūtini (ignoruojami).",
|
||||||
|
"The number of old versions to keep, per file.": "Kiek failo versijų saugoti.",
|
||||||
|
"The number of versions must be a number and cannot be blank.": "Versijų skaičius turi būti skaitmuo ir negali būti tuščias laukelis.",
|
||||||
|
"The repository ID cannot be blank.": "Saugyklos vardas negali būti tuščias.",
|
||||||
|
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Saugyklos vardas negali būti ilgesnis nei 64 simboliai. Galima naudoti tik raides ir skaičius bet tašką (.), brūkšnelį (-) ir pabraukimą (_).",
|
||||||
|
"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.": "Saugyklos vardas negali būti ilgesnis nei 64 simboliai. Galima naudoti tik raides ir skaičius bet tašką (.), brukšnelį (-) ir pabraukimą (_).",
|
||||||
|
"The repository ID must be unique.": "Saugyklos vardas turi būti unikalus.",
|
||||||
|
"The repository path cannot be blank.": "Kelias iki saugyklos negali būti tuščias.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "Nuskaityti galima nedažniau nei kas 5 sekundes.",
|
||||||
|
"Unknown": "Velniava",
|
||||||
|
"Up to Date": "Atnaujinta",
|
||||||
|
"Upgrade To {%version%}": "Atnaujinti į {{version}}",
|
||||||
|
"Upgrading": "Atnaujinama",
|
||||||
|
"Upload Rate": "Išsiuntimo greitis",
|
||||||
|
"Usage": "Vartosena",
|
||||||
|
"Use Compression": "Naudoti suspaudimą",
|
||||||
|
"Use HTTPS for GUI": "Valdymo skydeliui naudoti saugų ryšį ",
|
||||||
|
"Version": "Versija",
|
||||||
|
"Versions Path": "Kelias iki versijos",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versijos ištrinamos jei senesnės už nustatyta maksimalų amžių arba jei viršytas maksimalus failų skaičius per nustatytą laiko tarpą.",
|
||||||
|
"When adding a new node, keep in mind that this node must be added on the other side too.": "Kai pridedate naują mazgą neužmirškite, kad kitame gale reikia sukonfigūruoti šį mazgą.",
|
||||||
|
"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.": "Kai įvedate naują saugyklą neužmirškite, kad ji bus naudojama visuose mazguose. Svarbu visur įvesti visiškai tokį pat saugyklos vardą neužmirštant apie didžiąsias ir mažąsias raides.",
|
||||||
|
"Yes": "Taip",
|
||||||
|
"You must keep at least one version.": "Būtina saugoti bent vieną versiją.",
|
||||||
|
"full documentation": "pilna dokumentacija",
|
||||||
|
"items": "įrašai"
|
||||||
|
}
|
||||||
@@ -8,9 +8,12 @@
|
|||||||
"Allow Anonymous Usage Reporting?": "Bijhouden van anonieme gebruikers statistieken toestaan?",
|
"Allow Anonymous Usage Reporting?": "Bijhouden van anonieme gebruikers statistieken toestaan?",
|
||||||
"Announce Server": "Aankondigings Server",
|
"Announce Server": "Aankondigings Server",
|
||||||
"Anonymous Usage Reporting": "Bijhouden anonieme gebruikers statistieken",
|
"Anonymous Usage Reporting": "Bijhouden anonieme gebruikers statistieken",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
"Bugs": "Fouten",
|
"Bugs": "Fouten",
|
||||||
"CPU Utilization": "CPU Gebruik",
|
"CPU Utilization": "CPU Gebruik",
|
||||||
"Close": "Sluiten",
|
"Close": "Sluiten",
|
||||||
|
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
"Connection Error": "Verbindingsfout",
|
"Connection Error": "Verbindingsfout",
|
||||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg en de onderstaande bijdragers:",
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg en de onderstaande bijdragers:",
|
||||||
"Delete": "Verwijderen",
|
"Delete": "Verwijderen",
|
||||||
@@ -20,8 +23,10 @@
|
|||||||
"Edit": "Bewerk",
|
"Edit": "Bewerk",
|
||||||
"Edit Node": "Bewerk node",
|
"Edit Node": "Bewerk node",
|
||||||
"Edit Repository": "Repository Bijwerken",
|
"Edit Repository": "Repository Bijwerken",
|
||||||
|
"Editing": "Editing",
|
||||||
"Enable UPnP": "UPnP aanzetten",
|
"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.",
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Geef, gescheiden door komma's, \"ip:port\" adressen of \"dynamic\" voor het automatische vinden van de addressen.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
"Error": "Fout",
|
"Error": "Fout",
|
||||||
"File Versioning": "Versiebeheer",
|
"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",
|
"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",
|
||||||
@@ -35,9 +40,14 @@
|
|||||||
"Global Discovery": "Globaal zoeken",
|
"Global Discovery": "Globaal zoeken",
|
||||||
"Global Discovery Server": "Globale zoekserver",
|
"Global Discovery Server": "Globale zoekserver",
|
||||||
"Global Repository": "Globale repository",
|
"Global Repository": "Globale repository",
|
||||||
"Idle": "Klaar",
|
"Idle": "Inactief",
|
||||||
|
"Ignore Patterns": "Ignore Patterns",
|
||||||
"Ignore Permissions": "Rechten negeren",
|
"Ignore Permissions": "Rechten negeren",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Download snelheidslimiet (KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
"Keep Versions": "Versies behouden",
|
"Keep Versions": "Versies behouden",
|
||||||
|
"Last seen": "Laatst gezien op",
|
||||||
"Latest Release": "Laatste uitgave",
|
"Latest Release": "Laatste uitgave",
|
||||||
"Local Discovery": "Lokaal zoeken",
|
"Local Discovery": "Lokaal zoeken",
|
||||||
"Local Discovery Port": "Lokaal zoeken-poort",
|
"Local Discovery Port": "Lokaal zoeken-poort",
|
||||||
@@ -45,7 +55,11 @@
|
|||||||
"Master Repo": "Tegen veranderingen beschermen",
|
"Master Repo": "Tegen veranderingen beschermen",
|
||||||
"Max File Change Rate (KiB/s)": "Maximale bestands uitwisselsnelheid (KiB/s)",
|
"Max File Change Rate (KiB/s)": "Maximale bestands uitwisselsnelheid (KiB/s)",
|
||||||
"Max Outstanding Requests": "Maximaal aantal openstaande aanvragen",
|
"Max Outstanding Requests": "Maximaal aantal openstaande aanvragen",
|
||||||
|
"Maximum Age": "Maximum leeftijd",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
|
"Never": "Nooit",
|
||||||
"No": "Nee",
|
"No": "Nee",
|
||||||
|
"No File Versioning": "Geen versiebeheer",
|
||||||
"Node ID": "Node ID",
|
"Node ID": "Node ID",
|
||||||
"Node Identification": "Node Identificatie",
|
"Node Identification": "Node Identificatie",
|
||||||
"Node Name": "Node naam",
|
"Node Name": "Node naam",
|
||||||
@@ -57,13 +71,18 @@
|
|||||||
"Outgoing Rate Limit (KiB/s)": "Uitgaande snelheidslimiet (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Uitgaande snelheidslimiet (KiB/s)",
|
||||||
"Override Changes": "Veranderingen overschrijven",
|
"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",
|
"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",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Pad waar de verschillende versies opgeslagen dienen te worden (laat leeg voor de standaard map '.stversion' in de repository).",
|
||||||
"Please wait": "Even geduld",
|
"Please wait": "Even geduld",
|
||||||
"Preview Usage Report": "Bekijk gebruikers statistieken",
|
"Preview": "Voorbeeld",
|
||||||
|
"Preview Usage Report": "Bekijk gebruiksstatistieken",
|
||||||
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
"RAM Utilization": "RAM gebruik",
|
"RAM Utilization": "RAM gebruik",
|
||||||
"Reconnect Interval (s)": "Herverbind-interval (s)",
|
"Reconnect Interval (s)": "Herverbind-interval (s)",
|
||||||
"Repository ID": "Repository ID",
|
"Repository ID": "Repository ID",
|
||||||
"Repository Master": "Hoofd repository",
|
"Repository Master": "Hoofd repository",
|
||||||
"Repository Path": "Pad van repository",
|
"Repository Path": "Pad van repository",
|
||||||
|
"Rescan": "Opnieuw scannen",
|
||||||
|
"Rescan Interval": "Scanfrequentie",
|
||||||
"Rescan Interval (s)": "Herscan interval (s)",
|
"Rescan Interval (s)": "Herscan interval (s)",
|
||||||
"Restart": "Herstart",
|
"Restart": "Herstart",
|
||||||
"Restart Needed": "Herstart nodig",
|
"Restart Needed": "Herstart nodig",
|
||||||
@@ -77,8 +96,13 @@
|
|||||||
"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.",
|
"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",
|
"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",
|
"Shown instead of Node ID in the cluster status.": "Wordt weergegeven i.p.v. het node ID in de cluster status",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "De node naam wordt getoond in plaats van de node ID in het cluster status overzicht. Deze naam wordt aan andere nodes voorgesteld als een optionele, standaardnaam.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "De node naam wordt getoond in plaats van de node ID in het cluster status overzicht. Deze naam wordt geüpdatet met de naam die de node zelf adverteert indien dit veld leeg wordt gelaten.",
|
||||||
"Shutdown": "Sluit af",
|
"Shutdown": "Sluit af",
|
||||||
|
"Simple File Versioning": "Eenvoudig versiebeheer",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
"Source Code": "Broncode",
|
"Source Code": "Broncode",
|
||||||
|
"Staggered File Versioning": "Staggered File Versioning",
|
||||||
"Start Browser": "Start browser",
|
"Start Browser": "Start browser",
|
||||||
"Stopped": "Gestopt",
|
"Stopped": "Gestopt",
|
||||||
"Support / Forum": "Support / Forum",
|
"Support / Forum": "Support / Forum",
|
||||||
@@ -95,14 +119,19 @@
|
|||||||
"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 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 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 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De volgende intervallen worden gebruikt: het eerste uur worden versies iedere 30 seconden bewaard, de eerste dag worden versies ieder uur bewaard, de eerste 30 dagen worden versies iedere dag bewaard, tot de maximale leeftijd worden versies iedere week bewaard.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "De maximum leeftijd moet uit cijfers bestaan en mag niet leeggelaten worden.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "De maximale tijdsduur om een versie te bewaren (in dagen, gebruik 0 om versies voor altijd te bewaren).",
|
||||||
"The node ID cannot be blank.": "Er moet een node ID ingevoerd worden",
|
"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 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 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 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 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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and 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.": "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 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 ID must be unique.": "Het repository ID moet uniek zijn.",
|
||||||
"The repository path cannot be blank.": "Het repository ID moet ingevuld worden.",
|
"The repository path cannot be blank.": "Het repository ID moet ingevuld worden.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "De scanfrequentie moet minimaal 5 seconde zijn.",
|
||||||
"Unknown": "Onbekend",
|
"Unknown": "Onbekend",
|
||||||
"Up to Date": "Gesynchroniseerd",
|
"Up to Date": "Gesynchroniseerd",
|
||||||
"Upgrade To {%version%}": "Upgrade naar {{version}}",
|
"Upgrade To {%version%}": "Upgrade naar {{version}}",
|
||||||
@@ -112,9 +141,12 @@
|
|||||||
"Use Compression": "Compressie gebruiken",
|
"Use Compression": "Compressie gebruiken",
|
||||||
"Use HTTPS for GUI": "Gebruik HTTPS voor de GUI",
|
"Use HTTPS for GUI": "Gebruik HTTPS voor de GUI",
|
||||||
"Version": "Versie",
|
"Version": "Versie",
|
||||||
|
"Versions Path": "Bestandspad versies",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versies worden automatisch verwijderd als deze ouder zijn dan de maximale leeftijd of als ze het maximaal aantal toegestane bestanden per interval overschrijden. ",
|
||||||
"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 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.",
|
"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",
|
"Yes": "Ja",
|
||||||
"You must keep at least one version.": "Minstens 1 versie moet bewaard blijven.",
|
"You must keep at least one version.": "Minstens 1 versie moet bewaard blijven.",
|
||||||
|
"full documentation": "full documentation",
|
||||||
"items": "objecten"
|
"items": "objecten"
|
||||||
}
|
}
|
||||||
@@ -7,10 +7,13 @@
|
|||||||
"Addresses": "Endereços",
|
"Addresses": "Endereços",
|
||||||
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
|
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
|
||||||
"Announce Server": "Servidor de anúncios",
|
"Announce Server": "Servidor de anúncios",
|
||||||
"Anonymous Usage Reporting": "Enviar de relatórios anónimos de utilização",
|
"Anonymous Usage Reporting": "Enviar relatórios anónimos de utilização",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
"Bugs": "Erros",
|
"Bugs": "Erros",
|
||||||
"CPU Utilization": "Utilização da CPU",
|
"CPU Utilization": "Utilização da CPU",
|
||||||
"Close": "Fechar",
|
"Close": "Fechar",
|
||||||
|
"Comment, when used at the start of a line": "Comentário, quando usado no início de uma linha",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
"Connection Error": "Erro de ligação",
|
"Connection Error": "Erro de ligação",
|
||||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Direitos reservados © 2014 Jakob Borg e os seguintes contribuidores:",
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Direitos reservados © 2014 Jakob Borg e os seguintes contribuidores:",
|
||||||
"Delete": "Eliminar",
|
"Delete": "Eliminar",
|
||||||
@@ -20,12 +23,14 @@
|
|||||||
"Edit": "Editar",
|
"Edit": "Editar",
|
||||||
"Edit Node": "Editar nó",
|
"Edit Node": "Editar nó",
|
||||||
"Edit Repository": "Editar repositório",
|
"Edit Repository": "Editar repositório",
|
||||||
|
"Editing": "Editando",
|
||||||
"Enable UPnP": "Activar UPnP",
|
"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 descobrir automaticamente o 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.",
|
||||||
|
"Enter ignore patterns, one per line.": "Coloque os padrões a ignorar, um por linha.",
|
||||||
"Error": "Erro",
|
"Error": "Erro",
|
||||||
"File Versioning": "Gestão de versões",
|
"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 ao procurar alterações. Utilize nos sistemas de ficheiros FAT.",
|
"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 moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Os ficheiros são movidos para versões marcadas 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.",
|
"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",
|
"Folder": "Pasta",
|
||||||
"GUI Authentication Password": "Senha da autenticação na interface gráfica",
|
"GUI Authentication Password": "Senha da autenticação na interface gráfica",
|
||||||
@@ -36,8 +41,13 @@
|
|||||||
"Global Discovery Server": "Servidor da busca global",
|
"Global Discovery Server": "Servidor da busca global",
|
||||||
"Global Repository": "Repositório global",
|
"Global Repository": "Repositório global",
|
||||||
"Idle": "Em espera",
|
"Idle": "Em espera",
|
||||||
|
"Ignore Patterns": "Ignorar padrões",
|
||||||
"Ignore Permissions": "Ignorar permissões",
|
"Ignore Permissions": "Ignorar permissões",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Limite de velocidade de recepção (KiB/s)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversão de uma dada condição (ou seja, não excluir)",
|
||||||
"Keep Versions": "Manter versões",
|
"Keep Versions": "Manter versões",
|
||||||
|
"Last seen": "Última vez que foi verificado",
|
||||||
"Latest Release": "Última versão",
|
"Latest Release": "Última versão",
|
||||||
"Local Discovery": "Busca local",
|
"Local Discovery": "Busca local",
|
||||||
"Local Discovery Port": "Porto da busca local",
|
"Local Discovery Port": "Porto da busca local",
|
||||||
@@ -45,11 +55,15 @@
|
|||||||
"Master Repo": "Repositório mestre",
|
"Master Repo": "Repositório mestre",
|
||||||
"Max File Change Rate (KiB/s)": "Velocidade máxima de alterações de ficheiros (KiB/s)",
|
"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",
|
"Max Outstanding Requests": "Número máximo de pedidos pendentes",
|
||||||
|
"Maximum Age": "Idade máxima",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Curinga multi-nível (faz corresponder a vários níveis de pastas)",
|
||||||
|
"Never": "Nunca",
|
||||||
"No": "Não",
|
"No": "Não",
|
||||||
|
"No File Versioning": "Sem gestão de versões de ficheiros",
|
||||||
"Node ID": "ID do nó",
|
"Node ID": "ID do nó",
|
||||||
"Node Identification": "Identificação do nó",
|
"Node Identification": "Identificação do nó",
|
||||||
"Node Name": "Nome do nó",
|
"Node Name": "Nome do nó",
|
||||||
"Notice": "Nota",
|
"Notice": "Avisos",
|
||||||
"OK": "OK",
|
"OK": "OK",
|
||||||
"Offline": "Desconectado",
|
"Offline": "Desconectado",
|
||||||
"Online": "Conectado",
|
"Online": "Conectado",
|
||||||
@@ -57,19 +71,24 @@
|
|||||||
"Outgoing Rate Limit (KiB/s)": "Limite da velocidade de envio (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Limite da velocidade de envio (KiB/s)",
|
||||||
"Override Changes": "Sobrepor alterações",
|
"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",
|
"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",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Caminho onde as versões são guardadas (deixe vazio para usar a pasta pré-definida .stversions no repositório).",
|
||||||
"Please wait": "Aguarde",
|
"Please wait": "Aguarde",
|
||||||
|
"Preview": "Previsão",
|
||||||
"Preview Usage Report": "Pré-visualizar relatório de utilização",
|
"Preview Usage Report": "Pré-visualizar relatório de utilização",
|
||||||
|
"Quick guide to supported patterns": "Guia rápido dos padrões suportados",
|
||||||
"RAM Utilization": "Utilização da RAM",
|
"RAM Utilization": "Utilização da RAM",
|
||||||
"Reconnect Interval (s)": "Intervalo de reestabelecimento de ligação (s)",
|
"Reconnect Interval (s)": "Intervalo de reestabelecimento de ligação (s)",
|
||||||
"Repository ID": "ID do repositório",
|
"Repository ID": "ID do repositório",
|
||||||
"Repository Master": "Repositório mestre",
|
"Repository Master": "Repositório mestre",
|
||||||
"Repository Path": "Caminho do repositório",
|
"Repository Path": "Caminho do repositório",
|
||||||
"Rescan Interval (s)": "Intervalo entre varrimentos (s)",
|
"Rescan": "Verificar agora",
|
||||||
|
"Rescan Interval": "Intervalo entre verificações",
|
||||||
|
"Rescan Interval (s)": "Intervalo entre verificações (em segundos)",
|
||||||
"Restart": "Reiniciar",
|
"Restart": "Reiniciar",
|
||||||
"Restart Needed": "É preciso reiniciar",
|
"Restart Needed": "É preciso reiniciar",
|
||||||
"Restarting": "Reiniciando",
|
"Restarting": "Reiniciando",
|
||||||
"Save": "Gravar",
|
"Save": "Gravar",
|
||||||
"Scanning": "Varrendo",
|
"Scanning": "Verificando",
|
||||||
"Select the nodes to share this repository with.": "Seleccione os nós com os quais vai partilhar este repositório.",
|
"Select the nodes to share this repository with.": "Seleccione os nós com os quais vai partilhar este repositório.",
|
||||||
"Settings": "Configurações",
|
"Settings": "Configurações",
|
||||||
"Share With Nodes": "Partilhar com os nós",
|
"Share With Nodes": "Partilhar com os nós",
|
||||||
@@ -77,8 +96,13 @@
|
|||||||
"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.",
|
"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",
|
"Show ID": "Mostrar ID",
|
||||||
"Shown instead of Node ID in the cluster status.": "Apresentado 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.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Apresentado ao invés do ID do nó na informação de estado do agrupamento. Será divulgado aos outros nós como um nome predefinido opcional.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Apresentado ao invés do ID do nó na informação de estado do agrupamento. Será actualizado para o nome que o nó divulga, se for deixado em branco.",
|
||||||
"Shutdown": "Desligar",
|
"Shutdown": "Desligar",
|
||||||
|
"Simple File Versioning": "Gestão de versões de ficheiros simples",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Curinga de um só nível (faz corresponder apenas dentro de uma pasta)",
|
||||||
"Source Code": "Código fonte",
|
"Source Code": "Código fonte",
|
||||||
|
"Staggered File Versioning": "Gestão de versões de ficheiros escalonada",
|
||||||
"Start Browser": "Iniciar navegador",
|
"Start Browser": "Iniciar navegador",
|
||||||
"Stopped": "Parado",
|
"Stopped": "Parado",
|
||||||
"Support / Forum": "Suporte / Fórum",
|
"Support / Forum": "Suporte / Fórum",
|
||||||
@@ -95,26 +119,34 @@
|
|||||||
"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 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 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 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 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "São utilizados os seguintes intervalos: na primeira hora é guardada uma versão a cada 30 segundos, no primeiro dia é guardada uma versão a cada hora, nos primeiros 30 dias é guardada uma versão por dia e, até que atinja a idade máxima, é guardada uma versão por semana.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "A idade máxima tem que ser um número e não pode estar vazia.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Tempo máximo para manter uma versão (em dias, use 0 para manter a versão para sempre).",
|
||||||
"The node ID cannot be blank.": "O ID do nó não pode estar vazio.",
|
"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 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 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 estar 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 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 dot (.), dash (-) and underscode (_) characters only.": "O ID do repositório tem que ser um identificador curto (64 caracteres ou menos) e ser constituído somente por letras, números e os caracteres ponto (.), traço (-) e sublinhado (_).",
|
||||||
"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 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 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 estar vazio.",
|
"The repository path cannot be blank.": "O caminho do repositório não pode estar vazio.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "O intervalo entre verificações tem que ser pelo menos de 5 segundos.",
|
||||||
"Unknown": "Desconhecido",
|
"Unknown": "Desconhecido",
|
||||||
"Up to Date": "Actualizado",
|
"Up to Date": "Actualizado",
|
||||||
"Upgrade To {%version%}": "Actualizar para {{version}}",
|
"Upgrade To {%version%}": "Actualizar para {{version}}",
|
||||||
"Upgrading": "Actualizando",
|
"Upgrading": "Actualizando",
|
||||||
"Upload Rate": "Taxa de envio",
|
"Upload Rate": "Velocidade de envio",
|
||||||
"Usage": "Utilização",
|
"Usage": "Utilização",
|
||||||
"Use Compression": "Usar compressão",
|
"Use Compression": "Usar compressão",
|
||||||
"Use HTTPS for GUI": "Utilizar HTTPS na interface gráfica",
|
"Use HTTPS for GUI": "Utilizar HTTPS na interface gráfica",
|
||||||
"Version": "Versão",
|
"Version": "Versão",
|
||||||
|
"Versions Path": "Caminho das versões",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são eliminadas automaticamente se forem mais antigas do que a idade máxima ou excederem o número máximo de ficheiros permitido num intervalo.",
|
||||||
"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 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.",
|
"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",
|
"Yes": "Sim",
|
||||||
"You must keep at least one version.": "Tem que manter pelo menos uma versão.",
|
"You must keep at least one version.": "Tem que manter pelo menos uma versão.",
|
||||||
|
"full documentation": "documentação completa",
|
||||||
"items": "itens"
|
"items": "itens"
|
||||||
}
|
}
|
||||||
@@ -8,10 +8,13 @@
|
|||||||
"Allow Anonymous Usage Reporting?": "Разрешить сбор анонимной статистики использования?",
|
"Allow Anonymous Usage Reporting?": "Разрешить сбор анонимной статистики использования?",
|
||||||
"Announce Server": "Сервер публикации",
|
"Announce Server": "Сервер публикации",
|
||||||
"Anonymous Usage Reporting": "Анонимная статистика использования",
|
"Anonymous Usage Reporting": "Анонимная статистика использования",
|
||||||
|
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
|
||||||
"Bugs": "Ошибки",
|
"Bugs": "Ошибки",
|
||||||
"CPU Utilization": "Загрузка ЦПУ",
|
"CPU Utilization": "Загрузка ЦПУ",
|
||||||
"Close": "Закрыть",
|
"Close": "Закрыть",
|
||||||
"Connection Error": "Connection Error",
|
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||||
|
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||||
|
"Connection Error": "Ошибка подключения",
|
||||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Все права защищены © 2014 Jakob Borg и следующие участники:",
|
"Copyright © 2014 Jakob Borg and the following Contributors:": "Все права защищены © 2014 Jakob Borg и следующие участники:",
|
||||||
"Delete": "Удалить",
|
"Delete": "Удалить",
|
||||||
"Disconnected": "Нет соединения",
|
"Disconnected": "Нет соединения",
|
||||||
@@ -20,8 +23,10 @@
|
|||||||
"Edit": "Изменить",
|
"Edit": "Изменить",
|
||||||
"Edit Node": "Изменить Узел",
|
"Edit Node": "Изменить Узел",
|
||||||
"Edit Repository": "Изменить Репозиторий",
|
"Edit Repository": "Изменить Репозиторий",
|
||||||
|
"Editing": "Editing",
|
||||||
"Enable UPnP": "Включить UPnP",
|
"Enable UPnP": "Включить UPnP",
|
||||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": " Введите пары \"IP:PORT\" разделённые запятыми, или слово \"dynamic\" для автоматического обнаружения адреса.",
|
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": " Введите пары \"IP:PORT\" разделённые запятыми, или слово \"dynamic\" для автоматического обнаружения адреса.",
|
||||||
|
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
|
||||||
"Error": "Ошибка",
|
"Error": "Ошибка",
|
||||||
"File Versioning": "Управление версиями",
|
"File Versioning": "Управление версиями",
|
||||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Права доступа к файлам будут игнорироваться. Используйте на файловых системах типа FAT.",
|
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Права доступа к файлам будут игнорироваться. Используйте на файловых системах типа FAT.",
|
||||||
@@ -36,8 +41,13 @@
|
|||||||
"Global Discovery Server": "Сервер глобального обнаружения",
|
"Global Discovery Server": "Сервер глобального обнаружения",
|
||||||
"Global Repository": "Глобальный репозиторий",
|
"Global Repository": "Глобальный репозиторий",
|
||||||
"Idle": "Бездействует",
|
"Idle": "Бездействует",
|
||||||
|
"Ignore Patterns": "Ignore Patterns",
|
||||||
"Ignore Permissions": "Игнорировать файловые права доступа",
|
"Ignore Permissions": "Игнорировать файловые права доступа",
|
||||||
|
"Incoming Rate Limit (KiB/s)": "Ограничение входящего потока (Кбит/сек)",
|
||||||
|
"Introducer": "Introducer",
|
||||||
|
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||||
"Keep Versions": "Количество хранимых версий",
|
"Keep Versions": "Количество хранимых версий",
|
||||||
|
"Last seen": "Был доступен",
|
||||||
"Latest Release": "Последняя версия",
|
"Latest Release": "Последняя версия",
|
||||||
"Local Discovery": "Локальное обнаружение",
|
"Local Discovery": "Локальное обнаружение",
|
||||||
"Local Discovery Port": "Порт локального обнаружения",
|
"Local Discovery Port": "Порт локального обнаружения",
|
||||||
@@ -45,9 +55,13 @@
|
|||||||
"Master Repo": "Главный репозиторий",
|
"Master Repo": "Главный репозиторий",
|
||||||
"Max File Change Rate (KiB/s)": "Максимальная скорость изменения файлов (KiB/s)",
|
"Max File Change Rate (KiB/s)": "Максимальная скорость изменения файлов (KiB/s)",
|
||||||
"Max Outstanding Requests": "Максимальное количество исходящих запросов",
|
"Max Outstanding Requests": "Максимальное количество исходящих запросов",
|
||||||
|
"Maximum Age": "Максимальный срок",
|
||||||
|
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||||
|
"Never": "Никогда",
|
||||||
"No": "Нет",
|
"No": "Нет",
|
||||||
|
"No File Versioning": "Без управления версиями файлов",
|
||||||
"Node ID": "ID Узла",
|
"Node ID": "ID Узла",
|
||||||
"Node Identification": "Node Identification",
|
"Node Identification": "Идентификация узла",
|
||||||
"Node Name": "Имя Узла",
|
"Node Name": "Имя Узла",
|
||||||
"Notice": "Внимание",
|
"Notice": "Внимание",
|
||||||
"OK": "ОК",
|
"OK": "ОК",
|
||||||
@@ -57,17 +71,22 @@
|
|||||||
"Outgoing Rate Limit (KiB/s)": "Предел скорости отдачи (KiB/s)",
|
"Outgoing Rate Limit (KiB/s)": "Предел скорости отдачи (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": "Путь к репозиторию на локальном компьютере. Если не существует, то будет создан. Знак тильды (~) может использоваться как ссылка",
|
"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": "Путь к репозиторию на локальном компьютере. Если не существует, то будет создан. Знак тильды (~) может использоваться как ссылка",
|
||||||
|
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Путь, куда нужно сохранять версии (оставьте пустым для папки .stversions в репозитории).",
|
||||||
"Please wait": "Пожалуйста, подождите",
|
"Please wait": "Пожалуйста, подождите",
|
||||||
|
"Preview": "Предварительный просмотр",
|
||||||
"Preview Usage Report": "Посмотреть отчёт об использовании",
|
"Preview Usage Report": "Посмотреть отчёт об использовании",
|
||||||
|
"Quick guide to supported patterns": "Quick guide to supported patterns",
|
||||||
"RAM Utilization": "Использование ОЗУ",
|
"RAM Utilization": "Использование ОЗУ",
|
||||||
"Reconnect Interval (s)": "Интервал переподключений (секунды)",
|
"Reconnect Interval (s)": "Интервал переподключений (секунды)",
|
||||||
"Repository ID": "ID Репозитория",
|
"Repository ID": "ID Репозитория",
|
||||||
"Repository Master": "Главный Репозиторий",
|
"Repository Master": "Главный Репозиторий",
|
||||||
"Repository Path": "Путь к Репозиторию",
|
"Repository Path": "Путь к Репозиторию",
|
||||||
|
"Rescan": "Пересканирование",
|
||||||
|
"Rescan Interval": "Интервал пересканирования",
|
||||||
"Rescan Interval (s)": "Интервал между сканированием (сек)",
|
"Rescan Interval (s)": "Интервал между сканированием (сек)",
|
||||||
"Restart": "Перезапуск",
|
"Restart": "Перезапуск",
|
||||||
"Restart Needed": "Требуется перезапуск",
|
"Restart Needed": "Требуется перезапуск",
|
||||||
"Restarting": "Restarting",
|
"Restarting": "Перезапуск",
|
||||||
"Save": "Сохранить",
|
"Save": "Сохранить",
|
||||||
"Scanning": "Сканирование",
|
"Scanning": "Сканирование",
|
||||||
"Select the nodes to share this repository with.": "Выберите узлы для которых будет доступен данный репозиторий.",
|
"Select the nodes to share this repository with.": "Выберите узлы для которых будет доступен данный репозиторий.",
|
||||||
@@ -77,8 +96,13 @@
|
|||||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Короткий идентификатор репозитория. Должен быть одинаковый на всех узлах кластера.",
|
"Short identifier for the repository. Must be the same on all cluster nodes.": "Короткий идентификатор репозитория. Должен быть одинаковый на всех узлах кластера.",
|
||||||
"Show ID": "Показать ID",
|
"Show ID": "Показать ID",
|
||||||
"Shown instead of Node ID in the cluster status.": "Отображается вместо ID узла в статусе кластера.",
|
"Shown instead of Node ID in the cluster status.": "Отображается вместо ID узла в статусе кластера.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Показано вместо идентификатора узла в статусе кластера. Будет разглашено другим узлам в качестве дополнительного имени по умолчанию.",
|
||||||
|
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Показано вместо идентификатора узла в статусе кластера. Будет обновлено именем, разглашённым узлом, если оставлено пустым.",
|
||||||
"Shutdown": "Выключить",
|
"Shutdown": "Выключить",
|
||||||
|
"Simple File Versioning": "Простое управление версиями файлов",
|
||||||
|
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||||
"Source Code": "Исходный код",
|
"Source Code": "Исходный код",
|
||||||
|
"Staggered File Versioning": "Ступенчатое управление версиями файлов",
|
||||||
"Start Browser": "Открыть браузер",
|
"Start Browser": "Открыть браузер",
|
||||||
"Stopped": "Остановлено",
|
"Stopped": "Остановлено",
|
||||||
"Support / Forum": "Поддержка / Форум",
|
"Support / Forum": "Поддержка / Форум",
|
||||||
@@ -87,34 +111,42 @@
|
|||||||
"Syncing": "Синхронизация",
|
"Syncing": "Синхронизация",
|
||||||
"Syncthing has been shut down.": "Syncthing выключен.",
|
"Syncthing has been shut down.": "Syncthing выключен.",
|
||||||
"Syncthing includes the following software or portions thereof:": "Syncthing включает в себя следующее ПО или его части:",
|
"Syncthing includes the following software or portions thereof:": "Syncthing включает в себя следующее ПО или его части:",
|
||||||
"Syncthing is restarting.": "Syncthing is restarting.",
|
"Syncthing is restarting.": "Перезапуск Syncthing",
|
||||||
"Syncthing is upgrading.": "Syncthing is upgrading.",
|
"Syncthing is upgrading.": "Обновление Syncthing ",
|
||||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, 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 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 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 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.": "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.": "Введённый ID узла выглядит неправильно: ID должен быть строкой, длинной 52 или 56 символов, обязательно содержащей группы букв и цифр которые могут быть разделены пробелами или тире.",
|
||||||
|
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Используются следующие интервалы: в первый час версия меняется каждые 30 секунд, в первый день - каждый час, первые 30 дней - каждый день, после, до максимального срока - каждую неделю.",
|
||||||
|
"The maximum age must be a number and cannot be blank.": "Максимальный срок должен быть числом и не может быть пустым.",
|
||||||
|
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максимальный срок хранения версии (в днях, 0 значит вечное хранение).",
|
||||||
"The node ID cannot be blank.": "ID узла не может быть пустым.",
|
"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 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.": "ID Репозитория не может быть пустым.",
|
"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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and 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.": "ID репозитория должен быть коротким id (64 символа или меньше) состоящий только из букв, цифр, точек (.), тире (-) и нижнего подчёркивания (_).",
|
"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 репозитория должен быть коротким id (64 символа или меньше) состоящий только из букв, цифр, точек (.), тире (-) и нижнего подчёркивания (_).",
|
||||||
"The repository ID must be unique.": "ID Репозитория должен быть уникальным.",
|
"The repository ID must be unique.": "ID Репозитория должен быть уникальным.",
|
||||||
"The repository path cannot be blank.": "Путь к репозиторию не может быть пустым.",
|
"The repository path cannot be blank.": "Путь к репозиторию не может быть пустым.",
|
||||||
|
"The rescan interval must be at least 5 seconds.": "Интервал пересканирования должен быть хотя бы 5 секунд.",
|
||||||
"Unknown": "Неизвестно",
|
"Unknown": "Неизвестно",
|
||||||
"Up to Date": "Обновлено",
|
"Up to Date": "Обновлено",
|
||||||
"Upgrade To {%version%}": "Обновить до {{version}}",
|
"Upgrade To {%version%}": "Обновить до {{version}}",
|
||||||
"Upgrading": "Upgrading",
|
"Upgrading": "Обновление",
|
||||||
"Upload Rate": "Скорость отдачи",
|
"Upload Rate": "Скорость отдачи",
|
||||||
"Usage": "Справка",
|
"Usage": "Справка",
|
||||||
"Use Compression": "Использовать сжатие",
|
"Use Compression": "Использовать сжатие",
|
||||||
"Use HTTPS for GUI": "Использовать HTTPS для панели управления",
|
"Use HTTPS for GUI": "Использовать HTTPS для панели управления",
|
||||||
"Version": "Версия",
|
"Version": "Версия",
|
||||||
|
"Versions Path": "Путь к версиям",
|
||||||
|
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версии удаляются автоматически, если они существуют дольше максимального срока или превышают разрешённое количество файлов за интервал.",
|
||||||
"When adding a new 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 чувствительны к регистру и должны совпадать на всех узлах.",
|
"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 чувствительны к регистру и должны совпадать на всех узлах.",
|
||||||
"Yes": "Да",
|
"Yes": "Да",
|
||||||
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
|
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
|
||||||
|
"full documentation": "full documentation",
|
||||||
"items": "элементы"
|
"items": "элементы"
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user