Compare commits

...

70 Commits

Author SHA1 Message Date
Jakob Borg
f9fcb44f3c Translation update 2014-12-07 08:30:54 +01:00
Jakob Borg
996cbbca38 Merge remote-tracking branch 'origin/pr/1091'
* origin/pr/1091:
  Escape plus sign (fixes #1090)
2014-12-07 08:05:21 +01:00
Jakob Borg
581f4b89bd Merge remote-tracking branch 'origin/pr/977'
* origin/pr/977:
  Cache file descriptors
2014-12-07 08:03:34 +01:00
Audrius Butkevicius
88a347dce0 Escape plus sign (fixes #1090) 2014-12-07 00:10:32 +00:00
Audrius Butkevicius
3e7b197a1d Merge pull request #1074 from syncthing/fix-1071
Handle symlinks in versioning (fixes #1071)
2014-12-06 20:19:25 +00:00
Jakob Borg
94ab06e92f Handle symlinks, Staggered versioner (fixes #1071) 2014-12-06 15:20:35 +01:00
Jakob Borg
d38c81fcff Handle broken symlinks, Simple versioner (fixes #1071) 2014-12-06 15:20:35 +01:00
Jakob Borg
3e26fdfb67 Run filetype and symlink integration tests with versioning (ref #1071) 2014-12-06 15:20:35 +01:00
Jakob Borg
e1be73232d Merge remote-tracking branch 'origin/pr/1086'
* origin/pr/1086:
  Enable URL lang parameter for switching languages (fixes #1080)
2014-12-06 14:41:41 +01:00
Jakob Borg
06fd2268d9 Use Go 1.4 'generate' to create XDR codec 2014-12-06 14:23:10 +01:00
Jakob Borg
15251dfae1 Update calmh/xdr 2014-12-06 14:20:49 +01:00
Dennis Wilson
f62812a8dc Enable URL lang parameter for switching languages (fixes #1080) 2014-12-06 14:11:42 +01:00
Jakob Borg
c6041d2590 Skip dotfiles when generating assets 2014-12-06 13:46:02 +01:00
Jakob Borg
c7e779107c Staggered versioning should use current time for filename tag (fixes #994) 2014-12-06 13:26:46 +01:00
Jakob Borg
7cd25c919f Asset rebuild 2014-12-06 12:36:35 +01:00
Jakob Borg
47d67d3985 Merge remote-tracking branch 'origin/pr/1087'
* origin/pr/1087:
  Folder/device panel header with progress indicator
2014-12-06 12:32:43 +01:00
Jakob Borg
1a7921b46c Merge remote-tracking branch 'origin/pr/1083'
* origin/pr/1083:
  Select repos to share with in node editor dialog (fixes #719)
2014-12-06 12:30:46 +01:00
Jakob Borg
43d569741b Merge remote-tracking branch 'origin/pr/1081'
* origin/pr/1081:
  Revisit -no-console option for Windows
2014-12-06 12:17:46 +01:00
Jakob Borg
52c6869eab Merge remote-tracking branch 'origin/pr/1082'
* origin/pr/1082:
  Scrap IsSymlink for native support on Go 1.4
2014-12-06 12:12:53 +01:00
Jakob Borg
6dff9097a2 Use Go 1.4 build environment (currently 1.4rc2) 2014-12-06 12:12:33 +01:00
Ben Schulz
4ff211662a Folder/device panel header with progress indicator 2014-12-05 18:44:38 +01:00
Audrius Butkevicius
05eab51a0d Select repos to share with in node editor dialog (fixes #719) 2014-12-05 00:22:16 +00:00
Audrius Butkevicius
604a4e7dbc Scrap IsSymlink for native support on Go 1.4
Obviously needs Go 1.4 to go back in.

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

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

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

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

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

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

1
.gitignore vendored
View File

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

View File

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

92
CONDUCT.md Normal file
View File

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

View File

@@ -37,6 +37,22 @@ 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.
### 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 +75,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

10
Godeps/Godeps.json generated
View File

@@ -6,8 +6,8 @@
],
"Deps": [
{
"ImportPath": "github.com/AudriusButkevicius/lfu-go",
"Rev": "164bcecceb92fd6037f4d18a8d97b495ec6ef669"
"ImportPath": "github.com/AudriusButkevicius/lrufdcache",
"Rev": "9bddff8f67224ab3e7d80525a6ae9bcf1ce10769"
},
{
"ImportPath": "github.com/bkaradzic/go-lz4",
@@ -23,7 +23,11 @@
},
{
"ImportPath": "github.com/calmh/xdr",
"Rev": "ec3d404f43731551258977b38dd72cf557d00398"
"Rev": "45c46b7db7ff83b8b9ee09bbd95f36ab50043ece"
},
{
"ImportPath": "github.com/golang/groupcache/lru",
"Rev": "f391194b967ae0d21deadc861ea87120d9687447"
},
{
"ImportPath": "github.com/juju/ratelimit",

View File

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

View File

@@ -1,19 +0,0 @@
A simple LFU cache for golang. Based on the paper [An O(1) algorithm for implementing the LFU cache eviction scheme](http://dhruvbird.com/lfu.pdf).
Usage:
```go
import "github.com/dgrijalva/lfu-go"
// Make a new thing
c := lfu.New()
// Set some values
c.Set("myKey", myValue)
// Retrieve some values
myValue = c.Get("myKey")
// Evict some values
c.Evict(1)
```

View File

@@ -1,156 +0,0 @@
package lfu
import (
"container/list"
"sync"
)
type Eviction struct {
Key string
Value interface{}
}
type Cache struct {
// If len > UpperBound, cache will automatically evict
// down to LowerBound. If either value is 0, this behavior
// is disabled.
UpperBound int
LowerBound int
values map[string]*cacheEntry
freqs *list.List
len int
lock *sync.Mutex
EvictionChannel chan<- Eviction
}
type cacheEntry struct {
key string
value interface{}
freqNode *list.Element
}
type listEntry struct {
entries map[*cacheEntry]byte
freq int
}
func New() *Cache {
c := new(Cache)
c.values = make(map[string]*cacheEntry)
c.freqs = list.New()
c.lock = new(sync.Mutex)
return c
}
func (c *Cache) Get(key string) interface{} {
c.lock.Lock()
defer c.lock.Unlock()
if e, ok := c.values[key]; ok {
c.increment(e)
return e.value
}
return nil
}
func (c *Cache) Set(key string, value interface{}) {
c.lock.Lock()
defer c.lock.Unlock()
if e, ok := c.values[key]; ok {
// value already exists for key. overwrite
e.value = value
c.increment(e)
} else {
// value doesn't exist. insert
e := new(cacheEntry)
e.key = key
e.value = value
c.values[key] = e
c.increment(e)
c.len++
// bounds mgmt
if c.UpperBound > 0 && c.LowerBound > 0 {
if c.len > c.UpperBound {
c.evict(c.len - c.LowerBound)
}
}
}
}
func (c *Cache) Len() int {
c.lock.Lock()
defer c.lock.Unlock()
return c.len
}
func (c *Cache) Evict(count int) int {
c.lock.Lock()
defer c.lock.Unlock()
return c.evict(count)
}
func (c *Cache) evict(count int) int {
// No lock here so it can be called
// from within the lock (during Set)
var evicted int
for i := 0; i < count; {
if place := c.freqs.Front(); place != nil {
for entry, _ := range place.Value.(*listEntry).entries {
if i < count {
if c.EvictionChannel != nil {
c.EvictionChannel <- Eviction{
Key: entry.key,
Value: entry.value,
}
}
delete(c.values, entry.key)
c.remEntry(place, entry)
evicted++
c.len--
i++
}
}
}
}
return evicted
}
func (c *Cache) increment(e *cacheEntry) {
currentPlace := e.freqNode
var nextFreq int
var nextPlace *list.Element
if currentPlace == nil {
// new entry
nextFreq = 1
nextPlace = c.freqs.Front()
} else {
// move up
nextFreq = currentPlace.Value.(*listEntry).freq + 1
nextPlace = currentPlace.Next()
}
if nextPlace == nil || nextPlace.Value.(*listEntry).freq != nextFreq {
// create a new list entry
li := new(listEntry)
li.freq = nextFreq
li.entries = make(map[*cacheEntry]byte)
if currentPlace != nil {
nextPlace = c.freqs.InsertAfter(li, currentPlace)
} else {
nextPlace = c.freqs.PushFront(li)
}
}
e.freqNode = nextPlace
nextPlace.Value.(*listEntry).entries[e] = 1
if currentPlace != nil {
// remove from current position
c.remEntry(currentPlace, e)
}
}
func (c *Cache) remEntry(place *list.Element, entry *cacheEntry) {
entries := place.Value.(*listEntry).entries
delete(entries, entry)
if len(entries) == 0 {
c.freqs.Remove(place)
}
}

View File

@@ -1,68 +0,0 @@
package lfu
import (
"fmt"
"testing"
)
func TestLFU(t *testing.T) {
c := New()
c.Set("a", "a")
if v := c.Get("a"); v != "a" {
t.Errorf("Value was not saved: %v != 'a'", v)
}
if l := c.Len(); l != 1 {
t.Errorf("Length was not updated: %v != 1", l)
}
c.Set("b", "b")
if v := c.Get("b"); v != "b" {
t.Errorf("Value was not saved: %v != 'b'", v)
}
if l := c.Len(); l != 2 {
t.Errorf("Length was not updated: %v != 2", l)
}
c.Get("a")
evicted := c.Evict(1)
if v := c.Get("a"); v != "a" {
t.Errorf("Value was improperly evicted: %v != 'a'", v)
}
if v := c.Get("b"); v != nil {
t.Errorf("Value was not evicted: %v", v)
}
if l := c.Len(); l != 1 {
t.Errorf("Length was not updated: %v != 1", l)
}
if evicted != 1 {
t.Errorf("Number of evicted items is wrong: %v != 1", evicted)
}
}
func TestBoundsMgmt(t *testing.T) {
c := New()
c.UpperBound = 10
c.LowerBound = 5
for i := 0; i < 100; i++ {
c.Set(fmt.Sprintf("%v", i), i)
}
if c.Len() > 10 {
t.Errorf("Bounds management failed to evict properly: %v", c.Len())
}
}
func TestEviction(t *testing.T) {
ch := make(chan Eviction, 1)
c := New()
c.EvictionChannel = ch
c.Set("a", "b")
c.Evict(1)
ev := <-ch
if ev.Key != "a" || ev.Value.(string) != "b" {
t.Error("Incorrect item")
}
}

View File

@@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof

View File

@@ -0,0 +1,25 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>

View File

@@ -0,0 +1,4 @@
lrufdcache
==========
A LRU file descriptor cache

View File

@@ -0,0 +1,88 @@
// Package logger implements a LRU file descriptor cache for concurrent ReadAt
// calls.
package lrufdcache
import (
"os"
"sync"
"github.com/golang/groupcache/lru"
)
// A wrapper around *os.File which counts references
type CachedFile struct {
file *os.File
wg sync.WaitGroup
// Locking between file.Close and file.ReadAt
// (just to please the race detector...)
flock sync.RWMutex
}
// Tells the cache that we are done using the file, but it's up to the cache
// to decide when this file will really be closed. The error, if any, will be
// lost.
func (f *CachedFile) Close() error {
f.wg.Done()
return nil
}
// Read the file at the given offset.
func (f *CachedFile) ReadAt(buf []byte, at int64) (int, error) {
f.flock.RLock()
defer f.flock.RUnlock()
return f.file.ReadAt(buf, at)
}
type FileCache struct {
cache *lru.Cache
mut sync.Mutex
}
// Create a new cache with the number of entries to hold.
func NewCache(entries int) *FileCache {
c := FileCache{
cache: lru.New(entries),
}
c.cache.OnEvicted = func(key lru.Key, fdi interface{}) {
// The file might not have been closed by all openers yet, therefore
// spawn a routine which waits for that to happen and then closes the
// file.
go func(item *CachedFile) {
item.wg.Wait()
item.flock.Lock()
item.file.Close()
item.flock.Unlock()
}(fdi.(*CachedFile))
}
return &c
}
// Open and cache a file descriptor or use an existing cached descriptor for
// the given path.
func (c *FileCache) Open(path string) (*CachedFile, error) {
// Evictions can only happen during c.cache.Add, and there is a potential
// race between c.cache.Get and cfd.wg.Add where if not guarded by a mutex
// could result in cfd getting closed before the counter is incremented if
// a concurrent routine does a c.cache.Add
c.mut.Lock()
defer c.mut.Unlock()
fdi, ok := c.cache.Get(path)
if ok {
cfd := fdi.(*CachedFile)
cfd.wg.Add(1)
return cfd, nil
}
fd, err := os.Open(path)
if err != nil {
return nil, err
}
cfd := &CachedFile{
file: fd,
wg: sync.WaitGroup{},
}
cfd.wg.Add(1)
c.cache.Add(path, cfd)
return cfd, nil
}

View File

@@ -0,0 +1,195 @@
package lrufdcache
import (
"io/ioutil"
"os"
"sync"
"time"
"testing"
)
func TestNoopReadFailsOnClosed(t *testing.T) {
fd, err := ioutil.TempFile("", "fdcache")
if err != nil {
t.Fatal(err)
return
}
fd.WriteString("test")
fd.Close()
buf := make([]byte, 4)
defer os.Remove(fd.Name())
_, err = fd.ReadAt(buf, 0)
if err == nil {
t.Fatal("Expected error")
}
}
func TestSingleFileEviction(t *testing.T) {
c := NewCache(1)
wg := sync.WaitGroup{}
fd, err := ioutil.TempFile("", "fdcache")
if err != nil {
t.Fatal(err)
return
}
fd.WriteString("test")
fd.Close()
buf := make([]byte, 4)
defer os.Remove(fd.Name())
for k := 0; k < 100; k++ {
wg.Add(1)
go func() {
defer wg.Done()
cfd, err := c.Open(fd.Name())
if err != nil {
t.Fatal(err)
return
}
defer cfd.Close()
_, err = cfd.ReadAt(buf, 0)
if err != nil {
t.Fatal(err)
}
}()
}
wg.Wait()
}
func TestMultifileEviction(t *testing.T) {
c := NewCache(1)
wg := sync.WaitGroup{}
for k := 0; k < 100; k++ {
wg.Add(1)
go func() {
defer wg.Done()
fd, err := ioutil.TempFile("", "fdcache")
if err != nil {
t.Fatal(err)
return
}
fd.WriteString("test")
fd.Close()
buf := make([]byte, 4)
defer os.Remove(fd.Name())
cfd, err := c.Open(fd.Name())
if err != nil {
t.Fatal(err)
return
}
defer cfd.Close()
_, err = cfd.ReadAt(buf, 0)
if err != nil {
t.Fatal(err)
}
}()
}
wg.Wait()
}
func TestMixedEviction(t *testing.T) {
c := NewCache(1)
wg := sync.WaitGroup{}
wg2 := sync.WaitGroup{}
for i := 0; i < 100; i++ {
wg2.Add(1)
go func() {
defer wg2.Done()
fd, err := ioutil.TempFile("", "fdcache")
if err != nil {
t.Fatal(err)
return
}
fd.WriteString("test")
fd.Close()
buf := make([]byte, 4)
for k := 0; k < 100; k++ {
wg.Add(1)
go func() {
defer wg.Done()
cfd, err := c.Open(fd.Name())
if err != nil {
t.Fatal(err)
return
}
defer cfd.Close()
_, err = cfd.ReadAt(buf, 0)
if err != nil {
t.Fatal(err)
}
}()
}
}()
}
wg2.Wait()
wg.Wait()
}
func TestLimit(t *testing.T) {
testcase := 50
fd, err := ioutil.TempFile("", "fdcache")
if err != nil {
t.Fatal(err)
return
}
fd.Close()
defer os.Remove(fd.Name())
c := NewCache(testcase)
fds := make([]*CachedFile, testcase*2)
for i := 0; i < testcase*2; i++ {
fd, err := ioutil.TempFile("", "fdcache")
if err != nil {
t.Fatal(err)
return
}
fd.WriteString("test")
fd.Close()
defer os.Remove(fd.Name())
nfd, err := c.Open(fd.Name())
if err != nil {
t.Fatal(err)
return
}
fds = append(fds, nfd)
nfd.Close()
}
// Allow closes to happen
time.Sleep(time.Millisecond * 100)
buf := make([]byte, 4)
ok := 0
for _, fd := range fds {
if fd == nil {
continue
}
_, err := fd.ReadAt(buf, 0)
if err == nil {
ok++
}
}
if ok > testcase {
t.Fatal("More than", testcase, "fds open")
}
}

View File

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

View File

@@ -0,0 +1,121 @@
/*
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package lru implements an LRU cache.
package lru
import "container/list"
// Cache is an LRU cache. It is not safe for concurrent access.
type Cache struct {
// MaxEntries is the maximum number of cache entries before
// an item is evicted. Zero means no limit.
MaxEntries int
// OnEvicted optionally specificies a callback function to be
// executed when an entry is purged from the cache.
OnEvicted func(key Key, value interface{})
ll *list.List
cache map[interface{}]*list.Element
}
// A Key may be any value that is comparable. See http://golang.org/ref/spec#Comparison_operators
type Key interface{}
type entry struct {
key Key
value interface{}
}
// New creates a new Cache.
// If maxEntries is zero, the cache has no limit and it's assumed
// that eviction is done by the caller.
func New(maxEntries int) *Cache {
return &Cache{
MaxEntries: maxEntries,
ll: list.New(),
cache: make(map[interface{}]*list.Element),
}
}
// Add adds a value to the cache.
func (c *Cache) Add(key Key, value interface{}) {
if c.cache == nil {
c.cache = make(map[interface{}]*list.Element)
c.ll = list.New()
}
if ee, ok := c.cache[key]; ok {
c.ll.MoveToFront(ee)
ee.Value.(*entry).value = value
return
}
ele := c.ll.PushFront(&entry{key, value})
c.cache[key] = ele
if c.MaxEntries != 0 && c.ll.Len() > c.MaxEntries {
c.RemoveOldest()
}
}
// Get looks up a key's value from the cache.
func (c *Cache) Get(key Key) (value interface{}, ok bool) {
if c.cache == nil {
return
}
if ele, hit := c.cache[key]; hit {
c.ll.MoveToFront(ele)
return ele.Value.(*entry).value, true
}
return
}
// Remove removes the provided key from the cache.
func (c *Cache) Remove(key Key) {
if c.cache == nil {
return
}
if ele, hit := c.cache[key]; hit {
c.removeElement(ele)
}
}
// RemoveOldest removes the oldest item from the cache.
func (c *Cache) RemoveOldest() {
if c.cache == nil {
return
}
ele := c.ll.Back()
if ele != nil {
c.removeElement(ele)
}
}
func (c *Cache) removeElement(e *list.Element) {
c.ll.Remove(e)
kv := e.Value.(*entry)
delete(c.cache, kv.key)
if c.OnEvicted != nil {
c.OnEvicted(kv.key, kv.value)
}
}
// Len returns the number of items in the cache.
func (c *Cache) Len() int {
if c.cache == nil {
return 0
}
return c.ll.Len()
}

View File

@@ -0,0 +1,73 @@
/*
Copyright 2013 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lru
import (
"testing"
)
type simpleStruct struct {
int
string
}
type complexStruct struct {
int
simpleStruct
}
var getTests = []struct {
name string
keyToAdd interface{}
keyToGet interface{}
expectedOk bool
}{
{"string_hit", "myKey", "myKey", true},
{"string_miss", "myKey", "nonsense", false},
{"simple_struct_hit", simpleStruct{1, "two"}, simpleStruct{1, "two"}, true},
{"simeple_struct_miss", simpleStruct{1, "two"}, simpleStruct{0, "noway"}, false},
{"complex_struct_hit", complexStruct{1, simpleStruct{2, "three"}},
complexStruct{1, simpleStruct{2, "three"}}, true},
}
func TestGet(t *testing.T) {
for _, tt := range getTests {
lru := New(0)
lru.Add(tt.keyToAdd, 1234)
val, ok := lru.Get(tt.keyToGet)
if ok != tt.expectedOk {
t.Fatalf("%s: cache hit = %v; want %v", tt.name, ok, !ok)
} else if ok && val != 1234 {
t.Fatalf("%s expected get to return 1234 but got %v", tt.name, val)
}
}
}
func TestRemove(t *testing.T) {
lru := New(0)
lru.Add("myKey", 1234)
if val, ok := lru.Get("myKey"); !ok {
t.Fatal("TestRemove returned no match")
} else if val != 1234 {
t.Fatalf("TestRemove failed. Expected %d, got %v", 1234, val)
}
lru.Remove("myKey")
if _, ok := lru.Get("myKey"); ok {
t.Fatal("TestRemove returned a removed entry")
}
}

View File

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

View File

@@ -2,7 +2,7 @@
set -euo pipefail
IFS=$'\n\t'
DOCKERIMGV=1.3.3-4
DOCKERIMGV=1.4-1
case "${1:-default}" in
default)
@@ -128,7 +128,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'
;;
*)

View File

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

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

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

View File

@@ -598,7 +598,7 @@ func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
}
if upgrade.CompareVersions(rel.Tag, Version) == 1 {
err = upgrade.UpgradeTo(rel, GoArchExtra)
err = upgrade.UpgradeTo(rel)
if err != nil {
l.Warnln("upgrading:", err)
http.Error(w, err.Error(), 500)

View File

@@ -45,6 +45,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 +63,6 @@ var (
IsRelease bool
IsBeta bool
LongVersion string
GoArchExtra string // "", "v5", "v6", "v7"
)
const (
@@ -188,6 +188,7 @@ var (
doUpgrade bool
doUpgradeCheck bool
noBrowser bool
noConsole bool
generateDir string
logFile string
noRestart = os.Getenv("STNORESTART") != ""
@@ -214,6 +215,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 +236,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
@@ -329,7 +337,7 @@ 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.UpgradeTo(rel)
if err != nil {
l.Fatalln("Upgrade:", err) // exits 1
}
@@ -458,6 +466,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))
}
@@ -1246,7 +1258,7 @@ func autoUpgrade() {
}
l.Infof("Automatic upgrade (current %q < latest %q)", Version, rel.Tag)
err = upgrade.UpgradeTo(rel, GoArchExtra)
err = upgrade.UpgradeTo(rel)
if err != nil {
l.Warnln("Automatic upgrade:", err)
continue

View File

@@ -1,7 +1,7 @@
FROM debian:squeeze
MAINTAINER Jakob Borg <jakob@nym.se>
ENV GOLANG_VERSION 1.3.3
ENV GOLANG_VERSION 1.4rc2
# SCMs for "go get", gcc for cgo
RUN apt-get update && apt-get install -y \
@@ -47,7 +47,7 @@ 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

View File

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

View File

@@ -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,19 +81,19 @@
"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",
"Stopped": "Gestoppt",
@@ -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 den 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,7 +125,7 @@
"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",
@@ -134,7 +134,7 @@
"Versions Path": "Versionierungspfad",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Alte Versionen werden automatisch gelöscht wenn sie älter als das angegebene Höchstalter sind oder die Höchstzahl der Dateien pro Zeitabschnitt überschritten wird.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Gerätes, dass dieses Gerät auch auf der Gegenseite hinzugefügt werden muss.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Beim Hinzufügen eines neuen Verzeichnisses, beachte dass die Verzeichnis-ID dazu verwendet wird, Verzeichnisse zwischen Geräten zu verbinden. Die ID muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Beim Hinzufügen eines neuen Verzeichnisses, beachte dass die Verzeichnis ID dazu verwendet wird, Verzeichnisse zwischen Geräten zu verbinden. Die ID muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
"Yes": "Ja",
"You must keep at least one version.": "Du musst mindestens eine Version behalten.",
"full documentation": "Komplette Dokumentation",

View File

@@ -49,7 +49,6 @@
"Generate": "Generate",
"Global Discovery": "Global Discovery",
"Global Discovery Server": "Global Discovery Server",
"Global Discovery Servers": "Global Discovery Servers",
"Global State": "Global State",
"Idle": "Idle",
"Ignore Patterns": "Ignore Patterns",

View File

@@ -8,7 +8,7 @@
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation ?",
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
"Any devices configured on an introducer device will be added to this device as well.": "Tout appareil ajouté depuis un appareil initiateur sera aussi ajouté sur cet appareil.",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "Mises à jour automatiques",
"Bugs": "Bugs",
"CPU Utilization": "Utilisation du CPU",
"Close": "Fermer",
@@ -17,14 +17,14 @@
"Connection Error": "Erreur de connexion",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants :",
"Delete": "Supprimer",
"Device ID": "ID de l'appareil",
"Device ID": "ID du périphérique",
"Device Identification": "Identification de l'appareil",
"Device Name": "Nom de l'appareil",
"Device Name": "Nom du périphérique",
"Disconnected": "Déconnecté",
"Documentation": "Documentation",
"Download Rate": "Débit de réception",
"Edit": "Éditer",
"Edit Device": "Éditer l'appareil",
"Edit Device": "Éditer le périphérique",
"Edit Folder": "Éditer le répertoire",
"Editing": "Édition",
"Enable UPnP": "Activer l'UPnP",
@@ -84,7 +84,7 @@
"Scanning": "En cours de scan",
"Select the devices to share this folder with.": "Sélectionner les appareils avec qui partager ce répertoire.",
"Settings": "Configuration",
"Share With Devices": "Partager avec les appareils",
"Share With Devices": "Partager avec les périphériques",
"Shared With": "Partagé avec",
"Short identifier for the folder. Must be the same on all cluster devices.": "Court identifiant du répertoire. Il doit être le même sur l'ensemble des appareils du groupe.",
"Show ID": "Montrer l'ID",
@@ -121,7 +121,7 @@
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Le temps maximum de conservation d'une version (en jours, mettre à 0 pour conserver les versions pour toujours)",
"The number of old versions to keep, per file.": "Le nombre d'anciennes versions à garder, par fichier.",
"The number of versions must be a number and cannot be blank.": "Le nombre de version doit être un nombre et ne peut pas être vide.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "L'intervalle de scan ne doit pas être un nombre négatif de secondes.",
"The rescan interval must be at least 5 seconds.": "L'intervalle de scan doit être d'au minimum 5 secondes.",
"Unknown": "Inconnu",
"Up to Date": "Synchronisation à jour",

View File

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

View File

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

View File

@@ -91,6 +91,7 @@
<div class="panel-group" id="folders">
<div class="panel panel-default" ng-repeat="folder in folderList()">
<div class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer">
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.ID)}}%"></div>
<h3 class="panel-title">
<span class="glyphicon glyphicon-hdd"></span>&emsp;{{folder.ID}}
<span class="pull-right hidden-xs text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
@@ -148,13 +149,17 @@
<th><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan Interval</span></th>
<td class="text-right">{{folder.RescanIntervalS}} s</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-share-alt"></span>&emsp;<span translate>Shared With</span></th>
<td class="text-right">{{sharesFolder(folder)}}</td>
</tr>
</tbody>
</table>
</div>
<div class="panel-footer">
<button class="btn btn-sm btn-danger" ng-if="folder.ReadOnly && model[folder.ID].needFiles > 0" ng-click="override(folder.ID)" href=""><span class="glyphicon glyphicon-upload"></span>&emsp;<span translate>Override Changes</span></button>
<span class="pull-right">
<button class="btn btn-sm btn-default" href="" ng-show="folderStatus(folder.ID) == 'idle'" ng-click="rescanFolder(folder.ID)"><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan</span></button>
<button class="btn btn-sm btn-default" href="" ng-show="folderStatus(folder) == 'idle'" ng-click="rescanFolder(folder.ID)"><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan</span></button>
<button class="btn btn-sm btn-default" href="" ng-click="editFolder(folder)"><span class="glyphicon glyphicon-pencil"></span>&emsp;<span translate>Edit</span></button>
</span>
<div class="clearfix"></div>
@@ -201,7 +206,7 @@
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
</tr>
<tr ng-if="system.extAnnounceOK != undefined && announceServersTotal > 0">
<th><span class="glyphicon glyphicon-bullhorn"></span>&emsp;<span translate>Global Discovery Servers</span></th>
<th><span class="glyphicon glyphicon-bullhorn"></span>&emsp;<span translate>Global Discovery</span></th>
<td class="text-right">
<span ng-if="announceServersFailed.length == 0" class="data text-success">
<span>OK</span>
@@ -228,6 +233,7 @@
<div class="panel-group" id="devices">
<div class="panel panel-default" ng-repeat="deviceCfg in otherDevices()">
<div class="panel-heading" data-toggle="collapse" data-parent="#devices" href="#device-{{$index}}" style="cursor: pointer">
<div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.DeviceID]._total | number:0}}%"></div>
<h3 class="panel-title">
<identicon data-value="deviceCfg.DeviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
<span ng-switch="deviceStatus(deviceCfg)" class="pull-right hidden-xs text-{{deviceClass(deviceCfg)}}">
@@ -412,6 +418,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 +560,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)}}
@@ -796,27 +819,25 @@
<p translate>Copyright &copy; 2014 Jakob Borg and the following Contributors:</p>
<div class="row">
<div class="col-md-6">
<ul>
<div class="col-md-12">
<ul class="list-unstyled three-columns">
<li>Aaron Bieber</li>
<li>Andrew Dunham</li>
<li>Alexander Graf</li>
<li>Arthur Axel fREW Schmidt</li>
<li>Audrius Butkevicius</li>
<li>Ben Schulz</li>
<li>Ben Sidhom</li>
<li>Brandon Philips</li>
<li>Caleb Callaway</li>
<li>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 +858,7 @@
<hr/>
<p translate>Syncthing includes the following software or portions thereof:</p>
<ul>
<ul class="list-unstyled two-columns">
<li><a href="http://golang.org/">The Go Programming Language</a>, Copyright &copy; 2012 The Go Authors.</li>
<li><a href="https://bitbucket.org/kardianos/osext">kardianos/osext</a>, Copyright &copy; 2012 Daniel Theophanes.</li>
<li><a href="https://code.google.com/p/snappy-go/">snappy-go</a>, Copyright &copy; 2011 The Snappy-Go Authors.</li>
@@ -871,6 +892,7 @@
<script src="scripts/syncthing/core/filters/basenameFilter.js"></script>
<script src="scripts/syncthing/core/filters/binaryFilter.js"></script>
<script src="scripts/syncthing/core/filters/naturalFilter.js"></script>
<script src="scripts/syncthing/core/services/localeService.js"></script>
<script src="assets/lang/valid-langs.js"></script>
<script src="scripts/syncthing/app.js"></script>

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,43 @@
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();
$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;
@@ -25,47 +55,12 @@ angular.module('syncthing.core')
$scope.stats = {};
$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 +82,7 @@ angular.module('syncthing.core')
}
console.log('UIOnline');
$scope.init();
initController();
online = true;
restarting = false;
$('#networkError').modal('hide');
@@ -181,40 +176,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 +267,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]) {
@@ -379,27 +373,6 @@ angular.module('syncthing.core')
});
}, 500);
$scope.init = function () {
refreshSystem();
refreshConfig();
refreshConnectionStats();
refreshDeviceStats();
$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;
});
};
$scope.refresh = function () {
refreshSystem();
refreshConnectionStats();
@@ -610,7 +583,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 +640,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 +663,8 @@ angular.module('syncthing.core')
$scope.currentDevice = {
AddressesStr: 'dynamic',
Compression: true,
Introducer: false
Introducer: false,
selectedFolders: {}
};
$scope.editingExisting = false;
$scope.editingSelf = false;
@@ -738,6 +718,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 +901,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 +1004,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 +1041,6 @@ angular.module('syncthing.core')
$http.post(urlbase + "/scan?folder=" + encodeURIComponent(folder));
};
$scope.init();
setInterval($scope.refresh, 10000);
// pseudo main. called on all definitions assigned
initController();
});

View File

@@ -0,0 +1,88 @@
angular.module('syncthing.core')
.provider('LocaleService', function () {
var _defaultLocale,
_availableLocales;
this.setDefaultLocale = function (locale) {
_defaultLocale = locale;
};
this.setAvailableLocales = function (locales) {
_availableLocales = locales;
};
this.$get = ['$http', '$translate', '$location', function ($http, $translate, $location) {
/**
* Requests the server in order to get the browser's requested locale strings.
*
* @returns promise which on success resolves with a locales array
*/
function readBrowserLocales() {
// @TODO: check if there is nice way to utilize window.navigator.languages or similiar api.
return $http.get(urlbase + "/lang");
}
function autoConfigLocale() {
var params = $location.search();
if(params.lang) {
$translate.use(params.lang);
} else {
readBrowserLocales().success(function (langs) {
// Find the first language in the list provided by the user's browser
// that is a prefix of a language we have available. That is, "en"
// sent by the browser will match "en" or "en-US", while "zh-TW" will
// match only "zh-TW" and not "zh-CN".
var i,
lang,
matching,
locale = _defaultLocale;
for (i = 0; i < langs.length; i++) {
lang = langs[i];
if (lang.length < 2) {
continue;
}
matching = _availableLocales.filter(function (possibleLang) {
// The langs returned by the /rest/langs call will be in lower
// case. We compare to the lowercase version of the language
// code we have as well.
possibleLang = possibleLang.toLowerCase();
if (possibleLang.length > lang.length) {
return possibleLang.indexOf(lang) === 0;
} else {
return lang.indexOf(possibleLang) === 0;
}
});
if (matching.length >= 1) {
locale = matching[0];
break;
}
}
// Fallback if nothing matched
$translate.use(locale);
});
}
}
function useLocale(language) {
// @TODO: eventually check for valid locale format
if (language) {
$translate.use(language);
}
}
return {
autoConfigLocale: autoConfigLocale,
useLocale: useLocale
}
}];
});

View File

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -41,6 +41,8 @@ import (
"github.com/syncthing/syncthing/internal/stats"
"github.com/syncthing/syncthing/internal/symlinks"
"github.com/syncthing/syncthing/internal/versioner"
"github.com/AudriusButkevicius/lrufdcache"
"github.com/syndtr/goleveldb/leveldb"
)
@@ -86,6 +88,7 @@ type Model struct {
db *leveldb.DB
finder *files.BlockFinder
progressEmitter *ProgressEmitter
cache *lrufdcache.FileCache
deviceName string
clientName string
@@ -127,6 +130,7 @@ func NewModel(cfg *config.ConfigWrapper, deviceName, clientName, clientVersion s
m := &Model{
cfg: cfg,
db: db,
cache: lrufdcache.NewCache(25),
deviceName: deviceName,
clientName: clientName,
clientVersion: clientVersion,
@@ -695,12 +699,11 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
}
reader = strings.NewReader(target)
} else {
reader, err = os.Open(fn) // XXX: Inefficient, should cache fd?
reader, err = m.cache.Open(fn)
if err != nil {
return nil, err
}
defer reader.(*os.File).Close()
defer reader.(*lrufdcache.CachedFile).Close()
}
buf := make([]byte, size)
@@ -1360,7 +1363,7 @@ func (m *Model) leveldbPanicWorkaround() {
func symlinkInvalid(isLink bool) bool {
if !symlinks.Supported && isLink {
SymlinkWarning.Do(func() {
l.Warnln("Symlinks are unsupported as they require Administrator priviledges. This might cause your folder to appear out of sync.")
l.Warnln("Symlinks are disabled, unsupported or require Administrator priviledges. This might cause your folder to appear out of sync.")
})
return true
}

View File

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

View File

@@ -26,8 +26,6 @@ import (
"sync"
"time"
"github.com/AudriusButkevicius/lfu-go"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/osutil"
@@ -373,12 +371,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)
@@ -603,19 +600,6 @@ nextFile:
p.progressEmitter.Register(state.sharedPullerState)
}
evictionChan := make(chan lfu.Eviction)
fdCache := lfu.New()
fdCache.UpperBound = 50
fdCache.LowerBound = 20
fdCache.EvictionChannel = evictionChan
go func() {
for item := range evictionChan {
item.Value.(*os.File).Close()
}
}()
folderRoots := make(map[string]string)
p.model.fmut.RLock()
for folder, cfg := range p.model.folderCfgs {
@@ -629,18 +613,11 @@ nextFile:
found := p.model.finder.Iterate(block.Hash, func(folder, file string, index uint32) bool {
path := filepath.Join(folderRoots[folder], file)
var fd *os.File
fdi := fdCache.Get(path)
if fdi != nil {
fd = fdi.(*os.File)
} else {
fd, err = os.Open(path)
if err != nil {
return false
}
fdCache.Set(path, fd)
fd, err := p.model.cache.Open(path)
if err != nil {
return false
}
defer fd.Close()
_, err = fd.ReadAt(buf, protocol.BlockSize*int64(index))
if err != nil {
@@ -689,8 +666,6 @@ nextFile:
state.copyDone()
}
}
fdCache.Evict(fdCache.Len())
close(evictionChan)
out <- state.sharedPullerState
}
}
@@ -795,8 +770,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

View File

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

View File

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

View File

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

View File

@@ -131,12 +131,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,7 +196,7 @@ 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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,19 +28,12 @@ import (
"os"
"path"
"path/filepath"
"runtime"
"strings"
)
// Upgrade to the given release, saving the previous binary with a ".old" extension.
func upgradeTo(path string, rel Release, archExtra string) error {
osName := runtime.GOOS
if osName == "darwin" {
// We call the darwin release bundles macosx because that makes more
// sense for people downloading them
osName = "macosx"
}
expectedRelease := fmt.Sprintf("syncthing-%s-%s%s-%s.", osName, runtime.GOARCH, archExtra, rel.Tag)
func upgradeTo(path string, rel Release) error {
expectedRelease := releaseName(rel.Tag)
if debug {
l.Debugf("expected release asset %q", expectedRelease)
}

View File

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

View File

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

View File

@@ -18,7 +18,6 @@ package versioner
import (
"os"
"path/filepath"
"sort"
"strconv"
"github.com/syncthing/syncthing/internal/osutil"
@@ -56,7 +55,7 @@ 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 {
@@ -124,10 +123,9 @@ func (v Simple) Archive(filePath string) error {
// Use all the found filenames. "~" sorts after "." so all old pattern
// files will be deleted before any new, which is as it should be.
versions := append(oldVersions, newVersions...)
versions := uniqueSortedStrings(append(oldVersions, newVersions...))
if len(versions) > v.keep {
sort.Strings(versions)
for _, toRemove := range versions[:len(versions)-v.keep] {
if debug {
l.Debugln("cleaning out", toRemove)

View File

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

View File

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

View File

@@ -106,21 +106,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 +155,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,
},
@@ -373,9 +379,12 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) {
if rn == "." || rn == ".stfolder" {
return nil
}
if rn == ".stversions" {
return filepath.SkipDir
}
var f fileInfo
if ok, err := symlinks.IsSymlink(path); err == nil && ok {
if info.Mode()&os.ModeSymlink != 0 {
f = fileInfo{
name: rn,
mode: os.ModeSymlink,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,10 +24,53 @@ import (
"testing"
"time"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/symlinks"
)
func TestSymlinks(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()
testSymlinks(t)
}
func TestSymlinksSimpleVersioning(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{
Type: "simple",
Params: map[string]string{"keep": "5"},
}
cfg.SetFolder(fld)
cfg.Save()
testSymlinks(t)
}
func TestSymlinksStaggeredVersioning(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{
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 +142,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 +279,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...")