Compare commits

...

151 Commits

Author SHA1 Message Date
Jakob Borg
ebec4fbc24 Translation update (add Bulgarian, Lithuanian) 2014-08-22 18:18:13 +02:00
Jakob Borg
1d4105ae3d UI tweaks for staggered versioner 2014-08-22 18:16:05 +02:00
Jakob Borg
586d49f0c3 Merge pull request #541 from alex2108/master 2014-08-22 17:58:01 +02:00
Jakob Borg
5b0fab0697 Add alex2108 2014-08-22 17:57:43 +02:00
Alexander Graf
2b3359dff3 add staggered versioner 2014-08-22 00:41:17 +02:00
Jakob Borg
63203aa14c Merge pull request #548 from AudriusButkevicius/warning
Do not warn about failed IPv6 discovery, warn about no discovery
2014-08-21 18:54:33 +02:00
Audrius Butkevicius
716a8329c2 Do not warn about failed IPv6 discovery 2014-08-20 22:06:58 +01:00
Jakob Borg
dab0aec85e Latest build badge should link to latest build 2014-08-20 12:23:04 +02:00
Jakob Borg
1f1ab017c0 Show rescan interval per repo 2014-08-20 01:44:05 +02:00
Audrius Butkevicius
b6912ef95e Merge pull request #544 from marcindziadus/rescan-interval
Per repository scan intervals
2014-08-20 00:02:34 +01:00
Audrius Butkevicius
db54dca694 Do not fire UIOffline when navigating away
Fixes #487
2014-08-19 23:44:40 +01:00
Marcin
0e751b983c Enable to configure scan interval per each repository independently
Fix broken tests

Bugfix

Clean up

Refactor variable name

Adjust tests

Minor fixes

