Compare commits

..

216 Commits

Author SHA1 Message Date
Audrius Butkevicius
5ce8261d89 Fix build on Solaris 2014-09-28 21:55:08 +01:00
Jakob Borg
25c080b46f Fix config tests for autoUpgradeIntervalH
Conflicts:
	internal/config/config_test.go
2014-09-28 21:49:00 +01:00
Audrius Butkevicius
ee765990b7 Add autoUpgrade option in UI
Conflicts:
	gui/app.js
	internal/auto/gui.files.go
2014-09-28 21:20:29 +01:00
Audrius Butkevicius
77fcb52b6a Add autoUpgrade coroutine (fixes #727)
Conflicts:
	internal/config/config.go
2014-09-28 21:19:54 +01:00
Audrius Butkevicius
aaec3330a9 Allow a single upgrade at a time 2014-09-28 21:17:44 +01:00
Jakob Borg
3f55a885a1 Don't require godep to build 2014-09-28 21:22:01 +02:00
Jakob Borg
80a1c931a9 Short benchmark test only by default 2014-09-28 21:21:39 +02:00
Jakob Borg
a147f4bb77 Genfiles should be random by default 2014-09-28 21:21:28 +02:00
Audrius Butkevicius
e4316aec6e Remove field placeholders (fixes #748)
All of them are either required, or empty by default, or for example
node name is later populated on first connection.
2014-09-28 21:04:48 +02:00
Jakob Borg
7bc4589d4d Simple reproducible syncing benchmark 2014-09-27 21:13:04 +02:00
Jakob Borg
9af586d4ac Total in and out bytes in perfstat 2014-09-27 21:13:02 +02:00
Jakob Borg
87e68cac6c Fix spacing under Add buttons 2014-09-27 14:43:11 +02:00
Jakob Borg
7d5a98409b Move discovery protocol spec 2014-09-27 12:54:23 +02:00
Jakob Borg
14817e31f6 Move top level packages to internal. 2014-09-27 09:42:10 +02:00
Jakob Borg
fbdbd722b1 Don't add duplicates for introduced nodes without repos (fixes #745) 2014-09-27 09:37:37 +02:00
Jakob Borg
b34102cd11 Add note about squashing/rebasing 2014-09-26 09:57:16 +02:00
Audrius Butkevicius
3c51cd6626 Name is not always the hostname 2014-09-24 22:10:20 +01:00
Jakob Borg
b0b34236e3 Revert "Merge branch 'pr/711'" (...)
Temporary revert to the old debounce behavior. It's a bit bad and drives
up CPU usage, but mostly shows correct info in the GUI. This will be
improved shortly.

This reverts commit 5144330807, reversing
changes made to c34f3defe1.

Conflicts:
	auto/gui.files.go
2014-09-24 22:01:30 +02:00
Jakob Borg
a502836002 Translation update 2014-09-24 21:55:12 +02:00
Jakob Borg
09417d4b83 Merge remote-tracking branch 'origin/pr/731'
* origin/pr/731:
  Use leveldb database lock for concurrent upgrade protection (fixes #703)
2014-09-24 14:05:37 +02:00
Audrius Butkevicius
83ef2fa84c Add CPU usage tracker for Windows (fixes #729) 2014-09-24 09:57:21 +01:00
Jakob Borg
e596a45e9f Add "cluster introducer" functionality to nodes (ref #120) 2014-09-23 16:04:20 +02:00
Jakob Borg
24e5000c37 Use JoinHostPort for URL that browser opens (fixes #732) 2014-09-23 14:16:16 +02:00
Audrius Butkevicius
e3bcfa17f8 Use leveldb database lock for concurrent upgrade protection (fixes #703)
Doesn't work if config directories are different though
2014-09-22 23:37:19 +01:00
Jakob Borg
3b512676b7 Don't create 'test' file in model dir 2014-09-22 21:34:54 +02:00
Jakob Borg
928198bbfe Use the same temporary naming as the puller 2014-09-22 16:57:06 +02:00
Jakob Borg
1fb56f0ad2 Woops, I screw up the writer again. 2014-09-22 16:53:57 +02:00
Jakob Borg
9797f62cb8 Use ioutil.TempFile, not some nasty homebrew crap 2014-09-22 15:54:36 +02:00
Jakob Borg
4ddd87e773 Locking around osutil.Rename and some descriptive text 2014-09-22 15:48:46 +02:00
Jakob Borg
7fd2e4d2db Use temp file in same location as final .stignore 2014-09-22 15:39:25 +02:00
Jakob Borg
55c7d86205 Text and layout tweaks 2014-09-22 15:22:15 +02:00
Jakob Borg
737a28050c Merge remote-tracking branch 'origin/pr/721'
* origin/pr/721:
  Add tests for model.GetIgnores model.SetIgnores
  Expose ignores in the UI
  Add comments directive to ignores
  Expose ignores rest endpoints
  Expose ignores from model
2014-09-22 14:59:13 +02:00
Jakob Borg
434ecdac6b LocalVersion is unavailable until after AddRepo (fixes #154) 2014-09-22 14:06:25 +02:00
Audrius Butkevicius
709570afcc Add tests for model.GetIgnores model.SetIgnores 2014-09-21 22:35:00 +01:00
Audrius Butkevicius
b084b4faaf Expose ignores in the UI 2014-09-21 22:34:53 +01:00
Audrius Butkevicius
d96ce23451 Add comments directive to ignores 2014-09-21 20:30:13 +01:00
Audrius Butkevicius
760a9d6d35 Expose ignores rest endpoints 2014-09-21 20:30:06 +01:00
Audrius Butkevicius
8e624cedb1 Expose ignores from model 2014-09-21 20:18:21 +01:00
Jakob Borg
39cf269d6b Correct Max Version -> Max Local Version 2014-09-21 15:04:41 +02:00
Jakob Borg
6a00b5a79e Fix import (merge error) that broke the build. 2014-09-20 22:18:03 +02:00
Jakob Borg
2ce674e3fd Merge pull request #717 from Cathryne/master
fixed a typo in GUI
2014-09-20 22:07:38 +02:00
Jakob Borg
0fcc25d7c9 Error handler in staggered Walk() (fixes #718) 2014-09-20 22:06:48 +02:00
Cathryne
63bd0136fb fixed a typo 2014-09-20 21:54:23 +02:00
Jakob Borg
80a2a934dd Correct handling of WasSeen for new nodes 2014-09-20 20:23:44 +02:00
Jakob Borg
e13976a3b3 Adding a node does not require restart; move logic to config package 2014-09-20 20:23:44 +02:00
Jakob Borg
5144330807 Merge branch 'pr/711'
* pr/711:
  Asset update
  Move function-specific constants to the top and rename debouncedFcts
  Improve debounce functionality of REST requests
2014-09-20 20:22:23 +02:00
Jakob Borg
bb29639183 Asset update 2014-09-20 19:20:41 +02:00
Lode Hoste
4667cb9de9 Move function-specific constants to the top and rename debouncedFcts 2014-09-20 17:12:39 +02:00
Jakob Borg
c34f3defe1 l.FatalErr was an antipattern 2014-09-20 15:42:20 +02:00
Jakob Borg
eb0d742672 Chmod error should not be fatal (fixes #612) 2014-09-20 15:41:52 +02:00
Jakob Borg
d9b0a73787 Forgot to check some errors 2014-09-20 15:31:15 +02:00
Jakob Borg
4810879b2f Add Zillode 2014-09-20 15:14:51 +02:00
Lode Hoste
f4b6704aad Improve debounce functionality of REST requests 2014-09-19 22:42:29 +02:00
Jakob Borg
b1a31d3b30 Send correct Node IDs in cluster config message (fixes #707) 2014-09-19 13:21:58 +02:00
Jakob Borg
bf909db3f9 jshint and format app.js 2014-09-18 21:29:29 +02:00
Jakob Borg
9c68be4d5e GET and POST /rest/ping as no-op (fixes #680) 2014-09-18 12:55:28 +02:00
Jakob Borg
d7956dd495 /rest/version should return JSON (fixes #694) 2014-09-18 12:52:45 +02:00
Jakob Borg
37a473e7d6 /rest/errors should return an object (fixes #695) 2014-09-18 12:49:59 +02:00
Jakob Borg
5a1c885e8f Translation update 2014-09-18 12:01:08 +02:00
Jakob Borg
0b1136ad82 Panic if http.Serve() returns an error 2014-09-18 11:46:20 +02:00
Jakob Borg
45af549897 Don't take down HTTP(S) server on connection errors (fixes #700) 2014-09-18 11:45:48 +02:00
Jakob Borg
97844603fc Forced DB GC:s should be done before creating a lot of garbage, not after 2014-09-18 10:04:37 +02:00
Jakob Borg
c07b39e58b Stress test the HTTP(S) server 2014-09-17 17:11:53 +02:00
Jakob Borg
384c543ab9 Ignore patterns should be case insensitive on OS X and Windows 2014-09-16 23:16:39 +02:00
Jakob Borg
592b13d7db Merge branch 'integration-tests'
* integration-tests:
  Script cleanups
  Fail integration tests early
  Improve integration tests
  Add integration test HTTPS certificates
  Upgrade integration test configs
2014-09-16 22:59:42 +02:00
Jakob Borg
6fdba3c02e Script cleanups 2014-09-16 23:26:52 +02:00
Jakob Borg
cbf758ead9 Fail integration tests early 2014-09-16 23:22:03 +02:00
Jakob Borg
d1ad778a64 Improve integration tests 2014-09-16 23:14:19 +02:00
Jakob Borg
ce5ad296ae Add integration test HTTPS certificates 2014-09-16 23:08:24 +02:00
Jakob Borg
797e105786 Upgrade integration test configs 2014-09-16 23:03:18 +02:00
Jakob Borg
d17d80747e Update dependencies (fixes #692) 2014-09-15 18:15:16 +02:00
Jakob Borg
55ea207a55 Merge branch 'new-tls'
* new-tls:
  Cleanups and tweaks
  Add redirection middleware
  Add DowngradingListener

Conflicts:
	auto/gui.files.go
2014-09-15 00:19:07 +02:00
Jakob Borg
6384d1e5a3 Cleanups and tweaks 2014-09-15 00:18:05 +02:00
Jakob Borg
aba01cdace Print error on monitor restart failure 2014-09-14 23:19:28 +02:00
Jakob Borg
517b7a14b4 Merge branch 'pr/683'
* pr/683:
  Restart monitor as part of the upgrade process (fixes #682)
2014-09-14 23:18:48 +02:00
Jakob Borg
2927de7cf9 More than a year ago might as well be never (fixes #690) 2014-09-14 23:16:15 +02:00
Jakob Borg
6471ba70e4 Merge pull request #686 from AudriusButkevicius/auth
Send the real hash as part of the config (fixes #681)
2014-09-14 10:50:17 +02:00
Jakob Borg
9f9de01c51 Merge pull request #685 from AudriusButkevicius/preview
Add usage reporting preview (closes #395)
2014-09-14 10:43:22 +02:00
Audrius Butkevicius
3662decb8b Add redirection middleware 2014-09-13 22:10:55 +01:00
Audrius Butkevicius
583bcfb3c7 Add DowngradingListener
"Inspired" by https://github.com/BenLubar/Rnoadm/maybetls
but avoids pulling the whole game as a dependency, and has the API slightly changed,
as it makes no sense to have non-tcp TLS listeners.
2014-09-13 22:10:47 +01:00
Audrius Butkevicius
c45e3fa4d5 Require username and password for authentication 2014-09-13 22:06:25 +01:00
Audrius Butkevicius
24cbcef620 Send the real hash as part of the config (fixes #681) 2014-09-13 21:52:20 +01:00
Audrius Butkevicius
e2a520ff49 Add usage reporting preview (closes #395) 2014-09-13 21:40:13 +01:00
Audrius Butkevicius
a5e3317e28 Restart monitor as part of the upgrade process (fixes #682) 2014-09-13 15:32:47 +01:00
Jakob Borg
5638c4ba87 Woops (fixup of previous) 2014-09-13 15:11:47 +02:00
Jakob Borg
bf7a128142 Smarter limit on size of pull block queue 2014-09-13 10:57:36 +02:00
Jakob Borg
c5243cd4d5 Translation update 2014-09-11 20:26:10 +02:00
Jakob Borg
db868ed29d Increase restart delay to 60s 2014-09-11 20:25:08 +02:00
Jakob Borg
450c7d80f8 Don't crash on walk error (fixes #663) 2014-09-11 20:23:22 +02:00
Jakob Borg
abbb001975 Typo in panic message (fixes ##662) 2014-09-11 18:42:42 +02:00
Jakob Borg
f35d83ae48 We have an extra field in compressed messages 2014-09-11 18:42:03 +02:00
Jakob Borg
a2315dc95e Merge pull request #665 from spaam/discover-readme
Update magic number in PROTOCOL.md
2014-09-11 18:36:15 +02:00
Johan Andersson
4e2feb6fbc Update magic number in PROTOCOL.md
Use the same magic number as in packets.go
2014-09-11 15:29:27 +02:00
Jakob Borg
13602b6769 Make the restart on wakeup configurable 2014-09-10 22:24:53 +02:00
Jakob Borg
85dba25246 Add pause before restart after standby 2014-09-10 22:20:03 +02:00
Jakob Borg
66432672b3 Clearfix to not hide Add Node on small screens (fixes #659) 2014-09-10 16:57:08 +02:00
Jakob Borg
e6d96e4c18 Fix hit zone for remote nodes accordion (ref #651) 2014-09-10 14:43:23 +02:00
Jakob Borg
9812305bb9 Translation update 2014-09-10 14:21:44 +02:00
Jakob Borg
f680a63a1f Woops, broke LastSeen 2014-09-10 11:29:01 +02:00
Jakob Borg
781d63cb2a UI Tweaks 2014-09-10 11:27:21 +02:00
Jakob Borg
9ff04ee3d8 Don't start when the config dir is not a dir 2014-09-10 10:36:05 +02:00
Jakob Borg
5d85a24977 Don't potentially block forever in Close() (fixes #655) 2014-09-10 08:48:15 +02:00
Jakob Borg
1e51fca0b0 Don't crash on new nodes (fixes #656) 2014-09-10 08:31:30 +02:00
Jakob Borg
5537d53f9a Timestamp the panic log 2014-09-10 08:25:56 +02:00
Jakob Borg
50a4170541 Announce actual port when UPnP is disabled (fixes #657) 2014-09-10 08:22:38 +02:00
Jakob Borg
3a8255bda1 Update lang-en.json 2014-09-10 07:48:35 +02:00
Jakob Borg
a617846f0f Merge pull request #654 from AudriusButkevicius/disco
Check if global discovery was actually started before trying to stop it (fixes #653)
2014-09-10 07:41:49 +02:00
Audrius Butkevicius
5772588c29 Check if global discovery was actually started before trying to stop it (fixes #653) 2014-09-10 00:05:28 +01:00
Jakob Borg
9d0dc45f74 Clarify clickability of top Edit menu (ref #651) 2014-09-08 19:54:11 +02:00
Jakob Borg
c6aefbc9a0 Entire panel title should be clickable (ref #651) 2014-09-08 19:46:33 +02:00
Jakob Borg
dbbafb0cc9 Hide irrelevant fields for disconnected nodes (ref #592) 2014-09-08 19:41:20 +02:00
Jakob Borg
6e8272f78f Implement incoming rate limit (fixes #613) 2014-09-08 17:25:55 +02:00
Jakob Borg
baf8a63121 Announce Server -> Discovery Server 2014-09-08 09:42:33 +02:00
Jakob Borg
fc4a76ee50 Only add one instance of a file to the need list (fixes #592) 2014-09-08 09:37:42 +02:00
Jakob Borg
2117d1d035 Tone down insignificant discovery error messages (ref #241) 2014-09-08 09:14:21 +02:00
Jakob Borg
0a70e0b7b6 Remove orphaned temp files before attempting to remove directories (fixes #492) 2014-09-07 21:29:06 +02:00
Jakob Borg
64ffac5671 Update goleveldb (fixes #644, closes #648) 2014-09-07 14:18:00 +02:00
Jakob Borg
ac384e8a9c Merge remote-tracking branch 'origin/pr/647'
* origin/pr/647:
  Listen for ConfigSaved event in the UI (fixes #244)
  Emit ConfigSaved event
  Save config after updating node name
2014-09-07 14:15:17 +02:00
Jakob Borg
f97c8222c7 Cleanup imports in previous 2014-09-07 14:08:49 +02:00
Jakob Borg
728289ee3a Merge remote-tracking branch 'origin/pr/646'
* origin/pr/646:
  Add session support (fixes #611)
2014-09-07 14:07:21 +02:00
Alexander Graf
5faa16f9ee use modification time for version timestamp; change version format to filename~yyyymmdd-hhmmss 2014-09-07 13:40:22 +02:00
Audrius Butkevicius
4e608b116a Add session support (fixes #611) 2014-09-07 12:10:17 +01:00
Audrius Butkevicius
521b49166e Listen for ConfigSaved event in the UI (fixes #244) 2014-09-07 12:07:25 +01:00
Audrius Butkevicius
8f32decf2d Emit ConfigSaved event 2014-09-07 12:04:40 +01:00
Audrius Butkevicius
0d51f83d2d Save config after updating node name 2014-09-07 12:04:40 +01:00
Audrius Butkevicius
78c6a68db9 Merge pull request #645 from AudriusButkevicius/cfg
Allow saving config from anywhere
2014-09-07 12:02:35 +01:00
Audrius Butkevicius
2949ab73e2 Add tests 2014-09-07 12:00:41 +01:00
Audrius Butkevicius
223741820d Fix tests 2014-09-07 12:00:41 +01:00
Audrius Butkevicius
4b57821f52 Allow saving config from anywhere 2014-09-07 12:00:37 +01:00
Audrius Butkevicius
74271a479f Silence failing ulimit calls 2014-09-06 15:04:49 +01:00
Audrius Butkevicius
c377177108 Fix tests on Windows 2014-09-06 14:56:12 +01:00
Jakob Borg
84eb729bd4 Don't start the browser on restarts (fixes #636) 2014-09-06 07:35:30 +02:00
Jakob Borg
14aea365c5 Don't stop permanently on exit (fixes #637) 2014-09-06 07:28:57 +02:00
Jakob Borg
97cb3fa5a5 Translation update (add Catalan) 2014-09-05 14:24:20 +02:00
Jakob Borg
b5368db704 Update assets 2014-09-05 13:26:17 +02:00
Jakob Borg
8c442b72f3 Merge remote-tracking branch 'origin/pr/634'
* origin/pr/634:
  Removed unused `optionEditor` directive from app.js
  Removed unused `clean` filter from app.js.
  Removed unused `shortPath` filter from app.js.
  Removed  unused `short` filter from app.js.
2014-09-05 13:25:53 +02:00
Jakob Borg
f8f6791d39 Add pyfisch 2014-09-05 13:25:40 +02:00
Pyfisch
0c09f077aa Removed unused optionEditor directive from app.js 2014-09-05 12:42:52 +02:00
Pyfisch
af2831d7b6 Removed unused clean filter from app.js. 2014-09-05 12:40:45 +02:00
Pyfisch
64d5d4aec7 Removed unused shortPath filter from app.js. 2014-09-05 12:39:35 +02:00
Pyfisch
619a6b2adb Removed unused short filter from app.js. 2014-09-05 12:38:21 +02:00
Jakob Borg
33a26bc0cf Merge pull request #631 from AudriusButkevicius/upnp
Check if we had successfully acquired a UPnP mapping before (fixes #627)
2014-09-05 09:09:23 +02:00
Audrius Butkevicius
b445a7c4d3 Check if we had successfully acquired a UPnP mapping before (fixes #627) 2014-09-04 23:02:10 +01:00
Jakob Borg
e6892d0c3e Autogen warning in lang dir 2014-09-04 23:37:23 +02:00
Jakob Borg
33e9a88b56 Proper signal handling in monitor process 2014-09-04 23:31:22 +02:00
Jakob Borg
df00a2251e Pesky copyright is pesky 2014-09-04 22:33:01 +02:00
Jakob Borg
92c44c8abe Rework .stignore functionality (fixes #561) (...)
- Only one .stignore is supported, at the repo root
 - Negative patterns (!) are supported
 - Ignore patterns affect sent and received indexes, not only scanning
2014-09-04 22:30:42 +02:00
Jakob Borg
8e4f7bbd3e Merge pull request #626 from alex2108/master
staggered versioner: count directories as files (fixes #607)
2014-09-04 21:59:38 +02:00
Jakob Borg
a40217cf07 Trim dead bits of code 2014-09-04 22:07:59 +02:00
Jakob Borg
e586fda5f2 Woops, close the right fd 2014-09-04 22:03:25 +02:00
Alexander Graf
a58564ff88 count directories as files (fixes #607) 2014-09-04 16:48:24 +02:00
Jakob Borg
89885b9fb9 Clean up GUI directory 2014-09-04 08:53:28 +02:00
Jakob Borg
5c7d977ae0 Use woff instead of ttf font 2014-09-04 08:47:23 +02:00
Jakob Borg
2cd3ee9698 Dead code cleanup 2014-09-04 08:39:39 +02:00
Jakob Borg
dd3080e018 Copyright cleanup 2014-09-04 08:31:38 +02:00
Jakob Borg
5915e8e86a Don't trust mime.TypeByExtension for the easy stuff (fixes #598) 2014-09-04 08:26:12 +02:00
Jakob Borg
3c67c06654 Merge pull request #619 from marcindziadus/sorting-order
Change sorting order (fix #618)
2014-09-03 23:26:20 +02:00
Marcin
76232ca573 change sorting order 2014-09-03 18:41:45 +02:00
Jakob Borg
5235e82bda Limit number of open db files (fixes #587) 2014-09-02 14:47:36 +02:00
Jakob Borg
10f0713257 Use a monitor process to handle panics and restarts (fixes #586) 2014-09-02 13:24:41 +02:00
Jakob Borg
e9c7970ea4 Only create assets map on demand 2014-09-02 13:07:33 +02:00
Jakob Borg
1a6ac4aeb1 Integration tests should use v4 localhost 2014-09-02 12:10:18 +02:00
Jakob Borg
f633bdddf0 Update goleveldb 2014-09-02 09:44:07 +02:00
Jakob Borg
de0b91d157 Show IPv6 GUI URL correctly 2014-09-01 20:04:22 +02:00
Jakob Borg
2e77e498f5 Use more compact base64 encoding for assets 2014-09-01 20:04:22 +02:00
Jakob Borg
4ac67eb1f9 Merge pull request #589 from AudriusButkevicius/include
Add #include directive to .stignore (fixes #424)
2014-09-01 18:08:53 +02:00
Jakob Borg
2b536de37f Don't fake indexes for stopped repos 2014-09-01 17:48:39 +02:00
Jakob Borg
2ffa92ba1b Warn on startup for stopped repositories 2014-09-01 17:47:18 +02:00
Jakob Borg
6ecddd8388 Don't fail build on Solaris 2014-09-01 17:26:28 +02:00
Jakob Borg
bd2772ea4c If all instances of the global version is invalid, the file should not be on the need list 2014-09-01 09:07:51 +02:00
Audrius Butkevicius
92bf79d53b Fix tests 2014-08-31 22:34:13 +01:00
Audrius Butkevicius
eebe0eeb71 Handle recursive includes 2014-08-31 22:33:49 +01:00
Jakob Borg
1068eaa0b9 Translation update 2014-08-31 21:52:29 +02:00
Jakob Borg
faac3e7d7c Don't clobber staggeredMaxAge = 0 (fixes #604) 2014-08-31 21:44:06 +02:00
Jakob Borg
dab4340207 Merge pull request #603 from AudriusButkevicius/restart
Fix GUI breaking during restarts (fixes #577)
2014-08-31 21:30:51 +02:00
Audrius Butkevicius
fd2567748f Fix GUI breaking during restarts (fixes #577) 2014-08-31 15:49:08 +01:00
Jakob Borg
c2daedbd11 Try not to crash the box with failing tests 2014-08-31 15:36:05 +01:00
Jakob Borg
7c604beb73 Test cases for ignore #include 2014-08-31 15:35:48 +01:00
Audrius Butkevicius
8c42aea827 Add #include directive to .stignore (fixes #424)
Though breaks #502 in a way, as .stignore is not the only place where
stuff gets defined anymore.

Though it never was, as .stignore can be placed in each dir, but I think we
should phase that out in favor of globbing which means that we can then
have a single file, which means that we can have a UI for editing that.

Alternative would be as suggested to include a .stglobalignore which is then synced
as a normal file, but gets included by default.

Then when the UI would have two editors, a local ignore, and a global ignore.
2014-08-31 15:32:22 +01:00
Jakob Borg
cf1bfdfb61 Hold rmut read lock when looking at nodeStatRefs 2014-08-31 13:48:43 +02:00
Jakob Borg
75b26513e1 Don't crash under suspicious circumstances... (fixes #602) 2014-08-31 13:48:16 +02:00
Jakob Borg
6c09a77a97 Clean out index for nonexistent repositories (fixes #549) 2014-08-31 13:34:17 +02:00
Jakob Borg
67389c39fb For now, don't allow changing repo path (ref #549) 2014-08-31 13:05:08 +02:00
Jakob Borg
c326103e6e Add X-Syncthing-Version header to HTTP responses 2014-08-31 12:59:20 +02:00
Jakob Borg
c2120a16da Try to set some reasonable resource limits when running tests 2014-08-30 10:02:10 +02:00
Jakob Borg
258ad4352e Fix connecting to discovered IPv6 address 2014-08-29 17:18:25 +02:00
Jakob Borg
435d3958f4 Update goleveldb 2014-08-29 12:36:45 +02:00
Jakob Borg
b0408ef5c6 Info line formatting (ref #583) 2014-08-28 21:35:55 +02:00
Jakob Borg
1c41b0bc2f Document GOMAXPROCS instead of (useless) STDEADLOCKTIMEOUT 2014-08-28 15:29:49 +02:00
Jakob Borg
aa827f3042 Fix language detection, never show untranslated strings (fixes #543) 2014-08-28 13:23:23 +02:00
Audrius Butkevicius
f44f5964bb Set rescan interval on default repository (fixes #579) 2014-08-27 23:45:09 +01:00
Audrius Butkevicius
91ba93bd7a Merge pull request #571 from syncthing/recheck
Add routine for checking possible standby (fixes #565)
2014-08-27 22:44:36 +01:00
Audrius Butkevicius
0abe4cefb4 Add routine for checking possible standby (fixes #565) 2014-08-27 22:42:59 +01:00
Jakob Borg
bccd460f3b Translation update 2014-08-27 10:20:44 +02:00
Jakob Borg
d1023004e1 Saner error/debug messsages for permission issues 2014-08-27 07:00:15 +02:00
Jakob Borg
04a5f9cb04 Fix fnmatch tests for Windows 2014-08-26 13:26:52 +02:00
Jakob Borg
9818e2b550 Use more fnmatch-like matcher in .stignore (fixes #426) 2014-08-26 11:12:20 +02:00
Jakob Borg
fe43e3b89d Try not to leave directories behind with incorrect permissions 2014-08-26 11:12:20 +02:00
Jakob Borg
e1f1ae041f Don't leak request slots (fixes #483) 2014-08-25 17:48:18 +02:00
Jakob Borg
5bcf26e324 Fix table layout for wide elements, at the price of ellipsis (fixes #326, fixes #309) 2014-08-25 16:37:15 +02:00
Jakob Borg
5f47a8149f Use ISO date format because I'm opinionated 2014-08-25 15:53:32 +02:00
Jakob Borg
00b662b53a Merge branch 'pr/556'
* pr/556:
  Add translation strings
  Display Last Seen value in the UI
  Add /rest/stats/node endpoint
  Add stats package and node related statistics model
2014-08-25 15:52:59 +02:00
Jakob Borg
faf519ab1b Warn about incorrect -goarch values 2014-08-25 14:55:19 +02:00
Jakob Borg
fce73f6f17 Verify CONTRIBUTORS file 2014-08-25 14:55:19 +02:00
Audrius Butkevicius
887890baf5 Add translation strings 2014-08-25 12:57:44 +01:00
Audrius Butkevicius
c66b24feeb Display Last Seen value in the UI 2014-08-25 12:54:50 +01:00
Audrius Butkevicius
84c6f147ad Add /rest/stats/node endpoint 2014-08-25 12:49:22 +01:00
Audrius Butkevicius
0cdb0daa8c Add stats package and node related statistics model 2014-08-25 12:49:21 +01:00
Jakob Borg
eee702f299 Don't run tests in build.sh all 2014-08-25 08:50:13 +02:00
Jakob Borg
df65247325 Increase max path length 1024 -> 8192 bytes (fixes #551)
PATH_MAX seems to be 4096 most of the time; Windows limit is much lower.
2014-08-25 08:48:49 +02:00
Jakob Borg
1a174e75d3 Merge pull request #562 from AudriusButkevicius/restart
Fix race condition while restarting (fixes #560)
2014-08-25 08:03:18 +02:00
Audrius Butkevicius
9e1fd3454f Fix race condition while restarting (fixes #560) 2014-08-25 00:15:28 +01:00
Audrius Butkevicius
3b1603cadf Merge pull request #557 from AudriusButkevicius/opts
Allow configuring GUI options from command line and environment (fixes #505, closes #507)
2014-08-24 16:56:15 +01:00
Audrius Butkevicius
8803bac708 Allow configuring GUI options from command line and environment (fixes #505, closes #507) 2014-08-24 16:55:35 +01:00
Audrius Butkevicius
3a01eaa4a6 Fix build.go on Windows 2014-08-23 21:19:29 +01:00
Jakob Borg
9f84c1c448 New repos must have a default rescan interval (fixes #555) 2014-08-23 19:40:39 +02:00
272 changed files with 7876 additions and 3529 deletions

View File

@@ -39,6 +39,17 @@ attention. In general, any open issues are fair game! Be prepared for a
review](https://discourse.syncthing.net/t/733); it's all in the name of
quality. :)
You may be asked to "rebase" or "squash" your pull request before it's
accepted. This means to make sure that the pull request:
- Is based on the latest commit on `master`.
- Only contains a single commit (exceptions to be made where
appropriate).
- Has a descriptive commit message, ending in `(fixes #123)` if it
resolves an existing issue.
## Licensing
All contributions are made under the same MIT License as the rest of the

View File

@@ -2,13 +2,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>
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com> <frioux@gmail.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>
James Patterson <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
Jens Diemer <github.com@jensdiemer.de> <git@jensdiemer.de>
Lode Hoste <zillode@zillode.be>
Marcin Dziadus <dziadus.marcin@gmail.com>
Michael Tilli <pyfisch@gmail.com>
Philippe Schommers <philippe@schommers.be>
Ryan Sullivan <kayoticsully@gmail.com>
Tully Robinson <tully@tojr.org>

6
Godeps/Godeps.json generated
View File

@@ -37,11 +37,11 @@
},
{
"ImportPath": "github.com/bkaradzic/go-lz4",
"Rev": "77e2ba877bde9da31213bec75dbbe197fa507c21"
"Rev": "93a831dcee242be64a9cc9803dda84af25932de7"
},
{
"ImportPath": "github.com/calmh/xdr",
"Rev": "e1714bbe4764b15490fcc8ebd25d4bd9ea50a4b9"
"Rev": "a597b63b87d6140f79084c8aab214b4d533833a1"
},
{
"ImportPath": "github.com/juju/ratelimit",
@@ -49,7 +49,7 @@
},
{
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
"Rev": "17fd8940e0f778c27793a25bff8c48ddd7bf53ac"
"Rev": "9bca75c48d6c31becfbb127702b425e7226052e3"
},
{
"ImportPath": "github.com/vitrun/qart/coding",

View File

@@ -72,10 +72,18 @@ func main() {
if *decompress {
data, _ = ioutil.ReadAll(input)
data, _ = lz4.Decode(nil, data)
data, err = lz4.Decode(nil, data)
if err != nil {
fmt.Println("Failed to decode:", err)
return
}
} else {
data, _ = ioutil.ReadAll(input)
data, _ = lz4.Encode(nil, data)
data, err = lz4.Encode(nil, data)
if err != nil {
fmt.Println("Failed to encode:", err)
return
}
}
err = ioutil.WriteFile(args[1], data, 0644)

View File

@@ -121,7 +121,7 @@ func Encode(dst, src []byte) ([]byte, error) {
)
for {
if int(e.pos)+4 >= len(e.src) {
if int(e.pos)+12 >= len(e.src) {
e.writeLiterals(uint32(len(e.src))-e.anchor, 0, e.anchor)
return e.dst[:e.dpos], nil
}
@@ -158,7 +158,7 @@ func Encode(dst, src []byte) ([]byte, error) {
ref += minMatch
e.anchor = e.pos
for int(e.pos) < len(e.src) && e.src[e.pos] == e.src[ref] {
for int(e.pos) < len(e.src)-5 && e.src[e.pos] == e.src[ref] {
e.pos++
ref++
}

View File

@@ -22,7 +22,7 @@ func (r *Reader) ReadUint8() uint8 {
}
if debug {
dl.Printf("rd uint8=%d (0x%08x)", r.b[0], r.b[0])
dl.Printf("rd uint8=%d (0x%02x)", r.b[0], r.b[0])
}
return r.b[0]
}
@@ -43,7 +43,7 @@ func (r *Reader) ReadUint16() uint16 {
v := uint16(r.b[1]) | uint16(r.b[0])<<8
if debug {
dl.Printf("rd uint16=%d (0x%08x)", v, v)
dl.Printf("rd uint16=%d (0x%04x)", v, v)
}
return v
}

View File

@@ -249,7 +249,9 @@ func (p *dbBench) newIter() iterator.Iterator {
}
func (p *dbBench) close() {
p.b.Log(p.db.s.tops.bpool)
if bp, err := p.db.GetProperty("leveldb.blockpool"); err == nil {
p.b.Log("Block pool stats: ", bp)
}
p.db.Close()
p.stor.Close()
os.RemoveAll(benchDB)

View File

@@ -11,115 +11,149 @@ import (
"sync/atomic"
)
// SetFunc used by Namespace.Get method to create a cache object. SetFunc
// may return ok false, in that case the cache object will not be created.
type SetFunc func() (ok bool, value interface{}, charge int, fin SetFin)
// SetFunc is the function that will be called by Namespace.Get to create
// a cache object, if charge is less than one than the cache object will
// not be registered to cache tree, if value is nil then the cache object
// will not be created.
type SetFunc func() (charge int, value interface{})
// SetFin will be called when corresponding cache object are released.
type SetFin func()
// DelFin is the function that will be called as the result of a delete operation.
// Exist == true is indication that the object is exist, and pending == true is
// indication of deletion already happen but haven't done yet (wait for all handles
// to be released). And exist == false means the object doesn't exist.
type DelFin func(exist, pending bool)
// DelFin will be called when corresponding cache object are released.
// DelFin will be called after SetFin. The exist is true if the corresponding
// cache object is actually exist in the cache tree.
type DelFin func(exist bool)
// PurgeFin will be called when corresponding cache object are released.
// PurgeFin will be called after SetFin. If PurgeFin present DelFin will
// not be executed but passed to the PurgeFin, it is up to the caller
// to call it or not.
type PurgeFin func(ns, key uint64, delfin DelFin)
// PurgeFin is the function that will be called as the result of a purge operation.
type PurgeFin func(ns, key uint64)
// Cache is a cache tree. A cache instance must be goroutine-safe.
type Cache interface {
// SetCapacity sets cache capacity.
// SetCapacity sets cache tree capacity.
SetCapacity(capacity int)
// GetNamespace gets or creates a cache namespace for the given id.
// Capacity returns cache tree capacity.
Capacity() int
// Used returns used cache tree capacity.
Used() int
// Size returns entire alive cache objects size.
Size() int
// NumObjects returns number of alive objects.
NumObjects() int
// GetNamespace gets cache namespace with the given id.
// GetNamespace is never return nil.
GetNamespace(id uint64) Namespace
// Purge purges all cache namespaces, read Namespace.Purge method documentation.
// PurgeNamespace purges cache namespace with the given id from this cache tree.
// Also read Namespace.Purge.
PurgeNamespace(id uint64, fin PurgeFin)
// ZapNamespace detaches cache namespace with the given id from this cache tree.
// Also read Namespace.Zap.
ZapNamespace(id uint64)
// Purge purges all cache namespace from this cache tree.
// This is behave the same as calling Namespace.Purge method on all cache namespace.
Purge(fin PurgeFin)
// Zap zaps all cache namespaces, read Namespace.Zap method documentation.
Zap(closed bool)
// Zap detaches all cache namespace from this cache tree.
// This is behave the same as calling Namespace.Zap method on all cache namespace.
Zap()
}
// Namespace is a cache namespace. A namespace instance must be goroutine-safe.
type Namespace interface {
// Get gets cache object for the given key. The given SetFunc (if not nil) will
// be called if the given key does not exist.
// If the given key does not exist, SetFunc is nil or SetFunc return ok false, Get
// will return ok false.
Get(key uint64, setf SetFunc) (obj Object, ok bool)
// Get deletes cache object for the given key. If exist the cache object will
// be deleted later when all of its handles have been released (i.e. no one use
// it anymore) and the given DelFin (if not nil) will finally be executed. If
// such cache object does not exist the given DelFin will be executed anyway.
// Get gets cache object with the given key.
// If cache object is not found and setf is not nil, Get will atomically creates
// the cache object by calling setf. Otherwise Get will returns nil.
//
// Delete returns true if such cache object exist.
// The returned cache handle should be released after use by calling Release
// method.
Get(key uint64, setf SetFunc) Handle
// Delete removes cache object with the given key from cache tree.
// A deleted cache object will be released as soon as all of its handles have
// been released.
// Delete only happen once, subsequent delete will consider cache object doesn't
// exist, even if the cache object ins't released yet.
//
// If not nil, fin will be called if the cache object doesn't exist or when
// finally be released.
//
// Delete returns true if such cache object exist and never been deleted.
Delete(key uint64, fin DelFin) bool
// Purge deletes all cache objects, read Delete method documentation.
// Purge removes all cache objects within this namespace from cache tree.
// This is the same as doing delete on all cache objects.
//
// If not nil, fin will be called on all cache objects when its finally be
// released.
Purge(fin PurgeFin)
// Zap detaches the namespace from the cache tree and delete all its cache
// objects. The cache objects deletion and finalizers execution are happen
// immediately, even if its existing handles haven't yet been released.
// A zapped namespace can't never be filled again.
// If closed is false then the Get function will always call the given SetFunc
// if it is not nil, but resultant of the SetFunc will not be cached.
Zap(closed bool)
// Zap detaches namespace from cache tree and release all its cache objects.
// A zapped namespace can never be filled again.
// Calling Get on zapped namespace will always return nil.
Zap()
}
// Object is a cache object.
type Object interface {
// Release releases the cache object. Other methods should not be called
// after the cache object has been released.
// Handle is a cache handle.
type Handle interface {
// Release releases this cache handle. This method can be safely called mutiple
// times.
Release()
// Value returns value of the cache object.
// Value returns value of this cache handle.
// Value will returns nil after this cache handle have be released.
Value() interface{}
}
const (
DelNotExist = iota
DelExist
DelPendig
)
// Namespace state.
type nsState int
const (
nsEffective nsState = iota
nsZapped
nsClosed
)
// Node state.
type nodeState int
const (
nodeEffective nodeState = iota
nodeZero nodeState = iota
nodeEffective
nodeEvicted
nodeRemoved
nodeDeleted
)
// Fake object.
type fakeObject struct {
// Fake handle.
type fakeHandle struct {
value interface{}
fin func()
once uint32
}
func (o *fakeObject) Value() interface{} {
if atomic.LoadUint32(&o.once) == 0 {
return o.value
func (h *fakeHandle) Value() interface{} {
if atomic.LoadUint32(&h.once) == 0 {
return h.value
}
return nil
}
func (o *fakeObject) Release() {
if !atomic.CompareAndSwapUint32(&o.once, 0, 1) {
func (h *fakeHandle) Release() {
if !atomic.CompareAndSwapUint32(&h.once, 0, 1) {
return
}
if o.fin != nil {
o.fin()
o.fin = nil
if h.fin != nil {
h.fin()
h.fin = nil
}
}

View File

@@ -7,15 +7,35 @@
package cache
import (
"fmt"
"math/rand"
"runtime"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
func set(ns Namespace, key uint64, value interface{}, charge int, fin func()) Object {
obj, _ := ns.Get(key, func() (bool, interface{}, int, SetFin) {
return true, value, charge, fin
type releaserFunc struct {
fn func()
value interface{}
}
func (r releaserFunc) Release() {
if r.fn != nil {
r.fn()
}
}
func set(ns Namespace, key uint64, value interface{}, charge int, relf func()) Handle {
return ns.Get(key, func() (int, interface{}) {
if relf != nil {
return charge, releaserFunc{relf, value}
} else {
return charge, value
}
})
return obj
}
func TestCache_HitMiss(t *testing.T) {
@@ -43,29 +63,31 @@ func TestCache_HitMiss(t *testing.T) {
setfin++
}).Release()
for j, y := range cases {
r, ok := ns.Get(y.key, nil)
h := ns.Get(y.key, nil)
if j <= i {
// should hit
if !ok {
if h == nil {
t.Errorf("case '%d' iteration '%d' is miss", i, j)
} else if r.Value().(string) != y.value {
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, r.Value().(string), y.value)
} else {
if x := h.Value().(releaserFunc).value.(string); x != y.value {
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, x, y.value)
}
}
} else {
// should miss
if ok {
t.Errorf("case '%d' iteration '%d' is hit , value '%s'", i, j, r.Value().(string))
if h != nil {
t.Errorf("case '%d' iteration '%d' is hit , value '%s'", i, j, h.Value().(releaserFunc).value.(string))
}
}
if ok {
r.Release()
if h != nil {
h.Release()
}
}
}
for i, x := range cases {
finalizerOk := false
ns.Delete(x.key, func(exist bool) {
ns.Delete(x.key, func(exist, pending bool) {
finalizerOk = true
})
@@ -74,22 +96,24 @@ func TestCache_HitMiss(t *testing.T) {
}
for j, y := range cases {
r, ok := ns.Get(y.key, nil)
h := ns.Get(y.key, nil)
if j > i {
// should hit
if !ok {
if h == nil {
t.Errorf("case '%d' iteration '%d' is miss", i, j)
} else if r.Value().(string) != y.value {
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, r.Value().(string), y.value)
} else {
if x := h.Value().(releaserFunc).value.(string); x != y.value {
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, x, y.value)
}
}
} else {
// should miss
if ok {
t.Errorf("case '%d' iteration '%d' is hit, value '%s'", i, j, r.Value().(string))
if h != nil {
t.Errorf("case '%d' iteration '%d' is hit, value '%s'", i, j, h.Value().(releaserFunc).value.(string))
}
}
if ok {
r.Release()
if h != nil {
h.Release()
}
}
}
@@ -107,42 +131,42 @@ func TestLRUCache_Eviction(t *testing.T) {
set(ns, 3, 3, 1, nil).Release()
set(ns, 4, 4, 1, nil).Release()
set(ns, 5, 5, 1, nil).Release()
if r, ok := ns.Get(2, nil); ok { // 1,3,4,5,2
r.Release()
if h := ns.Get(2, nil); h != nil { // 1,3,4,5,2
h.Release()
}
set(ns, 9, 9, 10, nil).Release() // 5,2,9
for _, x := range []uint64{9, 2, 5, 1} {
r, ok := ns.Get(x, nil)
if !ok {
t.Errorf("miss for key '%d'", x)
for _, key := range []uint64{9, 2, 5, 1} {
h := ns.Get(key, nil)
if h == nil {
t.Errorf("miss for key '%d'", key)
} else {
if r.Value().(int) != int(x) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
if x := h.Value().(int); x != int(key) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
}
r.Release()
h.Release()
}
}
o1.Release()
for _, x := range []uint64{1, 2, 5} {
r, ok := ns.Get(x, nil)
if !ok {
t.Errorf("miss for key '%d'", x)
for _, key := range []uint64{1, 2, 5} {
h := ns.Get(key, nil)
if h == nil {
t.Errorf("miss for key '%d'", key)
} else {
if r.Value().(int) != int(x) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
if x := h.Value().(int); x != int(key) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
}
r.Release()
h.Release()
}
}
for _, x := range []uint64{3, 4, 9} {
r, ok := ns.Get(x, nil)
if ok {
t.Errorf("hit for key '%d'", x)
if r.Value().(int) != int(x) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
for _, key := range []uint64{3, 4, 9} {
h := ns.Get(key, nil)
if h != nil {
t.Errorf("hit for key '%d'", key)
if x := h.Value().(int); x != int(key) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
}
r.Release()
h.Release()
}
}
}
@@ -153,16 +177,15 @@ func TestLRUCache_SetGet(t *testing.T) {
for i := 0; i < 200; i++ {
n := uint64(rand.Intn(99999) % 20)
set(ns, n, n, 1, nil).Release()
if p, ok := ns.Get(n, nil); ok {
if p.Value() == nil {
if h := ns.Get(n, nil); h != nil {
if h.Value() == nil {
t.Errorf("key '%d' contains nil value", n)
} else {
got := p.Value().(uint64)
if got != n {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", n, n, got)
if x := h.Value().(uint64); x != n {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", n, n, x)
}
}
p.Release()
h.Release()
} else {
t.Errorf("key '%d' doesn't exist", n)
}
@@ -176,31 +199,369 @@ func TestLRUCache_Purge(t *testing.T) {
o2 := set(ns1, 2, 2, 1, nil)
ns1.Purge(nil)
set(ns1, 3, 3, 1, nil).Release()
for _, x := range []uint64{1, 2, 3} {
r, ok := ns1.Get(x, nil)
if !ok {
t.Errorf("miss for key '%d'", x)
for _, key := range []uint64{1, 2, 3} {
h := ns1.Get(key, nil)
if h == nil {
t.Errorf("miss for key '%d'", key)
} else {
if r.Value().(int) != int(x) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
if x := h.Value().(int); x != int(key) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
}
r.Release()
h.Release()
}
}
o1.Release()
o2.Release()
for _, x := range []uint64{1, 2} {
r, ok := ns1.Get(x, nil)
if ok {
t.Errorf("hit for key '%d'", x)
if r.Value().(int) != int(x) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
for _, key := range []uint64{1, 2} {
h := ns1.Get(key, nil)
if h != nil {
t.Errorf("hit for key '%d'", key)
if x := h.Value().(int); x != int(key) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
}
r.Release()
h.Release()
}
}
}
type testingCacheObjectCounter struct {
created uint32
released uint32
}
func (c *testingCacheObjectCounter) createOne() {
atomic.AddUint32(&c.created, 1)
}
func (c *testingCacheObjectCounter) releaseOne() {
atomic.AddUint32(&c.released, 1)
}
type testingCacheObject struct {
t *testing.T
cnt *testingCacheObjectCounter
ns, key uint64
releaseCalled uint32
}
func (x *testingCacheObject) Release() {
if atomic.CompareAndSwapUint32(&x.releaseCalled, 0, 1) {
x.cnt.releaseOne()
} else {
x.t.Errorf("duplicate setfin NS#%d KEY#%s", x.ns, x.key)
}
}
func TestLRUCache_Finalizer(t *testing.T) {
const (
capacity = 100
goroutines = 100
iterations = 10000
keymax = 8000
)
runtime.GOMAXPROCS(runtime.NumCPU())
defer runtime.GOMAXPROCS(1)
wg := &sync.WaitGroup{}
cnt := &testingCacheObjectCounter{}
c := NewLRUCache(capacity)
type instance struct {
seed int64
rnd *rand.Rand
ns uint64
effective int32
handles []Handle
handlesMap map[uint64]int
delete bool
purge bool
zap bool
wantDel int32
delfinCalledAll int32
delfinCalledEff int32
purgefinCalled int32
}
instanceGet := func(p *instance, ns Namespace, key uint64) {
h := ns.Get(key, func() (charge int, value interface{}) {
to := &testingCacheObject{
t: t, cnt: cnt,
ns: p.ns,
key: key,
}
atomic.AddInt32(&p.effective, 1)
cnt.createOne()
return 1, releaserFunc{func() {
to.Release()
atomic.AddInt32(&p.effective, -1)
}, to}
})
p.handles = append(p.handles, h)
p.handlesMap[key] = p.handlesMap[key] + 1
}
instanceRelease := func(p *instance, ns Namespace, i int) {
h := p.handles[i]
key := h.Value().(releaserFunc).value.(*testingCacheObject).key
if n := p.handlesMap[key]; n == 0 {
t.Fatal("key ref == 0")
} else if n > 1 {
p.handlesMap[key] = n - 1
} else {
delete(p.handlesMap, key)
}
h.Release()
p.handles = append(p.handles[:i], p.handles[i+1:]...)
p.handles[len(p.handles) : len(p.handles)+1][0] = nil
}
seeds := make([]int64, goroutines)
instances := make([]instance, goroutines)
for i := range instances {
p := &instances[i]
p.handlesMap = make(map[uint64]int)
if seeds[i] == 0 {
seeds[i] = time.Now().UnixNano()
}
p.seed = seeds[i]
p.rnd = rand.New(rand.NewSource(p.seed))
p.ns = uint64(i)
p.delete = i%6 == 0
p.purge = i%8 == 0
p.zap = i%12 == 0 || i%3 == 0
}
seedsStr := make([]string, len(seeds))
for i, seed := range seeds {
seedsStr[i] = fmt.Sprint(seed)
}
t.Logf("seeds := []int64{%s}", strings.Join(seedsStr, ", "))
// Get and release.
for i := range instances {
p := &instances[i]
wg.Add(1)
go func(p *instance) {
defer wg.Done()
ns := c.GetNamespace(p.ns)
for i := 0; i < iterations; i++ {
if len(p.handles) == 0 || p.rnd.Int()%2 == 0 {
instanceGet(p, ns, uint64(p.rnd.Intn(keymax)))
} else {
instanceRelease(p, ns, p.rnd.Intn(len(p.handles)))
}
}
}(p)
}
wg.Wait()
if used, cap := c.Used(), c.Capacity(); used > cap {
t.Errorf("Used > capacity, used=%d cap=%d", used, cap)
}
// Check effective objects.
for i := range instances {
p := &instances[i]
if int(p.effective) < len(p.handlesMap) {
t.Errorf("#%d effective objects < acquired handle, eo=%d ah=%d", i, p.effective, len(p.handlesMap))
}
}
if want := int(cnt.created - cnt.released); c.Size() != want {
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
}
// Delete and purge.
for i := range instances {
p := &instances[i]
p.wantDel = p.effective
wg.Add(1)
go func(p *instance) {
defer wg.Done()
ns := c.GetNamespace(p.ns)
if p.delete {
for key := uint64(0); key < keymax; key++ {
_, wantExist := p.handlesMap[key]
gotExist := ns.Delete(key, func(exist, pending bool) {
atomic.AddInt32(&p.delfinCalledAll, 1)
if exist {
atomic.AddInt32(&p.delfinCalledEff, 1)
}
})
if !gotExist && wantExist {
t.Errorf("delete on NS#%d KEY#%d not found", p.ns, key)
}
}
var delfinCalled int
for key := uint64(0); key < keymax; key++ {
func(key uint64) {
gotExist := ns.Delete(key, func(exist, pending bool) {
if exist && !pending {
t.Errorf("delete fin on NS#%d KEY#%d exist and not pending for deletion", p.ns, key)
}
delfinCalled++
})
if gotExist {
t.Errorf("delete on NS#%d KEY#%d found", p.ns, key)
}
}(key)
}
if delfinCalled != keymax {
t.Errorf("(2) #%d not all delete fin called, diff=%d", p.ns, keymax-delfinCalled)
}
}
if p.purge {
ns.Purge(func(ns, key uint64) {
atomic.AddInt32(&p.purgefinCalled, 1)
})
}
}(p)
}
wg.Wait()
if want := int(cnt.created - cnt.released); c.Size() != want {
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
}
// Release.
for i := range instances {
p := &instances[i]
if !p.zap {
wg.Add(1)
go func(p *instance) {
defer wg.Done()
ns := c.GetNamespace(p.ns)
for i := len(p.handles) - 1; i >= 0; i-- {
instanceRelease(p, ns, i)
}
}(p)
}
}
wg.Wait()
if want := int(cnt.created - cnt.released); c.Size() != want {
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
}
// Zap.
for i := range instances {
p := &instances[i]
if p.zap {
wg.Add(1)
go func(p *instance) {
defer wg.Done()
ns := c.GetNamespace(p.ns)
ns.Zap()
p.handles = nil
p.handlesMap = nil
}(p)
}
}
wg.Wait()
if want := int(cnt.created - cnt.released); c.Size() != want {
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
}
if notrel, used := int(cnt.created-cnt.released), c.Used(); notrel != used {
t.Errorf("Invalid used value, want=%d got=%d", notrel, used)
}
c.Purge(nil)
for i := range instances {
p := &instances[i]
if p.delete {
if p.delfinCalledAll != keymax {
t.Errorf("#%d not all delete fin called, purge=%v zap=%v diff=%d", p.ns, p.purge, p.zap, keymax-p.delfinCalledAll)
}
if p.delfinCalledEff != p.wantDel {
t.Errorf("#%d not all effective delete fin called, diff=%d", p.ns, p.wantDel-p.delfinCalledEff)
}
if p.purge && p.purgefinCalled > 0 {
t.Errorf("#%d some purge fin called, delete=%v zap=%v n=%d", p.ns, p.delete, p.zap, p.purgefinCalled)
}
} else {
if p.purge {
if p.purgefinCalled != p.wantDel {
t.Errorf("#%d not all purge fin called, delete=%v zap=%v diff=%d", p.ns, p.delete, p.zap, p.wantDel-p.purgefinCalled)
}
}
}
}
if cnt.created != cnt.released {
t.Errorf("Some cache object weren't released, created=%d released=%d", cnt.created, cnt.released)
}
}
func BenchmarkLRUCache_Set(b *testing.B) {
c := NewLRUCache(0)
ns := c.GetNamespace(0)
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
set(ns, i, "", 1, nil)
}
}
func BenchmarkLRUCache_Get(b *testing.B) {
c := NewLRUCache(0)
ns := c.GetNamespace(0)
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
set(ns, i, "", 1, nil)
}
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
ns.Get(i, nil)
}
}
func BenchmarkLRUCache_Get2(b *testing.B) {
c := NewLRUCache(0)
ns := c.GetNamespace(0)
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
set(ns, i, "", 1, nil)
}
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
ns.Get(i, func() (charge int, value interface{}) {
return 0, nil
})
}
}
func BenchmarkLRUCache_Release(b *testing.B) {
c := NewLRUCache(0)
ns := c.GetNamespace(0)
handles := make([]Handle, b.N)
for i := uint64(0); i < uint64(b.N); i++ {
handles[i] = set(ns, i, "", 1, nil)
}
b.ResetTimer()
for _, h := range handles {
h.Release()
}
}
func BenchmarkLRUCache_SetRelease(b *testing.B) {
capacity := b.N / 100
if capacity <= 0 {
@@ -210,7 +571,7 @@ func BenchmarkLRUCache_SetRelease(b *testing.B) {
ns := c.GetNamespace(0)
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
set(ns, i, nil, 1, nil).Release()
set(ns, i, "", 1, nil).Release()
}
}
@@ -227,10 +588,10 @@ func BenchmarkLRUCache_SetReleaseTwice(b *testing.B) {
nb := b.N - na
for i := uint64(0); i < uint64(na); i++ {
set(ns, i, nil, 1, nil).Release()
set(ns, i, "", 1, nil).Release()
}
for i := uint64(0); i < uint64(nb); i++ {
set(ns, i, nil, 1, nil).Release()
set(ns, i, "", 1, nil).Release()
}
}

View File

@@ -1,246 +0,0 @@
// Copyright (c) 2013, Suryandaru Triandana <syndtr@gmail.com>
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package cache
import (
"sync"
"sync/atomic"
)
type emptyCache struct {
sync.Mutex
table map[uint64]*emptyNS
}
// NewEmptyCache creates a new initialized empty cache.
func NewEmptyCache() Cache {
return &emptyCache{
table: make(map[uint64]*emptyNS),
}
}
func (c *emptyCache) GetNamespace(id uint64) Namespace {
c.Lock()
defer c.Unlock()
if ns, ok := c.table[id]; ok {
return ns
}
ns := &emptyNS{
cache: c,
id: id,
table: make(map[uint64]*emptyNode),
}
c.table[id] = ns
return ns
}
func (c *emptyCache) Purge(fin PurgeFin) {
c.Lock()
for _, ns := range c.table {
ns.purgeNB(fin)
}
c.Unlock()
}
func (c *emptyCache) Zap(closed bool) {
c.Lock()
for _, ns := range c.table {
ns.zapNB(closed)
}
c.table = make(map[uint64]*emptyNS)
c.Unlock()
}
func (*emptyCache) SetCapacity(capacity int) {}
type emptyNS struct {
cache *emptyCache
id uint64
table map[uint64]*emptyNode
state nsState
}
func (ns *emptyNS) Get(key uint64, setf SetFunc) (o Object, ok bool) {
ns.cache.Lock()
switch ns.state {
case nsZapped:
ns.cache.Unlock()
if setf == nil {
return
}
var value interface{}
var fin func()
ok, value, _, fin = setf()
if ok {
o = &fakeObject{
value: value,
fin: fin,
}
}
return
case nsClosed:
ns.cache.Unlock()
return
}
n, ok := ns.table[key]
if ok {
n.ref++
} else {
if setf == nil {
ns.cache.Unlock()
return
}
var value interface{}
var fin func()
ok, value, _, fin = setf()
if !ok {
ns.cache.Unlock()
return
}
n = &emptyNode{
ns: ns,
key: key,
value: value,
setfin: fin,
ref: 1,
}
ns.table[key] = n
}
ns.cache.Unlock()
o = &emptyObject{node: n}
return
}
func (ns *emptyNS) Delete(key uint64, fin DelFin) bool {
ns.cache.Lock()
if ns.state != nsEffective {
ns.cache.Unlock()
if fin != nil {
fin(false)
}
return false
}
n, ok := ns.table[key]
if !ok {
ns.cache.Unlock()
if fin != nil {
fin(false)
}
return false
}
n.delfin = fin
ns.cache.Unlock()
return true
}
func (ns *emptyNS) purgeNB(fin PurgeFin) {
if ns.state != nsEffective {
return
}
for _, n := range ns.table {
n.purgefin = fin
}
}
func (ns *emptyNS) Purge(fin PurgeFin) {
ns.cache.Lock()
ns.purgeNB(fin)
ns.cache.Unlock()
}
func (ns *emptyNS) zapNB(closed bool) {
if ns.state != nsEffective {
return
}
for _, n := range ns.table {
n.execFin()
}
if closed {
ns.state = nsClosed
} else {
ns.state = nsZapped
}
ns.table = nil
}
func (ns *emptyNS) Zap(closed bool) {
ns.cache.Lock()
ns.zapNB(closed)
delete(ns.cache.table, ns.id)
ns.cache.Unlock()
}
type emptyNode struct {
ns *emptyNS
key uint64
value interface{}
ref int
setfin SetFin
delfin DelFin
purgefin PurgeFin
}
func (n *emptyNode) execFin() {
if n.setfin != nil {
n.setfin()
n.setfin = nil
}
if n.purgefin != nil {
n.purgefin(n.ns.id, n.key, n.delfin)
n.delfin = nil
n.purgefin = nil
} else if n.delfin != nil {
n.delfin(true)
n.delfin = nil
}
}
func (n *emptyNode) evict() {
n.ns.cache.Lock()
n.ref--
if n.ref == 0 {
if n.ns.state == nsEffective {
// Remove elem.
delete(n.ns.table, n.key)
// Execute finalizer.
n.execFin()
}
} else if n.ref < 0 {
panic("leveldb/cache: emptyNode: negative node reference")
}
n.ns.cache.Unlock()
}
type emptyObject struct {
node *emptyNode
once uint32
}
func (o *emptyObject) Value() interface{} {
if atomic.LoadUint32(&o.once) == 0 {
return o.node.value
}
return nil
}
func (o *emptyObject) Release() {
if !atomic.CompareAndSwapUint32(&o.once, 0, 1) {
return
}
o.node.evict()
o.node = nil
}

View File

@@ -9,16 +9,24 @@ package cache
import (
"sync"
"sync/atomic"
"github.com/syndtr/goleveldb/leveldb/util"
)
// The LLRB implementation were taken from https://github.com/petar/GoLLRB.
// Which contains the following header:
//
// Copyright 2010 Petar Maymounkov. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// lruCache represent a LRU cache state.
type lruCache struct {
sync.Mutex
recent lruNode
table map[uint64]*lruNs
capacity int
size int
mu sync.Mutex
recent lruNode
table map[uint64]*lruNs
capacity int
used, size, alive int
}
// NewLRUCache creates a new initialized LRU cache with the given capacity.
@@ -32,245 +40,408 @@ func NewLRUCache(capacity int) Cache {
return c
}
func (c *lruCache) Capacity() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.capacity
}
func (c *lruCache) Used() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.used
}
func (c *lruCache) Size() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.size
}
func (c *lruCache) NumObjects() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.alive
}
// SetCapacity set cache capacity.
func (c *lruCache) SetCapacity(capacity int) {
c.Lock()
c.mu.Lock()
c.capacity = capacity
c.evict()
c.Unlock()
c.mu.Unlock()
}
// GetNamespace return namespace object for given id.
func (c *lruCache) GetNamespace(id uint64) Namespace {
c.Lock()
defer c.Unlock()
c.mu.Lock()
defer c.mu.Unlock()
if p, ok := c.table[id]; ok {
return p
if ns, ok := c.table[id]; ok {
return ns
}
p := &lruNs{
lru: c,
id: id,
table: make(map[uint64]*lruNode),
ns := &lruNs{lru: c, id: id}
c.table[id] = ns
return ns
}
func (c *lruCache) ZapNamespace(id uint64) {
c.mu.Lock()
if ns, exist := c.table[id]; exist {
ns.zapNB()
delete(c.table, id)
}
c.table[id] = p
return p
c.mu.Unlock()
}
func (c *lruCache) PurgeNamespace(id uint64, fin PurgeFin) {
c.mu.Lock()
if ns, exist := c.table[id]; exist {
ns.purgeNB(fin)
}
c.mu.Unlock()
}
// Purge purge entire cache.
func (c *lruCache) Purge(fin PurgeFin) {
c.Lock()
c.mu.Lock()
for _, ns := range c.table {
ns.purgeNB(fin)
}
c.Unlock()
c.mu.Unlock()
}
func (c *lruCache) Zap(closed bool) {
c.Lock()
func (c *lruCache) Zap() {
c.mu.Lock()
for _, ns := range c.table {
ns.zapNB(closed)
ns.zapNB()
}
c.table = make(map[uint64]*lruNs)
c.Unlock()
c.mu.Unlock()
}
func (c *lruCache) evict() {
top := &c.recent
for n := c.recent.rPrev; c.size > c.capacity && n != top; {
for n := c.recent.rPrev; c.used > c.capacity && n != top; {
n.state = nodeEvicted
n.rRemove()
n.evictNB()
c.size -= n.charge
n.derefNB()
c.used -= n.charge
n = c.recent.rPrev
}
}
type lruNs struct {
lru *lruCache
id uint64
table map[uint64]*lruNode
state nsState
lru *lruCache
id uint64
rbRoot *lruNode
state nsState
}
func (ns *lruNs) Get(key uint64, setf SetFunc) (o Object, ok bool) {
lru := ns.lru
lru.Lock()
switch ns.state {
case nsZapped:
lru.Unlock()
if setf == nil {
return
}
var value interface{}
var fin func()
ok, value, _, fin = setf()
if ok {
o = &fakeObject{
value: value,
fin: fin,
}
}
return
case nsClosed:
lru.Unlock()
return
func (ns *lruNs) rbGetOrCreateNode(h *lruNode, key uint64) (hn, n *lruNode) {
if h == nil {
n = &lruNode{ns: ns, key: key}
return n, n
}
n, ok := ns.table[key]
if ok {
switch n.state {
case nodeEvicted:
// Insert to recent list.
n.state = nodeEffective
n.ref++
lru.size += n.charge
lru.evict()
fallthrough
case nodeEffective:
// Bump to front
n.rRemove()
n.rInsert(&lru.recent)
if key < h.key {
hn, n = ns.rbGetOrCreateNode(h.rbLeft, key)
if hn != nil {
h.rbLeft = hn
} else {
return nil, n
}
} else if key > h.key {
hn, n = ns.rbGetOrCreateNode(h.rbRight, key)
if hn != nil {
h.rbRight = hn
} else {
return nil, n
}
n.ref++
} else {
if setf == nil {
lru.Unlock()
return
}
var value interface{}
var charge int
var fin func()
ok, value, charge, fin = setf()
if !ok {
lru.Unlock()
return
}
n = &lruNode{
ns: ns,
key: key,
value: value,
charge: charge,
setfin: fin,
ref: 2,
}
ns.table[key] = n
n.rInsert(&lru.recent)
lru.size += charge
lru.evict()
return nil, h
}
lru.Unlock()
o = &lruObject{node: n}
return
if rbIsRed(h.rbRight) && !rbIsRed(h.rbLeft) {
h = rbRotLeft(h)
}
if rbIsRed(h.rbLeft) && rbIsRed(h.rbLeft.rbLeft) {
h = rbRotRight(h)
}
if rbIsRed(h.rbLeft) && rbIsRed(h.rbRight) {
rbFlip(h)
}
return h, n
}
func (ns *lruNs) getOrCreateNode(key uint64) *lruNode {
hn, n := ns.rbGetOrCreateNode(ns.rbRoot, key)
if hn != nil {
ns.rbRoot = hn
ns.rbRoot.rbBlack = true
}
return n
}
func (ns *lruNs) rbGetNode(key uint64) *lruNode {
h := ns.rbRoot
for h != nil {
switch {
case key < h.key:
h = h.rbLeft
case key > h.key:
h = h.rbRight
default:
return h
}
}
return nil
}
func (ns *lruNs) getNode(key uint64) *lruNode {
return ns.rbGetNode(key)
}
func (ns *lruNs) rbDeleteNode(h *lruNode, key uint64) *lruNode {
if h == nil {
return nil
}
if key < h.key {
if h.rbLeft == nil { // key not present. Nothing to delete
return h
}
if !rbIsRed(h.rbLeft) && !rbIsRed(h.rbLeft.rbLeft) {
h = rbMoveLeft(h)
}
h.rbLeft = ns.rbDeleteNode(h.rbLeft, key)
} else {
if rbIsRed(h.rbLeft) {
h = rbRotRight(h)
}
// If @key equals @h.key and no right children at @h
if h.key == key && h.rbRight == nil {
return nil
}
if h.rbRight != nil && !rbIsRed(h.rbRight) && !rbIsRed(h.rbRight.rbLeft) {
h = rbMoveRight(h)
}
// If @key equals @h.key, and (from above) 'h.Right != nil'
if h.key == key {
var x *lruNode
h.rbRight, x = rbDeleteMin(h.rbRight)
if x == nil {
panic("logic")
}
x.rbLeft, h.rbLeft = h.rbLeft, nil
x.rbRight, h.rbRight = h.rbRight, nil
x.rbBlack = h.rbBlack
h = x
} else { // Else, @key is bigger than @h.key
h.rbRight = ns.rbDeleteNode(h.rbRight, key)
}
}
return rbFixup(h)
}
func (ns *lruNs) deleteNode(key uint64) {
ns.rbRoot = ns.rbDeleteNode(ns.rbRoot, key)
if ns.rbRoot != nil {
ns.rbRoot.rbBlack = true
}
}
func (ns *lruNs) rbIterateNodes(h *lruNode, pivot uint64, iter func(n *lruNode) bool) bool {
if h == nil {
return true
}
if h.key >= pivot {
if !ns.rbIterateNodes(h.rbLeft, pivot, iter) {
return false
}
if !iter(h) {
return false
}
}
return ns.rbIterateNodes(h.rbRight, pivot, iter)
}
func (ns *lruNs) iterateNodes(iter func(n *lruNode) bool) {
ns.rbIterateNodes(ns.rbRoot, 0, iter)
}
func (ns *lruNs) Get(key uint64, setf SetFunc) Handle {
ns.lru.mu.Lock()
defer ns.lru.mu.Unlock()
if ns.state != nsEffective {
return nil
}
var n *lruNode
if setf == nil {
n = ns.getNode(key)
if n == nil {
return nil
}
} else {
n = ns.getOrCreateNode(key)
}
switch n.state {
case nodeZero:
charge, value := setf()
if value == nil {
ns.deleteNode(key)
return nil
}
if charge < 0 {
charge = 0
}
n.value = value
n.charge = charge
n.state = nodeEvicted
ns.lru.size += charge
ns.lru.alive++
fallthrough
case nodeEvicted:
if n.charge == 0 {
break
}
// Insert to recent list.
n.state = nodeEffective
n.ref++
ns.lru.used += n.charge
ns.lru.evict()
fallthrough
case nodeEffective:
// Bump to front.
n.rRemove()
n.rInsert(&ns.lru.recent)
}
n.ref++
return &lruHandle{node: n}
}
func (ns *lruNs) Delete(key uint64, fin DelFin) bool {
lru := ns.lru
lru.Lock()
ns.lru.mu.Lock()
defer ns.lru.mu.Unlock()
if ns.state != nsEffective {
lru.Unlock()
if fin != nil {
fin(false)
fin(false, false)
}
return false
}
n, ok := ns.table[key]
if !ok {
lru.Unlock()
n := ns.getNode(key)
if n == nil {
if fin != nil {
fin(false)
fin(false, false)
}
return false
}
n.delfin = fin
switch n.state {
case nodeRemoved:
lru.Unlock()
return false
case nodeEffective:
lru.size -= n.charge
ns.lru.used -= n.charge
n.state = nodeDeleted
n.delfin = fin
n.rRemove()
n.evictNB()
n.derefNB()
case nodeEvicted:
n.state = nodeDeleted
n.delfin = fin
case nodeDeleted:
if fin != nil {
fin(true, true)
}
return false
default:
panic("invalid state")
}
n.state = nodeRemoved
lru.Unlock()
return true
}
func (ns *lruNs) purgeNB(fin PurgeFin) {
lru := ns.lru
if ns.state != nsEffective {
return
}
for _, n := range ns.table {
n.purgefin = fin
if n.state == nodeEffective {
lru.size -= n.charge
n.rRemove()
n.evictNB()
if ns.state == nsEffective {
var nodes []*lruNode
ns.iterateNodes(func(n *lruNode) bool {
nodes = append(nodes, n)
return true
})
for _, n := range nodes {
switch n.state {
case nodeEffective:
ns.lru.used -= n.charge
n.state = nodeDeleted
n.purgefin = fin
n.rRemove()
n.derefNB()
case nodeEvicted:
n.state = nodeDeleted
n.purgefin = fin
case nodeDeleted:
default:
panic("invalid state")
}
}
n.state = nodeRemoved
}
}
func (ns *lruNs) Purge(fin PurgeFin) {
ns.lru.Lock()
ns.lru.mu.Lock()
ns.purgeNB(fin)
ns.lru.Unlock()
ns.lru.mu.Unlock()
}
func (ns *lruNs) zapNB(closed bool) {
lru := ns.lru
if ns.state != nsEffective {
return
}
if closed {
ns.state = nsClosed
} else {
func (ns *lruNs) zapNB() {
if ns.state == nsEffective {
ns.state = nsZapped
ns.iterateNodes(func(n *lruNode) bool {
if n.state == nodeEffective {
ns.lru.used -= n.charge
n.rRemove()
}
ns.lru.size -= n.charge
n.state = nodeDeleted
n.fin()
return true
})
ns.rbRoot = nil
}
for _, n := range ns.table {
if n.state == nodeEffective {
lru.size -= n.charge
n.rRemove()
}
n.state = nodeRemoved
n.execFin()
}
ns.table = nil
}
func (ns *lruNs) Zap(closed bool) {
ns.lru.Lock()
ns.zapNB(closed)
func (ns *lruNs) Zap() {
ns.lru.mu.Lock()
ns.zapNB()
delete(ns.lru.table, ns.id)
ns.lru.Unlock()
ns.lru.mu.Unlock()
}
type lruNode struct {
ns *lruNs
rNext, rPrev *lruNode
rNext, rPrev *lruNode
rbLeft, rbRight *lruNode
rbBlack bool
key uint64
value interface{}
charge int
ref int
state nodeState
setfin SetFin
delfin DelFin
purgefin PurgeFin
}
@@ -284,7 +455,6 @@ func (n *lruNode) rInsert(at *lruNode) {
}
func (n *lruNode) rRemove() bool {
// only remove if not already removed
if n.rPrev == nil {
return false
}
@@ -297,58 +467,147 @@ func (n *lruNode) rRemove() bool {
return true
}
func (n *lruNode) execFin() {
if n.setfin != nil {
n.setfin()
n.setfin = nil
func (n *lruNode) fin() {
if r, ok := n.value.(util.Releaser); ok {
r.Release()
}
if n.purgefin != nil {
n.purgefin(n.ns.id, n.key, n.delfin)
n.purgefin(n.ns.id, n.key)
n.delfin = nil
n.purgefin = nil
} else if n.delfin != nil {
n.delfin(true)
n.delfin(true, false)
n.delfin = nil
}
}
func (n *lruNode) evictNB() {
func (n *lruNode) derefNB() {
n.ref--
if n.ref == 0 {
if n.ns.state == nsEffective {
// remove elem
delete(n.ns.table, n.key)
// execute finalizer
n.execFin()
// Remove elemement.
n.ns.deleteNode(n.key)
n.ns.lru.size -= n.charge
n.ns.lru.alive--
n.fin()
}
n.value = nil
} else if n.ref < 0 {
panic("leveldb/cache: lruCache: negative node reference")
}
}
func (n *lruNode) evict() {
n.ns.lru.Lock()
n.evictNB()
n.ns.lru.Unlock()
func (n *lruNode) deref() {
n.ns.lru.mu.Lock()
n.derefNB()
n.ns.lru.mu.Unlock()
}
type lruObject struct {
type lruHandle struct {
node *lruNode
once uint32
}
func (o *lruObject) Value() interface{} {
if atomic.LoadUint32(&o.once) == 0 {
return o.node.value
func (h *lruHandle) Value() interface{} {
if atomic.LoadUint32(&h.once) == 0 {
return h.node.value
}
return nil
}
func (o *lruObject) Release() {
if !atomic.CompareAndSwapUint32(&o.once, 0, 1) {
func (h *lruHandle) Release() {
if !atomic.CompareAndSwapUint32(&h.once, 0, 1) {
return
}
o.node.evict()
o.node = nil
h.node.deref()
h.node = nil
}
func rbIsRed(h *lruNode) bool {
if h == nil {
return false
}
return !h.rbBlack
}
func rbRotLeft(h *lruNode) *lruNode {
x := h.rbRight
if x.rbBlack {
panic("rotating a black link")
}
h.rbRight = x.rbLeft
x.rbLeft = h
x.rbBlack = h.rbBlack
h.rbBlack = false
return x
}
func rbRotRight(h *lruNode) *lruNode {
x := h.rbLeft
if x.rbBlack {
panic("rotating a black link")
}
h.rbLeft = x.rbRight
x.rbRight = h
x.rbBlack = h.rbBlack
h.rbBlack = false
return x
}
func rbFlip(h *lruNode) {
h.rbBlack = !h.rbBlack
h.rbLeft.rbBlack = !h.rbLeft.rbBlack
h.rbRight.rbBlack = !h.rbRight.rbBlack
}
func rbMoveLeft(h *lruNode) *lruNode {
rbFlip(h)
if rbIsRed(h.rbRight.rbLeft) {
h.rbRight = rbRotRight(h.rbRight)
h = rbRotLeft(h)
rbFlip(h)
}
return h
}
func rbMoveRight(h *lruNode) *lruNode {
rbFlip(h)
if rbIsRed(h.rbLeft.rbLeft) {
h = rbRotRight(h)
rbFlip(h)
}
return h
}
func rbFixup(h *lruNode) *lruNode {
if rbIsRed(h.rbRight) {
h = rbRotLeft(h)
}
if rbIsRed(h.rbLeft) && rbIsRed(h.rbLeft.rbLeft) {
h = rbRotRight(h)
}
if rbIsRed(h.rbLeft) && rbIsRed(h.rbRight) {
rbFlip(h)
}
return h
}
func rbDeleteMin(h *lruNode) (hn, n *lruNode) {
if h == nil {
return nil, nil
}
if h.rbLeft == nil {
return nil, h
}
if !rbIsRed(h.rbLeft) && !rbIsRed(h.rbLeft.rbLeft) {
h = rbMoveLeft(h)
}
h.rbLeft, n = rbDeleteMin(h.rbLeft)
return rbFixup(h), n
}

View File

@@ -14,6 +14,7 @@ import (
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/syndtr/goleveldb/leveldb/iterator"
@@ -35,7 +36,7 @@ type DB struct {
// MemDB.
memMu sync.RWMutex
memPool *util.Pool
memPool chan *memdb.DB
mem, frozenMem *memDB
journal *journal.Writer
journalWriter storage.Writer
@@ -47,6 +48,9 @@ type DB struct {
snapsMu sync.Mutex
snapsRoot snapshotElement
// Stats.
aliveSnaps, aliveIters int32
// Write.
writeC chan *Batch
writeMergedC chan bool
@@ -80,7 +84,7 @@ func openDB(s *session) (*DB, error) {
// Initial sequence
seq: s.stSeq,
// MemDB
memPool: util.NewPool(1),
memPool: make(chan *memdb.DB, 1),
// Write
writeC: make(chan *Batch),
writeMergedC: make(chan bool),
@@ -122,6 +126,7 @@ func openDB(s *session) (*DB, error) {
go db.tCompaction()
go db.mCompaction()
go db.jWriter()
go db.mpoolDrain()
s.logf("db@open done T·%v", time.Since(start))
@@ -568,7 +573,7 @@ func (db *DB) get(key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, er
}
defer m.decref()
mk, mv, me := m.db.Find(ikey)
mk, mv, me := m.mdb.Find(ikey)
if me == nil {
ukey, _, t, ok := parseIkey(mk)
if ok && db.s.icmp.uCompare(ukey, key) == 0 {
@@ -655,6 +660,16 @@ func (db *DB) GetSnapshot() (*Snapshot, error) {
// Returns statistics of the underlying DB.
// leveldb.sstables
// Returns sstables list for each level.
// leveldb.blockpool
// Returns block pool stats.
// leveldb.cachedblock
// Returns size of cached block.
// leveldb.openedtables
// Returns number of opened tables.
// leveldb.alivesnaps
// Returns number of alive snapshots.
// leveldb.aliveiters
// Returns number of alive iterators.
func (db *DB) GetProperty(name string) (value string, err error) {
err = db.ok()
if err != nil {
@@ -700,6 +715,20 @@ func (db *DB) GetProperty(name string) (value string, err error) {
value += fmt.Sprintf("%d:%d[%q .. %q]\n", t.file.Num(), t.size, t.imin, t.imax)
}
}
case p == "blockpool":
value = fmt.Sprintf("%v", db.s.tops.bpool)
case p == "cachedblock":
if bc := db.s.o.GetBlockCache(); bc != nil {
value = fmt.Sprintf("%d", bc.Size())
} else {
value = "<nil>"
}
case p == "openedtables":
value = fmt.Sprintf("%d", db.s.tops.cache.Size())
case p == "alivesnaps":
value = fmt.Sprintf("%d", atomic.LoadInt32(&db.aliveSnaps))
case p == "aliveiters":
value = fmt.Sprintf("%d", atomic.LoadInt32(&db.aliveIters))
default:
err = errors.New("leveldb: GetProperty: unknown property: " + name)
}

View File

@@ -221,10 +221,10 @@ func (db *DB) memCompaction() {
c := newCMem(db.s)
stats := new(cStatsStaging)
db.logf("mem@flush N·%d S·%s", mem.db.Len(), shortenb(mem.db.Size()))
db.logf("mem@flush N·%d S·%s", mem.mdb.Len(), shortenb(mem.mdb.Size()))
// Don't compact empty memdb.
if mem.db.Len() == 0 {
if mem.mdb.Len() == 0 {
db.logf("mem@flush skipping")
// drop frozen mem
db.dropFrozenMem()
@@ -242,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.db, -1)
return c.flush(mem.mdb, -1)
}, func() error {
for _, r := range c.rec.addedTables {
db.logf("mem@flush rollback @%d", r.num)

View File

@@ -10,6 +10,7 @@ import (
"errors"
"runtime"
"sync"
"sync/atomic"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/opt"
@@ -38,11 +39,11 @@ 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)
emi := em.db.NewIterator(slice)
emi := em.mdb.NewIterator(slice)
emi.SetReleaser(&memdbReleaser{m: em})
i = append(i, emi)
if fm != nil {
fmi := fm.db.NewIterator(slice)
fmi := fm.mdb.NewIterator(slice)
fmi.SetReleaser(&memdbReleaser{m: fm})
i = append(i, fmi)
}
@@ -66,6 +67,7 @@ func (db *DB) newIterator(seq uint64, slice *util.Range, ro *opt.ReadOptions) *d
}
rawIter := db.newRawIterator(islice, ro)
iter := &dbIter{
db: db,
icmp: db.s.icmp,
iter: rawIter,
seq: seq,
@@ -73,6 +75,7 @@ func (db *DB) newIterator(seq uint64, slice *util.Range, ro *opt.ReadOptions) *d
key: make([]byte, 0),
value: make([]byte, 0),
}
atomic.AddInt32(&db.aliveIters, 1)
runtime.SetFinalizer(iter, (*dbIter).Release)
return iter
}
@@ -89,6 +92,7 @@ const (
// dbIter represent an interator states over a database session.
type dbIter struct {
db *DB
icmp *iComparer
iter iterator.Iterator
seq uint64
@@ -303,6 +307,7 @@ func (i *dbIter) Release() {
if i.releaser != nil {
i.releaser.Release()
i.releaser = nil
}
i.dir = dirReleased
@@ -310,6 +315,8 @@ func (i *dbIter) Release() {
i.value = nil
i.iter.Release()
i.iter = nil
atomic.AddInt32(&i.db.aliveIters, -1)
i.db = nil
}
}

View File

@@ -9,6 +9,7 @@ package leveldb
import (
"runtime"
"sync"
"sync/atomic"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/opt"
@@ -81,7 +82,7 @@ func (db *DB) minSeq() uint64 {
type Snapshot struct {
db *DB
elem *snapshotElement
mu sync.Mutex
mu sync.RWMutex
released bool
}
@@ -91,6 +92,7 @@ func (db *DB) newSnapshot() *Snapshot {
db: db,
elem: db.acquireSnapshot(),
}
atomic.AddInt32(&db.aliveSnaps, 1)
runtime.SetFinalizer(snap, (*Snapshot).Release)
return snap
}
@@ -105,8 +107,8 @@ func (snap *Snapshot) Get(key []byte, ro *opt.ReadOptions) (value []byte, err er
if err != nil {
return
}
snap.mu.Lock()
defer snap.mu.Unlock()
snap.mu.RLock()
defer snap.mu.RUnlock()
if snap.released {
err = ErrSnapshotReleased
return
@@ -160,6 +162,7 @@ func (snap *Snapshot) Release() {
snap.released = true
snap.db.releaseSnapshot(snap.elem)
atomic.AddInt32(&snap.db.aliveSnaps, -1)
snap.db = nil
snap.elem = nil
}

View File

@@ -8,16 +8,16 @@ package leveldb
import (
"sync/atomic"
"time"
"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
db *DB
mdb *memdb.DB
ref int32
}
func (m *memDB) incref() {
@@ -26,7 +26,13 @@ func (m *memDB) incref() {
func (m *memDB) decref() {
if ref := atomic.AddInt32(&m.ref, -1); ref == 0 {
m.pool.Put(m)
// Only put back memdb with std capacity.
if m.mdb.Capacity() == m.db.s.o.GetWriteBuffer() {
m.mdb.Reset()
m.db.mpoolPut(m.mdb)
}
m.db = nil
m.mdb = nil
} else if ref < 0 {
panic("negative memdb ref")
}
@@ -42,6 +48,41 @@ func (db *DB) addSeq(delta uint64) {
atomic.AddUint64(&db.seq, delta)
}
func (db *DB) mpoolPut(mem *memdb.DB) {
defer func() {
recover()
}()
select {
case db.memPool <- mem:
default:
}
}
func (db *DB) mpoolGet() *memdb.DB {
select {
case mem := <-db.memPool:
return mem
default:
return nil
}
}
func (db *DB) mpoolDrain() {
ticker := time.NewTicker(30 * time.Second)
for {
select {
case <-ticker.C:
select {
case <-db.memPool:
default:
}
case _, _ = <-db.closeC:
close(db.memPool)
return
}
}
}
// Create new memdb and froze the old one; need external synchronization.
// newMem only called synchronously by the writer.
func (db *DB) newMem(n int) (mem *memDB, err error) {
@@ -70,18 +111,15 @@ func (db *DB) newMem(n int) (mem *memDB, err error) {
db.journalWriter = w
db.journalFile = file
db.frozenMem = 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,
}
mdb := db.mpoolGet()
if mdb == nil || mdb.Capacity() < n {
mdb = memdb.New(db.s.icmp, maxInt(db.s.o.GetWriteBuffer(), n))
}
mem = &memDB{
db: db,
mdb: mdb,
ref: 2,
}
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.

View File

@@ -1210,7 +1210,7 @@ func TestDb_DeletionMarkers2(t *testing.T) {
}
func TestDb_CompactionTableOpenError(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{MaxOpenFiles: 0})
h := newDbHarnessWopt(t, &opt.Options{CachedOpenFiles: -1})
defer h.close()
im := 10
@@ -1577,7 +1577,11 @@ func TestDb_BloomFilter(t *testing.T) {
return fmt.Sprintf("key%06d", i)
}
n := 10000
const (
n = 10000
indexOverhead = 19898
filterOverhead = 19799
)
// Populate multiple layers
for i := 0; i < n; i++ {
@@ -1601,7 +1605,7 @@ func TestDb_BloomFilter(t *testing.T) {
cnt := int(h.stor.ReadCounter())
t.Logf("lookup of %d present keys yield %d sstable I/O reads", n, cnt)
if min, max := n, n+2*n/100; cnt < min || cnt > max {
if min, max := n+indexOverhead+filterOverhead, n+indexOverhead+filterOverhead+2*n/100; cnt < min || cnt > max {
t.Errorf("num of sstable I/O reads of present keys not in range of %d - %d, got %d", min, max, cnt)
}
@@ -1612,7 +1616,7 @@ func TestDb_BloomFilter(t *testing.T) {
}
cnt = int(h.stor.ReadCounter())
t.Logf("lookup of %d missing keys yield %d sstable I/O reads", n, cnt)
if max := 3 * n / 100; cnt > max {
if max := 3*n/100 + indexOverhead + filterOverhead; cnt > max {
t.Errorf("num of sstable I/O reads of missing keys was more than %d, got %d", max, cnt)
}

View File

@@ -75,7 +75,7 @@ func (db *DB) flush(n int) (mem *memDB, nn int, err error) {
mem = nil
}
}()
nn = mem.db.Free()
nn = mem.mdb.Free()
switch {
case v.tLen(0) >= kL0_SlowdownWritesTrigger && !delayed:
delayed = true
@@ -90,13 +90,13 @@ func (db *DB) flush(n int) (mem *memDB, nn int, err error) {
}
default:
// Allow memdb to grow if it has no entry.
if mem.db.Len() == 0 {
if mem.mdb.Len() == 0 {
nn = n
} else {
mem.decref()
mem, err = db.rotateMem(n)
if err == nil {
nn = mem.db.Free()
nn = mem.mdb.Free()
} else {
nn = 0
}
@@ -190,7 +190,7 @@ drain:
return
case db.journalC <- b:
// Write into memdb
b.memReplay(mem.db)
b.memReplay(mem.mdb)
}
// Wait for journal writer
select {
@@ -200,7 +200,7 @@ drain:
case err = <-db.journalAckC:
if err != nil {
// Revert memdb if error detected
b.revertMemReplay(mem.db)
b.revertMemReplay(mem.mdb)
return
}
}
@@ -209,7 +209,7 @@ drain:
if err != nil {
return
}
b.memReplay(mem.db)
b.memReplay(mem.mdb)
}
// Set last seq number.
@@ -271,7 +271,7 @@ func (db *DB) CompactRange(r util.Range) error {
// Check for overlaps in memdb.
mem := db.getEffectiveMem()
defer mem.decref()
if isMemOverlaps(db.s.icmp, mem.db, r.Start, r.Limit) {
if isMemOverlaps(db.s.icmp, mem.mdb, r.Start, r.Limit) {
// Memdb compaction.
if _, err := db.rotateMem(0); err != nil {
<-db.writeLockC

View File

@@ -37,6 +37,16 @@
// err = iter.Error()
// ...
//
// Iterate over subset of database content with a particular prefix:
// iter := db.NewIterator(util.BytesPrefix([]byte("foo-")), nil)
// for iter.Next() {
// // Use key/value.
// ...
// }
// iter.Release()
// err = iter.Error()
// ...
//
// Seek-then-Iterate:
//
// iter := db.NewIterator(nil, nil)

View File

@@ -21,7 +21,7 @@ var _ = testutil.Defer(func() {
BlockRestartInterval: 5,
BlockSize: 50,
Compression: opt.NoCompression,
MaxOpenFiles: 0,
CachedOpenFiles: -1,
Strict: opt.StrictAll,
WriteBuffer: 1000,
}
@@ -40,18 +40,17 @@ var _ = testutil.Defer(func() {
})
Describe("read test", func() {
testutil.AllKeyValueTesting(nil, func(kv testutil.KeyValue) testutil.DB {
testutil.AllKeyValueTesting(nil, nil, func(kv testutil.KeyValue) testutil.DB {
// Building the DB.
db := newTestingDB(o, nil, nil)
kv.IterateShuffled(nil, func(i int, key, value []byte) {
err := db.TestPut(key, value)
Expect(err).NotTo(HaveOccurred())
})
testutil.Defer("teardown", func() {
db.TestClose()
})
return db
}, func(db testutil.DB) {
db.(*testingDB).TestClose()
})
})
})

View File

@@ -129,7 +129,7 @@ var _ = testutil.Defer(func() {
}
return db
})
}, nil, nil)
})
})
})

View File

@@ -24,16 +24,22 @@ const (
DefaultBlockRestartInterval = 16
DefaultBlockSize = 4 * KiB
DefaultCompressionType = SnappyCompression
DefaultMaxOpenFiles = 1000
DefaultCachedOpenFiles = 500
DefaultWriteBuffer = 4 * MiB
)
type noCache struct{}
func (noCache) SetCapacity(capacity int) {}
func (noCache) GetNamespace(id uint64) cache.Namespace { return nil }
func (noCache) Purge(fin cache.PurgeFin) {}
func (noCache) Zap(closed bool) {}
func (noCache) SetCapacity(capacity int) {}
func (noCache) Capacity() int { return 0 }
func (noCache) Used() int { return 0 }
func (noCache) Size() int { return 0 }
func (noCache) NumObjects() int { return 0 }
func (noCache) GetNamespace(id uint64) cache.Namespace { return nil }
func (noCache) PurgeNamespace(id uint64, fin cache.PurgeFin) {}
func (noCache) ZapNamespace(id uint64) {}
func (noCache) Purge(fin cache.PurgeFin) {}
func (noCache) Zap() {}
var NoCache cache.Cache = noCache{}
@@ -119,6 +125,13 @@ type Options struct {
// The default value is 4KiB.
BlockSize int
// CachedOpenFiles defines number of open files to kept around when not
// in-use, the counting includes still in-use files.
// Set this to negative value to disable caching.
//
// The default value is 500.
CachedOpenFiles int
// Comparer defines a total ordering over the space of []byte keys: a 'less
// than' relationship. The same comparison algorithm must be used for reads
// and writes over the lifetime of the DB.
@@ -159,13 +172,6 @@ type Options struct {
// The default value is nil.
Filter filter.Filter
// MaxOpenFiles defines maximum number of open files to kept around
// (cached). This is not an hard limit, actual open files may exceed
// the defined value.
//
// The default value is 1000.
MaxOpenFiles int
// Strict defines the DB strict level.
Strict Strict
@@ -207,6 +213,15 @@ func (o *Options) GetBlockSize() int {
return o.BlockSize
}
func (o *Options) GetCachedOpenFiles() int {
if o == nil || o.CachedOpenFiles == 0 {
return DefaultCachedOpenFiles
} else if o.CachedOpenFiles < 0 {
return 0
}
return o.CachedOpenFiles
}
func (o *Options) GetComparer() comparer.Comparer {
if o == nil || o.Comparer == nil {
return comparer.DefaultComparer
@@ -242,13 +257,6 @@ func (o *Options) GetFilter() filter.Filter {
return o.Filter
}
func (o *Options) GetMaxOpenFiles() int {
if o == nil || o.MaxOpenFiles <= 0 {
return DefaultMaxOpenFiles
}
return o.MaxOpenFiles
}
func (o *Options) GetStrict(strict Strict) bool {
if o == nil || o.Strict == 0 {
return DefaultStrict&strict != 0

View File

@@ -58,7 +58,7 @@ func newSession(stor storage.Storage, o *opt.Options) (s *session, err error) {
storLock: storLock,
}
s.setOptions(o)
s.tops = newTableOps(s, s.o.GetMaxOpenFiles())
s.tops = newTableOps(s, s.o.GetCachedOpenFiles())
s.setVersion(&version{s: s})
s.log("log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock D·DeletedEntry L·Level Q·SeqNum T·TimeElapsed")
return

View File

@@ -297,7 +297,7 @@ func (t *tOps) create() (*tWriter, error) {
func (t *tOps) createFrom(src iterator.Iterator) (f *tFile, n int, err error) {
w, err := t.create()
if err != nil {
return f, n, err
return
}
defer func() {
@@ -322,33 +322,24 @@ func (t *tOps) createFrom(src iterator.Iterator) (f *tFile, n int, err error) {
return
}
// Opens table. It returns a cache object, which should
// Opens table. It returns a cache handle, which should
// be released after use.
func (t *tOps) open(f *tFile) (c cache.Object, err error) {
func (t *tOps) open(f *tFile) (ch cache.Handle, err error) {
num := f.file.Num()
c, ok := t.cacheNS.Get(num, func() (ok bool, value interface{}, charge int, fin cache.SetFin) {
ch = t.cacheNS.Get(num, func() (charge int, value interface{}) {
var r storage.Reader
r, err = f.file.Open()
if err != nil {
return
return 0, nil
}
o := t.s.o
var cacheNS cache.Namespace
if bc := o.GetBlockCache(); bc != nil {
cacheNS = bc.GetNamespace(num)
var bcacheNS cache.Namespace
if bc := t.s.o.GetBlockCache(); bc != nil {
bcacheNS = bc.GetNamespace(num)
}
ok = true
value = table.NewReader(r, int64(f.size), cacheNS, t.bpool, o)
charge = 1
fin = func() {
r.Close()
}
return
return 1, table.NewReader(r, int64(f.size), bcacheNS, t.bpool, t.s.o)
})
if !ok && err == nil {
if ch == nil && err == nil {
err = ErrClosed
}
return
@@ -357,34 +348,33 @@ func (t *tOps) open(f *tFile) (c cache.Object, err error) {
// Finds key/value pair whose key is greater than or equal to the
// given key.
func (t *tOps) find(f *tFile, key []byte, ro *opt.ReadOptions) (rkey, rvalue []byte, err error) {
c, err := t.open(f)
ch, err := t.open(f)
if err != nil {
return nil, nil, err
}
defer c.Release()
return c.Value().(*table.Reader).Find(key, ro)
defer ch.Release()
return ch.Value().(*table.Reader).Find(key, ro)
}
// Returns approximate offset of the given key.
func (t *tOps) offsetOf(f *tFile, key []byte) (offset uint64, err error) {
c, err := t.open(f)
ch, err := t.open(f)
if err != nil {
return
}
_offset, err := c.Value().(*table.Reader).OffsetOf(key)
offset = uint64(_offset)
c.Release()
return
defer ch.Release()
offset_, err := ch.Value().(*table.Reader).OffsetOf(key)
return uint64(offset_), err
}
// Creates an iterator from the given table.
func (t *tOps) newIterator(f *tFile, slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
c, err := t.open(f)
ch, err := t.open(f)
if err != nil {
return iterator.NewEmptyIterator(err)
}
iter := c.Value().(*table.Reader).NewIterator(slice, ro)
iter.SetReleaser(c)
iter := ch.Value().(*table.Reader).NewIterator(slice, ro)
iter.SetReleaser(ch)
return iter
}
@@ -392,14 +382,16 @@ func (t *tOps) newIterator(f *tFile, slice *util.Range, ro *opt.ReadOptions) ite
// no one use the the table.
func (t *tOps) remove(f *tFile) {
num := f.file.Num()
t.cacheNS.Delete(num, func(exist bool) {
if err := f.file.Remove(); err != nil {
t.s.logf("table@remove removing @%d %q", num, err)
} else {
t.s.logf("table@remove removed @%d", num)
}
if bc := t.s.o.GetBlockCache(); bc != nil {
bc.GetNamespace(num).Zap(false)
t.cacheNS.Delete(num, func(exist, pending bool) {
if !pending {
if err := f.file.Remove(); err != nil {
t.s.logf("table@remove removing @%d %q", num, err)
} else {
t.s.logf("table@remove removed @%d", num)
}
if bc := t.s.o.GetBlockCache(); bc != nil {
bc.ZapNamespace(num)
}
}
})
}
@@ -407,7 +399,8 @@ func (t *tOps) remove(f *tFile) {
// Closes the table ops instance. It will close all tables,
// regadless still used or not.
func (t *tOps) close() {
t.cache.Zap(true)
t.cache.Zap()
t.bpool.Close()
}
// Creates new initialized table ops instance.

View File

@@ -40,7 +40,7 @@ var _ = testutil.Defer(func() {
data := bw.buf.Bytes()
restartsLen := int(binary.LittleEndian.Uint32(data[len(data)-4:]))
return &block{
cmp: comparer.DefaultComparer,
tr: &Reader{cmp: comparer.DefaultComparer},
data: data,
restartsLen: restartsLen,
restartsOffset: len(data) - (restartsLen+1)*4,
@@ -59,7 +59,7 @@ var _ = testutil.Defer(func() {
// Make block.
br := Build(kv, restartInterval)
// Do testing.
testutil.KeyValueTesting(nil, br, kv.Clone())
testutil.KeyValueTesting(nil, kv.Clone(), br, nil, nil)
}
Describe(Text(), Test)

View File

@@ -37,7 +37,7 @@ func max(x, y int) int {
}
type block struct {
cmp comparer.BasicComparer
tr *Reader
data []byte
restartsLen int
restartsOffset int
@@ -46,31 +46,25 @@ type block struct {
}
func (b *block) seek(rstart, rlimit int, key []byte) (index, offset int, err error) {
n := b.restartsOffset
data := b.data
cmp := b.cmp
index = sort.Search(b.restartsLen-rstart-(b.restartsLen-rlimit), func(i int) bool {
offset := int(binary.LittleEndian.Uint32(data[n+4*(rstart+i):]))
offset += 1 // shared always zero, since this is a restart point
v1, n1 := binary.Uvarint(data[offset:]) // key length
_, n2 := binary.Uvarint(data[offset+n1:]) // value length
offset := int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*(rstart+i):]))
offset += 1 // shared always zero, since this is a restart point
v1, n1 := binary.Uvarint(b.data[offset:]) // key length
_, n2 := binary.Uvarint(b.data[offset+n1:]) // value length
m := offset + n1 + n2
return cmp.Compare(data[m:m+int(v1)], key) > 0
return b.tr.cmp.Compare(b.data[m:m+int(v1)], key) > 0
}) + rstart - 1
if index < rstart {
// The smallest key is greater-than key sought.
index = rstart
}
offset = int(binary.LittleEndian.Uint32(data[n+4*index:]))
offset = int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*index:]))
return
}
func (b *block) restartIndex(rstart, rlimit, offset int) int {
n := b.restartsOffset
data := b.data
return sort.Search(b.restartsLen-rstart-(b.restartsLen-rlimit), func(i int) bool {
return int(binary.LittleEndian.Uint32(data[n+4*(rstart+i):])) > offset
return int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*(rstart+i):])) > offset
}) + rstart - 1
}
@@ -139,6 +133,14 @@ func (b *block) newIterator(slice *util.Range, inclLimit bool, cache util.Releas
return bi
}
func (b *block) Release() {
if b.tr.bpool != nil {
b.tr.bpool.Put(b.data)
}
b.tr = nil
b.data = nil
}
type dir int
const (
@@ -261,7 +263,7 @@ func (i *blockIter) Seek(key []byte) bool {
i.dir = dirForward
}
for i.Next() {
if i.block.cmp.Compare(i.key, key) >= 0 {
if i.block.tr.cmp.Compare(i.key, key) >= 0 {
return true
}
}
@@ -438,6 +440,7 @@ func (i *blockIter) Value() []byte {
func (i *blockIter) Release() {
if i.dir > dirReleased {
i.block = nil
i.prevNode = nil
i.prevKeys = nil
i.key = nil
@@ -469,7 +472,7 @@ func (i *blockIter) Error() error {
}
type filterBlock struct {
filter filter.Filter
tr *Reader
data []byte
oOffset int
baseLg uint
@@ -483,7 +486,7 @@ func (b *filterBlock) contains(offset uint64, key []byte) bool {
n := int(binary.LittleEndian.Uint32(o))
m := int(binary.LittleEndian.Uint32(o[4:]))
if n < m && m <= b.oOffset {
return b.filter.Contains(b.data[n:m], key)
return b.tr.filter.Contains(b.data[n:m], key)
} else if n == m {
return false
}
@@ -491,10 +494,17 @@ func (b *filterBlock) contains(offset uint64, key []byte) bool {
return true
}
func (b *filterBlock) Release() {
if b.tr.bpool != nil {
b.tr.bpool.Put(b.data)
}
b.tr = nil
b.data = nil
}
type indexIter struct {
blockIter
tableReader *Reader
slice *util.Range
*blockIter
slice *util.Range
// Options
checksum bool
fillCache bool
@@ -513,7 +523,7 @@ func (i *indexIter) Get() iterator.Iterator {
if i.slice != nil && (i.blockIter.isFirst() || i.blockIter.isLast()) {
slice = i.slice
}
return i.tableReader.getDataIter(dataBH, slice, i.checksum, i.fillCache)
return i.blockIter.block.tr.getDataIter(dataBH, slice, i.checksum, i.fillCache)
}
// Reader is a table reader.
@@ -528,9 +538,8 @@ type Reader struct {
checksum bool
strictIter bool
dataEnd int64
indexBlock *block
filterBlock *filterBlock
dataEnd int64
indexBH, filterBH blockHandle
}
func verifyChecksum(data []byte) bool {
@@ -547,6 +556,7 @@ func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
}
if checksum || r.checksum {
if !verifyChecksum(data) {
r.bpool.Put(data)
return nil, errors.New("leveldb/table: Reader: invalid block (checksum mismatch)")
}
}
@@ -565,6 +575,7 @@ func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
return nil, err
}
default:
r.bpool.Put(data)
return nil, fmt.Errorf("leveldb/table: Reader: unknown block compression type: %d", data[bh.length])
}
return data, nil
@@ -577,7 +588,7 @@ func (r *Reader) readBlock(bh blockHandle, checksum bool) (*block, error) {
}
restartsLen := int(binary.LittleEndian.Uint32(data[len(data)-4:]))
b := &block{
cmp: r.cmp,
tr: r,
data: data,
restartsLen: restartsLen,
restartsOffset: len(data) - (restartsLen+1)*4,
@@ -586,7 +597,44 @@ func (r *Reader) readBlock(bh blockHandle, checksum bool) (*block, error) {
return b, nil
}
func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterBlock, error) {
func (r *Reader) readBlockCached(bh blockHandle, checksum, fillCache bool) (*block, util.Releaser, error) {
if r.cache != nil {
var err error
ch := r.cache.Get(bh.offset, func() (charge int, value interface{}) {
if !fillCache {
return 0, nil
}
var b *block
b, err = r.readBlock(bh, checksum)
if err != nil {
return 0, nil
}
return cap(b.data), b
})
if ch != nil {
b, ok := ch.Value().(*block)
if !ok {
ch.Release()
return nil, nil, errors.New("leveldb/table: Reader: inconsistent block type")
}
if !b.checksum && (r.checksum || checksum) {
if !verifyChecksum(b.data) {
ch.Release()
return nil, nil, errors.New("leveldb/table: Reader: invalid block (checksum mismatch)")
}
b.checksum = true
}
return b, ch, err
} else if err != nil {
return nil, nil, err
}
}
b, err := r.readBlock(bh, checksum)
return b, b, err
}
func (r *Reader) readFilterBlock(bh blockHandle) (*filterBlock, error) {
data, err := r.readRawBlock(bh, true)
if err != nil {
return nil, err
@@ -601,7 +649,7 @@ func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterB
return nil, errors.New("leveldb/table: Reader: invalid filter block (invalid offset)")
}
b := &filterBlock{
filter: filter,
tr: r,
data: data,
oOffset: oOffset,
baseLg: uint(data[n-1]),
@@ -610,60 +658,42 @@ 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) readFilterBlockCached(bh blockHandle, fillCache bool) (*filterBlock, util.Releaser, error) {
if r.cache != nil {
var err error
ch := r.cache.Get(bh.offset, func() (charge int, value interface{}) {
if !fillCache {
return 0, nil
}
var b *filterBlock
b, err = r.readFilterBlock(bh)
if err != nil {
return 0, nil
}
return cap(b.data), b
})
if ch != nil {
b, ok := ch.Value().(*filterBlock)
if !ok {
ch.Release()
return nil, nil, errors.New("leveldb/table: Reader: inconsistent block type")
}
return b, ch, err
} else if err != nil {
return nil, nil, err
}
}
b, err := r.readFilterBlock(bh)
return b, b, err
}
func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fillCache bool) iterator.Iterator {
if r.cache != nil {
// Get/set block cache.
var err error
cache, ok := r.cache.Get(dataBH.offset, func() (ok bool, value interface{}, charge int, fin cache.SetFin) {
if !fillCache {
return
}
var dataBlock *block
dataBlock, err = r.readBlock(dataBH, checksum)
if err == nil {
ok = true
value = dataBlock
charge = int(dataBH.length)
fin = func() {
r.bpool.Put(dataBlock.data)
dataBlock.data = nil
}
}
return
})
if err != nil {
return iterator.NewEmptyIterator(err)
}
if ok {
dataBlock := cache.Value().(*block)
if !dataBlock.checksum && (r.checksum || checksum) {
if !verifyChecksum(dataBlock.data) {
return iterator.NewEmptyIterator(errors.New("leveldb/table: Reader: invalid block (checksum mismatch)"))
}
dataBlock.checksum = true
}
iter := dataBlock.newIterator(slice, false, cache)
return iter
}
}
dataBlock, err := r.readBlock(dataBH, checksum)
b, rel, err := r.readBlockCached(dataBH, checksum, fillCache)
if err != nil {
return iterator.NewEmptyIterator(err)
}
iter := dataBlock.newIterator(slice, false, releaseBlock{r, dataBlock})
return iter
return b.newIterator(slice, false, rel)
}
// NewIterator creates an iterator from the table.
@@ -677,18 +707,21 @@ func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fi
// when not used.
//
// Also read Iterator documentation of the leveldb/iterator package.
func (r *Reader) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
if r.err != nil {
return iterator.NewEmptyIterator(r.err)
}
fillCache := !ro.GetDontFillCache()
b, rel, err := r.readBlockCached(r.indexBH, true, fillCache)
if err != nil {
return iterator.NewEmptyIterator(err)
}
index := &indexIter{
blockIter: *r.indexBlock.newIterator(slice, true, nil),
tableReader: r,
slice: slice,
checksum: ro.GetStrict(opt.StrictBlockChecksum),
fillCache: !ro.GetDontFillCache(),
blockIter: b.newIterator(slice, true, rel),
slice: slice,
checksum: ro.GetStrict(opt.StrictBlockChecksum),
fillCache: !ro.GetDontFillCache(),
}
return iterator.NewIndexedIterator(index, r.strictIter || ro.GetStrict(opt.StrictIterator), false)
}
@@ -705,7 +738,13 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
return
}
index := r.indexBlock.newIterator(nil, true, nil)
indexBlock, rel, err := r.readBlockCached(r.indexBH, true, true)
if err != nil {
return
}
defer rel.Release()
index := indexBlock.newIterator(nil, true, nil)
defer index.Release()
if !index.Seek(key) {
err = index.Error()
@@ -719,9 +758,15 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
err = errors.New("leveldb/table: Reader: invalid table (bad data block handle)")
return
}
if r.filterBlock != nil && !r.filterBlock.contains(dataBH.offset, key) {
err = ErrNotFound
return
if r.filter != nil {
filterBlock, rel, ferr := r.readFilterBlockCached(r.filterBH, true)
if ferr == nil {
if !filterBlock.contains(dataBH.offset, key) {
rel.Release()
return nil, nil, ErrNotFound
}
rel.Release()
}
}
data := r.getDataIter(dataBH, nil, ro.GetStrict(opt.StrictBlockChecksum), !ro.GetDontFillCache())
defer data.Release()
@@ -768,7 +813,13 @@ func (r *Reader) OffsetOf(key []byte) (offset int64, err error) {
return
}
index := r.indexBlock.newIterator(nil, true, nil)
indexBlock, rel, err := r.readBlockCached(r.indexBH, true, true)
if err != nil {
return
}
defer rel.Release()
index := indexBlock.newIterator(nil, true, nil)
defer index.Release()
if index.Seek(key) {
dataBH, n := decodeBlockHandle(index.Value())
@@ -786,6 +837,17 @@ func (r *Reader) OffsetOf(key []byte) (offset int64, err error) {
return
}
// Release implements util.Releaser.
// It also close the file if it is an io.Closer.
func (r *Reader) Release() {
if closer, ok := r.reader.(io.Closer); ok {
closer.Close()
}
r.reader = nil
r.cache = nil
r.bpool = nil
}
// NewReader creates a new initialized table reader for the file.
// The cache and bpool is optional and can be nil.
//
@@ -825,16 +887,11 @@ func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, bpool *util.Buf
return r
}
// Decode the index block handle.
indexBH, n := decodeBlockHandle(footer[n:])
r.indexBH, n = decodeBlockHandle(footer[n:])
if n == 0 {
r.err = errors.New("leveldb/table: Reader: invalid table (bad index block handle)")
return r
}
// Read index block.
r.indexBlock, r.err = r.readBlock(indexBH, true)
if r.err != nil {
return r
}
// Read metaindex block.
metaBlock, err := r.readBlock(metaBH, true)
if err != nil {
@@ -850,32 +907,28 @@ func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, bpool *util.Buf
continue
}
fn := key[7:]
var filter filter.Filter
if f0 := o.GetFilter(); f0 != nil && f0.Name() == fn {
filter = f0
r.filter = f0
} else {
for _, f0 := range o.GetAltFilters() {
if f0.Name() == fn {
filter = f0
r.filter = f0
break
}
}
}
if filter != nil {
if r.filter != nil {
filterBH, n := decodeBlockHandle(metaIter.Value())
if n == 0 {
continue
}
r.filterBH = filterBH
// Update data end.
r.dataEnd = int64(filterBH.offset)
filterBlock, err := r.readFilterBlock(filterBH, filter)
if err != nil {
continue
}
r.filterBlock = filterBlock
break
}
}
metaIter.Release()
metaBlock.Release()
return r
}

View File

@@ -104,14 +104,16 @@ var _ = testutil.Defer(func() {
if body != nil {
body(db.(tableWrapper).Reader)
}
testutil.KeyValueTesting(nil, db, *kv)
testutil.KeyValueTesting(nil, *kv, db, nil, nil)
}
}
testutil.AllKeyValueTesting(nil, Build)
testutil.AllKeyValueTesting(nil, Build, nil, nil)
Describe("with one key per block", Test(testutil.KeyValue_Generate(nil, 9, 1, 10, 512, 512), func(r *Reader) {
It("should have correct blocks number", func() {
Expect(r.indexBlock.restartsLen).Should(Equal(9))
indexBlock, err := r.readBlock(r.indexBH, true)
Expect(err).To(BeNil())
Expect(indexBlock.restartsLen).Should(Equal(9))
})
}))
})

View File

@@ -16,13 +16,22 @@ import (
"github.com/syndtr/goleveldb/leveldb/util"
)
func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
func KeyValueTesting(rnd *rand.Rand, kv KeyValue, p DB, setup func(KeyValue) DB, teardown func(DB)) {
if rnd == nil {
rnd = NewRand()
}
if db, ok := p.(Find); ok {
It("Should find all keys with Find", func() {
if p == nil {
BeforeEach(func() {
p = setup(kv)
})
AfterEach(func() {
teardown(p)
})
}
It("Should find all keys with Find", func() {
if db, ok := p.(Find); ok {
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
key_, key, value := kv.IndexInexact(i)
@@ -38,9 +47,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
Expect(rkey).Should(Equal(key))
Expect(rvalue).Should(Equal(value), "Value for key %q (%q)", key_, key)
})
})
}
})
It("Should return error if the key is not present", func() {
It("Should return error if the key is not present", func() {
if db, ok := p.(Find); ok {
var key []byte
if kv.Len() > 0 {
key_, _ := kv.Index(kv.Len() - 1)
@@ -49,11 +60,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
rkey, _, err := db.TestFind(key)
Expect(err).Should(HaveOccurred(), "Find for key %q yield key %q", key, rkey)
Expect(err).Should(Equal(util.ErrNotFound))
})
}
}
})
if db, ok := p.(Get); ok {
It("Should only find exact key with Get", func() {
It("Should only find exact key with Get", func() {
if db, ok := p.(Get); ok {
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
key_, key, value := kv.IndexInexact(i)
@@ -69,11 +80,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
Expect(err).Should(Equal(util.ErrNotFound))
}
})
})
}
}
})
if db, ok := p.(NewIterator); ok {
TestIter := func(r *util.Range, _kv KeyValue) {
TestIter := func(r *util.Range, _kv KeyValue) {
if db, ok := p.(NewIterator); ok {
iter := db.TestNewIterator(r)
Expect(iter.Error()).ShouldNot(HaveOccurred())
@@ -84,45 +95,48 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
DoIteratorTesting(&t)
}
}
It("Should iterates and seeks correctly", func(done Done) {
TestIter(nil, kv.Clone())
done <- true
}, 3.0)
It("Should iterates and seeks correctly", func(done Done) {
TestIter(nil, kv.Clone())
done <- true
}, 3.0)
RandomIndex(rnd, kv.Len(), kv.Len(), func(i int) {
type slice struct {
r *util.Range
start, limit int
}
RandomIndex(rnd, kv.Len(), kv.Len(), func(i int) {
type slice struct {
r *util.Range
start, limit int
}
key_, _, _ := kv.IndexInexact(i)
for _, x := range []slice{
{&util.Range{Start: key_, Limit: nil}, i, kv.Len()},
{&util.Range{Start: nil, Limit: key_}, 0, i},
} {
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", x.start, x.limit), func(done Done) {
TestIter(x.r, kv.Slice(x.start, x.limit))
done <- true
}, 3.0)
}
})
RandomRange(rnd, kv.Len(), kv.Len(), func(start, limit int) {
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", start, limit), func(done Done) {
r := kv.Range(start, limit)
TestIter(&r, kv.Slice(start, limit))
key_, _, _ := kv.IndexInexact(i)
for _, x := range []slice{
{&util.Range{Start: key_, Limit: nil}, i, kv.Len()},
{&util.Range{Start: nil, Limit: key_}, 0, i},
} {
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", x.start, x.limit), func(done Done) {
TestIter(x.r, kv.Slice(x.start, x.limit))
done <- true
}, 3.0)
})
}
}
})
RandomRange(rnd, kv.Len(), kv.Len(), func(start, limit int) {
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", start, limit), func(done Done) {
r := kv.Range(start, limit)
TestIter(&r, kv.Slice(start, limit))
done <- true
}, 3.0)
})
}
func AllKeyValueTesting(rnd *rand.Rand, body func(kv KeyValue) DB) {
func AllKeyValueTesting(rnd *rand.Rand, body, setup func(KeyValue) DB, teardown func(DB)) {
Test := func(kv *KeyValue) func() {
return func() {
db := body(*kv)
KeyValueTesting(rnd, db, *kv)
var p DB
if body != nil {
p = body(*kv)
}
KeyValueTesting(rnd, *kv, p, setup, teardown)
}
}

View File

@@ -19,13 +19,21 @@ type buffer struct {
// BufferPool is a 'buffer pool'.
type BufferPool struct {
pool [4]chan []byte
size [3]uint32
sizeMiss [3]uint32
baseline0 int
baseline1 int
baseline2 int
pool [6]chan []byte
size [5]uint32
sizeMiss [5]uint32
sizeHalf [5]uint32
baseline [4]int
baselinex0 int
baselinex1 int
baseline0 int
baseline1 int
baseline2 int
close chan struct{}
get uint32
put uint32
half uint32
less uint32
equal uint32
greater uint32
@@ -33,20 +41,21 @@ type BufferPool struct {
}
func (p *BufferPool) poolNum(n int) int {
switch {
case n <= p.baseline0:
if n <= p.baseline0 && n > p.baseline0/2 {
return 0
case n <= p.baseline1:
return 1
case n <= p.baseline2:
return 2
default:
return 3
}
for i, x := range p.baseline {
if n <= x {
return i + 1
}
}
return len(p.baseline) + 1
}
// Get returns buffer with length of n.
func (p *BufferPool) Get(n int) []byte {
atomic.AddUint32(&p.get, 1)
poolNum := p.poolNum(n)
pool := p.pool[poolNum]
if poolNum == 0 {
@@ -55,13 +64,22 @@ func (p *BufferPool) Get(n int) []byte {
case b := <-pool:
switch {
case cap(b) > n:
atomic.AddUint32(&p.less, 1)
return b[:n]
if cap(b)-n >= n {
atomic.AddUint32(&p.half, 1)
select {
case pool <- b:
default:
}
return make([]byte, n)
} else {
atomic.AddUint32(&p.less, 1)
return b[:n]
}
case cap(b) == n:
atomic.AddUint32(&p.equal, 1)
return b[:n]
default:
panic("not reached")
atomic.AddUint32(&p.greater, 1)
}
default:
atomic.AddUint32(&p.miss, 1)
@@ -75,8 +93,23 @@ func (p *BufferPool) Get(n int) []byte {
case b := <-pool:
switch {
case cap(b) > n:
atomic.AddUint32(&p.less, 1)
return b[:n]
if cap(b)-n >= n {
atomic.AddUint32(&p.half, 1)
sizeHalfPtr := &p.sizeHalf[poolNum-1]
if atomic.AddUint32(sizeHalfPtr, 1) == 20 {
atomic.StoreUint32(sizePtr, uint32(cap(b)/2))
atomic.StoreUint32(sizeHalfPtr, 0)
} else {
select {
case pool <- b:
default:
}
}
return make([]byte, n)
} else {
atomic.AddUint32(&p.less, 1)
return b[:n]
}
case cap(b) == n:
atomic.AddUint32(&p.equal, 1)
return b[:n]
@@ -112,6 +145,8 @@ func (p *BufferPool) Get(n int) []byte {
// Put adds given buffer to the pool.
func (p *BufferPool) Put(b []byte) {
atomic.AddUint32(&p.put, 1)
pool := p.pool[p.poolNum(cap(b))]
select {
case pool <- b:
@@ -120,20 +155,34 @@ func (p *BufferPool) Put(b []byte) {
}
func (p *BufferPool) Close() {
select {
case p.close <- struct{}{}:
default:
}
}
func (p *BufferPool) String() string {
return fmt.Sprintf("BufferPool{B·%d Z·%v Zm·%v L·%d E·%d G·%d M·%d}",
p.baseline0, p.size, p.sizeMiss, p.less, p.equal, p.greater, p.miss)
return fmt.Sprintf("BufferPool{B·%d Z·%v Zm·%v Zh·%v G·%d P·%d H·%d <·%d =·%d >·%d M·%d}",
p.baseline0, p.size, p.sizeMiss, p.sizeHalf, p.get, p.put, p.half, p.less, p.equal, p.greater, p.miss)
}
func (p *BufferPool) drain() {
ticker := time.NewTicker(2 * time.Second)
for {
time.Sleep(1 * time.Second)
select {
case <-p.pool[0]:
case <-p.pool[1]:
case <-p.pool[2]:
case <-p.pool[3]:
default:
case <-ticker.C:
for _, ch := range p.pool {
select {
case <-ch:
default:
}
}
case <-p.close:
for _, ch := range p.pool {
close(ch)
}
return
}
}
}
@@ -145,10 +194,10 @@ func NewBufferPool(baseline int) *BufferPool {
}
p := &BufferPool{
baseline0: baseline,
baseline1: baseline * 2,
baseline2: baseline * 4,
baseline: [...]int{baseline / 4, baseline / 2, baseline * 2, baseline * 4},
close: make(chan struct{}, 1),
}
for i, cap := range []int{6, 6, 3, 1} {
for i, cap := range []int{2, 2, 4, 4, 2, 1} {
p.pool[i] = make(chan []byte, cap)
}
go p.drain()

View File

@@ -14,3 +14,19 @@ type Range struct {
// Limit of the key range, not include in the range.
Limit []byte
}
// BytesPrefix returns key range that satisfy the given prefix.
// This only applicable for the standard 'bytes comparer'.
func BytesPrefix(prefix []byte) *Range {
var limit []byte
for i := len(prefix) - 1; i >= 0; i-- {
c := prefix[i]
if c < 0xff {
limit = make([]byte, i+1)
copy(limit, prefix)
limit[i] = c + 1
break
}
}
return &Range{prefix, limit}
}

View File

@@ -1,2 +0,0 @@
// Package auto contains auto generated files for web assets.
package auto

View File

File diff suppressed because one or more lines are too long

151
build.go
View File

@@ -55,77 +55,74 @@ func main() {
flag.BoolVar(&noupgrade, "no-upgrade", false, "Disable upgrade functionality")
flag.Parse()
checkRequiredGoVersion()
if check() != nil {
setup()
switch goarch {
case "386", "amd64", "armv5", "armv6", "armv7":
break
case "arm":
log.Println("Invalid goarch \"arm\". Use one of \"armv5\", \"armv6\", \"armv7\".")
log.Fatalln("Note that producing a correct \"armv5\" binary requires a rebuilt stdlib.")
default:
log.Printf("Unknown goarch %q; proceed with caution!", goarch)
}
checkRequiredGoVersion()
if flag.NArg() == 0 {
install("./cmd/...")
return
}
switch flag.Arg(0) {
case "install":
pkg := "./cmd/..."
if flag.NArg() > 2 {
pkg = flag.Arg(1)
for _, cmd := range flag.Args() {
switch cmd {
case "setup":
setup()
case "install":
pkg := "./cmd/..."
install(pkg)
case "build":
pkg := "./cmd/syncthing"
var tags []string
if noupgrade {
tags = []string{"noupgrade"}
}
build(pkg, tags)
case "test":
pkg := "./..."
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", cmd)
}
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+)`)
@@ -153,24 +150,25 @@ func setup() {
}
func test(pkg string) {
runPrint("godep", "go", "test", pkg)
setBuildEnv()
runPrint("go", "test", "-short", "-timeout", "10s", pkg)
}
func install(pkg string) {
os.Setenv("GOBIN", "./bin")
setBuildEnv()
runPrint("godep", "go", "install", "-ldflags", ldflags(), pkg)
runPrint("go", "install", "-ldflags", ldflags(), pkg)
}
func build(pkg string, tags []string) {
rmr("syncthing", "syncthing.exe")
args := []string{"go", "build", "-ldflags", ldflags()}
args := []string{"build", "-ldflags", ldflags()}
if len(tags) > 0 {
args = append(args, "-tags", strings.Join(tags, ","))
}
args = append(args, pkg)
setBuildEnv()
runPrint("godep", args...)
runPrint("go", args...)
}
func buildTar() {
@@ -220,33 +218,41 @@ func setBuildEnv() {
if goarch == "386" {
os.Setenv("GO386", "387")
}
wd, err := os.Getwd()
if err != nil {
log.Println("Warning: can't determine current dir:", err)
log.Println("Build might not work as expected")
}
os.Setenv("GOPATH", fmt.Sprintf("%s%c%s", filepath.Join(wd, "Godeps", "_workspace"), os.PathListSeparator, os.Getenv("GOPATH")))
log.Println("GOPATH=" + os.Getenv("GOPATH"))
}
func assets() {
runPipe("auto/gui.files.go", "godep", "go", "run", "cmd/genassets/main.go", "gui")
setBuildEnv()
runPipe("internal/auto/gui.files.go", "go", "run", "cmd/genassets/main.go", "gui")
}
func xdr() {
for _, f := range []string{"discover/packets", "files/leveldb", "protocol/message"} {
for _, f := range []string{"internal/discover/packets", "internal/files/leveldb", "internal/protocol/message"} {
runPipe(f+"_xdr.go", "go", "run", "./Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go", "--", f+".go")
}
}
func translate() {
os.Chdir("gui")
runPipe("lang-en-new.json", "go", "run", "../cmd/translate/main.go", "lang-en.json", "index.html")
os.Chdir("gui/lang")
runPipe("lang-en-new.json", "go", "run", "../../cmd/translate/main.go", "lang-en.json", "../index.html")
os.Remove("lang-en.json")
err := os.Rename("lang-en-new.json", "lang-en.json")
if err != nil {
log.Fatal(err)
}
os.Chdir("..")
os.Chdir("../..")
}
func transifex() {
os.Chdir("gui")
runPrint("go", "run", "../cmd/transifexdl/main.go")
os.Chdir("..")
os.Chdir("gui/lang")
runPrint("go", "run", "../../cmd/transifexdl/main.go")
os.Chdir("../..")
assets()
}
@@ -366,6 +372,7 @@ func runPipe(file, cmd string, args ...string) {
if err != nil {
log.Fatal(err)
}
fd.Close()
}
type archiveFile struct {

View File

@@ -12,6 +12,10 @@ case "${1:-default}" in
;;
test)
ulimit -t 60 &>/dev/null || true
ulimit -d 512000 &>/dev/null || true
ulimit -m 512000 &>/dev/null || true
go run build.go "$1"
;;
@@ -44,8 +48,6 @@ case "${1:-default}" in
;;
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
@@ -66,6 +68,10 @@ case "${1:-default}" in
;;
test-cov)
ulimit -t 60 &>/dev/null || true
ulimit -d 512000 &>/dev/null || true
ulimit -m 512000 &>/dev/null || true
go get github.com/axw/gocov/gocov
go get github.com/AlekSi/gocov-xml

29
check-contrib.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
missing-contribs() {
for email in $(git log --format=%ae master | grep -v jakob@nym.se | sort | uniq) ; do
grep -q "$email" CONTRIBUTORS || echo $email
done
}
no-docs-typos() {
# Commits that are known to not change code
grep -v f2459ef3319b2f060dbcdacd0c35a1788a94b8bd |\
grep -v b61f418bf2d1f7d5a9d7088a20a2a448e5e66801 |\
grep -v f0621207e3953711f9ab86d99724f1d0faac45b1 |\
grep -v f1120d7aa936c0658429edef0037792520b46334
}
print-missing-contribs() {
for email in $(missing-contribs) ; do
git log --author="$email" --format="%H %ae %s" | no-docs-typos
done
}
print-missing-copyright() {
find . -name \*.go | xargs grep -L 'Copyright (C)' | grep -v Godeps
}
print-missing-contribs
print-missing-copyright

View File

@@ -9,8 +9,8 @@ package main
import (
"bytes"
"compress/gzip"
"encoding/base64"
"flag"
"fmt"
"go/format"
"io"
"os"
@@ -23,27 +23,27 @@ var tpl = template.Must(template.New("assets").Parse(`package auto
import (
"bytes"
"compress/gzip"
"encoding/hex"
"encoding/base64"
"io/ioutil"
)
var Assets = make(map[string][]byte)
func init() {
func Assets() map[string][]byte {
var assets = make(map[string][]byte, {{.assets | len}})
var bs []byte
var gr *gzip.Reader
{{range $asset := .assets}}
bs, _ = hex.DecodeString("{{$asset.HexData}}")
bs, _ = base64.StdEncoding.DecodeString("{{$asset.Data}}")
gr, _ = gzip.NewReader(bytes.NewBuffer(bs))
bs, _ = ioutil.ReadAll(gr)
Assets["{{$asset.Name}}"] = bs
assets["{{$asset.Name}}"] = bs
{{end}}
return assets
}
`))
type asset struct {
Name string
HexData string
Name string
Data string
}
var assets []asset
@@ -69,8 +69,8 @@ func walkerFor(basePath string) filepath.WalkFunc {
name, _ = filepath.Rel(basePath, name)
assets = append(assets, asset{
Name: filepath.ToSlash(name),
HexData: fmt.Sprintf("%x", buf.Bytes()),
Name: filepath.ToSlash(name),
Data: base64.StdEncoding.EncodeToString(buf.Bytes()),
})
}

View File

@@ -10,8 +10,8 @@ import (
"log"
"os"
"github.com/syncthing/syncthing/files"
"github.com/syncthing/syncthing/protocol"
"github.com/syncthing/syncthing/internal/files"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syndtr/goleveldb/leveldb"
)

View File

@@ -5,34 +5,29 @@
package main
import (
"bytes"
"encoding/base64"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/rand"
"mime"
"net"
"net/http"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"time"
"crypto/tls"
"code.google.com/p/go.crypto/bcrypt"
"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/syncthing/syncthing/internal/auto"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/logger"
"github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/upgrade"
"github.com/vitrun/qart/qr"
)
@@ -45,16 +40,10 @@ var (
configInSync = true
guiErrors = []guiError{}
guiErrorsMut sync.Mutex
static func(http.ResponseWriter, *http.Request, *log.Logger)
apiKey string
modt = time.Now().UTC().Format(http.TimeFormat)
eventSub *events.BufferedSubscription
)
const (
unchangedPassword = "--password-unchanged--"
)
func init() {
l.AddHandler(logger.LevelWarn, showGuiError)
sub := events.Default.Subscribe(events.AllEvents)
@@ -62,39 +51,32 @@ func init() {
}
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
var listener net.Listener
var err error
if cfg.UseTLS {
cert, err := loadCert(confDir, "https-")
if err != nil {
l.Infoln("Loading HTTPS certificate:", err)
l.Infoln("Creating new HTTPS certificate")
newCertificate(confDir, "https-")
cert, err = loadCert(confDir, "https-")
}
if err != nil {
return err
}
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: "syncthing",
}
listener, err = tls.Listen("tcp", cfg.Address, tlsCfg)
if err != nil {
return err
}
} else {
listener, err = net.Listen("tcp", cfg.Address)
if err != nil {
return err
}
cert, err := loadCert(confDir, "https-")
if err != nil {
l.Infoln("Loading HTTPS certificate:", err)
l.Infoln("Creating new HTTPS certificate")
newCertificate(confDir, "https-")
cert, err = loadCert(confDir, "https-")
}
if err != nil {
return err
}
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: "syncthing",
}
apiKey = cfg.APIKey
loadCsrfTokens()
rawListener, err := net.Listen("tcp", cfg.Address)
if err != nil {
return err
}
listener := &DowngradingListener{rawListener, tlsCfg}
// The GET handlers
getRestMux := http.NewServeMux()
getRestMux.HandleFunc("/rest/ping", restPing)
getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion))
getRestMux.HandleFunc("/rest/config", restGetConfig)
getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync)
@@ -102,6 +84,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
getRestMux.HandleFunc("/rest/errors", restGetErrors)
getRestMux.HandleFunc("/rest/events", restGetEvents)
getRestMux.HandleFunc("/rest/ignores", withModel(m, restGetIgnores))
getRestMux.HandleFunc("/rest/lang", restGetLang)
getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion))
@@ -111,16 +94,19 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
getRestMux.HandleFunc("/rest/system", restGetSystem)
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
getRestMux.HandleFunc("/rest/version", restGetVersion)
getRestMux.HandleFunc("/rest/stats/node", withModel(m, restGetNodeStats))
// Debug endpoints, not for general use
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
// The POST handlers
postRestMux := http.NewServeMux()
postRestMux.HandleFunc("/rest/ping", restPing)
postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig))
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
postRestMux.HandleFunc("/rest/error", restPostError)
postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
postRestMux.HandleFunc("/rest/ignores", withModel(m, restPostIgnores))
postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride))
postRestMux.HandleFunc("/rest/reset", restPostReset)
postRestMux.HandleFunc("/rest/restart", restPostRestart)
@@ -142,14 +128,27 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
// Wrap everything in CSRF protection. The /rest prefix should be
// protected, other requests will grant cookies.
handler := csrfMiddleware("/rest", mux)
handler := csrfMiddleware("/rest", cfg.APIKey, mux)
// Add our version as a header to responses
handler = withVersionMiddleware(handler)
// Wrap everything in basic auth, if user/password is set.
if len(cfg.User) > 0 {
handler = basicAuthMiddleware(cfg.User, cfg.Password, handler)
if len(cfg.User) > 0 && len(cfg.Password) > 0 {
handler = basicAuthAndSessionMiddleware(cfg, handler)
}
go http.Serve(listener, handler)
// Redirect to HTTPS if we are supposed to
if cfg.UseTLS {
handler = redirectToHTTPSMiddleware(handler)
}
go func() {
err := http.Serve(listener, handler)
if err != nil {
panic(err)
}
}()
return nil
}
@@ -166,6 +165,23 @@ func getPostHandler(get, post http.Handler) http.Handler {
})
}
func redirectToHTTPSMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Add a generous access-control-allow-origin header since we may be
// redirecting REST requests over protocols
w.Header().Add("Access-Control-Allow-Origin", "*")
if r.TLS == nil {
// Redirect HTTP requests to HTTPS
r.URL.Host = r.Host
r.URL.Scheme = "https"
http.Redirect(w, r, r.URL.String(), http.StatusFound)
} else {
h.ServeHTTP(w, r)
}
})
}
func noCacheMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
@@ -173,14 +189,34 @@ func noCacheMiddleware(h http.Handler) http.Handler {
})
}
func withVersionMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Syncthing-Version", Version)
h.ServeHTTP(w, r)
})
}
func withModel(m *model.Model, h func(m *model.Model, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h(m, w, r)
}
}
func restPing(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]string{
"ping": "pong",
})
}
func restGetVersion(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(Version))
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(map[string]string{
"version": Version,
"longVersion": LongVersion,
"os": runtime.GOOS,
"arch": runtime.GOARCH,
})
}
func restGetCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
@@ -246,14 +282,14 @@ func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
func restPostOverride(m *model.Model, w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var repo = qs.Get("repo")
m.Override(repo)
go m.Override(repo)
}
func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var repo = qs.Get("repo")
files := m.NeedFilesRepo(repo)
files := m.NeedFilesRepoLimited(repo, 100, 2500) // max 100 files or 2500 blocks
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(files)
@@ -265,13 +301,15 @@ func restGetConnections(m *model.Model, w http.ResponseWriter, r *http.Request)
json.NewEncoder(w).Encode(res)
}
func restGetConfig(w http.ResponseWriter, r *http.Request) {
encCfg := cfg
if encCfg.GUI.Password != "" {
encCfg.GUI.Password = unchangedPassword
}
func restGetNodeStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
var res = m.NodeStatistics()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(encCfg)
json.NewEncoder(w).Encode(res)
}
func restGetConfig(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(cfg)
}
func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
@@ -282,49 +320,20 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), 500)
return
} else {
if newCfg.GUI.Password == "" {
// Leave it empty
} else if newCfg.GUI.Password == unchangedPassword {
newCfg.GUI.Password = cfg.GUI.Password
} else {
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
if err != nil {
l.Warnln("bcrypting password:", err)
http.Error(w, err.Error(), 500)
return
} else {
newCfg.GUI.Password = string(hash)
}
}
// Figure out if any changes require a restart
if len(cfg.Repositories) != len(newCfg.Repositories) {
configInSync = false
} else {
om := cfg.RepoMap()
nm := newCfg.RepoMap()
for id := range om {
if !reflect.DeepEqual(om[id], nm[id]) {
configInSync = false
break
if newCfg.GUI.Password != cfg.GUI.Password {
if newCfg.GUI.Password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
if err != nil {
l.Warnln("bcrypting password:", err)
http.Error(w, err.Error(), 500)
return
} else {
newCfg.GUI.Password = string(hash)
}
}
}
if len(cfg.Nodes) != len(newCfg.Nodes) {
configInSync = false
} else {
om := cfg.NodeMap()
nm := newCfg.NodeMap()
for k := range om {
if _, ok := nm[k]; !ok {
// A node was removed and another added
configInSync = false
break
}
}
}
// Start or stop usage reporting as appropriate
if newCfg.Options.URAccepted > cfg.Options.URAccepted {
// UR was enabled
@@ -340,14 +349,12 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
stopUsageReporting()
}
if !reflect.DeepEqual(cfg.Options, newCfg.Options) || !reflect.DeepEqual(cfg.GUI, newCfg.GUI) {
configInSync = false
}
// Activate and save
configInSync = !config.ChangeRequiresRestart(cfg, newCfg)
newCfg.Location = cfg.Location
newCfg.Save()
cfg = newCfg
saveConfig()
}
}
@@ -409,7 +416,7 @@ func restGetSystem(w http.ResponseWriter, r *http.Request) {
func restGetErrors(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
guiErrorsMut.Lock()
json.NewEncoder(w).Encode(guiErrors)
json.NewEncoder(w).Encode(map[string][]guiError{"errors": guiErrors})
guiErrorsMut.Unlock()
}
@@ -452,6 +459,41 @@ func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(reportData(m))
}
func restGetIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
ignores, err := m.GetIgnores(qs.Get("repo"))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(map[string][]string{
"ignore": ignores,
})
}
func restPostIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
var data map[string][]string
err := json.NewDecoder(r.Body).Decode(&data)
r.Body.Close()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = m.SetIgnores(qs.Get("repo"), data["ignore"])
if err != nil {
http.Error(w, err.Error(), 500)
return
}
restGetIgnores(m, w, r)
}
func restGetEvents(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
sinceStr := qs.Get("since")
@@ -510,7 +552,7 @@ func restGetLang(w http.ResponseWriter, r *http.Request) {
var langs []string
for _, l := range strings.Split(lang, ",") {
parts := strings.SplitN(l, ";", 2)
langs = append(langs, strings.TrimSpace(parts[0]))
langs = append(langs, strings.ToLower(strings.TrimSpace(parts[0])))
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(langs)
@@ -532,7 +574,9 @@ func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
return
}
restPostRestart(w, r)
flushResponse(`{"ok": "restarting"}`, w)
l.Infoln("Upgrading")
stop <- exitUpgrading
}
}
@@ -584,57 +628,9 @@ func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Reques
json.NewEncoder(w).Encode(comp)
}
func basicAuthMiddleware(username string, passhash string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if validAPIKey(r.Header.Get("X-API-Key")) {
next.ServeHTTP(w, r)
return
}
error := func() {
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
http.Error(w, "Not Authorized", http.StatusUnauthorized)
}
hdr := r.Header.Get("Authorization")
if !strings.HasPrefix(hdr, "Basic ") {
error()
return
}
hdr = hdr[6:]
bs, err := base64.StdEncoding.DecodeString(hdr)
if err != nil {
error()
return
}
fields := bytes.SplitN(bs, []byte(":"), 2)
if len(fields) != 2 {
error()
return
}
if string(fields[0]) != username {
error()
return
}
if err := bcrypt.CompareHashAndPassword([]byte(passhash), fields[1]); err != nil {
error()
return
}
next.ServeHTTP(w, r)
})
}
func validAPIKey(k string) bool {
return len(apiKey) > 0 && k == apiKey
}
func embeddedStatic(assetDir string) http.Handler {
assets := auto.Assets()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
file := r.URL.Path
@@ -655,13 +651,13 @@ func embeddedStatic(assetDir string) http.Handler {
}
}
bs, ok := auto.Assets[file]
bs, ok := assets[file]
if !ok {
http.NotFound(w, r)
return
}
mtype := mime.TypeByExtension(filepath.Ext(r.URL.Path))
mtype := mimeTypeForFile(file)
if len(mtype) != 0 {
w.Header().Set("Content-Type", mtype)
}
@@ -671,3 +667,28 @@ func embeddedStatic(assetDir string) http.Handler {
w.Write(bs)
})
}
func mimeTypeForFile(file string) string {
// We use a built in table of the common types since the system
// TypeByExtension might be unreliable. But if we don't know, we delegate
// to the system.
ext := filepath.Ext(file)
switch ext {
case ".htm", ".html":
return "text/html"
case ".css":
return "text/css"
case ".js":
return "application/javascript"
case ".json":
return "application/json"
case ".png":
return "image/png"
case ".ttf":
return "application/x-font-ttf"
case ".woff":
return "application/x-font-woff"
default:
return mime.TypeByExtension(ext)
}
}

90
cmd/syncthing/gui_auth.go Executable file
View File

@@ -0,0 +1,90 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"encoding/base64"
"math/rand"
"net/http"
"strings"
"sync"
"time"
"code.google.com/p/go.crypto/bcrypt"
"github.com/syncthing/syncthing/internal/config"
)
var (
sessions = make(map[string]bool)
sessionsMut sync.Mutex
)
func basicAuthAndSessionMiddleware(cfg config.GUIConfiguration, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if cfg.APIKey != "" && r.Header.Get("X-API-Key") == cfg.APIKey {
next.ServeHTTP(w, r)
return
}
cookie, err := r.Cookie("sessionid")
if err == nil && cookie != nil {
sessionsMut.Lock()
_, ok := sessions[cookie.Value]
sessionsMut.Unlock()
if ok {
next.ServeHTTP(w, r)
return
}
}
error := func() {
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
http.Error(w, "Not Authorized", http.StatusUnauthorized)
}
hdr := r.Header.Get("Authorization")
if !strings.HasPrefix(hdr, "Basic ") {
error()
return
}
hdr = hdr[6:]
bs, err := base64.StdEncoding.DecodeString(hdr)
if err != nil {
error()
return
}
fields := bytes.SplitN(bs, []byte(":"), 2)
if len(fields) != 2 {
error()
return
}
if string(fields[0]) != cfg.User {
error()
return
}
if err := bcrypt.CompareHashAndPassword([]byte(cfg.Password), fields[1]); err != nil {
error()
return
}
sessionid := randomString(32)
sessionsMut.Lock()
sessions[sessionid] = true
sessionsMut.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "sessionid",
Value: sessionid,
MaxAge: 0,
})
next.ServeHTTP(w, r)
})
}

View File

@@ -1,3 +1,7 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (
@@ -12,7 +16,7 @@ import (
"sync"
"time"
"github.com/syncthing/syncthing/osutil"
"github.com/syncthing/syncthing/internal/osutil"
)
var csrfTokens []string
@@ -21,10 +25,11 @@ var csrfMut sync.Mutex
// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
// the request with 403. For / and /index.html, set a new CSRF cookie if none
// is currently set.
func csrfMiddleware(prefix string, next http.Handler) http.Handler {
func csrfMiddleware(prefix, apiKey string, next http.Handler) http.Handler {
loadCsrfTokens()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow requests carrying a valid API key
if validAPIKey(r.Header.Get("X-API-Key")) {
if apiKey != "" && r.Header.Get("X-API-Key") == apiKey {
next.ServeHTTP(w, r)
return
}
@@ -72,13 +77,7 @@ func validCsrfToken(token string) bool {
}
func newCsrfToken() string {
bs := make([]byte, 30)
_, err := rand.Reader.Read(bs)
if err != nil {
l.Fatalln(err)
}
token := base64.StdEncoding.EncodeToString(bs)
token := randomString(30)
csrfMut.Lock()
csrfTokens = append(csrfTokens, token)
@@ -130,3 +129,13 @@ func loadCsrfTokens() {
csrfTokens = append(csrfTokens, s.Text())
}
}
func randomString(len int) string {
bs := make([]byte, len)
_, err := rand.Reader.Read(bs)
if err != nil {
l.Fatalln(err)
}
return base64.StdEncoding.EncodeToString(bs)
}

52
cmd/syncthing/gui_windows.go Executable file
View File

@@ -0,0 +1,52 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
//+build windows
package main
import (
"syscall"
"time"
)
func init() {
go trackCPUUsage()
}
func trackCPUUsage() {
handle, err := syscall.GetCurrentProcess()
if err != nil {
l.Warnln("Cannot track CPU usage:", err)
return
}
var ctime, etime, ktime, utime syscall.Filetime
err = syscall.GetProcessTimes(handle, &ctime, &etime, &ktime, &utime)
if err != nil {
l.Warnln("Cannot track CPU usage:", err)
return
}
prevTime := ctime.Nanoseconds()
prevUsage := ktime.Nanoseconds() + utime.Nanoseconds() // Always overflows
for _ = range time.NewTicker(time.Second).C {
err := syscall.GetProcessTimes(handle, &ctime, &etime, &ktime, &utime)
if err != nil {
continue
}
curTime := time.Now().UnixNano()
timeDiff := curTime - prevTime
curUsage := ktime.Nanoseconds() + utime.Nanoseconds()
usageDiff := curUsage - prevUsage
cpuUsageLock.Lock()
copy(cpuUsagePercent[1:], cpuUsagePercent[0:])
cpuUsagePercent[0] = 100 * float64(usageDiff) / float64(timeDiff)
cpuUsageLock.Unlock()
prevTime = curTime
prevUsage = curUsage
}
}

View File

@@ -14,7 +14,8 @@ import (
)
func init() {
if os.Getenv("STHEAPPROFILE") != "" {
if innerProcess && os.Getenv("STHEAPPROFILE") != "" {
l.Debugln("Starting heap profiling")
go saveHeapProfiles()
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (
"io"
"github.com/juju/ratelimit"
)
type limitedReader struct {
r io.Reader
bucket *ratelimit.Bucket
}
func (r *limitedReader) Read(buf []byte) (int, error) {
n, err := r.r.Read(buf)
if r.bucket != nil {
r.bucket.Wait(int64(n))
}
return n, err
}

View File

@@ -16,7 +16,6 @@ import (
"net/http"
_ "net/http/pprof"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
@@ -26,17 +25,19 @@ import (
"strings"
"time"
"code.google.com/p/go.crypto/bcrypt"
"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/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/discover"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/files"
"github.com/syncthing/syncthing/internal/logger"
"github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/upgrade"
"github.com/syncthing/syncthing/internal/upnp"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
)
var (
@@ -50,7 +51,16 @@ var (
GoArchExtra string // "", "v5", "v6", "v7"
)
const (
exitSuccess = 0
exitError = 1
exitNoUpgradeAvailable = 2
exitRestarting = 3
exitUpgrading = 4
)
var l = logger.DefaultLogger
var innerProcess = os.Getenv("STNORESTART") != ""
func init() {
if Version != "unknown-dev" {
@@ -73,17 +83,16 @@ 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
externalPort int
cert tls.Certificate
cfg config.Configuration
myID protocol.NodeID
confDir string
logFlags int = log.Ltime
writeRateLimit *ratelimit.Bucket
readRateLimit *ratelimit.Bucket
stop = make(chan int)
discoverer *discover.Discoverer
externalPort int
cert tls.Certificate
)
const (
@@ -102,6 +111,15 @@ show time only (2).
The following enviroment variables are interpreted by syncthing:
STGUIADDRESS Override GUI listen address set in config. Expects protocol type
followed by hostname or an IP address, followed by a port, such
as "https://127.0.0.1:8888".
STGUIAUTH Override GUI authentication credentials set in config. Expects
a colon separated username and password, such as "admin:secret".
STGUIAPIKEY Override GUI API key set in config.
STNORESTART Do not attempt to restart when requested to, instead just exit.
Set this variable when running under a service manager such as
runit, launchd, etc.
@@ -115,6 +133,7 @@ The following enviroment variables are interpreted by syncthing:
- "net" (the main package; connections & network messages)
- "model" (the model package)
- "scanner" (the scanner package)
- "stats" (the stats package)
- "upnp" (the upnp package)
- "xdr" (the xdr package)
- "all" (all of the above)
@@ -132,28 +151,39 @@ The following enviroment variables are interpreted by syncthing:
STPERFSTATS Write running performance statistics to perf-$pid.csv. Not
supported on Windows.
STDEADLOCKTIMEOUT Alter deadlock detection timeout (seconds; default 1200).`
GOMAXPROCS Set the maximum number of CPU cores to use. Defaults to all
available CPU cores.`
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// Command line options
var (
reset bool
showVersion bool
doUpgrade bool
doUpgradeCheck bool
noBrowser bool
generateDir string
guiAddress string
guiAuthentication string
guiAPIKey string
)
func main() {
var reset bool
var showVersion bool
var doUpgrade bool
var doUpgradeCheck bool
var generateDir string
var noBrowser bool
flag.StringVar(&confDir, "home", getDefaultConfDir(), "Set configuration directory")
flag.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.StringVar(&guiAddress, "gui-address", "", "Override GUI address")
flag.StringVar(&guiAuthentication, "gui-authentication", "", "Override GUI authentication. Expects 'username:password'")
flag.StringVar(&guiAPIKey, "gui-apikey", "", "Override GUI API key")
flag.IntVar(&logFlags, "logflags", logFlags, "Set log flags")
flag.Usage = usageFor(flag.CommandLine, usage, extraUsage)
flag.Parse()
@@ -168,7 +198,9 @@ func main() {
dir := expandTilde(generateDir)
info, err := os.Stat(dir)
l.FatalErr(err)
if err != nil {
l.Fatalln("generate:", err)
}
if !info.IsDir() {
l.Fatalln(dir, "is not a directory")
}
@@ -182,13 +214,24 @@ func main() {
newCertificate(dir, "")
cert, err = loadCert(dir, "")
l.FatalErr(err)
if err != nil {
l.Fatalln("load cert:", err)
}
if err == nil {
l.Infoln("Node ID:", protocol.NewNodeID(cert.Certificate[0]))
}
return
}
confDir = expandTilde(confDir)
if info, err := os.Stat(confDir); err == nil && !info.IsDir() {
l.Fatalln("Config directory", confDir, "is not a directory")
}
// Ensure that our home directory exists.
ensureDir(confDir, 0700)
if doUpgrade || doUpgradeCheck {
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
if err != nil {
@@ -197,12 +240,18 @@ func main() {
if upgrade.CompareVersions(rel.Tag, Version) <= 0 {
l.Infof("No upgrade available (current %q >= latest %q).", Version, rel.Tag)
os.Exit(2)
os.Exit(exitNoUpgradeAvailable)
}
l.Infof("Upgrade available (current %q < latest %q)", Version, rel.Tag)
if doUpgrade {
// Use leveldb database locks to protect against concurrent upgrades
_, err = leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{CachedOpenFiles: 100})
if err != nil {
l.Fatalln("Cannot upgrade, database seems to be locked. Is another copy of Syncthing already running?")
}
err = upgrade.UpgradeTo(rel, GoArchExtra)
if err != nil {
l.Fatalln("Upgrade:", err) // exits 1
@@ -214,12 +263,21 @@ func main() {
}
}
var err error
lockPort, err = getLockPort()
if err != nil {
l.Fatalln("Opening lock port:", err)
if reset {
resetRepositories()
return
}
if os.Getenv("STNORESTART") != "" {
syncthingMain()
} else {
monitorMain()
}
}
func syncthingMain() {
var err error
if len(os.Getenv("GOGC")) == 0 {
debug.SetGCPercent(25)
}
@@ -228,11 +286,9 @@ func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
}
confDir = expandTilde(confDir)
events.Default.Log(events.Starting, map[string]string{"home": confDir})
if _, err := os.Stat(confDir); err != nil && confDir == getDefaultConfDir() {
if _, err = os.Stat(confDir); err != nil && confDir == getDefaultConfDir() {
// We are supposed to use the default configuration directory. It
// doesn't exist. In the past our default has been ~/.syncthing, so if
// that directory exists we move it to the new default location and
@@ -253,14 +309,14 @@ func main() {
}
}
// Ensure that our home directory exists and that we have a certificate and key.
ensureDir(confDir, 0700)
// Ensure that that we have a certificate and key.
cert, err = loadCert(confDir, "")
if err != nil {
newCertificate(confDir, "")
cert, err = loadCert(confDir, "")
l.FatalErr(err)
if err != nil {
l.Fatalln("load cert:", err)
}
}
myID = protocol.NewNodeID(cert.Certificate[0])
@@ -272,21 +328,14 @@ func main() {
// Prepare to be able to save configuration
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.
cf, err := os.Open(cfgFile)
cfg, err = config.Load(cfgFile, myID)
if err == nil {
// Read config.xml
cfg, err = config.Load(cf, myID)
if err != nil {
l.Fatalln(err)
}
cf.Close()
myCfg := cfg.GetNodeConfiguration(myID)
if myCfg == nil || myCfg.Name == "" {
myName, _ = os.Hostname()
@@ -298,12 +347,13 @@ func main() {
myName, _ = os.Hostname()
defaultRepo := filepath.Join(getHomeDir(), "Sync")
cfg, err = config.Load(nil, myID)
cfg = config.New(cfgFile, myID)
cfg.Repositories = []config.RepositoryConfiguration{
{
ID: "default",
Directory: defaultRepo,
Nodes: []config.RepositoryNodeConfiguration{{NodeID: myID}},
ID: "default",
Directory: defaultRepo,
RescanIntervalS: 60,
Nodes: []config.RepositoryNodeConfiguration{{NodeID: myID}},
},
}
cfg.Nodes = []config.NodeConfiguration{
@@ -315,26 +365,21 @@ func main() {
}
port, err := getFreePort("127.0.0.1", 8080)
l.FatalErr(err)
if err != nil {
l.Fatalln("get free port (GUI):", err)
}
cfg.GUI.Address = fmt.Sprintf("127.0.0.1:%d", port)
port, err = getFreePort("0.0.0.0", 22000)
l.FatalErr(err)
if err != nil {
l.Fatalln("get free port (BEP):", err)
}
cfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)}
saveConfig()
cfg.Save()
l.Infof("Edit %s to taste or use the GUI\n", cfgFile)
}
if reset {
resetRepositories()
return
}
if len(os.Getenv("STRESTART")) > 0 {
waitForParentExit()
}
if profiler := os.Getenv("STPROFILER"); len(profiler) > 0 {
go func() {
l.Debugln("Starting profiler on", profiler)
@@ -359,20 +404,33 @@ func main() {
MinVersion: tls.VersionTLS12,
}
// If the write rate should be limited, set up a rate limiter for it.
// If the read or write rate should be limited, set up a rate limiter for it.
// This will be used on connections created in the connect and listen routines.
if cfg.Options.MaxSendKbps > 0 {
rateBucket = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps))
writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps))
}
if cfg.Options.MaxRecvKbps > 0 {
readRateLimit = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxRecvKbps), int64(5*1000*cfg.Options.MaxRecvKbps))
}
// If this is the first time the user runs v0.9, archive the old indexes and config.
archiveLegacyConfig()
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), nil)
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{CachedOpenFiles: 100})
if err != nil {
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
}
// Remove database entries for repos that no longer exist in the config
repoMap := cfg.RepoMap()
for _, repo := range files.ListRepos(db) {
if _, ok := repoMap[repo]; !ok {
l.Infof("Cleaning data for dropped repo %q", repo)
files.DropRepo(db, repo)
}
}
m := model.NewModel(confDir, &cfg, myName, "syncthing", Version, db)
nextRepo:
@@ -380,8 +438,8 @@ nextRepo:
if repo.Invalid != "" {
continue
}
repo.Directory = expandTilde(repo.Directory)
m.AddRepo(repo)
fi, err := os.Stat(repo.Directory)
if m.LocalVersion(repo.ID) > 0 {
@@ -390,6 +448,7 @@ nextRepo:
// that all files have been deleted which might not be the case,
// so mark it as invalid instead.
if err != nil || !fi.IsDir() {
l.Warnf("Stopping repository %q - directory missing, but has files in index", repo.ID)
cfg.Repositories[i].Invalid = "repo directory missing"
continue nextRepo
}
@@ -402,18 +461,20 @@ nextRepo:
if err != nil {
// If there was another error or we could not create the
// directory, the repository is invalid.
l.Warnf("Stopping repository %q - %v", err)
cfg.Repositories[i].Invalid = err.Error()
continue nextRepo
}
m.AddRepo(repo)
}
// GUI
if cfg.GUI.Enabled && cfg.GUI.Address != "" {
addr, err := net.ResolveTCPAddr("tcp", cfg.GUI.Address)
guiCfg := overrideGUIConfig(cfg.GUI, guiAddress, guiAuthentication, guiAPIKey)
if guiCfg.Enabled && guiCfg.Address != "" {
addr, err := net.ResolveTCPAddr("tcp", guiCfg.Address)
if err != nil {
l.Fatalf("Cannot start GUI on %q: %v", cfg.GUI.Address, err)
l.Fatalf("Cannot start GUI on %q: %v", guiCfg.Address, err)
} else {
var hostOpen, hostShow string
switch {
@@ -429,17 +490,19 @@ nextRepo:
}
var proto = "http"
if cfg.GUI.UseTLS {
if guiCfg.UseTLS {
proto = "https"
}
l.Infof("Starting web GUI on %s://%s:%d/", proto, hostShow, addr.Port)
err := startGUI(cfg.GUI, os.Getenv("STGUIASSETS"), m)
urlShow := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
l.Infoln("Starting web GUI on", urlShow)
err := startGUI(guiCfg, os.Getenv("STGUIASSETS"), m)
if err != nil {
l.Fatalln("Cannot start GUI:", err)
}
if !noBrowser && cfg.Options.StartBrowser && len(os.Getenv("STRESTART")) == 0 {
openURL(fmt.Sprintf("%s://%s:%d", proto, hostOpen, addr.Port))
urlOpen := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostOpen, strconv.Itoa(addr.Port)))
openURL(urlOpen)
}
}
}
@@ -448,7 +511,13 @@ nextRepo:
// 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 {
if repoCfg.Invalid != "" {
continue
}
for _, node := range repoCfg.NodeIDs() {
if node == myID {
continue
}
m.Index(node, repoCfg.ID, nil)
}
}
@@ -483,6 +552,14 @@ nextRepo:
}
}
// The default port we announce, possibly modified by setupUPnP next.
addr, err := net.ResolveTCPAddr("tcp", cfg.Options.ListenAddress[0])
if err != nil {
l.Fatalln("Bad listen address:", err)
}
externalPort = addr.Port
// UPnP
if cfg.Options.UPnPEnabled {
@@ -539,12 +616,21 @@ nextRepo:
}()
}
if cfg.Options.RestartOnWakeup {
go standbyMonitor()
}
if cfg.Options.AutoUpgradeIntervalH > 0 {
go autoUpgrade()
}
events.Default.Log(events.StartupComplete, nil)
go generateEvents()
<-stop
code := <-stop
l.Okln("Exiting")
os.Exit(code)
}
func generateEvents() {
@@ -554,25 +640,6 @@ func generateEvents() {
}
}
func waitForParentExit() {
l.Infoln("Waiting for parent to exit...")
lockPortStr := os.Getenv("STRESTART")
lockPort, err := strconv.Atoi(lockPortStr)
if err != nil {
l.Warnln("Invalid lock port %q: %v", lockPortStr, err)
}
// Wait for the listen address to become free, indicating that the parent has exited.
for {
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", lockPort))
if err == nil {
ln.Close()
break
}
time.Sleep(250 * time.Millisecond)
}
l.Infoln("Continuing")
}
func setupUPnP() {
if len(cfg.Options.ListenAddress) == 1 {
_, portStr, err := net.SplitHostPort(cfg.Options.ListenAddress[0])
@@ -628,13 +695,16 @@ func renewUPnP(port int) {
}
// 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
if externalPort != 0 {
err = igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", cfg.Options.UPnPLease*60)
if err == nil {
l.Infoln("Renewed UPnP port mapping - external port", externalPort)
continue
}
}
// Something strange has happened. Perhaps the gateway has changed?
// Something strange has happened. We didn't have an external port before?
// Or perhaps the gateway has changed?
// Retry the same port sequence from the beginning.
r := setupExternalPort(igd, port)
if r != 0 {
@@ -690,7 +760,7 @@ func archiveLegacyConfig() {
l.Warnf("Cannot archive config:", err)
return
}
defer src.Close()
defer dst.Close()
l.Infoln("Archiving config.xml")
io.Copy(dst, src)
@@ -699,74 +769,12 @@ func archiveLegacyConfig() {
func restart() {
l.Infoln("Restarting")
if os.Getenv("SMF_FMRI") != "" || os.Getenv("STNORESTART") != "" {
// Solaris SMF
l.Infoln("Service manager detected; exit instead of restart")
stop <- true
return
}
env := os.Environ()
newEnv := make([]string, 0, len(env))
for _, s := range env {
if !strings.HasPrefix(s, "STRESTART=") {
newEnv = append(newEnv, s)
}
}
newEnv = append(newEnv, fmt.Sprintf("STRESTART=%d", lockPort))
pgm, err := exec.LookPath(os.Args[0])
if err != nil {
l.Warnln("Cannot restart:", err)
return
}
proc, err := os.StartProcess(pgm, os.Args, &os.ProcAttr{
Env: newEnv,
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
})
if err != nil {
l.Fatalln(err)
}
proc.Release()
stop <- true
stop <- exitRestarting
}
func shutdown() {
stop <- true
}
var saveConfigCh = make(chan struct{})
func saveConfigLoop(cfgFile string) {
for _ = range saveConfigCh {
fd, err := os.Create(cfgFile + ".tmp")
if err != nil {
l.Warnln("Saving config:", err)
continue
}
err = config.Save(fd, cfg)
if err != nil {
l.Warnln("Saving config:", err)
fd.Close()
continue
}
err = fd.Close()
if err != nil {
l.Warnln("Saving config:", err)
continue
}
err = osutil.Rename(cfgFile+".tmp", cfgFile)
if err != nil {
l.Warnln("Saving config:", err)
}
}
}
func saveConfig() {
saveConfigCh <- struct{}{}
l.Infoln("Shutting down")
stop <- exitSuccess
}
func listenConnect(myID protocol.NodeID, m *model.Model, tlsCfg *tls.Config) {
@@ -822,15 +830,20 @@ next:
continue next
}
// If rate limiting is set, we wrap the write side of the
// connection in a limiter.
// If rate limiting is set, we wrap the connection in a
// limiter.
var wr io.Writer = conn
if rateBucket != nil {
wr = &limitedWriter{conn, rateBucket}
if writeRateLimit != nil {
wr = &limitedWriter{conn, writeRateLimit}
}
var rd io.Reader = conn
if readRateLimit != nil {
rd = &limitedReader{conn, readRateLimit}
}
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
protoConn := protocol.NewConnection(remoteID, conn, wr, m, name, nodeCfg.Compression)
protoConn := protocol.NewConnection(remoteID, rd, wr, m, name, nodeCfg.Compression)
l.Infof("Established secure connection to %s at %s", remoteID, name)
if debugNet {
@@ -861,9 +874,13 @@ func listenTLS(conns chan *tls.Conn, addr string, tlsCfg *tls.Config) {
}
tcaddr, err := net.ResolveTCPAddr("tcp", addr)
l.FatalErr(err)
if err != nil {
l.Fatalln("listen (BEP):", err)
}
listener, err := net.ListenTCP("tcp", tcaddr)
l.FatalErr(err)
if err != nil {
l.Fatalln("listen (BEP):", err)
}
for {
conn, err := listener.Accept()
@@ -1008,10 +1025,15 @@ func ensureDir(dir string, mode int) {
fi, err := os.Stat(dir)
if os.IsNotExist(err) {
err := os.MkdirAll(dir, 0700)
l.FatalErr(err)
if err != nil {
l.Fatalln(err)
}
} else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
err := os.Chmod(dir, os.FileMode(mode))
l.FatalErr(err)
// This can fail on crappy filesystems, nothing we can do about it.
if err != nil {
l.Warnln(err)
}
}
}
@@ -1086,12 +1108,104 @@ func getFreePort(host string, ports ...int) (int, error) {
return addr.Port, nil
}
func getLockPort() (int, error) {
var err error
lockConn, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.IP{127, 0, 0, 1}})
if err != nil {
return 0, err
func overrideGUIConfig(originalCfg config.GUIConfiguration, address, authentication, apikey string) config.GUIConfiguration {
// Make a copy of the config
cfg := originalCfg
if address == "" {
address = os.Getenv("STGUIADDRESS")
}
if address != "" {
cfg.Enabled = true
addressParts := strings.SplitN(address, "://", 2)
switch addressParts[0] {
case "http":
cfg.UseTLS = false
case "https":
cfg.UseTLS = true
default:
l.Fatalln("Unidentified protocol", addressParts[0])
}
cfg.Address = addressParts[1]
}
if authentication == "" {
authentication = os.Getenv("STGUIAUTH")
}
if authentication != "" {
authenticationParts := strings.SplitN(authentication, ":", 2)
hash, err := bcrypt.GenerateFromPassword([]byte(authenticationParts[1]), 0)
if err != nil {
l.Fatalln("Invalid GUI password:", err)
}
cfg.User = authenticationParts[0]
cfg.Password = string(hash)
}
if apikey == "" {
apikey = os.Getenv("STGUIAPIKEY")
}
if apikey != "" {
cfg.APIKey = apikey
}
return cfg
}
func standbyMonitor() {
restartDelay := time.Duration(60 * time.Second)
now := time.Now()
for {
time.Sleep(10 * time.Second)
if time.Since(now) > 2*time.Minute {
l.Infoln("Paused state detected, possibly woke up from standby. Restarting in", restartDelay)
// We most likely just woke from standby. If we restart
// immediately chances are we won't have networking ready. Give
// things a moment to stabilize.
time.Sleep(restartDelay)
restart()
return
}
now = time.Now()
}
}
func autoUpgrade() {
var skipped bool
interval := time.Duration(cfg.Options.AutoUpgradeIntervalH) * time.Hour
for {
if skipped {
time.Sleep(interval)
} else {
skipped = true
}
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
if err != nil {
l.Warnln("Automatic upgrade:", err)
continue
}
if upgrade.CompareVersions(rel.Tag, Version) <= 0 {
continue
}
l.Infof("Automatic upgrade (current %q < latest %q)", Version, rel.Tag)
err = upgrade.UpgradeTo(rel, GoArchExtra)
if err != nil {
l.Warnln("Automatic upgrade:", err)
continue
}
l.Warnf("Automatically upgraded to version %q. Restarting in 1 minute.", rel.Tag)
time.Sleep(time.Minute)
stop <- exitUpgrading
return
}
addr := lockConn.Addr().(*net.TCPAddr)
return addr.Port, nil
}

View File

@@ -1,3 +1,7 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (

View File

@@ -1,3 +1,7 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (

View File

@@ -1,3 +1,7 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// +build solaris
package main

View File

@@ -1,3 +1,7 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// +build freebsd
package main

View File

@@ -1,3 +1,7 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (

182
cmd/syncthing/monitor.go Normal file
View File

@@ -0,0 +1,182 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (
"bufio"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
)
var (
stdoutFirstLines []string // The first 10 lines of stdout
stdoutLastLines []string // The last 50 lines of stdout
stdoutMut sync.Mutex
)
const (
countRestarts = 5
loopThreshold = 15 * time.Second
)
func monitorMain() {
os.Setenv("STNORESTART", "yes")
l.SetPrefix("[monitor] ")
args := os.Args
var restarts [countRestarts]time.Time
sign := make(chan os.Signal, 1)
sigTerm := syscall.Signal(0xf)
signal.Notify(sign, os.Interrupt, sigTerm, os.Kill)
for {
if t := time.Since(restarts[0]); t < loopThreshold {
l.Warnf("%d restarts in %v; not retrying further", countRestarts, t)
os.Exit(exitError)
}
copy(restarts[0:], restarts[1:])
restarts[len(restarts)-1] = time.Now()
cmd := exec.Command(args[0], args[1:]...)
stderr, err := cmd.StderrPipe()
if err != nil {
l.Fatalln(err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
l.Fatalln(err)
}
l.Infoln("Starting syncthing")
err = cmd.Start()
if err != nil {
l.Fatalln(err)
}
stdoutMut.Lock()
stdoutFirstLines = make([]string, 0, 10)
stdoutLastLines = make([]string, 0, 50)
stdoutMut.Unlock()
go copyStderr(stderr)
go copyStdout(stdout)
exit := make(chan error)
go func() {
exit <- cmd.Wait()
}()
select {
case s := <-sign:
l.Infof("Signal %d received; exiting", s)
cmd.Process.Kill()
<-exit
return
case err = <-exit:
if err == nil {
// Successfull exit indicates an intentional shutdown
return
} else if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
switch status.ExitStatus() {
case exitUpgrading:
// Restart the monitor process to release the .old
// binary as part of the upgrade process.
l.Infoln("Restarting monitor...")
os.Setenv("STNORESTART", "")
err := exec.Command(args[0], args[1:]...).Start()
if err != nil {
l.Warnln("restart:", err)
}
return
}
}
}
}
l.Infoln("Syncthing exited:", err)
time.Sleep(1 * time.Second)
// Let the next child process know that this is not the first time
// it's starting up.
os.Setenv("STRESTART", "yes")
}
}
func copyStderr(stderr io.ReadCloser) {
br := bufio.NewReader(stderr)
var panicFd *os.File
for {
line, err := br.ReadString('\n')
if err != nil {
return
}
if panicFd == nil {
os.Stderr.WriteString(line)
if strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") {
panicFd, err = os.Create(filepath.Join(confDir, time.Now().Format("panic-20060102-150405.log")))
if err != nil {
l.Warnln("Create panic log:", err)
continue
}
l.Warnf("Panic detected, writing to \"%s\"", panicFd.Name())
l.Warnln("Please create an issue at https://github.com/syncthing/syncthing/issues/ with the panic log attached")
panicFd.WriteString("Panic at " + time.Now().Format(time.RFC1123) + "\n")
stdoutMut.Lock()
for _, line := range stdoutFirstLines {
panicFd.WriteString(line)
}
panicFd.WriteString("...\n")
for _, line := range stdoutLastLines {
panicFd.WriteString(line)
}
}
}
if panicFd != nil {
panicFd.WriteString(line)
}
}
}
func copyStdout(stderr io.ReadCloser) {
br := bufio.NewReader(stderr)
for {
line, err := br.ReadString('\n')
if err != nil {
return
}
stdoutMut.Lock()
if len(stdoutFirstLines) < cap(stdoutFirstLines) {
stdoutFirstLines = append(stdoutFirstLines, line)
}
if l := len(stdoutLastLines); l == cap(stdoutLastLines) {
stdoutLastLines = stdoutLastLines[:l-1]
}
stdoutLastLines = append(stdoutLastLines, line)
stdoutMut.Unlock()
os.Stdout.WriteString(line)
}
}

View File

@@ -2,7 +2,7 @@
// 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
// +build !solaris,!windows
package main
@@ -12,10 +12,12 @@ import (
"runtime"
"syscall"
"time"
"github.com/syncthing/syncthing/internal/protocol"
)
func init() {
if os.Getenv("STPERFSTATS") != "" {
if innerProcess && os.Getenv("STPERFSTATS") != "" {
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
}
}
@@ -30,6 +32,7 @@ func savePerfStats(file string) {
var prevTime int64
var rusage syscall.Rusage
var memstats runtime.MemStats
var prevIn, prevOut uint64
t0 := time.Now()
for t := range time.NewTicker(250 * time.Millisecond).C {
@@ -41,11 +44,18 @@ func savePerfStats(file string) {
cpuUsagePercent := 100 * float64(usageDiff) / float64(timeDiff)
prevTime = curTime
prevUsage = curUsage
in, out := protocol.TotalInOut()
var inRate, outRate float64
if timeDiff > 0 {
inRate = float64(in-prevIn) / (float64(timeDiff) / 1e9) // bytes per second
outRate = float64(out-prevOut) / (float64(timeDiff) / 1e9) // bytes per second
}
prevIn, prevOut = in, out
runtime.ReadMemStats(&memstats)
startms := int(t.Sub(t0).Seconds() * 1000)
fmt.Fprintf(fd, "%d\t%f\t%d\t%d\n", startms, cpuUsagePercent, memstats.Alloc, memstats.Sys-memstats.HeapReleased)
fmt.Fprintf(fd, "%d\t%f\t%d\t%d\t%.0f\t%.0f\n", startms, cpuUsagePercent, memstats.Alloc, memstats.Sys-memstats.HeapReleased, inRate, outRate)
}
}

View File

@@ -5,6 +5,7 @@
package main
import (
"bufio"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
@@ -13,8 +14,10 @@ import (
"crypto/x509/pkix"
"encoding/binary"
"encoding/pem"
"io"
"math/big"
mr "math/rand"
"net"
"os"
"path/filepath"
"time"
@@ -42,7 +45,9 @@ func newCertificate(dir string, prefix string) {
l.Infoln("Generating RSA key and certificate...")
priv, err := rsa.GenerateKey(rand.Reader, tlsRSABits)
l.FatalErr(err)
if err != nil {
l.Fatalln("generate key:", err)
}
notBefore := time.Now()
notAfter := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
@@ -61,15 +66,71 @@ func newCertificate(dir string, prefix string) {
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
l.FatalErr(err)
if err != nil {
l.Fatalln("create cert:", err)
}
certOut, err := os.Create(filepath.Join(dir, prefix+"cert.pem"))
l.FatalErr(err)
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
if err != nil {
l.Fatalln("save cert:", err)
}
err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
if err != nil {
l.Fatalln("save cert:", err)
}
err = certOut.Close()
if err != nil {
l.Fatalln("save cert:", err)
}
keyOut, err := os.OpenFile(filepath.Join(dir, prefix+"key.pem"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
l.FatalErr(err)
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
keyOut.Close()
if err != nil {
l.Fatalln("save key:", err)
}
err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
if err != nil {
l.Fatalln("save key:", err)
}
err = keyOut.Close()
if err != nil {
l.Fatalln("save key:", err)
}
}
type DowngradingListener struct {
net.Listener
TLSConfig *tls.Config
}
type WrappedConnection struct {
io.Reader
net.Conn
}
func (l *DowngradingListener) Accept() (net.Conn, error) {
conn, err := l.Listener.Accept()
if err != nil {
return nil, err
}
br := bufio.NewReader(conn)
bs, err := br.Peek(1)
if err != nil {
// We hit a read error here, but the Accept() call succeeded so we must not return an error.
// We return the connection as is and let whoever tries to use it deal with the error.
return conn, nil
}
wrapper := &WrappedConnection{br, conn}
// 0x16 is the first byte of a TLS handshake
if bs[0] == 0x16 {
return tls.Server(wrapper, l.TLSConfig), nil
}
return wrapper, nil
}
func (c *WrappedConnection) Read(b []byte) (n int, err error) {
return c.Reader.Read(b)
}

View File

@@ -1,3 +1,7 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package main
import (
@@ -11,7 +15,7 @@ import (
"strings"
"time"
"github.com/syncthing/syncthing/model"
"github.com/syncthing/syncthing/internal/model"
)
// Current version number of the usage report, for acceptance purposes. If

View File

@@ -1,378 +0,0 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package config
import (
"bytes"
"io"
"os"
"reflect"
"testing"
"github.com/syncthing/syncthing/protocol"
)
var node1, node2, node3, node4 protocol.NodeID
func init() {
node1, _ = protocol.NodeIDFromString("AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ")
node2, _ = protocol.NodeIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
node3, _ = protocol.NodeIDFromString("LGFPDIT-7SKNNJL-VJZA4FC-7QNCRKA-CE753K7-2BW5QDK-2FOZ7FR-FEP57QJ")
node4, _ = protocol.NodeIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
}
func TestDefaultValues(t *testing.T) {
expected := OptionsConfiguration{
ListenAddress: []string{"0.0.0.0:22000"},
GlobalAnnServer: "announce.syncthing.net:22026",
GlobalAnnEnabled: true,
LocalAnnEnabled: true,
LocalAnnPort: 21025,
LocalAnnMCAddr: "[ff32::5222]:21026",
ParallelRequests: 16,
MaxSendKbps: 0,
ReconnectIntervalS: 60,
StartBrowser: true,
UPnPEnabled: true,
UPnPLease: 0,
UPnPRenewal: 30,
}
cfg, err := Load(bytes.NewReader(nil), node1)
if err != io.EOF {
t.Error(err)
}
if !reflect.DeepEqual(cfg.Options, expected) {
t.Errorf("Default config differs;\n E: %#v\n A: %#v", expected, cfg.Options)
}
}
func TestNodeConfig(t *testing.T) {
v1data := []byte(`
<configuration version="1">
<repository id="test" directory="~/Sync">
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
<address>a</address>
</node>
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
<address>b</address>
</node>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
<address>a</address>
</node>
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
<address>b</address>
</node>
</repository>
<options>
<readOnly>true</readOnly>
<rescanIntervalS>600</rescanIntervalS>
</options>
</configuration>
`)
v2data := []byte(`
<configuration version="2">
<repository id="test" directory="~/Sync" ro="true">
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
</repository>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
<address>a</address>
</node>
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
<address>b</address>
</node>
<options>
<rescanIntervalS>600</rescanIntervalS>
</options>
</configuration>
`)
v3data := []byte(`
<configuration version="3">
<repository id="test" directory="~/Sync" ro="true" ignorePerms="false">
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" compression="false"></node>
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" compression="false"></node>
</repository>
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="true">
<address>a</address>
</node>
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="true">
<address>b</address>
</node>
<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)
}
expectedRepos := []RepositoryConfiguration{
{
ID: "test",
Directory: "~/Sync",
Nodes: []RepositoryNodeConfiguration{{NodeID: node1}, {NodeID: node4}},
ReadOnly: true,
RescanIntervalS: 600,
},
}
expectedNodes := []NodeConfiguration{
{
NodeID: node1,
Name: "node one",
Addresses: []string{"a"},
Compression: true,
},
{
NodeID: node4,
Name: "node two",
Addresses: []string{"b"},
Compression: true,
},
}
expectedNodeIDs := []protocol.NodeID{node1, node4}
if cfg.Version != 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)
}
if !reflect.DeepEqual(cfg.Nodes, expectedNodes) {
t.Errorf("%d: Incorrect Nodes\n A: %#v\n E: %#v", i, cfg.Nodes, expectedNodes)
}
if !reflect.DeepEqual(cfg.Repositories[0].NodeIDs(), expectedNodeIDs) {
t.Errorf("%d: Incorrect NodeIDs\n A: %#v\n E: %#v", i, cfg.Repositories[0].NodeIDs(), expectedNodeIDs)
}
if len(cfg.NodeMap()) != len(expectedNodes) {
t.Errorf("Unexpected number of NodeMap() entries")
}
if len(cfg.RepoMap()) != len(expectedRepos) {
t.Errorf("Unexpected number of RepoMap() entries")
}
}
}
func TestNoListenAddress(t *testing.T) {
data := []byte(`<configuration version="1">
<options>
<listenAddress></listenAddress>
</options>
</configuration>
`)
cfg, err := Load(bytes.NewReader(data), node1)
if err != nil {
t.Error(err)
}
expected := []string{""}
if !reflect.DeepEqual(cfg.Options.ListenAddress, expected) {
t.Errorf("Unexpected ListenAddress %#v", cfg.Options.ListenAddress)
}
}
func TestOverriddenValues(t *testing.T) {
data := []byte(`<configuration version="2">
<options>
<listenAddress>:23000</listenAddress>
<allowDelete>false</allowDelete>
<globalAnnounceServer>syncthing.nym.se:22026</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>false</localAnnounceEnabled>
<localAnnouncePort>42123</localAnnouncePort>
<localAnnounceMCAddr>quux:3232</localAnnounceMCAddr>
<parallelRequests>32</parallelRequests>
<maxSendKbps>1234</maxSendKbps>
<reconnectionIntervalS>6000</reconnectionIntervalS>
<startBrowser>false</startBrowser>
<upnpEnabled>false</upnpEnabled>
<upnpLeaseMinutes>60</upnpLeaseMinutes>
<upnpRenewalMinutes>15</upnpRenewalMinutes>
</options>
</configuration>
`)
expected := OptionsConfiguration{
ListenAddress: []string{":23000"},
GlobalAnnServer: "syncthing.nym.se:22026",
GlobalAnnEnabled: false,
LocalAnnEnabled: false,
LocalAnnPort: 42123,
LocalAnnMCAddr: "quux:3232",
ParallelRequests: 32,
MaxSendKbps: 1234,
ReconnectIntervalS: 6000,
StartBrowser: false,
UPnPEnabled: false,
UPnPLease: 60,
UPnPRenewal: 15,
}
cfg, err := Load(bytes.NewReader(data), node1)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(cfg.Options, expected) {
t.Errorf("Overridden config differs;\n E: %#v\n A: %#v", expected, cfg.Options)
}
}
func TestNodeAddressesDynamic(t *testing.T) {
data := []byte(`
<configuration version="2">
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
<address></address>
</node>
<node id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
</node>
<node id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
<address>dynamic</address>
</node>
</configuration>
`)
name, _ := os.Hostname()
expected := []NodeConfiguration{
{
NodeID: node1,
Addresses: []string{"dynamic"},
Compression: true,
},
{
NodeID: node2,
Addresses: []string{"dynamic"},
Compression: true,
},
{
NodeID: node3,
Addresses: []string{"dynamic"},
Compression: true,
},
{
NodeID: node4,
Name: name, // Set when auto created
Addresses: []string{"dynamic"},
},
}
cfg, err := Load(bytes.NewReader(data), node4)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(cfg.Nodes, expected) {
t.Errorf("Nodes differ;\n E: %#v\n A: %#v", expected, cfg.Nodes)
}
}
func TestNodeAddressesStatic(t *testing.T) {
data := []byte(`
<configuration version="3">
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
<address>192.0.2.1</address>
<address>192.0.2.2</address>
</node>
<node id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
<address>192.0.2.3:6070</address>
<address>[2001:db8::42]:4242</address>
</node>
<node id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
<address>[2001:db8::44]:4444</address>
<address>192.0.2.4:6090</address>
</node>
</configuration>
`)
name, _ := os.Hostname()
expected := []NodeConfiguration{
{
NodeID: node1,
Addresses: []string{"192.0.2.1", "192.0.2.2"},
},
{
NodeID: node2,
Addresses: []string{"192.0.2.3:6070", "[2001:db8::42]:4242"},
},
{
NodeID: node3,
Addresses: []string{"[2001:db8::44]:4444", "192.0.2.4:6090"},
},
{
NodeID: node4,
Name: name, // Set when auto created
Addresses: []string{"dynamic"},
},
}
cfg, err := Load(bytes.NewReader(data), node4)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(cfg.Nodes, expected) {
t.Errorf("Nodes differ;\n E: %#v\n A: %#v", expected, cfg.Nodes)
}
}
func TestVersioningConfig(t *testing.T) {
data := []byte(`
<configuration version="2">
<repository id="test" directory="~/Sync" ro="true">
<versioning type="simple">
<param key="foo" val="bar"/>
<param key="baz" val="quux"/>
</versioning>
</repository>
</configuration>
`)
cfg, err := Load(bytes.NewReader(data), node4)
if err != nil {
t.Error(err)
}
vc := cfg.Repositories[0].Versioning
if vc.Type != "simple" {
t.Errorf(`vc.Type %q != "simple"`, vc.Type)
}
if l := len(vc.Params); l != 2 {
t.Errorf("len(vc.Params) %d != 2", l)
}
expected := map[string]string{
"foo": "bar",
"baz": "quux",
}
if !reflect.DeepEqual(vc.Params, expected) {
t.Errorf("vc.Params differ;\n E: %#v\n A: %#v", expected, vc.Params)
}
}

View File

@@ -1,11 +0,0 @@
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)
}

View File

View File

@@ -3,7 +3,7 @@
// license that can be found in the LICENSE file.
/*jslint browser: true, continue: true, plusplus: true */
/*global $: false, angular: false */
/*global $: false, angular: false, console: false, validLangs: false */
'use strict';
@@ -15,52 +15,52 @@ syncthing.config(function ($httpProvider, $translateProvider) {
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token';
$translateProvider.useStaticFilesLoader({
prefix: 'lang-',
prefix: 'lang/lang-',
suffix: '.json'
});
});
syncthing.controller('EventCtrl', function ($scope, $http) {
$scope.lastEvent = null;
var online = false;
var lastID = 0;
$(window).bind('beforeunload', function() {
online = false;
});
var successFn = function (data) {
if (!online) {
$scope.$emit('UIOnline');
online = true;
// When Syncthing restarts while the long polling connection is in
// progress the browser on some platforms returns a 200 (since the
// headers has been flushed with the return code 200), with no data.
// This basically means that the connection has been reset, and the call
// was not actually sucessful.
if (!data) {
errorFn(data);
return;
}
$scope.$emit('UIOnline');
if (lastID > 0) {
data.forEach(function (event) {
console.log("event", event.id, event.type, event.data);
$scope.$emit(event.type, event);
});
};
}
$scope.lastEvent = data[data.length - 1];
lastID = $scope.lastEvent.id;
setTimeout(function () {
$http.get(urlbase + '/events?since=' + lastID)
.success(successFn)
.error(errorFn);
.success(successFn)
.error(errorFn);
}, 500);
};
var errorFn = function (data) {
if (online) {
$scope.$emit('UIOffline');
online = false;
}
$scope.$emit('UIOffline');
setTimeout(function () {
$http.get(urlbase + '/events?limit=1')
.success(successFn)
.error(errorFn);
.success(successFn)
.error(errorFn);
}, 1000);
};
@@ -72,6 +72,8 @@ syncthing.controller('EventCtrl', function ($scope, $http) {
syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $location) {
var prevDate = 0;
var getOK = true;
var navigatingAway = false;
var online = false;
var restarting = false;
$scope.completion = {};
@@ -88,8 +90,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.repos = {};
$scope.seenError = '';
$scope.upgradeInfo = {};
$scope.stats = {};
$http.get(urlbase+"/lang").success(function (langs) {
$http.get(urlbase + "/lang").success(function (langs) {
// 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
@@ -98,15 +101,32 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
var lang, matching;
for (var i = 0; i < langs.length; i++) {
lang = langs[i];
matching = validLangs.filter(function (l) {
return lang.length >= 2 && l.indexOf(lang) == 0;
if (lang.length < 2) {
continue;
}
matching = validLangs.filter(function (possibleLang) {
// The langs returned by the /rest/langs call will be in lower
// case. We compare to the lowercase version of the language
// code we have as well.
possibleLang = possibleLang.toLowerCase();
if (possibleLang.length > lang.length) {
return possibleLang.indexOf(lang) === 0;
} else {
return lang.indexOf(possibleLang) === 0;
}
});
if (matching.length >= 1) {
$translate.use(matching[0]);
break;
return;
}
}
})
// Fallback if nothing matched
$translate.use("en");
});
$(window).bind('beforeunload', function () {
navigatingAway = true;
});
$scope.$on("$locationChangeSuccess", function () {
var lang = $location.search().lang;
@@ -120,25 +140,39 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
'rmdir': 'Del (dir)',
'sync': 'Sync',
'touch': 'Update',
}
};
$scope.needIcons = {
'rm': 'remove',
'rmdir': 'remove',
'sync': 'download',
'touch': 'asterisk',
}
};
$scope.$on('UIOnline', function (event, arg) {
console.log('UIOnline');
$scope.init();
restarting = false;
$('#networkError').modal('hide');
$('#restarting').modal('hide');
$('#shutdown').modal('hide');
if (online && !restarting) {
return;
}
if (restarting) {
document.location.reload(true);
} else {
console.log('UIOnline');
$scope.init();
online = true;
restarting = false;
$('#networkError').modal('hide');
$('#restarting').modal('hide');
$('#shutdown').modal('hide');
}
});
$scope.$on('UIOffline', function (event, arg) {
if (navigatingAway || !online) {
return;
}
console.log('UIOffline');
online = false;
if (!restarting) {
$('#networkError').modal();
}
@@ -169,6 +203,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.$on('NodeDisconnected', function (event, arg) {
delete $scope.connections[arg.data.id];
refreshNodeStats();
});
$scope.$on('NodeConnected', function (event, arg) {
@@ -187,7 +222,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
});
$scope.$on('ConfigLoaded', function (event) {
if ($scope.config.Options.URAccepted == 0) {
if ($scope.config.Options.URAccepted === 0) {
// If usage reporting has been neither accepted nor declined,
// we want to ask the user to make a choice. But we don't want
// to bug them during initial setup, so we set a cookie with
@@ -197,14 +232,22 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
if (!firstVisit) {
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
} else {
if (+firstVisit < Date.now() - 4*3600*1000){
if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
$('#ur').modal();
}
}
}
})
});
$scope.$on('ConfigSaved', function (event, arg) {
updateLocalConfig(arg.data);
$http.get(urlbase + '/config/sync').success(function (data) {
$scope.configInSync = data.configInSync;
});
});
var debouncedFuncs = {};
@@ -221,6 +264,33 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
debouncedFuncs[key]();
}
function updateLocalConfig(config) {
var hasConfig = !isEmptyObject($scope.config);
$scope.config = config;
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
$scope.nodes = $scope.config.Nodes;
$scope.nodes.forEach(function (nodeCfg) {
$scope.completion[nodeCfg.NodeID] = {
_total: 100,
};
});
$scope.nodes.sort(nodeCompare);
$scope.repos = repoMap($scope.config.Repositories);
Object.keys($scope.repos).forEach(function (repo) {
refreshRepo(repo);
$scope.repos[repo].Nodes.forEach(function (nodeCfg) {
refreshCompletion(nodeCfg.NodeID, repo);
});
});
if (!hasConfig) {
$scope.$emit('ConfigLoaded');
}
}
function refreshSystem() {
$http.get(urlbase + '/system').success(function (data) {
$scope.myID = data.myID;
@@ -231,7 +301,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
function refreshCompletion(node, repo) {
if (node === $scope.myID) {
return
return;
}
var key = "refreshCompletion" + node + repo;
@@ -243,7 +313,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
}
$scope.completion[node][repo] = data.completion;
var tot = 0, cnt = 0;
var tot = 0,
cnt = 0;
for (var cmp in $scope.completion[node]) {
if (cmp === "_total") {
continue;
@@ -263,8 +334,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
function refreshConnectionStats() {
$http.get(urlbase + '/connections').success(function (data) {
var now = Date.now(),
td = (now - prevDate) / 1000,
id;
td = (now - prevDate) / 1000,
id;
prevDate = now;
for (id in data) {
@@ -286,38 +357,14 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
function refreshErrors() {
$http.get(urlbase + '/errors').success(function (data) {
$scope.errors = data;
$scope.errors = data.errors;
console.log("refreshErrors", data);
});
}
function refreshConfig() {
$http.get(urlbase + '/config').success(function (data) {
var hasConfig = !isEmptyObject($scope.config);
$scope.config = data;
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
$scope.nodes = $scope.config.Nodes;
$scope.nodes.forEach(function (nodeCfg) {
$scope.completion[nodeCfg.NodeID] = {
_total: 100,
};
});
$scope.nodes.sort(nodeCompare);
$scope.repos = repoMap($scope.config.Repositories);
Object.keys($scope.repos).forEach(function (repo) {
refreshRepo(repo);
$scope.repos[repo].Nodes.forEach(function (nodeCfg) {
refreshCompletion(nodeCfg.NodeID, repo);
});
});
if (!hasConfig) {
$scope.$emit('ConfigLoaded');
}
updateLocalConfig(data);
console.log("refreshConfig", data);
});
@@ -326,13 +373,25 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
});
}
$scope.init = function() {
var refreshNodeStats = debounce(function () {
$http.get(urlbase + "/stats/node").success(function (data) {
$scope.stats = data;
for (var node in $scope.stats) {
$scope.stats[node].LastSeen = new Date($scope.stats[node].LastSeen);
$scope.stats[node].LastSeenDays = (new Date() - $scope.stats[node].LastSeen) / 1000 / 86400;
}
console.log("refreshNodeStats", data);
});
}, 500);
$scope.init = function () {
refreshSystem();
refreshConfig();
refreshConnectionStats();
refreshNodeStats();
$http.get(urlbase + '/version').success(function (data) {
$scope.version = data;
$scope.version = data.version;
});
$http.get(urlbase + '/report').success(function (data) {
@@ -438,19 +497,10 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
return '';
};
$scope.nodeVer = function (nodeCfg) {
if (nodeCfg.NodeID === $scope.myID) {
return $scope.version;
}
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
return conn.ClientVersion;
}
return '?';
};
$scope.findNode = function (nodeID) {
var matches = $scope.nodes.filter(function (n) { return n.NodeID == nodeID; });
var matches = $scope.nodes.filter(function (n) {
return n.NodeID == nodeID;
});
if (matches.length != 1) {
return undefined;
}
@@ -482,13 +532,19 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
// Make a working copy
$scope.tmpOptions = angular.copy($scope.config.Options);
$scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
$scope.tmpOptions.NodeName = $scope.thisNode().Name;
$scope.tmpOptions.AutoUpgradeEnabled = ($scope.tmpOptions.AutoUpgradeIntervalH > 0);
$scope.tmpGUI = angular.copy($scope.config.GUI);
$('#settings').modal();
};
$scope.saveConfig = function() {
$scope.saveConfig = function () {
var cfg = JSON.stringify($scope.config);
var opts = {headers: {'Content-Type': 'application/json'}};
var opts = {
headers: {
'Content-Type': 'application/json'
}
};
$http.post(urlbase + '/config', cfg, opts).success(function () {
$http.get(urlbase + '/config/sync').success(function (data) {
$scope.configInSync = data.configInSync;
@@ -499,24 +555,34 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.saveSettings = function () {
// Make sure something changed
var changed = !angular.equals($scope.config.Options, $scope.tmpOptions) ||
!angular.equals($scope.config.GUI, $scope.tmpGUI);
!angular.equals($scope.config.GUI, $scope.tmpGUI);
if (changed) {
// Check if usage reporting has been enabled or disabled
if ($scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted <= 0) {
$scope.tmpOptions.URAccepted = 1000;
} else if (!$scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted > 0){
} else if (!$scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted > 0) {
$scope.tmpOptions.URAccepted = -1;
}
// Check if auto-upgrade has been enabled or disabled
if ($scope.tmpOptions.AutoUpgradeEnabled) {
$scope.tmpOptions.AutoUpgradeIntervalH = $scope.tmpOptions.AutoUpgradeIntervalH || 12;
} else {
$scope.tmpOptions.AutoUpgradeIntervalH = 0;
}
// Check if protocol will need to be changed on restart
if($scope.config.GUI.UseTLS !== $scope.tmpGUI.UseTLS){
if ($scope.config.GUI.UseTLS !== $scope.tmpGUI.UseTLS) {
$scope.protocolChanged = true;
}
// Apply new settings locally
$scope.thisNode().Name = $scope.tmpOptions.NodeName;
$scope.config.Options = angular.copy($scope.tmpOptions);
$scope.config.GUI = angular.copy($scope.tmpGUI);
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) {
return x.trim();
});
$scope.saveConfig();
}
@@ -531,16 +597,16 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.configInSync = true;
// Switch webpage protocol if needed
if($scope.protocolChanged){
if ($scope.protocolChanged) {
var protocol = 'http';
if($scope.config.GUI.UseTLS){
protocol = 'https';
if ($scope.config.GUI.UseTLS) {
protocol = 'https';
}
setTimeout(function(){
setTimeout(function () {
window.location.protocol = protocol;
}, 1000);
}, 2500);
$scope.protocolChanged = false;
}
@@ -579,7 +645,11 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
};
$scope.addNode = function () {
$scope.currentNode = {AddressesStr: 'dynamic', Compression: true};
$scope.currentNode = {
AddressesStr: 'dynamic',
Compression: true,
Introducer: true
};
$scope.editingExisting = false;
$scope.editingSelf = false;
$scope.nodeEditor.$setPristine();
@@ -611,7 +681,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$('#editNode').modal('hide');
nodeCfg = $scope.currentNode;
nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) { return x.trim(); });
nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) {
return x.trim();
});
done = false;
for (i = 0; i < $scope.nodes.length; i++) {
@@ -633,7 +705,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
};
$scope.otherNodes = function () {
return $scope.nodes.filter(function (n){
return $scope.nodes.filter(function (n) {
return n.NodeID !== $scope.myID;
});
};
@@ -698,17 +770,26 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$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 || "";
// staggeredMaxAge can validly be zero, which we should not replace
// with the default value of 365. So only set the default if it's
// actually undefined.
if (typeof $scope.currentRepo.staggeredMaxAge === 'undefined') {
$scope.currentRepo.staggeredMaxAge = 365;
}
$scope.editingExisting = true;
$scope.repoEditor.$setPristine();
$('#editRepo').modal();
};
$scope.addRepo = function () {
$scope.currentRepo = {selectedNodes: {}};
$scope.currentRepo = {
selectedNodes: {}
};
$scope.currentRepo.RescanIntervalS = 60;
$scope.currentRepo.FileVersioningSelector = "none";
$scope.currentRepo.simpleKeep = 5;
$scope.currentRepo.staggeredMaxAge = 365;
@@ -728,7 +809,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
repoCfg.selectedNodes[$scope.myID] = true;
for (var nodeID in repoCfg.selectedNodes) {
if (repoCfg.selectedNodes[nodeID] === true) {
repoCfg.Nodes.push({NodeID: nodeID});
repoCfg.Nodes.push({
NodeID: nodeID
});
}
}
delete repoCfg.selectedNodes;
@@ -766,7 +849,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.saveConfig();
};
$scope.sharesRepo = function(repoCfg) {
$scope.sharesRepo = function (repoCfg) {
var names = [];
repoCfg.Nodes.forEach(function (node) {
names.push($scope.nodeName($scope.findNode(node.NodeID)));
@@ -787,10 +870,55 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.saveConfig();
};
$scope.editIgnores = function () {
if (!$scope.editingExisting) {
return;
}
$('#editIgnoresButton').attr('disabled', 'disabled');
$http.get(urlbase + '/ignores?repo=' + encodeURIComponent($scope.currentRepo.ID))
.success(function (data) {
data.ignore = data.ignore || [];
$('#editRepo').modal('hide');
var textArea = $('#editIgnores textarea');
textArea.val(data.ignore.join('\n'));
$('#editIgnores').modal()
.on('hidden.bs.modal', function () {
$('#editRepo').modal();
})
.on('shown.bs.modal', function () {
textArea.focus();
});
})
.then(function () {
$('#editIgnoresButton').removeAttr('disabled');
});
};
$scope.saveIgnores = function () {
if (!$scope.editingExisting) {
return;
}
$http.post(urlbase + '/ignores?repo=' + encodeURIComponent($scope.currentRepo.ID), {
ignore: $('#editIgnores textarea').val().split('\n')
});
};
$scope.setAPIKey = function (cfg) {
cfg.APIKey = randomString(30, 32);
};
$scope.showURPreview = function () {
$('#settings').modal('hide');
$('#urPreview').modal().on('hidden.bs.modal', function () {
$('#settings').modal();
});
};
$scope.acceptUR = function () {
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
$scope.saveConfig();
@@ -816,7 +944,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
var fDelete = 4096;
var fDirectory = 16384;
if ((file.Flags & (fDelete+fDirectory)) === fDelete+fDirectory) {
if ((file.Flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
return 'rmdir';
} else if ((file.Flags & fDelete) === fDelete) {
return 'rm';
@@ -860,10 +988,10 @@ function nodeCompare(a, b) {
}
function repoCompare(a, b) {
if (a.Directory < b.Directory) {
if (a.ID < b.ID) {
return -1;
}
return a.Directory > b.Directory;
return a.ID > b.ID;
}
function repoMap(l) {
@@ -895,12 +1023,11 @@ function decimals(val, num) {
return decs;
}
function randomString(len, bits)
{
function randomString(len, bits) {
bits = bits || 36;
var outStr = "", newStr;
while (outStr.length < len)
{
var outStr = "",
newStr;
while (outStr.length < len) {
newStr = Math.random().toString(bits).slice(2);
outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
}
@@ -918,21 +1045,21 @@ function isEmptyObject(obj) {
function debounce(func, wait) {
var timeout, args, context, timestamp, result, again;
var later = function() {
var later = function () {
var last = Date.now() - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (again) {
again = false;
result = func.apply(context, args);
context = args = null;
again = false;
}
}
};
return function() {
return function () {
context = this;
args = arguments;
timestamp = Date.now();
@@ -997,12 +1124,6 @@ syncthing.filter('metric', function () {
};
});
syncthing.filter('short', function () {
return function (input) {
return input.substr(0, 6);
};
});
syncthing.filter('alwaysNumber', function () {
return function (input) {
if (input === undefined) {
@@ -1012,18 +1133,6 @@ syncthing.filter('alwaysNumber', function () {
};
});
syncthing.filter('shortPath', function () {
return function (input) {
if (input === undefined)
return "";
var parts = input.split(/[\/\\]/);
if (!parts || parts.length <= 3) {
return input;
}
return ".../" + parts.slice(parts.length-2).join("/");
};
});
syncthing.filter('basename', function () {
return function (input) {
if (input === undefined)
@@ -1032,33 +1141,15 @@ syncthing.filter('basename', function () {
if (!parts || parts.length < 1) {
return input;
}
return parts[parts.length-1];
return parts[parts.length - 1];
};
});
syncthing.filter('clean', function () {
return function (input) {
return encodeURIComponent(input).replace(/%/g, '');
};
});
syncthing.directive('optionEditor', function () {
return {
restrict: 'C',
replace: true,
transclude: true,
scope: {
setting: '=setting',
},
template: '<input type="text" ng-model="config.Options[setting.id]"></input>',
};
});
syncthing.directive('uniqueRepo', function() {
syncthing.directive('uniqueRepo', function () {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
link: function (scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
if (scope.editingExisting) {
// we shouldn't validate
ctrl.$setValidity('uniqueRepo', true);
@@ -1075,16 +1166,16 @@ syncthing.directive('uniqueRepo', function() {
};
});
syncthing.directive('validNodeid', function($http) {
syncthing.directive('validNodeid', function ($http) {
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function(viewValue) {
link: function (scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
if (scope.editingExisting) {
// we shouldn't validate
ctrl.$setValidity('validNodeid', true);
} else {
$http.get(urlbase + '/nodeid?id='+viewValue).success(function (resp) {
$http.get(urlbase + '/nodeid?id=' + viewValue).success(function (resp) {
if (resp.error) {
ctrl.$setValidity('validNodeid', false);
} else {
@@ -1111,5 +1202,5 @@ syncthing.directive('modal', function () {
close: '@',
large: '@',
},
}
};
});

BIN
gui/font/raleway-500.woff Normal file
View File

Binary file not shown.

View File

@@ -2,5 +2,5 @@
font-family: 'Raleway';
font-style: normal;
font-weight: 500;
src: local('Raleway'), url(raleway-500.ttf) format('truetype');
src: local('Raleway'), url(raleway-500.woff) format('woff');
}

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -11,86 +11,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link rel="shortcut icon" href="favicon.png">
<link rel="shortcut icon" href="img/favicon.png">
<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;
}
.text-monospace {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
.table-condensed>thead>tr>th, .table-condensed>tbody>tr>th, .table-condensed>tfoot>tr>th, .table-condensed>thead>tr>td, .table-condensed>tbody>tr>td, .table-condensed>tfoot>tr>td {
border-top: none;
}
.logo {
margin: 0;
padding: 0;
top: -5px;
position: relative;
}
.list-no-bullet {
list-style-type: none
}
.li-column {
display: inline-block;
min-width: 7em;
margin-right: 1em;
background-color: rgb(236, 240, 241);
border-radius: 3px;
padding: 1px 4px;
margin: 2px 2px;
}
.li-column span.data {
margin-left: 0.5em;
min-width: 10em;
text-align: right;
display: inline-block;
}
.ng-cloak {
display: none !important;
}
.table th {
white-space: nowrap;
font-weight: 400;
}
.table td {
padding-left: 20px !important;
}
.table td.small-data {
white-space: nowrap;
}
@media (max-width:767px) {
.table-responsive>.table>tbody>tr>td {
/* revert a bootstrap setting e.g.:
* for mobile phones to allow linebreaks in long repro folder/shared with
* columns. */
white-space: normal;
}
}
</style>
<link href="font/raleway.css" rel="stylesheet">
<link href="overrides.css" rel="stylesheet">
</head>
<body>
@@ -100,21 +26,18 @@
<nav class="navbar navbar-top navbar-default" role="navigation">
<div class="container">
<span class="navbar-brand"><img class="logo" src="logo-text-64.png" height="32" width="117"/></span>
<span class="navbar-brand"><img class="logo" src="img/logo-text-64.png" height="32" width="117"/></span>
<p class="navbar-text hidden-xs">{{thisNodeName()}}</p>
<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()">
<span class="glyphicon glyphicon-chevron-up"></span>&emsp;
<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
<button type="button" class="btn navbar-btn btn-primary btn-sm" href="" ng-click="upgrade()">
<span class="glyphicon glyphicon-chevron-up"></span>&emsp;
<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
</button>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span translate>Edit</spanq&nbsp;<b class="caret"></b></a>
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-cog"></span></a>
<ul class="dropdown-menu">
<li><a href="" ng-click="addRepo()"><span class="glyphicon glyphicon-hdd"></span>&emsp;<span translate>Add Repository</span></a></li>
<li><a href="" ng-click="addNode()"><span class="glyphicon glyphicon-retweet"></span>&emsp;<span translate>Add Node</span></a></li>
<li class="divider"></li>
<li><a href="" ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span>&emsp;<span translate>Settings</span></a></li>
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span>&emsp;<span translate>Show ID</span></a></li>
<li class="divider"></li>
@@ -156,200 +79,217 @@
<div class="col-md-6">
<div class="panel-group" id="repositories">
<div class="panel panel-{{repoClass(repo.ID)}}" ng-repeat="repo in repoList()">
<div class="panel-heading">
<div class="panel-heading" data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}" style="cursor: pointer">
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}">
<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>
<span translate ng-switch-when="scanning">Scanning</span>
<span ng-switch-when="syncing">
<span translate>Syncing</span>
({{syncPercentage(repo.ID)}}%)
</span>
<span ng-switch-when="idle">
<span translate>Idle</span>
({{syncPercentage(repo.ID)}}%)
</span>
<span class="glyphicon glyphicon-hdd"></span>&emsp;{{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>
<span translate ng-switch-when="scanning">Scanning</span>
<span ng-switch-when="syncing">
<span translate>Syncing</span>
({{syncPercentage(repo.ID)}}%)
</span>
</a>
<span ng-switch-when="idle">
<span translate>Idle</span>
({{syncPercentage(repo.ID)}}%)
</span>
</span>
</h3>
</div>
<div id="repo-{{$index}}" class="panel-collapse collapse">
<div id="repo-{{$index}}" class="panel-collapse collapse" ng-class="{in: $index === 0}">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Repository ID</span></th>
<td class="text-right">{{repo.ID}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-folder-open"></span>&emsp;<span translate>Folder</span></th>
<td class="text-right">{{repo.Directory}}</td>
</tr>
<tr ng-if="model[repo.ID].invalid">
<th><span class="glyphicon glyphicon-warning-sign"></span>&emsp;<span translate>Error</span></th>
<td class="text-right">{{model[repo.ID].invalid}}</td>
</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>
</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>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Out Of Sync</span></th>
<td class="text-right">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Repository ID</span></th>
<td class="text-right">{{repo.ID}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-folder-open"></span>&emsp;<span translate>Folder</span></th>
<td class="text-right">{{repo.Directory}}</td>
</tr>
<tr ng-if="model[repo.ID].invalid">
<th><span class="glyphicon glyphicon-warning-sign"></span>&emsp;<span translate>Error</span></th>
<td class="text-right">{{model[repo.ID].invalid}}</td>
</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>
</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>
</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>
<span ng-if="model[repo.ID].needFiles == 0">0 <span translate>items</span>, 0 B</span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-lock"></span>&emsp;<span translate>Master Repo</span></th>
<td class="text-right">
<span translate ng-if="repo.ReadOnly">Yes</span>
<span translate ng-if="!repo.ReadOnly">No</span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-unchecked"></span>&emsp;<span translate>Ignore Permissions</span></th>
<td class="text-right">
<span translate ng-if="repo.IgnorePerms">Yes</span>
<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>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-lock"></span>&emsp;<span translate>Master Repo</span></th>
<td class="text-right">
<span translate ng-if="repo.ReadOnly">Yes</span>
<span translate ng-if="!repo.ReadOnly">No</span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-unchecked"></span>&emsp;<span translate>Ignore Permissions</span></th>
<td class="text-right">
<span translate ng-if="repo.IgnorePerms">Yes</span>
<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>
</tr>
</tbody>
</table>
</div>
<div class="panel-footer">
<button class="btn btn-sm btn-danger" ng-if="repo.ReadOnly && model[repo.ID].needFiles > 0" ng-click="override(repo.ID)" href=""><span class="glyphicon glyphicon-upload"></span>&emsp;<span translate>Override Changes</span></button>
<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>
<button class="btn btn-sm btn-default" href="" ng-show="repoStatus(repo.ID) == 'idle'" ng-click="rescanRepo(repo.ID)"><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan</span></button>
<button class="btn btn-sm btn-default" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span>&emsp;<span translate>Edit</span></button>
</span>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-sm btn-default pull-right" ng-click="addRepo()"><span class="glyphicon glyphicon-plus"></span>&emsp;<span translate>Add Repository</span></button>
<div class="clearfix"></div>
</div>
<hr class="visible-sm"/>
</div>
<!-- Node list (top right) -->
<!-- This node -->
<div class="col-md-6">
<div class="panel-group" id="nodes">
<div class="panel panel-default" ng-repeat="nodeCfg in [thisNode()]">
<div class="panel-heading">
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#nodes" href="#node-this"><span class="glyphicon glyphicon-home"></span> {{nodeName(nodeCfg)}}</a>
</h3>
</div>
<div id="node-this" class="panel-collapse collapse in">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-th"></span>&emsp;<span translate>RAM Utilization</span></th>
<td class="text-right">{{system.sys | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tasks"></span>&emsp;<span translate>CPU Utilization</span></th>
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Download Rate</span></th>
<td class="text-right">{{connections['total'].inbps | metric}}bps ({{connections['total'].InBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-upload"></span>&emsp;<span translate>Upload Rate</span></th>
<td class="text-right">{{connections['total'].outbps | metric}}bps ({{connections['total'].OutBytesTotal | binary}}B)</td>
</tr>
<tr ng-if="system.extAnnounceOK != undefined">
<th><span class="glyphicon glyphicon-bullhorn"></span>&emsp;<span translate>Announce Server</span></th>
<td class="text-right">
<span class="data text-success" ng-if="system.extAnnounceOK"><span translate>Online</span></span>
<span class="data text-danger" ng-if="!system.extAnnounceOK"><span translate>Offline</span></span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Version</span></th>
<td class="text-right">{{version}}</td>
</tr>
</tbody>
</table>
</div>
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span>&emsp;<span translate>Edit</span></a></span>
</div>
<div class="panel panel-default" ng-repeat="nodeCfg in [thisNode()]">
<div class="panel-heading" data-toggle="collapse" href="#node-this" style="cursor: pointer">
<h3 class="panel-title">
<span class="glyphicon glyphicon-home"></span>&emsp;{{nodeName(nodeCfg)}}
</h3>
</div>
<div id="node-this" class="panel-collapse collapse in">
<div class="panel-body">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Download Rate</span></th>
<td class="text-right">{{connections['total'].inbps | metric}}bps ({{connections['total'].InBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-upload"></span>&emsp;<span translate>Upload Rate</span></th>
<td class="text-right">{{connections['total'].outbps | metric}}bps ({{connections['total'].OutBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-th"></span>&emsp;<span translate>RAM Utilization</span></th>
<td class="text-right">{{system.sys | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-dashboard"></span>&emsp;<span translate>CPU Utilization</span></th>
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
</tr>
<tr ng-if="system.extAnnounceOK != undefined">
<th><span class="glyphicon glyphicon-bullhorn"></span>&emsp;<span translate>Global Discovery Server</span></th>
<td class="text-right">
<span class="data text-success" ng-if="system.extAnnounceOK"><span translate>Online</span></span>
<span class="data text-danger" ng-if="!system.extAnnounceOK"><span translate>Offline</span></span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Version</span></th>
<td class="text-right">{{version}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Remote nodes -->
<div class="panel-group" id="nodes">
<div class="panel panel-{{nodeClass(nodeCfg)}}" ng-repeat="nodeCfg in otherNodes()">
<div class="panel-heading">
<div class="panel-heading" data-toggle="collapse" data-parent="#nodes" href="#node-{{$index}}" style="cursor: pointer">
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{$index}}">
<span class="glyphicon glyphicon-retweet"></span>
{{nodeName(nodeCfg)}}
<span class="pull-right hidden-xs">
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total == 100">
<span translate>Up to Date</span> (100%)
</span>
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total < 100">
<span translate>Syncing</span> ({{completion[nodeCfg.NodeID]._total | number:0}}%)
</span>
<span translate ng-if="!connections[nodeCfg.NodeID]">Disconnected</span>
<span class="glyphicon glyphicon-retweet"></span>&emsp;{{nodeName(nodeCfg)}}
<span class="pull-right hidden-xs">
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total == 100">
<span translate>Up to Date</span> (100%)
</span>
</a>
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total < 100">
<span translate>Syncing</span> ({{completion[nodeCfg.NodeID]._total | number:0}}%)
</span>
<span translate ng-if="!connections[nodeCfg.NodeID]">Disconnected</span>
</span>
</h3>
</div>
<div id="node-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-condensed table-striped">
<tbody>
<tr>
<th><span class="glyphicon glyphicon-link"></span>&emsp;<span translate>Address</span></th>
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-comment"></span>&emsp;<span translate>Synchronization</span></th>
<td class="text-right">{{completion[nodeCfg.NodeID]._total | alwaysNumber | number:0}}%</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-compressed"></span>&emsp;<span translate>Use Compression</span></th>
<td translate ng-if="nodeCfg.Compression" class="text-right">Yes</td>
<td translate ng-if="!nodeCfg.Compression" class="text-right">No</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Download Rate</span></th>
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps ({{connections[nodeCfg.NodeID].InBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-upload"></span>&emsp;<span translate>Upload Rate</span></th>
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps ({{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Version</span></th>
<td class="text-right">{{nodeVer(nodeCfg)}}</td>
</tr>
</tbody>
</table>
</div>
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span>&emsp;<span translate>Edit</span></a></span>
<table class="table table-condensed table-striped">
<tbody>
<tr ng-if="connections[nodeCfg.NodeID]">
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Download Rate</span></th>
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps ({{connections[nodeCfg.NodeID].InBytesTotal | binary}}B)</td>
</tr>
<tr ng-if="connections[nodeCfg.NodeID]">
<th><span class="glyphicon glyphicon-cloud-upload"></span>&emsp;<span translate>Upload Rate</span></th>
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps ({{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-link"></span>&emsp;<span translate>Address</span></th>
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
</tr>
<tr ng-if="connections[nodeCfg.NodeID]">
<th><span class="glyphicon glyphicon-comment"></span>&emsp;<span translate>Synchronization</span></th>
<td class="text-right">{{completion[nodeCfg.NodeID]._total | alwaysNumber | number:0}}%</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-compressed"></span>&emsp;<span translate>Use Compression</span></th>
<td translate ng-if="nodeCfg.Compression" class="text-right">Yes</td>
<td translate ng-if="!nodeCfg.Compression" class="text-right">No</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-thumbs-up"></span>&emsp;<span translate>Introducer</span></th>
<td translate ng-if="nodeCfg.Introducer" class="text-right">Yes</td>
<td translate ng-if="!nodeCfg.Introducer" class="text-right">No</td>
</tr>
<tr ng-if="connections[nodeCfg.NodeID]">
<th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Version</span></th>
<td class="text-right">{{connections[nodeCfg.NodeID].ClientVersion}}</td>
</tr>
<tr ng-if="!connections[nodeCfg.NodeID]">
<th><span class="glyphicon glyphicon-eye-open"></span>&emsp;<span translate>Last seen</span></th>
<td translate ng-if="!stats[nodeCfg.NodeID].LastSeenDays || stats[nodeCfg.NodeID].LastSeenDays >= 365" class="text-right">Never</td>
<td ng-if="stats[nodeCfg.NodeID].LastSeenDays < 365" class="text-right">{{stats[nodeCfg.NodeID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
</tr>
</tbody>
</table>
</div>
<div class="panel-footer">
<span class="pull-right"><a class="btn btn-sm btn-default" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span>&emsp;<span translate>Edit</span></a></span>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<div class="form-group">
<button class="btn btn-sm btn-default pull-right" ng-click="addNode()"><span class="glyphicon glyphicon-plus"></span>&emsp;<span translate>Add Node</span></button>
<div class="clearfix"></div>
</div>
</div>
</div> <!-- /row -->
@@ -443,13 +383,13 @@
</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>
<input id="name" class="form-control" type="text" ng-model="currentNode.Name"></input>
<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>
<input placeholder="dynamic" ng-disabled="currentNode.NodeID == myID" id="addresses" class="form-control" type="text" ng-model="currentNode.AddressesStr"></input>
<input ng-disabled="currentNode.NodeID == myID" id="addresses" class="form-control" type="text" ng-model="currentNode.AddressesStr"></input>
<p translate class="help-block">Enter comma separated "ip:port" addresses or "dynamic" to perform automatic discovery of the address.</p>
</div>
<div ng-if="!editingSelf" class="form-group">
@@ -457,14 +397,23 @@
<label>
<input type="checkbox" ng-model="currentNode.Compression"> <span translate>Use Compression</span>
</label>
<p translate class="help-block">Compression is recommended in most setups.</p>
</div>
</div>
<div ng-if="!editingSelf" class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentNode.Introducer"> <span translate>Introducer</span>
</label>
<p translate class="help-block">Any nodes configured on an introducer node will be added to this node as well.</p>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="saveNode()" ng-disabled="nodeEditor.$invalid"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
<button ng-if="editingExisting && !editingSelf" type="button" class="btn btn-danger pull-left" ng-click="deleteNode()"><span class="glyphicon glyphicon-minus"></span>&emsp;<span translate>Delete</span></button>
<button type="button" class="btn btn-primary btn-sm" ng-click="saveNode()" ng-disabled="nodeEditor.$invalid"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
<button ng-if="editingExisting && !editingSelf" type="button" class="btn btn-danger pull-left btn-sm" ng-click="deleteNode()"><span class="glyphicon glyphicon-minus"></span>&emsp;<span translate>Delete</span></button>
</div>
</div>
</div>
@@ -485,17 +434,17 @@
<div class="col-md-12">
<div class="form-group" ng-class="{'has-error': repoEditor.repoID.$invalid && repoEditor.repoID.$dirty}">
<label for="repoID"><span translate>Repository ID</span></label>
<input name="repoID" placeholder="documents" ng-disabled="editingExisting" id="repoID" class="form-control" type="text" ng-model="currentRepo.ID" required unique-repo ng-pattern="/^[a-zA-Z0-9-_.]{1,64}$/"></input>
<input name="repoID" ng-disabled="editingExisting" id="repoID" class="form-control" type="text" ng-model="currentRepo.ID" required unique-repo ng-pattern="/^[a-zA-Z0-9-_.]{1,64}$/"></input>
<p class="help-block">
<span translate ng-if="repoEditor.repoID.$valid || repoEditor.repoID.$pristine">Short identifier for the repository. Must be the same on all cluster nodes.</span>
<span translate ng-if="repoEditor.repoID.$error.uniqueRepo">The repository ID must be unique.</span>
<span translate ng-if="repoEditor.repoID.$error.required && repoEditor.repoID.$dirty">The repository ID cannot be blank.</span>
<span translate ng-if="repoEditor.repoID.$error.pattern && repoEditor.repoID.$dirty">The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.</span>
<span translate ng-if="repoEditor.repoID.$error.pattern && repoEditor.repoID.$dirty">The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.</span>
</p>
</div>
<div class="form-group" ng-class="{'has-error': repoEditor.repoPath.$invalid && repoEditor.repoPath.$dirty}">
<label translate for="repoPath">Repository Path</label>
<input name="repoPath" placeholder="~/Documents" id="repoPath" class="form-control" type="text" ng-model="currentRepo.Directory" required></input>
<input name="repoPath" ng-disabled="editingExisting" id="repoPath" class="form-control" type="text" ng-model="currentRepo.Directory" required></input>
<p class="help-block">
<span translate ng-if="repoEditor.repoPath.$valid || repoEditor.repoPath.$pristine">Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for</span> <code>{{system.tilde}}</code>.
<span translate ng-if="repoEditor.repoPath.$error.required && repoEditor.repoPath.$dirty">The repository path cannot be blank.</span>
@@ -503,7 +452,7 @@
</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>
<input name="rescanIntervalS" 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>
@@ -579,7 +528,7 @@
</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>
<input name="staggeredVersionsPath" 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>
@@ -588,9 +537,41 @@
<div translate ng-show="!editingExisting">When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="saveRepo()" ng-disabled="repoEditor.$invalid"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left" ng-click="deleteRepo()"><span class="glyphicon glyphicon-minus"></span>&emsp;<span translate>Delete</span></button>
<button type="button" class="btn btn-primary btn-sm" ng-click="saveRepo()" ng-disabled="repoEditor.$invalid"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left btn-sm" ng-click="deleteRepo()"><span class="glyphicon glyphicon-minus"></span>&emsp;<span translate>Delete</span></button>
<button id="editIgnoresButton" ng-if="editingExisting" type="button" class="btn btn-default pull-left btn-sm" ng-click="editIgnores()"><span class="glyphicon glyphicon-eye-close"></span>&emsp;<span translate>Ignore Patterns</span></button>
</div>
</div>
</div>
</div>
<!-- Ignores editor modal -->
<div id="editIgnores" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" translate>Ignore Patterns</h4>
</div>
<div class="modal-body">
<p translate>Enter ignore patterns, one per line.</p>
<textarea class="form-control" rows="15"></textarea>
<hr/>
<p class="small"><span translate>Quick guide to supported patterns</span> (<a href="https://discourse.syncthing.net/t/80" translate>full documentation</a>):</p>
<dl class="dl-horizontal dl-narrow small">
<dt><code>!</code></dt> <dd><span translate>Inversion of the given condition (i.e. do not exclude)</span></dd>
<dt><code>*</code></dt> <dd><span translate>Single level wildcard (matches within a directory only)</span></dd>
<dt><code>**</code></dt> <dd><span translate>Multi level wildcard (matches multiple directory levels)</span></dd>
<dt><code>//</code></dt> <dd><span translate>Comment, when used at the start of a line</span></dd>
</dl>
</div>
<div class="modal-footer">
<div class="pull-left"><span translate>Editing</span> <code>{{currentRepo.Directory}}/.stignore</code></div>
<button type="button" class="btn btn-primary btn-sm" data-dismiss="modal" ng-click="saveIgnores()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
</div>
</div>
</div>
@@ -609,48 +590,52 @@
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label translate for="NodeName">Node Name</label>
<input id="NodeName" class="form-control" type="text" ng-model="tmpOptions.NodeName">
</div>
<div class="form-group">
<label translate for="ListenStr">Sync Protocol Listen Addresses</label>
<input id="ListenStr" class="form-control" type="text" ng-model="tmpOptions.ListenStr">
</div>
<div class="form-group">
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
<input id="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.MaxRecvKbps">
</div>
<div class="form-group">
<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="ReconnectIntervalS">Reconnect Interval (s)</label>
<input id="ReconnectIntervalS" class="form-control" type="number" ng-model="tmpOptions.ReconnectIntervalS">
</div>
<div class="form-group">
<label translate for="ParallelRequests">Max Outstanding Requests</label>
<input id="ParallelRequests" class="form-control" type="number" ng-model="tmpOptions.ParallelRequests">
</div>
<div class="form-group">
<label translate for="MaxChangeKbps">Max File Change Rate (KiB/s)</label>
<input id="MaxChangeKbps" class="form-control" type="number" ng-model="tmpOptions.MaxChangeKbps">
</div>
-->
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Enable UPnP</span> <input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.UPnPEnabled">
</label>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Enable UPnP</span> <input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.UPnPEnabled">
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Global Discovery</span> <input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.GlobalAnnEnabled">
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Local Discovery</span> <input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.LocalAnnEnabled">
</label>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Automatic upgrades</span> <input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.AutoUpgradeEnabled">
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Global Discovery</span> <input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.GlobalAnnEnabled">
</label>
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Local Discovery</span> <input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.LocalAnnEnabled">
</label>
</div>
</div>
</div>
<div class="form-group">
@@ -675,7 +660,7 @@
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Use HTTPS for GUI</span> <input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS">
<span translate>Use HTTPS for GUI</span> <input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS">
</label>
</div>
</div>
@@ -689,7 +674,7 @@
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Anonymous Usage Reporting</span> <input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled">
<span translate>Anonymous Usage Reporting</span> <input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled"> (<a translate ng-click="showURPreview()" href="#">Preview</a>)
</label>
</div>
</div>
@@ -707,8 +692,8 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="saveSettings()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
</div>
</div>
</div>
@@ -723,14 +708,34 @@
<h4 translate class="modal-title">Allow Anonymous Usage Reporting?</h4>
</div>
<div class="modal-body">
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
<p translate translate-value-url="https://data.syncthing.net">The aggregated statistics are publicly available at {%url%}.</p>
<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>
<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 btn-sm" ng-show="!reportPreview" ng-click="showReportPreview()">Preview Usage Report</button>
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" ng-click="acceptUR()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Yes</span></button>
<button type="button" class="btn btn-danger" ng-click="declineUR()"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>No</span></button>
<button type="button" class="btn btn-success btn-sm" ng-click="acceptUR()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Yes</span></button>
<button type="button" class="btn btn-danger btn-sm" ng-click="declineUR()"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>No</span></button>
</div>
</div>
</div>
</div>
<!-- Usage report preview modal -->
<div id="urPreview" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header alert alert-success">
<h4 translate class="modal-title">Anonymous Usage Reporting</h4>
</div>
<div class="modal-body">
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
<p translate translate-value-url="https://data.syncthing.net">The aggregated statistics are publicly available at {%url%}.</p>
<pre><small>{{reportData | json}}</small></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>OK</span></button>
</div>
</div>
</div>
@@ -751,7 +756,7 @@
<!-- About modal -->
<modal id="about" large="yes" close="yes" status="info" title="About">
<h1 class="text-center"><img alt="Syncthing" title="Syncthing" src="logo-text-256.png" style="vertical-align: -16px" height="100" width="366"/><br/><small>{{version}}</small></h1>
<h1 class="text-center"><img alt="Syncthing" title="Syncthing" src="img/logo-text-256.png" style="vertical-align: -16px" height="100" width="366"/><br/><small>{{version}}</small></h1>
<hr/>
<p translate>Copyright &copy; 2014 Jakob Borg and the following Contributors:</p>
@@ -766,19 +771,21 @@
<li>Ben Sidhom</li>
<li>Brandon Philips</li>
<li>Gilli Sigurdsson</li>
<li>James Patterson</li>
</ul>
</div>
</div>
<div class="col-md-6">
<ul>
<li>James Patterson</li>
<li>Jens Diemer</li>
<li>Lode Hoste</li>
<li>Marcin Dziadus</li>
<li>Michael Tilli</li>
<li>Philippe Schommers</li>
<li>Ryan Sullivan</li>
<li>Tully Robinson</li>
<li>Veeti Paananen</li>
</ul>
</div>
</div>
</div>
<hr/>
@@ -797,12 +804,12 @@
</modal>
<script src="angular.min.js"></script>
<script src="angular-translate.min.js"></script>
<script src="angular-translate-loader.min.js"></script>
<script src="jquery-2.0.3.min.js"></script>
<script src="angular/angular.min.js"></script>
<script src="angular/angular-translate.min.js"></script>
<script src="angular/angular-translate-loader.min.js"></script>
<script src="jquery/jquery-2.0.3.min.js"></script>
<script src="bootstrap/js/bootstrap.min.js"></script>
<script src="valid-langs.js"></script>
<script src="lang/valid-langs.js"></script>
<script src="app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,7 @@
All files in this directory are auto generated. Do not change any of
them. To contribute translations, please head over to
https://www.transifex.com/projects/p/syncthing/
Any updates made on Transifex will be automatically pulled into these
files.

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Разреши анонимен доклад за ползване на програмата?",
"Announce Server": "Announce Server",
"Anonymous Usage Reporting": "Анонимен Доклад",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Бъгове",
"CPU Utilization": "Натоварване на Процесора",
"Close": "Затвори",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Грешка при Свързването",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Правата запазени © 2014 Jakob Borg и следните Сътрудници:",
"Delete": "Изтрий",
@@ -20,8 +23,10 @@
"Edit": "Промени",
"Edit Node": "Промени Машината",
"Edit Repository": "Промени Папката",
"Editing": "Editing",
"Enable UPnP": "Включи UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Въведи \"ip:port\" адреси разделени със запетая или \"dynamic\", за да извършиш автоматична връзка на адреси.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Грешка",
"File Versioning": "Файлови Версии",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Битовете за права за достъп са игнорирани, когато се проверява за промени. Използвай с файлови системи тип FAT.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Сървър за Глобално Откриване",
"Global Repository": "Глобална Папка",
"Idle": "Без Работа",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Игнорирай Права за Достъп",
"Incoming Rate Limit (KiB/s)": "Входящ Лимит на Скороста(KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Пази Версии",
"Last seen": "Последно видян",
"Latest Release": "Най-новата Версия",
"Local Discovery": "Локално Откриване",
"Local Discovery Port": "Порт за Локално Откриване",
@@ -45,7 +55,11 @@
"Master Repo": "Главна Папка",
"Max File Change Rate (KiB/s)": "Макс. Скорост на Промяна (KiB/s)",
"Max Outstanding Requests": "Макс. Неизпълени Заявки",
"Maximum Age": "Максимална Възраст",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Никога",
"No": "Не",
"No File Versioning": "Няма Файлови Версии",
"Node ID": "Код на Машината",
"Node Identification": "Идентификация на Машината",
"Node Name": "Име на Машината",
@@ -57,14 +71,18 @@
"Outgoing Rate Limit (KiB/s)": "Лимит на Изходящата Скорост (KiB/s)",
"Override Changes": "Замени Промените",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Пътят до папката на този компютър. Ще бъде създадена ако не съществува. Символът тилда (~) може да бъде използван като заместител на",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Пътят, където версиите да бъдат складирани(остави празно за папката .stversions).",
"Please wait": "Моля изчакай",
"Preview": "Preview",
"Preview Usage Report": "Разгледай Доклада за Използване",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "RAM Натоварване",
"Reconnect Interval (s)": "Интервал(и) на Свързване",
"Repository ID": "Идентификатор на Папката",
"Repository Master": "Главна Папка",
"Repository Path": "Път до Папката",
"Rescan": "Повторно Сканиране",
"Rescan Interval": "Интервал за Повторно Сканиране",
"Rescan Interval (s)": "Интеравал(и) на Сканиране",
"Restart": "Рестартирай",
"Restart Needed": "Изискава се Рестартиране",
@@ -81,7 +99,10 @@
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Покажи вмест ID-то на Компютъра в статус на клъстъра. Ще бъде предлагано на други комютри като име по подразбиране.",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Покажи вмест ID-то на Компютъра в статус на клъстъра. Ще бъде обновено с името по подразбиране изпратено от другия компютър.",
"Shutdown": "Спри Програмата",
"Simple File Versioning": "Просто Файлови Версии",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Сорс Код",
"Staggered File Versioning": "Наслагващи се Файлови Версии",
"Start Browser": "Стартирай Браузъра",
"Stopped": "Спряна",
"Support / Forum": "Помощ / Форум",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Криптираният доклад се изпраща дневно. Използва се, за да следи общи платформи, размери на папки и версии на приложението. Ако събираните данни се променят, ще бъдете информиран с подобен на този диалог.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Въведни код на машината не е валиден. Трябва да бъде 52 символа и да се състои от букви, цифри като интервалите и тиретата са пожелание.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Въведни код на машината не е валиден. Трябва да бъде 52 или 56 символа и да се състои от букви, цифри като интервалите и тиретата са пожелание.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Използва се следния интервал: за първия час се пази версия всеки 30 секунди, за първия ден се пази версия всеки час, за първите 30 дена се пази версия всеки ден, до максимума се пази една версия всяка седмица.",
"The maximum age must be a number and cannot be blank.": "Максималната възраст трябва да е число и не може д ае празна.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максималното време да се пазят весрсии (в дни, сложи 0, за да пазиш версии завинаги).",
"The node ID cannot be blank.": "Кодът на машината не може да бъде празен.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Кодът на машината, който си въвел може да бъде намерен в \"Промени > Покажи Идентификатора\". Интервалите и тиретата са пожелание(биват прескачани).",
"The number of old versions to keep, per file.": "Броят стари версии, които да бъдат пазени за всеки файл.",
"The number of versions must be a number and cannot be blank.": "Броят версии трябва да бъде число и не може да бъде празно.",
"The repository ID cannot be blank.": "Полето идентификатор на папка не може д абъде празно.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Идентификаторът на папка трябва да бъде къс(64 символа или по-малко) състоящ се само от букви, цифри, точка(.), тире(-) и подчерта (_).",
"The repository ID must be unique.": "Идентификаторът на папката тряба да бъде уникален.",
"The repository path cannot be blank.": "Пътят до папката не може да бъде празен.",
"The rescan interval must be at least 5 seconds.": "Интервала за повторно сканиране трябва да бъде поне 5 секунди.",
"Unknown": "Неясен",
"Up to Date": "Актуален",
"Upgrade To {%version%}": "Обновен До {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Използвай Компресиране",
"Use HTTPS for GUI": "Използвай HTTPS за Потребителския Интерфейс",
"Version": "Версия",
"Versions Path": "Път до Версиите",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версиите биват изтривани автоматично, когато са по-стари от максималната възраст или надминават броя файлове разрешени в даден интервал.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Когато добавяш нова машина помни, че твоята машина също трябва да бъде добавена от другата страна.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Когато добавяш нов идентификатор на папка помни, че той се използва за свързване на папките на различни машини. Главни/малки букви са от значение и трябва да са еднакви на всички машини.",
"Yes": "Да",
"You must keep at least one version.": "Трябва да пазиш поне една версия.",
"full documentation": "full documentation",
"items": "артикула"
}

152
gui/lang/lang-ca.json Normal file
View File

@@ -0,0 +1,152 @@
{
"API Key": "Clau API",
"About": "Sobre",
"Add Node": "Afegir Node",
"Add Repository": "Afegir Repositori",
"Address": "Adreça",
"Addresses": "Adreces",
"Allow Anonymous Usage Reporting?": "Permetre l'enviament anònim d'informes d'ús?",
"Announce Server": "Servidor d'anunciament",
"Anonymous Usage Reporting": "Informe anònim d'ús",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Bugs",
"CPU Utilization": "Utilització del CPU",
"Close": "Tancar",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Error de connexió",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg i els següents contribuïdors",
"Delete": "Esborrar",
"Disconnected": "Desconnectat",
"Documentation": "Documentació",
"Download Rate": "Tasa de descarrega",
"Edit": "Editar",
"Edit Node": "Editar Node",
"Edit Repository": "Editar Repositori",
"Editing": "Editing",
"Enable UPnP": "Habilitat UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduir, separat per comes, adreces \"ip:port\" o \"dynamic\" per descobrir automàticament les adreces.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Error",
"File Versioning": "Versionat de Fitxers",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Els bits de permisos dels fitxers son ignorats quan es cerquen canvis. Utilitzar en sistemes de fitxers FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Els fitxers es mouen amb l'estampat de la data a la carpeta .stversions quan son substituïts o esborrats per syncthing.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Els fitxers estan protegits de canvis fets per altres nodes, però els canvis fets en aquest node seran enviats a la resta del cluster.",
"Folder": "Carpeta",
"GUI Authentication Password": "Contrasenya d'autenticació GUI",
"GUI Authentication User": "Usuari d'autenticació GUI",
"GUI Listen Addresses": "Adreça d'escolta del GUI",
"Generate": "Generar",
"Global Discovery": "Descobriment Global",
"Global Discovery Server": "Servidor de Descobriment Global",
"Global Repository": "Repositori Global",
"Idle": "Inactiu",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Ignora Permisos",
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Mantenir Versions",
"Last seen": "Vist per última vegada",
"Latest Release": "Última publicació",
"Local Discovery": "Descobriment Local",
"Local Discovery Port": "Port de Descobriment Local",
"Local Repository": "Repositori Local",
"Master Repo": "Rep Master",
"Max File Change Rate (KiB/s)": "Tasa Màxima d'intercanvi de fitxer (KiB/s)",
"Max Outstanding Requests": "Màxim de Peticions Pendents",
"Maximum Age": "Antiguitat Màxima",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Mai",
"No": "No",
"No File Versioning": "Sense Versionat de Fitxer",
"Node ID": "ID del Node",
"Node Identification": "Identificació del Node",
"Node Name": "Nom Del Node",
"Notice": "Avís",
"OK": "OK",
"Offline": "Desconnectat",
"Online": "Connectat",
"Out Of Sync": "Fora de la Sincronització",
"Outgoing Rate Limit (KiB/s)": "Tasa Límit de Sortida (KiB/s)",
"Override Changes": "Sobreescriure Canvis",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta del repositori a l'equip local. Si no existeix serà creada. El caràcter (~) es pot fer servir com a drecera de",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Ruta on les versions s'haurien de guardar (deixa-ho buit per fer servir el directori .stversions per defecte al repositori)",
"Please wait": "Si-us-plau espera",
"Preview": "Preview",
"Preview Usage Report": "Vista Prèvia de l'Informe d'Ús",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "Utilització de la RAM",
"Reconnect Interval (s)": "Interval de Reconnexió (s)",
"Repository ID": "ID del Repositori",
"Repository Master": "Repositori Mestre",
"Repository Path": "Ruta del Repositori",
"Rescan": "Re-escanejar",
"Rescan Interval": "Interval de re-escaneig",
"Rescan Interval (s)": "Interval de re-escaneig (s)",
"Restart": "Reiniciar",
"Restart Needed": "És Necessari Reiniciar",
"Restarting": "Reiniciant",
"Save": "Guardar",
"Scanning": "Escanejant",
"Select the nodes to share this repository with.": "Seleccionar els nodes amb els que es comparteix el repositori.",
"Settings": "Preferències",
"Share With Nodes": "Compartir Amb Els Nodes",
"Shared With": "Compartir Amb",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identificador curt pel repositori. Ha de ser el mateix per tots els nodes del cluster.",
"Show ID": "Mostrar ID",
"Shown instead of Node ID in the cluster status.": "Mostrat en comptes del ID del Node en l'estat del cluster.",
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Mostrat en comptes del ID del Node en l'estat del cluster. Serà advertit als altres nodes com un nom opcional per defecte.",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Mostrat en comptes del ID del Node en l'estat del cluster. S'actualitzara al nom del node si es deixa buit.",
"Shutdown": "Apagar",
"Simple File Versioning": "Versionat de Fitxers Senzill",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Codi Font",
"Staggered File Versioning": "Versionat de Fitxers Esglaonat",
"Start Browser": "Arrancar Navegador",
"Stopped": "Aturat",
"Support / Forum": "Suport / Fòrum",
"Sync Protocol Listen Addresses": "Adreça d'escolta del Protocol Sync",
"Synchronization": "Sincronització",
"Syncing": "Synthing",
"Syncthing has been shut down.": "S'ha aturat el synthing.",
"Syncthing includes the following software or portions thereof:": "Syncthing inclou el següent programari o parts dels mateixos:",
"Syncthing is restarting.": "Reiniciant syncthing.",
"Syncthing is upgrading.": "Actualitzant syncthing.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Synthing sembla parat, o hi ha algun problema amb la connexió a Internet. Reintentant...",
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan públicament disponibles a {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració s'ha guardar però no s'ha activat. S'ha de reiniciar el synthing per activar la nova configuració.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "L'informe d'ús encriptat s'envia diàriament. Es fa servir per rastrejar plataformes habituals, mides de repositoris i versions de l'aplicació. Si es canvia el conjunt de dades reportades es demanarà amb aquest diàleg de nou.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "El ID del Node introduït no sembla vàlid. Hauria de tenir 52 caràcters amb lletres i números, els espais i les barres son opcionals.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "El ID del Node introduït no sembla vàlid. Hauria de tenir 52 o 56 caràcters amb lletres i números, els espais i les barres son opcionals.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Es fan servir els següents intervals: per la primera hora es manté una versió cada 30 segons, pel primer dia es manté una versió cada hora, pel primer cada 30 dies es manté una versió cada dia, fins el màxim d'antiguitat es manté una versió cada setmana.",
"The maximum age must be a number and cannot be blank.": "La màxima antiguitat ha de ser un número i no pot estar en blanc.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Temps màxim en mantenir una versió (en dies, si es deixa en 0 es mantenen les versions per sempre).",
"The node ID cannot be blank.": "El ID del node no pot estar en blanc.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "El ID del node per introduir aquí es pot trobar al diàleg \"Editar > Mostrar ID\" en l'altre node. Els espais i les barres son opcionals (s'ignoren).",
"The number of old versions to keep, per file.": "El nombre de versions antigues que es mantenen per fitxer.",
"The number of versions must be a number and cannot be blank.": "El nombre de versions ha de ser un número i no es pot deixar en blanc.",
"The repository ID cannot be blank.": "El ID del repositori no pot estar en blanc.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "El ID del repositori ha de ser un identificador curt (64 caràcters o menys) format només per lletres, nombres i el punt (.), barra (-) i barra baixa (_).",
"The repository ID must be unique.": "El ID del repositori ha de ser únic",
"The repository path cannot be blank.": "La carpeta del repositori no pot estar en blanc.",
"The rescan interval must be at least 5 seconds.": "El interval de re-escaneig ha de ser com a mínim de 5 segons.",
"Unknown": "Desconegut",
"Up to Date": "Actualitzat",
"Upgrade To {%version%}": "Actualitzar a {{version}}",
"Upgrading": "Actualitzant",
"Upload Rate": "Tasa de Pujada",
"Usage": "Ús",
"Use Compression": "Utilitza compressió",
"Use HTTPS for GUI": "Utilitzar HTTPS pel GUI",
"Version": "Versió",
"Versions Path": "Carpeta de les Versions",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions son automàticament eliminades si son més antigues que el màxim d'antiguitat o si excedeixen del nombre de fitxers permesos en un interval.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quan s'afegeix un nou node recorda que aquest node s'ha d'afegir tambe a l'altre banda.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Quan s'afegeix un nou repositori recorda que el ID del repositori s'utilitza per lligar repositoris entre nodes. Es distingeix entre majúscules i minúscules i ha de ser exactament iguals entre tots els nodes.",
"Yes": "Si",
"You must keep at least one version.": "Has de mantenir com a mínim una versió.",
"full documentation": "full documentation",
"items": "Elements"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Tillad anonym brugerstatistik?",
"Announce Server": "Opslagsserver",
"Anonymous Usage Reporting": "Anonym brugerstatistik",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Fejl",
"CPU Utilization": "CPU-forbrug",
"Close": "Luk",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Tilslutnings fejl",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg og følgende bidragsydere:",
"Delete": "Slet",
@@ -20,8 +23,10 @@
"Edit": "Rediger",
"Edit Node": "Rediger node",
"Edit Repository": "Rediger lager",
"Editing": "Editing",
"Enable UPnP": "Anvend UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv kommaseparerat \"ip:port\"-adresser eller ordet \"dynamic\" for at benytte automatisk opslag.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"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.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Global opslagsserver",
"Global Repository": "Global lagring",
"Idle": "Inaktiv",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Ignorér filrettigheder",
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Behold versioner",
"Last seen": "Last seen",
"Latest Release": "Seneste udgivelse",
"Local Discovery": "Lokal opslag",
"Local Discovery Port": "Lokal opslagsport",
@@ -45,7 +55,11 @@
"Master Repo": "Hovedlagring",
"Max File Change Rate (KiB/s)": "Højeste filændringshastighed (KiB/s)",
"Max Outstanding Requests": "Parallelitet",
"Maximum Age": "Maximum Age",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Never",
"No": "Nej",
"No File Versioning": "No File Versioning",
"Node ID": "Node ID",
"Node Identification": "Node identifikation",
"Node Name": "Nodenavn",
@@ -57,14 +71,18 @@
"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",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Path where versions should be stored (leave empty for the default .stversions folder in the repository).",
"Please wait": "Vent venligst",
"Preview": "Preview",
"Preview Usage Report": "Forhåndsvisning af forbrugsrapport",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"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": "Rescan Interval",
"Rescan Interval (s)": "Genscanningsinterval (s)",
"Restart": "Genstart",
"Restart Needed": "Programmet kræver genstart",
@@ -81,7 +99,10 @@
"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",
"Simple File Versioning": "Simple File Versioning",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Kildekode",
"Staggered File Versioning": "Staggered File Versioning",
"Start Browser": "Start browser",
"Stopped": "Stoppet",
"Support / Forum": "Support / Forum",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterede forbrugsrapport sendes dagligt. Den benyttes til at spore anvendte platforme, lagringsstørrelser og versioner. Hvis det typen af opsamlet data ændres på et senere tidspunkt, vil du blive spurgt om tilladelse igen.",
"The 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
"The node ID cannot be blank.": "Node-ID'et kan ikke være blankt.",
"The node ID 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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Lagrings-ID'et skal være en kort identificierende streng (64 karaktere eller mindre) bestående af bogstav-, tal-, punktum- (.), bindestreg- (-) og understregskaraktere (_).",
"The repository ID must be unique.": "Lagrings-ID'et skal være unikt.",
"The repository path cannot be blank.": "Lagringsstien kan ikke være blank.",
"The rescan interval must be at least 5 seconds.": "The rescan interval must be at least 5 seconds.",
"Unknown": "Ukendt",
"Up to Date": "Fuldt opdateret",
"Upgrade To {%version%}": "Opgradér til {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Anvend komprimering",
"Use HTTPS for GUI": "Anvend HTTPS til GUI adgang",
"Version": "Version",
"Versions Path": "Versions Path",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Når du tilføjer en ny node skal du huske, at den også skal tilføjes på den anden side.",
"When adding a new 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.",
"full documentation": "full documentation",
"items": "poster"
}

View File

@@ -8,20 +8,25 @@
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsstatistiken erlauben?",
"Announce Server": "Ankündigungs-Server",
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Fehler",
"CPU Utilization": "Prozessorauslastung",
"Close": "Schließen",
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Verbindungsfehler",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg und folgende Unterstützer:",
"Delete": "Löschen",
"Disconnected": "Verbindung getrennt",
"Documentation": "Dokumentation",
"Download Rate": "Downloadgeschwindigkeit",
"Edit": "Bearbeiten",
"Edit": "Einstellungen bearbeiten",
"Edit Node": "Knoten bearbeiten",
"Edit Repository": "Verzeichnis ändern",
"Editing": "Bearbeitung",
"Enable UPnP": "UPnP aktivieren",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Trage durch ein Komma getrennte \"IP:Port\" Adressen oder \"dynamic\" ein um automatische Adresserkennung durchzuführen.",
"Enter ignore patterns, one per line.": "Geben Sie Ignoriermuster ein, eines pro Zeile.",
"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.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Globaler Auffindungsserver",
"Global Repository": "Globales Verzeichnis",
"Idle": "Untätig",
"Ignore Patterns": "Ignoriermuster",
"Ignore Permissions": "Berechtigungen ignorieren",
"Incoming Rate Limit (KiB/s)": "Eingehendes Datenratelimit (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Umkehrung der angegebenen Bedingung (z.B. schließe nicht aus)",
"Keep Versions": "Versionen erhalten",
"Last seen": "Zuletzt online",
"Latest Release": "Letzte Veröffentlichung",
"Local Discovery": "Lokale Auffindung",
"Local Discovery Port": "Lokaler Aufindungsport",
@@ -45,7 +55,11 @@
"Master Repo": "Originalverzeichnis",
"Max File Change Rate (KiB/s)": "Maximale Datenänderungsrate (KiB/s)",
"Max Outstanding Requests": "Max. ausstehende Anfragen",
"Maximum Age": "Höchstalter",
"Multi level wildcard (matches multiple directory levels)": "Verschachteltes Maskenzeichen (wird für verschachtelte Verzeichnisse verwendet)",
"Never": "Nie",
"No": "Nein",
"No File Versioning": "Keine Dateiversionierung",
"Node ID": "Knoten-ID",
"Node Identification": "Knoten Identifikation",
"Node Name": "Knoten-Name",
@@ -57,14 +71,18 @@
"Outgoing Rate Limit (KiB/s)": "Ausgehendes Datenratelimit (KiB/s)",
"Override Changes": "Änderungen überschreiben",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Pfad zum Verzeichnis auf dem lokalen Rechner. Wird erzeugt, wenn es nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Pfad in dem die Versionen gespeichert werden sollen (ohne Angabe wird der Ordner .stversions im Verzeichnis verwendet).",
"Please wait": "Bitte warten",
"Preview": "Vorschau",
"Preview Usage Report": "Vorschau des Nutzungsberichts",
"Quick guide to supported patterns": "Schnellanleitung zu den unterstützten Suchstrukturen",
"RAM Utilization": "Verwendeter Arbeitsspeicher",
"Reconnect Interval (s)": "Wiederverbindungsintervall (s)",
"Repository ID": "Verzeichnis-ID",
"Repository Master": "Originalverzeichnis",
"Repository Path": "Pfad zum Verzeichnis",
"Rescan": "Erneut suchen",
"Rescan": "Überprüfen",
"Rescan Interval": "Suchintervall",
"Rescan Interval (s)": "Suchintervall (s)",
"Restart": "Neustart",
"Restart Needed": "Neustart notwendig",
@@ -81,7 +99,10 @@
"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",
"Simple File Versioning": "Einfache Dateiversionierung",
"Single level wildcard (matches within a directory only)": "Einzelnes Maskenzeichen (wird für ein einzelnes Verzeichnis verwendet)",
"Source Code": "Sourcecode",
"Staggered File Versioning": "Stufenweise Dateiversionierung",
"Start Browser": "Starte Browser",
"Stopped": "Gestoppt",
"Support / Forum": "Support / Forum",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Es wird in folgenden Abständen versioniert: in der ersten Stunde wird alle 30 Sekunden eine Version behalten, am ersten Tag eine jede Stunde, in den ersten 30 Tagen eine jeden Tag, danach wird bis zum Höchstalter eine Version pro Woche beibehalten.",
"The maximum age must be a number and cannot be blank.": "Das Höchstalter muss angegeben werden und eine Zahl sein.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Die längste Zeit, die alte Versionen vorgehalten werden (in Tagen, 0 bedeutet, alte Versionen für immer zu behalten).",
"The node ID cannot be blank.": "Die Knoten-ID darf nicht leer sein.",
"The node ID 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 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 dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis-ID muss eine kurze Kennung (64 Zeichen oder weniger) sein. Sie kann nur aus Buchstaben, Zahlen und den Punkt- (.), Strich- (-), und Unterstrich- (_) Zeichen bestehen.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis-ID muss eine kurze Kennung (64 Zeichen oder weniger) sein. Sie kann aus Buchstaben, Zahlen und den Punkt- (.), Strich- (-), und Unterstrich- (_) Zeichen bestehen.",
"The repository ID must be unique.": "Die Verzeichnis-ID muss eindeutig sein.",
"The repository path cannot be blank.": "Der Verzeichnis-Pfad kann nicht leer sein",
"The rescan interval must be at least 5 seconds.": "Das Suchintervall muss mindestens 5 Sekunden betragen.",
"Unknown": "Unbekannt",
"Up to Date": "Aktuell",
"Upgrade To {%version%}": "Upgrade auf {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Benutze Komprimierung",
"Use HTTPS for GUI": "Benutze HTTPS für Benutzeroberfläche",
"Version": "Version",
"Versions Path": "Versionierungspfad",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Alte Versionen werden automatisch gelöscht wenn sie älter als das angegebene Höchstalter sind oder die Höchstzahl der Dateien pro Zeitabschnitt überschritten wird.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Knotens, dass dieser Knoten auch auf der Gegenseite hinzugefügt werden muss.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Beim Hinzufügen eines neuen Verzeichnisses, beachte dass die Verzeichnis-ID dazu verwendet wird, Verzeichnisse zwischen Knoten zu verbinden. Die ID muss also auf allen Knoten gleich sein, Groß- und Kleinschreibung muss dabei beachtet werden.",
"Yes": "Ja",
"You must keep at least one version.": "Du musst mindestens eine Version behalten.",
"full documentation": "Komplette Dokumentation",
"items": "Einträge"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Να επιτρέπεται Ανώνυμη Αποστολή Αναφοράς Χρήσης?",
"Announce Server": "Διακομιστής Αναγγελίας",
"Anonymous Usage Reporting": "Ανώνυμη Αναφορά Χρήσης",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Bugs",
"CPU Utilization": "Χρήση CPU",
"Close": "Τέλος",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Σφάλμα Σύνδεσης",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg και οι παρακάτω Συνεισφορείς:",
"Delete": "Διαγραφή",
@@ -20,8 +23,10 @@
"Edit": "Επεξεργασία",
"Edit Node": "Επεξεργασία Κόμβου",
"Edit Repository": "Επεξεργασία Αποθετηρίου",
"Editing": "Editing",
"Enable UPnP": "Ενεργοποίηση UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Εισήγαγε διαχωρισμένα με κόμμα \"ip:port\" διευθύνσεις ή \"dynamic\", για να πραγματοποιηθεί η αυτόματη εξεύρεση διευθύνσεων.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Σφάλμα",
"File Versioning": "File Versioning",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Τα permission bits των αρχείων αγνοούνται όταν γίνεται αναζήτηση για αλλαγές. Χρήση σε FAT filesystems.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Διακομιστής Ανεύρεσης Κόμβου",
"Global Repository": "Global Repository",
"Idle": "Ανενεργό",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Αγνόησε Δικαιώματα",
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Διατήρησε Εκδόσεις",
"Last seen": "Last seen",
"Latest Release": "Τελευταία Έκδοση",
"Local Discovery": "Τοπική Ανεύρεση",
"Local Discovery Port": "Port Τοπικής Ανεύρεσης",
@@ -45,7 +55,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",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Never",
"No": "Αριθμός",
"No File Versioning": "No File Versioning",
"Node ID": "ID Κόμβου",
"Node Identification": "Ταυτοποίηση Κόμβου",
"Node Name": "Όνομα Κόμβου",
@@ -57,14 +71,18 @@
"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 (~) μπορεί να χρησιμοποιηθεί σαν συντόμευση για",
"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": "Παρακαλώ περιμένετε",
"Preview": "Preview",
"Preview Usage Report": "Προεπισκόπηση αναφοράς χρήσης",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "Χρήση RAM",
"Reconnect Interval (s)": "Χρονικό διάστημα επανασύνδεσης (s)",
"Repository ID": "ID Αποθετηρίου",
"Repository Master": "Repository Master",
"Repository Path": "Μονοπάτι Αποθετηρίου",
"Rescan": "Rescan",
"Rescan Interval": "Rescan Interval",
"Rescan Interval (s)": "Χρονικό διάστημα Επανασάρρωσης (s)",
"Restart": "Επανεκκίνηση",
"Restart Needed": "Απαιτείται Επανεκκίνηση",
@@ -81,7 +99,10 @@
"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": "Απενεργοποίηση",
"Simple File Versioning": "Simple File Versioning",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Πηγαίος Κώδικας",
"Staggered File Versioning": "Staggered File Versioning",
"Start Browser": "Έναρξη Φυλλομετρητή",
"Stopped": "Απενεργοποιημένο",
"Support / Forum": "Υποστήριξη / Forum",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Η κρυπτογραφημένη αναφοράς χρήσης στέλνεται καθημερινά. Χρησιμοποιείται ανίχνευση πληροφοριών πλατφόρμας, μεγέθους αποθετηρίων και εκδόσεων της εφαρμογής. Αν τα δεδομένα που αποστέλονται αλλάξουν, θα πληροφορηθείτε ξανά με αυτό το διάλογο.",
"The 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
"The node ID cannot be blank.": "Το ID Κόμβου δε μπορεί να είναι κενό.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Το ID Κόμβου μπορείτε να βρείτε στο μενού \"Επεξεργασία > Εμφάνιση ID\" του άλλου κόμβου. Κενά και παύλες είναι προαιρετικά (αγνοούνται).",
"The 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 repository ID cannot be blank.": "Το ID Αποθετηρίου δε μπορεί να είναι κενό.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be unique.": "Το ID Αποθετηρίου πρέπει να είναι μοναδικό.",
"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": "Άγνωστο",
"Up to Date": "Ενημερωμένος",
"Upgrade To {%version%}": "Αναβάθμιση στην έκδοση {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Χρήση συμπίεσης",
"Use HTTPS for GUI": "Χρήση HTTPS για το GUI",
"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 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.",
"full documentation": "full documentation",
"items": "items"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Allow Anonymous Usage Reporting?",
"Announce Server": "Announce Server",
"Anonymous Usage Reporting": "Anonymous Usage Reporting",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Bugs",
"CPU Utilization": "CPU Utilization",
"Close": "Close",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Connection Error",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
"Delete": "Delete",
@@ -20,8 +23,10 @@
"Edit": "Edit",
"Edit Node": "Edit Node",
"Edit Repository": "Edit Repository",
"Editing": "Editing",
"Enable UPnP": "Enable UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Error",
"File Versioning": "File Versioning",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "File permission bits are ignored when looking for changes. Use on FAT filesystems.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Global Discovery Server",
"Global Repository": "Global Repository",
"Idle": "Idle",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Ignore Permissions",
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Keep Versions",
"Last seen": "Last seen",
"Latest Release": "Latest Release",
"Local Discovery": "Local Discovery",
"Local Discovery Port": "Local Discovery Port",
@@ -46,6 +56,8 @@
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
"Max Outstanding Requests": "Max Outstanding Requests",
"Maximum Age": "Maximum Age",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Never",
"No": "No",
"No File Versioning": "No File Versioning",
"Node ID": "Node ID",
@@ -61,7 +73,9 @@
"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": "Preview",
"Preview Usage Report": "Preview Usage Report",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "RAM Utilization",
"Reconnect Interval (s)": "Reconnect Interval (s)",
"Repository ID": "Repository ID",
@@ -86,6 +100,7 @@
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.",
"Shutdown": "Shutdown",
"Simple File Versioning": "Simple File Versioning",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Source Code",
"Staggered File Versioning": "Staggered File Versioning",
"Start Browser": "Start Browser",
@@ -112,6 +127,7 @@
"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 repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be unique.": "The repository ID must be unique.",
"The repository path cannot be blank.": "The repository path cannot be blank.",
@@ -131,5 +147,6 @@
"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",
"You must keep at least one version.": "You must keep at least one version.",
"full documentation": "full documentation",
"items": "items"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Permitir reporte anónimo de uso?",
"Announce Server": "Anunciar servidor",
"Anonymous Usage Reporting": "Reporte anónimo de uso",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Errores",
"CPU Utilization": "Uso de la CPU",
"Close": "Cerrar",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Error de conexión",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Derechos de autor © 2014 Jakob Borg y los siguientes colaboradores:",
"Delete": "Suprimir",
@@ -20,8 +23,10 @@
"Edit": "Editar",
"Edit Node": "Editar nodo",
"Edit Repository": "Editar repositorio",
"Editing": "Editing",
"Enable UPnP": "Permitir UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Ingrese las direcciones \"ip:port\" separadas por coma o \"dynamic\" para descubrir automáticamente las direcciones.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Error",
"File Versioning": "Control de versiones",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Los permisos de archivo son ignorados al buscar cambios. Usar en sistemas de archivos FAT.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Servidor global de identificación",
"Global Repository": "Repositorio global",
"Idle": "Inactivo",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Ignorar permisos",
"Incoming Rate Limit (KiB/s)": "Límite de velocidad de entrada (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Conservar versiones",
"Last seen": "Visto por ultima vez",
"Latest Release": "Última versión",
"Local Discovery": "Búsqueda en red local",
"Local Discovery Port": "Puerto de búsqueda de red local",
@@ -45,7 +55,11 @@
"Master Repo": "Repositorio maestro",
"Max File Change Rate (KiB/s)": "Tasa máxima de cambios (KiB/s)",
"Max Outstanding Requests": "Cantidad máxima de peticiones pendientes",
"Maximum Age": "Edad máxima",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Nunca",
"No": "No",
"No File Versioning": "Sin control de versiones de archivos",
"Node ID": "Nodo ID",
"Node Identification": "Identificador del nodo",
"Node Name": "Nodo nombre",
@@ -57,14 +71,18 @@
"Outgoing Rate Limit (KiB/s)": "Tasa máxima de envío (KiB/s)",
"Override Changes": "Reemplazar los cambios",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta del repositorio en el equipo local. La carpeta sera creada si no existe. El carácter tilde (~) puede ser utilizado como atajo de ",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Ruta donde se deben almacenar versiones (dejar vacío para la carpeta .stversions predeterminada en el repositorio).",
"Please wait": "Aguarde por favor",
"Preview": "Vista previa",
"Preview Usage Report": "Ver reporte de uso",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "Utilización de RAM",
"Reconnect Interval (s)": "Intervalo de reconexión (s)",
"Repository ID": "ID de repositorio",
"Repository Master": "Repositorio maestro",
"Repository Path": "Ruta del repositorio",
"Rescan": "Rescan",
"Rescan Interval": "Rescan Interval",
"Rescan Interval (s)": "Intervalo de reescaneo (s)",
"Restart": "Reiniciar",
"Restart Needed": "Es necesario reiniciar",
@@ -81,7 +99,10 @@
"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",
"Simple File Versioning": "Versiones simple de archivos",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Código fuente",
"Staggered File Versioning": "Versiones del archivo escalonado",
"Start Browser": "Iniciar navegador",
"Stopped": "Parado",
"Support / Forum": "Soporte / Foro",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Los siguientes intervalos se utilizan: para la primera hora una versión se mantiene cada 30 segundos, para el primer día de una versión se mantiene cada hora, durante los primeros 30 días de la versión se mantiene todos los días, hasta que la edad máxima de una versión se mantiene cada semana.",
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar en blanco.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El tiempo máximo para mantener una versión (en días, establece en 0 para mantener versiones para siempre).",
"The node ID cannot be blank.": "El ID de nodo no puede estar vacío.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "El ID de nodo a ingresar aquí puede verse en la opción de menú \"Edición > Mostrar ID\" del otro nodo. Espacios y guiones son opcionales (ignorados).",
"The number of old versions to keep, per file.": "El numero de versiones anteriores a conservar, por archivo.",
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
"The repository ID cannot be blank.": "El ID de repositorio no puede estar vacio.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "El ID de repositorio debe ser una cadena corta (64 caracteres o menos) consistente solamente en letras, números, punto (.), guion (-) y guion bajo (_).",
"The repository ID must be unique.": "El ID de repositorio debe ser único.",
"The repository path cannot be blank.": "La ruta del repositorio no puede estar vacía.",
"The rescan interval must be at least 5 seconds.": "The rescan interval must be at least 5 seconds.",
"Unknown": "Desconocido",
"Up to Date": "Actualizado",
"Upgrade To {%version%}": "Actualizar a {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Usar compresión",
"Use HTTPS for GUI": "Usar HTTPS para la GUI",
"Version": "Versión",
"Versions Path": "Ruta de versiones",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Las versiones se eliminan automáticamente si son mayores de la edad máxima o mayor que el número de archivos permitidos en un intervalo.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Al agregar un nuevo nodo, recuerde que este nodo debe ser agregado en el otro lado también.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Al agregar un nuevo repositorio, tenga en mente que el ID de repositorio se utiliza para ligar los repositorios entre nodos. Distingue mayúsculas y minúsculas y debe ser exactamente igual en todos los nodos.",
"Yes": "Si",
"You must keep at least one version.": "Debe mantener al menos una versión",
"items": "items"
"full documentation": "full documentation",
"items": "Articulos"
}

View File

@@ -8,9 +8,12 @@
"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",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Bugs",
"CPU Utilization": "Utilisation du CPU",
"Close": "Fermer",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Erreur de connexion",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants :",
"Delete": "Supprimer",
@@ -20,8 +23,10 @@
"Edit": "Éditer",
"Edit Node": "Éditer le nœud",
"Edit Repository": "Éditer le répertoire",
"Editing": "Editing",
"Enable UPnP": "Activer l'UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses \"ip:port\" séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"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 de type FAT.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Serveur global de recherche",
"Global Repository": "Répertoire global",
"Idle": "Au repos",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Ignorer les permissions",
"Incoming Rate Limit (KiB/s)": "Limite du débit entrant (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Conserver les versions",
"Last seen": "Dernière apparition",
"Latest Release": "Dernière version",
"Local Discovery": "Recherche locale",
"Local Discovery Port": "Port de recherche locale",
@@ -45,7 +55,11 @@
"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 concurrentes de blocs de fichier",
"Maximum Age": "Ancienneté maximum",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Jamais",
"No": "Non",
"No File Versioning": "Pas de version de fichier",
"Node ID": "ID du nœud",
"Node Identification": "Identification du nœud",
"Node Name": "Nom du nœud",
@@ -57,14 +71,18 @@
"Outgoing Rate Limit (KiB/s)": "Limite du débit sortant (KiB/s)",
"Override Changes": "Écraser les changements",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Chemin du répertoire sur l'ordinateur local. Il sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
"Please wait": "Merci de patienter",
"Preview": "Aperçu",
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "Utilisation de la RAM",
"Reconnect Interval (s)": "Intervalle de reconnexion (s)",
"Repository ID": "ID du répertoire",
"Repository Master": "Répertoire maître",
"Repository Path": "Chemin du répertoire",
"Rescan": "Rescan",
"Rescan": "Rescanner",
"Rescan Interval": "Intervalle de scan",
"Rescan Interval (s)": "Intervalle de rescan (s)",
"Restart": "Redémarrer",
"Restart Needed": "Redémarrage nécessaire",
@@ -78,10 +96,13 @@
"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.",
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Affiché à la place de l'ID du nœud dans le statut du cluster. Sera annoncé aux autres nœuds comme un nom par défaut optionnel.",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Affiché à la place de l'ID du nœud dans le statut du cluster. Sera mis à jour par le nom que le nœud annonce si laissé vide.",
"Shutdown": "Éteindre",
"Simple File Versioning": "Versions simples de fichier",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Code source",
"Staggered File Versioning": "Versions échelonnées de fichier",
"Start Browser": "Démarrer le navigateur web",
"Stopped": "Arrêté",
"Support / Forum": "Aide / Forum",
@@ -89,7 +110,7 @@
"Synchronization": "Synchronisation",
"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 includes the following software or portions thereof:": "Syncthing intègre les logiciels suivants (ou des éléments provenant de ces logiciels) :",
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
"Syncthing is 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 ...",
@@ -98,26 +119,34 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des répertoires et les versions de l'application. Si le jeu de données rapportées devait être changé, il vous sera demandé de le valider de nouveau via ce dialogue.",
"The 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Les intervalles suivant sont utilisés: la première heure une version est conservée chaque 30 secondes, le premier jour une version est conservée chaque heure, les premiers 30 jours une version est conservée chaque jour, jusqu'à la limite d'âge maximum une version est conservée chaque semaine.",
"The maximum age must be a number and cannot be blank.": "L'ancienneté maximum doit être un nombre et ne peut être vide.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Le temps maximum de conservation d'une version (en jours, mettre à 0 pour conserver les versions pour toujours)",
"The node ID cannot be blank.": "L'ID du nœud ne peut être vide.",
"The node ID 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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "L'ID du répertoire doit être un identifiant court (64 caractères ou moins) comprenant des lettres, nombres, points (.), trait d'union (-) et tiret bas (_).",
"The repository ID must be unique.": "L'ID du répertoire doit être unique.",
"The repository path cannot be blank.": "Le chemin du répertoire ne peut pas être vide.",
"The rescan interval must be at least 5 seconds.": "L'intervalle de scan doit être d'au minimum 5 secondes.",
"Unknown": "Inconnu",
"Up to Date": "Synchronisation à jour",
"Upgrade To {%version%}": "Upgrader vers {{version}}",
"Upgrade To {%version%}": "Mettre à jour 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",
"Versions Path": "Emplacement des versions",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions sont supprimées automatiquement si celles-ci sont plus anciennes que l'ancienneté maximum ou que leur nombre est supérieur au nombre autorisé dans une intervale.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Lorsqu'un nœud est ajouté, gardez à l'esprit que ce nœud doit aussi être ajouté de l'autre coté.",
"When adding a new 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.",
"full documentation": "full documentation",
"items": "éléments"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Engedélyezed a névtelen felhasználási statisztikai adatok küldését?",
"Announce Server": "Cím hirdető szerver",
"Anonymous Usage Reporting": "Névtelen felhasználási statisztikák küldése",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Hibák",
"CPU Utilization": "Processzor használat",
"Close": "Bezárás",
"Comment, when used at the start of a line": "Megjegyzés, a sor elején használva",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Kapcsolódási hiba",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg és a következő Közreműködők",
"Delete": "Törlés",
@@ -20,8 +23,10 @@
"Edit": "Szerkesztés",
"Edit Node": "Csomópont szerkesztése",
"Edit Repository": "Tároló szerkesztése",
"Editing": "Szerkesztés",
"Enable UPnP": "UPnP engedélyezése",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Add meg kettősponttal elválasztva \"ip:port\" a címet vagy add meg a \"dynamic\" szót az a cím automatikus észleléséhez.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Hiba",
"File Versioning": "File verziózás",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "A file jogosultásgok figyelmen kívül hagyása. FAT file-rendszernél haszálatos.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Globális csomópont kereséshez használt szerver",
"Global Repository": "Globális tároló",
"Idle": "Tétlen",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Jogosultságok figyelmen kívül hagyása",
"Incoming Rate Limit (KiB/s)": "Bejövő sebesség korlát (KIB/mp)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Verziók megtartása",
"Last seen": "Utoljára látva",
"Latest Release": "Utolsó kiadás",
"Local Discovery": "Helyi csomópont keresés",
"Local Discovery Port": "Helyi csomópont keresés port-ja",
@@ -45,7 +55,11 @@
"Master Repo": "Központi tároló",
"Max File Change Rate (KiB/s)": "Maximális file változás sebessége (KiB/mp)",
"Max Outstanding Requests": "Maximális kimenő kérés",
"Maximum Age": "Maximális kor",
"Multi level wildcard (matches multiple directory levels)": "Több szintű joker (több könyvtár szintre érvényesül)",
"Never": "Soha",
"No": "Nem",
"No File Versioning": "Nincs file verziózás",
"Node ID": "Csomópont azonosító",
"Node Identification": "Csomópont azonosítás",
"Node Name": "Csomópont név",
@@ -57,14 +71,18 @@
"Outgoing Rate Limit (KiB/s)": "Kimenő sávszélesség (KiB/mp)",
"Override Changes": "Változtatások felülbírálása",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "A tároló útvonala ezen a számítógépen. Amennyiben nem létezik automatikusan létrehozzuk. A hullámvonal (~) karakter használható a rövidítésre",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Az útvonal ahol a régi verziókat tároljuk (ha üresen hagyod akkor .stversions mappa lesz a neve).",
"Please wait": "Kérem várj",
"Preview": "Előnézet",
"Preview Usage Report": "Felhasználási adatok átnézése",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "Memória használat",
"Reconnect Interval (s)": "Újracsatlakozási intervallum (mp)",
"Repository ID": "Tároló azonosító",
"Repository Master": "Központi tároló",
"Repository Path": "Tároló útvonala",
"Rescan": "Újraátvizsgálás",
"Rescan Interval": "Átnézési intervallum",
"Rescan Interval (s)": "Átnézési intervallum (mp)",
"Restart": "Újraindítás",
"Restart Needed": "Újraindítás szükséges",
@@ -81,7 +99,10 @@
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "A csomópont azonosító helyett jelenik meg a fürtben a státusznál. A csomópontoknak ez is hirdetve lesz, mint opcionális alapértelmezett név.",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "A csomópont azonosító helyett jelenik meg a fürtben a státusznál. A csomópont neve a hirdetettre lesz llítva amennyiben az üresen van hagyva.",
"Shutdown": "Leállítás",
"Simple File Versioning": "Egyszerű file verziózás",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Forráskód",
"Staggered File Versioning": "Többszintű file verziózás",
"Start Browser": "Böngésző indítása",
"Stopped": "Leállítva",
"Support / Forum": "Támogatás / Fórum",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "A titkosított felhasználási jelentés naponta kerül elküldésre. Arra használjuk, hogy a futtató platformot, tároló méreteket illetve program verziókat kövessük nyomon. Amennyiben ez változik akkor újra meg fog jelenni ez az ablak.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "A beírt csomópont azonosító nem tűnik megfelelőnek. 52 karakter hosszúnak kell lennie és csak számokat illetve betűket tartalmazhat, amit szóközök illetve kötőjelek tagolhatnak.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "A beírt csomópont azonosító nem tűnik megfelelőnek. 52 vagy 56 karakteresnek kell lennie csak számot és betűt kell tartalmaznia és szóközökkel vagy kötőjelekkel lehet tagolva.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "A következő intervallumokat használjuk: egy-egy régi verziót őrzünk meg az első órában minden 30 másodpercben, az első nap minden órában, az első 30 napban minden nap, egészen addig amíg el nem érjük a maximálisan megtartható verziók számát minden héten.",
"The maximum age must be a number and cannot be blank.": "A maximális kornak számnak kell lenni és nem lehet üres",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "A verziók megtartásának maximális ideje (napokban, ha 0-t adsz meg örökre megmaradnak).",
"The node ID cannot be blank.": "A csomópont azonosító nem lehet üres",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "A csomópont azonosító a túloldalon a \"Beállítások > Azonosító mutatása\" alatt található. A szóközök illetve a kötőjelek opcionálisak (kihagyhatóak). ",
"The number of old versions to keep, per file.": "Mennyi régi változatot tartsunk meg a file-okból",
"The number of versions must be a number and cannot be blank.": "A megtartott változatok száma nem lehet üres",
"The repository ID cannot be blank.": "A tároló azonosító nem lehet üres",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "A tároló azonosító egy rövid (maximálisan 64 karakteres) csak számokat, betűket, pontot (.), kötőjelet (-) illetve aláhúzást (_) tartalmazó karakterlánc.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "A tároló azonosító egy rövid (maximálisan 64 karakteres) csak számokat, betűket, pontot (.), kötőjelet (-) illetve aláhúzást (_) tartalmazó karakterlánc",
"The repository ID must be unique.": "A tároló azonosítónak egyedinek kell lennie",
"The repository path cannot be blank.": "A tároló útvonala nem lehet üres",
"The rescan interval must be at least 5 seconds.": "Az átnézési intervallumbak legalább 5 másodpercnek kell lennie.",
"Unknown": "Ismeretlen",
"Up to Date": "Friss",
"Upgrade To {%version%}": "Frissítés a {{version}} verzióra",
@@ -115,9 +141,12 @@
"Use Compression": "Tömörítés használata",
"Use HTTPS for GUI": "HTTPS használata a GUI-hoz",
"Version": "Verzió",
"Versions Path": "Verziók útvonala",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "A régi verziók automatikusan törlődnek, amennyiben öregebbek mint a maximum kor, vagy már több van belőlük mint az adott intervallumban megtartható maximum.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Amikor új csomópontot adsz hozzá, tartsd észben, hogy azt a túloldalhoz is hozzá kell majd adni.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Amikor hozzáadod a tárolót, tartsad észben, hogy a Tároló azonosító az ami összeköti a csomópontokat. Kis-nagybetű érzékeny, és pontosan ugyan úgy kell azokat megadni mindegyik csomóponton.",
"Yes": "Igen",
"You must keep at least one version.": "Legalább egy verziót meg kell tartanod",
"full documentation": "teljes dokumentáció",
"items": "tételek"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
"Announce Server": "Server di Presenza Globale dei Nodi",
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Bug",
"CPU Utilization": "Utilizzo CPU",
"Close": "Chiudi",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Errore di Connessione",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg e i seguenti Collaboratori:",
"Delete": "Elimina",
@@ -20,8 +23,10 @@
"Edit": "Modifica",
"Edit Node": "Modifica Nodo",
"Edit Repository": "Modifica Deposito",
"Editing": "Editing",
"Enable UPnP": "Attiva UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserisci gli indirizzi \"ip:porta\" separati da una virgola, altrimenti inserisci \"dynamic\" per effettuare il rilevamento automatico dell'indirizzo.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Errore",
"File Versioning": "Controllo Versione dei File",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Il software evita i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Server di Ricerca Globale",
"Global Repository": "Deposito Globale",
"Idle": "Inattivo",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Ignora Permessi",
"Keep Versions": "Mantieni le Versioni",
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Versioni Mantenute",
"Last seen": "Last seen",
"Latest Release": "Ultima Versione",
"Local Discovery": "Individuazione Locale",
"Local Discovery Port": "Porta di Individuazione Locale",
@@ -45,7 +55,11 @@
"Master Repo": "Deposito Principale",
"Max File Change Rate (KiB/s)": "Tasso Massimo di Cambiamento dei File (KiB/s)",
"Max Outstanding Requests": "Numero Massimo di Richieste Simultanee per i Blocchi di File",
"Maximum Age": "Durata Massima",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Never",
"No": "No",
"No File Versioning": "Nessun Controllo Versione",
"Node ID": "ID Nodo",
"Node Identification": "Identificazione Nodo",
"Node Name": "Nome Nodo",
@@ -57,14 +71,18 @@
"Outgoing Rate Limit (KiB/s)": "Limite di Velocità in Uscita (KiB/s)",
"Override Changes": "Ignora Modifiche",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Percorso del deposito nel computer locale. Verrà creato se non esiste già. Il carattere tilde (~) può essere utilizzato come scorciatoia per",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Percorso di salvataggio delle versioni (lasciare vuoto per utilizzare la cartella predefinita .stversions nel deposito).",
"Please wait": "Attendere prego",
"Preview": "Preview",
"Preview Usage Report": "Anteprima Statistiche di Utilizzo",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"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": "Riscansiona",
"Rescan Interval": "Intervallo di Scansione",
"Rescan Interval (s)": "Intervallo di Scansione dei File (s)",
"Restart": "Riavvia",
"Restart Needed": "Riavvio Necessario",
@@ -81,7 +99,10 @@
"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",
"Simple File Versioning": "Controllo Versione Semplice",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Codice Sorgente",
"Staggered File Versioning": "Controllo Versione Cadenzato",
"Start Browser": "Avvia Browser",
"Stopped": "Fermato",
"Support / Forum": "Supporto / Forum",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
"The maximum age must be a number and cannot be blank.": "La durata massima dev'essere un numero e non può essere vuoto.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La durata massima di una versione (in giorni, imposta a 0 per mantenere le versioni per sempre).",
"The node ID cannot be blank.": "L'ID del nodo non può essere vuoto.",
"The node ID 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.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "L'ID del deposito dev'essere un identificatore breve (64 caratteri o meno) costituito solamente da lettere, numeri, punti (.), trattini (-) e trattini bassi (_).",
"The repository ID must be unique.": "L'ID del deposito dev'essere unico.",
"The repository path cannot be blank.": "Il percorso del deposito non può essere vuoto.",
"The rescan interval must be at least 5 seconds.": "L'intervallo di scansione non può essere inferiore a 5 secondi.",
"Unknown": "Sconosciuto",
"Up to Date": "Sincronizzato",
"Upgrade To {%version%}": "Aggiorna Alla {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Utilizza Compressione",
"Use HTTPS for GUI": "Utilizza HTTPS per l'interfaccia grafica",
"Version": "Versione",
"Versions Path": "Percorso Cartella delle Versioni",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Le versioni vengono eliminate automaticamente se superano la durata massima o il numero di file permessi in un determinato intervallo temporale.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Ora la stessa operazione deve essere eseguita anche nel nuovo nodo inserendo l'ID di questo nodo.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Quando aggiungi un nuovo deposito ricordati che gli ID vengono utilizzati per collegare i depositi nei nodi. Distinguono maiuscole e minuscole e devono corrispondere esattamente su tutti i nodi.",
"Yes": "Sì",
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
"full documentation": "full documentation",
"items": "elementi"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Siųsti anonimišką vartojimo ataskaitą?",
"Announce Server": "Aptikimų serveris",
"Anonymous Usage Reporting": "Anoniminė vartojimo ataskaita",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Klaidos",
"CPU Utilization": "CAP sunaudojimas",
"Close": "Uždaryti",
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Susijungimo klaida",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Visos teisės saugomos © 2014 Jakob Borg ir šių bendraautorių:",
"Delete": "Trinti",
@@ -20,8 +23,10 @@
"Edit": "Redaguoti",
"Edit Node": "Redaguoti mazgą",
"Edit Repository": "Redaguoti saugyklą",
"Editing": "Redagavimas",
"Enable UPnP": "Įjungti UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Įveskite dvitaškiu atskirtą \"ip:port\" adresą arba žodį \"dynamic\" norėdami gauti adresą automatiškai",
"Enter ignore patterns, one per line.": "Suveskite nepaisomus šablonus, kiekvieną naujoje eilutėje.",
"Error": "Klaida",
"File Versioning": "Versijų valdymas",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Nekreipti dėmesio į failų naudojimosi leidimus.\nTaikyti FAT sistemose.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Visuotinio matomumo serveris",
"Global Repository": "Visuotinė saugykla",
"Idle": "Laisvas",
"Ignore Patterns": "Nepaisyti šablonų",
"Ignore Permissions": "Nepaisyti failų prieigos leidimų",
"Incoming Rate Limit (KiB/s)": "Įeinančio srauto maksimalus greitis (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Apversti sąlygas (pvz.: nenustoti naudoti)",
"Keep Versions": "Saugojamų versijų kiekis",
"Last seen": "Paskutinį kartą matytas",
"Latest Release": "Paskutinė versija",
"Local Discovery": "Vietinis matomumas",
"Local Discovery Port": "Vietinio matomumo jungtis",
@@ -45,7 +55,11 @@
"Master Repo": "Pagrindinė saugykla",
"Max File Change Rate (KiB/s)": "Maksimalus failų apsikeitimo greitis (KiB/s)",
"Max Outstanding Requests": "Maksimalus išeinančių užklausų skaičius",
"Maximum Age": "Maksimalus amžius",
"Multi level wildcard (matches multiple directory levels)": "Keletos lygių pakaitos (atitinka keletą direktorijų lygių)",
"Never": "Niekada",
"No": "Ne",
"No File Versioning": "Nėra versijų valdymo",
"Node ID": "Mazgo vardas",
"Node Identification": "Mazgo tapatybė",
"Node Name": "Mazgo vardas",
@@ -57,14 +71,18 @@
"Outgoing Rate Limit (KiB/s)": "Išeinančio srauto maksimalus greitis (KiB/s)",
"Override Changes": "Perrašyti pakeitimus",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Kelias iki vietinės saugyklos. Tildės ženklas (~) gali būti naudojamas nuorodai",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Kelias iki papkės kur saugomos senesnės versijos (palikite tuščią norėdami naudoti .stversions)",
"Please wait": "Prašome palaukti",
"Preview": "Peržiūra",
"Preview Usage Report": "Vartojimo statistikos peržiūra",
"Quick guide to supported patterns": "Trumpas leistinų šablonų vadovas",
"RAM Utilization": "LKA panaudojimas",
"Reconnect Interval (s)": "Pertrauka tarp susijungimų (s)",
"Repository ID": "Saugyklos vardas",
"Repository Master": "Pagrindinė saugykla",
"Repository Path": "Kelias iki saugyklos",
"Rescan": "Nuskaityti iš naujo",
"Rescan Interval": "Pertrauka tarp nuskaitymų",
"Rescan Interval (s)": "Pertrauka tarp nuskaitymų (s)",
"Restart": "Perleisti",
"Restart Needed": "Reikalingas perleidimas",
@@ -81,7 +99,10 @@
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Grupės būsenoje rodomas vietoje mazgo vardo. Kiti mazgai matys kaip pasirinktinį vardą.",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Grupės būsenoje rodomas vietoje mazgo vardo. Bus atnaujintas į mazgo vardą jei nieko neįrašysite.",
"Shutdown": "Išjungti",
"Simple File Versioning": "Supaprastintas versijų valdymas",
"Single level wildcard (matches within a directory only)": "Vieno lygio pakaitos (atitinka tik vieną direktorijos lygį)",
"Source Code": "Išeities kodas",
"Staggered File Versioning": "Pakopinis versijų valdymas",
"Start Browser": "Paleisti naršyklę",
"Stopped": "Sustabdyta",
"Support / Forum": "Palaikymas / Diskusijos",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Šifruota naudojimosi ataskaita siunčiama kasdien. Šiuo metu, ataskaitoje matosi tik operacinė sistema, saugyklos dydis ir programos versija. Jeigu ateityje bus siunčiama daugiau duomenų, jums vėl bus parodyta ši pranešimas.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Įvestas neteisingas mazgo vardas. Turi būti 52 simbolių eilutė su raidėmis ir skaičiais kuriuos galima atskirti tarpu arba brūkšneliu.",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Įvestas neteisingas mazgo vardas. Turi būti 52 ar 56 simbolių eilutė su raidėmis ir skaičiais kuriuos galima atskirti tarpu arba brūkšneliu.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Šie pertraukų nustatymai naudojami: pirmą valandą versijos laikomos 30 sekundžių, pirmą dieną versijos laikomos valandą, pirmas 30 dienų versijos laikomos parą, kol nebus viršytas nustatytas maksimalus amžius.",
"The maximum age must be a number and cannot be blank.": "Maksimalus amžius turi būti skaitmuo ir negali būti tuščias laukelis.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimalus laikas kurį bus saugojama versija (dienomis, nustatykite 0 norėdami saugoti amžinai).",
"The node ID cannot be blank.": "Mazgo vardas negali būti tuščias.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Mazgo vardas visada galima sužinoti apsilankius \"Edit > Show ID\". Tarpai ir brūkšneliai nebūtini (ignoruojami).",
"The number of old versions to keep, per file.": "Kiek failo versijų saugoti.",
"The number of versions must be a number and cannot be blank.": "Versijų skaičius turi būti skaitmuo ir negali būti tuščias laukelis.",
"The repository ID cannot be blank.": "Saugyklos vardas negali būti tuščias.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Saugyklos vardas negali būti ilgesnis nei 64 simboliai. Galima naudoti tik raides ir skaičius bet tašką (.), brūkšnelį (-) ir pabraukimą (_).",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Saugyklos vardas negali būti ilgesnis nei 64 simboliai. Galima naudoti tik raides ir skaičius bet tašką (.), brukšnelį (-) ir pabraukimą (_).",
"The repository ID must be unique.": "Saugyklos vardas turi būti unikalus.",
"The repository path cannot be blank.": "Kelias iki saugyklos negali būti tuščias.",
"The rescan interval must be at least 5 seconds.": "Nuskaityti galima nedažniau nei kas 5 sekundes.",
"Unknown": "Velniava",
"Up to Date": "Atnaujinta",
"Upgrade To {%version%}": "Atnaujinti į {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Naudoti suspaudimą",
"Use HTTPS for GUI": "Valdymo skydeliui naudoti saugų ryšį ",
"Version": "Versija",
"Versions Path": "Kelias iki versijos",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versijos ištrinamos jei senesnės už nustatyta maksimalų amžių arba jei viršytas maksimalus failų skaičius per nustatytą laiko tarpą.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Kai pridedate naują mazgą neužmirškite, kad kitame gale reikia sukonfigūruoti šį mazgą.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Kai įvedate naują saugyklą neužmirškite, kad ji bus naudojama visuose mazguose. Svarbu visur įvesti visiškai tokį pat saugyklos vardą neužmirštant apie didžiąsias ir mažąsias raides.",
"Yes": "Taip",
"You must keep at least one version.": "Būtina saugoti bent vieną versiją.",
"full documentation": "pilna dokumentacija",
"items": "įrašai"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Bijhouden van anonieme gebruikers statistieken toestaan?",
"Announce Server": "Aankondigings Server",
"Anonymous Usage Reporting": "Bijhouden anonieme gebruikers statistieken",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Fouten",
"CPU Utilization": "CPU Gebruik",
"Close": "Sluiten",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Verbindingsfout",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg en de onderstaande bijdragers:",
"Delete": "Verwijderen",
@@ -20,8 +23,10 @@
"Edit": "Bewerk",
"Edit Node": "Bewerk node",
"Edit Repository": "Repository Bijwerken",
"Editing": "Editing",
"Enable UPnP": "UPnP aanzetten",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Geef, gescheiden door komma's, \"ip:port\" adressen of \"dynamic\" voor het automatische vinden van de addressen.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"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",
@@ -35,9 +40,14 @@
"Global Discovery": "Globaal zoeken",
"Global Discovery Server": "Globale zoekserver",
"Global Repository": "Globale repository",
"Idle": "Klaar",
"Idle": "Inactief",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Rechten negeren",
"Incoming Rate Limit (KiB/s)": "Download snelheidslimiet (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Versies behouden",
"Last seen": "Laatst gezien op",
"Latest Release": "Laatste uitgave",
"Local Discovery": "Lokaal zoeken",
"Local Discovery Port": "Lokaal zoeken-poort",
@@ -45,7 +55,11 @@
"Master Repo": "Tegen veranderingen beschermen",
"Max File Change Rate (KiB/s)": "Maximale bestands uitwisselsnelheid (KiB/s)",
"Max Outstanding Requests": "Maximaal aantal openstaande aanvragen",
"Maximum Age": "Maximum leeftijd",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Nooit",
"No": "Nee",
"No File Versioning": "Geen versiebeheer",
"Node ID": "Node ID",
"Node Identification": "Node Identificatie",
"Node Name": "Node naam",
@@ -57,14 +71,18 @@
"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",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Pad waar de verschillende versies opgeslagen dienen te worden (laat leeg voor de standaard map '.stversion' in de repository).",
"Please wait": "Even geduld",
"Preview Usage Report": "Bekijk gebruikersstatistieken",
"Preview": "Voorbeeld",
"Preview Usage Report": "Bekijk gebruiksstatistieken",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"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": "Scanfrequentie",
"Rescan Interval (s)": "Herscan interval (s)",
"Restart": "Herstart",
"Restart Needed": "Herstart nodig",
@@ -81,7 +99,10 @@
"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",
"Simple File Versioning": "Eenvoudig versiebeheer",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Broncode",
"Staggered File Versioning": "Staggered File Versioning",
"Start Browser": "Start browser",
"Stopped": "Gestopt",
"Support / Forum": "Support / Forum",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "De versleutelde gebruikers statistieken worden dagelijks verstuurd. Deze worden gebruikt om veelgebruikte platformen, repo groottes en app versies bij te houden. Als er nieuwe statistieken worden bijgehouden, wordt dit venster weer getoond.",
"The 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De volgende intervallen worden gebruikt: het eerste uur worden versies iedere 30 seconden bewaard, de eerste dag worden versies ieder uur bewaard, de eerste 30 dagen worden versies iedere dag bewaard, tot de maximale leeftijd worden versies iedere week bewaard.",
"The maximum age must be a number and cannot be blank.": "De maximum leeftijd moet uit cijfers bestaan en mag niet leeggelaten worden.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "De maximale tijdsduur om een versie te bewaren (in dagen, gebruik 0 om versies voor altijd te bewaren).",
"The node ID cannot be blank.": "Er moet een node ID ingevoerd worden",
"The node ID 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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Het repository ID moet een korte naam zijn (met 64 karakters of minder) en mag alleen bestaan uit cijfers, letters, punten (.), streepjes (-) en liggende streepjes (_). ",
"The repository ID must be unique.": "Het repository ID moet uniek zijn.",
"The repository path cannot be blank.": "Het repository ID moet ingevuld worden.",
"The rescan interval must be at least 5 seconds.": "De scanfrequentie moet minimaal 5 seconde zijn.",
"Unknown": "Onbekend",
"Up to Date": "Gesynchroniseerd",
"Upgrade To {%version%}": "Upgrade naar {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Compressie gebruiken",
"Use HTTPS for GUI": "Gebruik HTTPS voor de GUI",
"Version": "Versie",
"Versions Path": "Bestandspad versies",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versies worden automatisch verwijderd als deze ouder zijn dan de maximale leeftijd of als ze het maximaal aantal toegestane bestanden per interval overschrijden. ",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Bedenk bij het toevoegen van een nieuwe node dat deze node ook toegevoegd moet worden aan de kant van de nieuwe node.",
"When adding a new 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.",
"full documentation": "full documentation",
"items": "objecten"
}

View File

@@ -7,10 +7,13 @@
"Addresses": "Endereços",
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
"Announce Server": "Servidor de anúncios",
"Anonymous Usage Reporting": "Enviar de relatórios anónimos de utilização",
"Anonymous Usage Reporting": "Enviar relatórios anónimos de utilização",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Erros",
"CPU Utilization": "Utilização da CPU",
"Close": "Fechar",
"Comment, when used at the start of a line": "Comentário, quando usado no início de uma linha",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Erro de ligação",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Direitos reservados © 2014 Jakob Borg e os seguintes contribuidores:",
"Delete": "Eliminar",
@@ -20,12 +23,14 @@
"Edit": "Editar",
"Edit Node": "Editar nó",
"Edit Repository": "Editar repositório",
"Editing": "Editando",
"Enable UPnP": "Activar UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços \"ip:porto\" separados por vírgulas ou \"dynamic\" para descobrir automaticamente o endereço.",
"Enter ignore patterns, one per line.": "Coloque os padrões a ignorar, um por linha.",
"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 ao procurar alterações. Utilize nos sistemas de ficheiros FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Os ficheiros são movidos para versões carimbadas com o tempo numa pasta .stversions quando substituídos ou apagados pelo syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Os ficheiros são movidos para versões marcadas com o tempo numa pasta .stversions, quando substituídos ou apagados pelo syncthing.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Os ficheiros são protegidos das alterações feitas noutros nós, mas alterações feitas neste nó serão enviadas para o resto do agrupamento.",
"Folder": "Pasta",
"GUI Authentication Password": "Senha da autenticação na interface gráfica",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Servidor da busca global",
"Global Repository": "Repositório global",
"Idle": "Em espera",
"Ignore Patterns": "Ignorar padrões",
"Ignore Permissions": "Ignorar permissões",
"Incoming Rate Limit (KiB/s)": "Limite de velocidade de recepção (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversão de uma dada condição (ou seja, não excluir)",
"Keep Versions": "Manter versões",
"Last seen": "Última vez que foi verificado",
"Latest Release": "Última versão",
"Local Discovery": "Busca local",
"Local Discovery Port": "Porto da busca local",
@@ -45,11 +55,15 @@
"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",
"Maximum Age": "Idade máxima",
"Multi level wildcard (matches multiple directory levels)": "Curinga multi-nível (faz corresponder a vários níveis de pastas)",
"Never": "Nunca",
"No": "Não",
"No File Versioning": "Sem gestão de versões de ficheiros",
"Node ID": "ID do nó",
"Node Identification": "Identificação do nó",
"Node Name": "Nome do nó",
"Notice": "Nota",
"Notice": "Avisos",
"OK": "OK",
"Offline": "Desconectado",
"Online": "Conectado",
@@ -57,20 +71,24 @@
"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",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Caminho onde as versões são guardadas (deixe vazio para usar a pasta pré-definida .stversions no repositório).",
"Please wait": "Aguarde",
"Preview": "Previsão",
"Preview Usage Report": "Pré-visualizar relatório de utilização",
"Quick guide to supported patterns": "Guia rápido dos padrões suportados",
"RAM Utilization": "Utilização da RAM",
"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": "Voltar a varrer",
"Rescan Interval (s)": "Intervalo entre varrimentos (s)",
"Rescan": "Verificar agora",
"Rescan Interval": "Intervalo entre verificações",
"Rescan Interval (s)": "Intervalo entre verificações (em segundos)",
"Restart": "Reiniciar",
"Restart Needed": "É preciso reiniciar",
"Restarting": "Reiniciando",
"Save": "Gravar",
"Scanning": "Varrendo",
"Scanning": "Verificando",
"Select the nodes to share this repository with.": "Seleccione os nós com os quais vai partilhar este repositório.",
"Settings": "Configurações",
"Share With Nodes": "Partilhar com os nós",
@@ -78,10 +96,13 @@
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identificador curto para o repositório. Tem que ser igual em todos os nós do agrupamento.",
"Show ID": "Mostrar ID",
"Shown instead of Node ID in the cluster status.": "Apresentado ao invés do ID do nó no estado do agrupamento.",
"Shown instead of Node ID in the cluster status. 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.",
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Apresentado ao invés do ID do nó na informação de estado do agrupamento. Será divulgado aos outros nós como um nome predefinido opcional.",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Apresentado ao invés do ID do nó na informação de estado do agrupamento. Será actualizado para o nome que o nó divulga, se for deixado em branco.",
"Shutdown": "Desligar",
"Simple File Versioning": "Gestão de versões de ficheiros simples",
"Single level wildcard (matches within a directory only)": "Curinga de um só nível (faz corresponder apenas dentro de uma pasta)",
"Source Code": "Código fonte",
"Staggered File Versioning": "Gestão de versões de ficheiros escalonada",
"Start Browser": "Iniciar navegador",
"Stopped": "Parado",
"Support / Forum": "Suporte / Fórum",
@@ -98,26 +119,34 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "O relatório de utilização cifrado é enviado diariamente. É utilizado para seguir plataformas comuns, tamanhos de repositórios e versões da aplicação. Se o conjunto de dados do relatório for alterado, será notificado novamente através desta janela.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "O ID do nó indicado não parece ser válido. Deveria conter uma palavra de 52 caracteres constituída por letras e números, com espaços e traços opcionais. ",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "O ID do nó fornecido não parece ser válido. Deveria ser um texto com 52 ou 56 caracteres constituídos por letras e números, com espaços e traços opcionais.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "São utilizados os seguintes intervalos: na primeira hora é guardada uma versão a cada 30 segundos, no primeiro dia é guardada uma versão a cada hora, nos primeiros 30 dias é guardada uma versão por dia e, até que atinja a idade máxima, é guardada uma versão por semana.",
"The maximum age must be a number and cannot be blank.": "A idade máxima tem que ser um número e não pode estar vazia.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Tempo máximo para manter uma versão (em dias, use 0 para manter a versão para sempre).",
"The node ID cannot be blank.": "O ID do nó não pode estar vazio.",
"The node ID 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 estar vazio.",
"The repository ID cannot be blank.": "O ID do repositório não pode estar vazio.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "O ID do repositório tem que ser um identificador curto (64 caracteres ou menos) e ser constituído somente por letras, números e os caracteres ponto (.), traço (-) e sublinhado (_).",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "O ID do repositório tem que ser um identificador curto (64 caracteres ou menos) e consiste em letras, números e os caracteres ponto (.), traço (-) e (_).",
"The repository ID must be unique.": "O ID do repositório tem que ser único.",
"The repository path cannot be blank.": "O caminho do repositório não pode estar vazio.",
"The rescan interval must be at least 5 seconds.": "O intervalo entre verificações tem que ser pelo menos de 5 segundos.",
"Unknown": "Desconhecido",
"Up to Date": "Actualizado",
"Upgrade To {%version%}": "Actualizar para {{version}}",
"Upgrading": "Actualizando",
"Upload Rate": "Taxa de envio",
"Upload Rate": "Velocidade de envio",
"Usage": "Utilização",
"Use Compression": "Usar compressão",
"Use HTTPS for GUI": "Utilizar HTTPS na interface gráfica",
"Version": "Versão",
"Versions Path": "Caminho das versões",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são eliminadas automaticamente se forem mais antigas do que a idade máxima ou excederem o número máximo de ficheiros permitido num intervalo.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quando adicionar um novo nó, lembre-se que este nó tem que ser adicionado no outro lado também.",
"When adding a new 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.",
"full documentation": "documentação completa",
"items": "itens"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Разрешить сбор анонимной статистики использования?",
"Announce Server": "Сервер публикации",
"Anonymous Usage Reporting": "Анонимная статистика использования",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Ошибки",
"CPU Utilization": "Загрузка ЦПУ",
"Close": "Закрыть",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Ошибка подключения",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Все права защищены © 2014 Jakob Borg и следующие участники:",
"Delete": "Удалить",
@@ -20,8 +23,10 @@
"Edit": "Изменить",
"Edit Node": "Изменить Узел",
"Edit Repository": "Изменить Репозиторий",
"Editing": "Editing",
"Enable UPnP": "Включить UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": " Введите пары \"IP:PORT\" разделённые запятыми, или слово \"dynamic\" для автоматического обнаружения адреса.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Ошибка",
"File Versioning": "Управление версиями",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Права доступа к файлам будут игнорироваться. Используйте на файловых системах типа FAT.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Сервер глобального обнаружения",
"Global Repository": "Глобальный репозиторий",
"Idle": "Бездействует",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Игнорировать файловые права доступа",
"Incoming Rate Limit (KiB/s)": "Ограничение входящего потока (Кбит/сек)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Количество хранимых версий",
"Last seen": "Был доступен",
"Latest Release": "Последняя версия",
"Local Discovery": "Локальное обнаружение",
"Local Discovery Port": "Порт локального обнаружения",
@@ -45,7 +55,11 @@
"Master Repo": "Главный репозиторий",
"Max File Change Rate (KiB/s)": "Максимальная скорость изменения файлов (KiB/s)",
"Max Outstanding Requests": "Максимальное количество исходящих запросов",
"Maximum Age": "Максимальный срок",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Никогда",
"No": "Нет",
"No File Versioning": "Без управления версиями файлов",
"Node ID": "ID Узла",
"Node Identification": "Идентификация узла",
"Node Name": "Имя Узла",
@@ -57,14 +71,18 @@
"Outgoing Rate Limit (KiB/s)": "Предел скорости отдачи (KiB/s)",
"Override Changes": "Перезаписать изменения",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Путь к репозиторию на локальном компьютере. Если не существует, то будет создан. Знак тильды (~) может использоваться как ссылка",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Путь, куда нужно сохранять версии (оставьте пустым для папки .stversions в репозитории).",
"Please wait": "Пожалуйста, подождите",
"Preview": "Предварительный просмотр",
"Preview Usage Report": "Посмотреть отчёт об использовании",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "Использование ОЗУ",
"Reconnect Interval (s)": "Интервал переподключений (секунды)",
"Repository ID": "ID Репозитория",
"Repository Master": "Главный Репозиторий",
"Repository Path": "Путь к Репозиторию",
"Rescan": "Rescan",
"Rescan": "Пересканирование",
"Rescan Interval": "Интервал пересканирования",
"Rescan Interval (s)": "Интервал между сканированием (сек)",
"Restart": "Перезапуск",
"Restart Needed": "Требуется перезапуск",
@@ -78,10 +96,13 @@
"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.",
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Показано вместо идентификатора узла в статусе кластера. Будет разглашено другим узлам в качестве дополнительного имени по умолчанию.",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Показано вместо идентификатора узла в статусе кластера. Будет обновлено именем, разглашённым узлом, если оставлено пустым.",
"Shutdown": "Выключить",
"Simple File Versioning": "Простое управление версиями файлов",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Исходный код",
"Staggered File Versioning": "Ступенчатое управление версиями файлов",
"Start Browser": "Открыть браузер",
"Stopped": "Остановлено",
"Support / Forum": "Поддержка / Форум",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Зашифрованные отчёты об использовании отправляются ежедневно. Они используются для отслеживания проблем, размеров репозиториев и версий 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Используются следующие интервалы: в первый час версия меняется каждые 30 секунд, в первый день - каждый час, первые 30 дней - каждый день, после, до максимального срока - каждую неделю.",
"The maximum age must be a number and cannot be blank.": "Максимальный срок должен быть числом и не может быть пустым.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максимальный срок хранения версии (в днях, 0 значит вечное хранение).",
"The node ID cannot be blank.": "ID узла не может быть пустым.",
"The node ID 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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "ID репозитория должен быть коротким id (64 символа или меньше) состоящий только из букв, цифр, точек (.), тире (-) и нижнего подчёркивания (_).",
"The repository ID must be unique.": "ID Репозитория должен быть уникальным.",
"The repository path cannot be blank.": "Путь к репозиторию не может быть пустым.",
"The rescan interval must be at least 5 seconds.": "Интервал пересканирования должен быть хотя бы 5 секунд.",
"Unknown": "Неизвестно",
"Up to Date": "Обновлено",
"Upgrade To {%version%}": "Обновить до {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Использовать сжатие",
"Use HTTPS for GUI": "Использовать HTTPS для панели управления",
"Version": "Версия",
"Versions Path": "Путь к версиям",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версии удаляются автоматически, если они существуют дольше максимального срока или превышают разрешённое количество файлов за интервал.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Добавляя новый узел помните, что новый узел должен быть добавлен и на другой стороне.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Добавляя новый репозиторий помните, что ID репозитория используется для связи всех репозиториев между узлами. ID чувствительны к регистру и должны совпадать на всех узлах.",
"Yes": "Да",
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
"full documentation": "full documentation",
"items": "элементы"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistik?",
"Announce Server": "Uppslagningsserver",
"Anonymous Usage Reporting": "Anonym användarstatistik",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Buggar",
"CPU Utilization": "CPU-användning",
"Close": "Stäng",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Anslutningsproblem",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg och de följande medarbetarna:",
"Delete": "Radera",
@@ -20,8 +23,10 @@
"Edit": "Redigera",
"Edit Node": "Redigera nod",
"Edit Repository": "Redigera lagring",
"Editing": "Editing",
"Enable UPnP": "Använd UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade \"ip:port\"-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Fel",
"File Versioning": "Versionshantering",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Filers rättighetsbitar tas inte hänsyn till vid synkronisering. Använd på FAT-filsystem.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Global uppslagningsserver",
"Global Repository": "Global lagring",
"Idle": "Vilande",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Ignorera filrättigheter",
"Incoming Rate Limit (KiB/s)": "Max nedladdningshastighet (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Behåll versioner",
"Last seen": "Senast online",
"Latest Release": "Senaste version",
"Local Discovery": "Lokal uppslagning",
"Local Discovery Port": "Lokal uppslagningsport",
@@ -45,7 +55,11 @@
"Master Repo": "Huvudlagring",
"Max File Change Rate (KiB/s)": "Högsta ändringshastighet (KiB/s)",
"Max Outstanding Requests": "Paralellitet",
"Maximum Age": "Högsta åldersgräns",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Aldrig",
"No": "Nej",
"No File Versioning": "Ingen versionshantering",
"Node ID": "Nod-ID",
"Node Identification": "Nod-identifiering",
"Node Name": "Nodnamn",
@@ -54,18 +68,22 @@
"Offline": "Ej tillgänglig",
"Online": "Tillgänglig",
"Out Of Sync": "Ur synk",
"Outgoing Rate Limit (KiB/s)": "Utgående hastighetsbegränsning (KiB/s)",
"Outgoing Rate Limit (KiB/s)": "Max uppladdningshastighet (KiB/s)",
"Override Changes": "Skriv över ändringar",
"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",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Katalog där versioner sparas (lämna blankt för att använda .stversions i lagringskatalogen).",
"Please wait": "Var god vänta",
"Preview": "Preview",
"Preview Usage Report": "Förhandsgranska statistik",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"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)",
"Rescan": "Uppdatera",
"Rescan Interval": "Uppdateringsintervall",
"Rescan Interval (s)": "Uppdateringsintervall (s)",
"Restart": "Starta om",
"Restart Needed": "Omstart behövs",
"Restarting": "Startar om",
@@ -81,7 +99,10 @@
"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",
"Simple File Versioning": "Enkel versionshantering",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Källkod",
"Staggered File Versioning": "Versionshantering i intervall",
"Start Browser": "Starta browser",
"Stopped": "Stoppad",
"Support / Forum": "Support / Forum",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De följande intervallen används: varje 30 sekunder under den första timmen; varje timme under den första dagen; varje dag för de första 30 dagarna; varje vecka tills den maximala åldersgränsen uppnås.",
"The maximum age must be a number and cannot be blank.": "Åldersgränsen måste vara ett tal och kan inte lämnas blankt.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tid att behålla en version (i dagar, sätt till 0 för att behålla versioner för evigt).",
"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.",
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett nummer och kan inte lämnas blankt.",
"The repository ID cannot be blank.": "Lagrings-ID kan inte vara blankt.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Lagrings-ID måste vara en kort identifierar (64 tecken eller mindre), bestående av endast bokstäver, siffror, punkt (.), bindestreck (-) och understräck (_).",
"The repository ID must be unique.": "Lagrings-ID måste vara unikt.",
"The repository path cannot be blank.": "Lagrings-ID kan inte lämnas blankt.",
"The rescan interval must be at least 5 seconds.": "Uppdateringsintervallet måste vara minst 5 sekunder.",
"Unknown": "Okänt",
"Up to Date": "Helt uppdaterad",
"Upgrade To {%version%}": "Uppgradera till {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Använd komprimering",
"Use HTTPS for GUI": "Använd HTTPS för GUI",
"Version": "Version",
"Versions Path": "Katalog för versioner",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i sitt interval.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "När du lägger till en ny nod, kom ihåg att den här noden måste läggas till på den andra noden också.",
"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 lägger till ny lagring, tänk på att lagrings-ID knyter ihop lagringen mellan olika noder. De måste vara exakt desamma mellan noder, och stora eller små bokstäver har betydelse.",
"Yes": "Ja",
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
"full documentation": "full documentation",
"items": "poster"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Anonim kullanım raporlarına izin ver ?",
"Announce Server": "Duyuru Sunucusu",
"Anonymous Usage Reporting": "Anonim Kullanım Raporlama",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Hatalar",
"CPU Utilization": "İşlemci Kullanımı",
"Close": "Kapat",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "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",
@@ -20,8 +23,10 @@
"Edit": "Seçenekler",
"Edit Node": "Düğümü Düzenle",
"Edit Repository": "Depoyu düzenle",
"Editing": "Editing",
"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.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"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.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Küresel Keşif Sunucusu",
"Global Repository": "Global Depo",
"Idle": "Boşta",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "İzinleri yoksay",
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Sürüm tut",
"Last seen": "Last seen",
"Latest Release": "Son sürüm",
"Local Discovery": "Yerel bulma",
"Local Discovery Port": "Yerel bulma portları",
@@ -45,7 +55,11 @@
"Master Repo": "Ana depo",
"Max File Change Rate (KiB/s)": "Mak. Dosya değiştirme oranı (KB/sn)",
"Max Outstanding Requests": "Maks Öncellikli İstekler",
"Maximum Age": "Maximum Age",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Never",
"No": "Hayır",
"No File Versioning": "No File Versioning",
"Node ID": "Düğüm ID",
"Node Identification": "Düğüm Kimliği",
"Node Name": "Düğüm İsmi",
@@ -57,14 +71,18 @@
"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",
"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": "Lütfen Bekleyin",
"Preview": "Preview",
"Preview Usage Report": "Kullanım raporunu gözden geçir",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"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": "Rescan Interval",
"Rescan Interval (s)": "Tekrar tarama süresi (sn)",
"Restart": "Yeniden Başlat",
"Restart Needed": "Yeniden başlatma gereklidir",
@@ -81,7 +99,10 @@
"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",
"Simple File Versioning": "Simple File Versioning",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Kaynak Kodu",
"Staggered File Versioning": "Staggered File Versioning",
"Start Browser": "Tarayıcıyı Başlat",
"Stopped": "Durduruldu",
"Support / Forum": "Destek / Forum",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Ş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 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.": "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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "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.",
"The rescan interval must be at least 5 seconds.": "The rescan interval must be at least 5 seconds.",
"Unknown": "Bilinmiyor",
"Up to Date": "Güncel",
"Upgrade To {%version%}": "{{version}} sürümüne yükselt",
@@ -115,9 +141,12 @@
"Use Compression": "Sıkıştırma kullan",
"Use HTTPS for GUI": "GUI için HTTPS kullan",
"Version": "Sürüm",
"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.": "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.",
"full documentation": "full documentation",
"items": "öğeler"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "Дозволити програмі збирати анонімну статистику викроистання?",
"Announce Server": "Сервер анонсування",
"Anonymous Usage Reporting": "Анонімна статистика використання",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Помилки",
"CPU Utilization": "Навантаження CPU",
"Close": "Закрити",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "Помилка з’єднання",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg та наступні контриб’ютори:",
"Delete": "Видалити",
@@ -20,8 +23,10 @@
"Edit": "Редагувати",
"Edit Node": "Редагувати вузол",
"Edit Repository": "Редагувати репозиторій",
"Editing": "Editing",
"Enable UPnP": "Увімкнути UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Уведіть адреси \"ip:port\" розділені комою, або слово \"dynamic\" для здійснення автоматичного виявлення адреси.",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Помилка",
"File Versioning": "Керування версіями",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Біти прав доступу до файлів будуть проігноровані під час визначення змін. Використовуйте на файлових системах FAT.",
@@ -36,8 +41,13 @@
"Global Discovery Server": "Сервер для глобального виявлення",
"Global Repository": "Глобальний репозиторій",
"Idle": "Очікування",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Ігнорувати права доступу до файлів",
"Incoming Rate Limit (KiB/s)": "Ліміт швидкості завантаження (КіБ/с)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Зберігати версії",
"Last seen": "З’являвся останній раз",
"Latest Release": "Останній реліз",
"Local Discovery": "Локальне виявлення",
"Local Discovery Port": "Локальний порт для виявлення",
@@ -45,7 +55,11 @@
"Master Repo": "Центральний репозиторій",
"Max File Change Rate (KiB/s)": "Максимальна швидкість змінення файлів (КіБ/с)",
"Max Outstanding Requests": "Максимальна кількість вихідних запитів",
"Maximum Age": "Максимальний вік",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "Ніколи",
"No": "Ні",
"No File Versioning": "Версіонування вимкнено",
"Node ID": "ID вузла",
"Node Identification": "Ідентифікатор вузла",
"Node Name": "Назва вузла",
@@ -57,14 +71,18 @@
"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": "Шлях до репозиторія на локальному комп’ютері. Буде створений, якщо такий не існує. Символ тильди (~) може бути використаний як ярлик для",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Шлях, де повинні зберігатися версії (залиште порожнім для зберігання в директорію .stversions в репозиторії)",
"Please wait": "Будь ласка, зачекайте",
"Preview": "Попередній перегляд",
"Preview Usage Report": "Попередній перегляд статистичного звіту",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "Використання RAM",
"Reconnect Interval (s)": "Інтервал повторного з’єднання (с)",
"Repository ID": "ID репозиторія",
"Repository Master": "Центральний репозиторій",
"Repository Path": "Шлях до репозиторія",
"Rescan": "Пересканувати",
"Rescan Interval": "Інтервал для повторного сканування",
"Rescan Interval (s)": "Інтервал для повторного сканування (с)",
"Restart": "Перезапуск",
"Restart Needed": "Необхідний перезапуск",
@@ -78,10 +96,13 @@
"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.",
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Показується замість ID вузла в статусі кластера. Буде розголошено іншим вузлам як опціональне типове ім’я.",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Показується замість ID вузла в статусі кластера. Буде оновлено ім’ям, яке розголошене вузлом, якщо залишити порожнім.",
"Shutdown": "Вимкнути",
"Simple File Versioning": "Просте версіонування",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "Сирцевий код",
"Staggered File Versioning": "Поступове версіонування",
"Start Browser": "Запустити браузер",
"Stopped": "Зупинено",
"Support / Forum": "Підтримка / Форум",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Зашифрована статистика використання відсилається щоденно. Вона використовується для того, щоб розробники розуміли, на яких платформах працює програма, розміри репозиторіїв та версії програми. Якщо набір даних, що збирається зазнає змін, ви обов’язково будете повідомлені через це діалогове вікно.",
"The 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Використовуються наступні інтервали: для першої години версія зберігається кожні 30 секунд, для першого дня версія зберігається щогодини, для перших 30 днів версія зберігається кожен день, опісля, до максимального строку, версія зберігається щотижня.",
"The maximum age must be a number and cannot be blank.": "Максимальний термін повинен бути числом та не може бути пустим.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максимальний термін, щоб зберігати версію (у днях, вствновіть в 0, щоби зберігати версії назавжди).",
"The node ID cannot be blank.": "ID вузла не може бути порожнім.",
"The node ID 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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "ID репозиторія повинен бути коротким ідентифікатором (64 символи або менше), що містить лише цифри та літери, знак крапки (.), тире (-) та нижнього підкреслення (_).",
"The repository ID must be unique.": "ID репозиторія повинен бути унікальним.",
"The repository path cannot be blank.": "Шлях до репозиторія не може бути порожнім.",
"The rescan interval must be at least 5 seconds.": "Інтервал повторного сканування повинен бути принаймні 5 секунд.",
"Unknown": "Невідомо",
"Up to Date": "Актуальа версія",
"Upgrade To {%version%}": "Оновити до {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "Використовувати компресію",
"Use HTTPS for GUI": "Використовувати HTTPS для доступу до панелі управління",
"Version": "Версія",
"Versions Path": "Шлях до версій",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версії автоматично видаляються, якщо вони старше, ніж максимальний вік, або перевищують допустиму кількість файлів за інтервал.",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Коли додаєте новий вузол, пам’ятайте, що цей вузол повинен бути доданий і на іншій стороні.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Коли додаєте новий репозиторій, пам’ятайте, що ID репозиторія використовується для того, щоб зв’язувати репозиторії разом між вузлами. Назви є чутливими до регістра та повинні співпадати точно між усіма вузлами.",
"Yes": "Так",
"You must keep at least one version.": "Ви повинні зберігати щонайменше одну версію.",
"full documentation": "full documentation",
"items": "елементи"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "允许匿名使用报告?",
"Announce Server": "Announce服务器",
"Anonymous Usage Reporting": "匿名使用报告",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "Bug汇报",
"CPU Utilization": "CPU使用率",
"Close": "关闭",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "连接出错",
"Copyright © 2014 Jakob Borg and the following Contributors:": "版权© 2014 Jakob Borg 及以下贡献者:",
"Delete": "删除",
@@ -20,8 +23,10 @@
"Edit": "选项",
"Edit Node": "编辑节点",
"Edit Repository": "编辑仓库",
"Editing": "Editing",
"Enable UPnP": "开启UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "输入以半角逗号分隔的\"ip:端口\"设置可用地址列表,或者输入\"dynamic\"表示自动寻找地址。",
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "错误",
"File Versioning": "版本控制",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "监控文件改变时忽略文件权限位。用于FAT文件系统。",
@@ -36,8 +41,13 @@
"Global Discovery Server": "用以在互联网上寻找节点的Announce服务器地址",
"Global Repository": "全局仓库",
"Idle": "空闲",
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "忽略文件权限",
"Incoming Rate Limit (KiB/s)": "下载速率限制(千字节/秒)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "保留历史版本数量",
"Last seen": "最后可见",
"Latest Release": "最新版本",
"Local Discovery": "在局域网上寻找节点",
"Local Discovery Port": "局域网寻找监听端口",
@@ -45,7 +55,11 @@
"Master Repo": "主仓库",
"Max File Change Rate (KiB/s)": "最大文件变化速率(千字节/每秒)",
"Max Outstanding Requests": "同时可响应的请求数上限",
"Maximum Age": "历史版本最长保留时间",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "从未",
"No": "否",
"No File Versioning": "不启用版本控制",
"Node ID": "节点ID",
"Node Identification": "节点ID",
"Node Name": "节点名",
@@ -57,14 +71,18 @@
"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": "该仓库的本地路径。如果该路径不存在,则会自动创建。波浪线字符(~)代表了以下文件夹:",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "历史版本保存路径(若留空,则会保存在默认的.stversions文件夹",
"Please wait": "请稍候",
"Preview": "预览",
"Preview Usage Report": "预览使用报告",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "内存使用量",
"Reconnect Interval (s)": "重连间隔(秒)",
"Repository ID": "仓库ID",
"Repository Master": "主仓库",
"Repository Path": "仓库路径",
"Rescan": "重新扫描",
"Rescan Interval": "扫描间隔",
"Rescan Interval (s)": "扫描间隔(秒)",
"Restart": "重启syncthing",
"Restart Needed": "需要重启Syncthing",
@@ -81,7 +99,10 @@
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "在节点群中将会显示本名称而不是节点ID。同时也会被做为默认名称通告至其他节点。",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "在节点群中将会显示本名称而不是节点ID。如果设置为空则会使用目标节点提供的默认名称。",
"Shutdown": "关闭Syncthing",
"Simple File Versioning": "简易版本控制",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "源代码",
"Staggered File Versioning": "阶段版本控制",
"Start Browser": "启动浏览器",
"Stopped": "已停止",
"Support / Forum": "支持/论坛",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "经过加密的使用报告会每天发送。它用来跟踪统计使用本软件的平台,仓库大小,以及本软件的版本。如果报告的内容有任何变化,本对话框会再次弹出提示您。",
"The 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 following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "保留的历史版本会满足以下条件最近一小时内的历史版本更新间隔小于30秒的仅保留一份。最近一天内的历史版本更新间隔小于1小时的仅保留一份。最近一个月内的历史版本更新间隔小于1天的仅保留一份。距离现在超过一个月且小于最长保留时间的更新间隔小于1周的仅保留一份。",
"The maximum age must be a number and cannot be blank.": "最长保留时间必须为数字,且不能为空。",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "历史版本保留的最长天数0为永久保存",
"The node ID cannot be blank.": "节点ID不能为空",
"The node ID 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 dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "仓库ID的长度,必须在64字节或更少只能包含字母数字点(.),以及下划线(_)",
"The repository ID must be unique.": "仓库ID必须唯一。",
"The repository path cannot be blank.": "仓库路径不能为空。",
"The rescan interval must be at least 5 seconds.": "扫描间隔必须至少为5秒。",
"Unknown": "未知",
"Up to Date": "同步完成",
"Upgrade To {%version%}": "升级至版本{{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "使用压缩",
"Use HTTPS for GUI": "使用HTTPS连接web管理页面",
"Version": "版本",
"Versions Path": "历史版本路径",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "超过最长保留时间,或者不满足下列条件的历史版本,将会被删除。",
"When adding a new node, keep in mind that this node must be added on the other side too.": "当您在本节点上添加新节点时,也必须在您所添加的节点上添加本节点。",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "添加新仓库时需要注意仓库ID是用来在不同节点间绑定关联的仓库的。仓库ID是大小写敏感的而且在所有的节点中必须完全相同才能正确建立同步。",
"Yes": "是",
"You must keep at least one version.": "您必须保留至少一个版本。",
"full documentation": "full documentation",
"items": "条目"
}

View File

@@ -8,9 +8,12 @@
"Allow Anonymous Usage Reporting?": "允許匿名的使用資訊回報?",
"Announce Server": "發佈伺服器",
"Anonymous Usage Reporting": "匿名的使用資訊回報",
"Any nodes configured on an introducer node will be added to this node as well.": "Any nodes configured on an introducer node will be added to this node as well.",
"Bugs": "程式錯誤",
"CPU Utilization": "CPU 使用率",
"Close": "關閉",
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
"Connection Error": "連線錯誤",
"Copyright © 2014 Jakob Borg and the following Contributors:": "版權所有 © 2014 Jakob Borg 及以下貢獻者:",
"Delete": "刪除",
@@ -20,8 +23,10 @@
"Edit": "編輯",
"Edit Node": "編輯節點",
"Edit Repository": "編輯儲存庫",
"Editing": "Editing",
"Enable UPnP": "啟用 UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "輸入以半形逗號區隔的 \"ip:連接埠\" 位址,或著輸入 \"dynamic\" 以進行位址的自動探索",
"Enter ignore patterns, one per line.": "輸入忽略樣式,一行一種。",
"Error": "錯誤",
"File Versioning": "檔案版本控制",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "當在尋找變化時,檔案權限位元會被忽略。用於 FAT 檔案系統。",
@@ -36,16 +41,25 @@
"Global Discovery Server": "全域探索伺服器",
"Global Repository": "全域儲存庫",
"Idle": "閒置",
"Ignore Patterns": "忽略樣式",
"Ignore Permissions": "忽略權限",
"Incoming Rate Limit (KiB/s)": "連入速率限制 (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "保留版本數",
"Last seen": "最後發現時間",
"Latest Release": "最新發佈",
"Local Discovery": "本地探索",
"Local Discovery Port": "本地探索連接埠",
"Local Repository": "本地儲存庫",
"Master Repo": "主儲存庫",
"Max File Change Rate (KiB/s)": "最大檔案改變速率 (KiB/s)",
"Max Outstanding Requests": "最大未完成的請求",
"Max Outstanding Requests": "最多待處理的請求",
"Maximum Age": "最長存留時間",
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
"Never": "從未",
"No": "否",
"No File Versioning": "無檔案版本控制",
"Node ID": "節點識別碼",
"Node Identification": "節點識別",
"Node Name": "節點名稱",
@@ -57,14 +71,18 @@
"Outgoing Rate Limit (KiB/s)": "連出速率限制 (KiB/s)",
"Override Changes": "覆蓋改變",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "儲存庫在本地電腦的路徑。若資料夾不存在則會建立。波浪符號 (~) 可用作下列資料夾的捷徑:",
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "儲存歷史版本的路徑 (若為空,則預設使用儲存庫中的 .stversions 資料夾)。",
"Please wait": "請稍後",
"Preview": "預覽",
"Preview Usage Report": "預覽使用資訊報告",
"Quick guide to supported patterns": "Quick guide to supported patterns",
"RAM Utilization": "記憶體使用率",
"Reconnect Interval (s)": "重新連接間隔 (秒)",
"Repository ID": "儲存庫識別碼",
"Repository Master": "主儲存庫",
"Repository Path": "儲存庫路徑",
"Rescan": "重新掃描",
"Rescan Interval": "重新掃描間隔",
"Rescan Interval (s)": "掃描間隔 (秒)",
"Restart": "重新啟動",
"Restart Needed": "需要重新啟動",
@@ -81,7 +99,10 @@
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "代替節點識別碼顯示在叢集狀態中。這段文字將會廣播到其他的節點作為一個可選的預設名稱。",
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "代替節點識別碼顯示在叢集狀態中。本欄若未填寫則將被更新為此節點所廣播的名稱。",
"Shutdown": "關閉",
"Simple File Versioning": "簡單檔案版本控制",
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
"Source Code": "原始碼",
"Staggered File Versioning": "變動式檔案版本控制",
"Start Browser": "啟動瀏覽器",
"Stopped": "已停止",
"Support / Forum": "支援 / 論壇",
@@ -98,14 +119,19 @@
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "經過加密的使用資訊報告會每天傳送。報告是用來追蹤常用的平台、儲存庫的大小以及應用程式的版本。若傳送的資料集有異動,您會再次看到這個對話框。",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "輸入的節點識別碼似乎無效。它應該為一串包含半形英文字母及數字,並可能會含有空白或連接符號的字串,且長度為 52 個字元。",
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "輸入的節點識別碼似乎無效。它應該為一串包含半形英文字母及數字,並可能會含有空白或連接符號的字串,且長度為 52 或 56 個字元。",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "使用下列的間隔:在第一個小時內每 30 秒保留一個版本,在第一天內每小時保留一個版本,在第 30 天內每一天保留一個版本,在達到最長存留時間前每一星期保留一個版本。",
"The maximum age must be a number and cannot be blank.": "最長存留時間最大必須為一個數字且不得為空。",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "一個版本被保留的最長時間 (單位為天,若設定為 0 則表示永遠保留)。",
"The node ID cannot be blank.": "節點識別碼不能為空白。",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "要輸入在這裡的節點識別碼可以在其他節點的 \"編輯 > 顯示識別碼\" 對話框找到。空白以及連接符號可不輸入 (省略)。",
"The number of old versions to keep, per file.": "每個檔案要保留的舊版本數量。",
"The number of versions must be a number and cannot be blank.": "每個檔案要保留的舊版本數量必須是數字且不能為空白。",
"The repository ID cannot be blank.": "儲存庫識別碼不能為空白。",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "儲存庫識別碼必須為一段只包含半形英文字母、數字、點 (.)、連接符號 (-) 以及底線 (_) 的簡短識別碼 (不多於 64 個字元)",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "儲存庫識別碼必須為一段只包含半形英文字母、數字、點 (.)、連接符號 (-) 以及底線 (_) 的簡短識別碼 (不多於 64 個字元)",
"The repository ID must be unique.": "儲存庫識別碼必須為獨一無二的。",
"The repository path cannot be blank.": "儲存庫路徑不能空白。",
"The rescan interval must be at least 5 seconds.": "重新掃描間隔至少必須為 5 秒。",
"Unknown": "未知",
"Up to Date": "最新",
"Upgrade To {%version%}": "升級到 {{version}}",
@@ -115,9 +141,12 @@
"Use Compression": "使用壓縮",
"Use HTTPS for GUI": "為 GUI 使用 HTTPS",
"Version": "版本",
"Versions Path": "歷史版本路徑",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "當檔案歷史版本的存留時間大於設定的最大值,或是其數量在一段時間內超出允許值時,則會被刪除。",
"When adding a new node, keep in mind that this node must be added on the other side too.": "當新增一個節點時,請記住,這個節點也必須被添加至另一邊。",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "當新增一個儲存庫時,請記住,儲存庫識別碼是用來將節點之間的儲存庫綁定在一起的。它們有區分大小寫,且必須在所有節點之間完全相同。",
"Yes": "是",
"You must keep at least one version.": "您必須保留至少一個版本。",
"full documentation": "full documentation",
"items": "個項目"
}

1
gui/lang/valid-langs.js Normal file
View File

@@ -0,0 +1 @@
var validLangs = ["bg","ca","da","de","el","en","es","fr","hu","it","lt","nl","pt-PT","ru","sv","tr","uk","zh-CN","zh-TW"]

View File

@@ -10,7 +10,7 @@
<div class="modal-body" ng-transclude>
</div>
<div ng-if="close" class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
</div>
</div>
</div>

97
gui/overrides.css Normal file
View File

@@ -0,0 +1,97 @@
body {
padding-bottom: 70px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
h1, h2, h3, h4, h5 {
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
ul+h5 {
margin-top: 1.5em;
}
.text-monospace {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
.table-condensed>thead>tr>th, .table-condensed>tbody>tr>th, .table-condensed>tfoot>tr>th, .table-condensed>thead>tr>td, .table-condensed>tbody>tr>td, .table-condensed>tfoot>tr>td {
border-top: none;
}
.logo {
margin: 0;
padding: 0;
top: -5px;
position: relative;
}
.list-no-bullet {
list-style-type: none
}
.li-column {
display: inline-block;
min-width: 7em;
margin-right: 1em;
background-color: rgb(236, 240, 241);
border-radius: 3px;
padding: 1px 4px;
margin: 2px 2px;
}
.li-column span.data {
margin-left: 0.5em;
min-width: 10em;
text-align: right;
display: inline-block;
}
.ng-cloak {
display: none !important;
}
.table th {
white-space: nowrap;
font-weight: 400;
}
.table td {
padding-left: 20px !important;
}
.table td.small-data {
white-space: nowrap;
}
table.table-condensed {
table-layout: fixed;
}
table.table-condensed td {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width:767px) {
table.table-condensed td {
/* for mobile phones to allow linebreaks in long repro folder/shared with
* columns. */
white-space: normal;
}
}
.navbar-right {
/* to align with panel */
padding-right: 15px;
}
.panel-body .table-condensed {
margin-bottom: 0;
}
.dl-horizontal.dl-narrow dt {
width: 40px;
}
.dl-horizontal.dl-narrow dd {
margin-left: 60px;
}

View File

Binary file not shown.

View File

@@ -1 +0,0 @@
var validLangs = ["bg","da","de","el","en","es","fr","hu","it","lt","nl","pt-PT","ru","sv","tr","uk","zh-CN","zh-TW"]

View File

@@ -1,5 +0,0 @@
#!/bin/sh
./test-http.sh || exit
./test-merge.sh || exit
./test-delupd.sh || exit

View File

@@ -1,49 +0,0 @@
<configuration version="2">
<repository id="default" directory="s2" ro="false" ignorePerms="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
<versioning></versioning>
<syncorder></syncorder>
</repository>
<repository id="s12" directory="s12-2" ro="false" ignorePerms="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<versioning></versioning>
<syncorder></syncorder>
</repository>
<repository id="s23" directory="s23-2" ro="false" ignorePerms="false">
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
<versioning></versioning>
<syncorder></syncorder>
</repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1" compression="true">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2" compression="true">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<address>127.0.0.1:22003</address>
</node>
<gui enabled="true" tls="false">
<address>127.0.0.1:8082</address>
<apikey>abc123</apikey>
</gui>
<options>
<listenAddress>127.0.0.1:22002</listenAddress>
<globalAnnounceServer>announce.syncthing.net:22025</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<parallelRequests>16</parallelRequests>
<maxSendKbps>0</maxSendKbps>
<rescanIntervalS>15</rescanIntervalS>
<reconnectionIntervalS>5</reconnectionIntervalS>
<maxChangeKbps>10000</maxChangeKbps>
<startBrowser>false</startBrowser>
<upnpEnabled>true</upnpEnabled>
<urAccepted>-1</urAccepted>
</options>
</configuration>

View File

@@ -1,40 +0,0 @@
<configuration version="2">
<repository id="unique" directory="s4" ro="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2"></node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3"></node>
<node id="EJHMPAQOGCVORISB4IS3SYYVJXTKJGLTU66DIQPGJ5D2GXGQ3OWQ" name="s4"></node>
</repository>
<repository id="default" directory="s4d" ro="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1"></node>
</repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<address>127.0.0.1:22003</address>
</node>
<node id="EJHMPAQOGCVORISB4IS3SYYVJXTKJGLTU66DIQPGJ5D2GXGQ3OWQ" name="s4">
<address>dynamic</address>
</node>
<gui enabled="true">
<address>127.0.0.1:8084</address>
<apikey>abc123</apikey>
</gui>
<options>
<listenAddress>:22004</listenAddress>
<globalAnnounceServer>announce.syncthing.net:22025</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
<parallelRequests>16</parallelRequests>
<maxSendKbps>0</maxSendKbps>
<rescanIntervalS>60</rescanIntervalS>
<reconnectionIntervalS>10</reconnectionIntervalS>
<maxChangeKbps>10000</maxChangeKbps>
<startBrowser>false</startBrowser>
<upnpEnabled>false</upnpEnabled>
</options>
</configuration>

View File

@@ -0,0 +1,23 @@
// 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
import (
"bytes"
"testing"
"github.com/syncthing/syncthing/internal/auto"
)
func TestAssets(t *testing.T) {
assets := auto.Assets()
idx, ok := assets["index.html"]
if !ok {
t.Fatal("No index.html in compiled in assets")
}
if !bytes.Contains(idx, []byte("<html")) {
t.Fatal("No html in index.html")
}
}

View File

@@ -2,6 +2,5 @@
// 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
// Package auto contains auto generated files for web assets.
package auto

221
internal/auto/gui.files.go Normal file
View File

File diff suppressed because one or more lines are too long

View File

@@ -11,11 +11,6 @@ type recv struct {
src net.Addr
}
type dst struct {
intf string
conn *net.UDPConn
}
type Interface interface {
Send(data []byte)
Recv() ([]byte, net.Addr)

View File

@@ -9,7 +9,6 @@ import "net"
type Broadcast struct {
conn *net.UDPConn
port int
conns []dst
inbox chan []byte
outbox chan recv
}

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