mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-04 11:59:12 -05:00
Compare commits
118 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e91f9a944e | ||
|
|
e7ddc7cf0f | ||
|
|
40dfa48756 | ||
|
|
579f92cf5f | ||
|
|
4565125da9 | ||
|
|
ce13a01e65 | ||
|
|
618a8682b7 | ||
|
|
963077f918 | ||
|
|
3704d2d86b | ||
|
|
fc6a029311 | ||
|
|
7c7b1e6c2d | ||
|
|
892920039d | ||
|
|
51cdd38c3e | ||
|
|
80977bd4c0 | ||
|
|
d8022f94ef | ||
|
|
1c43587d7d | ||
|
|
b2ed32b118 | ||
|
|
0cc815d816 | ||
|
|
d452b7593f | ||
|
|
5346bdc683 | ||
|
|
dc5c1e2002 | ||
|
|
2e48e298a2 | ||
|
|
7a1aaaf5c4 | ||
|
|
bde92d5cfe | ||
|
|
691f0f4845 | ||
|
|
fdd458d2fe | ||
|
|
d2c0b8374a | ||
|
|
c96c78892d | ||
|
|
957643f523 | ||
|
|
749bbec566 | ||
|
|
25e363c5fb | ||
|
|
febeed3277 | ||
|
|
9d07aa006d | ||
|
|
12d69e25dd | ||
|
|
0c9f1efc75 | ||
|
|
665b4506e7 | ||
|
|
cb5548ceb8 | ||
|
|
c9492e54f7 | ||
|
|
12490eafff | ||
|
|
6e83d11d5f | ||
|
|
9c6aedc91b | ||
|
|
a9339d0627 | ||
|
|
4d9aa10532 | ||
|
|
b00264b594 | ||
|
|
e329c7015e | ||
|
|
1392cfc72d | ||
|
|
c6688d8f89 | ||
|
|
87abea0ba3 | ||
|
|
f9fcb44f3c | ||
|
|
996cbbca38 | ||
|
|
581f4b89bd | ||
|
|
88a347dce0 | ||
|
|
3e7b197a1d | ||
|
|
94ab06e92f | ||
|
|
d38c81fcff | ||
|
|
3e26fdfb67 | ||
|
|
e1be73232d | ||
|
|
06fd2268d9 | ||
|
|
15251dfae1 | ||
|
|
f62812a8dc | ||
|
|
c6041d2590 | ||
|
|
c7e779107c | ||
|
|
7cd25c919f | ||
|
|
47d67d3985 | ||
|
|
1a7921b46c | ||
|
|
43d569741b | ||
|
|
52c6869eab | ||
|
|
6dff9097a2 | ||
|
|
4ff211662a | ||
|
|
05eab51a0d | ||
|
|
604a4e7dbc | ||
|
|
80dca96ee8 | ||
|
|
7f97037190 | ||
|
|
b658afd857 | ||
|
|
992ad97ad5 | ||
|
|
5af6cbae2c | ||
|
|
2abe792f36 | ||
|
|
1ff9bb8fdc | ||
|
|
5cb1039daf | ||
|
|
591c5dabf4 | ||
|
|
770fff287e | ||
|
|
cea7a179ae | ||
|
|
d80c40cfbf | ||
|
|
12e83374e9 | ||
|
|
98344d2e5e | ||
|
|
99dc1eec50 | ||
|
|
2a886576a6 | ||
|
|
919d005550 | ||
|
|
97abdaca5a | ||
|
|
9cc8b7c858 | ||
|
|
0726472b91 | ||
|
|
3cbe92d797 | ||
|
|
72a278c9ed | ||
|
|
e567c8adce | ||
|
|
dde8045109 | ||
|
|
c922c4c383 | ||
|
|
bc8907e90d | ||
|
|
a8ba7786ae | ||
|
|
c734e48ad0 | ||
|
|
d30d0b29a9 | ||
|
|
2912defb97 | ||
|
|
69f8ac6b56 | ||
|
|
e7441ff6e8 | ||
|
|
8a34158fa4 | ||
|
|
8d2a6d96f2 | ||
|
|
bb50b677c7 | ||
|
|
0fde4b3b2e | ||
|
|
ee9c109f07 | ||
|
|
1219423091 | ||
|
|
cf00ab854f | ||
|
|
c417dcb7e2 | ||
|
|
7ad711f554 | ||
|
|
2d7b0cf94d | ||
|
|
d669c07e8a | ||
|
|
8bd52946b4 | ||
|
|
27e81637be | ||
|
|
5c67e27a30 | ||
|
|
59af9809fe |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ bin
|
||||
perfstats*.csv
|
||||
coverage.xml
|
||||
!gui/scripts/syncthing
|
||||
.DS_Store
|
||||
|
||||
3
AUTHORS
3
AUTHORS
@@ -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
92
CONDUCT.md
Normal 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)*
|
||||
@@ -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
2
Godeps/Godeps.json
generated
@@ -23,7 +23,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/xdr",
|
||||
"Rev": "ec3d404f43731551258977b38dd72cf557d00398"
|
||||
"Rev": "45c46b7db7ff83b8b9ee09bbd95f36ab50043ece"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/juju/ratelimit",
|
||||
|
||||
112
Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
generated
vendored
112
Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
generated
vendored
@@ -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
34
NICKS
Normal 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>
|
||||
18
build.go
18
build.go
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
10
build.sh
10
build.sh
@@ -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
9
changelog.sh
Executable 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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
89
cmd/stfileinfo/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
68
cmd/syncthing/random.go
Normal 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:]))
|
||||
}
|
||||
80
cmd/syncthing/random_test.go
Normal file
80
cmd/syncthing/random_test.go
Normal 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])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
22
docker/go-9102.patch
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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": "Наслагващи се Файлови Версии",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
142
gui/assets/lang/lang-es.json
Normal file
142
gui/assets/lang/lang-es.json
Normal 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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "最新",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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> {{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> <span translate>Rescan Interval</span></th>
|
||||
<td class="text-right">{{folder.RescanIntervalS}} s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-share-alt"></span> <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> <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> <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> <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> <span translate>Rescan</span></button>
|
||||
<button class="btn btn-sm btn-default" href="" ng-click="editFolder(folder)"><span class="glyphicon glyphicon-pencil"></span> <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> <span translate>Global Discovery Servers</span></th>
|
||||
<th><span class="glyphicon glyphicon-bullhorn"></span> <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> {{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> <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> <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 © 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 © 2012 The Go Authors.</li>
|
||||
<li><a href="https://bitbucket.org/kardianos/osext">kardianos/osext</a>, Copyright © 2012 Daniel Theophanes.</li>
|
||||
<li><a href="https://code.google.com/p/snappy-go/">snappy-go</a>, Copyright © 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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": "элементы"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
88
gui/scripts/syncthing/core/services/localeService.js
Normal file
88
gui/scripts/syncthing/core/services/localeService.js
Normal 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
|
||||
}
|
||||
}];
|
||||
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
1
internal/config/testdata/overridenvalues.xml
vendored
1
internal/config/testdata/overridenvalues.xml
vendored
@@ -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
12
internal/config/testdata/v7.xml
vendored
Normal 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>
|
||||
@@ -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
|
||||
|
||||
59
internal/discover/client.go
Normal file
59
internal/discover/client.go
Normal 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()
|
||||
}
|
||||
227
internal/discover/client_test.go
Normal file
227
internal/discover/client_test.go
Normal 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()
|
||||
}
|
||||
246
internal/discover/client_udp.go
Normal file
246
internal/discover/client_udp.go
Normal 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()
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
95
internal/ignore/cache.go
Normal 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()
|
||||
}
|
||||
}
|
||||
84
internal/ignore/cache_test.go
Normal file
84
internal/ignore/cache_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
1
internal/ignore/testdata/.stignore
vendored
1
internal/ignore/testdata/.stignore
vendored
@@ -4,3 +4,4 @@ bfile
|
||||
dir1/cfile
|
||||
**/efile
|
||||
/ffile
|
||||
lost+found
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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}},
|
||||
|
||||
@@ -24,3 +24,5 @@ func HideFile(path string) error {
|
||||
func ShowFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func HideConsole() {}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
133
internal/stats/folder.go
Normal 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
|
||||
}
|
||||
@@ -18,4 +18,5 @@ package stats
|
||||
// Same key space as files/leveldb.go keyType* constants
|
||||
const (
|
||||
keyTypeDeviceStatistic = iota + 30
|
||||
keyTypeFolderStatistic
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
25
internal/upgrade/releasename_darwin.go
Normal file
25
internal/upgrade/releasename_darwin.go
Normal 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)
|
||||
}
|
||||
38
internal/upgrade/releasename_linux_arm.go
Normal file
38
internal/upgrade/releasename_linux_arm.go
Normal 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"
|
||||
}
|
||||
27
internal/upgrade/releasename_other.go
Normal file
27
internal/upgrade/releasename_other.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
package upgrade
|
||||
|
||||
func upgradeTo(path string, rel Release, extra string) error {
|
||||
func upgradeTo(path string, rel Release) error {
|
||||
return ErrUpgradeUnsupported
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
1
test/.gitignore
vendored
@@ -16,3 +16,4 @@ csrftokens.txt
|
||||
s4d
|
||||
http
|
||||
index
|
||||
*.syncthing-reset*
|
||||
|
||||
143
test/cli_test.go
Normal file
143
test/cli_test.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
91
test/parallell_scan_test.go
Normal file
91
test/parallell_scan_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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...")
|
||||
|
||||
Reference in New Issue
Block a user