Compare commits

..

118 Commits

Author SHA1 Message Date
Jakob Borg
e91f9a944e Revert "Update bootstrap" (fixes #1121)
This reverts commit 51cdd38c3e.

Conflicts:
	internal/auto/gui.files.go
2014-12-18 22:32:03 +01:00
Jakob Borg
e7ddc7cf0f ... also in index.html 2014-12-18 22:02:45 +01:00
Jakob Borg
40dfa48756 Rebuild assets 2014-12-18 22:01:38 +01:00
Jakob Borg
579f92cf5f Merge branch 'pr-1115'
* pr-1115:
  Make progress indicators less animated
  put legend above list of needed files
2014-12-18 22:01:27 +01:00
Jakob Borg
4565125da9 Add Cathryne 2014-12-18 21:59:54 +01:00
Jakob Borg
ce13a01e65 Clarify authorship requirements in contribution guidelines 2014-12-18 21:56:52 +01:00
Jakob Borg
618a8682b7 golint style tweaks 2014-12-16 23:33:56 +01:00
Jakob Borg
963077f918 Translation update 2014-12-16 23:20:59 +01:00
Jakob Borg
3704d2d86b Don't exit after creating HTTPS certs (fixes #1103) 2014-12-16 22:55:44 +01:00
Jakob Borg
fc6a029311 gofmt 2014-12-16 22:40:04 +01:00
Jakob Borg
7c7b1e6c2d Merge branch 'update-bootstrap'
* update-bootstrap:
  Fix checkbox breakage in Settings dialog
  Update bootstrap
2014-12-15 09:13:05 +01:00
Jakob Borg
892920039d Fix checkbox breakage in Settings dialog 2014-12-15 09:12:59 +01:00
Jakob Borg
51cdd38c3e Update bootstrap 2014-12-15 08:54:29 +01:00
Jakob Borg
80977bd4c0 Make progress indicators less animated 2014-12-15 00:34:03 +01:00
Cathryne
d8022f94ef put legend above list of needed files 2014-12-13 18:33:20 +01:00
Jakob Borg
1c43587d7d Patch Go for issue #9102 in build env (fixes #1112) 2014-12-13 10:38:05 +01:00
Jakob Borg
b2ed32b118 Command -generate should work on non-existent dir 2014-12-12 21:39:03 +01:00
Jakob Borg
0cc815d816 Need config available for -reset (fixes #1111) 2014-12-12 21:29:57 +01:00
Jakob Borg
d452b7593f Merge branch 'pr-1094'
* pr-1094:
  GUI tweaks for last file synced
  Display last received file and time (fixes #292, fixes #801)
2014-12-12 14:25:12 +01:00
Jakob Borg
5346bdc683 GUI tweaks for last file synced 2014-12-12 14:24:36 +01:00
Jakob Borg
dc5c1e2002 Use Go 1.4 for builds 2014-12-11 12:48:40 +01:00
Jakob Borg
2e48e298a2 Merge pull request #1107 from AudriusButkevicius/cleanup
Remove temporaries during scan (fixes #1092)
2014-12-10 09:19:34 +01:00
Audrius Butkevicius
7a1aaaf5c4 Remove temporaries during scan (fixes #1092) 2014-12-09 23:58:58 +00:00
Audrius Butkevicius
bde92d5cfe Display last received file and time (fixes #292, fixes #801) 2014-12-09 20:24:48 +00:00
Audrius Butkevicius
691f0f4845 Merge pull request #1102 from syncthing/gui-poodle
Protect GUI HTTPS from some attacks
2014-12-09 09:52:21 +00:00
Jakob Borg
fdd458d2fe Protect GUI HTTPS from some attacks
- Disable SSLv3 against POODLE
 - Disable RC4 as a weak cipher
 - Set the CommonName to the system host name
2014-12-09 10:49:58 +01:00
Jakob Borg
d2c0b8374a Fix integration tests for Windows native 2014-12-08 22:15:10 +01:00
Jakob Borg
c96c78892d Include error in randomness failure panic 2014-12-08 19:40:38 +01:00
Jakob Borg
957643f523 crypto/rand.Reader may not return all entropy immediately 2014-12-08 19:36:08 +01:00
Audrius Butkevicius
749bbec566 Merge pull request #1099 from syncthing/vet-and-lint
Various changes for vet and lint
2014-12-08 17:08:18 +00:00
Jakob Borg
25e363c5fb Style tweaks and some *IDG->IGD in UPnP code 2014-12-08 17:07:55 +01:00
Jakob Borg
febeed3277 config.ConfigWrapper -> config.Wrapper 2014-12-08 16:39:11 +01:00
Jakob Borg
9d07aa006d Various style fixes 2014-12-08 16:36:15 +01:00
Jakob Borg
12d69e25dd Fixes for go vet 2014-12-08 16:19:08 +01:00
Jakob Borg
0c9f1efc75 Run vet and lint during build 2014-12-08 16:12:53 +01:00
Jakob Borg
665b4506e7 Correct check-contrib.sh 2014-12-08 15:44:55 +01:00
Jakob Borg
cb5548ceb8 Fit better in with Jenkins 2014-12-08 15:42:53 +01:00
Jakob Borg
c9492e54f7 Copyright notice 2014-12-08 15:42:53 +01:00
Jakob Borg
12490eafff Script to fail build on missing authors and copyrights 2014-12-08 15:42:53 +01:00
Jakob Borg
6e83d11d5f Translation update 2014-12-08 13:25:27 +01:00
Jakob Borg
9c6aedc91b Merge remote-tracking branch 'origin/pr/1097'
* origin/pr/1097:
  Revert "Cache file descriptors" (fixes #1096)
2014-12-08 13:23:06 +01:00
Audrius Butkevicius
a9339d0627 Revert "Cache file descriptors" (fixes #1096)
This reverts commit 992ad97ad5.

Causes issues on Windows which uses file locking.
Meaning we cannot archive or modify the file while it's open.
2014-12-08 11:56:14 +00:00
Jakob Borg
4d9aa10532 Merge pull request #1093 from syncthing/random
Refactor random string stuff and seeding
2014-12-08 09:40:30 +01:00
Audrius Butkevicius
b00264b594 Copy compression setting while introducing 2014-12-07 22:43:30 +00:00
Jakob Borg
e329c7015e Refactor random string stuff and seeding
Make sure we have a good random seed on the default RNG, that the
predictable RNG is clearly marked as such, that random strings are
actually the length requested, and that they contain a restricted set of
characters only.
2014-12-07 16:47:24 +01:00
Jakob Borg
1392cfc72d Actually commit and use new random UR ID 2014-12-07 15:49:17 +01:00
Jakob Borg
c6688d8f89 Include ref#, show author nickname in release notes 2014-12-07 12:52:18 +01:00
Jakob Borg
87abea0ba3 Script for generating the change log 2014-12-07 09:07:13 +01:00
Jakob Borg
f9fcb44f3c Translation update 2014-12-07 08:30:54 +01:00
Jakob Borg
996cbbca38 Merge remote-tracking branch 'origin/pr/1091'
* origin/pr/1091:
  Escape plus sign (fixes #1090)
2014-12-07 08:05:21 +01:00
Jakob Borg
581f4b89bd Merge remote-tracking branch 'origin/pr/977'
* origin/pr/977:
  Cache file descriptors
2014-12-07 08:03:34 +01:00
Audrius Butkevicius
88a347dce0 Escape plus sign (fixes #1090) 2014-12-07 00:10:32 +00:00
Audrius Butkevicius
3e7b197a1d Merge pull request #1074 from syncthing/fix-1071
Handle symlinks in versioning (fixes #1071)
2014-12-06 20:19:25 +00:00
Jakob Borg
94ab06e92f Handle symlinks, Staggered versioner (fixes #1071) 2014-12-06 15:20:35 +01:00
Jakob Borg
d38c81fcff Handle broken symlinks, Simple versioner (fixes #1071) 2014-12-06 15:20:35 +01:00
Jakob Borg
3e26fdfb67 Run filetype and symlink integration tests with versioning (ref #1071) 2014-12-06 15:20:35 +01:00
Jakob Borg
e1be73232d Merge remote-tracking branch 'origin/pr/1086'
* origin/pr/1086:
  Enable URL lang parameter for switching languages (fixes #1080)
2014-12-06 14:41:41 +01:00
Jakob Borg
06fd2268d9 Use Go 1.4 'generate' to create XDR codec 2014-12-06 14:23:10 +01:00
Jakob Borg
15251dfae1 Update calmh/xdr 2014-12-06 14:20:49 +01:00
Dennis Wilson
f62812a8dc Enable URL lang parameter for switching languages (fixes #1080) 2014-12-06 14:11:42 +01:00
Jakob Borg
c6041d2590 Skip dotfiles when generating assets 2014-12-06 13:46:02 +01:00
Jakob Borg
c7e779107c Staggered versioning should use current time for filename tag (fixes #994) 2014-12-06 13:26:46 +01:00
Jakob Borg
7cd25c919f Asset rebuild 2014-12-06 12:36:35 +01:00
Jakob Borg
47d67d3985 Merge remote-tracking branch 'origin/pr/1087'
* origin/pr/1087:
  Folder/device panel header with progress indicator
2014-12-06 12:32:43 +01:00
Jakob Borg
1a7921b46c Merge remote-tracking branch 'origin/pr/1083'
* origin/pr/1083:
  Select repos to share with in node editor dialog (fixes #719)
2014-12-06 12:30:46 +01:00
Jakob Borg
43d569741b Merge remote-tracking branch 'origin/pr/1081'
* origin/pr/1081:
  Revisit -no-console option for Windows
2014-12-06 12:17:46 +01:00
Jakob Borg
52c6869eab Merge remote-tracking branch 'origin/pr/1082'
* origin/pr/1082:
  Scrap IsSymlink for native support on Go 1.4
2014-12-06 12:12:53 +01:00
Jakob Borg
6dff9097a2 Use Go 1.4 build environment (currently 1.4rc2) 2014-12-06 12:12:33 +01:00
Ben Schulz
4ff211662a Folder/device panel header with progress indicator 2014-12-05 18:44:38 +01:00
Audrius Butkevicius
05eab51a0d Select repos to share with in node editor dialog (fixes #719) 2014-12-05 00:22:16 +00:00
Audrius Butkevicius
604a4e7dbc Scrap IsSymlink for native support on Go 1.4
Obviously needs Go 1.4 to go back in.

I am still open to doing fix-up's on rescan interval on Windows, which
would still allow getting rid of all the Windows code.

Frankly, we could just defer creations of links (like we defer deletions of files)
in hopes that the target gets created, and if it doesn't, well tough luck, you'll
get a file symlink.

To be honest, nobody would even notice this 'issue' as I am sure nobody on
Windows uses symlinks.

But at the same time, this ugly code is hidden away in some creppy file in
it's own module far far away, and the interface that it exports is fine'ish,
so I wouldn't mind keeping it as it is.
2014-12-04 23:02:57 +00:00
Audrius Butkevicius
80dca96ee8 Revisit -no-console option for Windows
The reason for ShowWindow opose to your FreeConsole is because if you start up
cmd.exe and do syncthing.exe -no-output it actually hides the existing cmd.exe
window oppose to opening a separate window and then hiding it, which keeps the
existing console hanging on syncthing.exe running.

I tried playing around with compiling as GUI, then given the option is not present
allocating a console, and redirecting the std streams to the new console, but that
seems ugly as I'd have to make quite a few calls. But that does get of the initial
flash.
2014-12-04 21:59:40 +00:00
Jakob Borg
7f97037190 Revert "Merge pull request #1078 from syncthing/go1.4"
This reverts commit b658afd857, reversing
changes made to 591c5dabf4.
2014-12-04 22:24:49 +01:00
Jakob Borg
b658afd857 Merge pull request #1078 from syncthing/go1.4
Use Go 1.4 build env
2014-12-04 20:45:48 +01:00
Audrius Butkevicius
992ad97ad5 Cache file descriptors 2014-12-04 16:18:47 +00:00
Jakob Borg
5af6cbae2c Verify Windows support, report appropriate errors when unsupported 2014-12-04 12:10:25 +01:00
Jakob Borg
2abe792f36 Use same order of parameters as os.Symlink 2014-12-04 11:53:55 +01:00
Jakob Borg
1ff9bb8fdc Remove Windows specific implementation 2014-12-04 06:59:30 +01:00
Jakob Borg
5cb1039daf Use Go 1.4 build environment (currently 1.4rc2) 2014-12-04 06:59:13 +01:00
Jakob Borg
591c5dabf4 Merge pull request #1077 from AudriusButkevicius/round
Avoid rounding errors (fixes #1068)
2014-12-04 05:52:29 +01:00
Audrius Butkevicius
770fff287e Avoid rounding errors (fixes #1068) 2014-12-03 23:44:39 +00:00
Jakob Borg
cea7a179ae Utility to print all info we know about a path 2014-12-03 11:32:10 +01:00
Audrius Butkevicius
d80c40cfbf Merge pull request #1072 from syncthing/rewrite-ignore-cache
Rewrite ignore cache
2014-12-03 09:50:05 +00:00
Jakob Borg
12e83374e9 Verify that a symlink can be removed 2014-12-03 09:05:01 +01:00
Jakob Borg
98344d2e5e Rewrite ignores to fix data race, use fewer maps 2014-12-03 08:39:59 +01:00
Jakob Borg
99dc1eec50 Map is a reference type, does not need * here 2014-12-03 08:39:59 +01:00
Jakob Borg
2a886576a6 Fix announce timers on Solaris (and others, given the right timing) (...)
In the successfull case, we start the timer with NewTimer(0), then do a
bunch of stuff during which time it can fire, then reset it with
Reset(0). The result is that two timer firings are queued when we enter
the select loop, so we do two announcements back to back and fail the
tests.
2014-12-03 08:36:45 +01:00
Jakob Borg
919d005550 Print detected data races to stdout instead of hiding in a file 2014-12-03 07:47:40 +01:00
Jakob Borg
97abdaca5a Merge pull request #1070 from AudriusButkevicius/staggered
Use unique versions in staggered versioner (fixes #1063)
2014-12-02 23:27:01 +01:00
Jakob Borg
9cc8b7c858 Simple smoke test for parallell scans 2014-12-02 22:13:08 +01:00
Jakob Borg
0726472b91 Update test configs to v7 2014-12-02 22:05:15 +01:00
Audrius Butkevicius
3cbe92d797 Use unique versions in staggered versioner (fixes #1063) 2014-12-02 19:04:12 +00:00
Jakob Borg
72a278c9ed Merge pull request #1065 from syncthing/coc
Add Code of Conduct
2014-12-02 16:22:13 +01:00
Jakob Borg
e567c8adce Merge pull request #1064 from syncthing/contributors
Clarify/formalize contribution policy and commit access
2014-12-02 16:21:36 +01:00
Jakob Borg
dde8045109 Add Code of Conduct 2014-12-02 15:57:31 +01:00
Jakob Borg
c922c4c383 Clarify/formalize contribution policy and commit access 2014-12-02 15:55:45 +01:00
Audrius Butkevicius
bc8907e90d Check if announcement data is available 2014-12-01 19:53:13 +00:00
Jakob Borg
a8ba7786ae Reinstate 'Shared With' until a better alternative emerges (ref #1054) 2014-12-01 20:50:27 +01:00
Jakob Borg
c734e48ad0 Merge pull request #1052 from AudriusButkevicius/disco4
Change to URL based announce server addresses (fixes #943)
2014-12-01 17:53:48 +01:00
Audrius Butkevicius
d30d0b29a9 Fix CSS 2014-12-01 10:30:38 +00:00
Audrius Butkevicius
2912defb97 Add tests for new discovery 2014-12-01 10:30:25 +00:00
Audrius Butkevicius
69f8ac6b56 Change to URL based announce server addresses (fixes #943) 2014-12-01 10:30:25 +00:00
Jakob Borg
e7441ff6e8 DisableSymlinks -> !SymlinksEnabled 2014-12-01 11:27:07 +01:00
Jakob Borg
8a34158fa4 Merge pull request #1053 from AudriusButkevicius/symdis
Add option to disable symlinks (fixes #1017)
2014-12-01 11:22:04 +01:00
Jakob Borg
8d2a6d96f2 Shorter Global Discovery label 2014-12-01 11:14:11 +01:00
Jakob Borg
bb50b677c7 Merge pull request #1037 from snnd/locale-service
Added Locale Service. Minor Controller Refactoring.
2014-12-01 10:38:44 +01:00
Jakob Borg
0fde4b3b2e Use runtime info to determine ARM version for upgrade (fixes #1051) 2014-12-01 10:24:13 +01:00
Dennis Wilson
ee9c109f07 add locale service to GUI. minor cleanup of controller. 2014-12-01 10:00:03 +01:00
Jakob Borg
1219423091 Revert "Figure out GOARM without being told (ref #1051)"
This reverts commit 2d7b0cf94d.

GOARM is not actually embedded and printed by "go env"
2014-12-01 09:39:57 +01:00
Jakob Borg
cf00ab854f Translation update (fixes #1054) 2014-12-01 09:13:58 +01:00
Jakob Borg
c417dcb7e2 Repair Rescan button, cleanup CSS (fixes #1054) 2014-12-01 09:11:16 +01:00
Audrius Butkevicius
7ad711f554 Add option to disable symlinks (fixes #1017) 2014-11-30 22:10:32 +00:00
Jakob Borg
2d7b0cf94d Figure out GOARM without being told (ref #1051) 2014-11-30 21:46:00 +01:00
Jakob Borg
d669c07e8a Increase allowed test runtimes (fixes #1049) 2014-11-30 21:21:37 +01:00
Jakob Borg
8bd52946b4 Merge pull request #1048 from asdil12/goarm
Directly accept GOARM env var for ARM version
2014-11-30 20:57:49 +01:00
Jakob Borg
27e81637be Add asdil12 2014-11-30 20:57:34 +01:00
Jakob Borg
5c67e27a30 Use CSS column layouts in About box 2014-11-30 20:49:49 +01:00
Dominik Heidler
59af9809fe Directly accept GOARM env var for ARM version
As GOARCH defaults to 'arm' on arm systems this allows packagers to
specify the arm version by setting the GOARM env var to 5, 6 or 7.
2014-11-30 17:08:43 +01:00
99 changed files with 3141 additions and 1244 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ bin
perfstats*.csv
coverage.xml
!gui/scripts/syncthing
.DS_Store

1
.mailmap Symbolic link
View File

@@ -0,0 +1 @@
NICKS

View File

@@ -5,12 +5,15 @@ 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> <frioux@gmail.com>
Ben Schulz <ueomkail@gmail.com> <uok@users.noreply.github.com>
Ben Sidhom <bsidhom@gmail.com>
Brandon Philips <brandon@ifup.org>
Caleb Callaway <enlightened.despot@gmail.com>
Cathryne Linenweaver <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
Chris Joel <chris@scriptolo.gy>
Daniel Martí <mvdan@mvdan.cc>
Dennis Wilson <dw@risu.io>
Dominik Heidler <dominik@heidler.eu>
Emil Hessman <emil@hessman.se>
Felix Ableitner <me@nutomic.com>
Felix Unterpaintner <bigbear2nd@gmail.com>

92
CONDUCT.md Normal file
View File

@@ -0,0 +1,92 @@
## Conduct
* We are committed to providing a friendly, safe and welcoming
environment for all, regardless of gender, sexual orientation,
disability, ethnicity, religion, or similar personal characteristic.
* On IRC, please avoid using overtly sexual nicknames or other nicknames
that might detract from a friendly, safe and welcoming environment for
all.
* Please be kind and courteous. There's no need to be mean or rude.
* Respect that people have differences of opinion and that every design
or implementation choice carries a trade-off and numerous costs. There
is seldom a right answer.
* Please keep unstructured critique to a minimum. If you have solid
ideas you want to experiment with, make a fork and see how it works.
* We will exclude you from interaction if you insult, demean or harass
anyone. That is not welcome behaviour. We interpret the term
"harassment" as including the definition in the <a
href="http://citizencodeofconduct.org/">Citizen Code of Conduct</a>;
if you have any lack of clarity about what might be included in that
concept, please read their definition. In particular, we don't
tolerate behavior that excludes people in socially marginalized
groups.
* Private harassment is also unacceptable. No matter who you are, if you
feel you have been or are being harassed or made uncomfortable by a
community member, please contact one of the channel ops or any of the
Syncthing core team immediately. Whether you're a regular contributor
or a newcomer, we care about making this community a safe place for
you and we've got your back.
* Likewise any spamming, trolling, flaming, baiting or other
attention-stealing behaviour is not welcome.
## Moderation
These are the policies for upholding our community's standards of
conduct in our communication channels, most notably in Syncthing-related
IRC channels and on the web forum.
1. Remarks that violate the Syncthing standards of conduct, including
hateful, hurtful, oppressive, or exclusionary remarks, are not
allowed. (Cursing is allowed, but never targeting another user, and
never in a hateful manner.)
2. Remarks that moderators find inappropriate, whether listed in the
code of conduct or not, are also not allowed.
3. Moderators will first respond to such remarks with a warning.
4. If the warning is unheeded, the user will be "kicked," i.e., kicked
out of the communication channel to cool off.
5. If the user comes back and continues to make trouble, they will be
banned, i.e., indefinitely excluded.
6. Moderators may choose at their discretion to un-ban the user if it
was a first offense and they offer the offended party a genuine
apology.
7. If a moderator bans someone and you think it was unjustified, please
take it up with that moderator, or with a different moderator, **in
private**. Complaints about bans in-channel are not allowed.
8. Moderators are held to a higher standard than other community
members. If a moderator creates an inappropriate situation, they
should expect less leeway than others.
In the Syncthing community we strive to go the extra step to look out
for each other. Don't just aim to be technically unimpeachable, try to
be your best self. In particular, avoid flirting with offensive or
sensitive issues, particularly if they're off-topic; this all too
often leads to unnecessary fights, hurt feelings, and damaged trust;
worse, it can drive people away from the community entirely.
And if someone takes issue with something you said or did, resist the
urge to be defensive. Just stop doing what it was they complained about
and apologize. Even if you feel you were misinterpreted or unfairly
accused, chances are good there was something you could've communicated
better — remember that it's your responsibility to make your fellow
community members comfortable. Everyone wants to get along and we are
all here first and foremost because we want to talk about cool
technology. You will find that people will be eager to assume good
intent and forgive as long as you earn their trust.
*Adapted from the [Rust Code of Conduct](https://github.com/rust-lang/rust/wiki/Note-development-policy#conduct)*
*Adapted from the [Node.js Policy on Trolling](http://blog.izs.me/post/30036893703/policy-on-trolling)*

View File

@@ -37,6 +37,36 @@ where to start, any open issues are fair game! Be prepared for a
all in the name of quality. :) Following the points below will make this
a smoother process.
Individuals making significant and valuable contributions are given
commit-access to the project. If you make a significant contribution and
are not considered for commit-access, please contact any of the
Syncthing core team members.
All nontrivial contributions should go through the pull request
mechanism for internal review. Determining what is "nontrivial" is left
at the discretion of the contributor.
### Authorship
All code authors are listed in the AUTHORS file. Commits must be made
with the same name and email as listed in the AUTHORS file. To
accomplish this, ensure that your git configuration is set correctly
prior to making your first commit;
$ git config --global user.name "Jane Doe"
$ git config --global user.email janedoe@example.com
You must be reachable on the given email address. If you do not wish to
use your real name for whatever reason, using a nickname or pseudonym is
perfectly acceptable.
### Core Team
The Syncthing core team currently consists of the following members;
- Jakob Borg (@calmh)
- Audrius Butkevicius (@AudriusButkevicius)
## Coding Style
- Follow the conventions laid out in [Effective Go](https://golang.org/doc/effective_go.html)
@@ -59,7 +89,7 @@ a smoother process.
feature should probably be a single commit based on the current
`master` branch. You may be asked to "rebase" or "squash" your pull
request to make sure this is the case, especially if there have been
amendments during review.
amendments during review.
## Licensing

2
Godeps/Godeps.json generated
View File

@@ -23,7 +23,7 @@
},
{
"ImportPath": "github.com/calmh/xdr",
"Rev": "ec3d404f43731551258977b38dd72cf557d00398"
"Rev": "45c46b7db7ff83b8b9ee09bbd95f36ab50043ece"
},
{
"ImportPath": "github.com/juju/ratelimit",

View File

@@ -11,6 +11,8 @@ import (
"go/format"
"go/parser"
"go/token"
"io"
"log"
"os"
"regexp"
"strconv"
@@ -269,7 +271,7 @@ func handleStruct(t *ast.StructType) []fieldInfo {
return fs
}
func generateCode(s structInfo) {
func generateCode(output io.Writer, s structInfo) {
name := s.Name
fs := s.Fields
@@ -286,7 +288,7 @@ func generateCode(s structInfo) {
if err != nil {
panic(err)
}
fmt.Println(string(bs))
fmt.Fprintln(output, string(bs))
}
func uncamelize(s string) string {
@@ -295,16 +297,16 @@ func uncamelize(s string) string {
})
}
func generateDiagram(s structInfo) {
func generateDiagram(output io.Writer, s structInfo) {
sn := s.Name
fs := s.Fields
fmt.Println(sn + " Structure:")
fmt.Println()
fmt.Println(" 0 1 2 3")
fmt.Println(" 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1")
fmt.Fprintln(output, sn+" Structure:")
fmt.Fprintln(output)
fmt.Fprintln(output, " 0 1 2 3")
fmt.Fprintln(output, " 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1")
line := "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"
fmt.Println(line)
fmt.Fprintln(output, line)
for _, f := range fs {
tn := f.FieldType
@@ -312,52 +314,52 @@ func generateDiagram(s structInfo) {
name := uncamelize(f.Name)
if sl {
fmt.Printf("| %s |\n", center("Number of "+name, 61))
fmt.Println(line)
fmt.Fprintf(output, "| %s |\n", center("Number of "+name, 61))
fmt.Fprintln(output, line)
}
switch tn {
case "bool":
fmt.Printf("| %s |V|\n", center(name+" (V=0 or 1)", 59))
fmt.Println(line)
fmt.Fprintf(output, "| %s |V|\n", center(name+" (V=0 or 1)", 59))
fmt.Fprintln(output, line)
case "uint16":
fmt.Printf("| %s | %s |\n", center("0x0000", 29), center(name, 29))
fmt.Println(line)
fmt.Fprintf(output, "| %s | %s |\n", center("0x0000", 29), center(name, 29))
fmt.Fprintln(output, line)
case "uint32":
fmt.Printf("| %s |\n", center(name, 61))
fmt.Println(line)
fmt.Fprintf(output, "| %s |\n", center(name, 61))
fmt.Fprintln(output, line)
case "int64", "uint64":
fmt.Printf("| %-61s |\n", "")
fmt.Printf("+ %s +\n", center(name+" (64 bits)", 61))
fmt.Printf("| %-61s |\n", "")
fmt.Println(line)
fmt.Fprintf(output, "| %-61s |\n", "")
fmt.Fprintf(output, "+ %s +\n", center(name+" (64 bits)", 61))
fmt.Fprintf(output, "| %-61s |\n", "")
fmt.Fprintln(output, line)
case "string", "byte": // XXX We assume slice of byte!
fmt.Printf("| %s |\n", center("Length of "+name, 61))
fmt.Println(line)
fmt.Printf("/ %61s /\n", "")
fmt.Printf("\\ %s \\\n", center(name+" (variable length)", 61))
fmt.Printf("/ %61s /\n", "")
fmt.Println(line)
fmt.Fprintf(output, "| %s |\n", center("Length of "+name, 61))
fmt.Fprintln(output, line)
fmt.Fprintf(output, "/ %61s /\n", "")
fmt.Fprintf(output, "\\ %s \\\n", center(name+" (variable length)", 61))
fmt.Fprintf(output, "/ %61s /\n", "")
fmt.Fprintln(output, line)
default:
if sl {
tn = "Zero or more " + tn + " Structures"
fmt.Printf("/ %s /\n", center("", 61))
fmt.Printf("\\ %s \\\n", center(tn, 61))
fmt.Printf("/ %s /\n", center("", 61))
fmt.Fprintf(output, "/ %s /\n", center("", 61))
fmt.Fprintf(output, "\\ %s \\\n", center(tn, 61))
fmt.Fprintf(output, "/ %s /\n", center("", 61))
} else {
fmt.Printf("| %s |\n", center(tn, 61))
fmt.Fprintf(output, "| %s |\n", center(tn, 61))
}
fmt.Println(line)
fmt.Fprintln(output, line)
}
}
fmt.Println()
fmt.Println()
fmt.Fprintln(output)
fmt.Fprintln(output)
}
func generateXdr(s structInfo) {
func generateXdr(output io.Writer, s structInfo) {
sn := s.Name
fs := s.Fields
fmt.Printf("struct %s {\n", sn)
fmt.Fprintf(output, "struct %s {\n", sn)
for _, f := range fs {
tn := f.FieldType
@@ -373,21 +375,21 @@ func generateXdr(s structInfo) {
switch tn {
case "uint16", "uint32":
fmt.Printf("\tunsigned int %s%s;\n", fn, suf)
fmt.Fprintf(output, "\tunsigned int %s%s;\n", fn, suf)
case "int64":
fmt.Printf("\thyper %s%s;\n", fn, suf)
fmt.Fprintf(output, "\thyper %s%s;\n", fn, suf)
case "uint64":
fmt.Printf("\tunsigned hyper %s%s;\n", fn, suf)
fmt.Fprintf(output, "\tunsigned hyper %s%s;\n", fn, suf)
case "string":
fmt.Printf("\tstring %s<%s>;\n", fn, l)
fmt.Fprintf(output, "\tstring %s<%s>;\n", fn, l)
case "byte":
fmt.Printf("\topaque %s<%s>;\n", fn, l)
fmt.Fprintf(output, "\topaque %s<%s>;\n", fn, l)
default:
fmt.Printf("\t%s %s%s;\n", tn, fn, suf)
fmt.Fprintf(output, "\t%s %s%s;\n", tn, fn, suf)
}
}
fmt.Println("}")
fmt.Println()
fmt.Fprintln(output, "}")
fmt.Fprintln(output)
}
func center(s string, w int) string {
@@ -418,25 +420,35 @@ func inspector(structs *[]structInfo) func(ast.Node) bool {
}
func main() {
outputFile := flag.String("o", "", "Output file, blank for stdout")
flag.Parse()
fname := flag.Arg(0)
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, fname, nil, parser.ParseComments)
if err != nil {
panic(err)
log.Fatal(err)
}
var structs []structInfo
i := inspector(&structs)
ast.Inspect(f, i)
headerTpl.Execute(os.Stdout, map[string]string{"Package": f.Name.Name})
var output io.Writer = os.Stdout
if *outputFile != "" {
fd, err := os.Create(*outputFile)
if err != nil {
log.Fatal(err)
}
output = fd
}
headerTpl.Execute(output, map[string]string{"Package": f.Name.Name})
for _, s := range structs {
fmt.Printf("\n/*\n\n")
generateDiagram(s)
generateXdr(s)
fmt.Printf("*/\n")
generateCode(s)
fmt.Fprintf(output, "\n/*\n\n")
generateDiagram(output, s)
generateXdr(output, s)
fmt.Fprintf(output, "*/\n")
generateCode(output, s)
}
}

34
NICKS Normal file
View File

@@ -0,0 +1,34 @@
# This file maps email addresses used in commits to nicks used the changelog.
AudriusButkevicius <audrius.butkevicius@gmail.com>
KayoticSully <kayoticsully@gmail.com>
Nutomic <me@nutomic.com>
Vilbrekin <vilbrekin@gmail.com>
Zillode <zillode@zillode.be>
alex2108 <register-github@alex-graf.de>
andrew-d <andrew@du.nham.ca>
asdil12 <dominik@heidler.eu>
bigbear2nd <bigbear2nd@gmail.com>
bsidhom <bsidhom@gmail.com>
calmh <jakob@nym.se>
cdata <chris@scriptolo.gy>
ceh <emil@hessman.se>
cqcallaw <enlightened.despot@gmail.com>
filoozoom <philippe@schommers.be>
frioux <frew@afoolishmanifesto.com> <frioux@gmail.com>
gillisig <gilli@vx.is>
jedie <github.com@jensdiemer.de> <git@jensdiemer.de>
jpjp <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
kozec <kozec@kozec.com>
marcindziadus <dziadus.marcin@gmail.com>
mvdan <mvdan@mvdan.cc>
philips <brandon@ifup.org>
piobpl <piotrb10@gmail.com>
pluby <phill.luby@newredo.com>
pyfisch <pyfisch@gmail.com>
qbit <qbit@deftly.net>
seehuhn <voss@seehuhn.de>
snnd <dw@risu.io>
tojrobinson <tully@tojr.org>
uok <ueomkail@gmail.com> <uok@users.noreply.github.com>
veeti <veeti.paananen@rojekti.fi>

View File

@@ -72,11 +72,8 @@ func main() {
flag.Parse()
switch goarch {
case "386", "amd64", "armv5", "armv6", "armv7":
case "386", "amd64", "arm", "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)
}
@@ -155,7 +152,7 @@ func checkRequiredGoVersion() {
// This is a standard go build. Verify that it's new enough.
f, err := strconv.ParseFloat(vs, 64)
if err != nil {
log.Printf("*** Could parse Go version out of %q.\n*** This isn't known to work, proceed on your own risk.", vs)
log.Printf("*** Couldn't parse Go version out of %q.\n*** This isn't known to work, proceed on your own risk.", vs)
return
}
if f < minGoVersion {
@@ -175,7 +172,7 @@ func setup() {
func test(pkg string) {
setBuildEnv()
runPrint("go", "test", "-short", "-timeout", "10s", pkg)
runPrint("go", "test", "-short", "-timeout", "60s", pkg)
}
func install(pkg string, tags []string) {
@@ -263,7 +260,7 @@ func listFiles(dir string) []string {
func setBuildEnv() {
os.Setenv("GOOS", goos)
if strings.HasPrefix(goarch, "arm") {
if strings.HasPrefix(goarch, "armv") {
os.Setenv("GOARCH", "arm")
os.Setenv("GOARM", goarch[4:])
} else {
@@ -287,9 +284,7 @@ func assets() {
}
func xdr() {
for _, f := range []string{"internal/discover/packets", "internal/files/leveldb", "internal/protocol/message"} {
runPipe(f+"_xdr.go", "go", "run", "./Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go", "--", f+".go")
}
runPrint("go", "generate", "./internal/discover", "./internal/files", "./internal/protocol")
}
func translate() {
@@ -328,9 +323,6 @@ func ldflags() string {
b.WriteString(fmt.Sprintf(" -X main.BuildUser %s", buildUser()))
b.WriteString(fmt.Sprintf(" -X main.BuildHost %s", buildHost()))
b.WriteString(fmt.Sprintf(" -X main.BuildEnv %s", buildEnvironment()))
if strings.HasPrefix(goarch, "arm") {
b.WriteString(fmt.Sprintf(" -X main.GoArchExtra %s", goarch[3:]))
}
return b.String()
}

View File

@@ -2,7 +2,7 @@
set -euo pipefail
IFS=$'\n\t'
DOCKERIMGV=1.3.3-4
DOCKERIMGV=1.4-4
case "${1:-default}" in
default)
@@ -113,7 +113,11 @@ case "${1:-default}" in
-v $(pwd):/go/src/github.com/syncthing/syncthing \
-w /go/src/github.com/syncthing/syncthing \
syncthing/build:$DOCKERIMGV \
sh -c './build.sh clean && ./build.sh all && STTRACE=all ./build.sh test-cov'
sh -c './build.sh clean \
&& go vet ./cmd/... ./internal/... \
&& ( golint ./cmd/... ; golint ./internal/... ) | egrep -v "comment on exported|should have comment" \
&& ./build.sh all \
&& STTRACE=all ./build.sh test-cov'
;;
docker-test)
@@ -128,7 +132,7 @@ case "${1:-default}" in
&& go run build.go -race \
&& export GOPATH=$(pwd)/Godeps/_workspace:$GOPATH \
&& cd test \
&& go test -tags integration -v -timeout 30m -short'
&& go test -tags integration -v -timeout 60m -short'
;;
*)

9
changelog.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
since="$1"
if [[ -z $since ]] ; then
since="$(git describe --abbrev=0 HEAD^).."
fi
git log --pretty=format:'* %s (%h, @%aN)' "$since" | egrep 'fixes #\d|ref #\d'

View File

@@ -1,7 +1,7 @@
#!/bin/bash
missing-authors() {
for email in $(git log --format=%ae master | sort | uniq) ; do
for email in $(git log --format=%ae HEAD | sort | uniq) ; do
grep -q "$email" AUTHORS || echo $email
done
}
@@ -13,7 +13,9 @@ no-docs-typos() {
grep -v f2459ef3319b2f060dbcdacd0c35a1788a94b8bd |\
grep -v b61f418bf2d1f7d5a9d7088a20a2a448e5e66801 |\
grep -v f0621207e3953711f9ab86d99724f1d0faac45b1 |\
grep -v f1120d7aa936c0658429edef0037792520b46334
grep -v f1120d7aa936c0658429edef0037792520b46334 |\
grep -v a9339d0627fff439879d157c75077f02c9fac61b |\
grep -v 254c63763a3ad42fd82259f1767db526cff94a14
}
print-missing-authors() {
@@ -23,22 +25,24 @@ print-missing-authors() {
}
print-missing-copyright() {
find . -name \*.go | xargs grep -L 'Copyright (C)' | grep -v Godeps
find . -name \*.go | xargs egrep -L 'Copyright \(C\)|automatically generated' | grep -v Godeps | grep -v internal/auto/
}
print-line-blame() {
for f in $(find . -name \*.go | grep -v Godep) gui/app.js gui/index.html ; do
git blame --line-porcelain $f | grep author-mail
done | sort | uniq -c | sort -n
}
echo Author emails missing in AUTHORS file:
print-missing-authors
echo
authors=$(print-missing-authors)
if [[ ! -z $authors ]] ; then
echo '***'
echo Author emails not in AUTHORS:
echo $authors
echo '***'
exit 1
fi
echo Files missing copyright notice:
print-missing-copyright
echo
echo Blame lines per author:
print-line-blame
copy=$(print-missing-copyright)
if [[ ! -z $copy ]] ; then
echo ***
echo Files missing copyright notice:
echo $copy
echo ***
exit 1
fi

View File

@@ -26,6 +26,7 @@ import (
"io"
"os"
"path/filepath"
"strings"
"text/template"
)
@@ -65,6 +66,11 @@ func walkerFor(basePath string) filepath.WalkFunc {
return err
}
if strings.HasPrefix(filepath.Base(name), ".") {
// Skip dotfiles
return nil
}
if info.Mode().IsRegular() {
fd, err := os.Open(name)
if err != nil {

89
cmd/stfileinfo/main.go Normal file
View File

@@ -0,0 +1,89 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"flag"
"log"
"os"
"path/filepath"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/scanner"
)
func main() {
log.SetFlags(0)
log.SetOutput(os.Stdout)
standardBlocks := flag.Bool("s", false, "Use standard block size")
flag.Parse()
path := flag.Arg(0)
if path == "" {
log.Fatal("Need one argument: path to check")
}
log.Println("File:")
log.Println(" ", filepath.Clean(path))
log.Println()
fi, err := os.Lstat(path)
if err != nil {
log.Fatal(err)
}
log.Println("Lstat:")
log.Printf(" Size: %d bytes", fi.Size())
log.Printf(" Mode: 0%o", fi.Mode())
log.Printf(" Time: %v (%d)", fi.ModTime(), fi.ModTime().Unix())
log.Println()
if !fi.Mode().IsDir() && !fi.Mode().IsRegular() {
fi, err = os.Stat(path)
if err != nil {
log.Fatal(err)
}
log.Println("Stat:")
log.Printf(" Size: %d bytes", fi.Size())
log.Printf(" Mode: 0%o", fi.Mode())
log.Printf(" Time: %v (%d)", fi.ModTime(), fi.ModTime().Unix())
log.Println()
}
if fi.Mode().IsRegular() {
log.Println("Blocks:")
fd, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
blockSize := int(fi.Size())
if *standardBlocks || blockSize < protocol.BlockSize {
blockSize = protocol.BlockSize
}
bs, err := scanner.Blocks(fd, blockSize, fi.Size())
if err != nil {
log.Fatal(err)
}
for _, b := range bs {
log.Println(" ", b)
}
}
}

View File

@@ -70,7 +70,16 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
if err != nil {
l.Infoln("Loading HTTPS certificate:", err)
l.Infoln("Creating new HTTPS certificate")
newCertificate(confDir, "https-")
// When generating the HTTPS certificate, use the system host name per
// default. If that isn't available, use the "syncthing" default.
var name string
name, err = os.Hostname()
if err != nil {
name = tlsDefaultCommonName
}
newCertificate(confDir, "https-", name)
cert, err = loadCert(confDir, "https-")
}
if err != nil {
@@ -78,7 +87,20 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
}
tlsCfg := &tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: "syncthing",
MinVersion: tls.VersionTLS10, // No SSLv3
CipherSuites: []uint16{
// No RC4
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
},
}
rawListener, err := net.Listen("tcp", cfg.Address)
@@ -108,6 +130,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
getRestMux.HandleFunc("/rest/version", restGetVersion)
getRestMux.HandleFunc("/rest/stats/device", withModel(m, restGetDeviceStats))
getRestMux.HandleFunc("/rest/stats/folder", withModel(m, restGetFolderStats))
// Debug endpoints, not for general use
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
@@ -322,6 +345,12 @@ func restGetDeviceStats(m *model.Model, w http.ResponseWriter, r *http.Request)
json.NewEncoder(w).Encode(res)
}
func restGetFolderStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
var res = m.FolderStatistics()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
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.Raw())
@@ -334,44 +363,44 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
l.Warnln("decoding posted config:", err)
http.Error(w, err.Error(), 500)
return
} else {
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)
}
}
}
// Start or stop usage reporting as appropriate
if curAcc := cfg.Options().URAccepted; newCfg.Options.URAccepted > curAcc {
// UR was enabled
newCfg.Options.URAccepted = usageReportVersion
newCfg.Options.URUniqueID = randomString(6)
err := sendUsageReport(m)
if err != nil {
l.Infoln("Usage report:", err)
}
go usageReportingLoop(m)
} else if newCfg.Options.URAccepted < curAcc {
// UR was disabled
newCfg.Options.URAccepted = -1
newCfg.Options.URUniqueID = ""
stopUsageReporting()
}
// Activate and save
configInSync = !config.ChangeRequiresRestart(cfg.Raw(), newCfg)
cfg.Replace(newCfg)
cfg.Save()
}
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
}
newCfg.GUI.Password = string(hash)
}
}
// Start or stop usage reporting as appropriate
if curAcc := cfg.Options().URAccepted; newCfg.Options.URAccepted > curAcc {
// UR was enabled
newCfg.Options.URAccepted = usageReportVersion
newCfg.Options.URUniqueID = randomString(8)
err := sendUsageReport(m)
if err != nil {
l.Infoln("Usage report:", err)
}
go usageReportingLoop(m)
} else if newCfg.Options.URAccepted < curAcc {
// UR was disabled
newCfg.Options.URAccepted = -1
newCfg.Options.URUniqueID = ""
stopUsageReporting()
}
// Activate and save
configInSync = !config.ChangeRequiresRestart(cfg.Raw(), newCfg)
cfg.Replace(newCfg)
cfg.Save()
}
func restGetConfigInSync(w http.ResponseWriter, r *http.Request) {
@@ -598,7 +627,7 @@ func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
}
if upgrade.CompareVersions(rel.Tag, Version) == 1 {
err = upgrade.UpgradeTo(rel, GoArchExtra)
err = upgrade.To(rel)
if err != nil {
l.Warnln("upgrading:", err)
http.Error(w, err.Error(), 500)

View File

@@ -17,8 +17,6 @@ package main
import (
"bufio"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"os"
@@ -88,7 +86,7 @@ func validCsrfToken(token string) bool {
}
func newCsrfToken() string {
token := randomString(30)
token := randomString(32)
csrfMut.Lock()
csrfTokens = append(csrfTokens, token)
@@ -140,13 +138,3 @@ 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)
}

View File

@@ -21,7 +21,6 @@ import (
"fmt"
"io"
"log"
"math/rand"
"net"
"net/http"
_ "net/http/pprof"
@@ -45,6 +44,7 @@ import (
"github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/symlinks"
"github.com/syncthing/syncthing/internal/upgrade"
"github.com/syncthing/syncthing/internal/upnp"
"github.com/syndtr/goleveldb/leveldb"
@@ -62,7 +62,6 @@ var (
IsRelease bool
IsBeta bool
LongVersion string
GoArchExtra string // "", "v5", "v6", "v7"
)
const (
@@ -103,10 +102,10 @@ func init() {
}
var (
cfg *config.ConfigWrapper
cfg *config.Wrapper
myID protocol.DeviceID
confDir string
logFlags int = log.Ltime
logFlags = log.Ltime
writeRateLimit *ratelimit.Bucket
readRateLimit *ratelimit.Bucket
stop = make(chan int)
@@ -177,10 +176,6 @@ are mostly useful for developers. Use with care.
available CPU cores.`
)
func init() {
rand.Seed(time.Now().UnixNano())
}
// Command line and environment options
var (
reset bool
@@ -188,6 +183,7 @@ var (
doUpgrade bool
doUpgradeCheck bool
noBrowser bool
noConsole bool
generateDir string
logFile string
noRestart = os.Getenv("STNORESTART") != ""
@@ -214,6 +210,9 @@ func main() {
logFile = filepath.Join(defConfDir, "syncthing.log")
flag.StringVar(&logFile, "logfile", logFile, "Log file name (blank for stdout)")
// We also add an option to hide the console window
flag.BoolVar(&noConsole, "no-console", false, "Hide console window")
}
flag.StringVar(&generateDir, "generate", "", "Generate key and config in specified dir, then exit")
@@ -232,6 +231,10 @@ func main() {
flag.Usage = usageFor(flag.CommandLine, usage, fmt.Sprintf(extraUsage, defConfDir))
flag.Parse()
if noConsole {
osutil.HideConsole()
}
if confDir == "" {
// Not set as default above because the string can be really long.
confDir = defConfDir
@@ -258,19 +261,22 @@ func main() {
}
info, err := os.Stat(dir)
if err != nil {
l.Fatalln("generate:", err)
}
if !info.IsDir() {
if err == nil && !info.IsDir() {
l.Fatalln(dir, "is not a directory")
}
if err != nil && os.IsNotExist(err) {
err = os.MkdirAll(dir, 0700)
if err != nil {
l.Fatalln("generate:", err)
}
}
cert, err := loadCert(dir, "")
if err == nil {
l.Warnln("Key exists; will not overwrite.")
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
} else {
newCertificate(dir, "")
newCertificate(dir, "", tlsDefaultCommonName)
cert, err = loadCert(dir, "")
myID = protocol.NewDeviceID(cert.Certificate[0])
if err != nil {
@@ -329,15 +335,14 @@ func main() {
l.Fatalln("Cannot upgrade, database seems to be locked. Is another copy of Syncthing already running?")
}
err = upgrade.UpgradeTo(rel, GoArchExtra)
err = upgrade.To(rel)
if err != nil {
l.Fatalln("Upgrade:", err) // exits 1
}
l.Okf("Upgraded to %q", rel.Tag)
return
} else {
return
}
return
}
if reset {
@@ -368,13 +373,17 @@ func syncthingMain() {
// Ensure that that we have a certificate and key.
cert, err = loadCert(confDir, "")
if err != nil {
newCertificate(confDir, "")
newCertificate(confDir, "", tlsDefaultCommonName)
cert, err = loadCert(confDir, "")
if err != nil {
l.Fatalln("load cert:", err)
}
}
// We reinitialize the predictable RNG with our device ID, to get a
// sequence that is always the same but unique to this syncthing instance.
predictableRandom.Seed(seedFromBytes(cert.Certificate[0]))
myID = protocol.NewDeviceID(cert.Certificate[0])
l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5]))
@@ -458,6 +467,10 @@ func syncthingMain() {
opts := cfg.Options()
if !opts.SymlinksEnabled {
symlinks.Supported = false
}
if opts.MaxSendKbps > 0 {
writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*opts.MaxSendKbps), int64(5*1000*opts.MaxSendKbps))
}
@@ -562,7 +575,9 @@ func syncthingMain() {
if opts.URUniqueID == "" {
// Previously the ID was generated from the node ID. We now need
// to generate a new one.
opts.URUniqueID = randomString(6)
opts.URUniqueID = randomString(8)
cfg.SetOptions(opts)
cfg.Save()
}
go usageReportingLoop(m)
go func() {
@@ -597,7 +612,7 @@ func syncthingMain() {
os.Exit(code)
}
func setupGUI(cfg *config.ConfigWrapper, m *model.Model) {
func setupGUI(cfg *config.Wrapper, m *model.Model) {
opts := cfg.Options()
guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey)
@@ -638,7 +653,7 @@ func setupGUI(cfg *config.ConfigWrapper, m *model.Model) {
}
}
func sanityCheckFolders(cfg *config.ConfigWrapper, m *model.Model) {
func sanityCheckFolders(cfg *config.Wrapper, m *model.Model) {
nextFolder:
for id, folder := range cfg.Folders() {
if folder.Invalid != "" {
@@ -743,7 +758,7 @@ func setupUPnP() {
if len(igds) > 0 {
// Configure the first discovered IGD only. This is a work-around until we have a better mechanism
// for handling multiple IGDs, which will require changes to the global discovery service
igd = igds[0]
igd = &igds[0]
externalPort = setupExternalPort(igd, port)
if externalPort == 0 {
@@ -767,11 +782,8 @@ func setupExternalPort(igd *upnp.IGD, port int) int {
return 0
}
// We seed the random number generator with the node ID to get a
// repeatable sequence of random external ports.
rnd := rand.NewSource(certSeed(cert.Certificate[0]))
for i := 0; i < 10; i++ {
r := 1024 + int(rnd.Int63()%(65535-1024))
r := 1024 + predictableRandom.Intn(65535-1024)
err := igd.AddPortMapping(upnp.TCP, r, port, "syncthing", cfg.Options().UPnPLease*60)
if err == nil {
return r
@@ -794,7 +806,7 @@ func renewUPnP(port int) {
if len(igds) > 0 {
// Configure the first discovered IGD only. This is a work-around until we have a better mechanism
// for handling multiple IGDs, which will require changes to the global discovery service
igd = igds[0]
igd = &igds[0]
} else {
if debugNet {
l.Debugln("Failed to discover IGD during UPnP port mapping renewal.")
@@ -837,6 +849,17 @@ func renewUPnP(port int) {
}
func resetFolders() {
confDir, err := osutil.ExpandTilde(confDir)
if err != nil {
log.Fatal(err)
}
cfgFile := filepath.Join(confDir, "config.xml")
cfg, err := config.Load(cfgFile, myID)
if err != nil {
log.Fatal(err)
}
suffix := fmt.Sprintf(".syncthing-reset-%d", time.Now().UnixNano())
for _, folder := range cfg.Folders() {
if _, err := os.Stat(folder.Path); err == nil {
@@ -900,7 +923,7 @@ next:
// the certificate and used another name.
certName := deviceCfg.CertName
if certName == "" {
certName = "syncthing"
certName = tlsDefaultCommonName
}
err := remoteCert.VerifyHostname(certName)
if err != nil {
@@ -914,12 +937,12 @@ next:
// If rate limiting is set, we wrap the connection in a
// limiter.
var wr io.Writer = conn
wr := io.Writer(conn)
if writeRateLimit != nil {
wr = &limitedWriter{conn, writeRateLimit}
}
var rd io.Reader = conn
rd := io.Reader(conn)
if readRateLimit != nil {
rd = &limitedReader{conn, readRateLimit}
}
@@ -992,7 +1015,7 @@ func listenTLS(conns chan *tls.Conn, addr string, tlsCfg *tls.Config) {
}
func dialTLS(m *model.Model, conns chan *tls.Conn, tlsCfg *tls.Config) {
var delay time.Duration = 1 * time.Second
delay := time.Second
for {
nextDevice:
for deviceID, deviceCfg := range cfg.Devices() {
@@ -1131,9 +1154,8 @@ func getDefaultConfDir() (string, error) {
default:
if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" {
return filepath.Join(xdgCfg, "syncthing"), nil
} else {
return osutil.ExpandTilde("~/.config/syncthing")
}
return osutil.ExpandTilde("~/.config/syncthing")
}
}
@@ -1246,7 +1268,7 @@ func autoUpgrade() {
}
l.Infof("Automatic upgrade (current %q < latest %q)", Version, rel.Tag)
err = upgrade.UpgradeTo(rel, GoArchExtra)
err = upgrade.To(rel)
if err != nil {
l.Warnln("Automatic upgrade:", err)
continue

68
cmd/syncthing/random.go Normal file
View File

@@ -0,0 +1,68 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"crypto/md5"
cryptoRand "crypto/rand"
"encoding/binary"
"io"
mathRand "math/rand"
)
// randomCharset contains the characters that can make up a randomString().
const randomCharset = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
// predictableRandom is an RNG that will always have the same sequence. It
// will be seeded with the device ID during startup, so that the sequence is
// predictable but varies between instances.
var predictableRandom = mathRand.New(mathRand.NewSource(42))
func init() {
// The default RNG should be seeded with something good.
mathRand.Seed(randomInt64())
}
// randomString returns a string of random characters (taken from
// randomCharset) of the specified length.
func randomString(l int) string {
bs := make([]byte, l)
for i := range bs {
bs[i] = randomCharset[mathRand.Intn(len(randomCharset))]
}
return string(bs)
}
// randomInt64 returns a strongly random int64, slowly
func randomInt64() int64 {
var bs [8]byte
_, err := io.ReadFull(cryptoRand.Reader, bs[:])
if err != nil {
panic("randomness failure: " + err.Error())
}
return seedFromBytes(bs[:])
}
// seedFromBytes calculates a weak 64 bit hash from the given byte slice,
// suitable for use a predictable random seed.
func seedFromBytes(bs []byte) int64 {
h := md5.New()
h.Write(bs)
s := h.Sum(nil)
// The MD5 hash of the byte slice is 16 bytes long. We interpret it as two
// uint64s and XOR them together.
return int64(binary.BigEndian.Uint64(s[0:]) ^ binary.BigEndian.Uint64(s[8:]))
}

View File

@@ -0,0 +1,80 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import "testing"
func TestPredictableRandom(t *testing.T) {
// predictable random sequence is predictable
e := 3440579354231278675
if v := predictableRandom.Int(); v != e {
t.Errorf("Unexpected random value %d != %d", v, e)
}
}
func TestSeedFromBytes(t *testing.T) {
// should always return the same seed for the same bytes
tcs := []struct {
bs []byte
v int64
}{
{[]byte("hello world"), -3639725434188061933},
{[]byte("hello worlx"), -2539100776074091088},
}
for _, tc := range tcs {
if v := seedFromBytes(tc.bs); v != tc.v {
t.Errorf("Unexpected seed value %d != %d", v, tc.v)
}
}
}
func TestRandomString(t *testing.T) {
for _, l := range []int{0, 1, 2, 3, 4, 8, 42} {
s := randomString(l)
if len(s) != l {
t.Errorf("Incorrect length %d != %d", len(s), l)
}
}
strings := make([]string, 1000)
for i := range strings {
strings[i] = randomString(8)
for j := range strings {
if i == j {
continue
}
if strings[i] == strings[j] {
t.Errorf("Repeated random string %q", strings[i])
}
}
}
}
func TestRandomInt64(t *testing.T) {
ints := make([]int64, 1000)
for i := range ints {
ints[i] = randomInt64()
for j := range ints {
if i == j {
continue
}
if ints[i] == ints[j] {
t.Errorf("Repeated random int64 %d", ints[i])
}
}
}
}

View File

@@ -19,11 +19,9 @@ import (
"bufio"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/binary"
"encoding/pem"
"io"
"math/big"
@@ -35,8 +33,8 @@ import (
)
const (
tlsRSABits = 3072
tlsName = "syncthing"
tlsRSABits = 3072
tlsDefaultCommonName = "syncthing"
)
func loadCert(dir string, prefix string) (tls.Certificate, error) {
@@ -45,15 +43,8 @@ func loadCert(dir string, prefix string) (tls.Certificate, error) {
return tls.LoadX509KeyPair(cf, kf)
}
func certSeed(bs []byte) int64 {
hf := sha256.New()
hf.Write(bs)
id := hf.Sum(nil)
return int64(binary.BigEndian.Uint64(id))
}
func newCertificate(dir string, prefix string) {
l.Infoln("Generating RSA key and certificate...")
func newCertificate(dir, prefix, name string) {
l.Infof("Generating RSA key and certificate for %s...", name)
priv, err := rsa.GenerateKey(rand.Reader, tlsRSABits)
if err != nil {
@@ -66,7 +57,7 @@ func newCertificate(dir string, prefix string) {
template := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(mr.Int63()),
Subject: pkix.Name{
CommonName: tlsName,
CommonName: name,
},
NotBefore: notBefore,
NotAfter: notAfter,

View File

@@ -1,12 +1,12 @@
FROM debian:squeeze
MAINTAINER Jakob Borg <jakob@nym.se>
ENV GOLANG_VERSION 1.3.3
ENV GOLANG_VERSION 1.4
# SCMs for "go get", gcc for cgo
RUN apt-get update && apt-get install -y \
ca-certificates curl gcc libc6-dev make \
bzr git mercurial unzip \
bzr git mercurial unzip patch \
--no-install-recommends \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
@@ -29,7 +29,16 @@ RUN go get github.com/calmh/gonative \
&& rm -rf go \
&& gonative -version $GOLANG_VERSION
# Rebuild the special and missing versions
# Rebuild the special and missing versions, using patches as appropriate
RUN mkdir /tmp/patches
ADD *.patch /tmp/patches/
RUN bash -xec '\
cd /usr/local/go ; \
for patch in /tmp/patches/*.patch ; do \
patch -p0 < "$patch" ; \
done \
'
RUN bash -xec '\
cd /usr/local/go/src; \
@@ -47,10 +56,15 @@ RUN bash -xec '\
# Install packages needed for test coverage
RUN go get github.com/tools/godep \
&& go get code.google.com/p/go.tools/cmd/cover \
&& go get golang.org/x/tools/cmd/cover \
&& go get github.com/axw/gocov/gocov \
&& go get github.com/AlekSi/gocov-xml
# Install tools "go vet" and "golint"
RUN go get golang.org/x/tools/cmd/vet \
&& go get github.com/golang/lint/golint
# Build standard library for race
RUN go install -race std

22
docker/go-9102.patch Normal file
View File

@@ -0,0 +1,22 @@
--- src/syscall/route_openbsd.go.orig Fri Jul 25 23:38:47 2014
+++ src/syscall/route_openbsd.go Fri Jul 25 23:39:20 2014
@@ -12,16 +12,16 @@ func (any *anyMessage) toRoutingMessage(b []byte) Rout
switch any.Type {
case RTM_ADD, RTM_DELETE, RTM_CHANGE, RTM_GET, RTM_LOSING, RTM_REDIRECT, RTM_MISS, RTM_LOCK, RTM_RESOLVE:
p := (*RouteMessage)(unsafe.Pointer(any))
- return &RouteMessage{Header: p.Header, Data: b[SizeofRtMsghdr:any.Msglen]}
+ return &RouteMessage{Header: p.Header, Data: b[p.Header.Hdrlen:any.Msglen]}
case RTM_IFINFO:
p := (*InterfaceMessage)(unsafe.Pointer(any))
- return &InterfaceMessage{Header: p.Header, Data: b[SizeofIfMsghdr:any.Msglen]}
+ return &InterfaceMessage{Header: p.Header, Data: b[p.Header.Hdrlen:any.Msglen]}
case RTM_IFANNOUNCE:
p := (*InterfaceAnnounceMessage)(unsafe.Pointer(any))
return &InterfaceAnnounceMessage{Header: p.Header}
case RTM_NEWADDR, RTM_DELADDR:
p := (*InterfaceAddrMessage)(unsafe.Pointer(any))
- return &InterfaceAddrMessage{Header: p.Header, Data: b[SizeofIfaMsghdr:any.Msglen]}
+ return &InterfaceAddrMessage{Header: p.Header, Data: b[p.Header.Hdrlen:any.Msglen]}
}
return nil
}

View File

@@ -28,6 +28,19 @@ ul+h5 {
margin-top: 1.5em;
}
.panel-progress {
background: #3498db;
height: 3px;
left: 0;
position: absolute;
top: 0;
display: block;
}
.panel-title {
position: relative;
}
identicon {
display: inline-block;
position: relative;
@@ -41,16 +54,19 @@ identicon {
margin-top: 0px;
}
.popover {
max-width: none;
}
.identicon {
width: 30px;
height: 30px;
width: 20px;
height: 20px;
}
.panel-heading .identicon {
display: block;
position: absolute;
top: -4px;
left: -9px;
top: 1px;
}
.panel-heading {
@@ -61,18 +77,6 @@ identicon {
.identicon rect {
fill: #666;
}
.identicon-success rect {
fill: #2ecc71;
}
.identicon-info rect {
fill: #9b59b6;
}
.identicon-warning rect {
fill: #f1c40f;
}
.identicon-primary rect {
fill: #3498db;
}
.text-monospace {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
@@ -174,3 +178,20 @@ table.table-condensed td {
display: block;
width: 100%;
}
.three-columns {
-webkit-column-count: 3;
-moz-column-count: 3;
column-count: 3;
}
.two-columns {
-webkit-column-count: 2;
-moz-column-count: 2;
column-count: 2;
}
ul.three-columns li, ul.two-columns li {
padding-left: 0.5em;
text-indent: -0.5em;
}

View File

@@ -8,7 +8,7 @@
"Allow Anonymous Usage Reporting?": "Разреши анонимен доклад за ползване на програмата?",
"Anonymous Usage Reporting": "Анонимен Доклад",
"Any devices configured on an introducer device will be added to this device as well.": "Устройства настроени на introducer компютъра също ще бъдат добавени към този компютър.",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "Автоматични ъпдейти",
"Bugs": "Бъгове",
"CPU Utilization": "Натоварване на Процесора",
"Close": "Затвори",
@@ -34,7 +34,7 @@
"File Versioning": "Файлови Версии",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Битовете за права за достъп са игнорирани, когато се проверява за промени. Използвай с файлови системи тип FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Когато syncthing замени или изтрие файл той се премества в .stversions и преименува с дабавени дата и час.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файловете са защитени от промени направени на други устройства, но промени направени на тава устройство ще бъдат синхронизирани до другите устройства.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файловете са защитени от промени направени на други устройства, но промени направени на това устройство ще бъдат синхронизирани с другите устройства.",
"Folder ID": "Идентификатор на папка",
"Folder Master": "Главна папка",
"Folder Path": "Път до папката",
@@ -48,7 +48,7 @@
"Idle": "Без Работа",
"Ignore Patterns": "Шаблони за Игнориране",
"Ignore Permissions": "Игнорирай Права за Достъп",
"Incoming Rate Limit (KiB/s)": "Входящ Лимит на Скороста(KiB/s)",
"Incoming Rate Limit (KiB/s)": "Входящ Лимит на Скоростта (KiB/s)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Обратното на даденото условие (пр. не изключвай)",
"Keep Versions": "Пази Версии",
@@ -85,13 +85,13 @@
"Select the devices to share this folder with.": "Избери устройствата, с които да споделиш тази папка.",
"Settings": "Настройки",
"Share With Devices": "Сподели с устройства",
"Shared With": "Сподел С",
"Shared With": "Споделена със",
"Short identifier for the folder. Must be the same on all cluster devices.": "Кратък идентификатор на папката. Трябва да бъде същият на всички компютри.",
"Show ID": "Покажи Идентификатора",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Покажи вместо идентификатор на устройството в статус на клъстъра. Ще бъде предлагано на други комютри като име по подразбиране.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Покажи вместо идентификатор на устройството в статус на клъстъра. Ще бъде обновено с името по подразбиране изпратено от другия компютър.",
"Shutdown": "Спри Програмата",
"Simple File Versioning": "Просто Файлови Версии",
"Simple File Versioning": "Опростени Файлови Версии",
"Single level wildcard (matches within a directory only)": "Маска на едно ниво (покрива само в папка)",
"Source Code": "Сорс Код",
"Staggered File Versioning": "Наслагващи се Файлови Версии",

View File

@@ -37,7 +37,7 @@
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Soubory jsou chráněny před změnami na ostatních přístrojích, ale změny provedené z tohoto umístění budou rozeslány na zbytek clusteru.",
"Folder ID": "ID adresáře",
"Folder Master": "Master adresář",
"Folder Path": "Cesta adresáře",
"Folder Path": "Cesta k adresáři",
"GUI Authentication Password": "Přihlašovací heslo pro GUI",
"GUI Authentication User": "Přihlašovací jméno pro GUI",
"GUI Listen Addresses": "Adresa naslouchání GUI",
@@ -90,7 +90,7 @@
"Show ID": "Zobrazit ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Zobrazeno místo ID přístroje na náhledu stavu clusteru. Bude odesíláno ostatním přístrojům jako dodatečné výchozí jméno.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Zobrazeno místo ID přístroje na náhledu stavu clusteru. Bude aktualizováno na jméno které přístroj odesílá pokud nebylo vyplněno.",
"Shutdown": "Vypnutí",
"Shutdown": "Vypnout",
"Simple File Versioning": "Jednoduché verze souborů",
"Single level wildcard (matches within a directory only)": "Jednoúrovňový zástupný znak (shody pouze uvnitř adresáře)",
"Source Code": "Zdrojový kód",

View File

@@ -1,14 +1,14 @@
{
"API Key": "API-Key",
"API Key": "API-Schlüssel",
"About": "Über Syncthing",
"Add Device": "Gerät hinzufügen",
"Add Folder": "Verzeichnis hinzufügen",
"Address": "Adresse",
"Addresses": "Adressen",
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsstatistiken erlauben?",
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsberichten erlauben?",
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
"Any devices configured on an introducer device will be added to this device as well.": "Alle Geräte, die beim Verteiler eingetragen sind, werden auch bei diesem Gerät eingetragen",
"Automatic upgrades": "Automatisches Upgrade",
"Automatic upgrades": "Automat. Updates",
"Bugs": "Fehler",
"CPU Utilization": "Prozessorauslastung",
"Close": "Schließen",
@@ -19,13 +19,13 @@
"Delete": "Löschen",
"Device ID": "Geräte ID",
"Device Identification": "Gerät Identifikation",
"Device Name": "Geräte-Name",
"Disconnected": "Verbindung getrennt",
"Device Name": "Gerätename",
"Disconnected": "Getrennt",
"Documentation": "Dokumentation",
"Download Rate": "Downloadgeschwindigkeit",
"Edit": "Einstellungen bearbeiten",
"Edit": "Bearbeiten",
"Edit Device": "Gerät bearbeiten",
"Edit Folder": "Verzeichnis Bearbeiten",
"Edit Folder": "Verzeichnis bearbeiten",
"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.",
@@ -33,11 +33,11 @@
"Error": "Fehler",
"File Versioning": "Dateiversionierung",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Dateizugriffsrechte beim Suchen nach Veränderungen ignorieren. Bei FAT-Dateisystemen verwenden.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Dateien werden, bevor syncthing sie löscht oder ersetzt, als datierte Versionen in einen Ordner names .stversions verschoben.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Dateien werden, bevor Syncthing sie löscht oder ersetzt, als datierte Versionen in einen Ordner names .stversions verschoben.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dateien sind vor Veränderung durch andere Geräte geschützt, auf diesem Gerät durchgeführte Veränderungen werden aber auf den Rest des Verbunds übertragen.",
"Folder ID": "Verzeichnis ID",
"Folder Master": "Keine Veränderungen zulassen",
"Folder Path": "Verzeichnis Pfad",
"Folder Path": "Verzeichnispfad",
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
"GUI Authentication User": "Nutzername für Zugang zur Benutzeroberfläche",
"GUI Listen Addresses": "Adresse(n) für die Benutzeroberfläche",
@@ -81,21 +81,21 @@
"Restart Needed": "Neustart notwendig",
"Restarting": "Wird neu gestartet",
"Save": "Speichern",
"Scanning": "Sucht",
"Select the devices to share this folder with.": "Wähle die Geräte aus, mit denen du dieses Verzeichnis teilen willst.",
"Scanning": "Suche",
"Select the devices to share this folder with.": "Wähle die Geräte aus, mit denen Du dieses Verzeichnis teilen willst.",
"Settings": "Einstellungen",
"Share With Devices": "Teile mit diesen Geräten",
"Shared With": "Geteilt mit",
"Short identifier for the folder. Must be the same on all cluster devices.": "Kurze ID für das Verzeichnis. Muss auf allen Verbunds-Geräten gleich sein.",
"Show ID": "ID anzeigen",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstatt der Geräte-ID im Verbunds-Status angezeigt. Wird als optionaler Standardname an andere Geräte bekannt gegeben.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird anstatt der Geräte-ID im Verbunds-Status angezeigt. Wird auf den Namen aktualisiert, den das Gerät angibt.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstatt der Geräte ID im Verbunds-Status angezeigt. Wird als optionaler Standardname an andere Geräte bekannt gegeben.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird anstatt der Geräte ID im Verbunds-Status angezeigt. Wird auf den Namen aktualisiert, den das Gerät 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",
"Source Code": "Quellcode",
"Staggered File Versioning": "Stufenweise Dateiversionierung",
"Start Browser": "Starte Browser",
"Start Browser": "Browser starten",
"Stopped": "Gestoppt",
"Support / Forum": "Support / Forum",
"Sync Protocol Listen Addresses": "Adresse(n) für das Synchronisierungsprotokoll",
@@ -105,17 +105,17 @@
"Syncthing includes the following software or portions thereof:": "Syncthing enthält die folgende Software oder Teile davon:",
"Syncthing is restarting.": "Syncthing wird neu gestartet",
"Syncthing is upgrading.": "Syncthing wird aktualisiert",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Ihrer Internetverbindung. Versuche erneut...",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Deiner Internetverbindung. Versuche erneut...",
"The aggregated statistics are publicly available at {%url%}.": "Die gesammelten Statistiken sind öffentlich verfügbar unter {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber nicht aktiviert. Syncthing muss neugestartet werden um die neue Konfiguration zu aktivieren.",
"The device ID cannot be blank.": "Die Geräte-ID darf nicht leer sein.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Die hier einzutragende Geräte-ID kann im \"Bearbeiten > Zeige ID\"-Dialog auf dem anderen Gerät gefunden werden. Leerzeichen und Striche sind optional (werden ignoriert).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Der verschlüsselte Benutzungsbericht wird täglich gesendet. Er wird benutzt um Statistiken über verwendete Betriebssysteme, Verzeichnis-Größen und Programm-Versionen zu erstellen. Sollte der Bericht in Zukunft weitere Daten erfassen, wird dieses Fenster erneut angezeigt.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Die eingegebene Geräte-ID scheint nicht gültig zu sein. Es sollte eine 52 oder 56 stellige Zeichenkette aus Buchstaben und Nummern sein. Leerzeichen und Bindestriche sind optional.",
"The folder ID cannot be blank.": "Die Verzeichnis-ID darf nicht leer sein.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis-ID muss eine kurze Kennung (64 Zeichen oder weniger) sein. Sie kann nur aus Buchstaben, Zahlen und den Punkt- (.), Strich- (-), und Unterstrich- (_) Zeichen bestehen.",
"The folder ID must be unique.": "Die Verzeichnis-ID muss eindeutig sein.",
"The folder path cannot be blank.": "Der Verzeichnis-Pfad kann nicht leer sein",
"The device ID cannot be blank.": "Die Geräte ID darf nicht leer sein.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Die hier einzutragende Geräte ID kann im \"Bearbeiten > Zeige ID\"-Dialog auf dem anderen Gerät gefunden werden. Leerzeichen und Bindestriche sind optional (werden ignoriert).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Der verschlüsselte Nutzungsbericht wird täglich gesendet. Er wird benutzt um Statistiken über verwendete Betriebssysteme, Verzeichnis-Größen und Programm-Versionen zu erstellen. Sollte der Bericht in Zukunft weitere Daten erfassen, wird dieses Fenster erneut angezeigt.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Die eingegebene Geräte ID scheint nicht gültig zu sein. Es sollte eine 52 oder 56 stellige Zeichenkette aus Buchstaben und Nummern sein. Leerzeichen und Bindestriche sind optional.",
"The folder ID cannot be blank.": "Die Verzeichnis ID darf nicht leer sein.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis ID muss eine kurze Kennung (64 Zeichen oder weniger) sein. Sie kann nur aus Buchstaben, Zahlen und dem Punkt- (.), Bindestrich- (-), und Unterstrich- (_) Zeichen bestehen.",
"The folder ID must be unique.": "Die Verzeichnis ID muss eindeutig sein.",
"The folder path cannot be blank.": "Der Verzeichnispfad kann nicht leer sein",
"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).",
@@ -125,16 +125,16 @@
"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}}",
"Upgrade To {%version%}": "Update auf {{version}}",
"Upgrading": "Wird aktualisiert",
"Upload Rate": "Uploadgeschwindigkeit",
"Use Compression": "Benutze Komprimierung",
"Use HTTPS for GUI": "Benutze HTTPS für Benutzeroberfläche",
"Use HTTPS for GUI": "HTTPS für Benutzeroberfläche benutzen",
"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 device, keep in mind that this device must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Gerätes, dass dieses Gerät auch auf der Gegenseite hinzugefügt werden muss.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Beim Hinzufügen eines neuen Verzeichnisses, beachte dass die Verzeichnis-ID dazu verwendet wird, Verzeichnisse zwischen Geräten zu verbinden. Die ID muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Beim Hinzufügen eines neuen Verzeichnisses, beachte dass die Verzeichnis ID dazu verwendet wird, Verzeichnisse zwischen Geräten zu verbinden. Die ID muss also auf allen Geräten gleich sein, die 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",

View File

@@ -49,7 +49,6 @@
"Generate": "Generate",
"Global Discovery": "Global Discovery",
"Global Discovery Server": "Global Discovery Server",
"Global Discovery Servers": "Global Discovery Servers",
"Global State": "Global State",
"Idle": "Idle",
"Ignore Patterns": "Ignore Patterns",
@@ -58,6 +57,7 @@
"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 File Synced": "Last File Synced",
"Last seen": "Last seen",
"Latest Release": "Latest Release",
"Legend:": "Legend:",
@@ -91,7 +91,9 @@
"Save": "Save",
"Scanning": "Scanning",
"Select the devices to share this folder with.": "Select the devices to share this folder with.",
"Select the folders to share with this device.": "Select the folders to share with this device.",
"Settings": "Settings",
"Share Folders With Device": "Share Folders With Device",
"Share With Devices": "Share With Devices",
"Shared With": "Shared With",
"Short identifier for the folder. Must be the same on all cluster devices.": "Short identifier for the folder. Must be the same on all cluster devices.",

View File

@@ -0,0 +1,142 @@
{
"API Key": "Clave API",
"About": "Acerca de",
"Add Device": "Agregar dispositivo",
"Add Folder": "Agregar repositorio",
"Address": "Dirección",
"Addresses": "Direcciones",
"Allow Anonymous Usage Reporting?": "Permitir reporte anónimo de uso?",
"Anonymous Usage Reporting": "Reporte anónimo de uso",
"Any devices configured on an introducer device will be added to this device as well.": "Cualquier dispositivo configurado en un dispositivo introductor será también agregado a este dispositivo.",
"Automatic upgrades": "Actualizaciones automáticas",
"Bugs": "Errores",
"CPU Utilization": "Uso de la CPU",
"Close": "Cerrar",
"Comment, when used at the start of a line": "Comentario, cuando es utilizado al inicio de una línea.",
"Compression is recommended in most setups.": "La compresión de datos es recomendada para la mayoría de las configuraciones.",
"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",
"Device ID": "ID del dispositivo",
"Device Identification": "Identificación del dispositivo",
"Device Name": "Nombre del dispositivo",
"Disconnected": "Desconectado",
"Documentation": "Documentación",
"Download Rate": "Tasa de descarga",
"Edit": "Editar",
"Edit Device": "Editar dispositivo",
"Edit Folder": "Editar repositorio",
"Editing": "Editando",
"Enable UPnP": "Permitir UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Ingrese las direcciones \"ip:puerto\" separadas por coma o \"dynamic\" para descubrir automáticamente las direcciones.",
"Enter ignore patterns, one per line.": "Añadir patrones de exclusión, uno por línea.",
"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.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Los archivos son movidos al directorio .stversions y renombrados a versiones marcadas por fecha cuando son reemplazados o eliminados por Syncthing,",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Los archivos están protegidos frente a los cambios realizados en otros dispositivos, peros los cambios realizados en este dispositivo serán envíados al resto del grupo",
"Folder ID": "ID del repositorio",
"Folder Master": "Repositorio maestro",
"Folder Path": "Ruta del repositorio",
"GUI Authentication Password": "Contraseña de autenticación de la GUI",
"GUI Authentication User": "Usuario de la GUI",
"GUI Listen Addresses": "Direcciones de escucha para la GUI.",
"Generate": "Generar",
"Global Discovery": "Búsqueda en internet",
"Global Discovery Server": "Servidor global de identificación",
"Global State": "Estado Global",
"Idle": "Inactivo",
"Ignore Patterns": "Patrones de exclusión",
"Ignore Permissions": "Ignorar permisos",
"Incoming Rate Limit (KiB/s)": "Límite de velocidad de entrada (KiB/s)",
"Introducer": "Introductor",
"Inversion of the given condition (i.e. do not exclude)": "Inversión de la condición dada (es decir, no excluir)",
"Keep Versions": "Conservar versiones",
"Last seen": "Visto por ultima vez",
"Latest Release": "Última versión",
"Local Discovery": "Búsqueda en red local",
"Local State": "Estado Local",
"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",
"Notice": "Aviso",
"OK": "OK",
"Offline": "Fuera de linea",
"Online": "En linea",
"Out Of Sync": "Fuera de sincronización",
"Outgoing Rate Limit (KiB/s)": "Tasa máxima de envío (KiB/s)",
"Override Changes": "Reemplazar los cambios",
"Path to the folder 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. Será creado 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 folder).": "Ruta donde serán guardas las versiones (dejar vacío para usar el directorio predifinido \".stversions\" en el repositorio)",
"Please wait": "Aguarde por favor",
"Preview": "Vista previa",
"Preview Usage Report": "Ver reporte de uso",
"Quick guide to supported patterns": "Guía rápida sobre los patrones soportados",
"RAM Utilization": "Utilización de RAM",
"Rescan": "Reescanear",
"Rescan Interval": "Intervalo de Reescaneo",
"Restart": "Reiniciar",
"Restart Needed": "Es necesario reiniciar",
"Restarting": "Reiniciando",
"Save": "Guardar",
"Scanning": "Actualización",
"Select the devices to share this folder with.": "Seleccione los dispositivos con los cuales compartir este repositorio.",
"Settings": "Configuración",
"Share With Devices": "Compartir con los dispositivos",
"Shared With": "Compartido con",
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador corto para el repositorio. Debe ser el mismo en todos los dispositivos del grupo.",
"Show ID": "Mostrar ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Shown instead of Device ID in the cluster status. Will be updated to the name the device 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",
"Sync Protocol Listen Addresses": "Dirección de escucha del protocolo de sincronización",
"Synchronization": "Sincronización",
"Syncing": "Sincronización",
"Syncthing has been shut down.": "La sincronización esta apagada",
"Syncthing includes the following software or portions thereof:": "Syncthing incluye los siguientes softwares o partes de ellos:",
"Syncthing is restarting.": "Syncthing está reiniciando.",
"Syncthing is upgrading.": "Syncthing se está actualizando.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece estar apagado, o hay un problema con su conexión de Internet. Reintentando...",
"The aggregated statistics are publicly available at {%url%}.": "Las estadísticas acumuladas están públicamente disponibles en {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido guardada pero no activada.\nSyncthing debe reiniciarse para activar la nueva configuración.",
"The device ID cannot be blank.": "El ID del dispositivo no puede estar en blanco.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "La ID del dispositivo a introducir se puede encontrar en la opción de menú \"Edición > Mostrar ID\" en el otro dispositivo. Espacios y guiones son opcionales (ignorados).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "El informe de uso se envía encriptado diariamente. Se utiliza para hacer un seguimiento de plataformas comunes, tamaño de repositorios y versiones de la aplicación. Si el conjunto de datos cambia será notificado mediante este dialogo nuevamente.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "La ID del dispositivo introducida no es válida. Debe ser una cadena de 52 o 56 caracteres consistente en letras y números, con espacios y guiones opcionales.",
"The folder ID cannot be blank.": "La ID del repositorio no puede estar en blanco.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "La ID del repositorio debe ser un identificador corto (64 caracteres o menos) consistente solamente en letras, números, punto (.), guion (-) y guion bajo (_).",
"The folder ID must be unique.": "La ID del repositorio debe ser única.",
"The folder path cannot be blank.": "La ruta del repositorio no puede estar vacía.",
"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 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 rescan interval must be a non-negative number of seconds.": "El intervalo de reescaneo debe ser un número no negativo de segundos.",
"The rescan interval must be at least 5 seconds.": "El intervalo de reescaneo debe ser al menos de 5 segundos.",
"Unknown": "Desconocido",
"Up to Date": "Actualizado",
"Upgrade To {%version%}": "Actualizar a {{version}}",
"Upgrading": "Actualizando",
"Upload Rate": "Tasa de subida",
"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 device, keep in mind that this device must be added on the other side too.": "Al agregar un nuevo dispositivo, tenga en cuenta que este dispositivo se debe agregar en el otro lado también.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Al agregar un nuevo repositorio, tenga en cuenta que la ID del repositorio se utiliza para conectar los repositorios entre dispositivos. Se distingue entre mayúsculas y minúsculas y debe ser exactamente igual en todos los dispositivos.",
"Yes": "Sí",
"You must keep at least one version.": "Debe mantener al menos una versión",
"full documentation": "documentación completa",
"items": "Artículos"
}

View File

@@ -8,7 +8,7 @@
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation ?",
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
"Any devices configured on an introducer device will be added to this device as well.": "Tout appareil ajouté depuis un appareil initiateur sera aussi ajouté sur cet appareil.",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "Mises à jour automatiques",
"Bugs": "Bugs",
"CPU Utilization": "Utilisation du CPU",
"Close": "Fermer",
@@ -17,14 +17,14 @@
"Connection Error": "Erreur de connexion",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants :",
"Delete": "Supprimer",
"Device ID": "ID de l'appareil",
"Device ID": "ID du périphérique",
"Device Identification": "Identification de l'appareil",
"Device Name": "Nom de l'appareil",
"Device Name": "Nom du périphérique",
"Disconnected": "Déconnecté",
"Documentation": "Documentation",
"Download Rate": "Débit de réception",
"Edit": "Éditer",
"Edit Device": "Éditer l'appareil",
"Edit Device": "Éditer le périphérique",
"Edit Folder": "Éditer le répertoire",
"Editing": "Édition",
"Enable UPnP": "Activer l'UPnP",
@@ -84,7 +84,7 @@
"Scanning": "En cours de scan",
"Select the devices to share this folder with.": "Sélectionner les appareils avec qui partager ce répertoire.",
"Settings": "Configuration",
"Share With Devices": "Partager avec les appareils",
"Share With Devices": "Partager avec les périphériques",
"Shared With": "Partagé avec",
"Short identifier for the folder. Must be the same on all cluster devices.": "Court identifiant du répertoire. Il doit être le même sur l'ensemble des appareils du groupe.",
"Show ID": "Montrer l'ID",
@@ -121,7 +121,7 @@
"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 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 rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "L'intervalle de scan ne doit pas être un nombre négatif de secondes.",
"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",

View File

@@ -117,7 +117,7 @@
"The folder ID must be unique.": "Mappe ID må vera unik.",
"The folder path cannot be blank.": "Mappeplasseringa kan ikkje vera tom.",
"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.": "Fylgjande intervall vert brukt: den fyrste timen vert ein versjon lagra kvart 30. sekund, den fyrste dagen vert ein versjon lagra kvar time, dei fyrste 30 dagane vert ein versjon lagra kvar dag, og inntil høgaste levetid vert ein versjon lagra kvar uke.",
"The maximum age must be a number and cannot be blank.": "Maksimal levetid må vera eit tal og kan ikkje vera blankt.",
"The maximum age must be a number and cannot be blank.": "Maksimal levetid må vera eit tal og kan ikkje vera tomt.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimalt tidsrom å behalda ein versjon (i dagar, set til 0 for å behalda versjonar for ubegrensa tid.)",
"The number of old versions to keep, per file.": "Tal på gamle versjonar ein skal behalda, per fil.",
"The number of versions must be a number and cannot be blank.": "Tal på versjonar må vera eit tal og kan ikkje vera tomt.",

View File

@@ -8,7 +8,7 @@
"Allow Anonymous Usage Reporting?": "Zezwalaj na anonimowe statystyki użycia",
"Anonymous Usage Reporting": "Anonimowe statystyki użycia",
"Any devices configured on an introducer device will be added to this device as well.": "Wszystkie urządzenia skonfigurowane na urządzeniu wprowadzającym zostaną dodane także do tego urządzenia.",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "Automatyczne aktualizacje",
"Bugs": "Błędy",
"CPU Utilization": "Użycie CPU",
"Close": "Zamknij",

View File

@@ -8,7 +8,7 @@
"Allow Anonymous Usage Reporting?": "允許匿名的使用資訊回報?",
"Anonymous Usage Reporting": "匿名的使用資訊回報",
"Any devices configured on an introducer device will be added to this device as well.": "任何在引入者裝置所設置的裝置將會一併新增至此裝置",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "自動升級",
"Bugs": "程式錯誤",
"CPU Utilization": "CPU 使用率",
"Close": "關閉",
@@ -121,7 +121,7 @@
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "一個版本被保留的最長時間 (單位為天,若設定為 0 則表示永遠保留)。",
"The number of old versions to keep, per file.": "每個檔案要保留的舊版本數量。",
"The number of versions must be a number and cannot be blank.": "每個檔案要保留的舊版本數量必須是數字且不能為空白。",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "重新掃描間隔必須為一個非負數的秒數。",
"The rescan interval must be at least 5 seconds.": "重新掃描間隔至少須為 5 秒。",
"Unknown": "未知",
"Up to Date": "最新",

View File

@@ -1 +1 @@
var validLangs = ["be","bg","cs","de","en","fr","hu","it","lt","nb","nl","nn","pl","pt-PT","ru","sv","zh-CN","zh-TW"]
var validLangs = ["be","bg","cs","de","en","es","fr","hu","it","lt","nb","nl","nn","pl","pt-PT","ru","sv","zh-CN","zh-TW"]

View File

@@ -91,6 +91,7 @@
<div class="panel-group" id="folders">
<div class="panel panel-default" ng-repeat="folder in folderList()">
<div class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer">
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.ID)}}%"></div>
<h3 class="panel-title">
<span class="glyphicon glyphicon-hdd"></span>&emsp;{{folder.ID}}
<span class="pull-right hidden-xs text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
@@ -148,13 +149,25 @@
<th><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan Interval</span></th>
<td class="text-right">{{folder.RescanIntervalS}} s</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-share-alt"></span>&emsp;<span translate>Shared With</span></th>
<td class="text-right">{{sharesFolder(folder)}}</td>
</tr>
<tr ng-if="folderStats[folder.ID].LastFile">
<th><span class="glyphicon glyphicon-transfer"></span>&emsp;<span translate>Last File Synced</span></th>
<td class="text-right">
<span title="{{folderStats[folder.ID].LastFile.Filename}} @ {{folderStats[folder.ID].LastFile.At | date:'yyyy-MM-dd HH:mm'}}">
{{folderStats[folder.ID].LastFile.Filename | basename}}
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="panel-footer">
<button class="btn btn-sm btn-danger" ng-if="folder.ReadOnly && model[folder.ID].needFiles > 0" ng-click="override(folder.ID)" href=""><span class="glyphicon glyphicon-upload"></span>&emsp;<span translate>Override Changes</span></button>
<span class="pull-right">
<button class="btn btn-sm btn-default" href="" ng-show="folderStatus(folder.ID) == 'idle'" ng-click="rescanFolder(folder.ID)"><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan</span></button>
<button class="btn btn-sm btn-default" href="" ng-show="folderStatus(folder) == 'idle'" ng-click="rescanFolder(folder.ID)"><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan</span></button>
<button class="btn btn-sm btn-default" href="" ng-click="editFolder(folder)"><span class="glyphicon glyphicon-pencil"></span>&emsp;<span translate>Edit</span></button>
</span>
<div class="clearfix"></div>
@@ -201,7 +214,7 @@
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
</tr>
<tr ng-if="system.extAnnounceOK != undefined && announceServersTotal > 0">
<th><span class="glyphicon glyphicon-bullhorn"></span>&emsp;<span translate>Global Discovery Servers</span></th>
<th><span class="glyphicon glyphicon-bullhorn"></span>&emsp;<span translate>Global Discovery</span></th>
<td class="text-right">
<span ng-if="announceServersFailed.length == 0" class="data text-success">
<span>OK</span>
@@ -228,6 +241,7 @@
<div class="panel-group" id="devices">
<div class="panel panel-default" ng-repeat="deviceCfg in otherDevices()">
<div class="panel-heading" data-toggle="collapse" data-parent="#devices" href="#device-{{$index}}" style="cursor: pointer">
<div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.DeviceID]._total | number:0}}%"></div>
<h3 class="panel-title">
<identicon data-value="deviceCfg.DeviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
<span ng-switch="deviceStatus(deviceCfg)" class="pull-right hidden-xs text-{{deviceClass(deviceCfg)}}">
@@ -270,8 +284,8 @@
</tr>
<tr ng-if="!connections[deviceCfg.DeviceID]">
<th><span class="glyphicon glyphicon-eye-open"></span>&emsp;<span translate>Last seen</span></th>
<td translate ng-if="!stats[deviceCfg.DeviceID].LastSeenDays || stats[deviceCfg.DeviceID].LastSeenDays >= 365" class="text-right">Never</td>
<td ng-if="stats[deviceCfg.DeviceID].LastSeenDays < 365" class="text-right">{{stats[deviceCfg.DeviceID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
<td translate ng-if="!deviceStats[deviceCfg.DeviceID].LastSeenDays || deviceStats[deviceCfg.DeviceID].LastSeenDays >= 365" class="text-right">Never</td>
<td ng-if="deviceStats[deviceCfg.DeviceID].LastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.DeviceID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
</tr>
<tr ng-if="deviceFolders(deviceCfg).length > 0">
<th><span class="glyphicon glyphicon-hdd"></span>&emsp;<span translate>Folders</span></th>
@@ -412,6 +426,23 @@
<p translate class="help-block">Any devices configured on an introducer device will be added to this device as well.</p>
</div>
</div>
<div class="row" ng-if="!editingSelf">
<div class="col-md-12">
<div class="form-group">
<label translate for="folders">Share Folders With Device</label>
<p translate class="help-block">Select the folders to share with this device.</p>
<div class="three-columns">
<div class="checkbox" ng-repeat="folder in folderList()">
<label>
<input type="checkbox" ng-model="currentDevice.selectedFolders[folder.ID]"> {{folder.ID}}
</label>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
@@ -537,7 +568,7 @@
<div class="form-group">
<label translate for="devices">Share With Devices</label>
<p translate class="help-block">Select the devices to share this folder with.</p>
<div style="-webkit-column-count: 3; -moz-column-count: 3; column-count: 3; vertical-align: top;">
<div class="three-columns">
<div class="checkbox" ng-repeat="device in otherDevices()">
<label>
<input type="checkbox" ng-model="currentFolder.selectedDevices[device.DeviceID]"> {{deviceName(device)}}
@@ -625,14 +656,14 @@
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Enable UPnP</span> <input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.UPnPEnabled">
<input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.UPnPEnabled"> <span translate>Enable UPnP</span>
</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">
<input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.GlobalAnnEnabled"> <span translate>Global Discovery</span>
</label>
</div>
</div>
@@ -641,14 +672,14 @@
<div class="form-group">
<div class="checkbox">
<label ng-if="upgradeInfo">
<span translate>Automatic upgrades</span> <input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.AutoUpgradeEnabled">
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.AutoUpgradeEnabled"> <span translate>Automatic upgrades</span>
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Local Discovery</span> <input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.LocalAnnEnabled">
<input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.LocalAnnEnabled"> <span translate>Local Discovery</span>
</label>
</div>
</div>
@@ -675,21 +706,21 @@
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Use HTTPS for GUI</span> <input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS">
<input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS"> <span translate>Use HTTPS for GUI</span>
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Start Browser</span> <input id="StartBrowser" type="checkbox" ng-model="tmpOptions.StartBrowser">
<input id="StartBrowser" type="checkbox" ng-model="tmpOptions.StartBrowser"> <span translate>Start Browser</span>
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<span translate>Anonymous Usage Reporting</span> <input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled"> (<a translate ng-click="showURPreview()" href="#">Preview</a>)
<input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled"> <span translate>Anonymous Usage Reporting</span> (<a translate ng-click="showURPreview()" href="#">Preview</a>)
</label>
</div>
</div>
@@ -759,32 +790,34 @@
<!-- Needed files modal -->
<modal id="needed" large="yes" status="info" icon="cloud-download" close="yes" title="Out of Sync Items">
<div class="progress">
<div class="progress-bar progress-bar-success" style="width: 20%"><span translate class="show">Reused</span></div>
<div class="progress-bar" style="width: 20%"><span translate class="show">Copied from original</span></div>
<div class="progress-bar progress-bar-info" style="width: 20%"><span translate class="show">Copied from elsewhere</span></div>
<div class="progress-bar progress-bar-warning" style="width: 20%"><span translate class="show">Downloaded</span></div>
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: 20%"><span translate class="show">Downloading</span></div>
</div>
<hr/>
<table class="table table-striped table-condensed">
<tr ng-repeat="f in needed" ng-init="a = needAction(f)">
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
<td title="{{f.Name}}">{{f.Name | basename}}</td>
<td>
<span ng-if="a == 'sync' && progress[neededFolder] && progress[neededFolder][f.Name]">
<div class="progress progress-striped active">
<div class="progress">
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.Name].Reused}}%"></div>
<div class="progress-bar" style="width: {{progress[neededFolder][f.Name].CopiedFromOrigin}}%"></div>
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.Name].CopiedFromElsewhere}}%"></div>
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.Name].Pulled}}%"></div>
<div class="progress-bar progress-bar-danger" style="width: {{progress[neededFolder][f.Name].Pulling}}%"></div>
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: {{progress[neededFolder][f.Name].Pulling}}%"></div>
<span class="show frontal">{{progress[neededFolder][f.Name].BytesDone | binary}}B / {{progress[neededFolder][f.Name].BytesTotal | binary}}B</span>
</div>
</span>
</td>
</tr>
</table>
<span translate>Legend:</span>
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" style="width: 20%"><span translate class="show">Reused</span></div>
<div class="progress-bar" style="width: 20%"><span translate class="show">Copied from original</span></div>
<div class="progress-bar progress-bar-info" style="width: 20%"><span translate class="show">Copied from elsewhere</span></div>
<div class="progress-bar progress-bar-warning" style="width: 20%"><span translate class="show">Downloaded</span></div>
<div class="progress-bar progress-bar-danger" style="width: 20%"><span translate class="show">Downloading</span></div>
</div>
</modal>
@@ -796,27 +829,26 @@
<p translate>Copyright &copy; 2014 Jakob Borg and the following Contributors:</p>
<div class="row">
<div class="col-md-6">
<ul>
<div class="col-md-12">
<ul class="list-unstyled three-columns">
<li>Aaron Bieber</li>
<li>Andrew Dunham</li>
<li>Alexander Graf</li>
<li>Arthur Axel fREW Schmidt</li>
<li>Audrius Butkevicius</li>
<li>Ben Schulz</li>
<li>Ben Sidhom</li>
<li>Brandon Philips</li>
<li>Caleb Callaway</li>
<li>Cathryne Linenweaver</li>
<li>Chris Joel</li>
<li>Daniel Martí</li>
<li>Dennis Wilson</li>
<li>Dominik Heidler</li>
<li>Emil Hessman</li>
<li>Felix Ableitner</li>
<li>Felix Unterpaintner</li>
<li>Gilli Sigurdsson</li>
</ul>
</div>
<div class="col-md-6">
<ul>
<li>James Patterson</li>
<li>Jens Diemer</li>
<li>Jochen Voss</li>
@@ -837,7 +869,7 @@
<hr/>
<p translate>Syncthing includes the following software or portions thereof:</p>
<ul>
<ul class="list-unstyled two-columns">
<li><a href="http://golang.org/">The Go Programming Language</a>, Copyright &copy; 2012 The Go Authors.</li>
<li><a href="https://bitbucket.org/kardianos/osext">kardianos/osext</a>, Copyright &copy; 2012 Daniel Theophanes.</li>
<li><a href="https://code.google.com/p/snappy-go/">snappy-go</a>, Copyright &copy; 2011 The Snappy-Go Authors.</li>
@@ -871,6 +903,7 @@
<script src="scripts/syncthing/core/filters/basenameFilter.js"></script>
<script src="scripts/syncthing/core/filters/binaryFilter.js"></script>
<script src="scripts/syncthing/core/filters/naturalFilter.js"></script>
<script src="scripts/syncthing/core/services/localeService.js"></script>
<script src="assets/lang/valid-langs.js"></script>
<script src="scripts/syncthing/app.js"></script>

View File

@@ -1,142 +0,0 @@
{
"API Key": "API-sleutel",
"About": "Over",
"Add Device": "Toestel toevoegen",
"Add Folder": "Folder toevoegen",
"Address": "Adres",
"Addresses": "Adressen",
"Allow Anonymous Usage Reporting?": "Bijhouden van anonieme gebruikers statistieken toestaan?",
"Anonymous Usage Reporting": "Bijhouden anonieme gebruikers statistieken",
"Any devices configured on an introducer device will be added to this device as well.": "Toestellen geconfigureerd op een introductie toestel zullen ook aan dit toestel worden toegevoegd.",
"Automatic upgrades": "Automatisch bijwerken",
"Bugs": "Fouten",
"CPU Utilization": "CPU Gebruik",
"Close": "Sluiten",
"Comment, when used at the start of a line": "Commentaar, indien gebruikt aan het begin van de lijn",
"Compression is recommended in most setups.": "Gegevenscompressie is aan te raden in de meeste situaties.",
"Connection Error": "Verbindingsfout",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg en de onderstaande bijdragers:",
"Delete": "Verwijderen",
"Device ID": "Toestel ID",
"Device Identification": "Toestel identificatie",
"Device Name": "Naam toestel",
"Disconnected": "Niet Verbonden",
"Documentation": "Documentatie",
"Download Rate": "Downloadsnelheid",
"Edit": "Bewerk",
"Edit Device": "Toestel aanpassen",
"Edit Folder": "Folder aanpassen",
"Editing": "Bezig met aanpassen",
"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.": "Geef te negeren patronen, één per regel.",
"Error": "Fout",
"File Versioning": "Versiebeheer",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Bestands permissiebits worden genegeerd wanneer naar veranderingen wordt gekeken. Gebruik dit op FAT bestandsystemen",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Bestanden worden naar de .stversions map verplaatst met een tijdsaanduiding, wanneer ze aangepast of verwijderd worden door syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Bestanden zijn beschermt tegen aanpassingen gemaakt door andere toestellen maar aanpassingen op dit toestel worden doorgestuurd naar de rest van de cluster.",
"Folder ID": "Folder ID",
"Folder Master": "Hoofdfolder",
"Folder Path": "Locatie folder",
"GUI Authentication Password": "GUI Authentificatie Wachtwoord",
"GUI Authentication User": "GUI Authentificatie Gebruikersnaam",
"GUI Listen Addresses": "GUI Inkomend adres",
"Generate": "Genereer",
"Global Discovery": "Globaal zoeken",
"Global Discovery Server": "Globale zoekserver",
"Global State": "Globale status",
"Idle": "Inactief",
"Ignore Patterns": "Te negeren patronen",
"Ignore Permissions": "Rechten negeren",
"Incoming Rate Limit (KiB/s)": "Download snelheidslimiet (KiB/s)",
"Introducer": "Introductietoestel",
"Inversion of the given condition (i.e. do not exclude)": "Inversie van de gegeven voorwaarde (bv. niet uitsluiten)",
"Keep Versions": "Versies behouden",
"Last seen": "Laatst gezien op",
"Latest Release": "Laatste uitgave",
"Local Discovery": "Lokaal zoeken",
"Local State": "Lokale status",
"Maximum Age": "Maximum leeftijd",
"Multi level wildcard (matches multiple directory levels)": "Wildcard op meerder niveaus (toepasbaar op meerdere niveaus van folders)",
"Never": "Nooit",
"No": "Nee",
"No File Versioning": "Geen versiebeheer",
"Notice": "Notificatie",
"OK": "OK",
"Offline": "Offline",
"Online": "Online",
"Out Of Sync": "Niet gesynchroniseerd",
"Outgoing Rate Limit (KiB/s)": "Uitgaande snelheidslimiet (KiB/s)",
"Override Changes": "Veranderingen overschrijven",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Locatie van de folder op de lokale computer. Zal aangemaakt worden wanneer deze niet bestaat. De tilde (~) kan gebruikt in plaats van",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Locatie waar de versies opgeslagen moeten worden (leeg laten voor de standaard .stversions subfolder).",
"Please wait": "Even geduld",
"Preview": "Voorbeeld",
"Preview Usage Report": "Bekijk gebruiksstatistieken",
"Quick guide to supported patterns": "Snelgids voor ondersteunde patronen",
"RAM Utilization": "RAM gebruik",
"Rescan": "Opnieuw scannen",
"Rescan Interval": "Scanfrequentie",
"Restart": "Herstart",
"Restart Needed": "Herstart nodig",
"Restarting": "Herstarten",
"Save": "Bewaar",
"Scanning": "Aan het zoeken",
"Select the devices to share this folder with.": "Selecteer de toestellen om deze folder mee te delen.",
"Settings": "Instellingen",
"Share With Devices": "Delen met toestellen",
"Shared With": "Gedeeld met",
"Short identifier for the folder. Must be the same on all cluster devices.": "Korte aanduiding voor deze folder. Moet dezelfde zijn op alle toestellen in de cluster.",
"Show ID": "Toon ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wordt getoond in plaats van de toestel ID in de cluster staat. Wordt doorgegeven aan andere toestellen as een bijkomende standaard toestelnaam.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wordt getoond in plaats van de toestel ID in de cluster staat. Wanneer leeg wordt deze aangepast met de naam aangekondigd door het toestel.",
"Shutdown": "Sluit af",
"Simple File Versioning": "Eenvoudig versiebeheer",
"Single level wildcard (matches within a directory only)": "Wildcard op enkel niveau (toepasbaar binnen een enkele folder)",
"Source Code": "Broncode",
"Staggered File Versioning": "Gelaagd versiebeheer",
"Start Browser": "Start browser",
"Stopped": "Gestopt",
"Support / Forum": "Support / Forum",
"Sync Protocol Listen Addresses": "Synchronisatie protocol luister adres",
"Synchronization": "Synchronisatie",
"Syncing": "Aan het synchroniseren",
"Syncthing has been shut down.": "Syncthing is afgesloten",
"Syncthing includes the following software or portions thereof:": "De volgende software of delen daarvan zijn onderdeel van syncthing:",
"Syncthing is restarting.": "Syncthing is aan het herstarten.",
"Syncthing is upgrading.": "Syncthing is aan het upgraden.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing lijkt afgesloten te zijn, of er is een verbindingsprobleem met het internet. Nieuwe poging....",
"The aggregated statistics are publicly available at {%url%}.": "The verzamelde statistieken zijn publiek beschikbaar op {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De configuratie is opslagen maar nog niet actief. Syncthing moet opnieuw opgestart worden om de nieuwe configuratie te activeren.",
"The device ID cannot be blank.": "Het toestel ID mag niet leeg zijn.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Het verwachte toestel ID kan teruggevonden worden in het \"Aanpassen > Toon ID\" scherm op het andere toestel. Spaties en streepjes zijn facultatief (worden genegeerd).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Het versleutelde gebruiksrapport wordt dagelijks opgestuurd en wordt gebruikt om de verschillende platformen, folder groottes en versies op te volgen. Als de reeks gegevens wijzigt zal opnieuw toestemming gevraagd worden.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Dit toestel ID lijkt ongeldig. Het toestel ID bestaat uit 52 of 56 letters en nummers met facultatieve spaties en streepjes.",
"The folder ID cannot be blank.": "De folder ID mag niet leeg zijn.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "De folder ID mag maximaal 64 tekens lang zijn en bestaat enkel uit letters, nummers, punten (.), streepjes (-) en onderstrepingstekens (_).",
"The folder ID must be unique.": "De folder ID moet uniek zijn.",
"The folder path cannot be blank.": "De folder locatie mag niet leeg zijn.",
"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 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 rescan interval must be a non-negative number of seconds.": "De scanfrequentie moet een positief getal in seconden zijn.",
"The rescan interval must be at least 5 seconds.": "De scanfrequentie moet minimaal 5 seconden zijn.",
"Unknown": "Onbekend",
"Up to Date": "Gesynchroniseerd",
"Upgrade To {%version%}": "Upgrade naar {{version}}",
"Upgrading": "Bezig met upgrade",
"Upload Rate": "Upload snelheid",
"Use Compression": "Compressie gebruiken",
"Use HTTPS for GUI": "Gebruik HTTPS voor de GUI",
"Version": "Versie",
"Versions Path": "Locatie 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 device, keep in mind that this device must be added on the other side too.": "Onthoud dat een toegevoegd toestel ook aan de andere kant moet worden toegevoegd.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Onthoud, bij het toevoegen van een folder, dat de folder ID gebruikt wordt om folders tussen toestellen te verbinden. Ze zijn hoofdletter gevoelig en moeten exact hetzelfde zijn op de andere toestellen.",
"Yes": "Ja",
"You must keep at least one version.": "Minstens 1 versie moet bewaard blijven.",
"full documentation": "volledige documentatie",
"items": "objecten"
}

View File

@@ -1,142 +0,0 @@
{
"API Key": "Ключ API",
"About": "О программе",
"Add Device": "Добавить устройство",
"Add Folder": "Добавить папку",
"Address": "Адрес",
"Addresses": "Адреса",
"Allow Anonymous Usage Reporting?": "Разрешить сбор анонимной статистики использования?",
"Anonymous Usage Reporting": "Анонимная статистика использования",
"Any devices configured on an introducer device will be added to this device as well.": "Все устройства, подключённые к устройству-рекомендателю, будут добавлены к текущему устройству.",
"Automatic upgrades": "Автообновление",
"Bugs": "Ошибки",
"CPU Utilization": "Загрузка ЦПУ",
"Close": "Закрыть",
"Comment, when used at the start of a line": "Комментарий, если используется в начале строки",
"Compression is recommended in most setups.": "Сжатие рекомендуется в большинстве случаев.",
"Connection Error": "Ошибка подключения",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Все права защищены © 2014 Jakob Borg и следующие участники:",
"Delete": "Удалить",
"Device ID": "ID устройства",
"Device Identification": "Идентификация устройства",
"Device Name": "Имя устройства",
"Disconnected": "Нет соединения",
"Documentation": "Документация",
"Download Rate": "Скорость загрузки",
"Edit": "Изменить",
"Edit Device": "Изменить устройство",
"Edit Folder": "Изменение папки",
"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.": "Введите шаблон игнорирования, по-одному на строку.",
"Error": "Ошибка",
"File Versioning": "Управление версиями",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Права доступа к файлам будут игнорироваться. Используйте на файловых системах типа FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Версии файлов с временнОй меткой перемещаются в директорию .stversions, если они удалены или перемещены в процессе синхронизации.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файлы защищены от изменений сделанных на других устройствах, но изменения сделанные на этом устройстве будут отправлены всему кластеру.",
"Folder ID": "ID папки",
"Folder Master": "Папка-оригинал",
"Folder Path": "Путь к папке",
"GUI Authentication Password": "Пароль для доступа к панели управления",
"GUI Authentication User": "Имя пользователя для доступа к панели управления",
"GUI Listen Addresses": "Адрес панели управления",
"Generate": "Сгенерировать",
"Global Discovery": "Глобальное обнаружение",
"Global Discovery Server": "Сервер глобального обнаружения",
"Global State": "Глобальное состояние",
"Idle": "Бездействует",
"Ignore Patterns": "Шаблоны игнорирования",
"Ignore Permissions": "Игнорировать файловые права доступа",
"Incoming Rate Limit (KiB/s)": "Ограничение входящего потока (Кбит/сек)",
"Introducer": "Рекомендатель",
"Inversion of the given condition (i.e. do not exclude)": "Инвертировать текущее условие (например, исключить)",
"Keep Versions": "Количество хранимых версий",
"Last seen": "Был доступен",
"Latest Release": "Последняя версия",
"Local Discovery": "Локальное обнаружение",
"Local State": "Локальное состояние",
"Maximum Age": "Максимальный срок",
"Multi level wildcard (matches multiple directory levels)": "Многоуровневая маска (поиск совпадений во всех подпапках)",
"Never": "Никогда",
"No": "Нет",
"No File Versioning": "Без управления версиями файлов",
"Notice": "Внимание",
"OK": "ОК",
"Offline": "Оффлайн",
"Online": "Онлайн",
"Out Of Sync": "Не синхронизировано",
"Outgoing Rate Limit (KiB/s)": "Предел скорости отдачи (KiB/s)",
"Override Changes": "Перезаписать изменения",
"Path to the folder 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 folder).": "Путь, где должны храниться версии (оставьте пустым по-умолчанию для папки .stversions в папке).",
"Please wait": "Пожалуйста, подождите",
"Preview": "Предварительный просмотр",
"Preview Usage Report": "Посмотреть отчёт об использовании",
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
"RAM Utilization": "Использование ОЗУ",
"Rescan": "Пересканирование",
"Rescan Interval": "Интервал пересканирования",
"Restart": "Перезапуск",
"Restart Needed": "Требуется перезапуск",
"Restarting": "Перезапуск",
"Save": "Сохранить",
"Scanning": "Сканирование",
"Select the devices to share this folder with.": "Выберите устройства, для которых будет доступна эта папка.",
"Settings": "Настройки",
"Share With Devices": "Предоставить доступ устройствам",
"Shared With": "Доступ предоставлен",
"Short identifier for the folder. Must be the same on all cluster devices.": "Короткий идентификатор папки. Должен быть одинаковым на всех устройствах кластера.",
"Show ID": "Показать ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Отображается вместо ID устройства в статусе группы. Будет разослан другим устройствам в качестве имени по умолчанию.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Отображается вместо ID устройства в статусе группы. Если поле не заполнено, то будет установлено имя, передаваемое этим устройством.",
"Shutdown": "Выключить",
"Simple File Versioning": "Простое управление версиями файлов",
"Single level wildcard (matches within a directory only)": "Одноуровневая маска (поиск совпадений только внутри папки)",
"Source Code": "Исходный код",
"Staggered File Versioning": "Ступенчатое управление версиями файлов",
"Start Browser": "Открыть браузер",
"Stopped": "Остановлено",
"Support / Forum": "Поддержка / Форум",
"Sync Protocol Listen Addresses": "Адрес протокола синхронизации",
"Synchronization": "Синхронизация",
"Syncing": "Синхронизация",
"Syncthing has been shut down.": "Syncthing выключен.",
"Syncthing includes the following software or portions thereof:": "Syncthing включает в себя следующее ПО или его части:",
"Syncthing is restarting.": "Перезапуск Syncthing",
"Syncthing is upgrading.": "Обновление Syncthing ",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",
"The aggregated statistics are publicly available at {%url%}.": "Суммарная статистика общедоступна на {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурация была сохранена но не активирована. Для активации новой конфигурации необходимо рестартовать Syncthing.",
"The device ID cannot be blank.": "ID устройства не может быть пустым.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор устройства для ввода здесь, может быть найден в диалоге \"Редактирование > Показать ID\" на другом устройстве. Пробелы и тире не обязательны (игнорируются).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Зашифрованный отчет об использовании отправляется ежедневно. Это используется для отслеживания общих платформ, размеров папок и версий приложения. Если отчетные данные изменятся, вам будет снова показано это диалоговое окно.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введённое ID устройства не валидное. Оно должно состоять из букв и цифр, может включать пробелы и дефисы, его длина должна быть от 52 до 56 символов, ",
"The folder ID cannot be blank.": "ID папки не может быть пустым.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "ID папки должен быть коротким (не более 64 символов), должен состоять только из букв, цифр, точек (.), дефисов (-) или подчёркиваний (_).",
"The folder ID must be unique.": "ID папки должен быть уникальным.",
"The folder path cannot be blank.": "Путь к папке не должен быть пустым.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Используются следующие интервалы: в первый час версия меняется каждые 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 number of old versions to keep, per file.": "Количество хранимых версий файла.",
"The number of versions must be a number and cannot be blank.": "Количество версий должно быть числом и не может быть пустым.",
"The rescan interval must be a non-negative number of seconds.": "Интервал пересканирования должен быть неотрицательным количеством секунд.",
"The rescan interval must be at least 5 seconds.": "Интервал пересканирования должен быть хотя бы 5 секунд.",
"Unknown": "Неизвестно",
"Up to Date": "Обновлено",
"Upgrade To {%version%}": "Обновить до {{version}}",
"Upgrading": "Обновление",
"Upload Rate": "Скорость отдачи",
"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 device, keep in mind that this device must be added on the other side too.": "Когда добавляете устройство, помните о том, что это же устройство должно быть добавлено и другой стороной.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Когда добавляете новую папку, помните, что ID папки используются для того, чтобы связывать папки между всеми устройствами. Они чувствительны к регистру и должны совпадать на всех используемых устройствах.",
"Yes": "Да",
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
"full documentation": "полная документация",
"items": "элементы"
}

View File

@@ -25,7 +25,7 @@ var syncthing = angular.module('syncthing', [
var urlbase = 'rest';
var guiVersion = null;
syncthing.config(function ($httpProvider, $translateProvider) {
syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvider) {
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token';
$httpProvider.interceptors.push(function () {
@@ -42,11 +42,16 @@ syncthing.config(function ($httpProvider, $translateProvider) {
};
});
// language and localisation
$translateProvider.useStaticFilesLoader({
prefix: 'assets/lang/lang-',
suffix: '.json'
});
LocaleServiceProvider.setAvailableLocales(validLangs);
LocaleServiceProvider.setDefaultLocale('en');
});
// @TODO: extract global level functions into seperate service(s)

View File

@@ -1,13 +1,44 @@
angular.module('syncthing.core')
.controller('SyncthingController', function ($scope, $http, $translate, $location) {
.controller('SyncthingController', function ($scope, $http, $location, LocaleService) {
'use strict';
// private/helper definitions
var prevDate = 0;
var getOK = true;
var navigatingAway = false;
var online = false;
var restarting = false;
function initController() {
LocaleService.autoConfigLocale();
refreshSystem();
refreshConfig();
refreshConnectionStats();
refreshDeviceStats();
refreshFolderStats();
$http.get(urlbase + '/version').success(function (data) {
$scope.version = data.version;
});
$http.get(urlbase + '/report').success(function (data) {
$scope.reportData = data;
});
$http.get(urlbase + '/upgrade').success(function (data) {
$scope.upgradeInfo = data;
}).error(function () {
$scope.upgradeInfo = null;
});
setInterval($scope.refresh, 10000);
}
// pubic/scope definitions
$scope.completion = {};
$scope.config = {};
$scope.configInSync = true;
@@ -22,50 +53,16 @@ angular.module('syncthing.core')
$scope.folders = {};
$scope.seenError = '';
$scope.upgradeInfo = null;
$scope.stats = {};
$scope.deviceStats = {};
$scope.folderStats = {};
$scope.progress = {};
$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
// match only "zh-TW" and not "zh-CN".
var lang, matching;
for (var i = 0; i < langs.length; i++) {
lang = langs[i];
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]);
return;
}
}
// Fallback if nothing matched
$translate.use("en");
});
$(window).bind('beforeunload', function () {
navigatingAway = true;
});
$scope.$on("$locationChangeSuccess", function () {
var lang = $location.search().lang;
if (lang) {
$translate.use(lang);
}
LocaleService.useLocale($location.search().lang);
});
$scope.needActions = {
@@ -87,7 +84,7 @@ angular.module('syncthing.core')
}
console.log('UIOnline');
$scope.init();
initController();
online = true;
restarting = false;
$('#networkError').modal('hide');
@@ -117,6 +114,7 @@ angular.module('syncthing.core')
$scope.$on('LocalIndexUpdated', function (event, arg) {
var data = arg.data;
refreshFolder(data.folder);
refreshFolderStats();
// Update completion status for all devices that we share this folder with.
$scope.folders[data.folder].Devices.forEach(function (deviceCfg) {
@@ -181,40 +179,39 @@ angular.module('syncthing.core')
$scope.$on('DownloadProgress', function (event, arg) {
var stats = arg.data;
var progress = {};
for(var folder in stats){
for (var folder in stats) {
refreshFolder(folder);
progress[folder] = {};
for(var file in stats[folder]){
for (var file in stats[folder]) {
var s = stats[folder][file];
var reused = Math.floor(100 * s.Reused / s.Total);
var copiedFromOrigin = Math.floor(100 * s.CopiedFromOrigin / s.Total);
var copiedFromElsewhere = Math.floor(100 * s.CopiedFromElsewhere / s.Total);
var pulled = Math.floor(100 * s.Pulled / s.Total);
var pulling = Math.floor(100 * s.Pulling / s.Total);
// We can do the following, because if s.Pulling > 0, than reused + copied + pulled < 100 because off rounding them down.
// We do this to show which files are currently being pulled
if (s.Pulling && pulling == 0) {
var reused = 100 * s.Reused / s.Total;
var copiedFromOrigin = 100 * s.CopiedFromOrigin / s.Total;
var copiedFromElsewhere = 100 * s.CopiedFromElsewhere / s.Total;
var pulled = 100 * s.Pulled / s.Total;
var pulling = 100 * s.Pulling / s.Total;
// We try to round up pulling to atleast a percent so that it would be atleast a bit visible.
if (pulling < 1 && pulled + copiedFromElsewhere + copiedFromOrigin + reused <= 99) {
pulling = 1;
}
progress[folder][file] = {
Reused: reused,
CopiedFromOrigin: copiedFromOrigin,
Reused: reused,
CopiedFromOrigin: copiedFromOrigin,
CopiedFromElsewhere: copiedFromElsewhere,
Pulled: pulled,
Pulling: pulling,
BytesTotal: s.BytesTotal,
BytesDone: s.BytesDone,
Pulled: pulled,
Pulling: pulling,
BytesTotal: s.BytesTotal,
BytesDone: s.BytesDone,
};
}
}
for(var folder in $scope.progress){
for (var folder in $scope.progress) {
if (!(folder in progress)) {
refreshFolder(folder);
if ($scope.neededFolder == folder) {
refreshNeed(folder);
}
} else if ($scope.neededFolder == folder) {
for(file in $scope.progress[folder]){
for (file in $scope.progress[folder]) {
if (!(file in progress[folder])) {
refreshNeed(folder);
break;
@@ -273,7 +270,7 @@ angular.module('syncthing.core')
$http.get(urlbase + '/system').success(function (data) {
$scope.myID = data.myID;
$scope.system = data;
$scope.announceServersTotal = Object.keys(data.extAnnounceOK).length;
$scope.announceServersTotal = data.extAnnounceOK ? Object.keys(data.extAnnounceOK).length : 0;
var failed = [];
for (var server in data.extAnnounceOK) {
if (!data.extAnnounceOK[server]) {
@@ -370,35 +367,26 @@ angular.module('syncthing.core')
var refreshDeviceStats = debounce(function () {
$http.get(urlbase + "/stats/device").success(function (data) {
$scope.stats = data;
for (var device in $scope.stats) {
$scope.stats[device].LastSeen = new Date($scope.stats[device].LastSeen);
$scope.stats[device].LastSeenDays = (new Date() - $scope.stats[device].LastSeen) / 1000 / 86400;
$scope.deviceStats = data;
for (var device in $scope.deviceStats) {
$scope.deviceStats[device].LastSeen = new Date($scope.deviceStats[device].LastSeen);
$scope.deviceStats[device].LastSeenDays = (new Date() - $scope.deviceStats[device].LastSeen) / 1000 / 86400;
}
console.log("refreshDeviceStats", data);
});
}, 500);
$scope.init = function () {
refreshSystem();
refreshConfig();
refreshConnectionStats();
refreshDeviceStats();
$http.get(urlbase + '/version').success(function (data) {
$scope.version = data.version;
var refreshFolderStats = debounce(function () {
$http.get(urlbase + "/stats/folder").success(function (data) {
$scope.folderStats = data;
for (var folder in $scope.folderStats) {
if ($scope.folderStats[folder].LastFile) {
$scope.folderStats[folder].LastFile.At = new Date($scope.folderStats[folder].LastFile.At);
}
}
console.log("refreshfolderStats", data);
});
$http.get(urlbase + '/report').success(function (data) {
$scope.reportData = data;
});
$http.get(urlbase + '/upgrade').success(function (data) {
$scope.upgradeInfo = data;
}).error(function () {
$scope.upgradeInfo = null;
});
};
}, 500);
$scope.refresh = function () {
refreshSystem();
@@ -610,7 +598,7 @@ angular.module('syncthing.core')
$scope.config.GUI = angular.copy($scope.tmpGUI);
['ListenAddress', 'GlobalAnnServers'].forEach(function (key) {
$scope.config.Options[key] = $scope.config.Options[key + "Str"].split(/[ ,]+/).map(function (x) {
$scope.config.Options[key] = $scope.config.Options[key + "Str"].split(/[ ,]+/).map(function (x) {
return x.trim();
});
});
@@ -667,6 +655,12 @@ angular.module('syncthing.core')
$scope.editingExisting = true;
$scope.editingSelf = (deviceCfg.DeviceID == $scope.myID);
$scope.currentDevice.AddressesStr = deviceCfg.Addresses.join(', ');
if (!$scope.editingSelf) {
$scope.currentDevice.selectedFolders = {};
$scope.deviceFolders($scope.currentDevice).forEach(function (folder) {
$scope.currentDevice.selectedFolders[folder] = true;
});
}
$scope.deviceEditor.$setPristine();
$('#editDevice').modal();
};
@@ -684,7 +678,8 @@ angular.module('syncthing.core')
$scope.currentDevice = {
AddressesStr: 'dynamic',
Compression: true,
Introducer: false
Introducer: false,
selectedFolders: {}
};
$scope.editingExisting = false;
$scope.editingSelf = false;
@@ -738,6 +733,31 @@ angular.module('syncthing.core')
$scope.devices.sort(deviceCompare);
$scope.config.Devices = $scope.devices;
if (!$scope.editingSelf) {
for (var id in deviceCfg.selectedFolders) {
if (deviceCfg.selectedFolders[id]) {
var found = false;
for (i = 0; i < $scope.folders[id].Devices.length; i++) {
if ($scope.folders[id].Devices[i].DeviceID == deviceCfg.DeviceID) {
found = true;
break;
}
}
if (!found) {
$scope.folders[id].Devices.push({
DeviceID: deviceCfg.DeviceID
});
}
continue
} else {
$scope.folders[id].Devices = $scope.folders[id].Devices.filter(function (n) {
return n.DeviceID != deviceCfg.DeviceID;
});
}
}
}
$scope.saveConfig();
};
@@ -896,17 +916,29 @@ angular.module('syncthing.core')
$scope.saveConfig();
};
$scope.sharesFolder = function (folderCfg) {
var names = [];
folderCfg.Devices.forEach(function (device) {
if (device.DeviceID != $scope.myID) {
names.push($scope.deviceName($scope.findDevice(device.DeviceID)));
}
});
names.sort();
return names.join(", ");
}
$scope.deviceFolders = function (deviceCfg) {
var folders = [];
for (var folderID in $scope.folders) {
var devices = $scope.folders[folderID].Devices
for (var i = 0; i < devices.length; i++) {
if (devices[i].DeviceID == deviceCfg.DeviceID) {
folders.push(folderID)
break
folders.push(folderID);
break;
}
}
};
folders.sort();
return folders;
};
@@ -987,7 +1019,7 @@ angular.module('syncthing.core')
$scope.showNeed = function (folder) {
$scope.neededFolder = folder;
refreshNeed(folder);
$('#needed').modal().on('hidden.bs.modal', function(){
$('#needed').modal().on('hidden.bs.modal', function () {
$scope.neededFolder = undefined;
$scope.needed = undefined;
});
@@ -1024,6 +1056,6 @@ angular.module('syncthing.core')
$http.post(urlbase + "/scan?folder=" + encodeURIComponent(folder));
};
$scope.init();
setInterval($scope.refresh, 10000);
// pseudo main. called on all definitions assigned
initController();
});

View File

@@ -0,0 +1,88 @@
angular.module('syncthing.core')
.provider('LocaleService', function () {
var _defaultLocale,
_availableLocales;
this.setDefaultLocale = function (locale) {
_defaultLocale = locale;
};
this.setAvailableLocales = function (locales) {
_availableLocales = locales;
};
this.$get = ['$http', '$translate', '$location', function ($http, $translate, $location) {
/**
* Requests the server in order to get the browser's requested locale strings.
*
* @returns promise which on success resolves with a locales array
*/
function readBrowserLocales() {
// @TODO: check if there is nice way to utilize window.navigator.languages or similiar api.
return $http.get(urlbase + "/lang");
}
function autoConfigLocale() {
var params = $location.search();
if(params.lang) {
$translate.use(params.lang);
} else {
readBrowserLocales().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
// match only "zh-TW" and not "zh-CN".
var i,
lang,
matching,
locale = _defaultLocale;
for (i = 0; i < langs.length; i++) {
lang = langs[i];
if (lang.length < 2) {
continue;
}
matching = _availableLocales.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) {
locale = matching[0];
break;
}
}
// Fallback if nothing matched
$translate.use(locale);
});
}
}
function useLocale(language) {
// @TODO: eventually check for valid locale format
if (language) {
$translate.use(language);
}
}
return {
autoConfigLocale: autoConfigLocale,
useLocale: useLocale
}
}];
});

View File

File diff suppressed because one or more lines are too long

View File

@@ -34,7 +34,7 @@ import (
var l = logger.DefaultLogger
const CurrentVersion = 6
const CurrentVersion = 7
type Configuration struct {
Version int `xml:"version,attr"`
@@ -92,13 +92,13 @@ func (f *FolderConfiguration) HasMarker() bool {
return true
}
func (r *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
if r.deviceIDs == nil {
for _, n := range r.Devices {
r.deviceIDs = append(r.deviceIDs, n.DeviceID)
func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
if f.deviceIDs == nil {
for _, n := range f.Devices {
f.deviceIDs = append(f.deviceIDs, n.DeviceID)
}
}
return r.deviceIDs
return f.deviceIDs
}
type VersioningConfiguration struct {
@@ -160,7 +160,7 @@ type FolderDeviceConfiguration struct {
type OptionsConfiguration struct {
ListenAddress []string `xml:"listenAddress" default:"0.0.0.0:22000"`
GlobalAnnServers []string `xml:"globalAnnounceServer" default:"announce.syncthing.net:22026"`
GlobalAnnServers []string `xml:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22026"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true"`
LocalAnnPort int `xml:"localAnnouncePort" default:"21025"`
@@ -179,6 +179,7 @@ type OptionsConfiguration struct {
KeepTemporariesH int `xml:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" default:"true"`
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" default:"5"`
SymlinksEnabled bool `xml:"symlinksEnabled" default:"true"`
Deprecated_RescanIntervalS int `xml:"rescanIntervalS,omitempty" json:"-"`
Deprecated_UREnabled bool `xml:"urEnabled,omitempty" json:"-"`
@@ -307,6 +308,11 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
convertV5V6(cfg)
}
// Upgrade to v7 configuration if appropriate
if cfg.Version == 6 {
convertV6V7(cfg)
}
// Hash old cleartext passwords
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0)
@@ -396,6 +402,15 @@ func ChangeRequiresRestart(from, to Configuration) bool {
return false
}
func convertV6V7(cfg *Configuration) {
// Migrate announce server addresses to the new URL based format
for i := range cfg.Options.GlobalAnnServers {
cfg.Options.GlobalAnnServers[i] = "udp4://" + cfg.Options.GlobalAnnServers[i]
}
cfg.Version = 7
}
func convertV5V6(cfg *Configuration) {
// Added ".stfolder" file at folder roots to identify mount issues
// Doesn't affect the config itself, but uses config migrations to identify

View File

@@ -36,7 +36,7 @@ func init() {
func TestDefaultValues(t *testing.T) {
expected := OptionsConfiguration{
ListenAddress: []string{"0.0.0.0:22000"},
GlobalAnnServers: []string{"announce.syncthing.net:22026"},
GlobalAnnServers: []string{"udp4://announce.syncthing.net:22026"},
GlobalAnnEnabled: true,
LocalAnnEnabled: true,
LocalAnnPort: 21025,
@@ -53,6 +53,7 @@ func TestDefaultValues(t *testing.T) {
KeepTemporariesH: 24,
CacheIgnoredFiles: true,
ProgressUpdateIntervalS: 5,
SymlinksEnabled: true,
}
cfg := New(device1)
@@ -138,7 +139,7 @@ func TestNoListenAddress(t *testing.T) {
func TestOverriddenValues(t *testing.T) {
expected := OptionsConfiguration{
ListenAddress: []string{":23000"},
GlobalAnnServers: []string{"syncthing.nym.se:22026"},
GlobalAnnServers: []string{"udp4://syncthing.nym.se:22026"},
GlobalAnnEnabled: false,
LocalAnnEnabled: false,
LocalAnnPort: 42123,
@@ -155,6 +156,7 @@ func TestOverriddenValues(t *testing.T) {
KeepTemporariesH: 48,
CacheIgnoredFiles: false,
ProgressUpdateIntervalS: 10,
SymlinksEnabled: false,
}
cfg, err := Load("testdata/overridenvalues.xml", device1)

View File

@@ -20,5 +20,6 @@
<keepTemporariesH>48</keepTemporariesH>
<cacheIgnoredFiles>false</cacheIgnoredFiles>
<progressUpdateIntervalS>10</progressUpdateIntervalS>
<symlinksEnabled>false</symlinksEnabled>
</options>
</configuration>

12
internal/config/testdata/v7.xml vendored Normal file
View File

@@ -0,0 +1,12 @@
<configuration version="7">
<folder id="test" path="testdata/" ro="true" ignorePerms="false" rescanIntervalS="600">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
</folder>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="true">
<address>a</address>
</device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="true">
<address>b</address>
</device>
</configuration>

View File

@@ -42,7 +42,7 @@ func (fn HandlerFunc) Changed(cfg Configuration) error {
// A wrapper around a Configuration that manages loads, saves and published
// notifications of changes to registered Handlers
type ConfigWrapper struct {
type Wrapper struct {
cfg Configuration
path string
@@ -57,8 +57,8 @@ type ConfigWrapper struct {
// Wrap wraps an existing Configuration structure and ties it to a file on
// disk.
func Wrap(path string, cfg Configuration) *ConfigWrapper {
w := &ConfigWrapper{cfg: cfg, path: path}
func Wrap(path string, cfg Configuration) *Wrapper {
w := &Wrapper{cfg: cfg, path: path}
w.replaces = make(chan Configuration)
go w.Serve()
return w
@@ -66,7 +66,7 @@ func Wrap(path string, cfg Configuration) *ConfigWrapper {
// Load loads an existing file on disk and returns a new configuration
// wrapper.
func Load(path string, myID protocol.DeviceID) (*ConfigWrapper, error) {
func Load(path string, myID protocol.DeviceID) (*Wrapper, error) {
fd, err := os.Open(path)
if err != nil {
return nil, err
@@ -84,7 +84,7 @@ func Load(path string, myID protocol.DeviceID) (*ConfigWrapper, error) {
// Serve handles configuration replace events and calls any interested
// handlers. It is started automatically by Wrap() and Load() and should not
// be run manually.
func (w *ConfigWrapper) Serve() {
func (w *Wrapper) Serve() {
for cfg := range w.replaces {
w.sMut.Lock()
subs := w.subs
@@ -97,25 +97,25 @@ func (w *ConfigWrapper) Serve() {
// Stop stops the Serve() loop. Set and Replace operations will panic after a
// Stop.
func (w *ConfigWrapper) Stop() {
func (w *Wrapper) Stop() {
close(w.replaces)
}
// Subscribe registers the given handler to be called on any future
// configuration changes.
func (w *ConfigWrapper) Subscribe(h Handler) {
func (w *Wrapper) Subscribe(h Handler) {
w.sMut.Lock()
w.subs = append(w.subs, h)
w.sMut.Unlock()
}
// Raw returns the currently wrapped Configuration object.
func (w *ConfigWrapper) Raw() Configuration {
func (w *Wrapper) Raw() Configuration {
return w.cfg
}
// Replace swaps the current configuration object for the given one.
func (w *ConfigWrapper) Replace(cfg Configuration) {
func (w *Wrapper) Replace(cfg Configuration) {
w.mut.Lock()
defer w.mut.Unlock()
@@ -127,7 +127,7 @@ func (w *ConfigWrapper) Replace(cfg Configuration) {
// Devices returns a map of devices. Device structures should not be changed,
// other than for the purpose of updating via SetDevice().
func (w *ConfigWrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
func (w *Wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
if w.deviceMap == nil {
@@ -141,7 +141,7 @@ func (w *ConfigWrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
// SetDevice adds a new device to the configuration, or overwrites an existing
// device with the same ID.
func (w *ConfigWrapper) SetDevice(dev DeviceConfiguration) {
func (w *Wrapper) SetDevice(dev DeviceConfiguration) {
w.mut.Lock()
defer w.mut.Unlock()
@@ -161,7 +161,7 @@ func (w *ConfigWrapper) SetDevice(dev DeviceConfiguration) {
// Devices returns a map of folders. Folder structures should not be changed,
// other than for the purpose of updating via SetFolder().
func (w *ConfigWrapper) Folders() map[string]FolderConfiguration {
func (w *Wrapper) Folders() map[string]FolderConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
if w.folderMap == nil {
@@ -181,7 +181,7 @@ func (w *ConfigWrapper) Folders() map[string]FolderConfiguration {
// SetFolder adds a new folder to the configuration, or overwrites an existing
// folder with the same ID.
func (w *ConfigWrapper) SetFolder(fld FolderConfiguration) {
func (w *Wrapper) SetFolder(fld FolderConfiguration) {
w.mut.Lock()
defer w.mut.Unlock()
@@ -200,14 +200,14 @@ func (w *ConfigWrapper) SetFolder(fld FolderConfiguration) {
}
// Options returns the current options configuration object.
func (w *ConfigWrapper) Options() OptionsConfiguration {
func (w *Wrapper) Options() OptionsConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
return w.cfg.Options
}
// SetOptions replaces the current options configuration object.
func (w *ConfigWrapper) SetOptions(opts OptionsConfiguration) {
func (w *Wrapper) SetOptions(opts OptionsConfiguration) {
w.mut.Lock()
defer w.mut.Unlock()
w.cfg.Options = opts
@@ -215,14 +215,14 @@ func (w *ConfigWrapper) SetOptions(opts OptionsConfiguration) {
}
// GUI returns the current GUI configuration object.
func (w *ConfigWrapper) GUI() GUIConfiguration {
func (w *Wrapper) GUI() GUIConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
return w.cfg.GUI
}
// SetGUI replaces the current GUI configuration object.
func (w *ConfigWrapper) SetGUI(gui GUIConfiguration) {
func (w *Wrapper) SetGUI(gui GUIConfiguration) {
w.mut.Lock()
defer w.mut.Unlock()
w.cfg.GUI = gui
@@ -230,7 +230,7 @@ func (w *ConfigWrapper) SetGUI(gui GUIConfiguration) {
}
// InvalidateFolder sets the invalid marker on the given folder.
func (w *ConfigWrapper) InvalidateFolder(id string, err string) {
func (w *Wrapper) InvalidateFolder(id string, err string) {
w.mut.Lock()
defer w.mut.Unlock()
@@ -246,7 +246,7 @@ func (w *ConfigWrapper) InvalidateFolder(id string, err string) {
}
// Save writes the configuration to disk, and generates a ConfigSaved event.
func (w *ConfigWrapper) Save() error {
func (w *Wrapper) Save() error {
fd, err := ioutil.TempFile(filepath.Dir(w.path), "cfg")
if err != nil {
return err

View File

@@ -0,0 +1,59 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package discover
import (
"fmt"
"net/url"
"time"
"github.com/syncthing/syncthing/internal/protocol"
)
type Factory func(*url.URL, *Announce) (Client, error)
var (
factories = make(map[string]Factory)
DefaultErrorRetryInternval = 60 * time.Second
DefaultGlobalBroadcastInterval = 1800 * time.Second
)
func Register(proto string, factory Factory) {
factories[proto] = factory
}
func New(addr string, pkt *Announce) (Client, error) {
uri, err := url.Parse(addr)
if err != nil {
return nil, err
}
factory, ok := factories[uri.Scheme]
if !ok {
return nil, fmt.Errorf("Unsupported scheme: %s", uri.Scheme)
}
client, err := factory(uri, pkt)
if err != nil {
return nil, err
}
return client, nil
}
type Client interface {
Lookup(device protocol.DeviceID) []string
StatusOK() bool
Address() string
Stop()
}

View File

@@ -0,0 +1,227 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package discover
import (
"fmt"
"net"
"sync"
"time"
"testing"
"github.com/syncthing/syncthing/internal/protocol"
)
var device protocol.DeviceID
func init() {
device, _ = protocol.DeviceIDFromString("P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2")
}
func TestUDP4Success(t *testing.T) {
conn, err := net.ListenUDP("udp4", nil)
if err != nil {
t.Fatal(err)
}
port := conn.LocalAddr().(*net.UDPAddr).Port
address := fmt.Sprintf("udp4://127.0.0.1:%d", port)
pkt := &Announce{
Magic: AnnouncementMagic,
This: Device{
device[:],
[]Address{{
IP: net.IPv4(123, 123, 123, 123),
Port: 1234,
}},
},
}
client, err := New(address, pkt)
if err != nil {
t.Fatal(err)
}
udpclient := client.(*UDPClient)
if udpclient.errorRetryInterval != DefaultErrorRetryInternval {
t.Fatal("Incorrect retry interval")
}
if udpclient.listenAddress.IP != nil || udpclient.listenAddress.Port != 0 {
t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
}
if client.Address() != address {
t.Fatal("Incorrect address")
}
buf := make([]byte, 2048)
// First announcement
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
_, err = conn.Read(buf)
if err != nil {
t.Fatal(err)
}
// Announcement verification
conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
_, addr, err := conn.ReadFromUDP(buf)
if err != nil {
t.Fatal(err)
}
// Reply to it.
_, err = conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
if err != nil {
t.Fatal(err)
}
// We should get nothing else
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
_, err = conn.Read(buf)
if err == nil {
t.Fatal("Expected error")
}
// Status should be ok
if !client.StatusOK() {
t.Fatal("Wrong status")
}
// Do a lookup in a separate routine
addrs := []string{}
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
addrs = client.Lookup(device)
wg.Done()
}()
// Receive the lookup and reply
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
_, addr, err = conn.ReadFromUDP(buf)
if err != nil {
t.Fatal(err)
}
conn.WriteToUDP(pkt.MustMarshalXDR(), addr)
// Wait for the lookup to arrive, verify that the number of answers is correct
wg.Wait()
if len(addrs) != 1 || addrs[0] != "123.123.123.123:1234" {
t.Fatal("Wrong number of answers")
}
client.Stop()
}
func TestUDP4Failure(t *testing.T) {
conn, err := net.ListenUDP("udp4", nil)
if err != nil {
t.Fatal(err)
}
port := conn.LocalAddr().(*net.UDPAddr).Port
address := fmt.Sprintf("udp4://127.0.0.1:%d/?listenaddress=127.0.0.1&retry=5", port)
pkt := &Announce{
Magic: AnnouncementMagic,
This: Device{
device[:],
[]Address{{
IP: net.IPv4(123, 123, 123, 123),
Port: 1234,
}},
},
}
client, err := New(address, pkt)
if err != nil {
t.Fatal(err)
}
udpclient := client.(*UDPClient)
if udpclient.errorRetryInterval != time.Second*5 {
t.Fatal("Incorrect retry interval")
}
if !udpclient.listenAddress.IP.Equal(net.IPv4(127, 0, 0, 1)) || udpclient.listenAddress.Port != 0 {
t.Fatal("Wrong listen IP or port", udpclient.listenAddress)
}
if client.Address() != address {
t.Fatal("Incorrect address")
}
buf := make([]byte, 2048)
// First announcement
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
_, err = conn.Read(buf)
if err != nil {
t.Fatal(err)
}
// Announcement verification
conn.SetDeadline(time.Now().Add(time.Millisecond * 1100))
_, _, err = conn.ReadFromUDP(buf)
if err != nil {
t.Fatal(err)
}
// Don't reply
// We should get nothing else
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
_, err = conn.Read(buf)
if err == nil {
t.Fatal("Expected error")
}
// Status should be failure
if client.StatusOK() {
t.Fatal("Wrong status")
}
// Do a lookup in a separate routine
addrs := []string{}
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
addrs = client.Lookup(device)
wg.Done()
}()
// Receive the lookup and don't reply
conn.SetDeadline(time.Now().Add(time.Millisecond * 100))
_, _, err = conn.ReadFromUDP(buf)
if err != nil {
t.Fatal(err)
}
// Wait for the lookup to timeout, verify that the number of answers is none
wg.Wait()
if len(addrs) != 0 {
t.Fatal("Wrong number of answers")
}
client.Stop()
}

View File

@@ -0,0 +1,246 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package discover
import (
"encoding/hex"
"io"
"net"
"net/url"
"strconv"
"sync"
"time"
"github.com/syncthing/syncthing/internal/protocol"
)
func init() {
for _, proto := range []string{"udp", "udp4", "udp6"} {
Register(proto, func(uri *url.URL, pkt *Announce) (Client, error) {
c := &UDPClient{}
err := c.Start(uri, pkt)
if err != nil {
return nil, err
}
return c, nil
})
}
}
type UDPClient struct {
url *url.URL
id protocol.DeviceID
stop chan struct{}
wg sync.WaitGroup
listenAddress *net.UDPAddr
globalBroadcastInterval time.Duration
errorRetryInterval time.Duration
status bool
mut sync.RWMutex
}
func (d *UDPClient) Start(uri *url.URL, pkt *Announce) error {
d.url = uri
d.id = protocol.DeviceIDFromBytes(pkt.This.ID)
d.stop = make(chan struct{})
params := uri.Query()
// The address must not have a port, as otherwise both announce and lookup
// sockets would try to bind to the same port.
addr, err := net.ResolveUDPAddr(d.url.Scheme, params.Get("listenaddress")+":0")
if err != nil {
return err
}
d.listenAddress = addr
broadcastSeconds, err := strconv.ParseUint(params.Get("broadcast"), 0, 0)
if err != nil {
d.globalBroadcastInterval = DefaultGlobalBroadcastInterval
} else {
d.globalBroadcastInterval = time.Duration(broadcastSeconds) * time.Second
}
retrySeconds, err := strconv.ParseUint(params.Get("retry"), 0, 0)
if err != nil {
d.errorRetryInterval = DefaultErrorRetryInternval
} else {
d.errorRetryInterval = time.Duration(retrySeconds) * time.Second
}
d.wg.Add(1)
go d.broadcast(pkt.MustMarshalXDR())
return nil
}
func (d *UDPClient) broadcast(pkt []byte) {
defer d.wg.Done()
conn, err := net.ListenUDP(d.url.Scheme, d.listenAddress)
for err != nil {
l.Warnf("Global UDP discovery (%s): %v; trying again in %v", d.url, err, d.errorRetryInterval)
select {
case <-d.stop:
return
case <-time.After(d.errorRetryInterval):
}
conn, err = net.ListenUDP(d.url.Scheme, d.listenAddress)
}
defer conn.Close()
remote, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
for err != nil {
l.Warnf("Global UDP discovery (%s): %v; trying again in %v", d.url, err, d.errorRetryInterval)
select {
case <-d.stop:
return
case <-time.After(d.errorRetryInterval):
}
remote, err = net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
}
timer := time.NewTimer(0)
for {
select {
case <-d.stop:
return
case <-timer.C:
var ok bool
if debug {
l.Debugf("Global UDP discovery (%s): send announcement -> %v\n%s", d.url, remote, hex.Dump(pkt))
}
_, err := conn.WriteTo(pkt, remote)
if err != nil {
if debug {
l.Debugf("discover %s: warning: %s", d.url, err)
}
ok = false
} else {
// Verify that the announce server responds positively for our device ID
time.Sleep(1 * time.Second)
res := d.Lookup(d.id)
if debug {
l.Debugf("discover %s: external lookup check: %v", d.url, res)
}
ok = len(res) > 0
}
d.mut.Lock()
d.status = ok
d.mut.Unlock()
if ok {
timer.Reset(d.globalBroadcastInterval)
} else {
timer.Reset(d.errorRetryInterval)
}
}
}
}
func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
extIP, err := net.ResolveUDPAddr(d.url.Scheme, d.url.Host)
if err != nil {
if debug {
l.Debugf("discover %s: %v; no external lookup", d.url, err)
}
return nil
}
conn, err := net.DialUDP(d.url.Scheme, d.listenAddress, extIP)
if err != nil {
if debug {
l.Debugf("discover %s: %v; no external lookup", d.url, err)
}
return nil
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(5 * time.Second))
if err != nil {
if debug {
l.Debugf("discover %s: %v; no external lookup", d.url, err)
}
return nil
}
buf := Query{QueryMagic, device[:]}.MustMarshalXDR()
_, err = conn.Write(buf)
if err != nil {
if debug {
l.Debugf("discover %s: %v; no external lookup", d.url, err)
}
return nil
}
buf = make([]byte, 2048)
n, err := conn.Read(buf)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
// Expected if the server doesn't know about requested device ID
return nil
}
if debug {
l.Debugf("discover %s: %v; no external lookup", d.url, err)
}
return nil
}
if debug {
l.Debugf("discover %s: read external:\n%s", d.url, hex.Dump(buf[:n]))
}
var pkt Announce
err = pkt.UnmarshalXDR(buf[:n])
if err != nil && err != io.EOF {
if debug {
l.Debugln("discover %s:", d.url, err)
}
return nil
}
var addrs []string
for _, a := range pkt.This.Addresses {
deviceAddr := net.JoinHostPort(net.IP(a.IP).String(), strconv.Itoa(int(a.Port)))
addrs = append(addrs, deviceAddr)
}
return addrs
}
func (d *UDPClient) Stop() {
if d.stop != nil {
close(d.stop)
d.wg.Wait()
}
}
func (d *UDPClient) StatusOK() bool {
d.mut.RLock()
defer d.mut.RUnlock()
return d.status
}
func (d *UDPClient) Address() string {
return d.url.String()
}

View File

@@ -31,25 +31,21 @@ import (
)
type Discoverer struct {
myID protocol.DeviceID
listenAddrs []string
localBcastIntv time.Duration
localBcastStart time.Time
globalBcastIntv time.Duration
errorRetryIntv time.Duration
cacheLifetime time.Duration
broadcastBeacon beacon.Interface
multicastBeacon beacon.Interface
registry map[protocol.DeviceID][]CacheEntry
registryLock sync.RWMutex
extServers []string
extPort uint16
localBcastTick <-chan time.Time
stopGlobal chan struct{}
globalWG sync.WaitGroup
forcedBcastTick chan time.Time
extAnnounceOK map[string]bool
extAnnounceOKmut sync.Mutex
myID protocol.DeviceID
listenAddrs []string
localBcastIntv time.Duration
localBcastStart time.Time
cacheLifetime time.Duration
broadcastBeacon beacon.Interface
multicastBeacon beacon.Interface
registry map[protocol.DeviceID][]CacheEntry
registryLock sync.RWMutex
extPort uint16
localBcastTick <-chan time.Time
forcedBcastTick chan time.Time
clients []Client
mut sync.RWMutex
}
type CacheEntry struct {
@@ -63,14 +59,11 @@ var (
func NewDiscoverer(id protocol.DeviceID, addresses []string) *Discoverer {
return &Discoverer{
myID: id,
listenAddrs: addresses,
localBcastIntv: 30 * time.Second,
globalBcastIntv: 1800 * time.Second,
errorRetryIntv: 60 * time.Second,
cacheLifetime: 5 * time.Minute,
registry: make(map[protocol.DeviceID][]CacheEntry),
extAnnounceOK: make(map[string]bool),
myID: id,
listenAddrs: addresses,
localBcastIntv: 30 * time.Second,
cacheLifetime: 5 * time.Minute,
registry: make(map[protocol.DeviceID][]CacheEntry),
}
}
@@ -112,38 +105,60 @@ func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
}
func (d *Discoverer) StartGlobal(servers []string, extPort uint16) {
// Wait for any previous announcer to stop before starting a new one.
d.globalWG.Wait()
d.extServers = servers
d.mut.Lock()
defer d.mut.Unlock()
if len(d.clients) > 0 {
d.stopGlobal()
}
d.extPort = extPort
d.stopGlobal = make(chan struct{})
d.globalWG.Add(1)
go func() {
defer d.globalWG.Done()
pkt := d.announcementPkt()
wg := sync.WaitGroup{}
clients := make(chan Client, len(servers))
for _, address := range servers {
wg.Add(1)
go func(addr string) {
defer wg.Done()
client, err := New(addr, pkt)
if err != nil {
l.Infoln("Error creating discovery client", addr, err)
return
}
clients <- client
}(address)
}
buf := d.announcementPkt()
wg.Wait()
close(clients)
for _, extServer := range d.extServers {
d.globalWG.Add(1)
go func(server string) {
d.sendExternalAnnouncements(server, buf)
d.globalWG.Done()
}(extServer)
}
}()
}
func (d *Discoverer) StopGlobal() {
if d.stopGlobal != nil {
close(d.stopGlobal)
d.globalWG.Wait()
for client := range clients {
d.clients = append(d.clients, client)
}
}
func (d *Discoverer) StopGlobal() {
d.mut.Lock()
defer d.mut.Unlock()
d.stopGlobal()
}
func (d *Discoverer) stopGlobal() {
for _, client := range d.clients {
client.Stop()
}
d.clients = []Client{}
}
func (d *Discoverer) ExtAnnounceOK() map[string]bool {
d.extAnnounceOKmut.Lock()
defer d.extAnnounceOKmut.Unlock()
return d.extAnnounceOK
d.mut.RLock()
defer d.mut.RUnlock()
ret := make(map[string]bool)
for _, client := range d.clients {
ret[client.Address()] = client.StatusOK()
}
return ret
}
func (d *Discoverer) Lookup(device protocol.DeviceID) []string {
@@ -151,22 +166,47 @@ func (d *Discoverer) Lookup(device protocol.DeviceID) []string {
cached := d.filterCached(d.registry[device])
d.registryLock.RUnlock()
d.mut.RLock()
defer d.mut.RUnlock()
var addrs []string
if len(cached) > 0 {
addrs := make([]string, len(cached))
addrs = make([]string, len(cached))
for i := range cached {
addrs[i] = cached[i].Address
}
return addrs
} else if len(d.extServers) != 0 && time.Since(d.localBcastStart) > d.localBcastIntv {
} else if len(d.clients) != 0 && time.Since(d.localBcastStart) > d.localBcastIntv {
// Only perform external lookups if we have at least one external
// server and one local announcement interval has passed. This is to
// avoid finding local peers on their remote address at startup.
addrs := d.externalLookup(device)
cached = make([]CacheEntry, len(addrs))
for i := range addrs {
cached[i] = CacheEntry{
Address: addrs[i],
Seen: time.Now(),
// server client and one local announcement interval has passed. This is
// to avoid finding local peers on their remote address at startup.
results := make(chan []string, len(d.clients))
wg := sync.WaitGroup{}
for _, client := range d.clients {
wg.Add(1)
go func(c Client) {
defer wg.Done()
results <- c.Lookup(device)
}(client)
}
wg.Wait()
close(results)
cached := []CacheEntry{}
seen := make(map[string]struct{})
now := time.Now()
for result := range results {
for _, addr := range result {
_, ok := seen[addr]
if !ok {
cached = append(cached, CacheEntry{
Address: addr,
Seen: now,
})
seen[addr] = struct{}{}
addrs = append(addrs, addr)
}
}
}
@@ -174,7 +214,7 @@ func (d *Discoverer) Lookup(device protocol.DeviceID) []string {
d.registry[device] = cached
d.registryLock.Unlock()
}
return nil
return addrs
}
func (d *Discoverer) Hint(device string, addrs []string) {
@@ -199,7 +239,7 @@ func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry {
return devices
}
func (d *Discoverer) announcementPkt() []byte {
func (d *Discoverer) announcementPkt() *Announce {
var addrs []Address
if d.extPort != 0 {
addrs = []Address{{Port: d.extPort}}
@@ -221,11 +261,10 @@ func (d *Discoverer) announcementPkt() []byte {
}
}
}
var pkt = Announce{
return &Announce{
Magic: AnnouncementMagic,
This: Device{d.myID[:], addrs},
}
return pkt.MustMarshalXDR()
}
func (d *Discoverer) sendLocalAnnouncements() {
@@ -252,80 +291,6 @@ func (d *Discoverer) sendLocalAnnouncements() {
}
}
func (d *Discoverer) sendExternalAnnouncements(extServer string, buf []byte) {
timer := time.NewTimer(0)
conn, err := net.ListenUDP("udp", nil)
for err != nil {
timer.Reset(d.errorRetryIntv)
l.Warnf("Global discovery: %v; trying again in %v", err, d.errorRetryIntv)
select {
case <-d.stopGlobal:
return
case <-timer.C:
}
conn, err = net.ListenUDP("udp", nil)
}
remote, err := net.ResolveUDPAddr("udp", extServer)
for err != nil {
timer.Reset(d.errorRetryIntv)
l.Warnf("Global discovery: %s: %v; trying again in %v", extServer, err, d.errorRetryIntv)
select {
case <-d.stopGlobal:
return
case <-timer.C:
}
remote, err = net.ResolveUDPAddr("udp", extServer)
}
// Delay the first announcement until after a full local announcement
// cycle, to increase the chance of other peers finding us locally first.
timer.Reset(d.localBcastIntv)
for {
select {
case <-d.stopGlobal:
return
case <-timer.C:
var ok bool
if debug {
l.Debugf("discover: send announcement -> %v\n%s", remote, hex.Dump(buf))
}
_, err := conn.WriteTo(buf, remote)
if err != nil {
if debug {
l.Debugln("discover: %s: warning:", extServer, err)
}
ok = false
} else {
// Verify that the announce server responds positively for our device ID
time.Sleep(1 * time.Second)
res := d.externalLookupOnServer(extServer, d.myID)
if debug {
l.Debugln("discover:", extServer, "external lookup check:", res)
}
ok = len(res) > 0
}
d.extAnnounceOKmut.Lock()
d.extAnnounceOK[extServer] = ok
d.extAnnounceOKmut.Unlock()
if ok {
timer.Reset(d.globalBcastIntv)
} else {
timer.Reset(d.errorRetryIntv)
}
}
}
}
func (d *Discoverer) recvAnnouncements(b beacon.Interface) {
for {
buf, addr := b.Recv()
@@ -406,104 +371,6 @@ func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool {
return len(current) > len(orig)
}
func (d *Discoverer) externalLookup(device protocol.DeviceID) []string {
// Buffer up to as many answers as we have servers to query.
results := make(chan []string, len(d.extServers))
// Query all servers.
wg := sync.WaitGroup{}
for _, extServer := range d.extServers {
wg.Add(1)
go func(server string) {
result := d.externalLookupOnServer(server, device)
if debug {
l.Debugln("discover:", result, "from", server, "for", device)
}
results <- result
wg.Done()
}(extServer)
}
wg.Wait()
close(results)
addrs := []string{}
for result := range results {
addrs = append(addrs, result...)
}
return addrs
}
func (d *Discoverer) externalLookupOnServer(extServer string, device protocol.DeviceID) []string {
extIP, err := net.ResolveUDPAddr("udp", extServer)
if err != nil {
if debug {
l.Debugf("discover: %s: %v; no external lookup", extServer, err)
}
return nil
}
conn, err := net.DialUDP("udp", nil, extIP)
if err != nil {
if debug {
l.Debugf("discover: %s: %v; no external lookup", extServer, err)
}
return nil
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(5 * time.Second))
if err != nil {
if debug {
l.Debugf("discover: %s: %v; no external lookup", extServer, err)
}
return nil
}
buf := Query{QueryMagic, device[:]}.MustMarshalXDR()
_, err = conn.Write(buf)
if err != nil {
if debug {
l.Debugf("discover: %s: %v; no external lookup", extServer, err)
}
return nil
}
buf = make([]byte, 2048)
n, err := conn.Read(buf)
if err != nil {
if err, ok := err.(net.Error); ok && err.Timeout() {
// Expected if the server doesn't know about requested device ID
return nil
}
if debug {
l.Debugf("discover: %s: %v; no external lookup", extServer, err)
}
return nil
}
if debug {
l.Debugf("discover: %s: read external:\n%s", extServer, hex.Dump(buf[:n]))
}
var pkt Announce
err = pkt.UnmarshalXDR(buf[:n])
if err != nil && err != io.EOF {
if debug {
l.Debugln("discover:", extServer, err)
}
return nil
}
var addrs []string
for _, a := range pkt.This.Addresses {
deviceAddr := net.JoinHostPort(net.IP(a.IP).String(), strconv.Itoa(int(a.Port)))
addrs = append(addrs, deviceAddr)
}
return addrs
}
func (d *Discoverer) filterCached(c []CacheEntry) []CacheEntry {
for i := 0; i < len(c); {
if ago := time.Since(c[i].Seen); ago > d.cacheLifetime {

View File

@@ -13,6 +13,136 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package discover_test
package discover
// Empty test file to generate 0% coverage rather than no coverage
import (
"net/url"
"time"
"testing"
"github.com/syncthing/syncthing/internal/protocol"
)
type DummyClient struct {
url *url.URL
lookups []protocol.DeviceID
lookupRet []string
stops int
statusRet bool
statusChecks int
}
func (c *DummyClient) Lookup(device protocol.DeviceID) []string {
c.lookups = append(c.lookups, device)
return c.lookupRet
}
func (c *DummyClient) StatusOK() bool {
c.statusChecks++
return c.statusRet
}
func (c *DummyClient) Stop() {
c.stops++
}
func (c *DummyClient) Address() string {
return c.url.String()
}
func TestGlobalDiscovery(t *testing.T) {
c1 := &DummyClient{
statusRet: false,
lookupRet: []string{"test.com:1234"},
}
c2 := &DummyClient{
statusRet: true,
lookupRet: []string{},
}
c3 := &DummyClient{
statusRet: true,
lookupRet: []string{"best.com:2345"},
}
clients := []*DummyClient{c1, c2}
Register("test1", func(uri *url.URL, pkt *Announce) (Client, error) {
c := clients[0]
clients = clients[1:]
c.url = uri
return c, nil
})
Register("test2", func(uri *url.URL, pkt *Announce) (Client, error) {
c3.url = uri
return c3, nil
})
d := NewDiscoverer(device, []string{})
d.localBcastStart = time.Time{}
servers := []string{
"test1://123.123.123.123:1234",
"test1://23.23.23.23:234",
"test2://234.234.234.234.2345",
}
d.StartGlobal(servers, 1234)
if len(d.clients) != 3 {
t.Fatal("Wrong number of clients")
}
status := d.ExtAnnounceOK()
for _, c := range []*DummyClient{c1, c2, c3} {
if status[c.url.String()] != c.statusRet || c.statusChecks != 1 {
t.Fatal("Wrong status")
}
}
addrs := d.Lookup(device)
if len(addrs) != 2 {
t.Fatal("Wrong numer of addresses", addrs)
}
for _, addr := range []string{"test.com:1234", "best.com:2345"} {
found := false
for _, laddr := range addrs {
if laddr == addr {
found = true
break
}
}
if !found {
t.Fatal("Couldn't find", addr)
}
}
for _, c := range []*DummyClient{c1, c2, c3} {
if len(c.lookups) != 1 || c.lookups[0] != device {
t.Fatal("Wrong lookups")
}
}
addrs = d.Lookup(device)
if len(addrs) != 2 {
t.Fatal("Wrong numer of addresses", addrs)
}
// Answer should be cached, so number of lookups should have not incresed
for _, c := range []*DummyClient{c1, c2, c3} {
if len(c.lookups) != 1 || c.lookups[0] != device {
t.Fatal("Wrong lookups")
}
}
d.StopGlobal()
for _, c := range []*DummyClient{c1, c2, c3} {
if c.stops != 1 {
t.Fatal("Wrong number of stops")
}
}
}

View File

@@ -13,6 +13,9 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
//go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
//go:generate genxdr -o packets_xdr.go packets.go
package discover
const (

View File

@@ -86,7 +86,7 @@ const BufferSize = 64
type Logger struct {
subs map[int]*Subscription
nextId int
nextID int
mutex sync.Mutex
}
@@ -120,15 +120,15 @@ func NewLogger() *Logger {
func (l *Logger) Log(t EventType, data interface{}) {
l.mutex.Lock()
if debug {
dl.Debugln("log", l.nextId, t.String(), data)
dl.Debugln("log", l.nextID, t.String(), data)
}
e := Event{
ID: l.nextId,
ID: l.nextID,
Time: time.Now(),
Type: t,
Data: data,
}
l.nextId++
l.nextID++
for _, s := range l.subs {
if s.mask&t != 0 {
select {
@@ -148,10 +148,10 @@ func (l *Logger) Subscribe(mask EventType) *Subscription {
}
s := &Subscription{
mask: mask,
id: l.nextId,
id: l.nextID,
events: make(chan Event, BufferSize),
}
l.nextId++
l.nextID++
l.subs[s.id] = s
l.mutex.Unlock()
return s

View File

@@ -126,7 +126,7 @@ type BlockFinder struct {
mut sync.RWMutex
}
func NewBlockFinder(db *leveldb.DB, cfg *config.ConfigWrapper) *BlockFinder {
func NewBlockFinder(db *leveldb.DB, cfg *config.Wrapper) *BlockFinder {
if blockFinder != nil {
return blockFinder
}

View File

@@ -13,6 +13,9 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
//go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
//go:generate genxdr -o leveldb_xdr.go leveldb.go
package files
import (

View File

@@ -55,6 +55,7 @@ func Convert(pattern string, flags int) (*regexp.Regexp, error) {
pattern = strings.Replace(pattern, "\\.", "[:escapeddot:]", -1)
}
pattern = strings.Replace(pattern, ".", "\\.", -1)
pattern = strings.Replace(pattern, "+", "\\+", -1)
pattern = strings.Replace(pattern, "**", "[:doublestar:]", -1)
pattern = strings.Replace(pattern, "*", any+"*", -1)
pattern = strings.Replace(pattern, "[:doublestar:]", ".*", -1)

95
internal/ignore/cache.go Normal file
View File

@@ -0,0 +1,95 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package ignore
import (
"sync"
"time"
)
var (
caches = make(map[string]*cache)
cacheMut sync.Mutex
)
func init() {
// Periodically go through the cache and remove cache entries that have
// not been touched in the last two hours.
go cleanIgnoreCaches(2 * time.Hour)
}
type cache struct {
patterns []Pattern
entries map[string]cacheEntry
mut sync.Mutex
}
type cacheEntry struct {
value bool
access time.Time
}
func newCache(patterns []Pattern) *cache {
return &cache{
patterns: patterns,
entries: make(map[string]cacheEntry),
}
}
func (c *cache) clean(d time.Duration) {
c.mut.Lock()
for k, v := range c.entries {
if time.Since(v.access) > d {
delete(c.entries, k)
}
}
c.mut.Unlock()
}
func (c *cache) get(key string) (result, ok bool) {
c.mut.Lock()
res, ok := c.entries[key]
if ok {
res.access = time.Now()
c.entries[key] = res
}
c.mut.Unlock()
return res.value, ok
}
func (c *cache) set(key string, val bool) {
c.mut.Lock()
c.entries[key] = cacheEntry{val, time.Now()}
c.mut.Unlock()
}
func (c *cache) len() int {
c.mut.Lock()
l := len(c.entries)
c.mut.Unlock()
return l
}
func cleanIgnoreCaches(dur time.Duration) {
for {
time.Sleep(dur)
cacheMut.Lock()
for _, v := range caches {
v.clean(dur)
}
cacheMut.Unlock()
}
}

View File

@@ -0,0 +1,84 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package ignore
import (
"testing"
"time"
)
func TestCache(t *testing.T) {
c := newCache(nil)
res, ok := c.get("nonexistent")
if res != false || ok != false {
t.Errorf("res %v, ok %v for nonexistent item", res, ok)
}
// Set and check some items
c.set("true", true)
c.set("false", false)
res, ok = c.get("true")
if res != true || ok != true {
t.Errorf("res %v, ok %v for true item", res, ok)
}
res, ok = c.get("false")
if res != false || ok != true {
t.Errorf("res %v, ok %v for false item", res, ok)
}
// Don't clean anything
c.clean(time.Second)
// Same values should exist
res, ok = c.get("true")
if res != true || ok != true {
t.Errorf("res %v, ok %v for true item", res, ok)
}
res, ok = c.get("false")
if res != false || ok != true {
t.Errorf("res %v, ok %v for false item", res, ok)
}
// Sleep and access, to get some data for clean
time.Sleep(100 * time.Millisecond)
c.get("true")
time.Sleep(100 * time.Millisecond)
// "false" was accessed 200 ms ago, "true" was accessed 100 ms ago.
// This should clean out "false" but not "true"
c.clean(150 * time.Millisecond)
// Same values should exist
_, ok = c.get("true")
if !ok {
t.Error("item should still exist")
}
_, ok = c.get("false")
if ok {
t.Errorf("item should have been cleaned")
}
}

View File

@@ -28,24 +28,15 @@ import (
"github.com/syncthing/syncthing/internal/fnmatch"
)
var caches = make(map[string]MatcherCache)
type Pattern struct {
match *regexp.Regexp
include bool
}
type Matcher struct {
patterns []Pattern
oldMatches map[string]bool
newMatches map[string]bool
mut sync.Mutex
}
type MatcherCache struct {
patterns []Pattern
matches *map[string]bool
matches *cache
mut sync.Mutex
}
func Load(file string, cache bool) (*Matcher, error) {
@@ -55,6 +46,9 @@ func Load(file string, cache bool) (*Matcher, error) {
return matcher, err
}
cacheMut.Lock()
defer cacheMut.Unlock()
// Get the current cache object for the given file
cached, ok := caches[file]
if !ok || !patternsEqual(cached.patterns, matcher.patterns) {
@@ -62,12 +56,9 @@ func Load(file string, cache bool) (*Matcher, error) {
// store matches for the given set of patterns.
// Initialize oldMatches to indicate that we are interested in
// caching.
matcher.oldMatches = make(map[string]bool)
matcher.newMatches = make(map[string]bool)
caches[file] = MatcherCache{
patterns: matcher.patterns,
matches: &matcher.newMatches,
}
cached = newCache(matcher.patterns)
matcher.matches = cached
caches[file] = cached
return matcher, nil
}
@@ -75,10 +66,7 @@ func Load(file string, cache bool) (*Matcher, error) {
// matches map and update the pointer. (This prevents matches map from
// growing indefinately, as we only cache whatever we've matched in the last
// iteration, rather than through runtime history)
matcher.oldMatches = *cached.matches
matcher.newMatches = make(map[string]bool)
cached.matches = &matcher.newMatches
caches[file] = cached
matcher.matches = cached
return matcher, nil
}
@@ -94,27 +82,27 @@ func (m *Matcher) Match(file string) (result bool) {
return false
}
// We have old matches map set, means we should do caching
if m.oldMatches != nil {
// Capture the result to the new matches regardless of who returns it
defer func() {
m.mut.Lock()
m.newMatches[file] = result
m.mut.Unlock()
}()
// Check perhaps we've seen this file before, and we already know
// what the outcome is going to be.
result, ok := m.oldMatches[file]
if m.matches != nil {
// Check the cache for a known result.
res, ok := m.matches.get(file)
if ok {
return result
return res
}
// Update the cache with the result at return time
defer func() {
m.matches.set(file, result)
}()
}
// Check all the patterns for a match.
for _, pattern := range m.patterns {
if pattern.match.MatchString(file) {
return pattern.include
}
}
// Default to false.
return false
}
@@ -181,9 +169,8 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (*M
includes, err := loadIgnoreFile(includeFile, seen)
if err != nil {
return err
} else {
exps.patterns = append(exps.patterns, includes.patterns...)
}
exps.patterns = append(exps.patterns, includes.patterns...)
} else {
// Path name or pattern, add it so it matches files both in
// current directory and subdirs.

View File

@@ -55,6 +55,8 @@ func TestIgnore(t *testing.T) {
{filepath.Join("dir3"), true},
{filepath.Join("dir3", "afile"), true},
{"lost+found", true},
}
for i, tc := range tests {
@@ -173,12 +175,8 @@ func TestCaching(t *testing.T) {
t.Fatal(err)
}
if pats.oldMatches == nil || len(pats.oldMatches) != 0 {
t.Fatal("Expected empty map")
}
if pats.newMatches == nil || len(pats.newMatches) != 0 {
t.Fatal("Expected empty map")
if pats.matches.len() != 0 {
t.Fatal("Expected empty cache")
}
if len(pats.patterns) != 4 {
@@ -191,7 +189,7 @@ func TestCaching(t *testing.T) {
pats.Match(letter)
}
if len(pats.newMatches) != 4 {
if pats.matches.len() != 4 {
t.Fatal("Expected 4 cached results")
}
@@ -201,30 +199,10 @@ func TestCaching(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 4 {
if pats.matches.len() != 4 {
t.Fatal("Expected 4 cached results")
}
// Match less this time
for _, letter := range []string{"b", "x", "y"} {
pats.Match(letter)
}
if len(pats.newMatches) != 3 {
t.Fatal("Expected 3 cached results")
}
// Reload file, expect the new outcomes to be provided
pats, err = Load(fd1.Name(), true)
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 3 {
t.Fatal("Expected 3 cached results", len(pats.oldMatches))
}
// Modify the include file, expect empty cache
fd2.WriteString("/z/\n")
@@ -234,7 +212,7 @@ func TestCaching(t *testing.T) {
t.Fatal(err)
}
if len(pats.oldMatches) != 0 {
if pats.matches.len() != 0 {
t.Fatal("Expected 0 cached results")
}
@@ -250,7 +228,7 @@ func TestCaching(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 3 {
if pats.matches.len() != 3 {
t.Fatal("Expected 3 cached results")
}
@@ -262,7 +240,7 @@ func TestCaching(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 0 {
if pats.matches.len() != 0 {
t.Fatal("Expected cache invalidation")
}
@@ -278,7 +256,7 @@ func TestCaching(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 3 {
if pats.matches.len() != 3 {
t.Fatal("Expected 3 cached results")
}
}

View File

@@ -4,3 +4,4 @@ bfile
dir1/cfile
**/efile
/ffile
lost+found

View File

@@ -37,7 +37,7 @@ func newDeviceActivity() *deviceActivity {
func (m *deviceActivity) leastBusy(availability []protocol.DeviceID) protocol.DeviceID {
m.mut.Lock()
var low int = 2<<30 - 1
low := 2<<30 - 1
var selected protocol.DeviceID
for _, device := range availability {
if usage := m.act[device]; usage < low {

View File

@@ -22,9 +22,9 @@ import (
)
func TestDeviceActivity(t *testing.T) {
n0 := protocol.DeviceID{1, 2, 3, 4}
n1 := protocol.DeviceID{5, 6, 7, 8}
n2 := protocol.DeviceID{9, 10, 11, 12}
n0 := protocol.DeviceID([32]byte{1, 2, 3, 4})
n1 := protocol.DeviceID([32]byte{5, 6, 7, 8})
n2 := protocol.DeviceID([32]byte{9, 10, 11, 12})
devices := []protocol.DeviceID{n0, n1, n2}
na := newDeviceActivity()

View File

@@ -82,7 +82,7 @@ type service interface {
}
type Model struct {
cfg *config.ConfigWrapper
cfg *config.Wrapper
db *leveldb.DB
finder *files.BlockFinder
progressEmitter *ProgressEmitter
@@ -98,6 +98,7 @@ type Model struct {
deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
folderRunners map[string]service // folder -> puller or scanner
folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef
fmut sync.RWMutex // protects the above
folderState map[string]folderState // folder -> state
@@ -123,7 +124,7 @@ var (
// NewModel creates and starts a new model. The model starts in read-only mode,
// where it sends index information to connected peers and responds to requests
// for file data without altering the local folder in any way.
func NewModel(cfg *config.ConfigWrapper, deviceName, clientName, clientVersion string, db *leveldb.DB) *Model {
func NewModel(cfg *config.Wrapper, deviceName, clientName, clientVersion string, db *leveldb.DB) *Model {
m := &Model{
cfg: cfg,
db: db,
@@ -137,6 +138,7 @@ func NewModel(cfg *config.ConfigWrapper, deviceName, clientName, clientVersion s
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
folderIgnores: make(map[string]*ignore.Matcher),
folderRunners: make(map[string]service),
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
folderState: make(map[string]folderState),
folderStateChanged: make(map[string]time.Time),
protoConn: make(map[protocol.DeviceID]protocol.Connection),
@@ -283,6 +285,15 @@ func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
return res
}
// Returns statistics about each folder
func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
var res = make(map[string]stats.FolderStatistics)
for id := range m.cfg.Folders() {
res[id] = m.folderStatRef(id).GetStatistics()
}
return res
}
// Returns the completion status, in percent, for the given device and folder.
func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
defer m.leveldbPanicWorkaround()
@@ -572,7 +583,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
l.Infof("Adding device %v to config (vouched for by introducer %v)", id, deviceID)
newDeviceCfg := config.DeviceConfiguration{
DeviceID: id,
Compression: true,
Compression: m.cfg.Devices()[deviceID].Compression,
Addresses: []string{"dynamic"},
}
@@ -862,17 +873,33 @@ func (m *Model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatistic
if sr, ok := m.deviceStatRefs[deviceID]; ok {
return sr
} else {
sr = stats.NewDeviceStatisticsReference(m.db, deviceID)
m.deviceStatRefs[deviceID] = sr
return sr
}
sr := stats.NewDeviceStatisticsReference(m.db, deviceID)
m.deviceStatRefs[deviceID] = sr
return sr
}
func (m *Model) deviceWasSeen(deviceID protocol.DeviceID) {
m.deviceStatRef(deviceID).WasSeen()
}
func (m *Model) folderStatRef(folder string) *stats.FolderStatisticsReference {
m.fmut.Lock()
defer m.fmut.Unlock()
sr, ok := m.folderStatRefs[folder]
if !ok {
sr = stats.NewFolderStatisticsReference(m.db, folder)
m.folderStatRefs[folder] = sr
}
return sr
}
func (m *Model) receivedFile(folder, filename string) {
m.folderStatRef(folder).ReceivedFile(filename)
}
func sendIndexes(conn protocol.Connection, folder string, fs *files.Set, ignores *ignore.Matcher) {
deviceID := conn.ID()
name := conn.Name()
@@ -1065,6 +1092,7 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
Matcher: ignores,
BlockSize: protocol.BlockSize,
TempNamer: defTempNamer,
TempLifetime: time.Duration(m.cfg.Options().KeepTemporariesH) * time.Hour,
CurrentFiler: cFiler{m, folder},
IgnorePerms: m.folderCfgs[folder].IgnorePerms,
}
@@ -1360,7 +1388,7 @@ func (m *Model) leveldbPanicWorkaround() {
func symlinkInvalid(isLink bool) bool {
if !symlinks.Supported && isLink {
SymlinkWarning.Do(func() {
l.Warnln("Symlinks are unsupported as they require Administrator priviledges. This might cause your folder to appear out of sync.")
l.Warnln("Symlinks are disabled, unsupported or require Administrator priviledges. This might cause your folder to appear out of sync.")
})
return true
}

View File

@@ -1,3 +1,18 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package model
import (
@@ -23,7 +38,7 @@ type ProgressEmitter struct {
// Creates a new progress emitter which emits DownloadProgress events every
// interval.
func NewProgressEmitter(cfg *config.ConfigWrapper) *ProgressEmitter {
func NewProgressEmitter(cfg *config.Wrapper) *ProgressEmitter {
t := &ProgressEmitter{
stop: make(chan struct{}),
registry: make(map[string]*sharedPullerState),

View File

@@ -31,15 +31,15 @@ func expectEvent(w *events.Subscription, t *testing.T, size int) {
t.Fatal("Unexpected error:", err)
}
if event.Type != events.DownloadProgress {
t.Fatal("Unexpected event:", event)
t.Fatal("Unexpected event:", event)
}
data := event.Data.(map[string]map[string]*pullerProgress)
if len(data) != size {
t.Fatal("Unexpected event data size:", data)
t.Fatal("Unexpected event data size:", data)
}
}
func expectTimeout(w *events.Subscription, t *testing.T){
func expectTimeout(w *events.Subscription, t *testing.T) {
_, err := w.Poll(timeout)
if err != events.ErrTimeout {
t.Fatal("Unexpected non-Timeout error:", err)
@@ -78,7 +78,7 @@ func TestProgressEmitter(t *testing.T) {
expectTimeout(w, t)
s.pullStarted()
expectEvent(w, t, 1)
expectTimeout(w, t)

View File

@@ -91,21 +91,16 @@ func (p *Puller) Serve() {
pullTimer := time.NewTimer(checkPullIntv)
scanTimer := time.NewTimer(time.Millisecond) // The first scan should be done immediately.
cleanTimer := time.NewTicker(time.Hour)
defer func() {
pullTimer.Stop()
scanTimer.Stop()
cleanTimer.Stop()
// TODO: Should there be an actual FolderStopped state?
p.model.setState(p.folder, FolderIdle)
}()
var prevVer uint64
// Clean out old temporaries before we start pulling
p.clean()
// We don't start pulling files until a scan has been completed.
initialScanCompleted := false
@@ -222,10 +217,6 @@ loop:
l.Infoln("Completed initial scan (rw) of folder", p.folder)
initialScanCompleted = true
}
// Clean out old temporaries
case <-cleanTimer.C:
p.clean()
}
}
}
@@ -373,12 +364,11 @@ func (p *Puller) handleDir(file protocol.FileInfo) {
}
info, err := os.Lstat(realName)
isLink, _ := symlinks.IsSymlink(realName)
switch {
// There is already something under that name, but it's a file/link.
// Most likely a file/link is getting replaced with a directory.
// Remove the file/link and fall through to directory creation.
case isLink || (err == nil && !info.IsDir()):
case err == nil && (!info.IsDir() || info.Mode()&os.ModeSymlink != 0):
err = osutil.InWritableDir(os.Remove, realName)
if err != nil {
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
@@ -795,8 +785,7 @@ func (p *Puller) performFinish(state *sharedPullerState) {
// If the target path is a symlink or a directory, we cannot copy
// over it, hence remove it before proceeding.
stat, err := os.Lstat(state.realName)
isLink, _ := symlinks.IsSymlink(state.realName)
if isLink || (err == nil && stat.IsDir()) {
if err == nil && (stat.IsDir() || stat.Mode()&os.ModeSymlink != 0) {
osutil.InWritableDir(os.Remove, state.realName)
}
// Replace the original content with the new one
@@ -841,6 +830,7 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
}
p.performFinish(state)
p.model.receivedFile(p.folder, state.file.Name)
if p.progressEmitter != nil {
p.progressEmitter.Deregister(state)
}
@@ -848,23 +838,6 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
}
}
// clean deletes orphaned temporary files
func (p *Puller) clean() {
keep := time.Duration(p.model.cfg.Options().KeepTemporariesH) * time.Hour
now := time.Now()
filepath.Walk(p.dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.Mode().IsRegular() && defTempNamer.IsTemporary(path) && info.ModTime().Add(keep).Before(now) {
os.Remove(path)
}
return nil
})
}
func invalidateFolder(cfg *config.Configuration, folderID string, err error) {
for i := range cfg.Folders {
folder := &cfg.Folders[i]

View File

@@ -19,6 +19,7 @@ import (
"os"
"path/filepath"
"testing"
"time"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/protocol"
@@ -28,6 +29,16 @@ import (
"github.com/syndtr/goleveldb/leveldb/storage"
)
func init() {
// We do this to make sure that the temp file required for the tests does
// not get removed during the tests.
future := time.Now().Add(time.Hour)
err := os.Chtimes(filepath.Join("testdata", defTempNamer.TempName("file")), future, future)
if err != nil {
panic(err)
}
}
var blocks = []protocol.BlockInfo{
{Hash: []uint8{0xfa, 0x43, 0x23, 0x9b, 0xce, 0xe7, 0xb9, 0x7c, 0xa6, 0x2f, 0x0, 0x7c, 0xc6, 0x84, 0x87, 0x56, 0xa, 0x39, 0xe1, 0x9f, 0x74, 0xf3, 0xdd, 0xe7, 0x48, 0x6d, 0xb3, 0xf9, 0x8d, 0xf8, 0xe4, 0x71}}, // Zero'ed out block
{Offset: 0, Size: 0x20000, Hash: []uint8{0x7e, 0xad, 0xbc, 0x36, 0xae, 0xbb, 0xcf, 0x74, 0x43, 0xe2, 0x7a, 0x5a, 0x4b, 0xb8, 0x5b, 0xce, 0xe6, 0x9e, 0x1e, 0x10, 0xf9, 0x8a, 0xbc, 0x77, 0x95, 0x2, 0x29, 0x60, 0x9e, 0x96, 0xae, 0x6c}},

View File

@@ -24,3 +24,5 @@ func HideFile(path string) error {
func ShowFile(path string) error {
return nil
}
func HideConsole() {}

View File

@@ -48,3 +48,14 @@ func ShowFile(path string) error {
attrs &^= syscall.FILE_ATTRIBUTE_HIDDEN
return syscall.SetFileAttributes(p, attrs)
}
func HideConsole() {
getConsoleWindow := syscall.NewLazyDLL("kernel32.dll").NewProc("GetConsoleWindow")
showWindow := syscall.NewLazyDLL("user32.dll").NewProc("ShowWindow")
if getConsoleWindow.Find() == nil && showWindow.Find() == nil {
hwnd, _, _ := getConsoleWindow.Call()
if hwnd != 0 {
showWindow.Call(hwnd, 0)
}
}
}

View File

@@ -13,6 +13,9 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
//go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
//go:generate genxdr -o message_xdr.go message.go
package protocol
import "fmt"

View File

@@ -21,6 +21,7 @@ import (
"path/filepath"
"runtime"
"strings"
"time"
"github.com/syncthing/syncthing/internal/ignore"
"github.com/syncthing/syncthing/internal/lamport"
@@ -40,6 +41,8 @@ type Walker struct {
Matcher *ignore.Matcher
// If TempNamer is not nil, it is used to ignore tempory files when walking.
TempNamer TempNamer
// Number of hours to keep temporary files for
TempLifetime time.Duration
// If CurrentFiler is not nil, it is queried for the current file before rescanning.
CurrentFiler CurrentFiler
// If IgnorePerms is true, changes to permission bits will not be
@@ -86,6 +89,7 @@ func (w *Walker) Walk() (chan protocol.FileInfo, error) {
}
func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFunc {
now := time.Now()
return func(p string, info os.FileInfo, err error) error {
if err != nil {
if debug {
@@ -111,6 +115,12 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
if debug {
l.Debugln("temporary:", rn)
}
if info.Mode().IsRegular() && info.ModTime().Add(w.TempLifetime).Before(now) {
os.Remove(p)
if debug {
l.Debugln("removing temporary:", rn, info.ModTime())
}
}
return nil
}
@@ -131,12 +141,9 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
return nil
}
// We must perform this check, as symlinks on Windows are always
// .IsRegular or .IsDir unlike on Unix.
// Index wise symlinks are always files, regardless of what the target
// is, because symlinks carry their target path as their content.
isSymlink, _ := symlinks.IsSymlink(p)
if isSymlink {
if info.Mode()&os.ModeSymlink != 0 {
var rval error
// If the target is a directory, do NOT descend down there.
// This will cause files to get tracked, and removing the symlink
@@ -199,12 +206,12 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
if w.CurrentFiler != nil {
cf := w.CurrentFiler.CurrentFile(rn)
permUnchanged := w.IgnorePerms || !cf.HasPermissionBits() || PermsEqual(cf.Flags, uint32(info.Mode()))
if !cf.IsDeleted() && cf.IsDirectory() && permUnchanged {
if !cf.IsDeleted() && cf.IsDirectory() && permUnchanged && !cf.IsSymlink() {
return nil
}
}
var flags uint32 = protocol.FlagDirectory
flags := uint32(protocol.FlagDirectory)
if w.IgnorePerms {
flags |= protocol.FlagNoPermBits | 0777
} else {

View File

@@ -190,16 +190,16 @@ func TestVerify(t *testing.T) {
type fileList []protocol.FileInfo
func (f fileList) Len() int {
return len(f)
func (l fileList) Len() int {
return len(l)
}
func (f fileList) Less(a, b int) bool {
return f[a].Name < f[b].Name
func (l fileList) Less(a, b int) bool {
return l[a].Name < l[b].Name
}
func (f fileList) Swap(a, b int) {
f[a], f[b] = f[b], f[a]
func (l fileList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l fileList) testfiles() testfileList {

133
internal/stats/folder.go Normal file
View File

@@ -0,0 +1,133 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package stats
import (
"encoding/binary"
"time"
"github.com/syndtr/goleveldb/leveldb"
)
const (
folderStatisticTypeLastFile = iota
)
var folderStatisticsTypes = []byte{
folderStatisticTypeLastFile,
}
type FolderStatistics struct {
LastFile *LastFile
}
type FolderStatisticsReference struct {
db *leveldb.DB
folder string
}
func NewFolderStatisticsReference(db *leveldb.DB, folder string) *FolderStatisticsReference {
return &FolderStatisticsReference{
db: db,
folder: folder,
}
}
func (s *FolderStatisticsReference) key(stat byte) []byte {
k := make([]byte, 1+1+64)
k[0] = keyTypeFolderStatistic
k[1] = stat
copy(k[1+1:], s.folder[:])
return k
}
func (s *FolderStatisticsReference) GetLastFile() *LastFile {
value, err := s.db.Get(s.key(folderStatisticTypeLastFile), nil)
if err != nil {
if err != leveldb.ErrNotFound {
l.Warnln("FolderStatisticsReference: Failed loading last file filename value for", s.folder, ":", err)
}
return nil
}
file := LastFile{}
err = file.UnmarshalBinary(value)
if err != nil {
l.Warnln("FolderStatisticsReference: Failed loading last file value for", s.folder, ":", err)
return nil
}
return &file
}
func (s *FolderStatisticsReference) ReceivedFile(filename string) {
f := LastFile{
Filename: filename,
At: time.Now(),
}
if debug {
l.Debugln("stats.FolderStatisticsReference.ReceivedFile:", s.folder)
}
value, err := f.MarshalBinary()
if err != nil {
l.Warnln("FolderStatisticsReference: Failed serializing last file value for", s.folder, ":", err)
return
}
err = s.db.Put(s.key(folderStatisticTypeLastFile), value, nil)
if err != nil {
l.Warnln("Failed update last file value for", s.folder, ":", err)
}
}
// Never called, maybe because it's worth while to keep the data
// or maybe because we have no easy way of knowing that a folder has been removed.
func (s *FolderStatisticsReference) Delete() error {
for _, stype := range folderStatisticsTypes {
err := s.db.Delete(s.key(stype), nil)
if debug && err == nil {
l.Debugln("stats.FolderStatisticsReference.Delete:", s.folder, stype)
}
if err != nil && err != leveldb.ErrNotFound {
return err
}
}
return nil
}
func (s *FolderStatisticsReference) GetStatistics() FolderStatistics {
return FolderStatistics{
LastFile: s.GetLastFile(),
}
}
type LastFile struct {
At time.Time
Filename string
}
func (f *LastFile) MarshalBinary() ([]byte, error) {
buf := make([]byte, 8+len(f.Filename))
binary.BigEndian.PutUint64(buf[:8], uint64(f.At.Unix()))
copy(buf[8:], []byte(f.Filename))
return buf, nil
}
func (f *LastFile) UnmarshalBinary(buf []byte) error {
f.At = time.Unix(int64(binary.BigEndian.Uint64(buf[:8])), 0)
f.Filename = string(buf[8:])
return nil
}

View File

@@ -18,4 +18,5 @@ package stats
// Same key space as files/leveldb.go keyType* constants
const (
keyTypeDeviceStatistic = iota + 30
keyTypeFolderStatistic
)

View File

@@ -41,14 +41,6 @@ func Read(path string) (string, uint32, error) {
return osutil.NormalizedFilename(path), mode, err
}
func IsSymlink(path string) (bool, error) {
lstat, err := os.Lstat(path)
if err != nil {
return false, err
}
return lstat.Mode()&os.ModeSymlink != 0, nil
}
func Create(source, target string, flags uint32) error {
return os.Symlink(osutil.NativeFilename(target), source)
}

View File

@@ -69,8 +69,8 @@ func init() {
return
}
isLink, err := IsSymlink(path)
if err != nil || !isLink {
stat, err := os.Lstat(path)
if err != nil || stat.Mode()&os.ModeSymlink == 0 {
return
}
@@ -139,19 +139,6 @@ func Read(path string) (string, uint32, error) {
return osutil.NormalizedFilename(data.PrintName()), flags, nil
}
func IsSymlink(path string) (bool, error) {
ptr, err := syscall.UTF16PtrFromString(path)
if err != nil {
return false, err
}
attr, err := syscall.GetFileAttributes(ptr)
if err != nil {
return false, err
}
return attr&FILE_ATTRIBUTE_REPARSE_POINT != 0, nil
}
func Create(source, target string, flags uint32) error {
srcp, err := syscall.UTF16PtrFromString(source)
if err != nil {

View File

@@ -0,0 +1,25 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package upgrade
import (
"fmt"
"runtime"
)
func releaseName(tag string) string {
return fmt.Sprintf("syncthing-macosx-%s-%s.", runtime.GOARCH, tag)
}

View File

@@ -0,0 +1,38 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package upgrade
import (
"fmt"
"strings"
"syscall"
)
func releaseName(tag string) string {
return fmt.Sprintf("syncthing-linux-armv%s-%s.", goARM(), tag)
}
// Get the current ARM architecture version for upgrade purposes. If we can't
// figure it out from the uname, default to ARMv6 (same as Go distribution).
func goARM() string {
var name syscall.Utsname
syscall.Uname(&name)
machine := string(name.Machine[:5])
if strings.HasPrefix(machine, "armv") {
return machine[4:]
}
return "6"
}

View File

@@ -0,0 +1,27 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !arm,!darwin
package upgrade
import (
"fmt"
"runtime"
)
func releaseName(tag string) string {
return fmt.Sprintf("syncthing-%s-%s-%s.", runtime.GOOS, runtime.GOARCH, tag)
}

View File

@@ -48,7 +48,7 @@ func init() {
}
// A wrapper around actual implementations
func UpgradeTo(rel Release, archExtra string) error {
func To(rel Release) error {
select {
case <-upgradeUnlocked:
path, err := osext.Executable()
@@ -56,7 +56,7 @@ func UpgradeTo(rel Release, archExtra string) error {
upgradeUnlocked <- true
return err
}
err = upgradeTo(path, rel, archExtra)
err = upgradeTo(path, rel)
// If we've failed to upgrade, unlock so that another attempt could be made
if err != nil {
upgradeUnlocked <- true

View File

@@ -28,19 +28,12 @@ import (
"os"
"path"
"path/filepath"
"runtime"
"strings"
)
// Upgrade to the given release, saving the previous binary with a ".old" extension.
func upgradeTo(path string, rel Release, archExtra string) error {
osName := runtime.GOOS
if osName == "darwin" {
// We call the darwin release bundles macosx because that makes more
// sense for people downloading them
osName = "macosx"
}
expectedRelease := fmt.Sprintf("syncthing-%s-%s%s-%s.", osName, runtime.GOARCH, archExtra, rel.Tag)
func upgradeTo(path string, rel Release) error {
expectedRelease := releaseName(rel.Tag)
if debug {
l.Debugf("expected release asset %q", expectedRelease)
}
@@ -93,15 +86,15 @@ func LatestRelease(prerelease bool) (Release, error) {
if prerelease {
// We are a beta version. Use the latest.
return rels[0], nil
} else {
// We are a regular release. Only consider non-prerelease versions for upgrade.
for _, rel := range rels {
if !rel.Prerelease {
return rel, nil
}
}
return Release{}, ErrVersionUnknown
}
// We are a regular release. Only consider non-prerelease versions for upgrade.
for _, rel := range rels {
if !rel.Prerelease {
return rel, nil
}
}
return Release{}, ErrVersionUnknown
}
func readTarGZ(url string, dir string) (string, error) {

View File

@@ -17,7 +17,7 @@
package upgrade
func upgradeTo(path string, rel Release, extra string) error {
func upgradeTo(path string, rel Release) error {
return ErrUpgradeUnsupported
}

View File

@@ -28,13 +28,12 @@ import (
"os"
"path"
"path/filepath"
"runtime"
"strings"
)
// Upgrade to the given release, saving the previous binary with a ".old" extension.
func upgradeTo(path string, rel Release, archExtra string) error {
expectedRelease := fmt.Sprintf("syncthing-%s-%s%s-%s.", runtime.GOOS, runtime.GOARCH, archExtra, rel.Tag)
func upgradeTo(path string, rel Release) error {
expectedRelease := releaseName(rel.Tag)
if debug {
l.Debugf("expected release asset %q", expectedRelease)
}

View File

@@ -101,8 +101,8 @@ type upnpRoot struct {
// Discover discovers UPnP InternetGatewayDevices.
// The order in which the devices appear in the result list is not deterministic.
func Discover() []*IGD {
result := make([]*IGD, 0)
func Discover() []IGD {
var result []IGD
l.Infoln("Starting UPnP discovery...")
timeout := 3
@@ -137,7 +137,7 @@ func Discover() []*IGD {
// Search for UPnP InternetGatewayDevices for <timeout> seconds, ignoring responses from any devices listed in knownDevices.
// The order in which the devices appear in the result list is not deterministic
func discover(deviceType string, timeout int, knownDevices []*IGD) []*IGD {
func discover(deviceType string, timeout int, knownDevices []IGD) []IGD {
ssdp := &net.UDPAddr{IP: []byte{239, 255, 255, 250}, Port: 1900}
tpl := `M-SEARCH * HTTP/1.1
@@ -155,8 +155,8 @@ Mx: %d
l.Debugln("Starting discovery of device type " + deviceType + "...")
}
results := make([]*IGD, 0)
resultChannel := make(chan *IGD, 8)
var results []IGD
resultChannel := make(chan IGD, 8)
socket, err := net.ListenUDP("udp4", &net.UDPAddr{})
if err != nil {
@@ -231,7 +231,7 @@ Mx: %d
return results
}
func handleSearchResponse(deviceType string, knownDevices []*IGD, resp []byte, length int, resultChannel chan<- *IGD, resultWaitGroup *sync.WaitGroup) {
func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, length int, resultChannel chan<- IGD, resultWaitGroup *sync.WaitGroup) {
defer resultWaitGroup.Done() // Signal when we've finished processing
if debug {
@@ -321,7 +321,7 @@ func handleSearchResponse(deviceType string, knownDevices []*IGD, resp []byte, l
return
}
igd := &IGD{
igd := IGD{
uuid: deviceUUID,
friendlyName: upnpRoot.Device.FriendlyName,
url: deviceDescriptionURL,
@@ -352,7 +352,7 @@ func localIP(url *url.URL) (string, error) {
}
func getChildDevices(d upnpDevice, deviceType string) []upnpDevice {
result := make([]upnpDevice, 0)
var result []upnpDevice
for _, dev := range d.Devices {
if dev.DeviceType == deviceType {
result = append(result, dev)
@@ -362,7 +362,7 @@ func getChildDevices(d upnpDevice, deviceType string) []upnpDevice {
}
func getChildServices(d upnpDevice, serviceType string) []upnpService {
result := make([]upnpService, 0)
var result []upnpService
for _, svc := range d.Services {
if svc.ServiceType == serviceType {
result = append(result, svc)
@@ -372,7 +372,7 @@ func getChildServices(d upnpDevice, serviceType string) []upnpService {
}
func getServiceDescriptions(rootURL string, device upnpDevice) ([]IGDService, error) {
result := make([]IGDService, 0)
var result []IGDService
if device.DeviceType == "urn:schemas-upnp-org:device:InternetGatewayDevice:1" {
descriptions := getIGDServices(rootURL, device,
@@ -400,7 +400,7 @@ func getServiceDescriptions(rootURL string, device upnpDevice) ([]IGDService, er
}
func getIGDServices(rootURL string, device upnpDevice, wanDeviceURN string, wanConnectionURN string, serviceURNs []string) []IGDService {
result := make([]IGDService, 0)
var result []IGDService
devices := getChildDevices(device, wanDeviceURN)

View File

@@ -21,7 +21,7 @@ import (
)
func TestExternalIPParsing(t *testing.T) {
soap_response :=
soapResponse :=
[]byte(`<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetExternalIPAddressResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
@@ -31,7 +31,7 @@ func TestExternalIPParsing(t *testing.T) {
</s:Envelope>`)
envelope := &soapGetExternalIPAddressResponseEnvelope{}
err := xml.Unmarshal(soap_response, envelope)
err := xml.Unmarshal(soapResponse, envelope)
if err != nil {
t.Error(err)
}

View File

@@ -18,7 +18,6 @@ package versioner
import (
"os"
"path/filepath"
"sort"
"strconv"
"github.com/syncthing/syncthing/internal/osutil"
@@ -56,16 +55,15 @@ func NewSimple(folderID, folderPath string, params map[string]string) Versioner
// Move away the named file to a version archive. If this function returns
// nil, the named file does not exist any more (has been archived).
func (v Simple) Archive(filePath string) error {
fileInfo, err := os.Stat(filePath)
fileInfo, err := os.Lstat(filePath)
if err != nil {
if os.IsNotExist(err) {
if debug {
l.Debugln("not archiving nonexistent file", filePath)
}
return nil
} else {
return err
}
return err
}
versionsDir := filepath.Join(v.folderPath, ".stversions")
@@ -124,10 +122,9 @@ func (v Simple) Archive(filePath string) error {
// Use all the found filenames. "~" sorts after "." so all old pattern
// files will be deleted before any new, which is as it should be.
versions := append(oldVersions, newVersions...)
versions := uniqueSortedStrings(append(oldVersions, newVersions...))
if len(versions) > v.keep {
sort.Strings(versions)
for _, toRemove := range versions[:len(versions)-v.keep] {
if debug {
l.Debugln("cleaning out", toRemove)

View File

@@ -18,7 +18,6 @@ package versioner
import (
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
@@ -46,16 +45,6 @@ type Staggered struct {
mutex *sync.Mutex
}
// Check if file or dir
func isFile(path string) bool {
fileInfo, err := os.Stat(path)
if err != nil {
l.Infoln("versioner isFile:", err)
return false
}
return fileInfo.Mode().IsRegular()
}
// Rename versions with old version format
func (v Staggered) renameOld() {
err := filepath.Walk(v.versionsPath, func(path string, f os.FileInfo, err error) error {
@@ -168,14 +157,16 @@ func (v Staggered) clean() {
if err != nil {
return err
}
switch mode := f.Mode(); {
case mode.IsDir():
if f.Mode().IsDir() && f.Mode()&os.ModeSymlink == 0 {
filesPerDir[path] = 0
if path != v.versionsPath {
dir := filepath.Dir(path)
filesPerDir[dir]++
}
case mode.IsRegular():
} else {
// Regular file, or possibly a symlink.
extension := filenameTag(path)
dir := filepath.Dir(path)
name := path[:len(path)-len(extension)-1]
@@ -228,56 +219,63 @@ func (v Staggered) expire(versions []string) {
var prevAge int64
firstFile := true
for _, file := range versions {
if isFile(file) {
versionTime, err := time.Parse(TimeFormat, filenameTag(file))
if err != nil {
l.Infof("Versioner: file name %q is invalid: %v", file, err)
continue
}
age := int64(time.Since(versionTime).Seconds())
// If the file is older than the max age of the last interval, remove it
if lastIntv := v.interval[len(v.interval)-1]; lastIntv.end > 0 && age > lastIntv.end {
if debug {
l.Debugln("Versioner: File over maximum age -> delete ", file)
}
err = os.Remove(file)
if err != nil {
l.Warnf("Versioner: can't remove %q: %v", file, err)
}
continue
}
// If it's the first (oldest) file in the list we can skip the interval checks
if firstFile {
prevAge = age
firstFile = false
continue
}
// Find the interval the file fits in
var usedInterval Interval
for _, usedInterval = range v.interval {
if age < usedInterval.end {
break
}
}
if prevAge-age < usedInterval.step {
if debug {
l.Debugln("too many files in step -> delete", file)
}
err = os.Remove(file)
if err != nil {
l.Warnf("Versioner: can't remove %q: %v", file, err)
}
continue
}
prevAge = age
} else {
l.Infof("non-file %q is named like a file version", file)
fi, err := os.Stat(file)
if err != nil {
l.Warnln("versioner:", err)
continue
}
if fi.IsDir() {
l.Infof("non-file %q is named like a file version", file)
continue
}
versionTime, err := time.Parse(TimeFormat, filenameTag(file))
if err != nil {
l.Infof("Versioner: file name %q is invalid: %v", file, err)
continue
}
age := int64(time.Since(versionTime).Seconds())
// If the file is older than the max age of the last interval, remove it
if lastIntv := v.interval[len(v.interval)-1]; lastIntv.end > 0 && age > lastIntv.end {
if debug {
l.Debugln("Versioner: File over maximum age -> delete ", file)
}
err = os.Remove(file)
if err != nil {
l.Warnf("Versioner: can't remove %q: %v", file, err)
}
continue
}
// If it's the first (oldest) file in the list we can skip the interval checks
if firstFile {
prevAge = age
firstFile = false
continue
}
// Find the interval the file fits in
var usedInterval Interval
for _, usedInterval = range v.interval {
if age < usedInterval.end {
break
}
}
if prevAge-age < usedInterval.step {
if debug {
l.Debugln("too many files in step -> delete", file)
}
err = os.Remove(file)
if err != nil {
l.Warnf("Versioner: can't remove %q: %v", file, err)
}
continue
}
prevAge = age
}
}
@@ -290,20 +288,17 @@ func (v Staggered) Archive(filePath string) error {
v.mutex.Lock()
defer v.mutex.Unlock()
fileInfo, err := os.Stat(filePath)
if err != nil {
if _, err := os.Lstat(filePath); err != nil {
if os.IsNotExist(err) {
if debug {
l.Debugln("not archiving nonexistent file", filePath)
}
return nil
} else {
return err
}
return err
}
_, err = os.Stat(v.versionsPath)
if err != nil {
if _, err := os.Stat(v.versionsPath); err != nil {
if os.IsNotExist(err) {
if debug {
l.Debugln("creating versions dir", v.versionsPath)
@@ -331,7 +326,7 @@ func (v Staggered) Archive(filePath string) error {
return err
}
ver := taggedFilename(file, fileInfo.ModTime().Format(TimeFormat))
ver := taggedFilename(file, time.Now().Format(TimeFormat))
dst := filepath.Join(dir, ver)
if debug {
l.Debugln("moving to", dst)
@@ -357,9 +352,7 @@ func (v Staggered) Archive(filePath string) error {
// Use all the found filenames.
versions := append(oldVersions, newVersions...)
sort.Strings(versions)
v.expire(versions)
v.expire(uniqueSortedStrings(versions))
return nil
}

View File

@@ -18,6 +18,7 @@ package versioner
import (
"path/filepath"
"regexp"
"sort"
)
// Inserts ~tag just before the extension of the filename.
@@ -40,3 +41,17 @@ func filenameTag(path string) string {
}
return match[1]
}
func uniqueSortedStrings(strings []string) []string {
seen := make(map[string]struct{}, len(strings))
unique := make([]string, 0, len(strings))
for _, str := range strings {
_, ok := seen[str]
if !ok {
seen[str] = struct{}{}
unique = append(unique, str)
}
}
sort.Strings(unique)
return unique
}

1
test/.gitignore vendored
View File

@@ -16,3 +16,4 @@ csrftokens.txt
s4d
http
index
*.syncthing-reset*

143
test/cli_test.go Normal file
View File

@@ -0,0 +1,143 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
// +build integration
package integration_test
import (
"os"
"os/exec"
"testing"
"time"
)
func TestCLIReset(t *testing.T) {
dirs := []string{"s1", "s12-1", "h1/index"}
// Create directories that reset will remove
for _, dir := range dirs {
err := os.Mkdir(dir, 0755)
if err != nil && !os.IsExist(err) {
t.Fatal(err)
}
}
// Run reset to clean up
cmd := exec.Command("../bin/syncthing", "-home", "h1", "-reset")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stdout
err := cmd.Run()
if err != nil {
t.Fatal(err)
}
// Verify that they're gone
for _, dir := range dirs {
_, err := os.Stat(dir)
if err == nil {
t.Errorf("%s still exists", dir)
}
}
}
func TestCLIGenerate(t *testing.T) {
err := os.RemoveAll("home.out")
if err != nil {
t.Fatal(err)
}
// -generate should create a bunch of stuff
cmd := exec.Command("../bin/syncthing", "-generate", "home.out")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stdout
err = cmd.Run()
if err != nil {
t.Fatal(err)
}
// Verify that the files that should have been created have been
for _, f := range []string{"home.out/config.xml", "home.out/cert.pem", "home.out/key.pem"} {
_, err := os.Stat(f)
if err != nil {
t.Errorf("%s is not correctly generated", f)
}
}
}
func TestCLIFirstStartup(t *testing.T) {
err := os.RemoveAll("home.out")
if err != nil {
t.Fatal(err)
}
// First startup should create config, BEP certificate, and HTTP certificate.
cmd := exec.Command("../bin/syncthing", "-home", "home.out")
cmd.Env = append(os.Environ(), "STNORESTART=1")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stdout
err = cmd.Start()
if err != nil {
t.Fatal(err)
}
exitError := make(chan error, 1)
filesOk := make(chan struct{})
processDone := make(chan struct{})
go func() {
// Wait for process exit.
exitError <- cmd.Wait()
close(processDone)
}()
go func() {
again:
for {
select {
case <-processDone:
return
default:
// Verify that the files that should have been created have been
for _, f := range []string{"home.out/config.xml", "home.out/cert.pem", "home.out/key.pem", "home.out/https-cert.pem", "home.out/https-key.pem"} {
_, err := os.Stat(f)
if err != nil {
time.Sleep(500 * time.Millisecond)
continue again
}
}
// Make sure the process doesn't exit with an error just after creating certificates.
time.Sleep(time.Second)
filesOk <- struct{}{}
return
}
}
}()
select {
case e := <-exitError:
t.Error(e)
case <-filesOk:
cmd.Process.Kill()
return
}
}

View File

@@ -48,10 +48,8 @@ const (
var env = []string{
"HOME=.",
"STTRACE=model,protocol",
"STGUIAPIKEY=" + apiKey,
"STNORESTART=1",
"STPERFSTATS=1",
}
type syncthingProcess struct {
@@ -78,7 +76,7 @@ func (p *syncthingProcess) start() error {
cmd := exec.Command("../bin/syncthing", p.argv...)
cmd.Stdout = p.logfd
cmd.Stderr = p.logfd
cmd.Env = append(env, fmt.Sprintf("STPROFILER=:%d", p.port+1000))
cmd.Env = append(os.Environ(), env...)
err := cmd.Start()
if err != nil {
@@ -97,7 +95,7 @@ func (p *syncthingProcess) start() error {
}
func (p *syncthingProcess) stop() error {
p.cmd.Process.Signal(os.Interrupt)
p.cmd.Process.Signal(os.Kill)
p.cmd.Wait()
fd, err := os.Open(p.log)
@@ -106,21 +104,27 @@ func (p *syncthingProcess) stop() error {
}
defer fd.Close()
raceCondition := []byte("DATA RACE")
raceConditionStart := []byte("WARNING: DATA RACE")
raceConditionSep := []byte("==================")
sc := bufio.NewScanner(fd)
race := false
for sc.Scan() {
line := sc.Bytes()
if bytes.Contains(line, raceCondition) {
name := fmt.Sprintf("race-%d.out", time.Now().Unix())
cp, _ := os.Create(name)
fd.Seek(0, os.SEEK_SET)
io.Copy(cp, fd)
cp.Close()
return errors.New("Race condition detected in " + name)
if race {
fmt.Printf("%s\n", line)
if bytes.Contains(line, raceConditionSep) {
race = false
}
} else if bytes.Contains(line, raceConditionStart) {
fmt.Printf("%s\n", raceConditionSep)
fmt.Printf("%s\n", raceConditionStart)
race = true
if err == nil {
err = errors.New("Race condition detected")
}
}
}
return nil
return err
}
func (p *syncthingProcess) get(path string) (*http.Response, error) {
@@ -149,7 +153,7 @@ func (p *syncthingProcess) get(path string) (*http.Response, error) {
func (p *syncthingProcess) post(path string, data io.Reader) (*http.Response, error) {
client := &http.Client{
Timeout: 2 * time.Second,
Timeout: 600 * time.Second,
Transport: &http.Transport{
DisableKeepAlives: true,
},
@@ -314,6 +318,16 @@ func (i *inifiteReader) Read(bs []byte) (int, error) {
// rm -rf
func removeAll(dirs ...string) error {
for _, dir := range dirs {
// Set any non-writeable files and dirs to writeable. This is necessary for os.RemoveAll to work on Windows.
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.Mode()&0700 != 0700 {
os.Chmod(path, 0777)
}
return nil
})
os.RemoveAll(dir)
}
return nil
@@ -373,9 +387,12 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) {
if rn == "." || rn == ".stfolder" {
return nil
}
if rn == ".stversions" {
return filepath.SkipDir
}
var f fileInfo
if ok, err := symlinks.IsSymlink(path); err == nil && ok {
if info.Mode()&os.ModeSymlink != 0 {
f = fileInfo{
name: rn,
mode: os.ModeSymlink,

View File

@@ -24,9 +24,53 @@ import (
"strings"
"testing"
"time"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/protocol"
)
func TestFiletypeChange(t *testing.T) {
func TestFileTypeChange(t *testing.T) {
// Use no versioning
id, _ := protocol.DeviceIDFromString(id2)
cfg, _ := config.Load("h2/config.xml", id)
fld := cfg.Folders()["default"]
fld.Versioning = config.VersioningConfiguration{}
cfg.SetFolder(fld)
cfg.Save()
testFileTypeChange(t)
}
func TestFileTypeChangeSimpleVersioning(t *testing.T) {
// Use simple versioning
id, _ := protocol.DeviceIDFromString(id2)
cfg, _ := config.Load("h2/config.xml", id)
fld := cfg.Folders()["default"]
fld.Versioning = config.VersioningConfiguration{
Type: "simple",
Params: map[string]string{"keep": "5"},
}
cfg.SetFolder(fld)
cfg.Save()
testFileTypeChange(t)
}
func TestFileTypeChangeStaggeredVersioning(t *testing.T) {
// Use staggered versioning
id, _ := protocol.DeviceIDFromString(id2)
cfg, _ := config.Load("h2/config.xml", id)
fld := cfg.Folders()["default"]
fld.Versioning = config.VersioningConfiguration{
Type: "staggered",
}
cfg.SetFolder(fld)
cfg.Save()
testFileTypeChange(t)
}
func testFileTypeChange(t *testing.T) {
log.Println("Cleaning...")
err := removeAll("s1", "s2", "h1/index", "h2/index")
if err != nil {

View File

@@ -1,16 +1,22 @@
<configuration version="6">
<configuration version="7">
<folder id="default" path="s1" ro="false" rescanIntervalS="10" ignorePerms="false">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
<copiers>1</copiers>
<pullers>16</pullers>
<finishers>1</finishers>
</folder>
<folder id="s12" path="s12-1" ro="false" rescanIntervalS="10" ignorePerms="false">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
<copiers>1</copiers>
<pullers>16</pullers>
<finishers>1</finishers>
</folder>
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK" name="s4" compression="true" introducer="false">
<address>127.0.0.1:22004</address>
@@ -32,7 +38,7 @@
</gui>
<options>
<listenAddress>127.0.0.1:22001</listenAddress>
<globalAnnounceServer>announce.syncthing.net:22026</globalAnnounceServer>
<globalAnnounceServer>udp4://announce.syncthing.net:22026</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
@@ -45,9 +51,12 @@
<upnpLeaseMinutes>0</upnpLeaseMinutes>
<upnpRenewalMinutes>30</upnpRenewalMinutes>
<urAccepted>-1</urAccepted>
<urUniqueID></urUniqueID>
<restartOnWakeup>true</restartOnWakeup>
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
<keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
<progressUpdateIntervalS>5</progressUpdateIntervalS>
<symlinksEnabled>true</symlinksEnabled>
</options>
</configuration>

View File

@@ -1,4 +1,4 @@
<configuration version="6">
<configuration version="7">
<folder id="default" path="s2" ro="false" rescanIntervalS="15" ignorePerms="false">
<device id="AF2HXQA-DOKKIMI-PKOG4RE-E25UTJ7-PSGQ7T5-WEY7YT5-SG6N7W5-NNA4BQM"></device>
<device id="AND7GW6-DZN66F2-TKYJSTC-ACI7MYT-75X4T63-SE5S4MQ-GBSDHDL-4CLHJA6"></device>
@@ -430,7 +430,7 @@
</gui>
<options>
<listenAddress>127.0.0.1:22002</listenAddress>
<globalAnnounceServer>announce.syncthing.net:22026</globalAnnounceServer>
<globalAnnounceServer>udp4://announce.syncthing.net:22026</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
@@ -449,5 +449,6 @@
<keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
<progressUpdateIntervalS>5</progressUpdateIntervalS>
<symlinksEnabled>true</symlinksEnabled>
</options>
</configuration>

View File

@@ -1,9 +1,12 @@
<configuration version="6">
<configuration version="7">
<folder id="s23" path="s23-3" ro="false" rescanIntervalS="20" ignorePerms="false">
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
<copiers>1</copiers>
<pullers>16</pullers>
<finishers>1</finishers>
</folder>
<folder id="default" path="s3" ro="false" rescanIntervalS="20" ignorePerms="false">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
@@ -13,6 +16,9 @@
<param key="keep" val="5"></param>
</versioning>
<lenientMtimes>false</lenientMtimes>
<copiers>1</copiers>
<pullers>16</pullers>
<finishers>1</finishers>
</folder>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="true" introducer="false">
<address>127.0.0.1:22001</address>
@@ -29,7 +35,7 @@
</gui>
<options>
<listenAddress>127.0.0.1:22003</listenAddress>
<globalAnnounceServer>announce.syncthing.net:22026</globalAnnounceServer>
<globalAnnounceServer>udp4://announce.syncthing.net:22026</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>false</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
@@ -42,9 +48,12 @@
<upnpLeaseMinutes>0</upnpLeaseMinutes>
<upnpRenewalMinutes>30</upnpRenewalMinutes>
<urAccepted>-1</urAccepted>
<urUniqueID></urUniqueID>
<restartOnWakeup>true</restartOnWakeup>
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
<keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
<progressUpdateIntervalS>5</progressUpdateIntervalS>
<symlinksEnabled>true</symlinksEnabled>
</options>
</configuration>

View File

@@ -1,4 +1,4 @@
<configuration version="6">
<configuration version="7">
<folder id="unique" path="s4" ro="false" rescanIntervalS="60" ignorePerms="false">
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK"></device>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
@@ -6,12 +6,18 @@
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
<copiers>1</copiers>
<pullers>16</pullers>
<finishers>1</finishers>
</folder>
<folder id="default" path="s4d" ro="false" rescanIntervalS="60" ignorePerms="false">
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK"></device>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
<copiers>1</copiers>
<pullers>16</pullers>
<finishers>1</finishers>
</folder>
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK" name="s4" compression="true" introducer="false">
<address>dynamic</address>
@@ -31,7 +37,7 @@
</gui>
<options>
<listenAddress>:22004</listenAddress>
<globalAnnounceServer>announce.syncthing.net:22026</globalAnnounceServer>
<globalAnnounceServer>udp4://announce.syncthing.net:22026</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>false</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
@@ -44,9 +50,12 @@
<upnpLeaseMinutes>0</upnpLeaseMinutes>
<upnpRenewalMinutes>30</upnpRenewalMinutes>
<urAccepted>-1</urAccepted>
<urUniqueID></urUniqueID>
<restartOnWakeup>true</restartOnWakeup>
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
<keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
<progressUpdateIntervalS>5</progressUpdateIntervalS>
<symlinksEnabled>true</symlinksEnabled>
</options>
</configuration>

View File

@@ -0,0 +1,91 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
// +build integration
package integration_test
import (
"io/ioutil"
"log"
"sync"
"testing"
"time"
)
func TestParallellScan(t *testing.T) {
log.Println("Cleaning...")
err := removeAll("s1", "h1/index")
if err != nil {
t.Fatal(err)
}
log.Println("Generating files...")
err = generateFiles("s1", 5000, 18, "../LICENSE")
if err != nil {
t.Fatal(err)
}
log.Println("Generaing .stignore...")
err = ioutil.WriteFile("s1/.stignore", []byte("some ignore data\n"), 0644)
if err != nil {
t.Fatal(err)
}
log.Println("Starting up...")
st := syncthingProcess{ // id1
log: "1.out",
argv: []string{"-home", "h1"},
port: 8081,
apiKey: apiKey,
}
err = st.start()
if err != nil {
t.Fatal(err)
}
time.Sleep(5 * time.Second)
var wg sync.WaitGroup
log.Println("Starting scans...")
for j := 0; j < 20; j++ {
j := j
wg.Add(1)
go func() {
defer wg.Done()
resp, err := st.post("/rest/scan?folder=default", nil)
log.Println(j)
if err != nil {
log.Println(err)
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("%d != 200", resp.StatusCode)
}
resp.Body.Close()
}()
}
wg.Wait()
log.Println("Scans done")
time.Sleep(2 * time.Second)
// This is where the real test is currently, since stop() checks for data
// race output in the log.
log.Println("Stopping...")
err = st.stop()
if err != nil {
t.Fatal(err)
}
}

View File

@@ -81,7 +81,8 @@ func testRestartDuringTransfer(t *testing.T, restartSender, restartReceiver bool
for {
comp, err := sender.peerCompletion()
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
if strings.Contains(err.Error(), "use of closed network connection") ||
strings.Contains(err.Error(), "request cancelled while waiting") {
time.Sleep(250 * time.Millisecond)
continue
}

View File

@@ -18,16 +18,83 @@
package integration_test
import (
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/symlinks"
)
func symlinksSupported() bool {
tmp, err := ioutil.TempDir("", "symlink-test")
if err != nil {
return false
}
defer os.RemoveAll(tmp)
err = os.Symlink("tmp", filepath.Join(tmp, "link"))
return err == nil
}
func TestSymlinks(t *testing.T) {
if !symlinksSupported() {
t.Skip("symlinks unsupported")
}
// Use no versioning
id, _ := protocol.DeviceIDFromString(id2)
cfg, _ := config.Load("h2/config.xml", id)
fld := cfg.Folders()["default"]
fld.Versioning = config.VersioningConfiguration{}
cfg.SetFolder(fld)
cfg.Save()
testSymlinks(t)
}
func TestSymlinksSimpleVersioning(t *testing.T) {
if !symlinksSupported() {
t.Skip("symlinks unsupported")
}
// Use no versioning
id, _ := protocol.DeviceIDFromString(id2)
cfg, _ := config.Load("h2/config.xml", id)
fld := cfg.Folders()["default"]
fld.Versioning = config.VersioningConfiguration{
Type: "simple",
Params: map[string]string{"keep": "5"},
}
cfg.SetFolder(fld)
cfg.Save()
testSymlinks(t)
}
func TestSymlinksStaggeredVersioning(t *testing.T) {
if !symlinksSupported() {
t.Skip("symlinks unsupported")
}
// Use no versioning
id, _ := protocol.DeviceIDFromString(id2)
cfg, _ := config.Load("h2/config.xml", id)
fld := cfg.Folders()["default"]
fld.Versioning = config.VersioningConfiguration{
Type: "staggered",
}
cfg.SetFolder(fld)
cfg.Save()
testSymlinks(t)
}
func testSymlinks(t *testing.T) {
log.Println("Cleaning...")
err := removeAll("s1", "s2", "h1/index", "h2/index")
if err != nil {
@@ -99,6 +166,13 @@ func TestSymlinks(t *testing.T) {
log.Fatal(err)
}
// A link we will remove later
err = symlinks.Create("s1/removeLink", "does/not/exist", 0)
if err != nil {
log.Fatal(err)
}
// Verify that the files and symlinks sync to the other side
log.Println("Syncing...")
@@ -229,6 +303,13 @@ func TestSymlinks(t *testing.T) {
log.Fatal(err)
}
// Remove a broken symlink
err = os.Remove("s1/removeLink")
if err != nil {
log.Fatal(err)
}
// Sync these changes and recheck
log.Println("Syncing...")