Fix typo. Remove indent.
2014-08-20 00:36:36 +02:00
Audrius Butkevicius
997b20a975 Set Content-Type before sending out headers 2014-08-19 23:30:32 +01:00
Jakob Borg
386f9c42c2 Merge pull request #545 from AudriusButkevicius/flush
Flush headers before potentially blocking
2014-08-20 00:21:49 +02:00
Audrius Butkevicius
cfae06db65 Flush headers before potentially blocking 2014-08-19 23:18:28 +01:00
Jakob Borg
44260b7b5c Add marcindziadus 2014-08-20 00:05:43 +02:00
Jakob Borg
13063b957f Use drained legacy pool in goleveldb 2014-08-19 23:49:03 +02:00
Jakob Borg
ee05e12480 Windows nodes should ignore deleted impossible files 2014-08-19 15:36:57 +02:00
Jakob Borg
5538545fb0 README links to build guide 2014-08-19 15:33:20 +02:00
Jakob Borg
bc1167c2c5 README links to build, not only artefacts 2014-08-19 15:20:53 +02:00
Jakob Borg
c57656e4c3 Do honest test coverage analysis in Jenkins 2014-08-19 12:43:50 +02:00
Jakob Borg
264400a984 Check for supported go version build.go 2014-08-19 11:04:20 +02:00
Jakob Borg
408db4eb1d rm -rf travis 2014-08-19 10:05:40 +02:00
Jakob Borg
9347f223ef Note about review of pull requests 2014-08-19 09:55:50 +02:00
Jakob Borg
518aa30c9c Don't consider empty language codes when selecting language (fixes #540) 2014-08-18 23:43:58 +02:00
Jakob Borg
6bbf1f9355 Emit Node/Repo Rejected events on unknown nodes / repos. 2014-08-18 23:34:03 +02:00
Jakob Borg
b221e4d445 build.sh is a shim 2014-08-18 22:05:26 +02:00
Jakob Borg
580fccbfca Don't build build.go on go get 2014-08-18 21:57:10 +02:00
Jakob Borg
045916efcc ARM builds in build.go 2014-08-18 21:53:08 +02:00
Jakob Borg
4f92482294 build.sh -> build.go for better cross platform support 2014-08-18 21:39:35 +02:00
Jakob Borg
2f055a75a0 Merge pull request #537 from marclaporte/patch-2
Fix some typos
2014-08-18 10:43:29 +02:00
Marc Laporte
f0621207e3 Fix some typos 2014-08-17 23:27:04 -04:00
Jakob Borg
d657bc4e3d Implement IPv6 multicast again (fixes #346) 2014-08-17 15:14:44 +02:00
Jakob Borg
a1fd07b27c beacon.Beacon -> beacon.Broadcast 2014-08-17 15:14:44 +02:00
Audrius Butkevicius
52219c5f3f Merge pull request #532 from AudriusButkevicius/config
Replace NodeConfiguration with RepositoryNodeConfiguration (Fixes #522)
2014-08-17 12:47:12 +01:00
Jakob Borg
1a66461e07 All printed warnings should have some context 2014-08-17 10:28:36 +02:00
Jakob Borg
d20df12168 Add repoPath and repoID as parameters to versioner factory (fixes #531) 2014-08-17 07:52:49 +02:00
Audrius Butkevicius
668b429615 Better error message
Closes #526
2014-08-17 00:03:41 +01:00
Audrius Butkevicius
7db528be39 Replace NodeConfiguration with RepositoryNodeConfiguration 2014-08-16 23:20:21 +01:00
Jakob Borg
60f760ee49 Translation update 2014-08-16 23:05:57 +02:00
Jakob Borg
884aaab751 Always print hostname on connect (even if something is set in config) 2014-08-16 22:55:05 +02:00
Jakob Borg
e968560ea4 Spelling 2014-08-16 22:35:15 +02:00
Jakob Borg
07caaa96e4 New translation strings 2014-08-16 22:29:21 +02:00
Audrius Butkevicius
e8a679c280 Advertise and update node names on cluster config exchange
Closes #244
2014-08-16 21:26:30 +01:00
Jakob Borg
bc885f1d08 Don't attempt to create default repo before config (fixes #530)
We'll create it anyway a little later during startup, as part of the
general "check all repos for viability" step.
2014-08-16 22:22:33 +02:00
Jakob Borg
f2f051d6de Merge pull request #529 from syncthing/windows-build
Fix tests on Windows
2014-08-16 21:37:00 +02:00
Jakob Borg
49a0bfccba Cache discovery results up to five minutes (fixes #358) 2014-08-16 21:27:00 +02:00
Audrius Butkevicius
0c1e60894f Fix tests on Windows 2014-08-16 17:33:01 +01:00
Jakob Borg
ace87ad7bb Normalize file name format in on disk db (fixes #479) 2014-08-15 12:52:16 +02:00
Jakob Borg
50f0097843 Add Rescan button to repositories 2014-08-15 12:48:36 +02:00
Jakob Borg
32a9466277 Update goleveldb 2014-08-15 09:18:38 +02:00
Jakob Borg
1ee3407946 Merge pull request #524 from marclaporte/patch-1
Fix typo
2014-08-15 08:35:25 +02:00
Marc Laporte
f1120d7aa9 Fix typo 2014-08-14 19:58:25 -04:00
Jakob Borg
2e7d6b2f99 Translation update, zh-CN 2014-08-14 17:09:29 +02:00
Jakob Borg
dfef929187 Translation update, handle locales precisely 2014-08-14 17:04:17 +02:00
Jakob Borg
e78d9ad592 Translation update (add Hungarian) 2014-08-14 14:00:33 +02:00
Jakob Borg
9f2948f595 Fix tests for UPnP options 2014-08-14 12:59:09 +02:00
Jakob Borg
198da910ed Use new StopGlobal on the discovery when external port changes 2014-08-14 12:49:41 +02:00
Jakob Borg
5f1bf9d9d6 Merge branch 'master' into pr/511
* master: (21 commits)
  Mechanism to stop external announcement routine
  Update goleveldb
  Perfstats are not supported on Windows
  Build should fail if a platform does not build
  Include perfstats and heap profiles in standard build
  Actually no, lets not do uploads at all from the build script.
  ./build.sh upload build server artifacts
  Sign checksums, not files.
  Badges, add build server
  Remove Solaris build again, for now
  Travis should build with 1.3 + tip
  Translation update
  Indicate aproximativeness of repo sizes...
  Slightly more conservative guess on file size
  Fix set tests
  Small goleveldb hack to reduce allocations somewhat
  Don't load block lists from db unless necessary
  Rip out the Suppressor (maybe to be reintroduced)
  Reduce allocations while hash scanning
  Add heap profiling support
  ...

Conflicts:
	discover/discover.go
2014-08-14 12:48:33 +02:00
Jakob Borg
798c4aef9a Mechanism to stop external announcement routine 2014-08-14 12:44:49 +02:00
Jakob Borg
f80f5b3bda Update goleveldb 2014-08-14 12:14:48 +02:00
Audrius Butkevicius
cbb07b0d67 Set default UPnP renewal to 30 minutes 2014-08-13 22:45:44 +01:00
Audrius Butkevicius
7cc9921615 Restart port sequence when UPnP renewal fails 2014-08-13 22:42:58 +01:00
Jakob Borg
7555fe065e Perfstats are not supported on Windows 2014-08-13 22:31:56 +02:00
Jakob Borg
d977f4278e Build should fail if a platform does not build 2014-08-13 22:27:16 +02:00
Audrius Butkevicius
870e3ca893 Rediscover gateway on UPnP renewal 2014-08-13 21:15:20 +01:00
Jakob Borg
213acaee3b Include perfstats and heap profiles in standard build 2014-08-13 14:39:47 +02:00
Jakob Borg
58381496a2 Actually no, lets not do uploads at all from the build script. 2014-08-13 13:11:41 +02:00
Jakob Borg
5981e42aed ./build.sh upload build server artifacts 2014-08-13 12:58:59 +02:00
Jakob Borg
3c9165d295 Sign checksums, not files. 2014-08-13 12:52:04 +02:00
Jakob Borg
60d0ef93ac Badges, add build server 2014-08-13 10:15:22 +02:00
Jakob Borg
f45d5b0066 Remove Solaris build again, for now 2014-08-13 09:42:21 +02:00
Jakob Borg
b71306480f Travis should build with 1.3 + tip 2014-08-13 09:01:17 +02:00
Jakob Borg
0c7771ccc5 Translation update 2014-08-13 00:35:37 +02:00
Audrius Butkevicius
dc9df0a79a Reannounce renewed UPnP mapping 2014-08-12 23:29:29 +01:00
Jakob Borg
17cd49fbdc Indicate aproximativeness of repo sizes... 2014-08-12 23:59:20 +02:00
Jakob Borg
ad273adb78 Slightly more conservative guess on file size 2014-08-12 16:36:24 +02:00
Jakob Borg
150e7daf2d Fix set tests 2014-08-12 16:17:32 +02:00
Jakob Borg
b004155e8f Small goleveldb hack to reduce allocations somewhat 2014-08-12 15:39:24 +02:00
Jakob Borg
92eed3b33b Don't load block lists from db unless necessary 2014-08-12 15:04:32 +02:00
Jakob Borg
fe7b77198c Rip out the Suppressor (maybe to be reintroduced) 2014-08-12 15:04:02 +02:00
Jakob Borg
f51b775698 Reduce allocations while hash scanning 2014-08-12 15:04:02 +02:00
Jakob Borg
939dd5cb31 Add heap profiling support 2014-08-12 15:04:01 +02:00
Jakob Borg
adcbe13ecd Update goleveldb 2014-08-12 09:24:36 +02:00
Audrius Butkevicius
8976e53998 Add UPnP renewal 2014-08-11 23:10:24 +01:00
Jakob Borg
97dda6a4bb Correct the memory stats in perfstats-*.csv 2014-08-11 22:10:15 +02:00
Jakob Borg
9e395eb883 Use a slightly heavier Raleway for headings (fixes #493) 2014-08-11 21:50:15 +02:00
Jakob Borg
60da59623e Limit size of sent indexes a bit, taking number of blocks into account 2014-08-11 20:54:59 +02:00
Jakob Borg
9752ea9ac3 Implement external scan request (fixes #9) 2014-08-11 20:20:01 +02:00
Jakob Borg
279693078a Update deps 2014-08-11 14:24:20 +02:00
Jakob Borg
19b93045a4 Merge pull request #508 from AudriusButkevicius/modals
Fix and refactor modals
2014-08-11 12:12:28 +02:00
Jakob Borg
5231a09820 Add ./build.sh noupgrade and all-noupgrade 2014-08-11 11:59:33 +02:00
Jakob Borg
ab952e6103 Add ./build.sh clean 2014-08-11 11:54:48 +02:00
Jakob Borg
a418771c04 Puller entrance warning 2014-08-11 07:52:03 +02:00
Audrius Butkevicius
b41590ce38 Fix and refactor modals 2014-08-10 23:28:04 +01:00
Jakob Borg
c7dde9499f Verify locking and correct update order for global 2014-08-10 07:27:24 +02:00
Jakob Borg
528cbf62ec POST to /config should return an error when something bad happens (fixes #489) 2014-08-08 14:09:27 +02:00
Jakob Borg
1be4b8bb5d Merge pull request #486 from AudriusButkevicius/windows
Add Windows upgrade support
2014-08-07 23:20:26 +02:00
Jakob Borg
c832fc9917 Merge pull request #485 from tojrobinson/world-writable-root
World writable root
2014-08-07 23:17:42 +02:00
Jakob Borg
4797a94689 Add explicit GC calls after expensive db ops (ref #468) 2014-08-07 23:09:50 +02:00
Audrius Butkevicius
6948903084 Add Windows upgrade support 2014-08-07 21:07:21 +01:00
treefingers
94164611ae Fix root being left world writable 2014-08-08 05:45:50 +10:00
treefingers
ae298e8902 Merge branch 'master' of https://github.com/syncthing/syncthing 2014-08-08 05:06:42 +10:00
Jakob Borg
3d8771ecb0 Woops, broke the build 2014-08-07 15:58:48 +02:00
Jakob Borg
28db264e90 Upgrade debugging, fix upgrade on ARM (fixes #482) 2014-08-07 15:57:20 +02:00
Jakob Borg
6af9fa4b81 Localize Close button in standard modals (fixes #481) 2014-08-07 12:35:38 +02:00
Jakob Borg
60b4d05860 Translation update, add Danish & Dutch 2014-08-07 10:49:29 +02:00
Jakob Borg
7b93839ed1 Woops, broke the build 2014-08-07 10:26:26 +02:00
Jakob Borg
fdb11d7c06 Correctly handle file updates in read only directories (fixes #470) 2014-08-07 08:31:22 +02:00
Jakob Borg
5651847877 Merge commit 'bc2bb22'
* commit 'bc2bb22':
  Add no-browser flag
2014-08-07 07:20:39 +02:00
Jakob Borg
e1442290b6 Add tojrobinson 2014-08-07 07:20:21 +02:00
Tully Robinson
c45b18cc75 Merge branch 'master' into browser-flag 2014-08-06 23:01:35 +10:00
Jakob Borg
bb2ad77987 Never remove currently valid languages when updating translations 2014-08-06 14:56:32 +02:00
Jakob Borg
68b1ffec19 Fix translation in upgrading/restarting dialogs 2014-08-06 14:41:46 +02:00
Tully Robinson
bc2bb22673 Add no-browser flag 2014-08-06 22:30:18 +10:00
Jakob Borg
83d707fc4b Add Transifex info to contribution guidelines 2014-08-06 11:03:39 +02:00
Jakob Borg
175b32e56c Forgot the favicon 2014-08-06 09:12:11 +02:00
Jakob Borg
97b4a6553b Logo update 2014-08-06 09:07:13 +02:00
Jakob Borg
4ade30e681 Merge branch 'pr/477'
* pr/477:
  Logo changed
2014-08-05 23:21:30 +02:00
Gilli Sigurdsson
4e03b4f191 Logo changed 2014-08-05 23:20:33 +02:00
Jakob Borg
bfe1d1d4ca Add Gilli 2014-08-05 23:19:11 +02:00
Jakob Borg
8918de85fd Correct memory usage in anonymous report 2014-08-05 23:13:55 +02:00
Jakob Borg
5e237aecae Reflect memory returned to OS in RAM Utilization 2014-08-05 22:14:11 +02:00
Jakob Borg
13291ad481 Tweak contribution guide 2014-08-05 20:54:53 +02:00
Jakob Borg
a47ee86bee Don't show 100 warnings for unknown repo at connect when once is enough 2014-08-05 20:26:05 +02:00
Jakob Borg
62d703f967 Show 100% complete status for nodes without any files to sync (fixes #453) 2014-08-05 20:16:25 +02:00
Jakob Borg
b2c196e5c7 Don't overwrite Node ID field with 'corrected' format 2014-08-05 19:47:29 +02:00
Jakob Borg
4be6a54bc0 Hide build version behind plus character (fixes #473) 2014-08-05 19:38:31 +02:00
Jakob Borg
8ce8476547 Exclude integration tests from normal go test 2014-08-05 15:50:05 +02:00
Jakob Borg
d82caf6bd4 Don't depend on a pretty printer just for testing 2014-08-05 15:43:29 +02:00
Jakob Borg
8ea1e302c3 Also expose ItemStarted events 2014-08-05 13:14:04 +02:00
Jakob Borg
a8799efa94 Don't reuse existing indexes, yet (fixes #463) 2014-08-05 12:20:50 +02:00
Jakob Borg
0cfac4e021 Start rewriting integration tests in Go instead of bash 2014-08-05 12:20:07 +02:00
Jakob Borg
f6c9642d72 Pull files in random-ish order again 2014-08-05 09:46:21 +02:00
Jakob Borg
5a07f9ddee Woops: don't consider all close()s to be failures... 2014-08-05 09:44:35 +02:00
Jakob Borg
9db75e91ac HTTP testing corrections 2014-08-05 09:38:38 +02:00
Jakob Borg
f288e00c37 Actually show Node ID in QR (fixes #471) 2014-08-04 22:53:37 +02:00
Jakob Borg
c9edd31993 Show pull errors, stop repo when not making progress (fixes #302) 2014-08-04 22:46:35 +02:00
Jakob Borg
5a7780ab5f Use Raleway font for headings 2014-08-04 22:46:29 +02:00
Jakob Borg
ac0fba99ad "52 or 56 characters" (fixes #466) 2014-08-04 22:11:44 +02:00
Jakob Borg
6f724a113c Use repo ID rather than path in header (fixes #425) 2014-08-03 21:58:36 +02:00
Jakob Borg
327cd4cb87 Fix statistics report preview (fixes #460) 2014-08-03 21:47:02 +02:00
Jakob Borg
25de3a2590 Also build for freebsd-386 (fixes #458) 2014-08-03 10:42:39 +02:00
Jakob Borg
06208a703a Implement -generate (fixes #459) 2014-08-03 09:41:08 +02:00
Jakob Borg
56afba6606 Only change the announce server when upgrading config version 2014-08-02 08:37:10 +02:00
Jakob Borg
d65bbf2113 Allow GET requests without CSRF 2014-08-02 08:19:10 +02:00
Jakob Borg
b8bfc9b732 Coveralls syncthing/syncthing 2014-08-02 08:18:55 +02:00
Jakob Borg
cec3bad373 Move calmh/syncthing -> syncthing/syncthing 2014-08-01 16:48:46 +02:00
Jakob Borg
9312e3c7de Config version 3: default to compression=true on nodes 2014-08-01 16:48:46 +02:00
Jakob Borg
43e7435c41 Call the darwin releases macosx instead 2014-08-01 16:30:28 +02:00
Jakob Borg
f34f5e41a4 Don't always run the tedious protocol tests 2014-08-01 16:30:13 +02:00
151 changed files with 7430 additions and 3434 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ coverage.out
files/pidx
bin
perfstats*.csv
coverage.xml

View File

@@ -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=calmh/syncthing -repotoken="$COVERALS_TOKEN"
env:
global:
secure: "zEV2h2XtKHNLVdXJjM4LA/VjMfLVydm6goF+ARit+nOSGxGoH7f7jIdzJzhxgh7shKG93q61eLO1Tug+WBMYB2EpBuYnTB5AIMYhCDwNI8C4uBV6c3brHfcrie7MASNao8TID2QScASKNFFWvjv/i1Ccn5ztxdcQuhSsNjGZp8A="

View File

@@ -1,7 +1,43 @@
## Reporting Bugs
Please file bugs in the [Github Issue
Tracker](https://github.com/syncthing/syncthing/issues). Include at
least the following:
- What happened
- What did you expect to happen instead of what *did* happen, if it's
not crazy obvious
- What operating system, operating system version and version of
Syncthing you are running
- The same for other connected nodes, where relevant
- Screenshot if the issue concerns something visible in the GUI
- Console log entries, where possible and relevant
If you're not sure whether something is relevant, erring on the side of
too much information will never get you yelled at. :)
## Contributing Translations
All translations are done via
[Transifex](https://www.transifex.com/projects/p/syncthing/). If you
wish to contribute to a translation, just head over there and sign up.
Before every release, the language resources are updated from the
latest info on Transifex.
## Contributing Code
Please do contribute! If you want to contribute but are unsure where to
start, the [Contributions Needed
topic](http://discourse.syncthing.net/t/contributions-needed/49)
lists areas in need of attention.
topic](http://discourse.syncthing.net/t/49) lists areas in need of
attention. In general, any open issues are fair game! Be prepared for a
[certain amount of
review](https://discourse.syncthing.net/t/733); it's all in the name of
quality. :)
## Licensing
@@ -16,8 +52,8 @@ to add yourself as a separate commit in your first pull request.
## Building
[See the
documentation](http://discourse.syncthing.net/t/building-syncthing/44)
[See the documentation](http://discourse.syncthing.net/t/44) on how to
get started with a build environment.
## Branches
@@ -44,7 +80,9 @@ Yes please!
## Style
`go fmt`
- `go fmt`
- Unix line breaks
## Documentation
@@ -53,4 +91,3 @@ Yes please!
## License
MIT

View File

@@ -1,11 +1,15 @@
Aaron Bieber <qbit@deftly.net>
Alexander Graf <register-github@alex-graf.de>
Andrew Dunham <andrew@du.nham.ca>
Audrius Butkevicius <audrius.butkevicius@gmail.com>
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com>
Ben Sidhom <bsidhom@gmail.com>
Brandon Philips <brandon@ifup.org>
Gilli Sigurdsson <gilli@vx.is>
James Patterson <jamespatterson@operamail.com>
Jens Diemer <github.com@jensdiemer.de>
Marcin Dziadus <dziadus.marcin@gmail.com>
Philippe Schommers <philippe@schommers.be>
Ryan Sullivan <kayoticsully@gmail.com>
Tully Robinson <tully@tojr.org>
Veeti Paananen <veeti.paananen@rojekti.fi>

24
Godeps/Godeps.json generated
View File

@@ -1,6 +1,6 @@
{
"ImportPath": "github.com/calmh/syncthing",
"GoVersion": "go1.3",
"ImportPath": "github.com/syncthing/syncthing",
"GoVersion": "go1.3.1",
"Packages": [
"./cmd/..."
],
@@ -12,23 +12,23 @@
},
{
"ImportPath": "code.google.com/p/go.crypto/bcrypt",
"Comment": "null-213",
"Rev": "aa2644fe4aa50e3b38d75187b4799b1f0c9ddcef"
"Comment": "null-216",
"Rev": "41cd4647fccc72b0b79ef1bd1fe6735e718257cd"
},
{
"ImportPath": "code.google.com/p/go.crypto/blowfish",
"Comment": "null-213",
"Rev": "aa2644fe4aa50e3b38d75187b4799b1f0c9ddcef"
"Comment": "null-216",
"Rev": "41cd4647fccc72b0b79ef1bd1fe6735e718257cd"
},
{
"ImportPath": "code.google.com/p/go.text/transform",
"Comment": "null-88",
"Rev": "1506dcc33592c369c3be7bd30b38f90445b86deb"
"Comment": "null-90",
"Rev": "d65bffbc88a153d23a6d2a864531e6e7c2cde59b"
},
{
"ImportPath": "code.google.com/p/go.text/unicode/norm",
"Comment": "null-88",
"Rev": "1506dcc33592c369c3be7bd30b38f90445b86deb"
"Comment": "null-90",
"Rev": "d65bffbc88a153d23a6d2a864531e6e7c2cde59b"
},
{
"ImportPath": "code.google.com/p/snappy-go/snappy",
@@ -41,7 +41,7 @@
},
{
"ImportPath": "github.com/calmh/xdr",
"Rev": "89d756f35ba26bcdd47ca25cf9795b3b8d1e1852"
"Rev": "e1714bbe4764b15490fcc8ebd25d4bd9ea50a4b9"
},
{
"ImportPath": "github.com/juju/ratelimit",
@@ -49,7 +49,7 @@
},
{
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
"Rev": "c5955912e3287376475731c5bc59c79a5a799105"
"Rev": "17fd8940e0f778c27793a25bff8c48ddd7bf53ac"
},
{
"ImportPath": "github.com/vitrun/qart/coding",

View File

@@ -4,6 +4,22 @@
package blowfish
// getNextWord returns the next big-endian uint32 value from the byte slice
// at the given position in a circular manner, updating the position.
func getNextWord(b []byte, pos *int) uint32 {
var w uint32
j := *pos
for i := 0; i < 4; i++ {
w = w<<8 | uint32(b[j])
j++
if j >= len(b) {
j = 0
}
}
*pos = j
return w
}
// ExpandKey performs a key expansion on the given *Cipher. Specifically, it
// performs the Blowfish algorithm's key schedule which sets up the *Cipher's
// pi and substitution tables for calls to Encrypt. This is used, primarily,
@@ -12,6 +28,7 @@ package blowfish
func ExpandKey(key []byte, c *Cipher) {
j := 0
for i := 0; i < 18; i++ {
// Using inlined getNextWord for performance.
var d uint32
for k := 0; k < 4; k++ {
d = d<<8 | uint32(key[j])
@@ -54,86 +71,44 @@ func ExpandKey(key []byte, c *Cipher) {
func expandKeyWithSalt(key []byte, salt []byte, c *Cipher) {
j := 0
for i := 0; i < 18; i++ {
var d uint32
for k := 0; k < 4; k++ {
d = d<<8 | uint32(key[j])
j++
if j >= len(key) {
j = 0
}
}
c.p[i] ^= d
c.p[i] ^= getNextWord(key, &j)
}
j = 0
var expandedSalt [4]uint32
for i := range expandedSalt {
var d uint32
for k := 0; k < 4; k++ {
d = d<<8 | uint32(salt[j])
j++
if j >= len(salt) {
j = 0
}
}
expandedSalt[i] = d
}
var l, r uint32
for i := 0; i < 18; i += 2 {
l ^= expandedSalt[i&2]
r ^= expandedSalt[(i&2)+1]
l ^= getNextWord(salt, &j)
r ^= getNextWord(salt, &j)
l, r = encryptBlock(l, r, c)
c.p[i], c.p[i+1] = l, r
}
for i := 0; i < 256; i += 4 {
l ^= expandedSalt[2]
r ^= expandedSalt[3]
for i := 0; i < 256; i += 2 {
l ^= getNextWord(salt, &j)
r ^= getNextWord(salt, &j)
l, r = encryptBlock(l, r, c)
c.s0[i], c.s0[i+1] = l, r
l ^= expandedSalt[0]
r ^= expandedSalt[1]
l, r = encryptBlock(l, r, c)
c.s0[i+2], c.s0[i+3] = l, r
}
for i := 0; i < 256; i += 4 {
l ^= expandedSalt[2]
r ^= expandedSalt[3]
for i := 0; i < 256; i += 2 {
l ^= getNextWord(salt, &j)
r ^= getNextWord(salt, &j)
l, r = encryptBlock(l, r, c)
c.s1[i], c.s1[i+1] = l, r
l ^= expandedSalt[0]
r ^= expandedSalt[1]
l, r = encryptBlock(l, r, c)
c.s1[i+2], c.s1[i+3] = l, r
}
for i := 0; i < 256; i += 4 {
l ^= expandedSalt[2]
r ^= expandedSalt[3]
for i := 0; i < 256; i += 2 {
l ^= getNextWord(salt, &j)
r ^= getNextWord(salt, &j)
l, r = encryptBlock(l, r, c)
c.s2[i], c.s2[i+1] = l, r
l ^= expandedSalt[0]
r ^= expandedSalt[1]
l, r = encryptBlock(l, r, c)
c.s2[i+2], c.s2[i+3] = l, r
}
for i := 0; i < 256; i += 4 {
l ^= expandedSalt[2]
r ^= expandedSalt[3]
for i := 0; i < 256; i += 2 {
l ^= getNextWord(salt, &j)
r ^= getNextWord(salt, &j)
l, r = encryptBlock(l, r, c)
c.s3[i], c.s3[i+1] = l, r
l ^= expandedSalt[0]
r ^= expandedSalt[1]
l, r = encryptBlock(l, r, c)
c.s3[i+2], c.s3[i+3] = l, r
}
}
@@ -182,9 +157,3 @@ func decryptBlock(l, r uint32, c *Cipher) (uint32, uint32) {
xr ^= c.p[0]
return xr, xl
}
func zero(x []uint32) {
for i := range x {
x[i] = 0
}
}

View File

@@ -4,9 +4,7 @@
package blowfish
import (
"testing"
)
import "testing"
type CryptTest struct {
key []byte
@@ -202,3 +200,75 @@ func TestSaltedCipherKeyLength(t *testing.T) {
t.Errorf("NewSaltedCipher with long key, gave error %#v", err)
}
}
// Test vectors generated with Blowfish from OpenSSH.
var saltedVectors = [][8]byte{
{0x0c, 0x82, 0x3b, 0x7b, 0x8d, 0x01, 0x4b, 0x7e},
{0xd1, 0xe1, 0x93, 0xf0, 0x70, 0xa6, 0xdb, 0x12},
{0xfc, 0x5e, 0xba, 0xde, 0xcb, 0xf8, 0x59, 0xad},
{0x8a, 0x0c, 0x76, 0xe7, 0xdd, 0x2c, 0xd3, 0xa8},
{0x2c, 0xcb, 0x7b, 0xee, 0xac, 0x7b, 0x7f, 0xf8},
{0xbb, 0xf6, 0x30, 0x6f, 0xe1, 0x5d, 0x62, 0xbf},
{0x97, 0x1e, 0xc1, 0x3d, 0x3d, 0xe0, 0x11, 0xe9},
{0x06, 0xd7, 0x4d, 0xb1, 0x80, 0xa3, 0xb1, 0x38},
{0x67, 0xa1, 0xa9, 0x75, 0x0e, 0x5b, 0xc6, 0xb4},
{0x51, 0x0f, 0x33, 0x0e, 0x4f, 0x67, 0xd2, 0x0c},
{0xf1, 0x73, 0x7e, 0xd8, 0x44, 0xea, 0xdb, 0xe5},
{0x14, 0x0e, 0x16, 0xce, 0x7f, 0x4a, 0x9c, 0x7b},
{0x4b, 0xfe, 0x43, 0xfd, 0xbf, 0x36, 0x04, 0x47},
{0xb1, 0xeb, 0x3e, 0x15, 0x36, 0xa7, 0xbb, 0xe2},
{0x6d, 0x0b, 0x41, 0xdd, 0x00, 0x98, 0x0b, 0x19},
{0xd3, 0xce, 0x45, 0xce, 0x1d, 0x56, 0xb7, 0xfc},
{0xd9, 0xf0, 0xfd, 0xda, 0xc0, 0x23, 0xb7, 0x93},
{0x4c, 0x6f, 0xa1, 0xe4, 0x0c, 0xa8, 0xca, 0x57},
{0xe6, 0x2f, 0x28, 0xa7, 0x0c, 0x94, 0x0d, 0x08},
{0x8f, 0xe3, 0xf0, 0xb6, 0x29, 0xe3, 0x44, 0x03},
{0xff, 0x98, 0xdd, 0x04, 0x45, 0xb4, 0x6d, 0x1f},
{0x9e, 0x45, 0x4d, 0x18, 0x40, 0x53, 0xdb, 0xef},
{0xb7, 0x3b, 0xef, 0x29, 0xbe, 0xa8, 0x13, 0x71},
{0x02, 0x54, 0x55, 0x41, 0x8e, 0x04, 0xfc, 0xad},
{0x6a, 0x0a, 0xee, 0x7c, 0x10, 0xd9, 0x19, 0xfe},
{0x0a, 0x22, 0xd9, 0x41, 0xcc, 0x23, 0x87, 0x13},
{0x6e, 0xff, 0x1f, 0xff, 0x36, 0x17, 0x9c, 0xbe},
{0x79, 0xad, 0xb7, 0x40, 0xf4, 0x9f, 0x51, 0xa6},
{0x97, 0x81, 0x99, 0xa4, 0xde, 0x9e, 0x9f, 0xb6},
{0x12, 0x19, 0x7a, 0x28, 0xd0, 0xdc, 0xcc, 0x92},
{0x81, 0xda, 0x60, 0x1e, 0x0e, 0xdd, 0x65, 0x56},
{0x7d, 0x76, 0x20, 0xb2, 0x73, 0xc9, 0x9e, 0xee},
}
func TestSaltedCipher(t *testing.T) {
var key, salt [32]byte
for i := range key {
key[i] = byte(i)
salt[i] = byte(i + 32)
}
for i, v := range saltedVectors {
c, err := NewSaltedCipher(key[:], salt[:i])
if err != nil {
t.Fatal(err)
}
var buf [8]byte
c.Encrypt(buf[:], buf[:])
if v != buf {
t.Errorf("%d: expected %x, got %x", i, v, buf)
}
}
}
func BenchmarkExpandKeyWithSalt(b *testing.B) {
key := make([]byte, 32)
salt := make([]byte, 16)
c, _ := NewCipher(key)
for i := 0; i < b.N; i++ {
expandKeyWithSalt(key, salt, c)
}
}
func BenchmarkExpandKey(b *testing.B) {
key := make([]byte, 32)
c, _ := NewCipher(key)
for i := 0; i < b.N; i++ {
ExpandKey(key, c)
}
}

View File

@@ -40,8 +40,11 @@ func NewCipher(key []byte) (*Cipher, error) {
// NewSaltedCipher creates a returns a Cipher that folds a salt into its key
// schedule. For most purposes, NewCipher, instead of NewSaltedCipher, is
// sufficient and desirable. For bcrypt compatiblity, the key can be over 56
// bytes. Only the first 16 bytes of salt are used.
// bytes.
func NewSaltedCipher(key, salt []byte) (*Cipher, error) {
if len(salt) == 0 {
return NewCipher(key)
}
var result Cipher
if k := len(key); k < 1 {
return nil, KeySizeError(k)

View File

@@ -11,7 +11,6 @@
package main
import (
"bufio"
"bytes"
"flag"
"fmt"
@@ -24,6 +23,8 @@ import (
"strconv"
"strings"
"unicode"
"code.google.com/p/go.text/internal/ucd"
)
func main() {
@@ -63,31 +64,7 @@ var localFiles = flag.Bool("local",
var logger = log.New(os.Stderr, "", log.Lshortfile)
// UnicodeData.txt has form:
// 0037;DIGIT SEVEN;Nd;0;EN;;7;7;7;N;;;;;
// 007A;LATIN SMALL LETTER Z;Ll;0;L;;;;;N;;;005A;;005A
// See http://unicode.org/reports/tr44/ for full explanation
// The fields:
const (
FCodePoint = iota
FName
FGeneralCategory
FCanonicalCombiningClass
FBidiClass
FDecompMapping
FDecimalValue
FDigitValue
FNumericValue
FBidiMirrored
FUnicode1Name
FISOComment
FSimpleUppercaseMapping
FSimpleLowercaseMapping
FSimpleTitlecaseMapping
NumField
MaxChar = 0x10FFFF // anything above this shouldn't exist
)
const MaxChar = 0x10FFFF // anything above this shouldn't exist
// Quick Check properties of runes allow us to quickly
// determine whether a rune may occur in a normal form.
@@ -232,7 +209,7 @@ func openReader(file string) (input io.ReadCloser) {
return
}
func parseDecomposition(s string, skipfirst bool) (a []rune, e error) {
func parseDecomposition(s string, skipfirst bool) (a []rune, err error) {
decomp := strings.Split(s, " ")
if len(decomp) > 0 && skipfirst {
decomp = decomp[1:]
@@ -247,56 +224,31 @@ func parseDecomposition(s string, skipfirst bool) (a []rune, e error) {
return a, nil
}
func parseCharacter(line string) {
field := strings.Split(line, ";")
if len(field) != NumField {
logger.Fatalf("%5s: %d fields (expected %d)\n", line, len(field), NumField)
}
x, err := strconv.ParseUint(field[FCodePoint], 16, 64)
point := int(x)
if err != nil {
logger.Fatalf("%.5s...: %s", line, err)
}
if point == 0 {
return // not interesting and we use 0 as unset
}
if point > MaxChar {
logger.Fatalf("%5s: Rune %X > MaxChar (%X)", line, point, MaxChar)
return
}
state := SNormal
switch {
case strings.Index(field[FName], ", First>") > 0:
state = SFirst
case strings.Index(field[FName], ", Last>") > 0:
state = SLast
}
firstChar := lastChar + 1
lastChar = rune(point)
if state != SLast {
firstChar = lastChar
}
x, err = strconv.ParseUint(field[FCanonicalCombiningClass], 10, 64)
if err != nil {
logger.Fatalf("%U: bad ccc field: %s", int(x), err)
}
ccc := uint8(x)
decmap := field[FDecompMapping]
exp, e := parseDecomposition(decmap, false)
isCompat := false
if e != nil {
if len(decmap) > 0 {
exp, e = parseDecomposition(decmap, true)
if e != nil {
logger.Fatalf(`%U: bad decomp |%v|: "%s"`, int(x), decmap, e)
func loadUnicodeData() {
f := openReader("UnicodeData.txt")
defer f.Close()
p := ucd.New(f)
for p.Next() {
r := p.Rune(ucd.CodePoint)
char := &chars[r]
char.ccc = uint8(p.Uint(ucd.CanonicalCombiningClass))
decmap := p.String(ucd.DecompMapping)
exp, err := parseDecomposition(decmap, false)
isCompat := false
if err != nil {
if len(decmap) > 0 {
exp, err = parseDecomposition(decmap, true)
if err != nil {
logger.Fatalf(`%U: bad decomp |%v|: "%s"`, r, decmap, err)
}
isCompat = true
}
isCompat = true
}
}
for i := firstChar; i <= lastChar; i++ {
char := &chars[i]
char.name = field[FName]
char.codePoint = i
char.name = p.String(ucd.Name)
char.codePoint = r
char.forms[FCompatibility].decomp = exp
if !isCompat {
char.forms[FCanonical].decomp = exp
@@ -306,24 +258,9 @@ func parseCharacter(line string) {
if len(decmap) > 0 {
char.forms[FCompatibility].decomp = exp
}
char.ccc = ccc
char.state = SMissing
if i == lastChar {
char.state = state
}
}
return
}
func loadUnicodeData() {
f := openReader("UnicodeData.txt")
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
parseCharacter(scanner.Text())
}
if scanner.Err() != nil {
logger.Fatal(scanner.Err())
if err := p.Err(); err != nil {
logger.Fatal(err)
}
}
@@ -354,47 +291,22 @@ func compactCCC() {
}
}
var singlePointRe = regexp.MustCompile(`^([0-9A-F]+) *$`)
// CompositionExclusions.txt has form:
// 0958 # ...
// See http://unicode.org/reports/tr44/ for full explanation
func parseExclusion(line string) int {
comment := strings.Index(line, "#")
if comment >= 0 {
line = line[0:comment]
}
if len(line) == 0 {
return 0
}
matches := singlePointRe.FindStringSubmatch(line)
if len(matches) != 2 {
logger.Fatalf("%s: %d matches (expected 1)\n", line, len(matches))
}
point, err := strconv.ParseUint(matches[1], 16, 64)
if err != nil {
logger.Fatalf("%.5s...: %s", line, err)
}
return int(point)
}
func loadCompositionExclusions() {
f := openReader("CompositionExclusions.txt")
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
point := parseExclusion(scanner.Text())
if point == 0 {
continue
}
c := &chars[point]
p := ucd.New(f)
for p.Next() {
c := &chars[p.Rune(0)]
if c.excludeInComp {
logger.Fatalf("%U: Duplicate entry in exclusions.", c.codePoint)
}
c.excludeInComp = true
}
if scanner.Err() != nil {
log.Fatal(scanner.Err())
if e := p.Err(); e != nil {
logger.Fatal(e)
}
}
@@ -988,8 +900,6 @@ func verifyComputed() {
}
}
var qcRe = regexp.MustCompile(`([0-9A-F\.]+) *; (NF.*_QC); ([YNM]) #.*`)
// Use values in DerivedNormalizationProps.txt to compare against the
// values we computed.
// DerivedNormalizationProps.txt has form:
@@ -999,27 +909,13 @@ var qcRe = regexp.MustCompile(`([0-9A-F\.]+) *; (NF.*_QC); ([YNM]) #.*`)
func testDerived() {
f := openReader("DerivedNormalizationProps.txt")
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
qc := qcRe.FindStringSubmatch(line)
if qc == nil {
continue
}
rng := strings.Split(qc[1], "..")
i, err := strconv.ParseUint(rng[0], 16, 64)
if err != nil {
log.Fatal(err)
}
j := i
if len(rng) > 1 {
j, err = strconv.ParseUint(rng[1], 16, 64)
if err != nil {
log.Fatal(err)
}
}
p := ucd.New(f)
for p.Next() {
r := p.Rune(0)
c := &chars[r]
var ftype, mode int
qt := strings.TrimSpace(qc[2])
qt := p.String(1)
switch qt {
case "NFC_QC":
ftype, mode = FCanonical, MComposed
@@ -1030,10 +926,10 @@ func testDerived() {
case "NFKD_QC":
ftype, mode = FCompatibility, MDecomposed
default:
log.Fatalf(`Unexpected quick check type "%s"`, qt)
continue
}
var qr QCResult
switch qc[3] {
switch p.String(2) {
case "Y":
qr = QCYes
case "N":
@@ -1041,27 +937,15 @@ func testDerived() {
case "M":
qr = QCMaybe
default:
log.Fatalf(`Unexpected quick check value "%s"`, qc[3])
log.Fatalf(`Unexpected quick check value "%s"`, p.String(2))
}
var lastFailed bool
// Verify current
for ; i <= j; i++ {
c := &chars[int(i)]
c.forms[ftype].verified[mode] = true
curqr := c.forms[ftype].quickCheck[mode]
if curqr != qr {
if !lastFailed {
logger.Printf("%s: %.4X..%.4X -- %s\n",
qt, int(i), int(j), line[0:50])
}
logger.Printf("%U: FAILED %s (was %v need %v)\n",
int(i), qt, curqr, qr)
lastFailed = true
}
if got := c.forms[ftype].quickCheck[mode]; got != qr {
logger.Printf("%U: FAILED %s (was %v need %v)\n", r, qt, got, qr)
}
c.forms[ftype].verified[mode] = true
}
if scanner.Err() != nil {
logger.Fatal(scanner.Err())
if err := p.Err(); err != nil {
logger.Fatal(err)
}
// Any unspecified value must be QCYes. Verify this.
for i, c := range chars {

View File

@@ -0,0 +1 @@
coverage.out

19
Godeps/_workspace/src/github.com/calmh/xdr/.travis.yml generated vendored Normal file
View File

@@ -0,0 +1,19 @@
language: go
go:
- tip
install:
- export PATH=$PATH:$HOME/gopath/bin
- go get code.google.com/p/go.tools/cmd/cover
- go get github.com/mattn/goveralls
script:
- ./generate.sh
- go test -coverprofile=coverage.out
after_success:
- goveralls -coverprofile=coverage.out -service=travis-ci -package=calmh/xdr -repotoken="$COVERALLS_TOKEN"
env:
global:
secure: SmgnrGfp2zLrA44ChRMpjPeujubt9veZ8Fx/OseMWECmacyV5N/TuDhzIbwo6QwV4xB0sBacoPzvxQbJRVjNKsPiSu72UbcQmQ7flN4Tf7nW09tSh1iW8NgrpBCq/3UYLoBu2iPBEBKm93IK0aGNAKs6oEkB0fU27iTVBwiTXOY=

19
Godeps/_workspace/src/github.com/calmh/xdr/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (C) 2014 Jakob Borg.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
- The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

12
Godeps/_workspace/src/github.com/calmh/xdr/README.md generated vendored Normal file
View File

@@ -0,0 +1,12 @@
xdr
===
[![Build Status](https://img.shields.io/travis/calmh/xdr.svg?style=flat)](https://travis-ci.org/calmh/xdr)
[![Coverage Status](https://img.shields.io/coveralls/calmh/xdr.svg?style=flat)](https://coveralls.io/r/calmh/xdr?branch=master)
[![API Documentation](http://img.shields.io/badge/api-Godoc-blue.svg?style=flat)](http://godoc.org/github.com/calmh/xdr)
[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://opensource.org/licenses/MIT)
This is an XDR encoding/decoding library. It uses code generation and
not reflection. It supports the IPDR bastardized XDR format when built
with `-tags ipdr`.

View File

@@ -7,6 +7,8 @@ import (
"io"
"io/ioutil"
"testing"
"github.com/calmh/xdr"
)
type XDRBenchStruct struct {
@@ -58,6 +60,16 @@ func BenchmarkThisEncode(b *testing.B) {
}
}
func BenchmarkThisEncoder(b *testing.B) {
w := xdr.NewWriter(ioutil.Discard)
for i := 0; i < b.N; i++ {
_, err := s.encodeXDR(w)
if err != nil {
b.Fatal(err)
}
}
}
type repeatReader struct {
data []byte
}
@@ -86,3 +98,16 @@ func BenchmarkThisDecode(b *testing.B) {
rr.Reset(e)
}
}
func BenchmarkThisDecoder(b *testing.B) {
rr := &repeatReader{e}
r := xdr.NewReader(rr)
var t XDRBenchStruct
for i := 0; i < b.N; i++ {
err := t.decodeXDR(r)
if err != nil {
b.Fatal(err)
}
rr.Reset(e)
}
}

View File

@@ -5,8 +5,12 @@ package xdr_test
import (
"bytes"
"math/rand"
"reflect"
"testing"
"testing/quick"
"github.com/calmh/xdr"
)
// Contains all supported types
@@ -22,6 +26,25 @@ type TestStruct struct {
UI64 uint64
BS []byte
S string
C Opaque
}
type Opaque [32]byte
func (u *Opaque) encodeXDR(w *xdr.Writer) (int, error) {
return w.WriteRaw(u[:])
}
func (u *Opaque) decodeXDR(r *xdr.Reader) (int, error) {
return r.ReadRaw(u[:])
}
func (Opaque) Generate(rand *rand.Rand, size int) reflect.Value {
var u Opaque
for i := range u[:] {
u[i] = byte(rand.Int())
}
return reflect.ValueOf(u)
}
func TestEncDec(t *testing.T) {
@@ -39,7 +62,7 @@ func TestEncDec(t *testing.T) {
t0.I32 != t1.I32 || t0.UI32 != t1.UI32 ||
t0.I64 != t1.I64 || t0.UI64 != t1.UI64 ||
bytes.Compare(t0.BS, t1.BS) != 0 ||
t0.S != t1.S {
t0.S != t1.S || t0.C != t1.C {
t.Logf("%#v", t0)
t.Logf("%#v", t1)
return false

View File

@@ -52,6 +52,8 @@ TestStruct Structure:
\ S (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Opaque |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct TestStruct {
@@ -66,6 +68,7 @@ struct TestStruct {
unsigned hyper UI64;
opaque BS<>;
string S<>;
Opaque C;
}
*/
@@ -98,6 +101,10 @@ func (o TestStruct) encodeXDR(xw *xdr.Writer) (int, error) {
xw.WriteUint64(o.UI64)
xw.WriteBytes(o.BS)
xw.WriteString(o.S)
_, err := o.C.encodeXDR(xw)
if err != nil {
return xw.Tot(), err
}
return xw.Tot(), xw.Error()
}
@@ -124,5 +131,6 @@ func (o *TestStruct) decodeXDR(xr *xdr.Reader) error {
o.UI64 = xr.ReadUint64()
o.BS = xr.ReadBytes()
o.S = xr.ReadString()
(&o.C).decodeXDR(xr)
return xr.Error()
}

View File

@@ -7,6 +7,8 @@ package xdr
import (
"errors"
"io"
"reflect"
"unsafe"
)
var ErrElementSizeExceeded = errors.New("element size exceeded")
@@ -15,7 +17,6 @@ type Reader struct {
r io.Reader
err error
b [8]byte
sb []byte
}
func NewReader(r io.Reader) *Reader {
@@ -35,23 +36,17 @@ func (r *Reader) ReadRaw(bs []byte) (int, error) {
}
func (r *Reader) ReadString() string {
if r.sb == nil {
r.sb = make([]byte, 64)
} else {
r.sb = r.sb[:cap(r.sb)]
}
r.sb = r.ReadBytesInto(r.sb)
return string(r.sb)
return r.ReadStringMax(0)
}
func (r *Reader) ReadStringMax(max int) string {
if r.sb == nil {
r.sb = make([]byte, 64)
} else {
r.sb = r.sb[:cap(r.sb)]
buf := r.ReadBytesMaxInto(max, nil)
bh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
sh := reflect.StringHeader{
Data: bh.Data,
Len: bh.Len,
}
r.sb = r.ReadBytesMaxInto(max, r.sb)
return string(r.sb)
return *((*string)(unsafe.Pointer(&sh)))
}
func (r *Reader) ReadBytes() []byte {
@@ -80,10 +75,10 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
return nil
}
if l+pad(l) > len(dst) {
dst = make([]byte, l+pad(l))
if fullLen := l + pad(l); fullLen > len(dst) {
dst = make([]byte, fullLen)
} else {
dst = dst[:l+pad(l)]
dst = dst[:fullLen]
}
var n int

View File

@@ -3,7 +3,11 @@
package xdr
import "io"
import (
"io"
"reflect"
"unsafe"
)
var padBytes = []byte{0, 0, 0}
@@ -38,7 +42,13 @@ func (w *Writer) WriteRaw(bs []byte) (int, error) {
}
func (w *Writer) WriteString(s string) (int, error) {
return w.WriteBytes([]byte(s))
sh := *((*reflect.StringHeader)(unsafe.Pointer(&s)))
bh := reflect.SliceHeader{
Data: sh.Data,
Len: sh.Len,
Cap: sh.Len,
}
return w.WriteBytes(*(*[]byte)(unsafe.Pointer(&bh)))
}
func (w *Writer) WriteBytes(bs []byte) (int, error) {

View File

@@ -67,7 +67,7 @@ func TestReadBytesMaxInto(t *testing.T) {
}
}
func TestReadBytesMaxIntoNil(t *testing.T) {
func TestReadStringMax(t *testing.T) {
for tot := 42; tot < 72; tot++ {
for max := 0; max < 128; max++ {
var b = new(bytes.Buffer)
@@ -77,8 +77,8 @@ func TestReadBytesMaxIntoNil(t *testing.T) {
var toWrite = make([]byte, tot)
w.WriteBytes(toWrite)
var bs = r.ReadBytesMaxInto(max, nil)
var read = len(bs)
var str = r.ReadStringMax(max)
var read = len(str)
if max == 0 || tot <= max {
if read != tot {

View File

@@ -170,7 +170,7 @@ func (p *dbBench) writes(perBatch int) {
b.SetBytes(116)
}
func (p *dbBench) drop() {
func (p *dbBench) gc() {
p.keys, p.values = nil, nil
runtime.GC()
}
@@ -249,6 +249,7 @@ func (p *dbBench) newIter() iterator.Iterator {
}
func (p *dbBench) close() {
p.b.Log(p.db.s.tops.bpool)
p.db.Close()
p.stor.Close()
os.RemoveAll(benchDB)
@@ -331,7 +332,7 @@ func BenchmarkDBRead(b *testing.B) {
p := openDBBench(b, false)
p.populate(b.N)
p.fill()
p.drop()
p.gc()
iter := p.newIter()
b.ResetTimer()
@@ -362,7 +363,7 @@ func BenchmarkDBReadUncompressed(b *testing.B) {
p := openDBBench(b, true)
p.populate(b.N)
p.fill()
p.drop()
p.gc()
iter := p.newIter()
b.ResetTimer()
@@ -379,7 +380,7 @@ func BenchmarkDBReadTable(b *testing.B) {
p.populate(b.N)
p.fill()
p.reopen()
p.drop()
p.gc()
iter := p.newIter()
b.ResetTimer()
@@ -395,7 +396,7 @@ func BenchmarkDBReadReverse(b *testing.B) {
p := openDBBench(b, false)
p.populate(b.N)
p.fill()
p.drop()
p.gc()
iter := p.newIter()
b.ResetTimer()
@@ -413,7 +414,7 @@ func BenchmarkDBReadReverseTable(b *testing.B) {
p.populate(b.N)
p.fill()
p.reopen()
p.drop()
p.gc()
iter := p.newIter()
b.ResetTimer()

View File

@@ -35,8 +35,8 @@ type DB struct {
// MemDB.
memMu sync.RWMutex
mem *memdb.DB
frozenMem *memdb.DB
memPool *util.Pool
mem, frozenMem *memDB
journal *journal.Writer
journalWriter storage.Writer
journalFile storage.File
@@ -79,6 +79,8 @@ func openDB(s *session) (*DB, error) {
s: s,
// Initial sequence
seq: s.stSeq,
// MemDB
memPool: util.NewPool(1),
// Write
writeC: make(chan *Batch),
writeMergedC: make(chan bool),
@@ -257,6 +259,7 @@ func recoverTable(s *session, o *opt.Options) error {
var mSeq uint64
var good, corrupted int
rec := new(sessionRecord)
bpool := util.NewBufferPool(o.GetBlockSize() + 5)
buildTable := func(iter iterator.Iterator) (tmp storage.File, size int64, err error) {
tmp = s.newTemp()
writer, err := tmp.Create()
@@ -314,7 +317,7 @@ func recoverTable(s *session, o *opt.Options) error {
var tSeq uint64
var tgood, tcorrupted, blockerr int
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.(iterator.ErrorCallbackSetter).SetErrorCallback(func(err error) {
s.logf("table@recovery found error @%d %q", file.Num(), err)
@@ -481,10 +484,11 @@ func (db *DB) recoverJournal() error {
buf.Reset()
if _, err := buf.ReadFrom(r); err != nil {
if strict {
if err == io.ErrUnexpectedEOF {
continue
} else {
return err
}
continue
}
if err := batch.decode(buf.Bytes()); err != nil {
return err
@@ -558,19 +562,20 @@ func (db *DB) get(key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, er
ikey := newIKey(key, seq, tSeek)
em, fm := db.getMems()
for _, m := range [...]*memdb.DB{em, fm} {
for _, m := range [...]*memDB{em, fm} {
if m == nil {
continue
}
defer m.decref()
mk, mv, me := m.Find(ikey)
mk, mv, me := m.db.Find(ikey)
if me == nil {
ukey, _, t, ok := parseIkey(mk)
if ok && db.s.icmp.uCompare(ukey, key) == 0 {
if t == tDel {
return nil, ErrNotFound
}
return mv, nil
return append([]byte{}, mv...), nil
}
} else if me != ErrNotFound {
return nil, me
@@ -590,8 +595,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
// DB does not contain the key.
//
// The caller should not modify the contents of the returned slice, but
// it is safe to modify the contents of the argument after Get returns.
// The returned slice is its own copy, it is safe to modify the contents
// 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) {
err = db.ok()
if err != nil {

View File

@@ -216,14 +216,15 @@ func (db *DB) memCompaction() {
if mem == nil {
return
}
defer mem.decref()
c := newCMem(db.s)
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.db.Len(), shortenb(mem.db.Size()))
// Don't compact empty memdb.
if mem.Len() == 0 {
if mem.db.Len() == 0 {
db.logf("mem@flush skipping")
// drop frozen mem
db.dropFrozenMem()
@@ -241,7 +242,7 @@ func (db *DB) memCompaction() {
db.compactionTransact("mem@flush", func(cnt *compactionTransactCounter) (err error) {
stats.startTimer()
defer stats.stopTimer()
return c.flush(mem, -1)
return c.flush(mem.db, -1)
}, func() error {
for _, r := range c.rec.addedTables {
db.logf("mem@flush rollback @%d", r.num)

View File

@@ -9,6 +9,7 @@ package leveldb
import (
"errors"
"runtime"
"sync"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/opt"
@@ -19,6 +20,17 @@ var (
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 {
em, fm := db.getMems()
v := db.s.version()
@@ -26,9 +38,13 @@ func (db *DB) newRawIterator(slice *util.Range, ro *opt.ReadOptions) iterator.It
ti := v.getIterators(slice, ro)
n := len(ti) + 2
i := make([]iterator.Iterator, 0, n)
i = append(i, em.NewIterator(slice))
emi := em.db.NewIterator(slice)
emi.SetReleaser(&memdbReleaser{m: em})
i = append(i, emi)
if fm != nil {
i = append(i, fm.NewIterator(slice))
fmi := fm.db.NewIterator(slice)
fmi.SetReleaser(&memdbReleaser{m: fm})
i = append(i, fmi)
}
i = append(i, ti...)
strict := db.s.o.GetStrict(opt.StrictIterator) || ro.GetStrict(opt.StrictIterator)

View File

@@ -11,8 +11,27 @@ import (
"github.com/syndtr/goleveldb/leveldb/journal"
"github.com/syndtr/goleveldb/leveldb/memdb"
"github.com/syndtr/goleveldb/leveldb/util"
)
type memDB struct {
pool *util.Pool
db *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 {
m.pool.Put(m)
} else if ref < 0 {
panic("negative memdb ref")
}
}
// Get latest sequence number.
func (db *DB) getSeq() uint64 {
return atomic.LoadUint64(&db.seq)
@@ -25,7 +44,7 @@ func (db *DB) addSeq(delta uint64) {
// Create new memdb and froze the old one; need external synchronization.
// 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()
file := db.s.getJournalFile(num)
w, err := file.Create()
@@ -37,6 +56,10 @@ func (db *DB) newMem(n int) (mem *memdb.DB, err error) {
db.memMu.Lock()
defer db.memMu.Unlock()
if db.frozenMem != nil {
panic("still has frozen mem")
}
if db.journal == nil {
db.journal = journal.NewWriter(w)
} else {
@@ -47,8 +70,19 @@ func (db *DB) newMem(n int) (mem *memdb.DB, err error) {
db.journalWriter = w
db.journalFile = file
db.frozenMem = db.mem
db.mem = memdb.New(db.s.icmp, maxInt(db.s.o.GetWriteBuffer(), n))
mem = db.mem
mem, ok := db.memPool.Get().(*memDB)
if ok && mem.db.Capacity() >= n {
mem.db.Reset()
mem.incref()
} else {
mem = &memDB{
pool: db.memPool,
db: memdb.New(db.s.icmp, maxInt(db.s.o.GetWriteBuffer(), n)),
ref: 1,
}
}
mem.incref()
db.mem = mem
// The seq only incremented by the writer. And whoever called newMem
// should hold write lock, so no need additional synchronization here.
db.frozenSeq = db.seq
@@ -56,16 +90,27 @@ func (db *DB) newMem(n int) (mem *memdb.DB, err error) {
}
// Get all memdbs.
func (db *DB) getMems() (e *memdb.DB, f *memdb.DB) {
func (db *DB) getMems() (e, f *memDB) {
db.memMu.RLock()
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
}
// Get frozen memdb.
func (db *DB) getEffectiveMem() *memdb.DB {
func (db *DB) getEffectiveMem() *memDB {
db.memMu.RLock()
defer db.memMu.RUnlock()
if db.mem == nil {
panic("nil effective mem")
}
db.mem.incref()
return db.mem
}
@@ -77,9 +122,12 @@ func (db *DB) hasFrozenMem() bool {
}
// Get frozen memdb.
func (db *DB) getFrozenMem() *memdb.DB {
func (db *DB) getFrozenMem() *memDB {
db.memMu.RLock()
defer db.memMu.RUnlock()
if db.frozenMem != nil {
db.frozenMem.incref()
}
return db.frozenMem
}
@@ -92,6 +140,7 @@ func (db *DB) dropFrozenMem() {
db.logf("journal@remove removed @%d", db.frozenJournalFile.Num())
}
db.frozenJournalFile = nil
db.frozenMem.decref()
db.frozenMem = nil
db.memMu.Unlock()
}

View File

@@ -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.
err = db.compSendIdle(db.mcompCmdC)
if err != nil {
@@ -63,13 +63,19 @@ func (db *DB) rotateMem(n int) (mem *memdb.DB, err error) {
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
flush := func() bool {
flush := func() (retry bool) {
v := db.s.version()
defer v.release()
mem = db.getEffectiveMem()
nn = mem.Free()
defer func() {
if retry {
mem.decref()
mem = nil
}
}()
nn = mem.db.Free()
switch {
case v.tLen(0) >= kL0_SlowdownWritesTrigger && !delayed:
delayed = true
@@ -84,12 +90,17 @@ func (db *DB) flush(n int) (mem *memdb.DB, nn int, err error) {
}
default:
// Allow memdb to grow if it has no entry.
if mem.Len() == 0 {
if mem.db.Len() == 0 {
nn = n
return false
} else {
mem.decref()
mem, err = db.rotateMem(n)
if err == nil {
nn = mem.db.Free()
} else {
nn = 0
}
}
mem, err = db.rotateMem(n)
nn = mem.Free()
return false
}
return true
@@ -140,6 +151,7 @@ retry:
if err != nil {
return
}
defer mem.decref()
// Calculate maximum size of the batch.
m := 1 << 20
@@ -178,7 +190,7 @@ drain:
return
case db.journalC <- b:
// Write into memdb
b.memReplay(mem)
b.memReplay(mem.db)
}
// Wait for journal writer
select {
@@ -188,7 +200,7 @@ drain:
case err = <-db.journalAckC:
if err != nil {
// Revert memdb if error detected
b.revertMemReplay(mem)
b.revertMemReplay(mem.db)
return
}
}
@@ -197,7 +209,7 @@ drain:
if err != nil {
return
}
b.memReplay(mem)
b.memReplay(mem.db)
}
// Set last seq number.
@@ -258,7 +270,8 @@ func (db *DB) CompactRange(r util.Range) error {
// Check for overlaps in memdb.
mem := db.getEffectiveMem()
if isMemOverlaps(db.s.icmp, mem, r.Start, r.Limit) {
defer mem.decref()
if isMemOverlaps(db.s.icmp, mem.db, r.Start, r.Limit) {
// Memdb compaction.
if _, err := db.rotateMem(0); err != nil {
<-db.writeLockC

View File

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

View File

@@ -103,18 +103,18 @@ type flusher interface {
Flush() error
}
// DroppedError is the error type that passed to Dropper.Drop method.
type DroppedError struct {
// ErrCorrupted is the error type that generated by corrupted block or chunk.
type ErrCorrupted struct {
Size int
Reason string
}
func (e DroppedError) Error() string {
return fmt.Sprintf("leveldb/journal: dropped %d bytes: %s", e.Size, e.Reason)
func (e ErrCorrupted) Error() string {
return fmt.Sprintf("leveldb/journal: block/chunk corrupted: %s (%d bytes)", e.Reason, e.Size)
}
// Dropper is the interface that wrap simple Drop method. The Drop
// method will be called when the journal reader dropping a chunk.
// method will be called when the journal reader dropping a block or chunk.
type Dropper interface {
Drop(err error)
}
@@ -158,76 +158,78 @@ func NewReader(r io.Reader, dropper Dropper, strict, checksum bool) *Reader {
}
}
var errSkip = errors.New("leveldb/journal: skipped")
func (r *Reader) corrupt(n int, reason string, skip bool) error {
if r.dropper != nil {
r.dropper.Drop(ErrCorrupted{n, reason})
}
if r.strict && !skip {
r.err = ErrCorrupted{n, reason}
return r.err
}
return errSkip
}
// nextChunk sets r.buf[r.i:r.j] to hold the next chunk's payload, reading the
// next block into the buffer if necessary.
func (r *Reader) nextChunk(wantFirst, skip bool) error {
func (r *Reader) nextChunk(first bool) error {
for {
if r.j+headerSize <= r.n {
checksum := binary.LittleEndian.Uint32(r.buf[r.j+0 : r.j+4])
length := binary.LittleEndian.Uint16(r.buf[r.j+4 : r.j+6])
chunkType := r.buf[r.j+6]
var err error
if checksum == 0 && length == 0 && chunkType == 0 {
// Drop entire block.
err = DroppedError{r.n - r.j, "zero header"}
m := r.n - r.j
r.i = r.n
r.j = r.n
return r.corrupt(m, "zero header", false)
} else {
m := r.n - r.j
r.i = r.j + headerSize
r.j = r.j + headerSize + int(length)
if r.j > r.n {
// Drop entire block.
err = DroppedError{m, "chunk length overflows block"}
r.i = r.n
r.j = r.n
return r.corrupt(m, "chunk length overflows block", false)
} else if r.checksum && checksum != util.NewCRC(r.buf[r.i-1:r.j]).Value() {
// Drop entire block.
err = DroppedError{m, "checksum mismatch"}
r.i = r.n
r.j = r.n
return r.corrupt(m, "checksum mismatch", false)
}
}
if wantFirst && err == nil && chunkType != fullChunkType && chunkType != firstChunkType {
if skip {
// The chunk are intentionally skipped.
if chunkType == lastChunkType {
skip = false
}
continue
} else {
// Drop the chunk.
err = DroppedError{r.j - r.i + headerSize, "orphan chunk"}
}
if first && chunkType != fullChunkType && chunkType != firstChunkType {
m := r.j - r.i
r.i = r.j
// Report the error, but skip it.
return r.corrupt(m+headerSize, "orphan chunk", true)
}
if err == nil {
r.last = chunkType == fullChunkType || chunkType == lastChunkType
} else {
if r.dropper != nil {
r.dropper.Drop(err)
}
if r.strict {
r.err = err
}
r.last = chunkType == fullChunkType || chunkType == lastChunkType
return nil
}
// The last block.
if r.n < blockSize && r.n > 0 {
if !first {
return r.corrupt(0, "missing chunk part", false)
}
r.err = io.EOF
return r.err
}
// Read block.
n, err := io.ReadFull(r.r, r.buf[:])
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return err
}
if r.n < blockSize && r.n > 0 {
// This is the last block.
if r.j != r.n {
r.err = io.ErrUnexpectedEOF
} else {
r.err = io.EOF
}
return r.err
}
n, err := io.ReadFull(r.r, r.buf[:])
if err != nil && err != io.ErrUnexpectedEOF {
r.err = err
return r.err
}
if n == 0 {
if !first {
return r.corrupt(0, "missing chunk part", false)
}
r.err = io.EOF
return r.err
}
@@ -237,29 +239,26 @@ func (r *Reader) nextChunk(wantFirst, skip bool) error {
// Next returns a reader for the next journal. It returns io.EOF if there are no
// more journals. The reader returned becomes stale after the next Next call,
// and should no longer be used.
// and should no longer be used. If strict is false, the reader will returns
// io.ErrUnexpectedEOF error when found corrupted journal.
func (r *Reader) Next() (io.Reader, error) {
r.seq++
if r.err != nil {
return nil, r.err
}
skip := !r.last
r.i = r.j
for {
r.i = r.j
if r.nextChunk(true, skip) != nil {
// So that 'orphan chunk' drop will be reported.
skip = false
} else {
if err := r.nextChunk(true); err == nil {
break
}
if r.err != nil {
return nil, r.err
} else if err != errSkip {
return nil, err
}
}
return &singleReader{r, r.seq, nil}, nil
}
// Reset resets the journal reader, allows reuse of the journal reader.
// Reset resets the journal reader, allows reuse of the journal reader. Reset returns
// last accumulated error.
func (r *Reader) Reset(reader io.Reader, dropper Dropper, strict, checksum bool) error {
r.seq++
err := r.err
@@ -296,7 +295,11 @@ func (x *singleReader) Read(p []byte) (int, error) {
if r.last {
return 0, io.EOF
}
if x.err = r.nextChunk(false, false); x.err != nil {
x.err = r.nextChunk(false)
if x.err != nil {
if x.err == errSkip {
x.err = io.ErrUnexpectedEOF
}
return 0, x.err
}
}
@@ -320,7 +323,11 @@ func (x *singleReader) ReadByte() (byte, error) {
if r.last {
return 0, io.EOF
}
if x.err = r.nextChunk(false, false); x.err != nil {
x.err = r.nextChunk(false)
if x.err != nil {
if x.err == errSkip {
x.err = io.ErrUnexpectedEOF
}
return 0, x.err
}
}

View File

@@ -12,6 +12,7 @@ package journal
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
@@ -326,3 +327,492 @@ func TestStaleWriter(t *testing.T) {
t.Fatalf("stale write #1: unexpected error: %v", err)
}
}
func TestCorrupt_MissingLastBlock(t *testing.T) {
buf := new(bytes.Buffer)
w := NewWriter(buf)
// First record.
ww, err := w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-1024)); err != nil {
t.Fatalf("write #0: unexpected error: %v", err)
}
// Second record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-headerSize)); err != nil {
t.Fatalf("write #1: unexpected error: %v", err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
// Cut the last block.
b := buf.Bytes()[:blockSize]
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
// First read.
rr, err := r.Next()
if err != nil {
t.Fatal(err)
}
n, err := io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #0: %v", err)
}
if n != blockSize-1024 {
t.Fatalf("read #0: got %d bytes want %d", n, blockSize-1024)
}
// Second read.
rr, err = r.Next()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(ioutil.Discard, rr)
if err != io.ErrUnexpectedEOF {
t.Fatalf("read #1: unexpected error: %v", err)
}
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)
}
}

View File

@@ -22,7 +22,7 @@ type dropper struct {
}
func (d dropper) Drop(err error) {
if e, ok := err.(journal.DroppedError); ok {
if e, ok := err.(journal.ErrCorrupted); ok {
d.s.logf("journal@drop %s-%d S·%s %q", d.file.Type(), d.file.Num(), shortenb(e.Size), e.Reason)
} else {
d.s.logf("journal@drop %s-%d %q", d.file.Type(), d.file.Num(), err)

View File

@@ -275,6 +275,7 @@ type tOps struct {
s *session
cache cache.Cache
cacheNS cache.Namespace
bpool *util.BufferPool
}
// Creates an empty table and returns table writer.
@@ -340,7 +341,7 @@ func (t *tOps) open(f *tFile) (c cache.Object, err error) {
}
ok = true
value = table.NewReader(r, int64(f.size), cacheNS, o)
value = table.NewReader(r, int64(f.size), cacheNS, t.bpool, o)
charge = 1
fin = func() {
r.Close()
@@ -412,8 +413,12 @@ func (t *tOps) close() {
// Creates new initialized table ops instance.
func newTableOps(s *session, cacheCap int) *tOps {
c := cache.NewLRUCache(cacheCap)
ns := c.GetNamespace(0)
return &tOps{s, c, ns}
return &tOps{
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

View File

@@ -437,18 +437,20 @@ func (i *blockIter) Value() []byte {
}
func (i *blockIter) Release() {
i.prevNode = nil
i.prevKeys = nil
i.key = nil
i.value = nil
i.dir = dirReleased
if i.cache != nil {
i.cache.Release()
i.cache = nil
}
if i.releaser != nil {
i.releaser.Release()
i.releaser = nil
if i.dir > dirReleased {
i.prevNode = nil
i.prevKeys = nil
i.key = nil
i.value = nil
i.dir = dirReleased
if i.cache != nil {
i.cache.Release()
i.cache = nil
}
if i.releaser != nil {
i.releaser.Release()
i.releaser = nil
}
}
}
@@ -519,6 +521,7 @@ type Reader struct {
reader io.ReaderAt
cache cache.Namespace
err error
bpool *util.BufferPool
// Options
cmp comparer.Comparer
filter filter.Filter
@@ -538,7 +541,7 @@ func verifyChecksum(data []byte) bool {
}
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 {
return nil, err
}
@@ -551,8 +554,13 @@ func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
case blockTypeNoCompression:
data = data[:bh.length]
case blockTypeSnappyCompression:
var err error
data, err = snappy.Decode(nil, data[:bh.length])
decLen, err := snappy.DecodedLen(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 {
return nil, err
}
@@ -602,6 +610,18 @@ func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterB
return b, nil
}
type releaseBlock struct {
r *Reader
b *block
}
func (r releaseBlock) Release() {
if r.b.data != nil {
r.r.bpool.Put(r.b.data)
r.b.data = nil
}
}
func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fillCache bool) iterator.Iterator {
if r.cache != nil {
// Get/set block cache.
@@ -616,6 +636,10 @@ func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fi
ok = true
value = dataBlock
charge = int(dataBH.length)
fin = func() {
r.bpool.Put(dataBlock.data)
dataBlock.data = nil
}
}
return
})
@@ -638,7 +662,7 @@ func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fi
if err != nil {
return iterator.NewEmptyIterator(err)
}
iter := dataBlock.newIterator(slice, false, nil)
iter := dataBlock.newIterator(slice, false, releaseBlock{r, dataBlock})
return iter
}
@@ -708,8 +732,11 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
}
return
}
// Don't use block buffer, no need to copy the buffer.
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
}
@@ -760,13 +787,17 @@ func (r *Reader) OffsetOf(key []byte) (offset int64, err error) {
}
// 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.
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{
reader: f,
cache: cache,
bpool: bpool,
cmp: o.GetComparer(),
checksum: o.GetStrict(opt.StrictBlockChecksum),
strictIter: o.GetStrict(opt.StrictIterator),

View File

@@ -59,7 +59,7 @@ var _ = testutil.Defer(func() {
It("Should be able to approximate offset of a key correctly", func() {
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) {
offset, err := tr.OffsetOf([]byte(key))
Expect(err).ShouldNot(HaveOccurred())
@@ -95,7 +95,7 @@ var _ = testutil.Defer(func() {
tw.Close()
// 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}
}
Test := func(kv *testutil.KeyValue, body func(r *Reader)) func() {

View File

@@ -0,0 +1,156 @@
// 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 [4]chan []byte
size [3]uint32
sizeMiss [3]uint32
baseline0 int
baseline1 int
baseline2 int
less uint32
equal uint32
greater uint32
miss uint32
}
func (p *BufferPool) poolNum(n int) int {
switch {
case n <= p.baseline0:
return 0
case n <= p.baseline1:
return 1
case n <= p.baseline2:
return 2
default:
return 3
}
}
// Get returns buffer with length of n.
func (p *BufferPool) Get(n int) []byte {
poolNum := p.poolNum(n)
pool := p.pool[poolNum]
if poolNum == 0 {
// Fast path.
select {
case b := <-pool:
switch {
case cap(b) > n:
atomic.AddUint32(&p.less, 1)
return b[:n]
case cap(b) == n:
atomic.AddUint32(&p.equal, 1)
return b[:n]
default:
panic("not reached")
}
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:
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) {
pool := p.pool[p.poolNum(cap(b))]
select {
case pool <- b:
default:
}
}
func (p *BufferPool) String() string {
return fmt.Sprintf("BufferPool{B·%d Z·%v Zm·%v L·%d E·%d G·%d M·%d}",
p.baseline0, p.size, p.sizeMiss, p.less, p.equal, p.greater, p.miss)
}
func (p *BufferPool) drain() {
for {
time.Sleep(1 * time.Second)
select {
case <-p.pool[0]:
case <-p.pool[1]:
case <-p.pool[2]:
case <-p.pool[3]:
default:
}
}
}
// 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,
baseline1: baseline * 2,
baseline2: baseline * 4,
}
for i, cap := range []int{6, 6, 3, 1} {
p.pool[i] = make(chan []byte, cap)
}
go p.drain()
return p
}

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
syncthing
=========
[![Build Status](https://img.shields.io/travis/calmh/syncthing.svg?style=flat)](https://travis-ci.org/calmh/syncthing)
[![Coverage Status](https://img.shields.io/coveralls/calmh/syncthing.svg?style=flat)](https://coveralls.io/r/calmh/syncthing?branch=master)
[![API Documentation](http://img.shields.io/badge/api-Godoc-blue.svg?style=flat)](http://godoc.org/github.com/calmh/syncthing)
[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://opensource.org/licenses/MIT)
[![Latest Build](http://img.shields.io/jenkins/s/http/build.syncthing.net/syncthing.svg?style=flat-square)](http://build.syncthing.net/job/syncthing/lastBuild/)
[![API Documentation](http://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](http://godoc.org/github.com/syncthing/syncthing)
[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](http://opensource.org/licenses/MIT)
This is the `syncthing` project. The following are the project goals:
@@ -12,7 +11,7 @@ This is the `syncthing` project. The following are the project goals:
number of collaborating nodes. The protocol should be well defined,
unambiguous, easily understood, free to use, efficient, secure and
language neutral. This is the [Block Exchange
Protocol](https://github.com/calmh/syncthing/blob/master/protocol/PROTOCOL.md).
Protocol](https://github.com/syncthing/syncthing/blob/master/protocol/PROTOCOL.md).
2. Provide the reference implementation to demonstrate the usability of
said protocol. This is the `syncthing` utility. It is the hope that
@@ -26,14 +25,22 @@ for incompatible changes.
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
---------------
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
normal release bundle as `syncthing.asc` or `syncthing.exe.asc`.
the key BCE524C7 (http://nym.se/gpg.txt). For release binaries, MD5 and
SHA1 checksums are calculated and signed, available in the
md5sum.txt.asc and sha1sum.txt.asc files.
Documentation
=============
@@ -50,4 +57,4 @@ under the [Creative Commons Attribution 4.0 International
License](http://creativecommons.org/licenses/by/4.0/).
All code is licensed under the [MIT
License](https://github.com/calmh/syncthing/blob/master/LICENSE).
License](https://github.com/syncthing/syncthing/blob/master/LICENSE).

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1008 B

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
assets/logo-text-128.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
assets/logo-text-256.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
assets/logo-text-64.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

7
auto/auto_test.go Normal file
View File

@@ -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 auto_test
// Empty test file to generate 0% coverage rather than no coverage

View File

File diff suppressed because one or more lines are too long

View File

@@ -16,47 +16,17 @@ type dst struct {
conn *net.UDPConn
}
type Beacon struct {
conn *net.UDPConn
port int
conns []dst
inbox chan []byte
outbox chan recv
type Interface interface {
Send(data []byte)
Recv() ([]byte, net.Addr)
}
func New(port int) (*Beacon, error) {
conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: port})
if err != nil {
return nil, err
}
b := &Beacon{
conn: conn,
port: port,
inbox: make(chan []byte),
outbox: make(chan recv, 16),
}
go b.reader()
go b.writer()
return b, nil
}
func (b *Beacon) Send(data []byte) {
b.inbox <- data
}
func (b *Beacon) Recv() ([]byte, net.Addr) {
recv := <-b.outbox
return recv.data, recv.src
}
func (b *Beacon) reader() {
func genericReader(conn *net.UDPConn, outbox chan<- recv) {
bs := make([]byte, 65536)
for {
n, addr, err := b.conn.ReadFrom(bs)
n, addr, err := conn.ReadFrom(bs)
if err != nil {
l.Warnln("Beacon read:", err)
l.Warnln("multicast read:", err)
return
}
if debug {
@@ -66,7 +36,7 @@ func (b *Beacon) reader() {
c := make([]byte, n)
copy(c, bs)
select {
case b.outbox <- recv{c, addr}:
case outbox <- recv{c, addr}:
default:
if debug {
l.Debugln("dropping message")
@@ -74,59 +44,3 @@ func (b *Beacon) reader() {
}
}
}
func (b *Beacon) writer() {
for bs := range b.inbox {
addrs, err := net.InterfaceAddrs()
if err != nil {
l.Warnln("Beacon: interface addresses:", err)
continue
}
var dsts []net.IP
for _, addr := range addrs {
if iaddr, ok := addr.(*net.IPNet); ok && iaddr.IP.IsGlobalUnicast() && iaddr.IP.To4() != nil {
baddr := bcast(iaddr)
dsts = append(dsts, baddr.IP)
}
}
if len(dsts) == 0 {
// Fall back to the general IPv4 broadcast address
dsts = append(dsts, net.IP{0xff, 0xff, 0xff, 0xff})
}
if debug {
l.Debugln("addresses:", dsts)
}
for _, ip := range dsts {
dst := &net.UDPAddr{IP: ip, Port: b.port}
_, err := b.conn.WriteTo(bs, dst)
if err != nil {
if debug {
l.Debugln(err)
}
} else if debug {
l.Debugf("sent %d bytes to %s", len(bs), dst)
}
}
}
}
func bcast(ip *net.IPNet) *net.IPNet {
var bc = &net.IPNet{}
bc.IP = make([]byte, len(ip.IP))
copy(bc.IP, ip.IP)
bc.Mask = ip.Mask
offset := len(bc.IP) - len(bc.Mask)
for i := range bc.IP {
if i-offset >= 0 {
bc.IP[i] = ip.IP[i] | ^ip.Mask[i-offset]
}
}
return bc
}

98
beacon/broadcast.go Normal file
View File

@@ -0,0 +1,98 @@
// 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 beacon
import "net"
type Broadcast struct {
conn *net.UDPConn
port int
conns []dst
inbox chan []byte
outbox chan recv
}
func NewBroadcast(port int) (*Broadcast, error) {
conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: port})
if err != nil {
return nil, err
}
b := &Broadcast{
conn: conn,
port: port,
inbox: make(chan []byte),
outbox: make(chan recv, 16),
}
go genericReader(b.conn, b.outbox)
go b.writer()
return b, nil
}
func (b *Broadcast) Send(data []byte) {
b.inbox <- data
}
func (b *Broadcast) Recv() ([]byte, net.Addr) {
recv := <-b.outbox
return recv.data, recv.src
}
func (b *Broadcast) writer() {
for bs := range b.inbox {
addrs, err := net.InterfaceAddrs()
if err != nil {
l.Warnln("Broadcast: interface addresses:", err)
continue
}
var dsts []net.IP
for _, addr := range addrs {
if iaddr, ok := addr.(*net.IPNet); ok && iaddr.IP.IsGlobalUnicast() && iaddr.IP.To4() != nil {
baddr := bcast(iaddr)
dsts = append(dsts, baddr.IP)
}
}
if len(dsts) == 0 {
// Fall back to the general IPv4 broadcast address
dsts = append(dsts, net.IP{0xff, 0xff, 0xff, 0xff})
}
if debug {
l.Debugln("addresses:", dsts)
}
for _, ip := range dsts {
dst := &net.UDPAddr{IP: ip, Port: b.port}
_, err := b.conn.WriteTo(bs, dst)
if err != nil {
if debug {
l.Debugln(err)
}
} else if debug {
l.Debugf("sent %d bytes to %s", len(bs), dst)
}
}
}
}
func bcast(ip *net.IPNet) *net.IPNet {
var bc = &net.IPNet{}
bc.IP = make([]byte, len(ip.IP))
copy(bc.IP, ip.IP)
bc.Mask = ip.Mask
offset := len(bc.IP) - len(bc.Mask)
for i := range bc.IP {
if i-offset >= 0 {
bc.IP[i] = ip.IP[i] | ^ip.Mask[i-offset]
}
}
return bc
}

View File

@@ -8,7 +8,7 @@ import (
"os"
"strings"
"github.com/calmh/syncthing/logger"
"github.com/syncthing/syncthing/logger"
)
var (

70
beacon/multicast.go Normal file
View File

@@ -0,0 +1,70 @@
// 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 beacon
import "net"
type Multicast struct {
conn *net.UDPConn
addr *net.UDPAddr
conns []dst
inbox chan []byte
outbox chan recv
}
func NewMulticast(addr string) (*Multicast, error) {
gaddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
conn, err := net.ListenMulticastUDP("udp", nil, gaddr)
if err != nil {
return nil, err
}
b := &Multicast{
conn: conn,
addr: gaddr,
inbox: make(chan []byte),
outbox: make(chan recv, 16),
}
go genericReader(b.conn, b.outbox)
go b.writer()
return b, nil
}
func (b *Multicast) Send(data []byte) {
b.inbox <- data
}
func (b *Multicast) Recv() ([]byte, net.Addr) {
recv := <-b.outbox
return recv.data, recv.src
}
func (b *Multicast) writer() {
for bs := range b.inbox {
intfs, err := net.Interfaces()
if err != nil {
l.Warnln("multicast interfaces:", err)
continue
}
for _, intf := range intfs {
if intf.Flags&net.FlagUp != 0 && intf.Flags&net.FlagMulticast != 0 {
addr := *b.addr
addr.Zone = intf.Name
_, err = b.conn.WriteTo(bs, &addr)
if err != nil {
if debug {
l.Debugln(err, "on write to", addr)
}
} else if debug {
l.Debugf("sent %d bytes to %s", len(bs), addr.String())
}
}
}
}
}

487
build.go Normal file
View File

@@ -0,0 +1,487 @@
// 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()
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", pkg)
}
func install(pkg string) {
os.Setenv("GOBIN", "./bin")
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")
} else {
os.Setenv("GOARCH", goarch)
}
if goarch == "386" {
os.Setenv("GO386", "387")
}
}
func assets() {
runPipe("auto/gui.files.go", "godep", "go", "run", "cmd/genassets/main.go", "gui")
}
func xdr() {
for _, f := range []string{"discover/packets", "files/leveldb", "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")
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")
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)
}
}
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)
}
}

275
build.sh
View File

@@ -1,236 +1,99 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
export COPYFILE_DISABLE=true
export GO386=387 # Don't use SSE on 32 bit builds
distFiles=(README.md LICENSE CONTRIBUTORS) # apart from the binary itself
version=$(git describe --always --dirty)
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
GOPATH="$GOPATH:$(godep path)" go get -v -t ./...
}
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 > valid-langs.js
popd
assets
}
case "$1" in
"")
shift
export GOBIN=$(pwd)/bin
godep go install $* -ldflags "$ldflags" ./cmd/...
case "${1:-default}" in
default)
go run build.go
;;
race)
build -race
;;
guidev)
echo "Syncthing is already built for GUI developments. Try:"
echo " STGUIASSETS=~/someDir/gui syncthing"
clean)
go run build.go "$1"
;;
test)
test
;;
test-cov)
test-cov
go run build.go "$1"
;;
tar)
rm -f *.tar.gz *.zip
test || exit 1
assets
build
eval $(go env)
name="syncthing-$GOOS-$GOARCH-$version"
tarDist "$name"
;;
all)
rm -f *.tar.gz *.zip
test || exit 1
assets
for os in darwin-amd64 linux-386 linux-amd64 freebsd-amd64 windows-amd64 windows-386 solaris-amd64 ; do
export GOOS=${os%-*}
export GOARCH=${os#*-}
build
name="syncthing-$os-$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 calmh/syncthing "$tag" "$f"
done
go run build.go "$1"
;;
deps)
deps
go run build.go "$1"
;;
assets)
assets
;;
setup)
setup
go run build.go "$1"
;;
xdr)
xdr
go run build.go "$1"
;;
translate)
translate
go run build.go "$1"
;;
transifex)
transifex
go run build.go "$1"
;;
noupgrade)
go run build.go -no-upgrade tar
;;
all)
go run build.go test
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)
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

View File

@@ -10,8 +10,8 @@ import (
"log"
"os"
"github.com/calmh/syncthing/files"
"github.com/calmh/syncthing/protocol"
"github.com/syncthing/syncthing/files"
"github.com/syncthing/syncthing/protocol"
"github.com/syndtr/goleveldb/leveldb"
)
@@ -32,7 +32,8 @@ func main() {
if *node == "" {
log.Printf("*** Global index for repo %q", *repo)
fs.WithGlobal(func(f protocol.FileInfo) bool {
fs.WithGlobalTruncated(func(fi protocol.FileIntf) bool {
f := fi.(protocol.FileInfoTruncated)
fmt.Println(f)
fmt.Println("\t", fs.Availability(f.Name))
return true
@@ -43,7 +44,8 @@ func main() {
log.Fatal(err)
}
log.Printf("*** Have index for repo %q node %q", *repo, n)
fs.WithHave(n, func(f protocol.FileInfo) bool {
fs.WithHaveTruncated(n, func(fi protocol.FileIntf) bool {
f := fi.(protocol.FileInfoTruncated)
fmt.Println(f)
return true
})

View File

@@ -26,13 +26,13 @@ import (
"crypto/tls"
"code.google.com/p/go.crypto/bcrypt"
"github.com/calmh/syncthing/auto"
"github.com/calmh/syncthing/config"
"github.com/calmh/syncthing/events"
"github.com/calmh/syncthing/logger"
"github.com/calmh/syncthing/model"
"github.com/calmh/syncthing/protocol"
"github.com/calmh/syncthing/upgrade"
"github.com/syncthing/syncthing/auto"
"github.com/syncthing/syncthing/config"
"github.com/syncthing/syncthing/events"
"github.com/syncthing/syncthing/logger"
"github.com/syncthing/syncthing/model"
"github.com/syncthing/syncthing/protocol"
"github.com/syncthing/syncthing/upgrade"
"github.com/vitrun/qart/qr"
)
@@ -57,7 +57,7 @@ const (
func init() {
l.AddHandler(logger.LevelWarn, showGuiError)
sub := events.Default.Subscribe(^events.EventType(events.ItemStarted | events.ItemCompleted))
sub := events.Default.Subscribe(events.AllEvents)
eventSub = events.NewBufferedSubscription(sub, 1000)
}
@@ -126,6 +126,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
postRestMux.HandleFunc("/rest/restart", restPostRestart)
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
postRestMux.HandleFunc("/rest/scan", withModel(m, restPostScan))
// A handler that splits requests between the two above and disables
// caching
@@ -277,7 +278,9 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
var newCfg config.Configuration
err := json.NewDecoder(r.Body).Decode(&newCfg)
if err != nil {
l.Warnln(err)
l.Warnln("decoding posted config:", err)
http.Error(w, err.Error(), 500)
return
} else {
if newCfg.GUI.Password == "" {
// Leave it empty
@@ -286,7 +289,9 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
} else {
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
if err != nil {
l.Warnln(err)
l.Warnln("bcrypting password:", err)
http.Error(w, err.Error(), 500)
return
} else {
newCfg.GUI.Password = string(hash)
}
@@ -384,7 +389,7 @@ func restGetSystem(w http.ResponseWriter, r *http.Request) {
res["myID"] = myID.String()
res["goroutines"] = runtime.NumGoroutine()
res["alloc"] = m.Alloc
res["sys"] = m.Sys
res["sys"] = m.Sys - m.HeapReleased
res["tilde"] = expandTilde("~")
if cfg.Options.GlobalAnnEnabled && discoverer != nil {
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
@@ -454,12 +459,18 @@ func restGetEvents(w http.ResponseWriter, r *http.Request) {
since, _ := strconv.Atoi(sinceStr)
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)
if 0 < limit && limit < len(evs) {
evs = evs[len(evs)-limit:]
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(evs)
}
@@ -498,25 +509,25 @@ func restGetLang(w http.ResponseWriter, r *http.Request) {
lang := r.Header.Get("Accept-Language")
var langs []string
for _, l := range strings.Split(lang, ",") {
if len(l) >= 2 {
langs = append(langs, l[:2])
}
parts := strings.SplitN(l, ";", 2)
langs = append(langs, strings.TrimSpace(parts[0]))
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(langs)
}
func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
if err != nil {
l.Warnln(err)
l.Warnln("getting latest release:", err)
http.Error(w, err.Error(), 500)
return
}
if upgrade.CompareVersions(rel.Tag, Version) == 1 {
err = upgrade.UpgradeTo(rel)
err = upgrade.UpgradeTo(rel, GoArchExtra)
if err != nil {
l.Warnln(err)
l.Warnln("upgrading:", err)
http.Error(w, err.Error(), 500)
return
}
@@ -525,9 +536,19 @@ func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
}
}
func restPostScan(m *model.Model, w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
repo := qs.Get("repo")
sub := qs.Get("sub")
err := m.ScanRepoSub(repo, sub)
if err != nil {
http.Error(w, err.Error(), 500)
}
}
func getQR(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
text := r.FormValue("text")
var qs = r.URL.Query()
var text = qs.Get("text")
code, err := qr.Encode(text, qr.M)
if err != nil {
http.Error(w, "Invalid", 500)

View File

@@ -12,7 +12,7 @@ import (
"sync"
"time"
"github.com/calmh/syncthing/osutil"
"github.com/syncthing/syncthing/osutil"
)
var csrfTokens []string
@@ -43,6 +43,12 @@ func csrfMiddleware(prefix string, next http.Handler) http.Handler {
return
}
if r.Method == "GET" {
// Allow GET requests unconditionally
next.ServeHTTP(w, r)
return
}
// Verify the CSRF token
token := r.Header.Get("X-CSRF-Token")
if !validCsrfToken(token) {

View File

@@ -79,7 +79,7 @@ func trackCPUUsage() {
for _ = range time.NewTicker(time.Second).C {
err := solarisPrusage(pid, &rusage)
if err != nil {
l.Warnln(err)
l.Warnln("getting prusage:", err)
continue
}
curTime := time.Now().UnixNano()

46
cmd/syncthing/heapprof.go Normal file
View File

@@ -0,0 +1,46 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"os"
"runtime"
"runtime/pprof"
"syscall"
"time"
)
func init() {
if os.Getenv("STHEAPPROFILE") != "" {
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
}
}
}

View File

@@ -26,16 +26,16 @@ import (
"strings"
"time"
"github.com/calmh/syncthing/config"
"github.com/calmh/syncthing/discover"
"github.com/calmh/syncthing/events"
"github.com/calmh/syncthing/logger"
"github.com/calmh/syncthing/model"
"github.com/calmh/syncthing/osutil"
"github.com/calmh/syncthing/protocol"
"github.com/calmh/syncthing/upgrade"
"github.com/calmh/syncthing/upnp"
"github.com/juju/ratelimit"
"github.com/syncthing/syncthing/config"
"github.com/syncthing/syncthing/discover"
"github.com/syncthing/syncthing/events"
"github.com/syncthing/syncthing/logger"
"github.com/syncthing/syncthing/model"
"github.com/syncthing/syncthing/osutil"
"github.com/syncthing/syncthing/protocol"
"github.com/syncthing/syncthing/upgrade"
"github.com/syncthing/syncthing/upnp"
"github.com/syndtr/goleveldb/leveldb"
)
@@ -47,6 +47,7 @@ var (
BuildHost = "unknown"
BuildUser = "unknown"
LongVersion string
GoArchExtra string // "", "v5", "v6", "v7"
)
var l = logger.DefaultLogger
@@ -54,7 +55,7 @@ var l = logger.DefaultLogger
func init() {
if Version != "unknown-dev" {
// If not a generic dev build, version string should come from git describe
exp := regexp.MustCompile(`^v\d+\.\d+\.\d+(-beta\d+)?(-\d+-g[0-9a-f]+)?(-dirty)?$`)
exp := regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z0-9]+)*(\+\d+-g[0-9a-f]+)?(-dirty)?$`)
if !exp.MatchString(Version) {
l.Fatalf("Invalid version string %q;\n\tdoes not match regexp %v", Version, exp)
}
@@ -72,15 +73,17 @@ func init() {
}
var (
cfg config.Configuration
myID protocol.NodeID
confDir string
logFlags int = log.Ltime
rateBucket *ratelimit.Bucket
stop = make(chan bool)
discoverer *discover.Discoverer
lockConn *net.TCPListener
lockPort int
cfg config.Configuration
myID protocol.NodeID
confDir string
logFlags int = log.Ltime
rateBucket *ratelimit.Bucket
stop = make(chan bool)
discoverer *discover.Discoverer
lockConn *net.TCPListener
lockPort int
externalPort int
cert tls.Certificate
)
const (
@@ -103,9 +106,6 @@ The following enviroment variables are interpreted by syncthing:
Set this variable when running under a service manager such as
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
facility strings:
- "beacon" (the beacon package)
@@ -119,10 +119,19 @@ The following enviroment variables are interpreted by syncthing:
- "xdr" (the xdr package)
- "all" (all of the above)
STCPUPROFILE Write CPU profile to the specified file.
STGUIASSETS Directory to load GUI assets from. Overrides compiled in assets.
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.
STDEADLOCKTIMEOUT Alter deadlock detection timeout (seconds; default 1200).`
)
@@ -135,12 +144,16 @@ func main() {
var showVersion bool
var doUpgrade bool
var doUpgradeCheck bool
var generateDir string
var noBrowser bool
flag.StringVar(&confDir, "home", getDefaultConfDir(), "Set configuration directory")
flag.BoolVar(&reset, "reset", false, "Prepare to resync from cluster")
flag.BoolVar(&showVersion, "version", false, "Show version")
flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade")
flag.BoolVar(&doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
flag.BoolVar(&noBrowser, "no-browser", false, "Do not start browser")
flag.IntVar(&logFlags, "logflags", logFlags, "Set log flags")
flag.StringVar(&generateDir, "generate", "", "Generate key in specified dir")
flag.Usage = usageFor(flag.CommandLine, usage, extraUsage)
flag.Parse()
@@ -151,10 +164,29 @@ func main() {
l.SetFlags(logFlags)
var err error
lockPort, err = getLockPort()
if err != nil {
l.Fatalln("Opening lock port:", err)
if generateDir != "" {
dir := expandTilde(generateDir)
info, err := os.Stat(dir)
l.FatalErr(err)
if !info.IsDir() {
l.Fatalln(dir, "is not a directory")
}
cert, err := loadCert(dir, "")
if err == nil {
l.Warnln("Key exists; will not overwrite.")
l.Infoln("Node ID:", protocol.NewNodeID(cert.Certificate[0]))
return
}
newCertificate(dir, "")
cert, err = loadCert(dir, "")
l.FatalErr(err)
if err == nil {
l.Infoln("Node ID:", protocol.NewNodeID(cert.Certificate[0]))
}
return
}
if doUpgrade || doUpgradeCheck {
@@ -171,7 +203,7 @@ func main() {
l.Infof("Upgrade available (current %q < latest %q)", Version, rel.Tag)
if doUpgrade {
err = upgrade.UpgradeTo(rel)
err = upgrade.UpgradeTo(rel, GoArchExtra)
if err != nil {
l.Fatalln("Upgrade:", err) // exits 1
}
@@ -182,6 +214,12 @@ func main() {
}
}
var err error
lockPort, err = getLockPort()
if err != nil {
l.Fatalln("Opening lock port:", err)
}
if len(os.Getenv("GOGC")) == 0 {
debug.SetGCPercent(25)
}
@@ -218,7 +256,7 @@ func main() {
// Ensure that our home directory exists and that we have a certificate and key.
ensureDir(confDir, 0700)
cert, err := loadCert(confDir, "")
cert, err = loadCert(confDir, "")
if err != nil {
newCertificate(confDir, "")
cert, err = loadCert(confDir, "")
@@ -236,6 +274,8 @@ func main() {
cfgFile := filepath.Join(confDir, "config.xml")
go saveConfigLoop(cfgFile)
var myName string
// Load the configuration file, if it exists.
// If it does not, create a template.
@@ -247,25 +287,30 @@ func main() {
l.Fatalln(err)
}
cf.Close()
myCfg := cfg.GetNodeConfiguration(myID)
if myCfg == nil || myCfg.Name == "" {
myName, _ = os.Hostname()
} else {
myName = myCfg.Name
}
} else {
l.Infoln("No config file; starting with empty defaults")
name, _ := os.Hostname()
myName, _ = os.Hostname()
defaultRepo := filepath.Join(getHomeDir(), "Sync")
ensureDir(defaultRepo, 0755)
cfg, err = config.Load(nil, myID)
cfg.Repositories = []config.RepositoryConfiguration{
{
ID: "default",
Directory: defaultRepo,
Nodes: []config.NodeConfiguration{{NodeID: myID}},
Nodes: []config.RepositoryNodeConfiguration{{NodeID: myID}},
},
}
cfg.Nodes = []config.NodeConfiguration{
{
NodeID: myID,
Addresses: []string{"dynamic"},
Name: name,
Name: myName,
},
}
@@ -326,9 +371,9 @@ func main() {
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), 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)
m := model.NewModel(confDir, &cfg, myName, "syncthing", Version, db)
nextRepo:
for i, repo := range cfg.Repositories {
@@ -393,12 +438,21 @@ nextRepo:
if err != nil {
l.Fatalln("Cannot start GUI:", err)
}
if cfg.Options.StartBrowser && len(os.Getenv("STRESTART")) == 0 {
if !noBrowser && cfg.Options.StartBrowser && len(os.Getenv("STRESTART")) == 0 {
openURL(fmt.Sprintf("%s://%s:%d", proto, hostOpen, addr.Port))
}
}
}
// Clear out old indexes for other nodes. Otherwise we'll start up and
// start needing a bunch of files which are nowhere to be found. This
// needs to be changed when we correctly do persistent indexes.
for _, repoCfg := range cfg.Repositories {
for _, node := range repoCfg.NodeIDs() {
m.Index(node, repoCfg.ID, nil)
}
}
// Walk the repository and update the local model before establishing any
// connections to other nodes.
@@ -431,11 +485,8 @@ nextRepo:
// UPnP
var externalPort = 0
if cfg.Options.UPnPEnabled {
// We seed the random number generator with the node ID to get a
// repeatable sequence of random external ports.
externalPort = setupUPnP(rand.NewSource(certSeed(cert.Certificate[0])))
setupUPnP()
}
// Routine to connect out to configured nodes
@@ -459,7 +510,7 @@ nextRepo:
}
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 {
log.Fatal(err)
}
@@ -522,8 +573,7 @@ func waitForParentExit() {
l.Infoln("Continuing")
}
func setupUPnP(r rand.Source) int {
var externalPort = 0
func setupUPnP() {
if len(cfg.Options.ListenAddress) == 1 {
_, portStr, err := net.SplitHostPort(cfg.Options.ListenAddress[0])
if err != nil {
@@ -533,17 +583,11 @@ func setupUPnP(r rand.Source) int {
port, _ := strconv.Atoi(portStr)
igd, err := upnp.Discover()
if err == nil {
for i := 0; i < 10; i++ {
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
}
}
externalPort = setupExternalPort(igd, port)
if externalPort == 0 {
l.Warnln("Failed to create UPnP port mapping")
} else {
l.Infoln("Created UPnP port mapping - external port", externalPort)
}
} else {
l.Infof("No UPnP gateway detected")
@@ -551,11 +595,57 @@ func setupUPnP(r rand.Source) int {
l.Debugf("UPnP: %v", err)
}
}
if cfg.Options.UPnPRenewal > 0 {
go renewUPnP(port)
}
}
} else {
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
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. 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() {
@@ -756,6 +846,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)
conn.Close()
}
@@ -895,19 +989,15 @@ func setTCPOptions(conn *net.TCPConn) {
}
func discovery(extPort int) *discover.Discoverer {
disc, err := discover.NewDiscoverer(myID, cfg.Options.ListenAddress, cfg.Options.LocalAnnPort)
if err != nil {
l.Warnf("No discovery possible (%v)", err)
return nil
}
disc := discover.NewDiscoverer(myID, cfg.Options.ListenAddress)
if cfg.Options.LocalAnnEnabled {
l.Infoln("Sending local discovery announcements")
disc.StartLocal()
l.Infoln("Starting local discovery announcements")
disc.StartLocal(cfg.Options.LocalAnnPort, cfg.Options.LocalAnnMCAddr)
}
if cfg.Options.GlobalAnnEnabled {
l.Infoln("Sending global discovery announcements")
l.Infoln("Starting global discovery announcements")
disc.StartGlobal(cfg.Options.GlobalAnnServer, uint16(extPort))
}

View File

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

View File

@@ -1,4 +1,8 @@
// +build perfstats
// 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
@@ -11,7 +15,9 @@ import (
)
func init() {
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
if os.Getenv("STPERFSTATS") != "" {
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
}
}
func savePerfStats(file string) {
@@ -40,6 +46,6 @@ func savePerfStats(file string) {
startms := int(t.Sub(t0).Seconds() * 1000)
fmt.Fprintf(fd, "%d\t%f\t%d\t%d\n", startms, cpuUsagePercent, memstats.Alloc, memstats.Sys)
fmt.Fprintf(fd, "%d\t%f\t%d\t%d\n", startms, cpuUsagePercent, memstats.Alloc, memstats.Sys-memstats.HeapReleased)
}
}

View File

@@ -39,7 +39,7 @@ func certSeed(bs []byte) int64 {
}
func newCertificate(dir string, prefix string) {
l.Infoln("Generating RSA certificate and key...")
l.Infoln("Generating RSA key and certificate...")
priv, err := rsa.GenerateKey(rand.Reader, tlsRSABits)
l.FatalErr(err)
@@ -67,11 +67,9 @@ func newCertificate(dir string, prefix string) {
l.FatalErr(err)
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
l.Okln("Created RSA certificate file")
keyOut, err := os.OpenFile(filepath.Join(dir, prefix+"key.pem"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
l.FatalErr(err)
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
keyOut.Close()
l.Okln("Created RSA key file")
}

View File

@@ -11,7 +11,7 @@ import (
"strings"
"time"
"github.com/calmh/syncthing/model"
"github.com/syncthing/syncthing/model"
)
// Current version number of the usage report, for acceptance purposes. If
@@ -51,7 +51,7 @@ func reportData(m *model.Model) map[string]interface{} {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
res["memoryUsageMiB"] = mem.Sys / 1024 / 1024
res["memoryUsageMiB"] = (mem.Sys - mem.HeapReleased) / 1024 / 1024
var perf float64
for i := 0; i < 5; i++ {

View File

@@ -9,10 +9,13 @@ package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"regexp"
"sort"
"strings"
)
type stat struct {
@@ -31,6 +34,12 @@ func main() {
log.Fatal("Need environment variables TRANSIFEX_USER and TRANSIFEX_PASS")
}
curValidLangs := map[string]bool{}
for _, lang := range loadValidLangs() {
curValidLangs[lang] = true
}
log.Println(curValidLangs)
resp := req("https://www.transifex.com/api/2/project/syncthing/resource/gui/stats")
var stats map[string]stat
@@ -42,19 +51,21 @@ func main() {
var langs []string
for code, stat := range stats {
shortCode := code[:2]
if pct := 100 * stat.Translated / (stat.Translated + stat.Untranslated); pct < 95 {
log.Printf("Skipping language %q (too low completion ratio %d%%)", shortCode, pct)
os.Remove("lang-" + shortCode + ".json")
code = strings.Replace(code, "_", "-", 1)
if !curValidLangs[code] {
if pct := 100 * stat.Translated / (stat.Translated + stat.Untranslated); pct < 95 {
log.Printf("Skipping language %q (too low completion ratio %d%%)", code, pct)
os.Remove("lang-" + code + ".json")
continue
}
}
langs = append(langs, code)
if code == "en" {
continue
}
langs = append(langs, shortCode)
if shortCode == "en" {
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)
var t translation
@@ -64,7 +75,7 @@ func main() {
}
resp.Body.Close()
fd, err := os.Create("lang-" + shortCode + ".json")
fd, err := os.Create("lang-" + code + ".json")
if err != nil {
log.Fatal(err)
}
@@ -72,9 +83,18 @@ func main() {
fd.Close()
}
saveValidLangs(langs)
}
func saveValidLangs(langs []string) {
sort.Strings(langs)
fmt.Print("var validLangs = ")
json.NewEncoder(os.Stdout).Encode(langs)
fd, err := os.Create("valid-langs.js")
if err != nil {
log.Fatal(err)
}
fmt.Fprint(fd, "var validLangs = ")
json.NewEncoder(fd).Encode(langs)
fd.Close()
}
func userPass() (string, string) {
@@ -97,3 +117,27 @@ func req(url string) *http.Response {
}
return resp
}
func loadValidLangs() []string {
fd, err := os.Open("valid-langs.js")
if err != nil {
log.Fatal(err)
}
defer fd.Close()
bs, err := ioutil.ReadAll(fd)
if err != nil {
log.Fatal(err)
}
var langs []string
exp := regexp.MustCompile(`\[([a-zA-Z",-]+)\]`)
if matches := exp.FindSubmatch(bs); len(matches) == 2 {
langs = strings.Split(string(matches[1]), ",")
for i := range langs {
// Remove quotes
langs[i] = langs[i][1 : len(langs[i])-1]
}
}
return langs
}

View File

@@ -10,12 +10,14 @@ import (
"encoding/json"
"log"
"os"
"regexp"
"strings"
"code.google.com/p/go.net/html"
)
var trans = make(map[string]string)
var attrRe = regexp.MustCompile(`\{\{'([^']+)'\s+\|\s+translate\}\}`)
func generalNode(n *html.Node) {
translate := false
@@ -24,6 +26,10 @@ func generalNode(n *html.Node) {
if a.Key == "translate" {
translate = true
break
} else {
if matches := attrRe.FindStringSubmatch(a.Val); len(matches) == 2 {
translation(matches[1])
}
}
}
} else if n.Type == html.TextNode {
@@ -44,12 +50,7 @@ func generalNode(n *html.Node) {
func inTranslate(n *html.Node) {
if n.Type == html.TextNode {
v := strings.TrimSpace(n.Data)
if _, ok := trans[v]; !ok {
av := strings.Replace(v, "{%", "{{", -1)
av = strings.Replace(av, "%}", "}}", -1)
trans[v] = av
}
translation(n.Data)
} else {
log.Println("translate node with non-text child <")
log.Println(n)
@@ -60,6 +61,15 @@ func inTranslate(n *html.Node) {
}
}
func translation(v string) {
v = strings.TrimSpace(v)
if _, ok := trans[v]; !ok {
av := strings.Replace(v, "{%", "{{", -1)
av = strings.Replace(av, "%}", "}}", -1)
trans[v] = av
}
}
func main() {
fd, err := os.Open(os.Args[1])
if err != nil {
@@ -71,10 +81,16 @@ func main() {
}
fd.Close()
doc, err := html.Parse(os.Stdin)
fd, err = os.Open(os.Args[2])
if err != nil {
log.Fatal(err)
}
doc, err := html.Parse(fd)
if err != nil {
log.Fatal(err)
}
fd.Close()
generalNode(doc)
bs, err := json.MarshalIndent(trans, "", " ")
if err != nil {

View File

@@ -15,14 +15,14 @@ import (
"strconv"
"code.google.com/p/go.crypto/bcrypt"
"github.com/calmh/syncthing/logger"
"github.com/calmh/syncthing/protocol"
"github.com/syncthing/syncthing/logger"
"github.com/syncthing/syncthing/protocol"
)
var l = logger.DefaultLogger
type Configuration struct {
Version int `xml:"version,attr" default:"2"`
Version int `xml:"version,attr" default:"3"`
Repositories []RepositoryConfiguration `xml:"repository"`
Nodes []NodeConfiguration `xml:"node"`
GUI GUIConfiguration `xml:"gui"`
@@ -31,13 +31,14 @@ type Configuration struct {
}
type RepositoryConfiguration struct {
ID string `xml:"id,attr"`
Directory string `xml:"directory,attr"`
Nodes []NodeConfiguration `xml:"node"`
ReadOnly bool `xml:"ro,attr"`
IgnorePerms bool `xml:"ignorePerms,attr"`
Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
Versioning VersioningConfiguration `xml:"versioning"`
ID string `xml:"id,attr"`
Directory string `xml:"directory,attr"`
Nodes []RepositoryNodeConfiguration `xml:"node"`
ReadOnly bool `xml:"ro,attr"`
RescanIntervalS int `xml:"rescanIntervalS,attr" default:"60"`
IgnorePerms bool `xml:"ignorePerms,attr"`
Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
Versioning VersioningConfiguration `xml:"versioning"`
nodeIDs []protocol.NodeID
}
@@ -100,26 +101,35 @@ type NodeConfiguration struct {
CertName string `xml:"certName,attr,omitempty"`
}
type RepositoryNodeConfiguration struct {
NodeID protocol.NodeID `xml:"id,attr"`
Deprecated_Name string `xml:"name,attr,omitempty" json:"-"`
Deprecated_Addresses []string `xml:"address,omitempty" json:"-"`
}
type OptionsConfiguration struct {
ListenAddress []string `xml:"listenAddress" default:"0.0.0.0:22000"`
GlobalAnnServer string `xml:"globalAnnounceServer" default:"announce.syncthing.net:22026"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true"`
LocalAnnPort int `xml:"localAnnouncePort" default:"21025"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" default:"[ff32::5222]:21026"`
ParallelRequests int `xml:"parallelRequests" default:"16"`
MaxSendKbps int `xml:"maxSendKbps"`
RescanIntervalS int `xml:"rescanIntervalS" default:"60"`
ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60"`
MaxChangeKbps int `xml:"maxChangeKbps" default:"10000"`
StartBrowser bool `xml:"startBrowser" default:"true"`
UPnPEnabled bool `xml:"upnpEnabled" default:"true"`
UPnPLease int `xml:"upnpLeaseMinutes" default:"0"`
UPnPRenewal int `xml:"upnpRenewalMinutes" default:"30"`
URAccepted int `xml:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
Deprecated_UREnabled bool `xml:"urEnabled,omitempty" json:"-"`
Deprecated_URDeclined bool `xml:"urDeclined,omitempty" json:"-"`
Deprecated_ReadOnly bool `xml:"readOnly,omitempty" json:"-"`
Deprecated_GUIEnabled bool `xml:"guiEnabled,omitempty" json:"-"`
Deprecated_GUIAddress string `xml:"guiAddress,omitempty" json:"-"`
Deprecated_RescanIntervalS int `xml:"rescanIntervalS,omitempty" json:"-"`
Deprecated_UREnabled bool `xml:"urEnabled,omitempty" json:"-"`
Deprecated_URDeclined bool `xml:"urDeclined,omitempty" json:"-"`
Deprecated_ReadOnly bool `xml:"readOnly,omitempty" json:"-"`
Deprecated_GUIEnabled bool `xml:"guiEnabled,omitempty" json:"-"`
Deprecated_GUIAddress string `xml:"guiAddress,omitempty" json:"-"`
}
type GUIConfiguration struct {
@@ -139,6 +149,15 @@ func (cfg *Configuration) NodeMap() map[protocol.NodeID]NodeConfiguration {
return m
}
func (cfg *Configuration) GetNodeConfiguration(nodeid protocol.NodeID) *NodeConfiguration {
for i, node := range cfg.Nodes {
if node.NodeID == nodeid {
return &cfg.Nodes[i]
}
}
return nil
}
func (cfg *Configuration) RepoMap() map[string]RepositoryConfiguration {
m := make(map[string]RepositoryConfiguration, len(cfg.Repositories))
for _, r := range cfg.Repositories {
@@ -296,11 +315,21 @@ func Load(rd io.Reader, myID protocol.NodeID) (Configuration, error) {
convertV1V2(&cfg)
}
// Upgrade to v3 configuration if appropriate
if cfg.Version == 2 {
convertV2V3(&cfg)
}
// Upgrade to v4 configuration if appropriate
if cfg.Version == 3 {
convertV3V4(&cfg)
}
// Hash old cleartext passwords
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0)
if err != nil {
l.Warnln(err)
l.Warnln("bcrypting password:", err)
} else {
cfg.GUI.Password = string(hash)
}
@@ -314,15 +343,22 @@ func Load(rd io.Reader, myID protocol.NodeID) (Configuration, error) {
}
// Ensure this node is present in all relevant places
me := cfg.GetNodeConfiguration(myID)
if me == nil {
myName, _ := os.Hostname()
cfg.Nodes = append(cfg.Nodes, NodeConfiguration{
NodeID: myID,
Name: myName,
})
}
sort.Sort(NodeConfigurationList(cfg.Nodes))
// Ensure that any loose nodes are not present in the wrong places
// Ensure that there are no duplicate nodes
cfg.Nodes = ensureNodePresent(cfg.Nodes, myID)
sort.Sort(NodeConfigurationList(cfg.Nodes))
for i := range cfg.Repositories {
cfg.Repositories[i].Nodes = ensureNodePresent(cfg.Repositories[i].Nodes, myID)
cfg.Repositories[i].Nodes = ensureExistingNodes(cfg.Repositories[i].Nodes, existingNodes)
cfg.Repositories[i].Nodes = ensureNoDuplicates(cfg.Repositories[i].Nodes)
sort.Sort(NodeConfigurationList(cfg.Repositories[i].Nodes))
sort.Sort(RepositoryNodeConfigurationList(cfg.Repositories[i].Nodes))
}
// An empty address list is equivalent to a single "dynamic" entry
@@ -333,20 +369,56 @@ func Load(rd io.Reader, myID protocol.NodeID) (Configuration, error) {
}
}
return cfg, err
}
func convertV3V4(cfg *Configuration) {
// In previous versions, rescan interval was common for each repository.
// From now, it can be set independently. We have to make sure, that after upgrade
// the individual rescan interval will be defined for every existing repository.
for i := range cfg.Repositories {
cfg.Repositories[i].RescanIntervalS = cfg.Options.Deprecated_RescanIntervalS
}
cfg.Options.Deprecated_RescanIntervalS = 0
// In previous versions, repositories held full node configurations.
// Since that's the only place where node configs were in V1, we still have
// to define the deprecated fields to be able to upgrade from V1 to V4.
for i, repo := range cfg.Repositories {
for j := range repo.Nodes {
rncfg := cfg.Repositories[i].Nodes[j]
rncfg.Deprecated_Name = ""
rncfg.Deprecated_Addresses = nil
}
}
cfg.Version = 4
}
func convertV2V3(cfg *Configuration) {
// In previous versions, compression was always on. When upgrading, enable
// compression on all existing new. New nodes will get compression on by
// default by the GUI.
for i := range cfg.Nodes {
cfg.Nodes[i].Compression = true
}
// The global discovery format and port number changed in v0.9. Having the
// default announce server but old port number is guaranteed to be legacy.
if cfg.Options.GlobalAnnServer == "announce.syncthing.net:22025" {
cfg.Options.GlobalAnnServer = "announce.syncthing.net:22026"
}
return cfg, err
cfg.Version = 3
}
func convertV1V2(cfg *Configuration) {
// Collect the list of nodes.
// Replace node configs inside repositories with only a reference to the nide ID.
// Set all repositories to read only if the global read only flag is set.
var nodes = map[string]NodeConfiguration{}
var nodes = map[string]RepositoryNodeConfiguration{}
for i, repo := range cfg.Repositories {
cfg.Repositories[i].ReadOnly = cfg.Options.Deprecated_ReadOnly
for j, node := range repo.Nodes {
@@ -354,14 +426,18 @@ func convertV1V2(cfg *Configuration) {
if _, ok := nodes[id]; !ok {
nodes[id] = node
}
cfg.Repositories[i].Nodes[j] = NodeConfiguration{NodeID: node.NodeID}
cfg.Repositories[i].Nodes[j] = RepositoryNodeConfiguration{NodeID: node.NodeID}
}
}
cfg.Options.Deprecated_ReadOnly = false
// Set and sort the list of nodes.
for _, node := range nodes {
cfg.Nodes = append(cfg.Nodes, node)
cfg.Nodes = append(cfg.Nodes, NodeConfiguration{
NodeID: node.NodeID,
Name: node.Deprecated_Name,
Addresses: node.Deprecated_Addresses,
})
}
sort.Sort(NodeConfigurationList(cfg.Nodes))
@@ -386,23 +462,33 @@ func (l NodeConfigurationList) Len() int {
return len(l)
}
func ensureNodePresent(nodes []NodeConfiguration, myID protocol.NodeID) []NodeConfiguration {
type RepositoryNodeConfigurationList []RepositoryNodeConfiguration
func (l RepositoryNodeConfigurationList) Less(a, b int) bool {
return l[a].NodeID.Compare(l[b].NodeID) == -1
}
func (l RepositoryNodeConfigurationList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l RepositoryNodeConfigurationList) Len() int {
return len(l)
}
func ensureNodePresent(nodes []RepositoryNodeConfiguration, myID protocol.NodeID) []RepositoryNodeConfiguration {
for _, node := range nodes {
if node.NodeID.Equals(myID) {
return nodes
}
}
name, _ := os.Hostname()
nodes = append(nodes, NodeConfiguration{
nodes = append(nodes, RepositoryNodeConfiguration{
NodeID: myID,
Name: name,
})
return nodes
}
func ensureExistingNodes(nodes []NodeConfiguration, existingNodes map[protocol.NodeID]bool) []NodeConfiguration {
func ensureExistingNodes(nodes []RepositoryNodeConfiguration, existingNodes map[protocol.NodeID]bool) []RepositoryNodeConfiguration {
count := len(nodes)
i := 0
loop:
@@ -417,7 +503,7 @@ loop:
return nodes[0:count]
}
func ensureNoDuplicates(nodes []NodeConfiguration) []NodeConfiguration {
func ensureNoDuplicates(nodes []RepositoryNodeConfiguration) []RepositoryNodeConfiguration {
count := len(nodes)
i := 0
seenNodes := make(map[protocol.NodeID]bool)

View File

@@ -11,7 +11,7 @@ import (
"reflect"
"testing"
"github.com/calmh/syncthing/protocol"
"github.com/syncthing/syncthing/protocol"
)
var node1, node2, node3, node4 protocol.NodeID
@@ -30,13 +30,14 @@ func TestDefaultValues(t *testing.T) {
GlobalAnnEnabled: true,
LocalAnnEnabled: true,
LocalAnnPort: 21025,
LocalAnnMCAddr: "[ff32::5222]:21026",
ParallelRequests: 16,
MaxSendKbps: 0,
RescanIntervalS: 60,
ReconnectIntervalS: 60,
MaxChangeKbps: 10000,
StartBrowser: true,
UPnPEnabled: true,
UPnPLease: 0,
UPnPRenewal: 30,
}
cfg, err := Load(bytes.NewReader(nil), node1)
@@ -68,6 +69,7 @@ func TestNodeConfig(t *testing.T) {
</repository>
<options>
<readOnly>true</readOnly>
<rescanIntervalS>600</rescanIntervalS>
</options>
</configuration>
`)
@@ -88,10 +90,44 @@ func TestNodeConfig(t *testing.T) {
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
<address>b</address>
</node>
<options>
<rescanIntervalS>600</rescanIntervalS>
</options>
</configuration>
`)
for i, data := range [][]byte{v1data, v2data} {
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>
<options>
<rescanIntervalS>600</rescanIntervalS>
</options>
</configuration>`)
v4data := []byte(`
<configuration version="4">
<repository id="test" directory="~/Sync" ro="true" ignorePerms="false" rescanIntervalS="600">
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></node>
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></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, v4data} {
cfg, err := Load(bytes.NewReader(data), node1)
if err != nil {
t.Error(err)
@@ -99,28 +135,31 @@ func TestNodeConfig(t *testing.T) {
expectedRepos := []RepositoryConfiguration{
{
ID: "test",
Directory: "~/Sync",
Nodes: []NodeConfiguration{{NodeID: node1}, {NodeID: node4}},
ReadOnly: true,
ID: "test",
Directory: "~/Sync",
Nodes: []RepositoryNodeConfiguration{{NodeID: node1}, {NodeID: node4}},
ReadOnly: true,
RescanIntervalS: 600,
},
}
expectedNodes := []NodeConfiguration{
{
NodeID: node1,
Name: "node one",
Addresses: []string{"a"},
NodeID: node1,
Name: "node one",
Addresses: []string{"a"},
Compression: true,
},
{
NodeID: node4,
Name: "node two",
Addresses: []string{"b"},
NodeID: node4,
Name: "node two",
Addresses: []string{"b"},
Compression: true,
},
}
expectedNodeIDs := []protocol.NodeID{node1, node4}
if cfg.Version != 2 {
t.Errorf("%d: Incorrect version %d != 2", i, cfg.Version)
if cfg.Version != 4 {
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)
@@ -169,13 +208,14 @@ func TestOverriddenValues(t *testing.T) {
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>false</localAnnounceEnabled>
<localAnnouncePort>42123</localAnnouncePort>
<localAnnounceMCAddr>quux:3232</localAnnounceMCAddr>
<parallelRequests>32</parallelRequests>
<maxSendKbps>1234</maxSendKbps>
<rescanIntervalS>600</rescanIntervalS>
<reconnectionIntervalS>6000</reconnectionIntervalS>
<maxChangeKbps>2345</maxChangeKbps>
<startBrowser>false</startBrowser>
<upnpEnabled>false</upnpEnabled>
<upnpLeaseMinutes>60</upnpLeaseMinutes>
<upnpRenewalMinutes>15</upnpRenewalMinutes>
</options>
</configuration>
`)
@@ -186,13 +226,14 @@ func TestOverriddenValues(t *testing.T) {
GlobalAnnEnabled: false,
LocalAnnEnabled: false,
LocalAnnPort: 42123,
LocalAnnMCAddr: "quux:3232",
ParallelRequests: 32,
MaxSendKbps: 1234,
RescanIntervalS: 600,
ReconnectIntervalS: 6000,
MaxChangeKbps: 2345,
StartBrowser: false,
UPnPEnabled: false,
UPnPLease: 60,
UPnPRenewal: 15,
}
cfg, err := Load(bytes.NewReader(data), node1)
@@ -222,16 +263,19 @@ func TestNodeAddressesDynamic(t *testing.T) {
name, _ := os.Hostname()
expected := []NodeConfiguration{
{
NodeID: node1,
Addresses: []string{"dynamic"},
NodeID: node1,
Addresses: []string{"dynamic"},
Compression: true,
},
{
NodeID: node2,
Addresses: []string{"dynamic"},
NodeID: node2,
Addresses: []string{"dynamic"},
Compression: true,
},
{
NodeID: node3,
Addresses: []string{"dynamic"},
NodeID: node3,
Addresses: []string{"dynamic"},
Compression: true,
},
{
NodeID: node4,
@@ -252,7 +296,7 @@ func TestNodeAddressesDynamic(t *testing.T) {
func TestNodeAddressesStatic(t *testing.T) {
data := []byte(`
<configuration version="2">
<configuration version="3">
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
<address>192.0.2.1</address>
<address>192.0.2.2</address>

View File

@@ -8,7 +8,7 @@ import (
"os"
"strings"
"github.com/calmh/syncthing/logger"
"github.com/syncthing/syncthing/logger"
)
var (

View File

@@ -14,9 +14,9 @@ import (
"sync"
"time"
"github.com/calmh/syncthing/beacon"
"github.com/calmh/syncthing/events"
"github.com/calmh/syncthing/protocol"
"github.com/syncthing/syncthing/beacon"
"github.com/syncthing/syncthing/events"
"github.com/syncthing/syncthing/protocol"
)
type Discoverer struct {
@@ -24,15 +24,26 @@ type Discoverer struct {
listenAddrs []string
localBcastIntv time.Duration
globalBcastIntv time.Duration
beacon *beacon.Beacon
registry map[protocol.NodeID][]string
errorRetryIntv time.Duration
cacheLifetime time.Duration
broadcastBeacon beacon.Interface
multicastBeacon beacon.Interface
registry map[protocol.NodeID][]cacheEntry
registryLock sync.RWMutex
extServer string
extPort uint16
localBcastTick <-chan time.Time
stopGlobal chan struct{}
globalWG sync.WaitGroup
forcedBcastTick chan time.Time
extAnnounceOK bool
extAnnounceOKmut sync.Mutex
globalBcastStop chan bool
}
type cacheEntry struct {
addr string
seen time.Time
}
var (
@@ -44,37 +55,63 @@ var (
// When we hit this many errors in succession, we stop.
const maxErrors = 30
func NewDiscoverer(id protocol.NodeID, addresses []string, localPort int) (*Discoverer, error) {
b, err := beacon.New(localPort)
if err != nil {
return nil, err
}
disc := &Discoverer{
func NewDiscoverer(id protocol.NodeID, addresses []string) *Discoverer {
return &Discoverer{
myID: id,
listenAddrs: addresses,
localBcastIntv: 30 * time.Second,
globalBcastIntv: 1800 * time.Second,
beacon: b,
registry: make(map[protocol.NodeID][]string),
errorRetryIntv: 60 * time.Second,
cacheLifetime: 5 * time.Minute,
registry: make(map[protocol.NodeID][]cacheEntry),
}
go disc.recvAnnouncements()
return disc, nil
}
func (d *Discoverer) StartLocal() {
d.localBcastTick = time.Tick(d.localBcastIntv)
d.forcedBcastTick = make(chan time.Time)
go d.sendLocalAnnouncements()
func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
if localPort > 0 {
bb, err := beacon.NewBroadcast(localPort)
if err != nil {
l.Infof("No IPv4 discovery possible (%v)", err)
} else {
d.broadcastBeacon = bb
go d.recvAnnouncements(bb)
}
}
if len(localMCAddr) > 0 {
mb, err := beacon.NewMulticast(localMCAddr)
if err != nil {
l.Infof("No IPv6 discovery possible (%v)", err)
} else {
d.multicastBeacon = mb
go d.recvAnnouncements(mb)
}
}
if d.broadcastBeacon == nil && d.multicastBeacon == nil {
l.Warnln("No local discovery method available")
} else {
d.localBcastTick = time.Tick(d.localBcastIntv)
d.forcedBcastTick = make(chan time.Time)
go d.sendLocalAnnouncements()
}
}
func (d *Discoverer) StartGlobal(server string, extPort uint16) {
// Wait for any previous announcer to stop before starting a new one.
d.globalWG.Wait()
d.extServer = server
d.extPort = extPort
d.stopGlobal = make(chan struct{})
d.globalWG.Add(1)
go d.sendExternalAnnouncements()
}
func (d *Discoverer) StopGlobal() {
close(d.stopGlobal)
d.globalWG.Wait()
}
func (d *Discoverer) ExtAnnounceOK() bool {
d.extAnnounceOKmut.Lock()
defer d.extAnnounceOKmut.Unlock()
@@ -83,14 +120,28 @@ func (d *Discoverer) ExtAnnounceOK() bool {
func (d *Discoverer) Lookup(node protocol.NodeID) []string {
d.registryLock.Lock()
addr, ok := d.registry[node]
cached := d.filterCached(d.registry[node])
d.registryLock.Unlock()
if ok {
return addr
if len(cached) > 0 {
addrs := make([]string, len(cached))
for i := range cached {
addrs[i] = cached[i].addr
}
return addrs
} else if len(d.extServer) != 0 {
// We might want to cache this, but not permanently so it needs some intelligence
return d.externalLookup(node)
addrs := d.externalLookup(node)
cached = make([]cacheEntry, len(addrs))
for i := range addrs {
cached[i] = cacheEntry{
addr: addrs[i],
seen: time.Now(),
}
}
d.registryLock.Lock()
d.registry[node] = cached
d.registryLock.Unlock()
}
return nil
}
@@ -105,11 +156,11 @@ func (d *Discoverer) Hint(node string, addrs []string) {
})
}
func (d *Discoverer) All() map[protocol.NodeID][]string {
func (d *Discoverer) All() map[protocol.NodeID][]cacheEntry {
d.registryLock.RLock()
nodes := make(map[protocol.NodeID][]string, len(d.registry))
nodes := make(map[protocol.NodeID][]cacheEntry, len(d.registry))
for node, addrs := range d.registry {
addrsCopy := make([]string, len(addrs))
addrsCopy := make([]cacheEntry, len(addrs))
copy(addrsCopy, addrs)
nodes[node] = addrsCopy
}
@@ -149,21 +200,15 @@ func (d *Discoverer) sendLocalAnnouncements() {
Magic: AnnouncementMagic,
This: Node{d.myID[:], addrs},
}
msg := pkt.MarshalXDR()
for {
pkt.Extra = nil
d.registryLock.RLock()
for node, addrs := range d.registry {
if len(pkt.Extra) == 16 {
break
}
anode := Node{node[:], resolveAddrs(addrs)}
pkt.Extra = append(pkt.Extra, anode)
if d.multicastBeacon != nil {
d.multicastBeacon.Send(msg)
}
if d.broadcastBeacon != nil {
d.broadcastBeacon.Send(msg)
}
d.registryLock.RUnlock()
d.beacon.Send(pkt.MarshalXDR())
select {
case <-d.localBcastTick:
@@ -173,20 +218,19 @@ func (d *Discoverer) sendLocalAnnouncements() {
}
func (d *Discoverer) sendExternalAnnouncements() {
// this should go in the Discoverer struct
errorRetryIntv := 60 * time.Second
defer d.globalWG.Done()
remote, err := net.ResolveUDPAddr("udp", d.extServer)
for err != nil {
l.Warnf("Global discovery: %v; trying again in %v", err, errorRetryIntv)
time.Sleep(errorRetryIntv)
l.Warnf("Global discovery: %v; trying again in %v", err, d.errorRetryIntv)
time.Sleep(d.errorRetryIntv)
remote, err = net.ResolveUDPAddr("udp", d.extServer)
}
conn, err := net.ListenUDP("udp", nil)
for err != nil {
l.Warnf("Global discovery: %v; trying again in %v", err, errorRetryIntv)
time.Sleep(errorRetryIntv)
l.Warnf("Global discovery: %v; trying again in %v", err, d.errorRetryIntv)
time.Sleep(d.errorRetryIntv)
conn, err = net.ListenUDP("udp", nil)
}
@@ -201,7 +245,10 @@ func (d *Discoverer) sendExternalAnnouncements() {
buf = d.announcementPkt()
}
for {
var bcastTick = time.Tick(d.globalBcastIntv)
var errTick <-chan time.Time
sendOneAnnouncement := func() {
var ok bool
if debug {
@@ -230,19 +277,40 @@ func (d *Discoverer) sendExternalAnnouncements() {
d.extAnnounceOKmut.Unlock()
if ok {
time.Sleep(d.globalBcastIntv)
} else {
time.Sleep(errorRetryIntv)
errTick = nil
} else if errTick != nil {
errTick = time.Tick(d.errorRetryIntv)
}
}
// Announce once, immediately
sendOneAnnouncement()
loop:
for {
select {
case <-d.stopGlobal:
break loop
case <-errTick:
sendOneAnnouncement()
case <-bcastTick:
sendOneAnnouncement()
}
}
if debug {
l.Debugln("discover: stopping global")
}
}
func (d *Discoverer) recvAnnouncements() {
func (d *Discoverer) recvAnnouncements(b beacon.Interface) {
for {
buf, addr := d.beacon.Recv()
buf, addr := b.Recv()
if debug {
l.Debugf("discover: read announcement:\n%s", hex.Dump(buf))
l.Debugf("discover: read announcement from %s:\n%s", addr, hex.Dump(buf))
}
var pkt Announce
@@ -251,20 +319,9 @@ func (d *Discoverer) recvAnnouncements() {
continue
}
if debug {
l.Debugf("discover: parsed announcement: %#v", pkt)
}
var newNode bool
if bytes.Compare(pkt.This.ID, d.myID[:]) != 0 {
newNode = d.registerNode(addr, pkt.This)
for _, node := range pkt.Extra {
if bytes.Compare(node.ID, d.myID[:]) != 0 {
if d.registerNode(nil, node) {
newNode = true
}
}
}
}
if newNode {
@@ -276,41 +333,57 @@ func (d *Discoverer) recvAnnouncements() {
}
func (d *Discoverer) registerNode(addr net.Addr, node Node) bool {
var addrs []string
var id protocol.NodeID
copy(id[:], node.ID)
d.registryLock.RLock()
current := d.filterCached(d.registry[id])
d.registryLock.RUnlock()
orig := current
for _, a := range node.Addresses {
var nodeAddr string
if len(a.IP) > 0 {
nodeAddr = fmt.Sprintf("%s:%d", net.IP(a.IP), a.Port)
addrs = append(addrs, nodeAddr)
} else if addr != nil {
ua := addr.(*net.UDPAddr)
ua.Port = int(a.Port)
nodeAddr = ua.String()
addrs = append(addrs, nodeAddr)
}
}
if len(addrs) == 0 {
if debug {
l.Debugln("discover: no valid address for", node.ID)
for i := range current {
if current[i].addr == nodeAddr {
current[i].seen = time.Now()
goto done
}
}
current = append(current, cacheEntry{
addr: nodeAddr,
seen: time.Now(),
})
done:
}
if debug {
l.Debugf("discover: register: %s -> %#v", node.ID, addrs)
l.Debugf("discover: register: %v -> %v", id, current)
}
var id protocol.NodeID
copy(id[:], node.ID)
d.registryLock.Lock()
_, seen := d.registry[id]
d.registry[id] = addrs
d.registry[id] = current
d.registryLock.Unlock()
if !seen {
if len(current) > len(orig) {
addrs := make([]string, len(current))
for i := range current {
addrs[i] = current[i].addr
}
events.Default.Log(events.NodeDiscovered, map[string]interface{}{
"node": id.String(),
"addrs": addrs,
})
}
return !seen
return len(current) > len(orig)
}
func (d *Discoverer) externalLookup(node protocol.NodeID) []string {
@@ -374,10 +447,6 @@ func (d *Discoverer) externalLookup(node protocol.NodeID) []string {
return nil
}
if debug {
l.Debugf("discover: parsed external: %#v", pkt)
}
var addrs []string
for _, a := range pkt.This.Addresses {
nodeAddr := fmt.Sprintf("%s:%d", net.IP(a.IP), a.Port)
@@ -386,6 +455,21 @@ func (d *Discoverer) externalLookup(node protocol.NodeID) []string {
return addrs
}
func (d *Discoverer) filterCached(c []cacheEntry) []cacheEntry {
for i := 0; i < len(c); {
if ago := time.Since(c[i].seen); ago > d.cacheLifetime {
if debug {
l.Debugf("removing cached address %s: seen %v ago", c[i].addr, ago)
}
c[i] = c[len(c)-1]
c = c[:len(c)-1]
} else {
i++
}
}
return c
}
func addrToAddr(addr *net.TCPAddr) Address {
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
return Address{Port: uint16(addr.Port)}

View File

@@ -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 discover_test
// Empty test file to generate 0% coverage rather than no coverage

View File

@@ -8,7 +8,7 @@ import (
"os"
"strings"
"github.com/calmh/syncthing/logger"
"github.com/syncthing/syncthing/logger"
)
var (

View File

@@ -20,11 +20,12 @@ const (
NodeDiscovered
NodeConnected
NodeDisconnected
NodeRejected
LocalIndexUpdated
RemoteIndexUpdated
ItemStarted
ItemCompleted
StateChanged
RepoRejected
AllEvents = ^EventType(0)
)
@@ -43,6 +44,8 @@ func (t EventType) String() string {
return "NodeConnected"
case NodeDisconnected:
return "NodeDisconnected"
case NodeRejected:
return "NodeRejected"
case LocalIndexUpdated:
return "LocalIndexUpdated"
case RemoteIndexUpdated:
@@ -51,6 +54,8 @@ func (t EventType) String() string {
return "ItemStarted"
case StateChanged:
return "StateChanged"
case RepoRejected:
return "RepoRejected"
default:
return "Unknown"
}

View File

@@ -9,7 +9,7 @@ import (
"testing"
"time"
"github.com/calmh/syncthing/events"
"github.com/syncthing/syncthing/events"
)
var timeout = 100 * time.Millisecond

View File

@@ -8,7 +8,7 @@ import (
"os"
"strings"
"github.com/calmh/syncthing/logger"
"github.com/syncthing/syncthing/logger"
)
var (

11
files/filenames_darwin.go Normal file
View File

@@ -0,0 +1,11 @@
package files
import "code.google.com/p/go.text/unicode/norm"
func normalizedFilename(s string) string {
return norm.NFC.String(s)
}
func nativeFilename(s string) string {
return norm.NFD.String(s)
}

13
files/filenames_unix.go Normal file
View File

@@ -0,0 +1,13 @@
// +build !windows,!darwin
package files
import "code.google.com/p/go.text/unicode/norm"
func normalizedFilename(s string) string {
return norm.NFC.String(s)
}
func nativeFilename(s string) string {
return s
}

View File

@@ -0,0 +1,15 @@
package files
import (
"path/filepath"
"code.google.com/p/go.text/unicode/norm"
)
func normalizedFilename(s string) string {
return norm.NFC.String(filepath.ToSlash(s))
}
func nativeFilename(s string) string {
return filepath.FromSlash(s)
}

View File

@@ -2,11 +2,12 @@ package files
import (
"bytes"
"runtime"
"sort"
"sync"
"github.com/calmh/syncthing/lamport"
"github.com/calmh/syncthing/protocol"
"github.com/syncthing/syncthing/lamport"
"github.com/syncthing/syncthing/protocol"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/opt"
@@ -118,9 +119,11 @@ func globalKeyName(key []byte) []byte {
type deletionHandler func(db dbReader, batch dbWriter, repo, node, name []byte, dbi iterator.Iterator) uint64
type fileIterator func(f protocol.FileInfo) bool
type fileIterator func(f protocol.FileIntf) bool
func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo, deleteFn deletionHandler) uint64 {
defer runtime.GC()
sort.Sort(fileList(fs)) // sort list on name, same as on disk
start := nodeKey(repo, node, nil) // before all repo/node files
@@ -178,7 +181,7 @@ func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo
case moreFs && moreDb && cmp == 0:
// File exists on both sides - compare versions.
var ef protocol.FileInfo
var ef protocol.FileInfoTruncated
ef.UnmarshalXDR(dbi.Value())
if fs[fsi].Version > ef.Version {
if lv := ldbInsert(batch, repo, node, newName, fs[fsi]); lv > maxLocalVer {
@@ -223,20 +226,23 @@ func ldbReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint6
func ldbReplaceWithDelete(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64 {
return ldbGenericReplace(db, repo, node, fs, func(db dbReader, batch dbWriter, repo, node, name []byte, dbi iterator.Iterator) uint64 {
var f protocol.FileInfo
err := f.UnmarshalXDR(dbi.Value())
var tf protocol.FileInfoTruncated
err := tf.UnmarshalXDR(dbi.Value())
if err != nil {
panic(err)
}
if !protocol.IsDeleted(f.Flags) {
if !tf.IsDeleted() {
if debug {
l.Debugf("mark deleted; repo=%q node=%v name=%q", repo, protocol.NodeIDFromBytes(node), name)
}
ts := clock(f.LocalVersion)
f.Blocks = nil
f.Version = lamport.Default.Tick(f.Version)
f.Flags |= protocol.FlagDeleted
f.LocalVersion = ts
ts := clock(tf.LocalVersion)
f := protocol.FileInfo{
Name: tf.Name,
Version: lamport.Default.Tick(tf.Version),
LocalVersion: ts,
Flags: tf.Flags | protocol.FlagDeleted,
Modified: tf.Modified,
}
batch.Put(dbi.Key(), f.MarshalXDR())
ldbUpdateGlobal(db, batch, repo, node, nodeKeyName(dbi.Key()), f.Version)
return ts
@@ -246,6 +252,8 @@ func ldbReplaceWithDelete(db *leveldb.DB, repo, node []byte, fs []protocol.FileI
}
func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64 {
defer runtime.GC()
batch := new(leveldb.Batch)
snap, err := db.GetSnapshot()
if err != nil {
@@ -266,7 +274,7 @@ func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64
continue
}
var ef protocol.FileInfo
var ef protocol.FileInfoTruncated
err = ef.UnmarshalXDR(bs)
if err != nil {
panic(err)
@@ -390,7 +398,7 @@ func ldbRemoveFromGlobal(db dbReader, batch dbWriter, repo, node, file []byte) {
}
}
func ldbWithHave(db *leveldb.DB, repo, node []byte, fn fileIterator) {
func ldbWithHave(db *leveldb.DB, repo, node []byte, truncate bool, fn fileIterator) {
start := nodeKey(repo, node, nil) // before all repo/node files
limit := nodeKey(repo, node, []byte{0xff, 0xff, 0xff, 0xff}) // after all repo/node files
snap, err := db.GetSnapshot()
@@ -402,8 +410,7 @@ func ldbWithHave(db *leveldb.DB, repo, node []byte, fn fileIterator) {
defer dbi.Release()
for dbi.Next() {
var f protocol.FileInfo
err := f.UnmarshalXDR(dbi.Value())
f, err := unmarshalTrunc(dbi.Value(), truncate)
if err != nil {
panic(err)
}
@@ -413,7 +420,9 @@ func ldbWithHave(db *leveldb.DB, repo, node []byte, fn fileIterator) {
}
}
func ldbWithAllRepo(db *leveldb.DB, repo []byte, fn func(node []byte, f protocol.FileInfo) bool) {
func ldbWithAllRepoTruncated(db *leveldb.DB, repo []byte, fn func(node []byte, f protocol.FileInfoTruncated) bool) {
defer runtime.GC()
start := nodeKey(repo, nil, nil) // before all repo/node files
limit := nodeKey(repo, protocol.LocalNodeID[:], []byte{0xff, 0xff, 0xff, 0xff}) // after all repo/node files
snap, err := db.GetSnapshot()
@@ -426,7 +435,7 @@ func ldbWithAllRepo(db *leveldb.DB, repo []byte, fn func(node []byte, f protocol
for dbi.Next() {
node := nodeKeyNode(dbi.Key())
var f protocol.FileInfo
var f protocol.FileInfoTruncated
err := f.UnmarshalXDR(dbi.Value())
if err != nil {
panic(err)
@@ -437,40 +446,6 @@ func ldbWithAllRepo(db *leveldb.DB, repo []byte, fn func(node []byte, f protocol
}
}
/*
func ldbCheckGlobalConsistency(db *leveldb.DB, repo []byte) {
l.Debugf("Checking global consistency for %q", repo)
start := nodeKey(repo, nil, nil) // before all repo/node files
limit := nodeKey(repo, protocol.LocalNodeID[:], []byte{0xff, 0xff, 0xff, 0xff}) // after all repo/node files
snap, err := db.GetSnapshot()
if err != nil {
panic(err)
}
defer snap.Release()
dbi := snap.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
defer dbi.Release()
batch := new(leveldb.Batch)
i := 0
for dbi.Next() {
repo := nodeKeyRepo(dbi.Key())
node := nodeKeyNode(dbi.Key())
var f protocol.FileInfo
err := f.UnmarshalXDR(dbi.Value())
if err != nil {
panic(err)
}
if ldbUpdateGlobal(snap, batch, repo, node, []byte(f.Name), f.Version) {
var nodeID protocol.NodeID
copy(nodeID[:], node)
l.Debugf("fixed global for %q %s %q", repo, nodeID, f.Name)
}
i++
}
l.Debugln("Done", i)
}
*/
func ldbGet(db *leveldb.DB, repo, node, file []byte) protocol.FileInfo {
nk := nodeKey(repo, node, file)
bs, err := db.Get(nk, nil)
@@ -529,7 +504,9 @@ func ldbGetGlobal(db *leveldb.DB, repo, file []byte) protocol.FileInfo {
return f
}
func ldbWithGlobal(db *leveldb.DB, repo []byte, fn fileIterator) {
func ldbWithGlobal(db *leveldb.DB, repo []byte, truncate bool, fn fileIterator) {
defer runtime.GC()
start := globalKey(repo, nil)
limit := globalKey(repo, []byte{0xff, 0xff, 0xff, 0xff})
snap, err := db.GetSnapshot()
@@ -556,8 +533,7 @@ func ldbWithGlobal(db *leveldb.DB, repo []byte, fn fileIterator) {
panic(err)
}
var f protocol.FileInfo
err = f.UnmarshalXDR(bs)
f, err := unmarshalTrunc(bs, truncate)
if err != nil {
panic(err)
}
@@ -596,7 +572,9 @@ func ldbAvailability(db *leveldb.DB, repo, file []byte) []protocol.NodeID {
return nodes
}
func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
func ldbWithNeed(db *leveldb.DB, repo, node []byte, truncate bool, fn fileIterator) {
defer runtime.GC()
start := globalKey(repo, nil)
limit := globalKey(repo, []byte{0xff, 0xff, 0xff, 0xff})
snap, err := db.GetSnapshot()
@@ -638,13 +616,12 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
panic(err)
}
var gf protocol.FileInfo
err = gf.UnmarshalXDR(bs)
gf, err := unmarshalTrunc(bs, truncate)
if err != nil {
panic(err)
}
if protocol.IsDeleted(gf.Flags) && !have {
if gf.IsDeleted() && !have {
// We don't need deleted files that we don't have
continue
}
@@ -659,3 +636,15 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
}
}
}
func unmarshalTrunc(bs []byte, truncate bool) (protocol.FileIntf, error) {
if truncate {
var tf protocol.FileInfoTruncated
err := tf.UnmarshalXDR(bs)
return tf, err
} else {
var tf protocol.FileInfo
err := tf.UnmarshalXDR(bs)
return tf, err
}
}

View File

@@ -2,14 +2,19 @@
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// Package files provides a set type to track local/remote files with newness checks.
// Package files provides a set type to track local/remote files with newness
// checks. We must do a certain amount of normalization in here. We will get
// fed paths with either native or wire-format separators and encodings
// depending on who calls us. We transform paths to wire-format (NFC and
// slashes) on the way to the database, and transform to native format
// (varying separator and encoding) on the way back out.
package files
import (
"sync"
"github.com/calmh/syncthing/lamport"
"github.com/calmh/syncthing/protocol"
"github.com/syncthing/syncthing/lamport"
"github.com/syncthing/syncthing/protocol"
"github.com/syndtr/goleveldb/leveldb"
)
@@ -36,7 +41,7 @@ func NewSet(repo string, db *leveldb.DB) *Set {
}
var nodeID protocol.NodeID
ldbWithAllRepo(db, []byte(repo), func(node []byte, f protocol.FileInfo) bool {
ldbWithAllRepoTruncated(db, []byte(repo), func(node []byte, f protocol.FileInfoTruncated) bool {
copy(nodeID[:], node)
if f.LocalVersion > s.localVersion[nodeID] {
s.localVersion[nodeID] = f.LocalVersion
@@ -56,6 +61,7 @@ func (s *Set) Replace(node protocol.NodeID, fs []protocol.FileInfo) {
if debug {
l.Debugf("%s Replace(%v, [%d])", s.repo, node, len(fs))
}
normalizeFilenames(fs)
s.mutex.Lock()
defer s.mutex.Unlock()
s.localVersion[node] = ldbReplace(s.db, []byte(s.repo), node[:], fs)
@@ -65,6 +71,7 @@ func (s *Set) ReplaceWithDelete(node protocol.NodeID, fs []protocol.FileInfo) {
if debug {
l.Debugf("%s ReplaceWithDelete(%v, [%d])", s.repo, node, len(fs))
}
normalizeFilenames(fs)
s.mutex.Lock()
defer s.mutex.Unlock()
if lv := ldbReplaceWithDelete(s.db, []byte(s.repo), node[:], fs); lv > s.localVersion[node] {
@@ -76,6 +83,7 @@ func (s *Set) Update(node protocol.NodeID, fs []protocol.FileInfo) {
if debug {
l.Debugf("%s Update(%v, [%d])", s.repo, node, len(fs))
}
normalizeFilenames(fs)
s.mutex.Lock()
defer s.mutex.Unlock()
if lv := ldbUpdate(s.db, []byte(s.repo), node[:], fs); lv > s.localVersion[node] {
@@ -87,33 +95,58 @@ func (s *Set) WithNeed(node protocol.NodeID, fn fileIterator) {
if debug {
l.Debugf("%s WithNeed(%v)", s.repo, node)
}
ldbWithNeed(s.db, []byte(s.repo), node[:], fn)
ldbWithNeed(s.db, []byte(s.repo), node[:], false, nativeFileIterator(fn))
}
func (s *Set) WithNeedTruncated(node protocol.NodeID, fn fileIterator) {
if debug {
l.Debugf("%s WithNeedTruncated(%v)", s.repo, node)
}
ldbWithNeed(s.db, []byte(s.repo), node[:], true, nativeFileIterator(fn))
}
func (s *Set) WithHave(node protocol.NodeID, fn fileIterator) {
if debug {
l.Debugf("%s WithHave(%v)", s.repo, node)
}
ldbWithHave(s.db, []byte(s.repo), node[:], fn)
ldbWithHave(s.db, []byte(s.repo), node[:], false, nativeFileIterator(fn))
}
func (s *Set) WithHaveTruncated(node protocol.NodeID, fn fileIterator) {
if debug {
l.Debugf("%s WithHaveTruncated(%v)", s.repo, node)
}
ldbWithHave(s.db, []byte(s.repo), node[:], true, nativeFileIterator(fn))
}
func (s *Set) WithGlobal(fn fileIterator) {
if debug {
l.Debugf("%s WithGlobal()", s.repo)
}
ldbWithGlobal(s.db, []byte(s.repo), fn)
ldbWithGlobal(s.db, []byte(s.repo), false, nativeFileIterator(fn))
}
func (s *Set) WithGlobalTruncated(fn fileIterator) {
if debug {
l.Debugf("%s WithGlobalTruncated()", s.repo)
}
ldbWithGlobal(s.db, []byte(s.repo), true, nativeFileIterator(fn))
}
func (s *Set) Get(node protocol.NodeID, file string) protocol.FileInfo {
return ldbGet(s.db, []byte(s.repo), node[:], []byte(file))
f := ldbGet(s.db, []byte(s.repo), node[:], []byte(normalizedFilename(file)))
f.Name = nativeFilename(f.Name)
return f
}
func (s *Set) GetGlobal(file string) protocol.FileInfo {
return ldbGetGlobal(s.db, []byte(s.repo), []byte(file))
f := ldbGetGlobal(s.db, []byte(s.repo), []byte(normalizedFilename(file)))
f.Name = nativeFilename(f.Name)
return f
}
func (s *Set) Availability(file string) []protocol.NodeID {
return ldbAvailability(s.db, []byte(s.repo), []byte(file))
return ldbAvailability(s.db, []byte(s.repo), []byte(normalizedFilename(file)))
}
func (s *Set) LocalVersion(node protocol.NodeID) uint64 {
@@ -121,3 +154,24 @@ func (s *Set) LocalVersion(node protocol.NodeID) uint64 {
defer s.mutex.Unlock()
return s.localVersion[node]
}
func normalizeFilenames(fs []protocol.FileInfo) {
for i := range fs {
fs[i].Name = normalizedFilename(fs[i].Name)
}
}
func nativeFileIterator(fn fileIterator) fileIterator {
return func(fi protocol.FileIntf) bool {
switch f := fi.(type) {
case protocol.FileInfo:
f.Name = nativeFilename(f.Name)
return fn(f)
case protocol.FileInfoTruncated:
f.Name = nativeFilename(f.Name)
return fn(f)
default:
panic("unknown interface type")
}
}
}

View File

@@ -9,9 +9,9 @@ import (
"sort"
"testing"
"github.com/calmh/syncthing/files"
"github.com/calmh/syncthing/lamport"
"github.com/calmh/syncthing/protocol"
"github.com/syncthing/syncthing/files"
"github.com/syncthing/syncthing/lamport"
"github.com/syncthing/syncthing/protocol"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage"
)
@@ -37,7 +37,8 @@ func genBlocks(n int) []protocol.BlockInfo {
func globalList(s *files.Set) []protocol.FileInfo {
var fs []protocol.FileInfo
s.WithGlobal(func(f protocol.FileInfo) bool {
s.WithGlobal(func(fi protocol.FileIntf) bool {
f := fi.(protocol.FileInfo)
fs = append(fs, f)
return true
})
@@ -46,7 +47,8 @@ func globalList(s *files.Set) []protocol.FileInfo {
func haveList(s *files.Set, n protocol.NodeID) []protocol.FileInfo {
var fs []protocol.FileInfo
s.WithHave(n, func(f protocol.FileInfo) bool {
s.WithHave(n, func(fi protocol.FileIntf) bool {
f := fi.(protocol.FileInfo)
fs = append(fs, f)
return true
})
@@ -55,7 +57,8 @@ func haveList(s *files.Set, n protocol.NodeID) []protocol.FileInfo {
func needList(s *files.Set, n protocol.NodeID) []protocol.FileInfo {
var fs []protocol.FileInfo
s.WithNeed(n, func(f protocol.FileInfo) bool {
s.WithNeed(n, func(fi protocol.FileIntf) bool {
f := fi.(protocol.FileInfo)
fs = append(fs, f)
return true
})
@@ -592,3 +595,64 @@ func TestLocalVersion(t *testing.T) {
t.Fatal("Local version number should be unchanged")
}
}
/*
var gf protocol.FileInfo
func TestStressGlobalVersion(t *testing.T) {
dur := 15 * time.Second
if testing.Short() {
dur = 1 * time.Second
}
set1 := []protocol.FileInfo{
protocol.FileInfo{Name: "a", Version: 1000},
protocol.FileInfo{Name: "b", Version: 1000},
}
set2 := []protocol.FileInfo{
protocol.FileInfo{Name: "b", Version: 1001},
protocol.FileInfo{Name: "c", Version: 1000},
}
db, err := leveldb.OpenFile("testdata/global.db", nil)
if err != nil {
t.Fatal(err)
}
m := files.NewSet("test", db)
done := make(chan struct{})
go stressWriter(m, remoteNode, set1, nil, done)
go stressWriter(m, protocol.LocalNodeID, set2, nil, done)
t0 := time.Now()
for time.Since(t0) < dur {
m.WithGlobal(func(f protocol.FileInfo) bool {
gf = f
return true
})
}
close(done)
}
func stressWriter(s *files.Set, id protocol.NodeID, set1, set2 []protocol.FileInfo, done chan struct{}) {
one := true
i := 0
for {
select {
case <-done:
return
default:
if one {
s.Replace(id, set1)
} else {
s.Replace(id, set2)
}
one = !one
}
i++
}
}
*/

View File

@@ -25,6 +25,10 @@ syncthing.controller('EventCtrl', function ($scope, $http) {
var online = false;
var lastID = 0;
$(window).bind('beforeunload', function() {
online = false;
});
var successFn = function (data) {
if (!online) {
$scope.$emit('UIOnline');
@@ -86,11 +90,19 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.upgradeInfo = {};
$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++) {
lang = langs[i];
if (validLangs.indexOf(lang) >= 0) {
$translate.use(lang);
matching = validLangs.filter(function (l) {
return lang.length >= 2 && l.indexOf(lang) == 0;
});
if (matching.length >= 1) {
$translate.use(matching[0]);
break;
}
}
@@ -128,7 +140,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.$on('UIOffline', function (event, arg) {
console.log('UIOffline');
if (!restarting) {
$('#networkError').modal({backdrop: 'static', keyboard: false});
$('#networkError').modal();
}
});
@@ -168,6 +180,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
OutBytesTotal: 0,
Address: arg.data.addr,
};
$scope.completion[arg.data.id] = {
_total: 100,
};
}
});
@@ -185,7 +200,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
} else {
if (+firstVisit < Date.now() - 4*3600*1000){
$('#ur').modal({backdrop: 'static', keyboard: false});
$('#ur').modal();
}
}
}
@@ -284,6 +299,11 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
$scope.nodes = $scope.config.Nodes;
$scope.nodes.forEach(function (nodeCfg) {
$scope.completion[nodeCfg.NodeID] = {
_total: 100,
};
});
$scope.nodes.sort(nodeCompare);
$scope.repos = repoMap($scope.config.Repositories);
@@ -463,7 +483,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.tmpOptions = angular.copy($scope.config.Options);
$scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
$scope.tmpGUI = angular.copy($scope.config.GUI);
$('#settings').modal({backdrop: 'static', keyboard: true});
$('#settings').modal();
};
$scope.saveConfig = function() {
@@ -506,9 +526,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.restart = function () {
restarting = true;
$scope.restartingTitle = "Restarting"
$scope.restartingBody = "Syncthing is restarting."
$('#restarting').modal({backdrop: 'static', keyboard: false});
$('#restarting').modal();
$http.post(urlbase + '/restart');
$scope.configInSync = true;
@@ -529,21 +547,20 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
};
$scope.upgrade = function () {
$scope.restartingTitle = "Upgrading"
$scope.restartingBody = "Syncthing is upgrading."
$('#restarting').modal({backdrop: 'static', keyboard: false});
restarting = true;
$('#upgrading').modal();
$http.post(urlbase + '/upgrade').success(function () {
restarting = true;
$scope.restartingBody = "Syncthing is restarting into the new version."
$('#restarting').modal();
$('#upgrading').modal('hide');
}).error(function () {
$('#restarting').modal('hide');
$('#upgrading').modal('hide');
});
};
$scope.shutdown = function () {
restarting = true;
$http.post(urlbase + '/shutdown').success(function () {
$('#shutdown').modal({backdrop: 'static', keyboard: false});
$('#shutdown').modal();
});
$scope.configInSync = true;
};
@@ -554,7 +571,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.editingSelf = (nodeCfg.NodeID == $scope.myID);
$scope.currentNode.AddressesStr = nodeCfg.Addresses.join(', ');
$scope.nodeEditor.$setPristine();
$('#editNode').modal({backdrop: 'static', keyboard: true});
$('#editNode').modal();
};
$scope.idNode = function () {
@@ -566,7 +583,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.editingExisting = false;
$scope.editingSelf = false;
$scope.nodeEditor.$setPristine();
$('#editNode').modal({backdrop: 'static', keyboard: true});
$('#editNode').modal();
};
$scope.deleteNode = function () {
@@ -669,19 +686,37 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
});
if ($scope.currentRepo.Versioning && $scope.currentRepo.Versioning.Type === "simple") {
$scope.currentRepo.simpleFileVersioning = true;
$scope.currentRepo.FileVersioningSelector = "simple";
$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.staggeredMaxAge = $scope.currentRepo.staggeredMaxAge || 365;
$scope.currentRepo.staggeredCleanInterval = $scope.currentRepo.staggeredCleanInterval || 3600;
$scope.currentRepo.staggeredVersionsPath = $scope.currentRepo.staggeredVersionsPath || "";
$scope.editingExisting = true;
$scope.repoEditor.$setPristine();
$('#editRepo').modal({backdrop: 'static', keyboard: true});
$('#editRepo').modal();
};
$scope.addRepo = function () {
$scope.currentRepo = {selectedNodes: {}};
$scope.currentRepo.FileVersioningSelector = "none";
$scope.currentRepo.simpleKeep = 5;
$scope.currentRepo.staggeredMaxAge = 365;
$scope.currentRepo.staggeredCleanInterval = 3600;
$scope.currentRepo.staggeredVersionsPath = "";
$scope.editingExisting = false;
$scope.repoEditor.$setPristine();
$('#editRepo').modal({backdrop: 'static', keyboard: true});
$('#editRepo').modal();
};
$scope.saveRepo = function () {
@@ -698,7 +733,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
}
delete repoCfg.selectedNodes;
if (repoCfg.simpleFileVersioning) {
if (repoCfg.FileVersioningSelector === "simple") {
repoCfg.Versioning = {
'Type': 'simple',
'Params': {
@@ -707,6 +742,20 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
};
delete repoCfg.simpleFileVersioning;
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 {
delete repoCfg.Versioning;
}
@@ -742,8 +791,6 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
cfg.APIKey = randomString(30, 32);
};
$scope.acceptUR = function () {
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
$scope.saveConfig();
@@ -758,7 +805,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.showNeed = function (repo) {
$scope.neededLoaded = false;
$('#needed').modal({backdrop: 'static', keyboard: true});
$('#needed').modal();
$http.get(urlbase + "/need?repo=" + encodeURIComponent(repo)).success(function (data) {
$scope.needed = data;
$scope.neededLoaded = true;
@@ -781,15 +828,21 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
};
$scope.override = function (repo) {
$http.post(urlbase + "/model/override?repo=" + encodeURIComponent(repo)).success(function () {
$scope.refresh();
});
$http.post(urlbase + "/model/override?repo=" + encodeURIComponent(repo));
};
$scope.about = function () {
$('#about').modal('show');
};
$scope.showReportPreview = function () {
$scope.reportPreview = true;
};
$scope.rescanRepo = function (repo) {
$http.post(urlbase + "/scan?repo=" + encodeURIComponent(repo));
};
$scope.init();
setInterval($scope.refresh, 10000);
});
@@ -1035,7 +1088,6 @@ syncthing.directive('validNodeid', function($http) {
if (resp.error) {
ctrl.$setValidity('validNodeid', false);
} else {
scope.currentNode.NodeID = resp.id;
ctrl.$setValidity('validNodeid', true);
}
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -15,12 +15,17 @@
<title>Syncthing | {{thisNodeName()}}</title>
<link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="raleway.css" rel="stylesheet">
<style type="text/css">
body {
padding-bottom: 70px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
h1, h2, h3, h4, h5 {
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
ul+h5 {
margin-top: 1.5em;
}
@@ -95,7 +100,8 @@
<nav class="navbar navbar-top navbar-default" role="navigation">
<div class="container">
<span class="navbar-brand"><img class="logo" src="st-logo-128.png" width="32" height="32" /> Syncthing<small class="hidden-xs"> <span class="text-muted">|</span> {{thisNodeName()}}</small></span>
<span class="navbar-brand"><img class="logo" src="logo-text-64.png" height="32" width="117"/></span>
<p class="navbar-text hidden-xs">{{thisNodeName()}}</p>
<ul class="nav navbar-nav navbar-right">
<li ng-if="upgradeInfo.newer">
<button type="button" class="btn navbar-btn btn-default" href="" ng-click="upgrade()">
@@ -153,7 +159,7 @@
<div class="panel-heading">
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}">
<span class="glyphicon glyphicon-hdd"></span> {{repo.Directory | shortPath}}
<span class="glyphicon glyphicon-hdd"></span> {{repo.ID}}
<span class="pull-right hidden-xs" ng-switch="repoStatus(repo.ID)">
<span translate ng-switch-when="unknown">Unknown</span>
<span translate ng-switch-when="stopped">Stopped</span>
@@ -189,16 +195,16 @@
</tr>
<tr>
<th><span class="glyphicon glyphicon-globe"></span>&emsp;<span translate>Global Repository</span></th>
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} <span translate>items</span>, {{model[repo.ID].globalBytes | binary}}B</td>
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].globalBytes | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-home"></span>&emsp;<span translate>Local Repository</span></th>
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} <span translate>items</span>, {{model[repo.ID].localBytes | binary}}B</td>
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].localBytes | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Out Of Sync</span></th>
<td class="text-right">
<a ng-if="model[repo.ID].needFiles > 0" ng-click="showNeed(repo.ID)" href="">{{model[repo.ID].needFiles | alwaysNumber}} <span translate>items</span>, {{model[repo.ID].needBytes | binary}}B</a>
<a ng-if="model[repo.ID].needFiles > 0" ng-click="showNeed(repo.ID)" href="">{{model[repo.ID].needFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].needBytes | binary}}B</a>
<span ng-if="model[repo.ID].needFiles == 0">0 <span translate>items</span>, 0 B</span>
</td>
</tr>
@@ -216,6 +222,10 @@
<span translate ng-if="!repo.IgnorePerms">No</span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan Interval</span></th>
<td class="text-right">{{repo.RescanIntervalS}} s</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-share-alt"></span>&emsp;<span translate>Shared With</span></th>
<td class="text-right">{{sharesRepo(repo)}}</td>
@@ -224,6 +234,7 @@
</table>
</div>
<span class="pull-right">
<a class="btn btn-sm btn-default" href="" ng-show="repoStatus(repo.ID) == 'idle'" ng-click="rescanRepo(repo.ID)"><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan</span></a>
<a class="btn btn-sm btn-primary" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span>&emsp;<span translate>Edit</span></a>
<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>&emsp;<span translate>Override Changes</span></a>
</span>
@@ -367,10 +378,10 @@
<div class="container">
<ul class="nav navbar-nav">
<li><a class="navbar-link" href="http://discourse.syncthing.net/"><span translate>Support / Forum</span></a></li>
<li><a class="navbar-link" href="https://github.com/calmh/syncthing/releases"><span translate>Latest Release</span></a></li>
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/releases"><span translate>Latest Release</span></a></li>
<li><a class="navbar-link" href="http://discourse.syncthing.net/category/documentation"><span translate>Documentation</span></a></li>
<li><a class="navbar-link" href="https://github.com/calmh/syncthing/issues"><span translate>Bugs</span></a></li>
<li><a class="navbar-link" href="https://github.com/calmh/syncthing"><span translate>Source Code</span></a></li>
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/issues"><span translate>Bugs</span></a></li>
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing"><span translate>Source Code</span></a></li>
</ul>
</div>
</nav>
@@ -385,8 +396,14 @@
<!-- Restarting modal -->
<modal id="restarting" icon="refresh" title="{{restartingTitle}}" status="info">
<p>{{restartingBody}} <span translate>Please wait</span>&hellip;</p>
<modal id="restarting" icon="refresh" title="{{'Restarting' | translate}}" status="info">
<p><span translate>Syncthing is restarting.</span> <span translate>Please wait</span>...</p>
</modal>
<!-- Upgrading modal -->
<modal id="upgrading" icon="refresh" title="{{'Upgrading' | translate}}" status="info">
<p><span translate>Syncthing is upgrading.</span> <span translate>Please wait</span>...</p>
</modal>
<!-- Shutdown modal -->
@@ -399,12 +416,12 @@
<modal id="idqr" large="yes" status="info" close="yes" icon="qrcode" title="{{'Node Identification' | translate}} &mdash; {{nodeName(thisNode())}}">
<div class="well well-sm text-monospace text-center">{{myID}}</div>
<img ng-if="myID" class="center-block img-thumbnail" src="qr/{{myID}}"/>
<img ng-if="myID" class="center-block img-thumbnail" src="qr/?text={{myID}}"/>
</modal>
<!-- Node editor modal -->
<div id="editNode" class="modal fade">
<div id="editNode" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
@@ -421,13 +438,14 @@
<span translate ng-if="nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine">The node ID to enter here can be found in the "Edit > Show ID" dialog on the other node. Spaces and dashes are optional (ignored).</span>
<span translate ng-show="!editingExisting && (nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine)">When adding a new node, keep in mind that this node must be added on the other side too.</span>
<span translate ng-if="nodeEditor.nodeID.$error.required && nodeEditor.nodeID.$dirty">The node ID cannot be blank.</span>
<span translate ng-if="nodeEditor.nodeID.$error.validNodeid && nodeEditor.nodeID.$dirty">The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.</span>
<span translate ng-if="nodeEditor.nodeID.$error.validNodeid && nodeEditor.nodeID.$dirty">The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.</span>
</p>
</div>
<div class="form-group">
<label translate for="name">Node Name</label>
<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 class="form-group">
<label translate for="addresses">Addresses</label>
@@ -454,7 +472,7 @@
<!-- Repo editor modal -->
<div id="editRepo" class="modal fade">
<div id="editRepo" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
@@ -483,6 +501,13 @@
<span translate ng-if="repoEditor.repoPath.$error.required && repoEditor.repoPath.$dirty">The repository path cannot be blank.</span>
</p>
</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 class="row">
@@ -515,14 +540,25 @@
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label translate>File Versioning</label>
<div class="radio">
<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>
</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 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>
<input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentRepo.simpleKeep" required min="1"></input>
<p class="help-block">
@@ -531,7 +567,21 @@
<span translate ng-if="repoEditor.simpleKeep.$error.min && repoEditor.simpleKeep.$dirty">You must keep at least one version.</span>
</p>
</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>
</form>
@@ -548,7 +598,7 @@
<!-- Settings modal -->
<div id="settings" class="modal fade">
<div id="settings" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
@@ -567,10 +617,6 @@
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
<input id="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.MaxSendKbps">
</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>
@@ -600,10 +646,6 @@
</label>
</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="checkbox">
<label>
@@ -674,7 +716,7 @@
<!-- Usage report modal -->
<div id="ur" class="modal fade">
<div id="ur" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header alert alert-success">
@@ -682,8 +724,8 @@
</div>
<div class="modal-body">
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
<p translate translate-value-url="https://data.syncthing.net">The aggregated statistics are publicly available at {{url}}</p>
<button translate type="button" class="btn btn-default" ng-show="!reportPreview" ng-click="reportPreview = true">Preview Usage Report</button>
<p translate translate-value-url="https://data.syncthing.net">The aggregated statistics are publicly available at {%url%}.</p>
<button translate type="button" class="btn btn-default" ng-show="!reportPreview" ng-click="showReportPreview()">Preview Usage Report</button>
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
</div>
<div class="modal-footer">
@@ -709,7 +751,7 @@
<!-- About modal -->
<modal id="about" large="yes" close="yes" status="info" title="About">
<h1 class="text-center"><img src="st-logo-128.png" style="vertical-align: -16px" width="64" height="64"/> Syncthing<br/><small>{{version}}</small></h1>
<h1 class="text-center"><img alt="Syncthing" title="Syncthing" src="logo-text-256.png" style="vertical-align: -16px" height="100" width="366"/><br/><small>{{version}}</small></h1>
<hr/>
<p translate>Copyright &copy; 2014 Jakob Borg and the following Contributors:</p>
@@ -718,18 +760,22 @@
<ul>
<li>Aaron Bieber</li>
<li>Andrew Dunham</li>
<li>Alexander Graf</li>
<li>Arthur Axel fREW Schmidt</li>
<li>Audrius Butkevicius</li>
<li>Ben Sidhom</li>
<li>Brandon Philips</li>
<li>Gilli Sigurdsson</li>
</ul>
</div>
<div class="col-md-6">
<ul>
<li>James Patterson</li>
<li>Jens Diemer</li>
<li>Marcin Dziadus</li>
<li>Philippe Schommers</li>
<li>Ryan Sullivan</li>
<li>Tully Robinson</li>
<li>Veeti Paananen</li>
</ul>
</div>
@@ -738,7 +784,7 @@
<p translate>Syncthing includes the following software or portions thereof:</p>
<ul>
<li><a href="http://golang.org/">The Go Programming Languange</a>, Copyright &copy; 2012 The Go Authors.</li>
<li><a href="http://golang.org/">The Go Programming Language</a>, Copyright &copy; 2012 The Go Authors.</li>
<li><a href="https://bitbucket.org/kardianos/osext">kardianos/osext</a>, Copyright &copy; 2012 Daniel Theophanes.</li>
<li><a href="https://code.google.com/p/snappy-go/">snappy-go</a>, Copyright &copy; 2011 The Snappy-Go Authors.</li>
<li><a href="https://github.com/golang/groupcache">groupcache/lru</a>, Copyright &copy; 2013 Google Inc.</li>

123
gui/lang-da.json Normal file
View File

@@ -0,0 +1,123 @@
{
"API Key": "API-nøgle",
"About": "Om",
"Add Node": "Tilføj node",
"Add Repository": "Tilføj lager",
"Address": "Adresse",
"Addresses": "Adresser",
"Allow Anonymous Usage Reporting?": "Tillad anonym brugerstatistik?",
"Announce Server": "Opslagsserver",
"Anonymous Usage Reporting": "Anonym brugerstatistik",
"Bugs": "Fejl",
"CPU Utilization": "CPU-forbrug",
"Close": "Luk",
"Connection Error": "Tilslutnings fejl",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg og følgende bidragsydere:",
"Delete": "Slet",
"Disconnected": "Ikke tilsluttet",
"Documentation": "Dokumentation",
"Download Rate": "Downloadhastighed",
"Edit": "Rediger",
"Edit Node": "Rediger node",
"Edit Repository": "Rediger lager",
"Enable UPnP": "Anvend UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv kommaseparerat \"ip:port\"-adresser eller ordet \"dynamic\" for at benytte automatisk opslag.",
"Error": "Fejl",
"File Versioning": "Filversionering",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Filrettigheder tages der ikke hensyn til ved synkronisering. Anvend på FAT-filsystemer.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Filer flyttes til et datostemplet versionsnavn i et .stversions-bibliotek, når de bliver opdateret eller slettet af syncthing.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Filer beskyttes mod ændringer fra andre noder, men ændringer på denne node bliver sendt til hele clusteret.",
"Folder": "Bibliotek",
"GUI Authentication Password": "GUI-kodeord",
"GUI Authentication User": "GUI-brugernavn",
"GUI Listen Addresses": "GUI-lytteadresse",
"Generate": "Opret",
"Global Discovery": "Globalt opslag",
"Global Discovery Server": "Global opslagsserver",
"Global Repository": "Global lagring",
"Idle": "Inaktiv",
"Ignore Permissions": "Ignorér filrettigheder",
"Keep Versions": "Behold versioner",
"Latest Release": "Seneste udgivelse",
"Local Discovery": "Lokal opslag",
"Local Discovery Port": "Lokal opslagsport",
"Local Repository": "Lokal lagring",
"Master Repo": "Hovedlagring",
"Max File Change Rate (KiB/s)": "Højeste filændringshastighed (KiB/s)",
"Max Outstanding Requests": "Parallelitet",
"No": "Nej",
"Node ID": "Node ID",
"Node Identification": "Node identifikation",
"Node Name": "Nodenavn",
"Notice": "OBS",
"OK": "OK",
"Offline": "Offline",
"Online": "Online",
"Out Of Sync": "Ude af sync",
"Outgoing Rate Limit (KiB/s)": "Udgående hastighedsbegrænsning (KiB/s)",
"Override Changes": "Overskriv ændringer",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sti til lagring på din lokale computer. Hvis biblioteket ikke findes vil det blive oprettet. Tegnet tilde (~) kan bruges som genvej til",
"Please wait": "Vent venligst",
"Preview Usage Report": "Forhåndsvisning af forbrugsrapport",
"RAM Utilization": "RAM-forbrug",
"Reconnect Interval (s)": "Gentilslutningsinterval (s)",
"Repository ID": "Lagrings-ID",
"Repository Master": "Hovedlagring",
"Repository Path": "Sti til lagring",
"Rescan": "Rescan",
"Rescan Interval (s)": "Genscanningsinterval (s)",
"Restart": "Genstart",
"Restart Needed": "Programmet kræver genstart",
"Restarting": "Genstarter",
"Save": "Gem",
"Scanning": "Opdaterer",
"Select the nodes to share this repository with.": "Vælg hvilke noder denne lagring skal deles med.",
"Settings": "Indstillinger",
"Share With Nodes": "Del med noderne",
"Shared With": "Delt med",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Kort identifikation for denne lagring. Skal være ens på alle noder i clusteret.",
"Show ID": "Vis ID",
"Shown instead of Node ID in the cluster status.": "Vises i stedet for node-ID under clusterstatus.",
"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",
"Source Code": "Kildekode",
"Start Browser": "Start browser",
"Stopped": "Stoppet",
"Support / Forum": "Support / Forum",
"Sync Protocol Listen Addresses": "Lytteadresser for indgående forbindelser",
"Synchronization": "Synkronisering",
"Syncing": "Synkroniserer",
"Syncthing has been shut down.": "Syncthing er blevet lukket ned.",
"Syncthing includes the following software or portions thereof:": "Syncthing indeholder følgende software eller dele heraf:",
"Syncthing is restarting.": "Syncthing genstarter",
"Syncthing is upgrading.": "Syncthing opgradere",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ud til at være stoppet eller oplever problemer med din internetforbindels. Prøver igen...",
"The aggregated statistics are publicly available at {%url%}.": "Samlet statistik er offentligt tilgængelig på {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen er gemt, men ikke aktiveret. Syncthing skal genstarte for at aktivere den nye konfiguration.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterede forbrugsrapport sendes dagligt. Den benyttes til at spore anvendte platforme, lagringsstørrelser og versioner. Hvis det typen af opsamlet data ændres på et senere tidspunkt, vil du blive spurgt om tilladelse igen.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det indtastede node ID ser ikke gyldigt ud. Det skal være en 52 tegn streng, bestående af tal og bogstaver, eventuelt med mellemrum og bindestreger.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det indtastede node ID ser ikke gyldigt ud. Det skal være en 52 eller 56 tegn streng, bestående af tal og bogstaver, eventuelt med mellemrum og bindestreger.",
"The node ID cannot be blank.": "Node-ID'et kan ikke være blankt.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Node-ID'et som skal bruges her, kan du finde i \"Rediger > Vis ID\"-dialogen på den anden node. Mellemrum og bindestreg er valgfri (ignoreres).",
"The number of old versions to keep, per file.": "Antallet af gamle versioner som gemmes, per fil.",
"The number of versions must be a number and cannot be blank.": "Antallet af versioner skal være et tal, og kan ikke være blankt.",
"The repository ID cannot be blank.": "Lagrings-ID kan ikke være blankt.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Lagrings-ID'et skal være en kort identificierende streng (64 karaktere eller mindre) bestående af bogstav-, tal-, punktum- (.), bindestreg- (-) og understregskaraktere (_).",
"The repository ID must be unique.": "Lagrings-ID'et skal være unikt.",
"The repository path cannot be blank.": "Lagringsstien kan ikke være blank.",
"Unknown": "Ukendt",
"Up to Date": "Fuldt opdateret",
"Upgrade To {%version%}": "Opgradér til {{version}}",
"Upgrading": "Opgradere",
"Upload Rate": "Uploadhastighed",
"Usage": "Forbrug",
"Use Compression": "Anvend komprimering",
"Use HTTPS for GUI": "Anvend HTTPS til GUI adgang",
"Version": "Version",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Når du tilføjer en ny node skal du huske, at den også skal tilføjes på den anden side.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Når du tilføjer en ny node skal du huske, at lagrings-ID'et bliver brugt til at knytte noder sammen. De er følsomme for store og små bogstaver og skal matche på alle noder.",
"Yes": "Ja",
"You must keep at least one version.": "Du skal beholde mindst én version.",
"items": "poster"
}

View File

@@ -1,6 +1,6 @@
{
"API Key": "API-Schlüssel",
"About": "Über",
"API Key": "API-Key",
"About": "Über Syncthing",
"Add Node": "Knoten hinzufügen",
"Add Repository": "Verzeichnis hinzufügen",
"Address": "Adresse",
@@ -11,6 +11,7 @@
"Bugs": "Fehler",
"CPU Utilization": "Prozessorauslastung",
"Close": "Schließen",
"Connection Error": "Verbindungsfehler",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg und folgende Unterstützer:",
"Delete": "Löschen",
"Disconnected": "Verbindung getrennt",
@@ -19,18 +20,18 @@
"Edit": "Bearbeiten",
"Edit Node": "Knoten bearbeiten",
"Edit Repository": "Verzeichnis ändern",
"Enable UPnP": "Aktiviere UPnP",
"Enable UPnP": "UPnP aktivieren",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Trage durch ein Komma getrennte \"IP:Port\" Adressen oder \"dynamic\" ein um automatische Adresserkennung durchzuführen.",
"Error": "Fehler",
"File Versioning": "Dateiversionierung",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Dateizugriffsrechte beim Suchen nach Veränderungen ignorieren. Bei FAT-Dateisystemen verwenden.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Dateien werden beim Löschen oder Ersetzen als datierte Versionen in einen .stversions -Ordner verschoben.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Dateien werden, bevor syncthing sie löscht oder ersetzt, als datierte Versionen in einen Ordner names .stversions verschoben.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Dateien sind vor Veränderung durch andere Knoten geschützt, auf diesem Knoten durchgeführte Veränderungen werden aber auf den Rest des Netzwerks übertragen.",
"Folder": "Ordner",
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
"GUI Authentication User": "Nutzername für Zugang zur Benutzeroberfläche.",
"GUI Listen Addresses": "Adresse(n) für die Benutzeroberfläche",
"Generate": "Generiere",
"Generate": "Generieren",
"Global Discovery": "Globale Auffindung",
"Global Discovery Server": "Globaler Auffindungsserver",
"Global Repository": "Globales Verzeichnis",
@@ -41,11 +42,12 @@
"Local Discovery": "Lokale Auffindung",
"Local Discovery Port": "Lokaler Aufindungsport",
"Local Repository": "Lokales Verzeichnis",
"Master Repo": "Keine Veränderungen zugelassen",
"Master Repo": "Originalverzeichnis",
"Max File Change Rate (KiB/s)": "Maximale Datenänderungsrate (KiB/s)",
"Max Outstanding Requests": "Max. ausstehende Anfragen",
"No": "Nein",
"Node ID": "Knoten-ID",
"Node Identification": "Knoten Identifikation",
"Node Name": "Knoten-Name",
"Notice": "Benachrichtigung",
"OK": "Ok",
@@ -60,13 +62,15 @@
"RAM Utilization": "Verwendeter Arbeitsspeicher",
"Reconnect Interval (s)": "Wiederverbindungsintervall (s)",
"Repository ID": "Verzeichnis-ID",
"Repository Master": "Keine Veränderungen zulassen",
"Repository Master": "Originalverzeichnis",
"Repository Path": "Pfad zum Verzeichnis",
"Rescan": "Erneut suchen",
"Rescan Interval (s)": "Suchintervall (s)",
"Restart": "Neustart",
"Restart Needed": "Neustart notwendig",
"Restarting": "Wird neu gestartet",
"Save": "Speichern",
"Scanning": "Überprüfe",
"Scanning": "Sucht",
"Select the nodes to share this repository with.": "Wähle die Knoten aus, mit denen du dieses Verzeichnis teilen willst.",
"Settings": "Einstellungen",
"Share With Nodes": "Teile mit diesen Knoten",
@@ -74,8 +78,10 @@
"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",
"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",
"Source Code": "Quellcode",
"Source Code": "Sourcecode",
"Start Browser": "Starte Browser",
"Stopped": "Gestoppt",
"Support / Forum": "Support / Forum",
@@ -84,15 +90,18 @@
"Syncing": "Synchronisiere",
"Syncthing has been shut down.": "Syncthing wurde heruntergefahren.",
"Syncthing includes the following software or portions thereof:": "Syncthing enthält die folgende Software oder Teile davon:",
"Syncthing is restarting.": "Syncthing wird neu gestartet",
"Syncthing is upgrading.": "Syncthing wird aktualisiert",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Ihrer Internetverbindung. Versuche erneut...",
"The aggregated statistics are publicly available at {{url}}": "Die aggregierten Statistiken sind öffentlich verfügbar unter {{url}}",
"The aggregated statistics are publicly available at {%url%}.": "Die gesammelten Statistiken sind öffentlich verfügbar unter {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber nicht aktiviert. Syncthing muss neugestartet werden um die neue Konfiguration zu aktivieren.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Der verschlüsselte Benutzungsbericht wird täglich gesendet. Er wird benutzt um Statistiken über verwendete Betriebssysteme, Verzeichnis-Größen und Programm-Versionen zu erstellen. Sobald der Bericht in Zukunft weitere Daten erfasst, wird dir dieses Fenster erneut angezeigt.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Die eingegebene Knoten-ID scheint nicht gültig zu sein. Sie sollte eine 52 Stellen lange Zeichenkette aus Buchstaben und Zahlen sein. Leerzeichen und Striche sind optional (werden ignoriert).",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Die eingegebene Knoten-ID scheint nicht gültig zu sein. Es sollte eine 52 oder 56 stellige Zeichenkette aus Buchstaben und Nummern sein. Leerzeichen und Bindestriche sind optional.",
"The node ID cannot be blank.": "Die Knoten-ID darf nicht leer sein.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Die hier einzutragende Knoten-ID kann im \"Bearbeiten > Zeige ID\"-Dialog auf dem anderen Knoten gefunden werden. Leerzeichen und Striche sind optional (werden ignoriert).",
"The number of old versions to keep, per file.": "Anzahl der alten Versionen, die von jeder Datei gespeichert werden sollen.",
"The number of versions must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Zahl sein und darf nicht leer sein.",
"The number of versions must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Zahl und darf nicht leer sein.",
"The repository ID cannot be blank.": "Die Verzeichnis-ID darf nicht leer sein.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis-ID muss eine kurze Kennung (64 Zeichen oder weniger) sein. Sie kann aus Buchstaben, Zahlen und den Punkt- (.), Strich- (-), und Unterstrich- (_) Zeichen bestehen.",
"The repository ID must be unique.": "Die Verzeichnis-ID muss eindeutig sein.",
@@ -100,6 +109,7 @@
"Unknown": "Unbekannt",
"Up to Date": "Aktuell",
"Upgrade To {%version%}": "Upgrade auf {{version}}",
"Upgrading": "Wird aktualisiert",
"Upload Rate": "Uploadgeschwindigkeit",
"Usage": "Benutzung",
"Use Compression": "Benutze Komprimierung",

View File

@@ -1,8 +1,8 @@
{
"API Key": "Κλειδί API",
"About": "Σχετικά",
"Add Node": "Πρόσθεσε Κόμβο",
"Add Repository": "Πρόσθεσε Αποθετήριο",
"Add Node": "Προσθήκη Κόμβου",
"Add Repository": "Προσθήκη Αποθετηρίου",
"Address": "Διεύθυνση",
"Addresses": "Διευθύνσεις",
"Allow Anonymous Usage Reporting?": "Να επιτρέπεται Ανώνυμη Αποστολή Αναφοράς Χρήσης?",
@@ -11,6 +11,7 @@
"Bugs": "Bugs",
"CPU Utilization": "Χρήση CPU",
"Close": "Τέλος",
"Connection Error": "Σφάλμα Σύνδεσης",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg και οι παρακάτω Συνεισφορείς:",
"Delete": "Διαγραφή",
"Disconnected": "Αποσυνδεδεμένος",
@@ -27,46 +28,49 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Όταν τα αρχεία αντικατασταθούν ή διαγραφούν από το syncthing, μεταφέρονται σε φάκελο .stversions με χρονική σήμανση.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Τα αρχεία προστατεύονται από αλλαγές που γίνονται σε άλλους κόμβους, αλλά όποιες αλλαγές γίνουν εδώ θα αποσταλούν στο όλο το cluster.",
"Folder": "Κατάλογος",
"GUI Authentication Password": "GUI Authentication Password",
"GUI Authentication User": "GUI Authentication User",
"GUI Listen Addresses": "GUI Listen Addresses",
"GUI Authentication Password": "Κωδικός πιστοποίησης στο GUI",
"GUI Authentication User": "Χρήστης πιστοποίησης στο GUI",
"GUI Listen Addresses": "GUI Listen διευθύνσεις",
"Generate": "Δημιουργία",
"Global Discovery": "Global Discovery",
"Global Discovery Server": "Global Discovery Server",
"Global Discovery Server": "Διακομιστής Ανεύρεσης Κόμβου",
"Global Repository": "Global Repository",
"Idle": "Ανενεργός",
"Ignore Permissions": "Ignore Permissions",
"Keep Versions": "Keep Versions",
"Idle": "Ανενεργό",
"Ignore Permissions": "Αγνόησε Δικαιώματα",
"Keep Versions": "Διατήρησε Εκδόσεις",
"Latest Release": "Τελευταία Έκδοση",
"Local Discovery": "Local Discovery",
"Local Discovery Port": "Local Discovery Port",
"Local Discovery": "Τοπική Ανεύρεση",
"Local Discovery Port": "Port Τοπικής Ανεύρεσης",
"Local Repository": "Τοπικό Αποθετήριο",
"Master Repo": "Master Repo",
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
"Max Outstanding Requests": "Max Outstanding Requests",
"No": "Αριθμός",
"Node ID": "ID Κόμβου",
"Node Identification": "Ταυτοποίηση Κόμβου",
"Node Name": "Όνομα Κόμβου",
"Notice": "Notice",
"OK": "OK",
"Offline": "Ανεργός",
"Offline": "Ανεργό",
"Online": "Ενεργός",
"Out Of Sync": "Εκτός Συγχρονισμού",
"Out Of Sync": "Μη Συγχρονισμένα",
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
"Override Changes": "Override Changes",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Μονοπάτι του αποθετηρίου στον τοπικό υπολογιστή. Σε περίπτωση που δεν υπάρχει, θα δημιουργηθεί. Ο χαρακτήρας tilde (~) μπορεί να χρησιμοποιηθεί σαν συντόμευση για",
"Please wait": "Παρακαλώ περιμένετε",
"Preview Usage Report": "Preview Usage Report",
"Preview Usage Report": "Προεπισκόπηση αναφοράς χρήσης",
"RAM Utilization": "Χρήση RAM",
"Reconnect Interval (s)": "Reconnect Interval (s)",
"Reconnect Interval (s)": "Χρονικό διάστημα επανασύνδεσης (s)",
"Repository ID": "ID Αποθετηρίου",
"Repository Master": "Repository Master",
"Repository Path": "Μονοπάτι Αποθετηρίου",
"Rescan Interval (s)": "Rescan Interval (s)",
"Rescan": "Rescan",
"Rescan Interval (s)": "Χρονικό διάστημα Επανασάρρωσης (s)",
"Restart": "Επανεκκίνηση",
"Restart Needed": "Απαιτείται Επανεκκίνηση",
"Restarting": "Επανεκκίνηση",
"Save": "Αποθήκευση",
"Scanning": "Scanning",
"Scanning": "Σάρρωση",
"Select the nodes to share this repository with.": "Επιλογή των κόμβων με τους οποίους θα διαμοιραστεί αυτό το αποθετήριο.",
"Settings": "Ρυθμίσεις",
"Share With Nodes": "Διαμοιρασμός με Κόμβους",
@@ -74,38 +78,44 @@
"Short identifier for the repository. Must be the same on all cluster nodes.": "Σύντομη περιγραφή του αποθετηρίου. Θα πρέπει να είναι το ίδιο σε όλους τους κόμβους του cluster.",
"Show ID": "Εμφάνιση ID",
"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": "Απενεργοποίηση",
"Source Code": "Πηγαίος Κώδικας",
"Start Browser": "Start Browser",
"Start Browser": "Έναρξη Φυλλομετρητή",
"Stopped": "Απενεργοποιημένο",
"Support / Forum": "Υποστήριξη / Forum",
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
"Synchronization": "Συγχρονισμός",
"Syncing": "Syncing",
"Syncthing has been shut down.": "Syncthing έχει απενεργοποιηθεί.",
"Syncthing includes the following software or portions thereof:": "Syncthing includes the following software or portions thereof:",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
"The aggregated statistics are publicly available at {{url}}": "The aggregated statistics are publicly available at {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.",
"The node ID cannot be blank.": "The node ID cannot be blank.",
"Syncing": "Συγχρονισμός",
"Syncthing has been shut down.": "Το Syncthing έχει απενεργοποιηθεί.",
"Syncthing includes the following software or portions thereof:": "Το Syncthing συμπεριλαμβάνει τα παρακάτω λογισμικά ή μέρη αυτών:",
"Syncthing is restarting.": "Το Syncthing επανεκκινεί.",
"Syncthing is upgrading.": "Το Syncthing αναβαθμίζεται.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Το Syncthing φαίνεται πως είναι απενεργοποιημένο ή υπάρχει πρόβλημα στη σύνδεσή σας στο Internet. Προσπάθεια ξανά…",
"The aggregated statistics are publicly available at {%url%}.": "Τα στατιστικά που έχουν συλλεγεί είναι διαθέσιμα στο κοινό, στο {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Οι ρυθμίσεις έχουν αποθηκευτεί αλλά δεν έχουν ενεργοποιηθεί. Πρέπει να επανεκκινήσετε το Syncthing για να ισχύσουν οι νέες ρυθμίσεις.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Η κρυπτογραφημένη αναφοράς χρήσης στέλνεται καθημερινά. Χρησιμοποιείται ανίχνευση πληροφοριών πλατφόρμας, μεγέθους αποθετηρίων και εκδόσεων της εφαρμογής. Αν τα δεδομένα που αποστέλονται αλλάξουν, θα πληροφορηθείτε ξανά με αυτό το διάλογο.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Το ID Κόμβου που έχει εισαχθεί δεν είναι σωστό. Θα πρέπει να είναι αλφαριθμητικό 52 χαρακτήρων που να αποτελείται από γράμματα και αριθμούς, όπου τα κενά και οι παύλες είναι προαιρετικά.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Το ID Κόμβου δεν είναι έγκυρο. Θα πρέπει να είναι αλφαριθμιτικό με 52 ή 56 χαρακτήρες και να αποτελείται από γράμματα και αριθμούς, που προαιρετικά χωρίζονται με κενά και παύλες.",
"The node ID cannot be blank.": "Το ID Κόμβου δε μπορεί να είναι κενό.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Το ID Κόμβου μπορείτε να βρείτε στο μενού \"Επεξεργασία > Εμφάνιση ID\" του άλλου κόμβου. Κενά και παύλες είναι προαιρετικά (αγνοούνται).",
"The number of old versions to keep, per file.": "The number of old versions to keep, per file.",
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
"The repository ID cannot be blank.": "The repository ID cannot be blank.",
"The number of versions must be a number and cannot be blank.": "Ο αριθμός εκδόσεων πρέπει να είναι αριθμός και σίγουρα όχι κενό.",
"The repository ID cannot be blank.": "Το ID Αποθετηρίου δε μπορεί να είναι κενό.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be unique.": "The repository ID must be unique.",
"The repository path cannot be blank.": "The repository path cannot be blank.",
"The repository ID must be unique.": "Το ID Αποθετηρίου πρέπει να είναι μοναδικό.",
"The repository path cannot be blank.": "Το μονοπάτι του αποθετηρίου δε μπορεί να είναι κενό.",
"Unknown": "Άγνωστο",
"Up to Date": "Ενημερώμενο",
"Up to Date": "Ενημερωμένος",
"Upgrade To {%version%}": "Αναβάθμιση στην έκδοση {{version}}",
"Upgrading": "Αναβάθμιση",
"Upload Rate": "Upload Rate",
"Usage": "Usage",
"Use Compression": "Χρήση συμπίεσης",
"Use HTTPS for GUI": "Use HTTPS for GUI",
"Use HTTPS for GUI": "Χρήση HTTPS για το GUI",
"Version": "Έκδοση",
"When adding a new node, keep in mind that this node must be added on the other side too.": "When adding a new node, keep in mind that this node must be added on the other side too.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Προσθέτοντας έναν καινούργιο κόμβο, θυμηθείται πως θα πρέπει να προσθέσετε και τον παρόν κόμβο στην άλλη πλευρά.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Κατά την πρόσθεση νέου αποθετηρίου, να γνωρίζεται πως το ID Αποθετηρίου χρησιμοποιείται για να συνδέει Αποθετήρια μεταξύ κόμβων. Τα ID είναι case sensitive και θα πρέπει να είναι ταυτόσημα μεταξύ όλων των κόμβων.",
"Yes": "Ναι",
"You must keep at least one version.": "You must keep at least one version.",

View File

@@ -11,6 +11,7 @@
"Bugs": "Bugs",
"CPU Utilization": "CPU Utilization",
"Close": "Close",
"Connection Error": "Connection Error",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
"Delete": "Delete",
"Disconnected": "Disconnected",
@@ -44,8 +45,11 @@
"Master Repo": "Master Repo",
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
"Max Outstanding Requests": "Max Outstanding Requests",
"Maximum Age": "Maximum Age",
"No": "No",
"No File Versioning": "No File Versioning",
"Node ID": "Node ID",
"Node Identification": "Node Identification",
"Node Name": "Node Name",
"Notice": "Notice",
"OK": "OK",
@@ -55,6 +59,7 @@
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
"Override Changes": "Override Changes",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "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",
"Preview Usage Report": "Preview Usage Report",
"RAM Utilization": "RAM Utilization",
@@ -62,9 +67,12 @@
"Repository ID": "Repository ID",
"Repository Master": "Repository Master",
"Repository Path": "Repository Path",
"Rescan": "Rescan",
"Rescan Interval": "Rescan Interval",
"Rescan Interval (s)": "Rescan Interval (s)",
"Restart": "Restart",
"Restart Needed": "Restart Needed",
"Restarting": "Restarting",
"Save": "Save",
"Scanning": "Scanning",
"Select the nodes to share this repository with.": "Select the nodes to share this repository with.",
@@ -74,8 +82,12 @@
"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",
"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",
"Simple File Versioning": "Simple File Versioning",
"Source Code": "Source Code",
"Staggered File Versioning": "Staggered File Versioning",
"Start Browser": "Start Browser",
"Stopped": "Stopped",
"Support / Forum": "Support / Forum",
@@ -84,11 +96,17 @@
"Syncing": "Syncing",
"Syncthing has been shut down.": "Syncthing has been shut down.",
"Syncthing includes the following software or portions thereof:": "Syncthing includes the following software or portions thereof:",
"Syncthing is restarting.": "Syncthing is restarting.",
"Syncthing is upgrading.": "Syncthing is upgrading.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
"The aggregated statistics are publicly available at {{url}}": "The aggregated statistics are publicly available at {{url}}",
"The aggregated statistics are publicly available at {%url%}.": "The aggregated statistics are publicly available at {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.",
"The 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 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.",
@@ -97,14 +115,18 @@
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be unique.": "The repository ID must be unique.",
"The repository path cannot be blank.": "The repository path cannot be blank.",
"The 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%}": "Upgrade To {{version}}",
"Upgrading": "Upgrading",
"Upload Rate": "Upload Rate",
"Usage": "Usage",
"Use Compression": "Use Compression",
"Use HTTPS for GUI": "Use HTTPS for GUI",
"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 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",

View File

@@ -11,6 +11,7 @@
"Bugs": "Errores",
"CPU Utilization": "Uso de la CPU",
"Close": "Cerrar",
"Connection Error": "Error de conexión",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Derechos de autor © 2014 Jakob Borg y los siguientes colaboradores:",
"Delete": "Suprimir",
"Disconnected": "Desconectado",
@@ -32,7 +33,7 @@
"GUI Listen Addresses": "Direcciones de escucha para la GUI.",
"Generate": "Generar",
"Global Discovery": "Búsqueda en internet",
"Global Discovery Server": "Global Discovery Server",
"Global Discovery Server": "Servidor global de identificación",
"Global Repository": "Repositorio global",
"Idle": "Inactivo",
"Ignore Permissions": "Ignorar permisos",
@@ -46,6 +47,7 @@
"Max Outstanding Requests": "Cantidad máxima de peticiones pendientes",
"No": "No",
"Node ID": "Nodo ID",
"Node Identification": "Identificador del nodo",
"Node Name": "Nodo nombre",
"Notice": "Aviso",
"OK": "OK",
@@ -62,9 +64,11 @@
"Repository ID": "ID de repositorio",
"Repository Master": "Repositorio maestro",
"Repository Path": "Ruta del repositorio",
"Rescan": "Rescan",
"Rescan Interval (s)": "Intervalo de reescaneo (s)",
"Restart": "Reiniciar",
"Restart Needed": "Es necesario reiniciar",
"Restarting": "Reiniciando",
"Save": "Guardar",
"Scanning": "Actualización",
"Select the nodes to share this repository with.": "Seleccione los nodos con los cuales compartir el repositorio.",
@@ -74,6 +78,8 @@
"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",
"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",
"Source Code": "Código fuente",
"Start Browser": "Iniciar navegador",
@@ -84,11 +90,14 @@
"Syncing": "Sincronización",
"Syncthing has been shut down.": "La sincronización esta apagada",
"Syncthing includes the following software or portions thereof:": "Syncthing incluye los siguientes softwares o partes de ellos:",
"Syncthing is restarting.": "Syncthing está reiniciando.",
"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...",
"The aggregated statistics are publicly available at {{url}}": "Las estadísticas acumuladas están públicamente disponibles en {{url}}",
"The aggregated statistics are publicly available at {%url%}.": "Las estadísticas acumuladas están públicamente disponibles en {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido guardada pero no activada.\nSyncthing debe reiniciarse para activar la nueva configuración.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "El reporte de uso se envía encriptado diariamente. Se utiliza para hacer un seguimiento de plataformas comunes, tamaño de repositorios y versión de aplicaciones. Si el conjunto de datos cambia sera notificado mediante este dialogo nuevamente.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "El ID de nodo ingresado no es valido. Debe ser una cadena de al menos 52 caracteres consistente en letras y números, con espacios y guiones opcionales.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "El ID de nodo ingresado no es valido. Debe ser una cadena de 52 o de 56 caracteres consistente en letras y números, con espacios y guiones opcionales.",
"The node ID cannot be blank.": "El ID de nodo no puede estar vacío.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "El ID de nodo a ingresar aquí puede verse en la opción de menú \"Edición > Mostrar ID\" del otro nodo. Espacios y guiones son opcionales (ignorados).",
"The number of old versions to keep, per file.": "El numero de versiones anteriores a conservar, por archivo.",
@@ -100,9 +109,10 @@
"Unknown": "Desconocido",
"Up to Date": "Actualizado",
"Upgrade To {%version%}": "Actualizar a {{version}}",
"Upgrading": "Actualizando",
"Upload Rate": "Tasa de subida",
"Usage": "Utilización",
"Use Compression": "Use Compression",
"Use Compression": "Usar compresn",
"Use HTTPS for GUI": "Usar HTTPS para la GUI",
"Version": "Versión",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Al agregar un nuevo nodo, recuerde que este nodo debe ser agregado en el otro lado también.",

View File

@@ -5,17 +5,18 @@
"Add Repository": "Ajouter un répertoire",
"Address": "Adresse",
"Addresses": "Adresses",
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation?",
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation ?",
"Announce Server": "Serveur d'annonce",
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
"Bugs": "Bugs",
"CPU Utilization": "Utilisation du CPU",
"Close": "Fermer",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants:",
"Connection Error": "Erreur de connexion",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants :",
"Delete": "Supprimer",
"Disconnected": "Déconnecté",
"Documentation": "Documentation",
"Download Rate": "Débit de téléchargement",
"Download Rate": "Débit de réception",
"Edit": "Éditer",
"Edit Node": "Éditer le nœud",
"Edit Repository": "Éditer le répertoire",
@@ -23,8 +24,8 @@
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses \"ip:port\" séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
"Error": "Erreur",
"File Versioning": "Versions de fichier",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Les permissions de fichier sont ignorées lors de la recherche de changements. À utiliser sur les systèmes de fichiers en FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Les fichiers sont datés et déplacés dans le dossier .stversions lors de leurs remplacements ou suppressions par syncthing.",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Les permissions de fichier sont ignorées lors de la recherche de changements. À utiliser sur les systèmes de fichiers de type FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Les fichiers sont datés et déplacés dans le dossier .stversions lors de leur remplacement ou suppression par syncthing.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Les fichiers sont protégés des changements réalisés sur les autres nœuds, mais les changements réalisés sur ce nœud seront transférés au reste du cluster.",
"Folder": "Dossier",
"GUI Authentication Password": "Mot de passe d'authentification GUI",
@@ -43,9 +44,10 @@
"Local Repository": "Dossier local",
"Master Repo": "Dossier maître",
"Max File Change Rate (KiB/s)": "Débit maximum de changement de fichier (KiB/s)",
"Max Outstanding Requests": "Nombre maximum de demandes conccurentes de blocs de fichier",
"Max Outstanding Requests": "Nombre maximum de demandes concurrentes de blocs de fichier",
"No": "Non",
"Node ID": "ID du nœud",
"Node Identification": "Identification du nœud",
"Node Name": "Nom du nœud",
"Notice": "Notification",
"OK": "OK",
@@ -62,18 +64,22 @@
"Repository ID": "ID du répertoire",
"Repository Master": "Répertoire maître",
"Repository Path": "Chemin du répertoire",
"Rescan": "Rescan",
"Rescan Interval (s)": "Intervalle de rescan (s)",
"Restart": "Redémarrer",
"Restart Needed": "Redémarrage nécessaire",
"Restarting": "Redémarrage",
"Save": "Sauver",
"Scanning": "Scanning",
"Select the nodes to share this repository with.": "Sélectionner les nœuds qui partageront ce répertoire.",
"Scanning": "En cours de scan",
"Select the nodes to share this repository with.": "Sélectionner les nœuds qui partagent ce répertoire.",
"Settings": "Configuration",
"Share With Nodes": "Partager avec les nœuds",
"Shared With": "Partagé avec",
"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",
"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.": "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": "Éteindre",
"Source Code": "Code source",
"Start Browser": "Démarrer le navigateur web",
@@ -84,29 +90,33 @@
"Syncing": "En cours de synchronisation",
"Syncthing has been shut down.": "Syncthing a été éteint.",
"Syncthing includes the following software or portions thereof:": "Syncthing inclut les logiciels, ou portion de ceux-ci, suivants:",
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
"Syncthing is upgrading.": "Syncthing est cours de mise à jour.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
"The aggregated statistics are publicly available at {{url}}": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}",
"The aggregated statistics are publicly available at {%url%}.": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été sauvée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des répertoires et les versions de l'application. Si le jeu de données rapportées devait être changé, il vous sera demandé de le valider de nouveau via ce dialogue.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID du nœud ne semble pas être valide. Il devrait ressembler à une chaine de 52 caractères comprenant lettres et chiffres, avec des espaces et des traits d'union optionnels.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID du nœud inséré ne semble pas être valide. Il devrait ressembler à une chaîne de 52 ou 56 comprenant lettres et chiffres, avec des espaces et des traits d'union optionnels.",
"The node ID cannot be blank.": "L'ID du nœud ne peut être vide.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "L'ID du nœud à insérer peut être trouvé à travers le menu \"Éditer > Montrer l'ID\" des autres nœuds. Les espaces et les traits d'union sont optionnels (ils seront ignorés).",
"The number of old versions to keep, per file.": "Le nombre d'anciennes versions à garder, par fichier.",
"The number of versions must be a number and cannot be blank.": "Le nombre de version doit être un nombre et ne peut pas être vide.",
"The repository ID cannot be blank.": "L'ID du répertoire ne peut être vide.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "L'ID du répertoire doit un identifiant court (64 caractères ou moins) comprenant des lettres, nombres, points (.), trait d'union (-) et tiret bas (_).",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "L'ID du répertoire doit être un identifiant court (64 caractères ou moins) comprenant des lettres, nombres, points (.), trait d'union (-) et tiret bas (_).",
"The repository ID must be unique.": "L'ID du répertoire doit être unique.",
"The repository path cannot be blank.": "Le chemin du répertoire ne peut pas être vide.",
"Unknown": "Inconnu",
"Up to Date": "Synchronistation à jour",
"Up to Date": "Synchronisation à jour",
"Upgrade To {%version%}": "Upgrader vers {{version}}",
"Upgrading": "Mise à jour de Syncthing",
"Upload Rate": "Débit d'envoi",
"Usage": "Utilisation",
"Use Compression": "Utiliser la compression",
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
"Version": "Version",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Lorsqu'un nœud est ajouté, gardez à l'esprit que ce nœud doit aussi être ajouté de l'autre coté.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que l'ID du répertoire est utilisé pour lier les répertoires à travers les nœuds. Ils sont sensibles à la case et doivent être semblables à travers tous les nœuds.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que l'ID du répertoire est utilisé pour lier les répertoires à travers les nœuds. Ils sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
"Yes": "Oui",
"You must keep at least one version.": "Vous devez garder au minimum une version.",
"items": "éléments"

123
gui/lang-hu.json Normal file
View File

@@ -0,0 +1,123 @@
{
"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",
"Bugs": "Hibák",
"CPU Utilization": "Processzor használat",
"Close": "Bezárás",
"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",
"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.",
"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 Permissions": "Jogosultságok figyelmen kívül hagyása",
"Keep Versions": "Verziók megtartása",
"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",
"No": "Nem",
"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",
"Please wait": "Kérem várj",
"Preview Usage Report": "Felhasználási adatok átnézése",
"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 (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",
"Source Code": "Forráskód",
"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 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 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",
"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ó",
"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",
"items": "tételek"
}

View File

@@ -6,11 +6,12 @@
"Address": "Indirizzo",
"Addresses": "Indirizzi",
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
"Announce Server": "Server di Ricerca Globale dei Nodi",
"Announce Server": "Server di Presenza Globale dei Nodi",
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
"Bugs": "Bug",
"CPU Utilization": "Utilizzo della CPU",
"CPU Utilization": "Utilizzo CPU",
"Close": "Chiudi",
"Connection Error": "Errore di Connessione",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg e i seguenti Collaboratori:",
"Delete": "Elimina",
"Disconnected": "Disconnesso",
@@ -32,12 +33,12 @@
"GUI Listen Addresses": "Indirizzi dell'Interfaccia Grafica",
"Generate": "Genera",
"Global Discovery": "Individuazione Globale",
"Global Discovery Server": "Global Discovery Server",
"Global Discovery Server": "Server di Ricerca Globale",
"Global Repository": "Deposito Globale",
"Idle": "Inattivo",
"Ignore Permissions": "Ignora Permessi",
"Keep Versions": "Mantieni le Versioni",
"Latest Release": "Ultimo Rilascio",
"Latest Release": "Ultima Versione",
"Local Discovery": "Individuazione Locale",
"Local Discovery Port": "Porta di Individuazione Locale",
"Local Repository": "Deposito Locale",
@@ -46,6 +47,7 @@
"Max Outstanding Requests": "Numero Massimo di Richieste Simultanee per i Blocchi di File",
"No": "No",
"Node ID": "ID Nodo",
"Node Identification": "Identificazione Nodo",
"Node Name": "Nome Nodo",
"Notice": "Avviso",
"OK": "OK",
@@ -57,14 +59,16 @@
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Percorso del deposito nel computer locale. Verrà creato se non esiste già. Il carattere tilde (~) può essere utilizzato come scorciatoia per",
"Please wait": "Attendere prego",
"Preview Usage Report": "Anteprima Statistiche di Utilizzo",
"RAM Utilization": "Utilizzo della RAM",
"RAM Utilization": "Utilizzo RAM",
"Reconnect Interval (s)": "Intervallo di Riconnessione (s)",
"Repository ID": "ID Deposito",
"Repository Master": "Deposito Principale",
"Repository Path": "Percorso del Deposito",
"Rescan": "Rescan",
"Rescan Interval (s)": "Intervallo di Scansione dei File (s)",
"Restart": "Riavvia",
"Restart Needed": "Riavvio Necessario",
"Restarting": "Riavvio",
"Save": "Salva",
"Scanning": "Scansione in corso",
"Select the nodes to share this repository with.": "Seleziona i nodi con i quali vuoi condividere questo deposito.",
@@ -74,6 +78,8 @@
"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",
"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",
"Source Code": "Codice Sorgente",
"Start Browser": "Avvia Browser",
@@ -84,13 +90,16 @@
"Syncing": "Sincronizzazione in corso",
"Syncthing has been shut down.": "Syncthing è stato arrestato.",
"Syncthing includes the following software or portions thereof:": "Syncthing include i seguenti software o porzioni di questi:",
"Syncthing is restarting.": "Riavvio di Syncthing in corso.",
"Syncthing is upgrading.": "Aggiornamento di Syncthing in corso.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing sembra inattivo, oppure c'è un problema con la tua connessione a Internet. Nuovo tentativo…",
"The aggregated statistics are publicly available at {{url}}": "Le statistiche aggregate sono disponibili pubblicamente su {{url}}",
"The aggregated statistics are publicly available at {%url%}.": "Le statistiche aggregate sono disponibili pubblicamente su {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Devi riavviare Syncthing per attivare la nuova configurazione.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Quotidianamente il software invia le statistiche di utilizzo in forma criptata. Questi dati riguardano i sistemi operativi utilizzati, le dimensioni dei depositi e le versioni del software. Se i dati riportati cambiano verrà mostrata di nuovo questa finestra di dialogo.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID del nodo inserito non sembra valido. Dovrebbe essere una stringa di 52 caratteri costituita da lettere e numeri, con spazi e trattini opzionali.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID del nodo inserito non sembra valido. Dovrebbe essere una stringa di 52 o 56 caratteri costituita da lettere e numeri, con spazi e trattini opzionali.",
"The node ID cannot be blank.": "L'ID del nodo non può essere vuoto.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Trova l'ID del nodo nella finestra di dialogo \"Modifica > Mostra ID\" dell'altro nodo e poi inseriscilo qui. Gli spazi e i trattini sono opzionali (ignorati).",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Trova l'ID nella finestra di dialogo \"Modifica > Mostra ID\" dell'altro nodo, poi inseriscilo qui. Gli spazi e i trattini sono opzionali (ignorati).",
"The number of old versions to keep, per file.": "Il numero di vecchie versioni da mantenere, per file.",
"The number of versions must be a number and cannot be blank.": "Il numero di versioni dev'essere un numero e non può essere vuoto.",
"The repository ID cannot be blank.": "L'ID del deposito non può essere vuoto.",
@@ -100,12 +109,13 @@
"Unknown": "Sconosciuto",
"Up to Date": "Sincronizzato",
"Upgrade To {%version%}": "Aggiorna Alla {{version}}",
"Upgrading": "Aggiornamento",
"Upload Rate": "Velocità Upload",
"Usage": "Utilizzo",
"Use Compression": "Utilizza Compressione",
"Use HTTPS for GUI": "Utilizza HTTPS per l'interfaccia grafica",
"Version": "Versione",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quando aggiungi un nuovo nodo, ricordati di aggiungerlo anche dall'altro lato.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Ora la stessa operazione deve essere eseguita anche nel nuovo nodo inserendo l'ID di questo nodo.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Quando aggiungi un nuovo deposito ricordati che gli ID vengono utilizzati per collegare i depositi nei nodi. Distinguono maiuscole e minuscole e devono corrispondere esattamente su tutti i nodi.",
"Yes": "Sì",
"You must keep at least one version.": "È necessario mantenere almeno una versione.",

123
gui/lang-nl.json Normal file
View File

@@ -0,0 +1,123 @@
{
"API Key": "API-sleutel",
"About": "Over",
"Add Node": "Voeg node toe",
"Add Repository": "Voeg repository toe",
"Address": "Adres",
"Addresses": "Adressen",
"Allow Anonymous Usage Reporting?": "Bijhouden van anonieme gebruikers statistieken toestaan?",
"Announce Server": "Aankondigings Server",
"Anonymous Usage Reporting": "Bijhouden anonieme gebruikers statistieken",
"Bugs": "Fouten",
"CPU Utilization": "CPU Gebruik",
"Close": "Sluiten",
"Connection Error": "Verbindingsfout",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg en de onderstaande bijdragers:",
"Delete": "Verwijderen",
"Disconnected": "Niet Verbonden",
"Documentation": "Documentatie",
"Download Rate": "Downloadsnelheid",
"Edit": "Bewerk",
"Edit Node": "Bewerk node",
"Edit Repository": "Repository Bijwerken",
"Enable UPnP": "UPnP aanzetten",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Geef, gescheiden door komma's, \"ip:port\" adressen of \"dynamic\" voor het automatische vinden van de addressen.",
"Error": "Fout",
"File Versioning": "Versiebeheer",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Bestands permissiebits worden genegeerd wanneer naar veranderingen wordt gekeken. Gebruik dit op FAT bestandsystemen",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Bestanden worden naar de .stversions map verplaatst met een tijdsaanduiding, wanneer ze aangepast of verwijderd worden door syncthing.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Bestanden zijn beschermd tegen verandering gedaan op andere nodes, maar veranderingen op deze node worden naar de rest van het cluster gestuurd",
"Folder": "Map",
"GUI Authentication Password": "GUI Authentificatie Wachtwoord",
"GUI Authentication User": "GUI Authentificatie Gebruikersnaam",
"GUI Listen Addresses": "GUI Inkomend adres",
"Generate": "Genereer",
"Global Discovery": "Globaal zoeken",
"Global Discovery Server": "Globale zoekserver",
"Global Repository": "Globale repository",
"Idle": "Klaar",
"Ignore Permissions": "Rechten negeren",
"Keep Versions": "Versies behouden",
"Latest Release": "Laatste uitgave",
"Local Discovery": "Lokaal zoeken",
"Local Discovery Port": "Lokaal zoeken-poort",
"Local Repository": "Lokale repository",
"Master Repo": "Tegen veranderingen beschermen",
"Max File Change Rate (KiB/s)": "Maximale bestands uitwisselsnelheid (KiB/s)",
"Max Outstanding Requests": "Maximaal aantal openstaande aanvragen",
"No": "Nee",
"Node ID": "Node ID",
"Node Identification": "Node Identificatie",
"Node Name": "Node naam",
"Notice": "Notificatie",
"OK": "OK",
"Offline": "Offline",
"Online": "Online",
"Out Of Sync": "Niet gesynchroniseerd",
"Outgoing Rate Limit (KiB/s)": "Uitgaande snelheidslimiet (KiB/s)",
"Override Changes": "Veranderingen overschrijven",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Pad naar de repository op de lokale computer. Word aangemaakt indien deze niet bestaat. Het tilde (~) karakter kan gebruikt worden als afkorting voor",
"Please wait": "Even geduld",
"Preview Usage Report": "Bekijk gebruikersstatistieken",
"RAM Utilization": "RAM gebruik",
"Reconnect Interval (s)": "Herverbind-interval (s)",
"Repository ID": "Repository ID",
"Repository Master": "Hoofd repository",
"Repository Path": "Pad van repository",
"Rescan": "Opnieuw scannen",
"Rescan Interval (s)": "Herscan interval (s)",
"Restart": "Herstart",
"Restart Needed": "Herstart nodig",
"Restarting": "Herstarten",
"Save": "Bewaar",
"Scanning": "Aan het zoeken",
"Select the nodes to share this repository with.": "Selecteer de nodes om deze repository mee te delen",
"Settings": "Instellingen",
"Share With Nodes": "Deel met nodes",
"Shared With": "Gedeeld met",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Korte naam voor de repository. Moet hetzelfde zijn op alle nodes in het cluster.",
"Show ID": "Toon ID",
"Shown instead of Node ID in the cluster status.": "Wordt weergegeven i.p.v. het node ID in de cluster status",
"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",
"Source Code": "Broncode",
"Start Browser": "Start browser",
"Stopped": "Gestopt",
"Support / Forum": "Support / Forum",
"Sync Protocol Listen Addresses": "Synchronisatie protocol luister adres",
"Synchronization": "Synchronisatie",
"Syncing": "Aan het synchroniseren",
"Syncthing has been shut down.": "Syncthing is afgesloten",
"Syncthing includes the following software or portions thereof:": "De volgende software of delen daarvan zijn onderdeel van syncthing:",
"Syncthing is restarting.": "Syncthing is aan het herstarten.",
"Syncthing is upgrading.": "Syncthing is aan het upgraden.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing lijkt afgesloten te zijn, of er is een verbindingsprobleem met het internet. Nieuwe poging....",
"The aggregated statistics are publicly available at {%url%}.": "The verzamelde statistieken zijn publiek beschikbaar op {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De configuratie is opslagen maar nog niet actief. Syncthing moet opnieuw opgestart worden om de nieuwe configuratie te activeren.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "De versleutelde gebruikers statistieken worden dagelijks verstuurd. Deze worden gebruikt om veelgebruikte platformen, repo groottes en app versies bij te houden. Als er nieuwe statistieken worden bijgehouden, wordt dit venster weer getoond.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Het ingevoerde node ID lijkt niet valide te zijn. Het moet een 52 karakter lange string zijn bestaande uit letters en cijfers, spaties en streepjes zijn optioneel.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Het ingevoerde node ID lijkt niet valide te zijn. Het moet een 52 of 56 karakter lange string zijn, bestaande uit letters en cijfers, spaties en streepjes zijn optioneel.",
"The node ID cannot be blank.": "Er moet een node ID ingevoerd worden",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Het node ID dat ingevoerd moet worden kan op de andere node gevonden via \"Bewerk > Toon ID\". Spaties en streepjes zijn toegestaan (worden genegeerd).",
"The number of old versions to keep, per file.": "Het aantal versies dat bewaard moet worden per file.",
"The number of versions must be a number and cannot be blank.": "Het aantal nummers moet een getal zijn en mag niet leeg blijven.",
"The repository ID cannot be blank.": "Er moet een repository ID ingevoerd worden.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Het repository ID moet een korte naam zijn (met 64 karakters of minder) en mag alleen bestaan uit cijfers, letters, punten (.), streepjes (-) en liggende streepjes (_). ",
"The repository ID must be unique.": "Het repository ID moet uniek zijn.",
"The repository path cannot be blank.": "Het repository ID moet ingevuld worden.",
"Unknown": "Onbekend",
"Up to Date": "Gesynchroniseerd",
"Upgrade To {%version%}": "Upgrade naar {{version}}",
"Upgrading": "Bezig met upgrade",
"Upload Rate": "Upload snelheid",
"Usage": "Gebruik",
"Use Compression": "Compressie gebruiken",
"Use HTTPS for GUI": "Gebruik HTTPS voor de GUI",
"Version": "Versie",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Bedenk bij het toevoegen van een nieuwe node dat deze node ook toegevoegd moet worden aan de kant van de nieuwe node.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Bedenk bij het toevoegen van een nieuwe repository dat het repository ID de repositores met elkaar verbindt tussen de nodes. Ze zijn hoofdlettergevoelig en moeten exact dezelfde naam hebben op alle nodes.",
"Yes": "Ja",
"You must keep at least one version.": "Minstens 1 versie moet bewaard blijven.",
"items": "objecten"
}

View File

@@ -1,112 +1,122 @@
{
"API Key": "Chave API",
"About": "Acerca de",
"Add Node": "Adicionar Nó",
"Add Repository": "Adicionar Repositório",
"API Key": "Chave da API",
"About": "Acerca da aplicação",
"Add Node": "Adicionar nó",
"Add Repository": "Adicionar repositório",
"Address": "Endereço",
"Addresses": "Endereços",
"Allow Anonymous Usage Reporting?": "Permitir Envio de Relatórios Anónimos?",
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
"Announce Server": "Servidor de anúncios",
"Anonymous Usage Reporting": "Envio de Relatórios Anónimos",
"Anonymous Usage Reporting": "Enviar de relatórios anónimos de utilização",
"Bugs": "Erros",
"CPU Utilization": "Utilização da CPU",
"Close": "Fechar",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Direitos Reservados © 2014 Jakob Borg e os seguintes Contribuidores:",
"Delete": "Apagar",
"Connection Error": "Erro de ligação",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Direitos reservados © 2014 Jakob Borg e os seguintes contribuidores:",
"Delete": "Eliminar",
"Disconnected": "Desconectado",
"Documentation": "Documentação",
"Download Rate": "Taxa de transferência",
"Download Rate": "Velocidade de recepção",
"Edit": "Editar",
"Edit Node": "Editar Nó",
"Edit Repository": "Editar Repositório",
"Edit Node": "Editar nó",
"Edit Repository": "Editar repositório",
"Enable UPnP": "Activar UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços \"ip:porto\" separados por vírgulas ou \"dynamic\" para o descobrimento automático do endereço.",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços \"ip:porto\" separados por vírgulas ou \"dynamic\" para descobrir automaticamente o endereço.",
"Error": "Erro",
"File Versioning": "Gestão de versões",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "As permissões do ficheiro são ignoradas na pesquisa por mudanças. Utilize nos sistemas de ficheiros FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Os ficheiros são movidos para versões carimbadas com o tempo numa pasta .stversions quando substituídos ou apagados por syncthing.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Os ficheiros são protegidos de mudanças feitas em outros nós, mas alterações feitas neste nó serão enviadas para o resto do agrupamento.",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "As permissões do ficheiro são ignoradas ao procurar alterações. Utilize nos sistemas de ficheiros FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Os ficheiros são movidos para versões carimbadas com o tempo numa pasta .stversions quando substituídos ou apagados pelo syncthing.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Os ficheiros são protegidos das alterações feitas noutros nós, mas alterações feitas neste nó serão enviadas para o resto do agrupamento.",
"Folder": "Pasta",
"GUI Authentication Password": "Senha de Autenticação GUI",
"GUI Authentication User": "Utilizador de autenticação GUI",
"GUI Listen Addresses": "Endereço de escuta GUI",
"GUI Authentication Password": "Senha da autenticação na interface gráfica",
"GUI Authentication User": "Utilizador da autenticação na interface gráfica",
"GUI Listen Addresses": "Endereço de escuta da interface gráfica",
"Generate": "Gerar",
"Global Discovery": "Descoberta Global",
"Global Discovery Server": "Servidor de Descoberta Global",
"Global Repository": "Repositório Global",
"Global Discovery": "Busca global",
"Global Discovery Server": "Servidor da busca global",
"Global Repository": "Repositório global",
"Idle": "Em espera",
"Ignore Permissions": "Ignorar Permissões",
"Keep Versions": "Manter Versões",
"Ignore Permissions": "Ignorar permissões",
"Keep Versions": "Manter versões",
"Latest Release": "Última versão",
"Local Discovery": "Descoberta Local",
"Local Discovery Port": "Porto de Descoberta Local",
"Local Discovery": "Busca local",
"Local Discovery Port": "Porto da busca local",
"Local Repository": "Repositório local",
"Master Repo": "Repositório Mestre",
"Max File Change Rate (KiB/s)": "Taxa máxima de troca de ficheiros (KiB/s)",
"Master Repo": "Repositório mestre",
"Max File Change Rate (KiB/s)": "Velocidade máxima de alterações de ficheiros (KiB/s)",
"Max Outstanding Requests": "Número máximo de pedidos pendentes",
"No": "Não",
"Node ID": "ID do Nó",
"Node Name": "Nome do Nó",
"Node ID": "ID do nó",
"Node Identification": "Identificação do nó",
"Node Name": "Nome do nó",
"Notice": "Nota",
"OK": "OK",
"Offline": "Desconectado",
"Online": "Conectado",
"Out Of Sync": "Não sincronizado",
"Outgoing Rate Limit (KiB/s)": "Limite da taxa de envio (KiB/s)",
"Override Changes": "Sobrepor Mudanças",
"Outgoing Rate Limit (KiB/s)": "Limite da velocidade de envio (KiB/s)",
"Override Changes": "Sobrepor alterações",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Caminho para o repositório no computador local. Será criado se não existir. O carácter (~) pode ser utilizado como atalho para",
"Please wait": "Aguarde",
"Preview Usage Report": "Visualização de Relatório",
"Preview Usage Report": "Pré-visualizar relatório de utilização",
"RAM Utilization": "Utilização da RAM",
"Reconnect Interval (s)": "Intervalo de reestabelecimento de ligação (s)",
"Repository ID": "ID do Repositório",
"Repository Master": "Repositório Mestre",
"Repository Path": "Caminho do Repositório",
"Rescan Interval (s)": "Intervalo de monitorização (s)",
"Repository ID": "ID do repositório",
"Repository Master": "Repositório mestre",
"Repository Path": "Caminho do repositório",
"Rescan": "Voltar a varrer",
"Rescan Interval (s)": "Intervalo entre varrimentos (s)",
"Restart": "Reiniciar",
"Restart Needed": "É preciso reiniciar",
"Restarting": "Reiniciando",
"Save": "Gravar",
"Scanning": "Varrendo",
"Select the nodes to share this repository with.": "Seleccione os nós com os quais vai partilhar este repositório.",
"Settings": "Configurações",
"Share With Nodes": "Partilhar com Nós",
"Shared With": "Partilhado Com",
"Share With Nodes": "Partilhar com os nós",
"Shared With": "Partilhado com",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identificador curto para o repositório. Tem que ser igual em todos os nós do agrupamento.",
"Show ID": "Mostrar ID",
"Shown instead of Node ID in the cluster status.": "Mostrado ao invés do ID do Nó no estado do agrupamento.",
"Shown instead of Node ID in the cluster status.": "Apresentado ao invés do ID do nó no estado do agrupamento.",
"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ó no estado do agrupamento. Será divulgado aos outros nós como um nome pré-definido 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ó no estado do agrupamento. Será actualizado para o nome que o nó divulga, se for deixado em branco.",
"Shutdown": "Desligar",
"Source Code": "Código Fonte",
"Start Browser": "Iniciar Navegador",
"Source Code": "Código fonte",
"Start Browser": "Iniciar navegador",
"Stopped": "Parado",
"Support / Forum": "Suporte / Fórum",
"Sync Protocol Listen Addresses": "Endereços de escuta do protocolo de sincronização",
"Synchronization": "Sincronização",
"Syncing": "Sincronizando",
"Syncthing has been shut down.": "Syncthing foi desligado.",
"Syncthing includes the following software or portions thereof:": "Syncthing inclui as seguintes aplicações ou partes delas:",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece estar em baixo, ou então existe um problema com a sua ligação à Internet. Tentando novamente...",
"The aggregated statistics are publicly available at {{url}}": "As estatísticas agrupadas estão disponíveis publicamente em {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi gravada mas não activada. Syncthing tem que reiniciar para activar a nova configuração.",
"Syncing": "A Sincronizar",
"Syncthing has been shut down.": "O Syncthing foi desligado.",
"Syncthing includes the following software or portions thereof:": "O Syncthing inclui as seguintes aplicações ou partes delas:",
"Syncthing is restarting.": "O Syncthing está a reiniciar.",
"Syncthing is upgrading.": "O Syncthing está a actualizar-se.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "O Syncthing parece estar em baixo, ou então existe um problema com a sua ligação à Internet. Tentando novamente...",
"The aggregated statistics are publicly available at {%url%}.": "As estatísticas agrupadas estão disponíveis publicamente em {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi gravada mas não activada. O Syncthing tem que reiniciar para activar a nova configuração.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "O relatório de utilização cifrado é enviado diariamente. É utilizado para seguir plataformas comuns, tamanhos de repositórios e versões da aplicação. Se o conjunto de dados do relatório for alterado, será notificado novamente através desta janela.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "O ID do nó indicado não parece ser válido. Deveria conter uma palavra de 52 caracteres constituída por letras e números, com espaços e traços opcionais. ",
"The node ID cannot be blank.": "O ID do nó não pode ser vazio.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "O ID do nó fornecido não parece ser válido. Deveria ser um texto com 52 ou 56 caracteres constituídos por letras e números, com espaços e traços opcionais.",
"The node ID cannot be blank.": "O ID do nó não pode estar vazio.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "O ID do nó a introduzir pode ser encontrado no diálogo \"Editar > Mostrar ID\" no outro nó. Espaços e traços são opcionais (ignorados).",
"The number of old versions to keep, per file.": "O número de versões antigas a manter, por ficheiro.",
"The number of versions must be a number and cannot be blank.": "O número de versões tem que ser um número e não pode ser vazio.",
"The repository ID cannot be blank.": "O ID do repositório não pode ser vazio.",
"The number of versions must be a number and cannot be blank.": "O número de versões tem que ser um número e não pode estar vazio.",
"The repository ID cannot be blank.": "O ID do repositório não pode estar vazio.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "O ID do repositório tem que ser um identificador curto (64 caracteres ou menos) e consiste em letras, números e os caracteres ponto (.), traço (-) e (_).",
"The repository ID must be unique.": "O ID do repositório tem que ser único.",
"The repository path cannot be blank.": "O caminho do repositório não pode ser vazio.",
"The repository path cannot be blank.": "O caminho do repositório não pode estar vazio.",
"Unknown": "Desconhecido",
"Up to Date": "Actualizado",
"Upgrade To {%version%}": "Atualizar para {{version}}",
"Upgrade To {%version%}": "Actualizar para {{version}}",
"Upgrading": "Actualizando",
"Upload Rate": "Taxa de envio",
"Usage": "Utilização",
"Use Compression": "Usar Compressão",
"Use HTTPS for GUI": "Utilizar HTTPS para GUI",
"Use Compression": "Usar compressão",
"Use HTTPS for GUI": "Utilizar HTTPS na interface gráfica",
"Version": "Versão",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quando adicionar um novo nó, lembre-se que este nó tem que ser adicionado do outro lado também.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Quando adicionar um novo repositório, lembre-se que o ID do Repositório é utilizado para juntar os repositórios entre nós. São sensíveis às maiúsculas e minúsculas e tem que corresponder exactamente entre todos os nós.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quando adicionar um novo nó, lembre-se que este nó tem que ser adicionado no outro lado também.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Quando adicionar um novo repositório, lembre-se que o ID do repositório é utilizado para juntar os repositórios entre nós. Os ID's são sensíveis às maiúsculas e minúsculas e têm que corresponder exactamente entre todos os nós.",
"Yes": "Sim",
"You must keep at least one version.": "Tem que manter pelo menos uma versão.",
"items": "itens"

View File

@@ -11,6 +11,7 @@
"Bugs": "Ошибки",
"CPU Utilization": "Загрузка ЦПУ",
"Close": "Закрыть",
"Connection Error": "Ошибка подключения",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Все права защищены © 2014 Jakob Borg и следующие участники:",
"Delete": "Удалить",
"Disconnected": "Нет соединения",
@@ -46,6 +47,7 @@
"Max Outstanding Requests": "Максимальное количество исходящих запросов",
"No": "Нет",
"Node ID": "ID Узла",
"Node Identification": "Идентификация узла",
"Node Name": "Имя Узла",
"Notice": "Внимание",
"OK": "ОК",
@@ -62,9 +64,11 @@
"Repository ID": "ID Репозитория",
"Repository Master": "Главный Репозиторий",
"Repository Path": "Путь к Репозиторию",
"Rescan": "Rescan",
"Rescan Interval (s)": "Интервал между сканированием (сек)",
"Restart": "Перезапуск",
"Restart Needed": "Требуется перезапуск",
"Restarting": "Перезапуск",
"Save": "Сохранить",
"Scanning": "Сканирование",
"Select the nodes to share this repository with.": "Выберите узлы для которых будет доступен данный репозиторий.",
@@ -74,6 +78,8 @@
"Short identifier for the repository. Must be the same on all cluster nodes.": "Короткий идентификатор репозитория. Должен быть одинаковый на всех узлах кластера.",
"Show ID": "Показать ID",
"Shown instead of Node ID in the cluster status.": "Отображается вместо ID узла в статусе кластера.",
"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": "Выключить",
"Source Code": "Исходный код",
"Start Browser": "Открыть браузер",
@@ -84,11 +90,14 @@
"Syncing": "Синхронизация",
"Syncthing has been shut down.": "Syncthing выключен.",
"Syncthing includes the following software or portions thereof:": "Syncthing включает в себя следующее ПО или его части:",
"Syncthing is restarting.": "Перезапуск Syncthing",
"Syncthing is upgrading.": "Обновление Syncthing ",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",
"The aggregated statistics are publicly available at {{url}}": "Суммарная статистика общедоступна на {{url}}",
"The aggregated statistics are publicly available at {%url%}.": "Суммарная статистика общедоступна на {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурация была сохранена но не активирована. Для активации новой конфигурации необходимо рестартовать Syncthing.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Зашифрованные отчёты об использовании отправляются ежедневно. Они используются для отслеживания проблем, размеров репозиториев и версий Syncthing. Если набор данных в отчёте будет изменён, то вы получите уведомление об этом в этом диалоге.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введённый ID узла выглядит неправильно: ID должен быть строкой, длинной 52 символа, обязательно содержащей группы букв и цифр которые могут быть разделены пробелами или тире.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введённый ID узла выглядит неправильно: ID должен быть строкой, длинной 52 или 56 символов, обязательно содержащей группы букв и цифр которые могут быть разделены пробелами или тире.",
"The node ID cannot be blank.": "ID узла не может быть пустым.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "ID узла можно узнать в окне \"Редактировать > Показать ID\" на требуемом узле. Пробелы и тире используются для удобства и не обязательны (игнорируются).",
"The number of old versions to keep, per file.": "Количество хранимых версий файла.",
@@ -100,6 +109,7 @@
"Unknown": "Неизвестно",
"Up to Date": "Обновлено",
"Upgrade To {%version%}": "Обновить до {{version}}",
"Upgrading": "Обновление",
"Upload Rate": "Скорость отдачи",
"Usage": "Справка",
"Use Compression": "Использовать сжатие",

View File

@@ -9,13 +9,14 @@
"Announce Server": "Uppslagningsserver",
"Anonymous Usage Reporting": "Anonym användarstatistik",
"Bugs": "Buggar",
"CPU Utilization": "CPU-utnyttjande",
"CPU Utilization": "CPU-användning",
"Close": "Stäng",
"Connection Error": "Anslutningsproblem",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg och de följande medarbetarna:",
"Delete": "Radera",
"Disconnected": "Ej ansluten",
"Documentation": "Dokumentation",
"Download Rate": "Nerladdningshastighet",
"Download Rate": "Nedladdningshastighet",
"Edit": "Redigera",
"Edit Node": "Redigera nod",
"Edit Repository": "Redigera lagring",
@@ -29,7 +30,7 @@
"Folder": "Katalog",
"GUI Authentication Password": "GUI-lösenord",
"GUI Authentication User": "GUI-användare",
"GUI Listen Addresses": "GUI-address",
"GUI Listen Addresses": "GUI-adress",
"Generate": "Skapa",
"Global Discovery": "Global uppslagning",
"Global Discovery Server": "Global uppslagningsserver",
@@ -46,6 +47,7 @@
"Max Outstanding Requests": "Paralellitet",
"No": "Nej",
"Node ID": "Nod-ID",
"Node Identification": "Nod-identifiering",
"Node Name": "Nodnamn",
"Notice": "OBS",
"OK": "OK",
@@ -57,14 +59,16 @@
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
"Please wait": "Var god vänta",
"Preview Usage Report": "Förhandsgranska statistik",
"RAM Utilization": "Minnesutnyttjande",
"RAM Utilization": "Minnesanvändning",
"Reconnect Interval (s)": "Anslutningsintervall (s)",
"Repository ID": "Lagrings-ID",
"Repository Master": "Huvudlagring",
"Repository Path": "Lagringskatalog",
"Rescan": "Scanna om",
"Rescan Interval (s)": "Förnyelseintervall (s)",
"Restart": "Starta om",
"Restart Needed": "Omstart behövs",
"Restarting": "Startar om",
"Save": "Spara",
"Scanning": "Uppdaterar",
"Select the nodes to share this repository with.": "Välj vilka noder förvaringen ska delas med.",
@@ -74,21 +78,26 @@
"Short identifier for the repository. Must be the same on all cluster nodes.": "Kort identifieringssträng för förvaringen. Måste vara samma på alla noder i klustern.",
"Show ID": "Visa ID",
"Shown instead of Node ID in the cluster status.": "Visas i stället för nod-ID",
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Visas istället för nod-ID. Skickas till andra noder som namn på denna nod.",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Visas istället för nod-ID. Sätts till namnet på den andra noden vid första anslutning om det lämnas blankt.",
"Shutdown": "Stäng av",
"Source Code": "Kälkod",
"Source Code": "Källkod",
"Start Browser": "Starta browser",
"Stopped": "Stoppad",
"Support / Forum": "Support / Forum",
"Sync Protocol Listen Addresses": "Address för inkommande anslutningar",
"Synchronization": "Synkronisation",
"Synchronization": "Synkronisering",
"Syncing": "Synkroniserar",
"Syncthing has been shut down.": "Syncthing har stängts ner.",
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller de följande mjukvarupaketen eller delar därav:",
"Syncthing is restarting.": "Syncthing startar om.",
"Syncthing is upgrading.": "Syncthing uppgraderas.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller så är där ett problem med din Internetanslutning. Försöker igen...",
"The aggregated statistics are publicly available at {{url}}": "Aggregerad statistik finns publikt tillgänglig på {{url}}",
"The aggregated statistics are publicly available at {%url%}.": "Aggregerad statistik finns publikt tillgänglig på {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, lagringsstorlekar och versioner. Om datan som rapporteras ändras så kommer du att bli tillfrågad igen.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade nod-ID:t verkar inte korrekt. Det ska vara en 52 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade nod-ID:t verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
"The node ID cannot be blank.": "Nod-ID:t kan inte vara blankt.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Nod-ID:t som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra noden. Mellanrum och bindestreck är valfria (ignoreras).",
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas, per fil.",
@@ -100,6 +109,7 @@
"Unknown": "Okänt",
"Up to Date": "Helt uppdaterad",
"Upgrade To {%version%}": "Uppgradera till {{version}}",
"Upgrading": "Uppgraderar",
"Upload Rate": "Uppladdningshastighet",
"Usage": "Användande",
"Use Compression": "Använd komprimering",

123
gui/lang-tr.json Normal file
View File

@@ -0,0 +1,123 @@
{
"API Key": "API Anahtarı",
"About": "Hakkında",
"Add Node": "Düğüm ekle",
"Add Repository": "Depo ekle",
"Address": "Adres",
"Addresses": "Adresler",
"Allow Anonymous Usage Reporting?": "Anonim kullanım raporlarına izin ver ?",
"Announce Server": "Duyuru Sunucusu",
"Anonymous Usage Reporting": "Anonim Kullanım Raporlama",
"Bugs": "Hatalar",
"CPU Utilization": "İşlemci Kullanımı",
"Close": "Kapat",
"Connection Error": "Bağlantı hatası",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Telif hakkı © 2014 Jakob Borg ve aşağıdaki katkıda bulunanlar",
"Delete": "Sil",
"Disconnected": "Bağlantı Kesik",
"Documentation": "Dokümantasyon",
"Download Rate": "İndirme Hızı",
"Edit": "Seçenekler",
"Edit Node": "Düğümü Düzenle",
"Edit Repository": "Depoyu düzenle",
"Enable UPnP": "UPnP Etkinleştir",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "IP adresleri eklemek için virgül ile ayırarak \"ip:port\" yazın, ya da \"dynamic\" yazarak otomatik bulma işlemini seçin.",
"Error": "Hata",
"File Versioning": "Dosya Sürümlendirme",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Değişim yoklarken dosya izin bilgilerini ihmal et. FAT dosya sisteminde kullanın.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Dosyalar syncthing tarafından değiştirildiğinde ya da silindiğinde, tarih damgalı sürümleri .stversions dizinine taşınır.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Dosyalar diğer düğümlerde yapılan değişikliklerden korunur, ancak bu düğümdeki değişiklikler kümedeki diğer düğümlere gönderilir.",
"Folder": "Dizin",
"GUI Authentication Password": "Kullanıcı arayüzü şifresi",
"GUI Authentication User": "Kullanıcı arayüzü kullanıcı ismi",
"GUI Listen Addresses": "Kullanıcı arayüzü bağlantı adresi",
"Generate": "Oluştur",
"Global Discovery": "Küresel Keşif",
"Global Discovery Server": "Küresel Keşif Sunucusu",
"Global Repository": "Global Depo",
"Idle": "Boşta",
"Ignore Permissions": "İzinleri yoksay",
"Keep Versions": "Sürüm tut",
"Latest Release": "Son sürüm",
"Local Discovery": "Yerel bulma",
"Local Discovery Port": "Yerel bulma portları",
"Local Repository": "Yerel Depo",
"Master Repo": "Ana depo",
"Max File Change Rate (KiB/s)": "Mak. Dosya değiştirme oranı (KB/sn)",
"Max Outstanding Requests": "Maks Öncellikli İstekler",
"No": "Hayır",
"Node ID": "Düğüm ID",
"Node Identification": "Düğüm Kimliği",
"Node Name": "Düğüm İsmi",
"Notice": "Uyarı",
"OK": "Tamam",
"Offline": "Çevrim dışı",
"Online": "Çevrim içi",
"Out Of Sync": "Senkronize edilmemiş",
"Outgoing Rate Limit (KiB/s)": "Yükleme hız sınırı (KB/sn)",
"Override Changes": "Değişiklikleri Geçersiz kıl",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Yerel bilgisayardaki depoya ulaşım yolu. Dizin yoksa yaratılacak. (~) karakterinin kısayol olarak kullanılabileceği yol",
"Please wait": "Lütfen Bekleyin",
"Preview Usage Report": "Kullanım raporunu gözden geçir",
"RAM Utilization": "RAM Kullanımı",
"Reconnect Interval (s)": "Yeniden bağlanma süresi (sn)",
"Repository ID": "Depo ID",
"Repository Master": "Ana depo",
"Repository Path": "Depo Yolu",
"Rescan": "Tekrar Tara",
"Rescan Interval (s)": "Tekrar tarama süresi (sn)",
"Restart": "Yeniden Başlat",
"Restart Needed": "Yeniden başlatma gereklidir",
"Restarting": "Yeniden başlatılıyor",
"Save": "Kaydet",
"Scanning": "Taranıyor",
"Select the nodes to share this repository with.": "Bu deponun paylaşılacağı düğümleri seçin.",
"Settings": "Ayarlar",
"Share With Nodes": "Paylaşılan düğümler:",
"Shared With": "Paylaşılan düğümler",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Depo için kısa tanımlayıcı. Kümedeki tüm düğümlerde aynı olmalı.",
"Show ID": "ID Göster",
"Shown instead of Node ID in the cluster status.": "Ana ekranda Düğüm ID yerine bunu göster.",
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Küme durumunda Düğüm ID yerine bunu göster. Varsayılan isim isteğe bağlı olarak diğer düğümlere ilan edilecektir.",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Küme durumunda Düğüm ID yerine bunu göster. Eğer düğüm ismi boş bırakılırsa düğüm ismi güncellenip ilan edilecektir.",
"Shutdown": "Kapat",
"Source Code": "Kaynak Kodu",
"Start Browser": "Tarayıcıyı Başlat",
"Stopped": "Durduruldu",
"Support / Forum": "Destek / Forum",
"Sync Protocol Listen Addresses": "Sync Protokol Dinleme Adresleri",
"Synchronization": "Senkronizasyon",
"Syncing": "Senkronize ediliyor",
"Syncthing has been shut down.": "Syncthing durduruldu",
"Syncthing includes the following software or portions thereof:": "Syncthing aşağıdaki yazılımları veya bunların bölümlerini içermektedir:",
"Syncthing is restarting.": "Syncthing yeniden başlatılıyor.",
"Syncthing is upgrading.": "Syncthing yükseltiliyor.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing görünüşe durdu veya internetin bağlantınızda problem var. Tekrar deniyor....",
"The aggregated statistics are publicly available at {%url%}.": "Toplanan halka açık istatistiklere ulaşabileceğiniz adres {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Ayarlar kaydedildi ancak aktifleştirilmedi. Aktifleştirmek için Syncthing yeniden başlatılmalı.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Şifrelenmiş kullanım bilgisi günlük olarak gönderilir. Platform, depo büyüklüğü ve uygulama sürümü hakkında bilgi toplanır. Toplanan bilgi çeşidi değişecek olursa, sizden tekrar onay istenecek.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Girilen düğüm ID'si geçerli gibi gözükmüyor. 52 karakter uzunluğunda, harf ve rakamlardan oluşmalı. Boşlukların ve kısa çizgilerin olup olmaması önemli değildir.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Girilen düğüm ID'si geçerli gibi gözükmüyor. 52 ya da 56 karakter uzunluğunda, harf ve rakamlardan oluşmalı. Boşlukların ve kısa çizgilerin olup olmaması önemli değildir.",
"The node ID cannot be blank.": "Düğüm ID'si boş olamaz.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Buraya girilecek düğüm ID'si diğer düğümde \"Düzenle > ID Göster\" menüsünden bulunabilir. Boşluk ve kısa çizginin olup olmaması önemli değildir. (İhmal edilir)",
"The number of old versions to keep, per file.": "Dosya başına saklanacak eski sürüm.",
"The number of versions must be a number and cannot be blank.": "Sürümlerin sayısı sayı olmalı ve boş bırakılamaz.",
"The repository ID cannot be blank.": "Depo ID boş bırakılamaz.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Depo ID uzun olmamalı (64 karakter ya da daha az). Sadece harf, rakam, nokta (.), kısa çizgi (-) ve alt çizgi (_) kullanabilirsiniz.",
"The repository ID must be unique.": "Depo ID'si benzersiz olmalıdır.",
"The repository path cannot be blank.": "Depo yolu boş bırakılamaz.",
"Unknown": "Bilinmiyor",
"Up to Date": "Güncel",
"Upgrade To {%version%}": "{{version}} sürümüne yükselt",
"Upgrading": "Yükseltiliyor",
"Upload Rate": "Yükleme hızı",
"Usage": "Kullanım",
"Use Compression": "Sıkıştırma kullan",
"Use HTTPS for GUI": "GUI için HTTPS kullan",
"Version": "Sürüm",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Yeni bir düğüm eklendiğinde unutmayın ki; bu düğüm diğer tarafa da eklenmelidir.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Unutmayın ki; Depo ID, depoları düğümler arasında bağlamak için kullanılıyor. Büyük - küçük harf duyarlı, ve bütün düğümlerde aynı olmalı.",
"Yes": "Evet",
"You must keep at least one version.": "En az bir sürümü tutmalısınız.",
"items": "öğeler"
}

123
gui/lang-uk.json Normal file
View File

@@ -0,0 +1,123 @@
{
"API Key": "API ключ",
"About": "Про програму",
"Add Node": "Додати вузол",
"Add Repository": "Додати репозиторій",
"Address": "Адреса",
"Addresses": "Адреси",
"Allow Anonymous Usage Reporting?": "Дозволити програмі збирати анонімну статистику викроистання?",
"Announce Server": "Сервер анонсування",
"Anonymous Usage Reporting": "Анонімна статистика використання",
"Bugs": "Помилки",
"CPU Utilization": "Навантаження CPU",
"Close": "Закрити",
"Connection Error": "Помилка з’єднання",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg та наступні контриб’ютори:",
"Delete": "Видалити",
"Disconnected": "З’єднання відсутнє",
"Documentation": "Документація",
"Download Rate": "Швидкість завантаження",
"Edit": "Редагувати",
"Edit Node": "Редагувати вузол",
"Edit Repository": "Редагувати репозиторій",
"Enable UPnP": "Увімкнути UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Уведіть адреси \"ip:port\" розділені комою, або слово \"dynamic\" для здійснення автоматичного виявлення адреси.",
"Error": "Помилка",
"File Versioning": "Керування версіями",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Біти прав доступу до файлів будуть проігноровані під час визначення змін. Використовуйте на файлових системах FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Файли будуть поміщатися у директорію .stversions із відповідною позначкою часу, коли вони будуть замінятися або видалятися програмою.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Файли захищені від змін на інших вузлах, але зміни на цьому вузлі будуть розіслані на інші вузли кластера.",
"Folder": "Директорія",
"GUI Authentication Password": "Пароль для доступу до панелі управління",
"GUI Authentication User": "Логін користувача для доступу до панелі управління",
"GUI Listen Addresses": "Адреса доступу до панелі управління",
"Generate": "Згенерувати",
"Global Discovery": "Глобальне виявлення",
"Global Discovery Server": "Сервер для глобального виявлення",
"Global Repository": "Глобальний репозиторій",
"Idle": "Очікування",
"Ignore Permissions": "Ігнорувати права доступу до файлів",
"Keep Versions": "Зберігати версії",
"Latest Release": "Останній реліз",
"Local Discovery": "Локальне виявлення",
"Local Discovery Port": "Локальний порт для виявлення",
"Local Repository": "Локальний репозиторій",
"Master Repo": "Центральний репозиторій",
"Max File Change Rate (KiB/s)": "Максимальна швидкість змінення файлів (КіБ/с)",
"Max Outstanding Requests": "Максимальна кількість вихідних запитів",
"No": "Ні",
"Node ID": "ID вузла",
"Node Identification": "Ідентифікатор вузла",
"Node Name": "Назва вузла",
"Notice": "Повідомлення",
"OK": "OK",
"Offline": "Офлайн",
"Online": "Онлайн",
"Out Of Sync": "Не синхронізовано",
"Outgoing Rate Limit (KiB/s)": "Ліміт швидкості віддачі (КіБ/с)",
"Override Changes": "Перезаписати зміни",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Шлях до репозиторія на локальному комп’ютері. Буде створений, якщо такий не існує. Символ тильди (~) може бути використаний як ярлик для",
"Please wait": "Будь ласка, зачекайте",
"Preview Usage Report": "Попередній перегляд статистичного звіту",
"RAM Utilization": "Використання RAM",
"Reconnect Interval (s)": "Інтервал повторного з’єднання (с)",
"Repository ID": "ID репозиторія",
"Repository Master": "Центральний репозиторій",
"Repository Path": "Шлях до репозиторія",
"Rescan": "Пересканувати",
"Rescan Interval (s)": "Інтервал для повторного сканування (с)",
"Restart": "Перезапуск",
"Restart Needed": "Необхідний перезапуск",
"Restarting": "Відбувається перезапуск",
"Save": "Зберегти",
"Scanning": "Сканування",
"Select the nodes to share this repository with.": "Оберіть вузли із якими обміняти даний репозиторій.",
"Settings": "Налаштування",
"Share With Nodes": "Обмінювати із вузлами",
"Shared With": "Доступно для",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Короткий ідентифікатор репозиторія. Повинен бути однаковим на всіх вузлах кластера.",
"Show ID": "Показати ID",
"Shown instead of Node ID in the cluster status.": "Показано замість ID вузла в статусі кластера.",
"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": "Вимкнути",
"Source Code": "Сирцевий код",
"Start Browser": "Запустити браузер",
"Stopped": "Зупинено",
"Support / Forum": "Підтримка / Форум",
"Sync Protocol Listen Addresses": "Адреса панелі управління",
"Synchronization": "Синхронізація",
"Syncing": "Синхронізація",
"Syncthing has been shut down.": "Syncthing вимкнено (закрито).",
"Syncthing includes the following software or portions thereof:": "Syncthing містить наступне програмне забезпечення (або його частини):",
"Syncthing is restarting.": "Syncthing перезавантажується.",
"Syncthing is upgrading.": "Syncthing оновлюється.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Схоже на те, що Syncthing закритий, або виникла проблема із Інтернет-з’єднанням. Проводиться повторна спроба з’єднання…",
"The aggregated statistics are publicly available at {%url%}.": "Зібрана статистика публічно доступна за посиланням {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфігурацію збережено, але не активовано. Необхідно перезапустити Syncthing для того, щоби активувати нову конфігурацію.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Зашифрована статистика використання відсилається щоденно. Вона використовується для того, щоб розробники розуміли, на яких платформах працює програма, розміри репозиторіїв та версії програми. Якщо набір даних, що збирається зазнає змін, ви обов’язково будете повідомлені через це діалогове вікно.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введений ID вузла невалідний. Ідентифікатор має вигляд строки довжиною 52 символи, що містить цифри та літери, із опціональними пробілами та тире.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введений ID вузла невалідний. Ідентифікатор має вигляд строки довжиною 52 або 56 символів, що містить цифри та літери, із опціональними пробілами та тире.",
"The node ID cannot be blank.": "ID вузла не може бути порожнім.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "ID вузла, який необхідно додати. Може бути знайдений у вікні \"Редагувати > Показати ID\" на іншому вузлі. Пробіли та тире опціональні (вони ігноруються програмою).",
"The number of old versions to keep, per file.": "Кількість старих версій, яку необхідно зберігати для кожного файлу.",
"The number of versions must be a number and cannot be blank.": "Кількість версій повинна бути цифрою та не може бути порожньою.",
"The repository ID cannot be blank.": "ID репозиторія не може бути порожнім.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "ID репозиторія повинен бути коротким ідентифікатором (64 символи або менше), що містить лише цифри та літери, знак крапки (.), тире (-) та нижнього підкреслення (_).",
"The repository ID must be unique.": "ID репозиторія повинен бути унікальним.",
"The repository path cannot be blank.": "Шлях до репозиторія не може бути порожнім.",
"Unknown": "Невідомо",
"Up to Date": "Актуальа версія",
"Upgrade To {%version%}": "Оновити до {{version}}",
"Upgrading": "Оновлення",
"Upload Rate": "Швидкість віддачі",
"Usage": "Допомога",
"Use Compression": "Використовувати компресію",
"Use HTTPS for GUI": "Використовувати HTTPS для доступу до панелі управління",
"Version": "Версія",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Коли додаєте новий вузол, пам’ятайте, що цей вузол повинен бути доданий і на іншій стороні.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Коли додаєте новий репозиторій, пам’ятайте, що ID репозиторія використовується для того, щоб зв’язувати репозиторії разом між вузлами. Назви є чутливими до регістра та повинні співпадати точно між усіма вузлами.",
"Yes": "Так",
"You must keep at least one version.": "Ви повинні зберігати щонайменше одну версію.",
"items": "елементи"
}

Some files were not shown because too many files have changed in this diff Show More