mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-16 09:48:51 -05:00
Compare commits
171 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1816320124 | ||
|
|
f09bfe293d | ||
|
|
7b4e8fda4b | ||
|
|
c95812353f | ||
|
|
b622ec7a28 | ||
|
|
d8fbe7b77f | ||
|
|
dbcac37d91 | ||
|
|
d4d391b34f | ||
|
|
571cf7d490 | ||
|
|
e18b19ca5a | ||
|
|
48651bf482 | ||
|
|
5034a41c08 | ||
|
|
6795173e77 | ||
|
|
219ef996f5 | ||
|
|
00af1db275 | ||
|
|
5935ea896f | ||
|
|
459983c05e | ||
|
|
ebf4f029ac | ||
|
|
0eec945df1 | ||
|
|
8824b9d68f | ||
|
|
d2862814c5 | ||
|
|
25fece2d50 | ||
|
|
beb4239d1b | ||
|
|
2b78e37d92 | ||
|
|
a2070d9ce4 | ||
|
|
5827a686b8 | ||
|
|
dec479532e | ||
|
|
5d173168cc | ||
|
|
2aac1cde04 | ||
|
|
3676f0268f | ||
|
|
a7b75a54bb | ||
|
|
961a87b743 | ||
|
|
e03d59e381 | ||
|
|
d46ce5003c | ||
|
|
4c4143d9be | ||
|
|
8bc7d259f4 | ||
|
|
2d047fa428 | ||
|
|
735d420d40 | ||
|
|
bc9fc1aece | ||
|
|
b88e3c99c1 | ||
|
|
ce3e6e084c | ||
|
|
2a58ca7697 | ||
|
|
af96f7a0cd | ||
|
|
7d39d1a925 | ||
|
|
1b6c700e18 | ||
|
|
6304bd60ee | ||
|
|
4ad4417740 | ||
|
|
6a4c259a73 | ||
|
|
12eabb220d | ||
|
|
d68ce2d68c | ||
|
|
8e02c040eb | ||
|
|
a7a317c284 | ||
|
|
9d6ef24660 | ||
|
|
14014408fb | ||
|
|
b933e9666a | ||
|
|
7aff59bcce | ||
|
|
8e2760cb3d | ||
|
|
7a9fc6dbd3 | ||
|
|
75d0dc251e | ||
|
|
9a50c4d93f | ||
|
|
010d5a0192 | ||
|
|
cf1594829a | ||
|
|
854d720ce0 | ||
|
|
2f43c74ece | ||
|
|
b9817ac6b4 | ||
|
|
1e8da0d494 | ||
|
|
c47be7b415 | ||
|
|
d3f6cb860f | ||
|
|
83d25f09a3 | ||
|
|
ed747a2d3d | ||
|
|
3a8ee4ce2e | ||
|
|
5ac01a3af4 | ||
|
|
46343f2f9e | ||
|
|
56ccb5b2ab | ||
|
|
9a946eed80 | ||
|
|
9c6cb0f630 | ||
|
|
1b066d6965 | ||
|
|
54c3caad53 | ||
|
|
9b5e8aaf83 | ||
|
|
5143c09bcf | ||
|
|
2496185629 | ||
|
|
34deb82aea | ||
|
|
8f72ae9da2 | ||
|
|
b753f01ac1 | ||
|
|
fd0a147ae6 | ||
|
|
e94bd90782 | ||
|
|
ce4b897d0e | ||
|
|
a7694029e2 | ||
|
|
1e9110b763 | ||
|
|
6f3fbbbe49 | ||
|
|
d346ec7bfe | ||
|
|
26a3613397 | ||
|
|
e6318bddf3 | ||
|
|
514bb0beda | ||
|
|
41b1bd2f05 | ||
|
|
bf40dadf04 | ||
|
|
cb1678ebec | ||
|
|
0c1ac568b5 | ||
|
|
0f9550c747 | ||
|
|
b13ae17a47 | ||
|
|
f762a12d18 | ||
|
|
20d30a80be | ||
|
|
229b218203 | ||
|
|
4b668aaca8 | ||
|
|
8c7f1421c6 | ||
|
|
d90b2c1d52 | ||
|
|
22f39be197 | ||
|
|
2fa45436c2 | ||
|
|
cadbb6bbce | ||
|
|
2c89f04be7 | ||
|
|
597011e3a9 | ||
|
|
0d433b58ba | ||
|
|
cde8ef56e5 | ||
|
|
110816c7aa | ||
|
|
fbb1e168f7 | ||
|
|
23085eb5ae | ||
|
|
7344a6205f | ||
|
|
4b76ec40c0 | ||
|
|
90101d0269 | ||
|
|
7ac84c0660 | ||
|
|
2090530bbb | ||
|
|
b6cb7ddbaf | ||
|
|
3422d9335c | ||
|
|
e91f9a944e | ||
|
|
e7ddc7cf0f | ||
|
|
40dfa48756 | ||
|
|
579f92cf5f | ||
|
|
4565125da9 | ||
|
|
ce13a01e65 | ||
|
|
618a8682b7 | ||
|
|
963077f918 | ||
|
|
3704d2d86b | ||
|
|
fc6a029311 | ||
|
|
7c7b1e6c2d | ||
|
|
892920039d | ||
|
|
51cdd38c3e | ||
|
|
80977bd4c0 | ||
|
|
d8022f94ef | ||
|
|
1c43587d7d | ||
|
|
b2ed32b118 | ||
|
|
0cc815d816 | ||
|
|
d452b7593f | ||
|
|
5346bdc683 | ||
|
|
dc5c1e2002 | ||
|
|
2e48e298a2 | ||
|
|
7a1aaaf5c4 | ||
|
|
bde92d5cfe | ||
|
|
691f0f4845 | ||
|
|
fdd458d2fe | ||
|
|
d2c0b8374a | ||
|
|
c96c78892d | ||
|
|
957643f523 | ||
|
|
749bbec566 | ||
|
|
25e363c5fb | ||
|
|
febeed3277 | ||
|
|
9d07aa006d | ||
|
|
12d69e25dd | ||
|
|
0c9f1efc75 | ||
|
|
665b4506e7 | ||
|
|
cb5548ceb8 | ||
|
|
c9492e54f7 | ||
|
|
12490eafff | ||
|
|
6e83d11d5f | ||
|
|
9c6aedc91b | ||
|
|
a9339d0627 | ||
|
|
4d9aa10532 | ||
|
|
b00264b594 | ||
|
|
e329c7015e | ||
|
|
1392cfc72d | ||
|
|
c6688d8f89 | ||
|
|
87abea0ba3 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,3 +13,5 @@ perfstats*.csv
|
||||
coverage.xml
|
||||
!gui/scripts/syncthing
|
||||
.DS_Store
|
||||
syncthing.md5
|
||||
syncthing.exe.md5
|
||||
|
||||
7
AUTHORS
7
AUTHORS
@@ -5,15 +5,18 @@ 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 Schulz <ueomkail@gmail.com> <uok@users.noreply.github.com>
|
||||
Ben Sidhom <bsidhom@gmail.com>
|
||||
Brandon Philips <brandon@ifup.org>
|
||||
Brendan Long <self@brendanlong.com>
|
||||
Caleb Callaway <enlightened.despot@gmail.com>
|
||||
Cathryne Linenweaver <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
|
||||
Chris Joel <chris@scriptolo.gy>
|
||||
Daniel Martí <mvdan@mvdan.cc>
|
||||
Dennis Wilson <dw@risu.io>
|
||||
Dominik Heidler <dominik@heidler.eu>
|
||||
Emil Hessman <emil@hessman.se>
|
||||
Federico Castagnini <federico.castagnini@gmail.com>
|
||||
Felix Ableitner <me@nutomic.com>
|
||||
Felix Unterpaintner <bigbear2nd@gmail.com>
|
||||
Gilli Sigurdsson <gilli@vx.is>
|
||||
@@ -24,10 +27,12 @@ Jochen Voss <voss@seehuhn.de>
|
||||
Lode Hoste <zillode@zillode.be>
|
||||
Marcin Dziadus <dziadus.marcin@gmail.com>
|
||||
Michael Tilli <pyfisch@gmail.com>
|
||||
Peter Hoeg <peter@speartail.com>
|
||||
Philippe Schommers <philippe@schommers.be>
|
||||
Phill Luby <phill.luby@newredo.com>
|
||||
Piotr Bejda <piotrb10@gmail.com>
|
||||
Ryan Sullivan <kayoticsully@gmail.com>
|
||||
Tim Abell <tim@timwise.co.uk>
|
||||
Tomas Cerveny <kozec@kozec.com>
|
||||
Tully Robinson <tully@tojr.org>
|
||||
Veeti Paananen <veeti.paananen@rojekti.fi>
|
||||
|
||||
@@ -46,6 +46,20 @@ All nontrivial contributions should go through the pull request
|
||||
mechanism for internal review. Determining what is "nontrivial" is left
|
||||
at the discretion of the contributor.
|
||||
|
||||
### Authorship
|
||||
|
||||
All code authors are listed in the AUTHORS file. Commits must be made
|
||||
with the same name and email as listed in the AUTHORS file. To
|
||||
accomplish this, ensure that your git configuration is set correctly
|
||||
prior to making your first commit;
|
||||
|
||||
$ git config --global user.name "Jane Doe"
|
||||
$ git config --global user.email janedoe@example.com
|
||||
|
||||
You must be reachable on the given email address. If you do not wish to
|
||||
use your real name for whatever reason, using a nickname or pseudonym is
|
||||
perfectly acceptable.
|
||||
|
||||
### Core Team
|
||||
|
||||
The Syncthing core team currently consists of the following members;
|
||||
|
||||
26
Godeps/Godeps.json
generated
26
Godeps/Godeps.json
generated
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"ImportPath": "github.com/syncthing/syncthing",
|
||||
"GoVersion": "go1.4rc1",
|
||||
"GoVersion": "go1.4",
|
||||
"Packages": [
|
||||
"./cmd/..."
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/AudriusButkevicius/lrufdcache",
|
||||
"Rev": "9bddff8f67224ab3e7d80525a6ae9bcf1ce10769"
|
||||
"ImportPath": "github.com/AudriusButkevicius/lfu-go",
|
||||
"Rev": "164bcecceb92fd6037f4d18a8d97b495ec6ef669"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/bkaradzic/go-lz4",
|
||||
@@ -23,11 +23,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/xdr",
|
||||
"Rev": "45c46b7db7ff83b8b9ee09bbd95f36ab50043ece"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/golang/groupcache/lru",
|
||||
"Rev": "f391194b967ae0d21deadc861ea87120d9687447"
|
||||
"Rev": "214788d8fedfc310c18eca9ed12be408a5054cd5"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/juju/ratelimit",
|
||||
@@ -35,7 +31,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||
"Rev": "97e257099d2ab9578151ba85e2641e2cd14d3ca8"
|
||||
"Rev": "63c9e642efad852f49e20a6f90194cae112fd2ac"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syndtr/gosnappy/snappy",
|
||||
@@ -55,23 +51,19 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/bcrypt",
|
||||
"Comment": "null-236",
|
||||
"Rev": "69e2a90ed92d03812364aeb947b7068dc42e561e"
|
||||
"Rev": "731db29863ea7213d9556d0170afb38987f401d4"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/blowfish",
|
||||
"Comment": "null-236",
|
||||
"Rev": "69e2a90ed92d03812364aeb947b7068dc42e561e"
|
||||
"Rev": "731db29863ea7213d9556d0170afb38987f401d4"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/text/transform",
|
||||
"Comment": "null-112",
|
||||
"Rev": "2f707e0ad64637ca1318279be7201f5ed19c4050"
|
||||
"Rev": "985ee5acfaf1ff6712c7c99438752f8e09416ccb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/text/unicode/norm",
|
||||
"Comment": "null-112",
|
||||
"Rev": "2f707e0ad64637ca1318279be7201f5ed19c4050"
|
||||
"Rev": "985ee5acfaf1ff6712c7c99438752f8e09416ccb"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
19
Godeps/_workspace/src/github.com/AudriusButkevicius/lfu-go/LICENSE
generated
vendored
Normal file
19
Godeps/_workspace/src/github.com/AudriusButkevicius/lfu-go/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
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.
|
||||
19
Godeps/_workspace/src/github.com/AudriusButkevicius/lfu-go/README.md
generated
vendored
Normal file
19
Godeps/_workspace/src/github.com/AudriusButkevicius/lfu-go/README.md
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
```
|
||||
156
Godeps/_workspace/src/github.com/AudriusButkevicius/lfu-go/lfu.go
generated
vendored
Normal file
156
Godeps/_workspace/src/github.com/AudriusButkevicius/lfu-go/lfu.go
generated
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
68
Godeps/_workspace/src/github.com/AudriusButkevicius/lfu-go/lfu_test.go
generated
vendored
Normal file
68
Godeps/_workspace/src/github.com/AudriusButkevicius/lfu-go/lfu_test.go
generated
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
24
Godeps/_workspace/src/github.com/AudriusButkevicius/lrufdcache/.gitignore
generated
vendored
24
Godeps/_workspace/src/github.com/AudriusButkevicius/lrufdcache/.gitignore
generated
vendored
@@ -1,24 +0,0 @@
|
||||
# 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
|
||||
25
Godeps/_workspace/src/github.com/AudriusButkevicius/lrufdcache/LICENSE
generated
vendored
25
Godeps/_workspace/src/github.com/AudriusButkevicius/lrufdcache/LICENSE
generated
vendored
@@ -1,25 +0,0 @@
|
||||
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>
|
||||
|
||||
4
Godeps/_workspace/src/github.com/AudriusButkevicius/lrufdcache/README.md
generated
vendored
4
Godeps/_workspace/src/github.com/AudriusButkevicius/lrufdcache/README.md
generated
vendored
@@ -1,4 +0,0 @@
|
||||
lrufdcache
|
||||
==========
|
||||
|
||||
A LRU file descriptor cache
|
||||
88
Godeps/_workspace/src/github.com/AudriusButkevicius/lrufdcache/lrufdcache.go
generated
vendored
88
Godeps/_workspace/src/github.com/AudriusButkevicius/lrufdcache/lrufdcache.go
generated
vendored
@@ -1,88 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
195
Godeps/_workspace/src/github.com/AudriusButkevicius/lrufdcache/lrufdcache_test.go
generated
vendored
195
Godeps/_workspace/src/github.com/AudriusButkevicius/lrufdcache/lrufdcache_test.go
generated
vendored
@@ -1,195 +0,0 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
4
Godeps/_workspace/src/github.com/calmh/xdr/reader.go
generated
vendored
4
Godeps/_workspace/src/github.com/calmh/xdr/reader.go
generated
vendored
@@ -154,6 +154,10 @@ func (e XDRError) Error() string {
|
||||
return "xdr " + e.op + ": " + e.err.Error()
|
||||
}
|
||||
|
||||
func (e XDRError) IsEOF() bool {
|
||||
return e.err == io.EOF
|
||||
}
|
||||
|
||||
func (r *Reader) Error() error {
|
||||
if r.err == nil {
|
||||
return nil
|
||||
|
||||
121
Godeps/_workspace/src/github.com/golang/groupcache/lru/lru.go
generated
vendored
121
Godeps/_workspace/src/github.com/golang/groupcache/lru/lru.go
generated
vendored
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
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()
|
||||
}
|
||||
73
Godeps/_workspace/src/github.com/golang/groupcache/lru/lru_test.go
generated
vendored
73
Godeps/_workspace/src/github.com/golang/groupcache/lru/lru_test.go
generated
vendored
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
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")
|
||||
}
|
||||
}
|
||||
753
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache.go
generated
vendored
753
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache.go
generated
vendored
@@ -8,152 +8,669 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
// SetFunc is the function that will be called by Namespace.Get to create
|
||||
// a cache object, if charge is less than one than the cache object will
|
||||
// not be registered to cache tree, if value is nil then the cache object
|
||||
// will not be created.
|
||||
type SetFunc func() (charge int, value interface{})
|
||||
|
||||
// DelFin is the function that will be called as the result of a delete operation.
|
||||
// Exist == true is indication that the object is exist, and pending == true is
|
||||
// indication of deletion already happen but haven't done yet (wait for all handles
|
||||
// to be released). And exist == false means the object doesn't exist.
|
||||
type DelFin func(exist, pending bool)
|
||||
|
||||
// PurgeFin is the function that will be called as the result of a purge operation.
|
||||
type PurgeFin func(ns, key uint64)
|
||||
|
||||
// Cache is a cache tree. A cache instance must be goroutine-safe.
|
||||
type Cache interface {
|
||||
// SetCapacity sets cache tree capacity.
|
||||
SetCapacity(capacity int)
|
||||
|
||||
// Capacity returns cache tree capacity.
|
||||
// Cacher provides interface to implements a caching functionality.
|
||||
// An implementation must be goroutine-safe.
|
||||
type Cacher interface {
|
||||
// Capacity returns cache capacity.
|
||||
Capacity() int
|
||||
|
||||
// Used returns used cache tree capacity.
|
||||
Used() int
|
||||
// SetCapacity sets cache capacity.
|
||||
SetCapacity(capacity int)
|
||||
|
||||
// Size returns entire alive cache objects size.
|
||||
Size() int
|
||||
// Promote promotes the 'cache node'.
|
||||
Promote(n *Node)
|
||||
|
||||
// NumObjects returns number of alive objects.
|
||||
NumObjects() int
|
||||
// Ban evicts the 'cache node' and prevent subsequent 'promote'.
|
||||
Ban(n *Node)
|
||||
|
||||
// GetNamespace gets cache namespace with the given id.
|
||||
// GetNamespace is never return nil.
|
||||
GetNamespace(id uint64) Namespace
|
||||
// Evict evicts the 'cache node'.
|
||||
Evict(n *Node)
|
||||
|
||||
// PurgeNamespace purges cache namespace with the given id from this cache tree.
|
||||
// Also read Namespace.Purge.
|
||||
PurgeNamespace(id uint64, fin PurgeFin)
|
||||
// EvictNS evicts 'cache node' with the given namespace.
|
||||
EvictNS(ns uint64)
|
||||
|
||||
// ZapNamespace detaches cache namespace with the given id from this cache tree.
|
||||
// Also read Namespace.Zap.
|
||||
ZapNamespace(id uint64)
|
||||
// EvictAll evicts all 'cache node'.
|
||||
EvictAll()
|
||||
|
||||
// Purge purges all cache namespace from this cache tree.
|
||||
// This is behave the same as calling Namespace.Purge method on all cache namespace.
|
||||
Purge(fin PurgeFin)
|
||||
|
||||
// Zap detaches all cache namespace from this cache tree.
|
||||
// This is behave the same as calling Namespace.Zap method on all cache namespace.
|
||||
Zap()
|
||||
// Close closes the 'cache tree'
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Namespace is a cache namespace. A namespace instance must be goroutine-safe.
|
||||
type Namespace interface {
|
||||
// Get gets cache object with the given key.
|
||||
// If cache object is not found and setf is not nil, Get will atomically creates
|
||||
// the cache object by calling setf. Otherwise Get will returns nil.
|
||||
//
|
||||
// The returned cache handle should be released after use by calling Release
|
||||
// method.
|
||||
Get(key uint64, setf SetFunc) Handle
|
||||
// Value is a 'cacheable object'. It may implements util.Releaser, if
|
||||
// so the the Release method will be called once object is released.
|
||||
type Value interface{}
|
||||
|
||||
// Delete removes cache object with the given key from cache tree.
|
||||
// A deleted cache object will be released as soon as all of its handles have
|
||||
// been released.
|
||||
// Delete only happen once, subsequent delete will consider cache object doesn't
|
||||
// exist, even if the cache object ins't released yet.
|
||||
//
|
||||
// If not nil, fin will be called if the cache object doesn't exist or when
|
||||
// finally be released.
|
||||
//
|
||||
// Delete returns true if such cache object exist and never been deleted.
|
||||
Delete(key uint64, fin DelFin) bool
|
||||
|
||||
// Purge removes all cache objects within this namespace from cache tree.
|
||||
// This is the same as doing delete on all cache objects.
|
||||
//
|
||||
// If not nil, fin will be called on all cache objects when its finally be
|
||||
// released.
|
||||
Purge(fin PurgeFin)
|
||||
|
||||
// Zap detaches namespace from cache tree and release all its cache objects.
|
||||
// A zapped namespace can never be filled again.
|
||||
// Calling Get on zapped namespace will always return nil.
|
||||
Zap()
|
||||
type CacheGetter struct {
|
||||
Cache *Cache
|
||||
NS uint64
|
||||
}
|
||||
|
||||
// Handle is a cache handle.
|
||||
type Handle interface {
|
||||
// Release releases this cache handle. This method can be safely called mutiple
|
||||
// times.
|
||||
Release()
|
||||
|
||||
// Value returns value of this cache handle.
|
||||
// Value will returns nil after this cache handle have be released.
|
||||
Value() interface{}
|
||||
func (g *CacheGetter) Get(key uint64, setFunc func() (size int, value Value)) *Handle {
|
||||
return g.Cache.Get(g.NS, key, setFunc)
|
||||
}
|
||||
|
||||
// The hash tables implementation is based on:
|
||||
// "Dynamic-Sized Nonblocking Hash Tables", by Yujie Liu, Kunlong Zhang, and Michael Spear. ACM Symposium on Principles of Distributed Computing, Jul 2014.
|
||||
|
||||
const (
|
||||
DelNotExist = iota
|
||||
DelExist
|
||||
DelPendig
|
||||
mInitialSize = 1 << 4
|
||||
mOverflowThreshold = 1 << 5
|
||||
mOverflowGrowThreshold = 1 << 7
|
||||
)
|
||||
|
||||
// Namespace state.
|
||||
type nsState int
|
||||
|
||||
const (
|
||||
nsEffective nsState = iota
|
||||
nsZapped
|
||||
)
|
||||
|
||||
// Node state.
|
||||
type nodeState int
|
||||
|
||||
const (
|
||||
nodeZero nodeState = iota
|
||||
nodeEffective
|
||||
nodeEvicted
|
||||
nodeDeleted
|
||||
)
|
||||
|
||||
// Fake handle.
|
||||
type fakeHandle struct {
|
||||
value interface{}
|
||||
fin func()
|
||||
once uint32
|
||||
type mBucket struct {
|
||||
mu sync.Mutex
|
||||
node []*Node
|
||||
frozen bool
|
||||
}
|
||||
|
||||
func (h *fakeHandle) Value() interface{} {
|
||||
if atomic.LoadUint32(&h.once) == 0 {
|
||||
return h.value
|
||||
func (b *mBucket) freeze() []*Node {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if !b.frozen {
|
||||
b.frozen = true
|
||||
}
|
||||
return b.node
|
||||
}
|
||||
|
||||
func (b *mBucket) get(r *Cache, h *mNode, hash uint32, ns, key uint64, noset bool) (done, added bool, n *Node) {
|
||||
b.mu.Lock()
|
||||
|
||||
if b.frozen {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Scan the node.
|
||||
for _, n := range b.node {
|
||||
if n.hash == hash && n.ns == ns && n.key == key {
|
||||
atomic.AddInt32(&n.ref, 1)
|
||||
b.mu.Unlock()
|
||||
return true, false, n
|
||||
}
|
||||
}
|
||||
|
||||
// Get only.
|
||||
if noset {
|
||||
b.mu.Unlock()
|
||||
return true, false, nil
|
||||
}
|
||||
|
||||
// Create node.
|
||||
n = &Node{
|
||||
r: r,
|
||||
hash: hash,
|
||||
ns: ns,
|
||||
key: key,
|
||||
ref: 1,
|
||||
}
|
||||
// Add node to bucket.
|
||||
b.node = append(b.node, n)
|
||||
bLen := len(b.node)
|
||||
b.mu.Unlock()
|
||||
|
||||
// Update counter.
|
||||
grow := atomic.AddInt32(&r.nodes, 1) >= h.growThreshold
|
||||
if bLen > mOverflowThreshold {
|
||||
grow = grow || atomic.AddInt32(&h.overflow, 1) >= mOverflowGrowThreshold
|
||||
}
|
||||
|
||||
// Grow.
|
||||
if grow && atomic.CompareAndSwapInt32(&h.resizeInProgess, 0, 1) {
|
||||
nhLen := len(h.buckets) << 1
|
||||
nh := &mNode{
|
||||
buckets: make([]unsafe.Pointer, nhLen),
|
||||
mask: uint32(nhLen) - 1,
|
||||
pred: unsafe.Pointer(h),
|
||||
growThreshold: int32(nhLen * mOverflowThreshold),
|
||||
shrinkThreshold: int32(nhLen >> 1),
|
||||
}
|
||||
ok := atomic.CompareAndSwapPointer(&r.mHead, unsafe.Pointer(h), unsafe.Pointer(nh))
|
||||
if !ok {
|
||||
panic("BUG: failed swapping head")
|
||||
}
|
||||
go nh.initBuckets()
|
||||
}
|
||||
|
||||
return true, true, n
|
||||
}
|
||||
|
||||
func (b *mBucket) delete(r *Cache, h *mNode, hash uint32, ns, key uint64) (done, deleted bool) {
|
||||
b.mu.Lock()
|
||||
|
||||
if b.frozen {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Scan the node.
|
||||
var (
|
||||
n *Node
|
||||
bLen int
|
||||
)
|
||||
for i := range b.node {
|
||||
n = b.node[i]
|
||||
if n.ns == ns && n.key == key {
|
||||
if atomic.LoadInt32(&n.ref) == 0 {
|
||||
deleted = true
|
||||
|
||||
// Call releaser.
|
||||
if n.value != nil {
|
||||
if r, ok := n.value.(util.Releaser); ok {
|
||||
r.Release()
|
||||
}
|
||||
n.value = nil
|
||||
}
|
||||
|
||||
// Remove node from bucket.
|
||||
b.node = append(b.node[:i], b.node[i+1:]...)
|
||||
bLen = len(b.node)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
if deleted {
|
||||
// Call OnDel.
|
||||
for _, f := range n.onDel {
|
||||
f()
|
||||
}
|
||||
|
||||
// Update counter.
|
||||
atomic.AddInt32(&r.size, int32(n.size)*-1)
|
||||
shrink := atomic.AddInt32(&r.nodes, -1) < h.shrinkThreshold
|
||||
if bLen >= mOverflowThreshold {
|
||||
atomic.AddInt32(&h.overflow, -1)
|
||||
}
|
||||
|
||||
// Shrink.
|
||||
if shrink && len(h.buckets) > mInitialSize && atomic.CompareAndSwapInt32(&h.resizeInProgess, 0, 1) {
|
||||
nhLen := len(h.buckets) >> 1
|
||||
nh := &mNode{
|
||||
buckets: make([]unsafe.Pointer, nhLen),
|
||||
mask: uint32(nhLen) - 1,
|
||||
pred: unsafe.Pointer(h),
|
||||
growThreshold: int32(nhLen * mOverflowThreshold),
|
||||
shrinkThreshold: int32(nhLen >> 1),
|
||||
}
|
||||
ok := atomic.CompareAndSwapPointer(&r.mHead, unsafe.Pointer(h), unsafe.Pointer(nh))
|
||||
if !ok {
|
||||
panic("BUG: failed swapping head")
|
||||
}
|
||||
go nh.initBuckets()
|
||||
}
|
||||
}
|
||||
|
||||
return true, deleted
|
||||
}
|
||||
|
||||
type mNode struct {
|
||||
buckets []unsafe.Pointer // []*mBucket
|
||||
mask uint32
|
||||
pred unsafe.Pointer // *mNode
|
||||
resizeInProgess int32
|
||||
|
||||
overflow int32
|
||||
growThreshold int32
|
||||
shrinkThreshold int32
|
||||
}
|
||||
|
||||
func (n *mNode) initBucket(i uint32) *mBucket {
|
||||
if b := (*mBucket)(atomic.LoadPointer(&n.buckets[i])); b != nil {
|
||||
return b
|
||||
}
|
||||
|
||||
p := (*mNode)(atomic.LoadPointer(&n.pred))
|
||||
if p != nil {
|
||||
var node []*Node
|
||||
if n.mask > p.mask {
|
||||
// Grow.
|
||||
pb := (*mBucket)(atomic.LoadPointer(&p.buckets[i&p.mask]))
|
||||
if pb == nil {
|
||||
pb = p.initBucket(i & p.mask)
|
||||
}
|
||||
m := pb.freeze()
|
||||
// Split nodes.
|
||||
for _, x := range m {
|
||||
if x.hash&n.mask == i {
|
||||
node = append(node, x)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Shrink.
|
||||
pb0 := (*mBucket)(atomic.LoadPointer(&p.buckets[i]))
|
||||
if pb0 == nil {
|
||||
pb0 = p.initBucket(i)
|
||||
}
|
||||
pb1 := (*mBucket)(atomic.LoadPointer(&p.buckets[i+uint32(len(n.buckets))]))
|
||||
if pb1 == nil {
|
||||
pb1 = p.initBucket(i + uint32(len(n.buckets)))
|
||||
}
|
||||
m0 := pb0.freeze()
|
||||
m1 := pb1.freeze()
|
||||
// Merge nodes.
|
||||
node = make([]*Node, 0, len(m0)+len(m1))
|
||||
node = append(node, m0...)
|
||||
node = append(node, m1...)
|
||||
}
|
||||
b := &mBucket{node: node}
|
||||
if atomic.CompareAndSwapPointer(&n.buckets[i], nil, unsafe.Pointer(b)) {
|
||||
if len(node) > mOverflowThreshold {
|
||||
atomic.AddInt32(&n.overflow, int32(len(node)-mOverflowThreshold))
|
||||
}
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
return (*mBucket)(atomic.LoadPointer(&n.buckets[i]))
|
||||
}
|
||||
|
||||
func (n *mNode) initBuckets() {
|
||||
for i := range n.buckets {
|
||||
n.initBucket(uint32(i))
|
||||
}
|
||||
atomic.StorePointer(&n.pred, nil)
|
||||
}
|
||||
|
||||
// Cache is a 'cache map'.
|
||||
type Cache struct {
|
||||
mu sync.RWMutex
|
||||
mHead unsafe.Pointer // *mNode
|
||||
nodes int32
|
||||
size int32
|
||||
cacher Cacher
|
||||
closed bool
|
||||
}
|
||||
|
||||
// NewCache creates a new 'cache map'. The cacher is optional and
|
||||
// may be nil.
|
||||
func NewCache(cacher Cacher) *Cache {
|
||||
h := &mNode{
|
||||
buckets: make([]unsafe.Pointer, mInitialSize),
|
||||
mask: mInitialSize - 1,
|
||||
growThreshold: int32(mInitialSize * mOverflowThreshold),
|
||||
shrinkThreshold: 0,
|
||||
}
|
||||
for i := range h.buckets {
|
||||
h.buckets[i] = unsafe.Pointer(&mBucket{})
|
||||
}
|
||||
r := &Cache{
|
||||
mHead: unsafe.Pointer(h),
|
||||
cacher: cacher,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Cache) getBucket(hash uint32) (*mNode, *mBucket) {
|
||||
h := (*mNode)(atomic.LoadPointer(&r.mHead))
|
||||
i := hash & h.mask
|
||||
b := (*mBucket)(atomic.LoadPointer(&h.buckets[i]))
|
||||
if b == nil {
|
||||
b = h.initBucket(i)
|
||||
}
|
||||
return h, b
|
||||
}
|
||||
|
||||
func (r *Cache) delete(n *Node) bool {
|
||||
for {
|
||||
h, b := r.getBucket(n.hash)
|
||||
done, deleted := b.delete(r, h, n.hash, n.ns, n.key)
|
||||
if done {
|
||||
return deleted
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Nodes returns number of 'cache node' in the map.
|
||||
func (r *Cache) Nodes() int {
|
||||
return int(atomic.LoadInt32(&r.nodes))
|
||||
}
|
||||
|
||||
// Size returns sums of 'cache node' size in the map.
|
||||
func (r *Cache) Size() int {
|
||||
return int(atomic.LoadInt32(&r.size))
|
||||
}
|
||||
|
||||
// Capacity returns cache capacity.
|
||||
func (r *Cache) Capacity() int {
|
||||
if r.cacher == nil {
|
||||
return 0
|
||||
}
|
||||
return r.cacher.Capacity()
|
||||
}
|
||||
|
||||
// SetCapacity sets cache capacity.
|
||||
func (r *Cache) SetCapacity(capacity int) {
|
||||
if r.cacher != nil {
|
||||
r.cacher.SetCapacity(capacity)
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets 'cache node' with the given namespace and key.
|
||||
// If cache node is not found and setFunc is not nil, Get will atomically creates
|
||||
// the 'cache node' by calling setFunc. Otherwise Get will returns nil.
|
||||
//
|
||||
// The returned 'cache handle' should be released after use by calling Release
|
||||
// method.
|
||||
func (r *Cache) Get(ns, key uint64, setFunc func() (size int, value Value)) *Handle {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if r.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
hash := murmur32(ns, key, 0xf00)
|
||||
for {
|
||||
h, b := r.getBucket(hash)
|
||||
done, _, n := b.get(r, h, hash, ns, key, setFunc == nil)
|
||||
if done {
|
||||
if n != nil {
|
||||
n.mu.Lock()
|
||||
if n.value == nil {
|
||||
if setFunc == nil {
|
||||
n.mu.Unlock()
|
||||
n.unref()
|
||||
return nil
|
||||
}
|
||||
|
||||
n.size, n.value = setFunc()
|
||||
if n.value == nil {
|
||||
n.size = 0
|
||||
n.mu.Unlock()
|
||||
n.unref()
|
||||
return nil
|
||||
}
|
||||
atomic.AddInt32(&r.size, int32(n.size))
|
||||
}
|
||||
n.mu.Unlock()
|
||||
if r.cacher != nil {
|
||||
r.cacher.Promote(n)
|
||||
}
|
||||
return &Handle{unsafe.Pointer(n)}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *fakeHandle) Release() {
|
||||
if !atomic.CompareAndSwapUint32(&h.once, 0, 1) {
|
||||
// Delete removes and ban 'cache node' with the given namespace and key.
|
||||
// A banned 'cache node' will never inserted into the 'cache tree'. Ban
|
||||
// only attributed to the particular 'cache node', so when a 'cache node'
|
||||
// is recreated it will not be banned.
|
||||
//
|
||||
// If onDel is not nil, then it will be executed if such 'cache node'
|
||||
// doesn't exist or once the 'cache node' is released.
|
||||
//
|
||||
// Delete return true is such 'cache node' exist.
|
||||
func (r *Cache) Delete(ns, key uint64, onDel func()) bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if r.closed {
|
||||
return false
|
||||
}
|
||||
|
||||
hash := murmur32(ns, key, 0xf00)
|
||||
for {
|
||||
h, b := r.getBucket(hash)
|
||||
done, _, n := b.get(r, h, hash, ns, key, true)
|
||||
if done {
|
||||
if n != nil {
|
||||
if onDel != nil {
|
||||
n.mu.Lock()
|
||||
n.onDel = append(n.onDel, onDel)
|
||||
n.mu.Unlock()
|
||||
}
|
||||
if r.cacher != nil {
|
||||
r.cacher.Ban(n)
|
||||
}
|
||||
n.unref()
|
||||
return true
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if onDel != nil {
|
||||
onDel()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Evict evicts 'cache node' with the given namespace and key. This will
|
||||
// simply call Cacher.Evict.
|
||||
//
|
||||
// Evict return true is such 'cache node' exist.
|
||||
func (r *Cache) Evict(ns, key uint64) bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if r.closed {
|
||||
return false
|
||||
}
|
||||
|
||||
hash := murmur32(ns, key, 0xf00)
|
||||
for {
|
||||
h, b := r.getBucket(hash)
|
||||
done, _, n := b.get(r, h, hash, ns, key, true)
|
||||
if done {
|
||||
if n != nil {
|
||||
if r.cacher != nil {
|
||||
r.cacher.Evict(n)
|
||||
}
|
||||
n.unref()
|
||||
return true
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// EvictNS evicts 'cache node' with the given namespace. This will
|
||||
// simply call Cacher.EvictNS.
|
||||
func (r *Cache) EvictNS(ns uint64) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if r.closed {
|
||||
return
|
||||
}
|
||||
if h.fin != nil {
|
||||
h.fin()
|
||||
h.fin = nil
|
||||
|
||||
if r.cacher != nil {
|
||||
r.cacher.EvictNS(ns)
|
||||
}
|
||||
}
|
||||
|
||||
// EvictAll evicts all 'cache node'. This will simply call Cacher.EvictAll.
|
||||
func (r *Cache) EvictAll() {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
if r.closed {
|
||||
return
|
||||
}
|
||||
|
||||
if r.cacher != nil {
|
||||
r.cacher.EvictAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the 'cache map' and releases all 'cache node'.
|
||||
func (r *Cache) Close() error {
|
||||
r.mu.Lock()
|
||||
if !r.closed {
|
||||
r.closed = true
|
||||
|
||||
if r.cacher != nil {
|
||||
if err := r.cacher.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
h := (*mNode)(r.mHead)
|
||||
h.initBuckets()
|
||||
|
||||
for i := range h.buckets {
|
||||
b := (*mBucket)(h.buckets[i])
|
||||
for _, n := range b.node {
|
||||
// Call releaser.
|
||||
if n.value != nil {
|
||||
if r, ok := n.value.(util.Releaser); ok {
|
||||
r.Release()
|
||||
}
|
||||
n.value = nil
|
||||
}
|
||||
|
||||
// Call OnDel.
|
||||
for _, f := range n.onDel {
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Node is a 'cache node'.
|
||||
type Node struct {
|
||||
r *Cache
|
||||
|
||||
hash uint32
|
||||
ns, key uint64
|
||||
|
||||
mu sync.Mutex
|
||||
size int
|
||||
value Value
|
||||
|
||||
ref int32
|
||||
onDel []func()
|
||||
|
||||
CacheData unsafe.Pointer
|
||||
}
|
||||
|
||||
// NS returns this 'cache node' namespace.
|
||||
func (n *Node) NS() uint64 {
|
||||
return n.ns
|
||||
}
|
||||
|
||||
// Key returns this 'cache node' key.
|
||||
func (n *Node) Key() uint64 {
|
||||
return n.key
|
||||
}
|
||||
|
||||
// Size returns this 'cache node' size.
|
||||
func (n *Node) Size() int {
|
||||
return n.size
|
||||
}
|
||||
|
||||
// Value returns this 'cache node' value.
|
||||
func (n *Node) Value() Value {
|
||||
return n.value
|
||||
}
|
||||
|
||||
// Ref returns this 'cache node' ref counter.
|
||||
func (n *Node) Ref() int32 {
|
||||
return atomic.LoadInt32(&n.ref)
|
||||
}
|
||||
|
||||
// GetHandle returns an handle for this 'cache node'.
|
||||
func (n *Node) GetHandle() *Handle {
|
||||
if atomic.AddInt32(&n.ref, 1) <= 1 {
|
||||
panic("BUG: Node.GetHandle on zero ref")
|
||||
}
|
||||
return &Handle{unsafe.Pointer(n)}
|
||||
}
|
||||
|
||||
func (n *Node) unref() {
|
||||
if atomic.AddInt32(&n.ref, -1) == 0 {
|
||||
n.r.delete(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) unrefLocked() {
|
||||
if atomic.AddInt32(&n.ref, -1) == 0 {
|
||||
n.r.mu.RLock()
|
||||
if !n.r.closed {
|
||||
n.r.delete(n)
|
||||
}
|
||||
n.r.mu.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
type Handle struct {
|
||||
n unsafe.Pointer // *Node
|
||||
}
|
||||
|
||||
func (h *Handle) Value() Value {
|
||||
n := (*Node)(atomic.LoadPointer(&h.n))
|
||||
if n != nil {
|
||||
return n.value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handle) Release() {
|
||||
nPtr := atomic.LoadPointer(&h.n)
|
||||
if nPtr != nil && atomic.CompareAndSwapPointer(&h.n, nPtr, nil) {
|
||||
n := (*Node)(nPtr)
|
||||
n.unrefLocked()
|
||||
}
|
||||
}
|
||||
|
||||
func murmur32(ns, key uint64, seed uint32) uint32 {
|
||||
const (
|
||||
m = uint32(0x5bd1e995)
|
||||
r = 24
|
||||
)
|
||||
|
||||
k1 := uint32(ns >> 32)
|
||||
k2 := uint32(ns)
|
||||
k3 := uint32(key >> 32)
|
||||
k4 := uint32(key)
|
||||
|
||||
k1 *= m
|
||||
k1 ^= k1 >> r
|
||||
k1 *= m
|
||||
|
||||
k2 *= m
|
||||
k2 ^= k2 >> r
|
||||
k2 *= m
|
||||
|
||||
k3 *= m
|
||||
k3 ^= k3 >> r
|
||||
k3 *= m
|
||||
|
||||
k4 *= m
|
||||
k4 ^= k4 >> r
|
||||
k4 *= m
|
||||
|
||||
h := seed
|
||||
|
||||
h *= m
|
||||
h ^= k1
|
||||
h *= m
|
||||
h ^= k2
|
||||
h *= m
|
||||
h ^= k3
|
||||
h *= m
|
||||
h ^= k4
|
||||
|
||||
h ^= h >> 13
|
||||
h *= m
|
||||
h ^= h >> 15
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
907
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache_test.go
generated
vendored
907
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache_test.go
generated
vendored
@@ -13,11 +13,26 @@ import (
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type int32o int32
|
||||
|
||||
func (o *int32o) acquire() {
|
||||
if atomic.AddInt32((*int32)(o), 1) != 1 {
|
||||
panic("BUG: invalid ref")
|
||||
}
|
||||
}
|
||||
|
||||
func (o *int32o) Release() {
|
||||
if atomic.AddInt32((*int32)(o), -1) != 0 {
|
||||
panic("BUG: invalid ref")
|
||||
}
|
||||
}
|
||||
|
||||
type releaserFunc struct {
|
||||
fn func()
|
||||
value interface{}
|
||||
value Value
|
||||
}
|
||||
|
||||
func (r releaserFunc) Release() {
|
||||
@@ -26,8 +41,8 @@ func (r releaserFunc) Release() {
|
||||
}
|
||||
}
|
||||
|
||||
func set(ns Namespace, key uint64, value interface{}, charge int, relf func()) Handle {
|
||||
return ns.Get(key, func() (int, interface{}) {
|
||||
func set(c *Cache, ns, key uint64, value Value, charge int, relf func()) *Handle {
|
||||
return c.Get(ns, key, func() (int, Value) {
|
||||
if relf != nil {
|
||||
return charge, releaserFunc{relf, value}
|
||||
} else {
|
||||
@@ -36,7 +51,246 @@ func set(ns Namespace, key uint64, value interface{}, charge int, relf func()) H
|
||||
})
|
||||
}
|
||||
|
||||
func TestCache_HitMiss(t *testing.T) {
|
||||
func TestCacheMap(t *testing.T) {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
nsx := []struct {
|
||||
nobjects, nhandles, concurrent, repeat int
|
||||
}{
|
||||
{10000, 400, 50, 3},
|
||||
{100000, 1000, 100, 10},
|
||||
}
|
||||
|
||||
var (
|
||||
objects [][]int32o
|
||||
handles [][]unsafe.Pointer
|
||||
)
|
||||
|
||||
for _, x := range nsx {
|
||||
objects = append(objects, make([]int32o, x.nobjects))
|
||||
handles = append(handles, make([]unsafe.Pointer, x.nhandles))
|
||||
}
|
||||
|
||||
c := NewCache(nil)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
var done int32
|
||||
|
||||
for ns, x := range nsx {
|
||||
for i := 0; i < x.concurrent; i++ {
|
||||
wg.Add(1)
|
||||
go func(ns, i, repeat int, objects []int32o, handles []unsafe.Pointer) {
|
||||
defer wg.Done()
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
for j := len(objects) * repeat; j >= 0; j-- {
|
||||
key := uint64(r.Intn(len(objects)))
|
||||
h := c.Get(uint64(ns), key, func() (int, Value) {
|
||||
o := &objects[key]
|
||||
o.acquire()
|
||||
return 1, o
|
||||
})
|
||||
if v := h.Value().(*int32o); v != &objects[key] {
|
||||
t.Fatalf("#%d invalid value: want=%p got=%p", ns, &objects[key], v)
|
||||
}
|
||||
if objects[key] != 1 {
|
||||
t.Fatalf("#%d invalid object %d: %d", ns, key, objects[key])
|
||||
}
|
||||
if !atomic.CompareAndSwapPointer(&handles[r.Intn(len(handles))], nil, unsafe.Pointer(h)) {
|
||||
h.Release()
|
||||
}
|
||||
}
|
||||
}(ns, i, x.repeat, objects[ns], handles[ns])
|
||||
}
|
||||
|
||||
go func(handles []unsafe.Pointer) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
for atomic.LoadInt32(&done) == 0 {
|
||||
i := r.Intn(len(handles))
|
||||
h := (*Handle)(atomic.LoadPointer(&handles[i]))
|
||||
if h != nil && atomic.CompareAndSwapPointer(&handles[i], unsafe.Pointer(h), nil) {
|
||||
h.Release()
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}(handles[ns])
|
||||
}
|
||||
|
||||
go func() {
|
||||
handles := make([]*Handle, 100000)
|
||||
for atomic.LoadInt32(&done) == 0 {
|
||||
for i := range handles {
|
||||
handles[i] = c.Get(999999999, uint64(i), func() (int, Value) {
|
||||
return 1, 1
|
||||
})
|
||||
}
|
||||
for _, h := range handles {
|
||||
h.Release()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
atomic.StoreInt32(&done, 1)
|
||||
|
||||
for _, handles0 := range handles {
|
||||
for i := range handles0 {
|
||||
h := (*Handle)(atomic.LoadPointer(&handles0[i]))
|
||||
if h != nil && atomic.CompareAndSwapPointer(&handles0[i], unsafe.Pointer(h), nil) {
|
||||
h.Release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ns, objects0 := range objects {
|
||||
for i, o := range objects0 {
|
||||
if o != 0 {
|
||||
t.Fatalf("invalid object #%d.%d: ref=%d", ns, i, o)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheMap_NodesAndSize(t *testing.T) {
|
||||
c := NewCache(nil)
|
||||
if c.Nodes() != 0 {
|
||||
t.Errorf("invalid nodes counter: want=%d got=%d", 0, c.Nodes())
|
||||
}
|
||||
if c.Size() != 0 {
|
||||
t.Errorf("invalid size counter: want=%d got=%d", 0, c.Size())
|
||||
}
|
||||
set(c, 0, 1, 1, 1, nil)
|
||||
set(c, 0, 2, 2, 2, nil)
|
||||
set(c, 1, 1, 3, 3, nil)
|
||||
set(c, 2, 1, 4, 1, nil)
|
||||
if c.Nodes() != 4 {
|
||||
t.Errorf("invalid nodes counter: want=%d got=%d", 4, c.Nodes())
|
||||
}
|
||||
if c.Size() != 7 {
|
||||
t.Errorf("invalid size counter: want=%d got=%d", 4, c.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCache_Capacity(t *testing.T) {
|
||||
c := NewCache(NewLRU(10))
|
||||
if c.Capacity() != 10 {
|
||||
t.Errorf("invalid capacity: want=%d got=%d", 10, c.Capacity())
|
||||
}
|
||||
set(c, 0, 1, 1, 1, nil).Release()
|
||||
set(c, 0, 2, 2, 2, nil).Release()
|
||||
set(c, 1, 1, 3, 3, nil).Release()
|
||||
set(c, 2, 1, 4, 1, nil).Release()
|
||||
set(c, 2, 2, 5, 1, nil).Release()
|
||||
set(c, 2, 3, 6, 1, nil).Release()
|
||||
set(c, 2, 4, 7, 1, nil).Release()
|
||||
set(c, 2, 5, 8, 1, nil).Release()
|
||||
if c.Nodes() != 7 {
|
||||
t.Errorf("invalid nodes counter: want=%d got=%d", 7, c.Nodes())
|
||||
}
|
||||
if c.Size() != 10 {
|
||||
t.Errorf("invalid size counter: want=%d got=%d", 10, c.Size())
|
||||
}
|
||||
c.SetCapacity(9)
|
||||
if c.Capacity() != 9 {
|
||||
t.Errorf("invalid capacity: want=%d got=%d", 9, c.Capacity())
|
||||
}
|
||||
if c.Nodes() != 6 {
|
||||
t.Errorf("invalid nodes counter: want=%d got=%d", 6, c.Nodes())
|
||||
}
|
||||
if c.Size() != 8 {
|
||||
t.Errorf("invalid size counter: want=%d got=%d", 8, c.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheMap_NilValue(t *testing.T) {
|
||||
c := NewCache(NewLRU(10))
|
||||
h := c.Get(0, 0, func() (size int, value Value) {
|
||||
return 1, nil
|
||||
})
|
||||
if h != nil {
|
||||
t.Error("cache handle is non-nil")
|
||||
}
|
||||
if c.Nodes() != 0 {
|
||||
t.Errorf("invalid nodes counter: want=%d got=%d", 0, c.Nodes())
|
||||
}
|
||||
if c.Size() != 0 {
|
||||
t.Errorf("invalid size counter: want=%d got=%d", 0, c.Size())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCache_GetLatency(t *testing.T) {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
const (
|
||||
concurrentSet = 30
|
||||
concurrentGet = 3
|
||||
duration = 3 * time.Second
|
||||
delay = 3 * time.Millisecond
|
||||
maxkey = 100000
|
||||
)
|
||||
|
||||
var (
|
||||
set, getHit, getAll int32
|
||||
getMaxLatency, getDuration int64
|
||||
)
|
||||
|
||||
c := NewCache(NewLRU(5000))
|
||||
wg := &sync.WaitGroup{}
|
||||
until := time.Now().Add(duration)
|
||||
for i := 0; i < concurrentSet; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for time.Now().Before(until) {
|
||||
c.Get(0, uint64(r.Intn(maxkey)), func() (int, Value) {
|
||||
time.Sleep(delay)
|
||||
atomic.AddInt32(&set, 1)
|
||||
return 1, 1
|
||||
}).Release()
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
for i := 0; i < concurrentGet; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for {
|
||||
mark := time.Now()
|
||||
if mark.Before(until) {
|
||||
h := c.Get(0, uint64(r.Intn(maxkey)), nil)
|
||||
latency := int64(time.Now().Sub(mark))
|
||||
m := atomic.LoadInt64(&getMaxLatency)
|
||||
if latency > m {
|
||||
atomic.CompareAndSwapInt64(&getMaxLatency, m, latency)
|
||||
}
|
||||
atomic.AddInt64(&getDuration, latency)
|
||||
if h != nil {
|
||||
atomic.AddInt32(&getHit, 1)
|
||||
h.Release()
|
||||
}
|
||||
atomic.AddInt32(&getAll, 1)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
getAvglatency := time.Duration(getDuration) / time.Duration(getAll)
|
||||
t.Logf("set=%d getHit=%d getAll=%d getMaxLatency=%v getAvgLatency=%v",
|
||||
set, getHit, getAll, time.Duration(getMaxLatency), getAvglatency)
|
||||
|
||||
if getAvglatency > delay/3 {
|
||||
t.Errorf("get avg latency > %v: got=%v", delay/3, getAvglatency)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCache_HitMiss(t *testing.T) {
|
||||
cases := []struct {
|
||||
key uint64
|
||||
value string
|
||||
@@ -54,14 +308,13 @@ func TestCache_HitMiss(t *testing.T) {
|
||||
}
|
||||
|
||||
setfin := 0
|
||||
c := NewLRUCache(1000)
|
||||
ns := c.GetNamespace(0)
|
||||
c := NewCache(NewLRU(1000))
|
||||
for i, x := range cases {
|
||||
set(ns, x.key, x.value, len(x.value), func() {
|
||||
set(c, 0, x.key, x.value, len(x.value), func() {
|
||||
setfin++
|
||||
}).Release()
|
||||
for j, y := range cases {
|
||||
h := ns.Get(y.key, nil)
|
||||
h := c.Get(0, y.key, nil)
|
||||
if j <= i {
|
||||
// should hit
|
||||
if h == nil {
|
||||
@@ -85,7 +338,7 @@ func TestCache_HitMiss(t *testing.T) {
|
||||
|
||||
for i, x := range cases {
|
||||
finalizerOk := false
|
||||
ns.Delete(x.key, func(exist, pending bool) {
|
||||
c.Delete(0, x.key, func() {
|
||||
finalizerOk = true
|
||||
})
|
||||
|
||||
@@ -94,7 +347,7 @@ func TestCache_HitMiss(t *testing.T) {
|
||||
}
|
||||
|
||||
for j, y := range cases {
|
||||
h := ns.Get(y.key, nil)
|
||||
h := c.Get(0, y.key, nil)
|
||||
if j > i {
|
||||
// should hit
|
||||
if h == nil {
|
||||
@@ -122,20 +375,19 @@ func TestCache_HitMiss(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLRUCache_Eviction(t *testing.T) {
|
||||
c := NewLRUCache(12)
|
||||
ns := c.GetNamespace(0)
|
||||
o1 := set(ns, 1, 1, 1, nil)
|
||||
set(ns, 2, 2, 1, nil).Release()
|
||||
set(ns, 3, 3, 1, nil).Release()
|
||||
set(ns, 4, 4, 1, nil).Release()
|
||||
set(ns, 5, 5, 1, nil).Release()
|
||||
if h := ns.Get(2, nil); h != nil { // 1,3,4,5,2
|
||||
c := NewCache(NewLRU(12))
|
||||
o1 := set(c, 0, 1, 1, 1, nil)
|
||||
set(c, 0, 2, 2, 1, nil).Release()
|
||||
set(c, 0, 3, 3, 1, nil).Release()
|
||||
set(c, 0, 4, 4, 1, nil).Release()
|
||||
set(c, 0, 5, 5, 1, nil).Release()
|
||||
if h := c.Get(0, 2, nil); h != nil { // 1,3,4,5,2
|
||||
h.Release()
|
||||
}
|
||||
set(ns, 9, 9, 10, nil).Release() // 5,2,9
|
||||
set(c, 0, 9, 9, 10, nil).Release() // 5,2,9
|
||||
|
||||
for _, key := range []uint64{9, 2, 5, 1} {
|
||||
h := ns.Get(key, nil)
|
||||
h := c.Get(0, key, nil)
|
||||
if h == nil {
|
||||
t.Errorf("miss for key '%d'", key)
|
||||
} else {
|
||||
@@ -147,7 +399,7 @@ func TestLRUCache_Eviction(t *testing.T) {
|
||||
}
|
||||
o1.Release()
|
||||
for _, key := range []uint64{1, 2, 5} {
|
||||
h := ns.Get(key, nil)
|
||||
h := c.Get(0, key, nil)
|
||||
if h == nil {
|
||||
t.Errorf("miss for key '%d'", key)
|
||||
} else {
|
||||
@@ -158,7 +410,7 @@ func TestLRUCache_Eviction(t *testing.T) {
|
||||
}
|
||||
}
|
||||
for _, key := range []uint64{3, 4, 9} {
|
||||
h := ns.Get(key, nil)
|
||||
h := c.Get(0, key, nil)
|
||||
if h != nil {
|
||||
t.Errorf("hit for key '%d'", key)
|
||||
if x := h.Value().(int); x != int(key) {
|
||||
@@ -169,487 +421,150 @@ func TestLRUCache_Eviction(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCache_SetGet(t *testing.T) {
|
||||
c := NewLRUCache(13)
|
||||
ns := c.GetNamespace(0)
|
||||
for i := 0; i < 200; i++ {
|
||||
n := uint64(rand.Intn(99999) % 20)
|
||||
set(ns, n, n, 1, nil).Release()
|
||||
if h := ns.Get(n, nil); h != nil {
|
||||
if h.Value() == nil {
|
||||
t.Errorf("key '%d' contains nil value", n)
|
||||
func TestLRUCache_Evict(t *testing.T) {
|
||||
c := NewCache(NewLRU(6))
|
||||
set(c, 0, 1, 1, 1, nil).Release()
|
||||
set(c, 0, 2, 2, 1, nil).Release()
|
||||
set(c, 1, 1, 4, 1, nil).Release()
|
||||
set(c, 1, 2, 5, 1, nil).Release()
|
||||
set(c, 2, 1, 6, 1, nil).Release()
|
||||
set(c, 2, 2, 7, 1, nil).Release()
|
||||
|
||||
for ns := 0; ns < 3; ns++ {
|
||||
for key := 1; key < 3; key++ {
|
||||
if h := c.Get(uint64(ns), uint64(key), nil); h != nil {
|
||||
h.Release()
|
||||
} else {
|
||||
if x := h.Value().(uint64); x != n {
|
||||
t.Errorf("invalid value for key '%d' want '%d', got '%d'", n, n, x)
|
||||
}
|
||||
t.Errorf("Cache.Get on #%d.%d return nil", ns, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ok := c.Evict(0, 1); !ok {
|
||||
t.Error("first Cache.Evict on #0.1 return false")
|
||||
}
|
||||
if ok := c.Evict(0, 1); ok {
|
||||
t.Error("second Cache.Evict on #0.1 return true")
|
||||
}
|
||||
if h := c.Get(0, 1, nil); h != nil {
|
||||
t.Errorf("Cache.Get on #0.1 return non-nil: %v", h.Value())
|
||||
}
|
||||
|
||||
c.EvictNS(1)
|
||||
if h := c.Get(1, 1, nil); h != nil {
|
||||
t.Errorf("Cache.Get on #1.1 return non-nil: %v", h.Value())
|
||||
}
|
||||
if h := c.Get(1, 2, nil); h != nil {
|
||||
t.Errorf("Cache.Get on #1.2 return non-nil: %v", h.Value())
|
||||
}
|
||||
|
||||
c.EvictAll()
|
||||
for ns := 0; ns < 3; ns++ {
|
||||
for key := 1; key < 3; key++ {
|
||||
if h := c.Get(uint64(ns), uint64(key), nil); h != nil {
|
||||
t.Errorf("Cache.Get on #%d.%d return non-nil: %v", ns, key, h.Value())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCache_Delete(t *testing.T) {
|
||||
delFuncCalled := 0
|
||||
delFunc := func() {
|
||||
delFuncCalled++
|
||||
}
|
||||
|
||||
c := NewCache(NewLRU(2))
|
||||
set(c, 0, 1, 1, 1, nil).Release()
|
||||
set(c, 0, 2, 2, 1, nil).Release()
|
||||
|
||||
if ok := c.Delete(0, 1, delFunc); !ok {
|
||||
t.Error("Cache.Delete on #1 return false")
|
||||
}
|
||||
if h := c.Get(0, 1, nil); h != nil {
|
||||
t.Errorf("Cache.Get on #1 return non-nil: %v", h.Value())
|
||||
}
|
||||
if ok := c.Delete(0, 1, delFunc); ok {
|
||||
t.Error("Cache.Delete on #1 return true")
|
||||
}
|
||||
|
||||
h2 := c.Get(0, 2, nil)
|
||||
if h2 == nil {
|
||||
t.Error("Cache.Get on #2 return nil")
|
||||
}
|
||||
if ok := c.Delete(0, 2, delFunc); !ok {
|
||||
t.Error("(1) Cache.Delete on #2 return false")
|
||||
}
|
||||
if ok := c.Delete(0, 2, delFunc); !ok {
|
||||
t.Error("(2) Cache.Delete on #2 return false")
|
||||
}
|
||||
|
||||
set(c, 0, 3, 3, 1, nil).Release()
|
||||
set(c, 0, 4, 4, 1, nil).Release()
|
||||
c.Get(0, 2, nil).Release()
|
||||
|
||||
for key := 2; key <= 4; key++ {
|
||||
if h := c.Get(0, uint64(key), nil); h != nil {
|
||||
h.Release()
|
||||
} else {
|
||||
t.Errorf("key '%d' doesn't exist", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCache_Purge(t *testing.T) {
|
||||
c := NewLRUCache(3)
|
||||
ns1 := c.GetNamespace(0)
|
||||
o1 := set(ns1, 1, 1, 1, nil)
|
||||
o2 := set(ns1, 2, 2, 1, nil)
|
||||
ns1.Purge(nil)
|
||||
set(ns1, 3, 3, 1, nil).Release()
|
||||
for _, key := range []uint64{1, 2, 3} {
|
||||
h := ns1.Get(key, nil)
|
||||
if h == nil {
|
||||
t.Errorf("miss for key '%d'", key)
|
||||
} else {
|
||||
if x := h.Value().(int); x != int(key) {
|
||||
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
|
||||
}
|
||||
h.Release()
|
||||
}
|
||||
}
|
||||
o1.Release()
|
||||
o2.Release()
|
||||
for _, key := range []uint64{1, 2} {
|
||||
h := ns1.Get(key, nil)
|
||||
if h != nil {
|
||||
t.Errorf("hit for key '%d'", key)
|
||||
if x := h.Value().(int); x != int(key) {
|
||||
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
|
||||
}
|
||||
h.Release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type testingCacheObjectCounter struct {
|
||||
created uint
|
||||
released uint
|
||||
}
|
||||
|
||||
func (c *testingCacheObjectCounter) createOne() {
|
||||
c.created++
|
||||
}
|
||||
|
||||
func (c *testingCacheObjectCounter) releaseOne() {
|
||||
c.released++
|
||||
}
|
||||
|
||||
type testingCacheObject struct {
|
||||
t *testing.T
|
||||
cnt *testingCacheObjectCounter
|
||||
|
||||
ns, key uint64
|
||||
|
||||
releaseCalled bool
|
||||
}
|
||||
|
||||
func (x *testingCacheObject) Release() {
|
||||
if !x.releaseCalled {
|
||||
x.releaseCalled = true
|
||||
x.cnt.releaseOne()
|
||||
} else {
|
||||
x.t.Errorf("duplicate setfin NS#%d KEY#%d", x.ns, x.key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCache_ConcurrentSetGet(t *testing.T) {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
seed := time.Now().UnixNano()
|
||||
t.Logf("seed=%d", seed)
|
||||
|
||||
const (
|
||||
N = 2000000
|
||||
M = 4000
|
||||
C = 3
|
||||
)
|
||||
|
||||
var set, get uint32
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
c := NewLRUCache(M / 4)
|
||||
for ni := uint64(0); ni < C; ni++ {
|
||||
r0 := rand.New(rand.NewSource(seed + int64(ni)))
|
||||
r1 := rand.New(rand.NewSource(seed + int64(ni) + 1))
|
||||
ns := c.GetNamespace(ni)
|
||||
|
||||
wg.Add(2)
|
||||
go func(ns Namespace, r *rand.Rand) {
|
||||
for i := 0; i < N; i++ {
|
||||
x := uint64(r.Int63n(M))
|
||||
o := ns.Get(x, func() (int, interface{}) {
|
||||
atomic.AddUint32(&set, 1)
|
||||
return 1, x
|
||||
})
|
||||
if v := o.Value().(uint64); v != x {
|
||||
t.Errorf("#%d invalid value, got=%d", x, v)
|
||||
}
|
||||
o.Release()
|
||||
}
|
||||
wg.Done()
|
||||
}(ns, r0)
|
||||
go func(ns Namespace, r *rand.Rand) {
|
||||
for i := 0; i < N; i++ {
|
||||
x := uint64(r.Int63n(M))
|
||||
o := ns.Get(x, nil)
|
||||
if o != nil {
|
||||
atomic.AddUint32(&get, 1)
|
||||
if v := o.Value().(uint64); v != x {
|
||||
t.Errorf("#%d invalid value, got=%d", x, v)
|
||||
}
|
||||
o.Release()
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(ns, r1)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
t.Logf("set=%d get=%d", set, get)
|
||||
}
|
||||
|
||||
func TestLRUCache_Finalizer(t *testing.T) {
|
||||
const (
|
||||
capacity = 100
|
||||
goroutines = 100
|
||||
iterations = 10000
|
||||
keymax = 8000
|
||||
)
|
||||
|
||||
cnt := &testingCacheObjectCounter{}
|
||||
|
||||
c := NewLRUCache(capacity)
|
||||
|
||||
type instance struct {
|
||||
seed int64
|
||||
rnd *rand.Rand
|
||||
nsid uint64
|
||||
ns Namespace
|
||||
effective int
|
||||
handles []Handle
|
||||
handlesMap map[uint64]int
|
||||
|
||||
delete bool
|
||||
purge bool
|
||||
zap bool
|
||||
wantDel int
|
||||
delfinCalled int
|
||||
delfinCalledAll int
|
||||
delfinCalledEff int
|
||||
purgefinCalled int
|
||||
}
|
||||
|
||||
instanceGet := func(p *instance, key uint64) {
|
||||
h := p.ns.Get(key, func() (charge int, value interface{}) {
|
||||
to := &testingCacheObject{
|
||||
t: t, cnt: cnt,
|
||||
ns: p.nsid,
|
||||
key: key,
|
||||
}
|
||||
p.effective++
|
||||
cnt.createOne()
|
||||
return 1, releaserFunc{func() {
|
||||
to.Release()
|
||||
p.effective--
|
||||
}, to}
|
||||
})
|
||||
p.handles = append(p.handles, h)
|
||||
p.handlesMap[key] = p.handlesMap[key] + 1
|
||||
}
|
||||
instanceRelease := func(p *instance, i int) {
|
||||
h := p.handles[i]
|
||||
key := h.Value().(releaserFunc).value.(*testingCacheObject).key
|
||||
if n := p.handlesMap[key]; n == 0 {
|
||||
t.Fatal("key ref == 0")
|
||||
} else if n > 1 {
|
||||
p.handlesMap[key] = n - 1
|
||||
} else {
|
||||
delete(p.handlesMap, key)
|
||||
}
|
||||
h.Release()
|
||||
p.handles = append(p.handles[:i], p.handles[i+1:]...)
|
||||
p.handles[len(p.handles) : len(p.handles)+1][0] = nil
|
||||
}
|
||||
|
||||
seed := time.Now().UnixNano()
|
||||
t.Logf("seed=%d", seed)
|
||||
|
||||
instances := make([]*instance, goroutines)
|
||||
for i := range instances {
|
||||
p := &instance{}
|
||||
p.handlesMap = make(map[uint64]int)
|
||||
p.seed = seed + int64(i)
|
||||
p.rnd = rand.New(rand.NewSource(p.seed))
|
||||
p.nsid = uint64(i)
|
||||
p.ns = c.GetNamespace(p.nsid)
|
||||
p.delete = i%6 == 0
|
||||
p.purge = i%8 == 0
|
||||
p.zap = i%12 == 0 || i%3 == 0
|
||||
instances[i] = p
|
||||
}
|
||||
|
||||
runr := rand.New(rand.NewSource(seed - 1))
|
||||
run := func(rnd *rand.Rand, x []*instance, init func(p *instance) bool, fn func(p *instance, i int) bool) {
|
||||
var (
|
||||
rx []*instance
|
||||
rn []int
|
||||
)
|
||||
if init == nil {
|
||||
rx = append([]*instance{}, x...)
|
||||
rn = make([]int, len(x))
|
||||
} else {
|
||||
for _, p := range x {
|
||||
if init(p) {
|
||||
rx = append(rx, p)
|
||||
rn = append(rn, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
for len(rx) > 0 {
|
||||
i := rand.Intn(len(rx))
|
||||
if fn(rx[i], rn[i]) {
|
||||
rn[i]++
|
||||
} else {
|
||||
rx = append(rx[:i], rx[i+1:]...)
|
||||
rn = append(rn[:i], rn[i+1:]...)
|
||||
}
|
||||
t.Errorf("Cache.Get on #%d return nil", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Get and release.
|
||||
run(runr, instances, nil, func(p *instance, i int) bool {
|
||||
if i < iterations {
|
||||
if len(p.handles) == 0 || p.rnd.Int()%2 == 0 {
|
||||
instanceGet(p, uint64(p.rnd.Intn(keymax)))
|
||||
} else {
|
||||
instanceRelease(p, p.rnd.Intn(len(p.handles)))
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
h2.Release()
|
||||
if h := c.Get(0, 2, nil); h != nil {
|
||||
t.Errorf("Cache.Get on #2 return non-nil: %v", h.Value())
|
||||
}
|
||||
|
||||
if delFuncCalled != 4 {
|
||||
t.Errorf("delFunc isn't called 4 times: got=%d", delFuncCalled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLRUCache_Close(t *testing.T) {
|
||||
relFuncCalled := 0
|
||||
relFunc := func() {
|
||||
relFuncCalled++
|
||||
}
|
||||
delFuncCalled := 0
|
||||
delFunc := func() {
|
||||
delFuncCalled++
|
||||
}
|
||||
|
||||
c := NewCache(NewLRU(2))
|
||||
set(c, 0, 1, 1, 1, relFunc).Release()
|
||||
set(c, 0, 2, 2, 1, relFunc).Release()
|
||||
|
||||
h3 := set(c, 0, 3, 3, 1, relFunc)
|
||||
if h3 == nil {
|
||||
t.Error("Cache.Get on #3 return nil")
|
||||
}
|
||||
if ok := c.Delete(0, 3, delFunc); !ok {
|
||||
t.Error("Cache.Delete on #3 return false")
|
||||
}
|
||||
|
||||
c.Close()
|
||||
|
||||
if relFuncCalled != 3 {
|
||||
t.Errorf("relFunc isn't called 3 times: got=%d", relFuncCalled)
|
||||
}
|
||||
if delFuncCalled != 1 {
|
||||
t.Errorf("delFunc isn't called 1 times: got=%d", delFuncCalled)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUCache(b *testing.B) {
|
||||
c := NewCache(NewLRU(10000))
|
||||
|
||||
b.SetParallelism(10)
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
for pb.Next() {
|
||||
key := uint64(r.Intn(1000000))
|
||||
c.Get(0, key, func() (int, Value) {
|
||||
return 1, key
|
||||
}).Release()
|
||||
}
|
||||
})
|
||||
|
||||
if used, cap := c.Used(), c.Capacity(); used > cap {
|
||||
t.Errorf("Used > capacity, used=%d cap=%d", used, cap)
|
||||
}
|
||||
|
||||
// Check effective objects.
|
||||
for i, p := range instances {
|
||||
if int(p.effective) < len(p.handlesMap) {
|
||||
t.Errorf("#%d effective objects < acquired handle, eo=%d ah=%d", i, p.effective, len(p.handlesMap))
|
||||
}
|
||||
}
|
||||
|
||||
if want := int(cnt.created - cnt.released); c.Size() != want {
|
||||
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
|
||||
}
|
||||
|
||||
// First delete.
|
||||
run(runr, instances, func(p *instance) bool {
|
||||
p.wantDel = p.effective
|
||||
return p.delete
|
||||
}, func(p *instance, i int) bool {
|
||||
key := uint64(i)
|
||||
if key < keymax {
|
||||
_, wantExist := p.handlesMap[key]
|
||||
gotExist := p.ns.Delete(key, func(exist, pending bool) {
|
||||
p.delfinCalledAll++
|
||||
if exist {
|
||||
p.delfinCalledEff++
|
||||
}
|
||||
})
|
||||
if !gotExist && wantExist {
|
||||
t.Errorf("delete on NS#%d KEY#%d not found", p.nsid, key)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// Second delete.
|
||||
run(runr, instances, func(p *instance) bool {
|
||||
p.delfinCalled = 0
|
||||
return p.delete
|
||||
}, func(p *instance, i int) bool {
|
||||
key := uint64(i)
|
||||
if key < keymax {
|
||||
gotExist := p.ns.Delete(key, func(exist, pending bool) {
|
||||
if exist && !pending {
|
||||
t.Errorf("delete fin on NS#%d KEY#%d exist and not pending for deletion", p.nsid, key)
|
||||
}
|
||||
p.delfinCalled++
|
||||
})
|
||||
if gotExist {
|
||||
t.Errorf("delete on NS#%d KEY#%d found", p.nsid, key)
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
if p.delfinCalled != keymax {
|
||||
t.Errorf("(2) NS#%d not all delete fin called, diff=%d", p.nsid, keymax-p.delfinCalled)
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// Purge.
|
||||
run(runr, instances, func(p *instance) bool {
|
||||
return p.purge
|
||||
}, func(p *instance, i int) bool {
|
||||
p.ns.Purge(func(ns, key uint64) {
|
||||
p.purgefinCalled++
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
if want := int(cnt.created - cnt.released); c.Size() != want {
|
||||
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
|
||||
}
|
||||
|
||||
// Release.
|
||||
run(runr, instances, func(p *instance) bool {
|
||||
return !p.zap
|
||||
}, func(p *instance, i int) bool {
|
||||
if len(p.handles) > 0 {
|
||||
instanceRelease(p, len(p.handles)-1)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
if want := int(cnt.created - cnt.released); c.Size() != want {
|
||||
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
|
||||
}
|
||||
|
||||
// Zap.
|
||||
run(runr, instances, func(p *instance) bool {
|
||||
return p.zap
|
||||
}, func(p *instance, i int) bool {
|
||||
p.ns.Zap()
|
||||
p.handles = nil
|
||||
p.handlesMap = nil
|
||||
return false
|
||||
})
|
||||
|
||||
if want := int(cnt.created - cnt.released); c.Size() != want {
|
||||
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
|
||||
}
|
||||
|
||||
if notrel, used := int(cnt.created-cnt.released), c.Used(); notrel != used {
|
||||
t.Errorf("Invalid used value, want=%d got=%d", notrel, used)
|
||||
}
|
||||
|
||||
c.Purge(nil)
|
||||
|
||||
for _, p := range instances {
|
||||
if p.delete {
|
||||
if p.delfinCalledAll != keymax {
|
||||
t.Errorf("#%d not all delete fin called, purge=%v zap=%v diff=%d", p.nsid, p.purge, p.zap, keymax-p.delfinCalledAll)
|
||||
}
|
||||
if p.delfinCalledEff != p.wantDel {
|
||||
t.Errorf("#%d not all effective delete fin called, diff=%d", p.nsid, p.wantDel-p.delfinCalledEff)
|
||||
}
|
||||
if p.purge && p.purgefinCalled > 0 {
|
||||
t.Errorf("#%d some purge fin called, delete=%v zap=%v n=%d", p.nsid, p.delete, p.zap, p.purgefinCalled)
|
||||
}
|
||||
} else {
|
||||
if p.purge {
|
||||
if p.purgefinCalled != p.wantDel {
|
||||
t.Errorf("#%d not all purge fin called, delete=%v zap=%v diff=%d", p.nsid, p.delete, p.zap, p.wantDel-p.purgefinCalled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cnt.created != cnt.released {
|
||||
t.Errorf("Some cache object weren't released, created=%d released=%d", cnt.created, cnt.released)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUCache_Set(b *testing.B) {
|
||||
c := NewLRUCache(0)
|
||||
ns := c.GetNamespace(0)
|
||||
b.ResetTimer()
|
||||
for i := uint64(0); i < uint64(b.N); i++ {
|
||||
set(ns, i, "", 1, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUCache_Get(b *testing.B) {
|
||||
c := NewLRUCache(0)
|
||||
ns := c.GetNamespace(0)
|
||||
b.ResetTimer()
|
||||
for i := uint64(0); i < uint64(b.N); i++ {
|
||||
set(ns, i, "", 1, nil)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := uint64(0); i < uint64(b.N); i++ {
|
||||
ns.Get(i, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUCache_Get2(b *testing.B) {
|
||||
c := NewLRUCache(0)
|
||||
ns := c.GetNamespace(0)
|
||||
b.ResetTimer()
|
||||
for i := uint64(0); i < uint64(b.N); i++ {
|
||||
set(ns, i, "", 1, nil)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := uint64(0); i < uint64(b.N); i++ {
|
||||
ns.Get(i, func() (charge int, value interface{}) {
|
||||
return 0, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUCache_Release(b *testing.B) {
|
||||
c := NewLRUCache(0)
|
||||
ns := c.GetNamespace(0)
|
||||
handles := make([]Handle, b.N)
|
||||
for i := uint64(0); i < uint64(b.N); i++ {
|
||||
handles[i] = set(ns, i, "", 1, nil)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for _, h := range handles {
|
||||
h.Release()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUCache_SetRelease(b *testing.B) {
|
||||
capacity := b.N / 100
|
||||
if capacity <= 0 {
|
||||
capacity = 10
|
||||
}
|
||||
c := NewLRUCache(capacity)
|
||||
ns := c.GetNamespace(0)
|
||||
b.ResetTimer()
|
||||
for i := uint64(0); i < uint64(b.N); i++ {
|
||||
set(ns, i, "", 1, nil).Release()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLRUCache_SetReleaseTwice(b *testing.B) {
|
||||
capacity := b.N / 100
|
||||
if capacity <= 0 {
|
||||
capacity = 10
|
||||
}
|
||||
c := NewLRUCache(capacity)
|
||||
ns := c.GetNamespace(0)
|
||||
b.ResetTimer()
|
||||
|
||||
na := b.N / 2
|
||||
nb := b.N - na
|
||||
|
||||
for i := uint64(0); i < uint64(na); i++ {
|
||||
set(ns, i, "", 1, nil).Release()
|
||||
}
|
||||
|
||||
for i := uint64(0); i < uint64(nb); i++ {
|
||||
set(ns, i, "", 1, nil).Release()
|
||||
}
|
||||
}
|
||||
|
||||
195
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/lru.go
generated
vendored
Normal file
195
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/lru.go
generated
vendored
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type lruNode struct {
|
||||
n *Node
|
||||
h *Handle
|
||||
ban bool
|
||||
|
||||
next, prev *lruNode
|
||||
}
|
||||
|
||||
func (n *lruNode) insert(at *lruNode) {
|
||||
x := at.next
|
||||
at.next = n
|
||||
n.prev = at
|
||||
n.next = x
|
||||
x.prev = n
|
||||
}
|
||||
|
||||
func (n *lruNode) remove() {
|
||||
if n.prev != nil {
|
||||
n.prev.next = n.next
|
||||
n.next.prev = n.prev
|
||||
n.prev = nil
|
||||
n.next = nil
|
||||
} else {
|
||||
panic("BUG: removing removed node")
|
||||
}
|
||||
}
|
||||
|
||||
type lru struct {
|
||||
mu sync.Mutex
|
||||
capacity int
|
||||
used int
|
||||
recent lruNode
|
||||
}
|
||||
|
||||
func (r *lru) reset() {
|
||||
r.recent.next = &r.recent
|
||||
r.recent.prev = &r.recent
|
||||
r.used = 0
|
||||
}
|
||||
|
||||
func (r *lru) Capacity() int {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.capacity
|
||||
}
|
||||
|
||||
func (r *lru) SetCapacity(capacity int) {
|
||||
var evicted []*lruNode
|
||||
|
||||
r.mu.Lock()
|
||||
r.capacity = capacity
|
||||
for r.used > r.capacity {
|
||||
rn := r.recent.prev
|
||||
if rn == nil {
|
||||
panic("BUG: invalid LRU used or capacity counter")
|
||||
}
|
||||
rn.remove()
|
||||
rn.n.CacheData = nil
|
||||
r.used -= rn.n.Size()
|
||||
evicted = append(evicted, rn)
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
for _, rn := range evicted {
|
||||
rn.h.Release()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *lru) Promote(n *Node) {
|
||||
var evicted []*lruNode
|
||||
|
||||
r.mu.Lock()
|
||||
if n.CacheData == nil {
|
||||
if n.Size() <= r.capacity {
|
||||
rn := &lruNode{n: n, h: n.GetHandle()}
|
||||
rn.insert(&r.recent)
|
||||
n.CacheData = unsafe.Pointer(rn)
|
||||
r.used += n.Size()
|
||||
|
||||
for r.used > r.capacity {
|
||||
rn := r.recent.prev
|
||||
if rn == nil {
|
||||
panic("BUG: invalid LRU used or capacity counter")
|
||||
}
|
||||
rn.remove()
|
||||
rn.n.CacheData = nil
|
||||
r.used -= rn.n.Size()
|
||||
evicted = append(evicted, rn)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rn := (*lruNode)(n.CacheData)
|
||||
if !rn.ban {
|
||||
rn.remove()
|
||||
rn.insert(&r.recent)
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
for _, rn := range evicted {
|
||||
rn.h.Release()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *lru) Ban(n *Node) {
|
||||
r.mu.Lock()
|
||||
if n.CacheData == nil {
|
||||
n.CacheData = unsafe.Pointer(&lruNode{n: n, ban: true})
|
||||
} else {
|
||||
rn := (*lruNode)(n.CacheData)
|
||||
if !rn.ban {
|
||||
rn.remove()
|
||||
rn.ban = true
|
||||
r.used -= rn.n.Size()
|
||||
r.mu.Unlock()
|
||||
|
||||
rn.h.Release()
|
||||
rn.h = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func (r *lru) Evict(n *Node) {
|
||||
r.mu.Lock()
|
||||
rn := (*lruNode)(n.CacheData)
|
||||
if rn == nil || rn.ban {
|
||||
r.mu.Unlock()
|
||||
return
|
||||
}
|
||||
n.CacheData = nil
|
||||
r.mu.Unlock()
|
||||
|
||||
rn.h.Release()
|
||||
}
|
||||
|
||||
func (r *lru) EvictNS(ns uint64) {
|
||||
var evicted []*lruNode
|
||||
|
||||
r.mu.Lock()
|
||||
for e := r.recent.prev; e != &r.recent; {
|
||||
rn := e
|
||||
e = e.prev
|
||||
if rn.n.NS() == ns {
|
||||
rn.remove()
|
||||
rn.n.CacheData = nil
|
||||
r.used -= rn.n.Size()
|
||||
evicted = append(evicted, rn)
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
for _, rn := range evicted {
|
||||
rn.h.Release()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *lru) EvictAll() {
|
||||
r.mu.Lock()
|
||||
back := r.recent.prev
|
||||
for rn := back; rn != &r.recent; rn = rn.prev {
|
||||
rn.n.CacheData = nil
|
||||
}
|
||||
r.reset()
|
||||
r.mu.Unlock()
|
||||
|
||||
for rn := back; rn != &r.recent; rn = rn.prev {
|
||||
rn.h.Release()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *lru) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewLRU create a new LRU-cache.
|
||||
func NewLRU(capacity int) Cacher {
|
||||
r := &lru{capacity: capacity}
|
||||
r.reset()
|
||||
return r
|
||||
}
|
||||
622
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/lru_cache.go
generated
vendored
622
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/lru_cache.go
generated
vendored
@@ -1,622 +0,0 @@
|
||||
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
// The LLRB implementation were taken from https://github.com/petar/GoLLRB.
|
||||
// Which contains the following header:
|
||||
//
|
||||
// Copyright 2010 Petar Maymounkov. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// lruCache represent a LRU cache state.
|
||||
type lruCache struct {
|
||||
mu sync.Mutex
|
||||
recent lruNode
|
||||
table map[uint64]*lruNs
|
||||
capacity int
|
||||
used, size, alive int
|
||||
}
|
||||
|
||||
// NewLRUCache creates a new initialized LRU cache with the given capacity.
|
||||
func NewLRUCache(capacity int) Cache {
|
||||
c := &lruCache{
|
||||
table: make(map[uint64]*lruNs),
|
||||
capacity: capacity,
|
||||
}
|
||||
c.recent.rNext = &c.recent
|
||||
c.recent.rPrev = &c.recent
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *lruCache) Capacity() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.capacity
|
||||
}
|
||||
|
||||
func (c *lruCache) Used() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.used
|
||||
}
|
||||
|
||||
func (c *lruCache) Size() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.size
|
||||
}
|
||||
|
||||
func (c *lruCache) NumObjects() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.alive
|
||||
}
|
||||
|
||||
// SetCapacity set cache capacity.
|
||||
func (c *lruCache) SetCapacity(capacity int) {
|
||||
c.mu.Lock()
|
||||
c.capacity = capacity
|
||||
c.evict()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetNamespace return namespace object for given id.
|
||||
func (c *lruCache) GetNamespace(id uint64) Namespace {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if ns, ok := c.table[id]; ok {
|
||||
return ns
|
||||
}
|
||||
|
||||
ns := &lruNs{lru: c, id: id}
|
||||
c.table[id] = ns
|
||||
return ns
|
||||
}
|
||||
|
||||
func (c *lruCache) ZapNamespace(id uint64) {
|
||||
c.mu.Lock()
|
||||
if ns, exist := c.table[id]; exist {
|
||||
ns.zapNB()
|
||||
delete(c.table, id)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *lruCache) PurgeNamespace(id uint64, fin PurgeFin) {
|
||||
c.mu.Lock()
|
||||
if ns, exist := c.table[id]; exist {
|
||||
ns.purgeNB(fin)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// Purge purge entire cache.
|
||||
func (c *lruCache) Purge(fin PurgeFin) {
|
||||
c.mu.Lock()
|
||||
for _, ns := range c.table {
|
||||
ns.purgeNB(fin)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *lruCache) Zap() {
|
||||
c.mu.Lock()
|
||||
for _, ns := range c.table {
|
||||
ns.zapNB()
|
||||
}
|
||||
c.table = make(map[uint64]*lruNs)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *lruCache) evict() {
|
||||
top := &c.recent
|
||||
for n := c.recent.rPrev; c.used > c.capacity && n != top; {
|
||||
if n.state != nodeEffective {
|
||||
panic("evicting non effective node")
|
||||
}
|
||||
n.state = nodeEvicted
|
||||
n.rRemove()
|
||||
n.derefNB()
|
||||
c.used -= n.charge
|
||||
n = c.recent.rPrev
|
||||
}
|
||||
}
|
||||
|
||||
type lruNs struct {
|
||||
lru *lruCache
|
||||
id uint64
|
||||
rbRoot *lruNode
|
||||
state nsState
|
||||
}
|
||||
|
||||
func (ns *lruNs) rbGetOrCreateNode(h *lruNode, key uint64) (hn, n *lruNode) {
|
||||
if h == nil {
|
||||
n = &lruNode{ns: ns, key: key}
|
||||
return n, n
|
||||
}
|
||||
|
||||
if key < h.key {
|
||||
hn, n = ns.rbGetOrCreateNode(h.rbLeft, key)
|
||||
if hn != nil {
|
||||
h.rbLeft = hn
|
||||
} else {
|
||||
return nil, n
|
||||
}
|
||||
} else if key > h.key {
|
||||
hn, n = ns.rbGetOrCreateNode(h.rbRight, key)
|
||||
if hn != nil {
|
||||
h.rbRight = hn
|
||||
} else {
|
||||
return nil, n
|
||||
}
|
||||
} else {
|
||||
return nil, h
|
||||
}
|
||||
|
||||
if rbIsRed(h.rbRight) && !rbIsRed(h.rbLeft) {
|
||||
h = rbRotLeft(h)
|
||||
}
|
||||
if rbIsRed(h.rbLeft) && rbIsRed(h.rbLeft.rbLeft) {
|
||||
h = rbRotRight(h)
|
||||
}
|
||||
if rbIsRed(h.rbLeft) && rbIsRed(h.rbRight) {
|
||||
rbFlip(h)
|
||||
}
|
||||
return h, n
|
||||
}
|
||||
|
||||
func (ns *lruNs) getOrCreateNode(key uint64) *lruNode {
|
||||
hn, n := ns.rbGetOrCreateNode(ns.rbRoot, key)
|
||||
if hn != nil {
|
||||
ns.rbRoot = hn
|
||||
ns.rbRoot.rbBlack = true
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (ns *lruNs) rbGetNode(key uint64) *lruNode {
|
||||
h := ns.rbRoot
|
||||
for h != nil {
|
||||
switch {
|
||||
case key < h.key:
|
||||
h = h.rbLeft
|
||||
case key > h.key:
|
||||
h = h.rbRight
|
||||
default:
|
||||
return h
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *lruNs) getNode(key uint64) *lruNode {
|
||||
return ns.rbGetNode(key)
|
||||
}
|
||||
|
||||
func (ns *lruNs) rbDeleteNode(h *lruNode, key uint64) *lruNode {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if key < h.key {
|
||||
if h.rbLeft == nil { // key not present. Nothing to delete
|
||||
return h
|
||||
}
|
||||
if !rbIsRed(h.rbLeft) && !rbIsRed(h.rbLeft.rbLeft) {
|
||||
h = rbMoveLeft(h)
|
||||
}
|
||||
h.rbLeft = ns.rbDeleteNode(h.rbLeft, key)
|
||||
} else {
|
||||
if rbIsRed(h.rbLeft) {
|
||||
h = rbRotRight(h)
|
||||
}
|
||||
// If @key equals @h.key and no right children at @h
|
||||
if h.key == key && h.rbRight == nil {
|
||||
return nil
|
||||
}
|
||||
if h.rbRight != nil && !rbIsRed(h.rbRight) && !rbIsRed(h.rbRight.rbLeft) {
|
||||
h = rbMoveRight(h)
|
||||
}
|
||||
// If @key equals @h.key, and (from above) 'h.Right != nil'
|
||||
if h.key == key {
|
||||
var x *lruNode
|
||||
h.rbRight, x = rbDeleteMin(h.rbRight)
|
||||
if x == nil {
|
||||
panic("logic")
|
||||
}
|
||||
x.rbLeft, h.rbLeft = h.rbLeft, nil
|
||||
x.rbRight, h.rbRight = h.rbRight, nil
|
||||
x.rbBlack = h.rbBlack
|
||||
h = x
|
||||
} else { // Else, @key is bigger than @h.key
|
||||
h.rbRight = ns.rbDeleteNode(h.rbRight, key)
|
||||
}
|
||||
}
|
||||
|
||||
return rbFixup(h)
|
||||
}
|
||||
|
||||
func (ns *lruNs) deleteNode(key uint64) {
|
||||
ns.rbRoot = ns.rbDeleteNode(ns.rbRoot, key)
|
||||
if ns.rbRoot != nil {
|
||||
ns.rbRoot.rbBlack = true
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *lruNs) rbIterateNodes(h *lruNode, pivot uint64, iter func(n *lruNode) bool) bool {
|
||||
if h == nil {
|
||||
return true
|
||||
}
|
||||
if h.key >= pivot {
|
||||
if !ns.rbIterateNodes(h.rbLeft, pivot, iter) {
|
||||
return false
|
||||
}
|
||||
if !iter(h) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return ns.rbIterateNodes(h.rbRight, pivot, iter)
|
||||
}
|
||||
|
||||
func (ns *lruNs) iterateNodes(iter func(n *lruNode) bool) {
|
||||
ns.rbIterateNodes(ns.rbRoot, 0, iter)
|
||||
}
|
||||
|
||||
func (ns *lruNs) Get(key uint64, setf SetFunc) Handle {
|
||||
ns.lru.mu.Lock()
|
||||
defer ns.lru.mu.Unlock()
|
||||
|
||||
if ns.state != nsEffective {
|
||||
return nil
|
||||
}
|
||||
|
||||
var n *lruNode
|
||||
if setf == nil {
|
||||
n = ns.getNode(key)
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
n = ns.getOrCreateNode(key)
|
||||
}
|
||||
switch n.state {
|
||||
case nodeZero:
|
||||
charge, value := setf()
|
||||
if value == nil {
|
||||
ns.deleteNode(key)
|
||||
return nil
|
||||
}
|
||||
if charge < 0 {
|
||||
charge = 0
|
||||
}
|
||||
|
||||
n.value = value
|
||||
n.charge = charge
|
||||
n.state = nodeEvicted
|
||||
|
||||
ns.lru.size += charge
|
||||
ns.lru.alive++
|
||||
|
||||
fallthrough
|
||||
case nodeEvicted:
|
||||
if n.charge == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Insert to recent list.
|
||||
n.state = nodeEffective
|
||||
n.ref++
|
||||
ns.lru.used += n.charge
|
||||
ns.lru.evict()
|
||||
|
||||
fallthrough
|
||||
case nodeEffective:
|
||||
// Bump to front.
|
||||
n.rRemove()
|
||||
n.rInsert(&ns.lru.recent)
|
||||
case nodeDeleted:
|
||||
// Do nothing.
|
||||
default:
|
||||
panic("invalid state")
|
||||
}
|
||||
n.ref++
|
||||
|
||||
return &lruHandle{node: n}
|
||||
}
|
||||
|
||||
func (ns *lruNs) Delete(key uint64, fin DelFin) bool {
|
||||
ns.lru.mu.Lock()
|
||||
defer ns.lru.mu.Unlock()
|
||||
|
||||
if ns.state != nsEffective {
|
||||
if fin != nil {
|
||||
fin(false, false)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
n := ns.getNode(key)
|
||||
if n == nil {
|
||||
if fin != nil {
|
||||
fin(false, false)
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
switch n.state {
|
||||
case nodeEffective:
|
||||
ns.lru.used -= n.charge
|
||||
n.state = nodeDeleted
|
||||
n.delfin = fin
|
||||
n.rRemove()
|
||||
n.derefNB()
|
||||
case nodeEvicted:
|
||||
n.state = nodeDeleted
|
||||
n.delfin = fin
|
||||
case nodeDeleted:
|
||||
if fin != nil {
|
||||
fin(true, true)
|
||||
}
|
||||
return false
|
||||
default:
|
||||
panic("invalid state")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ns *lruNs) purgeNB(fin PurgeFin) {
|
||||
if ns.state == nsEffective {
|
||||
var nodes []*lruNode
|
||||
ns.iterateNodes(func(n *lruNode) bool {
|
||||
nodes = append(nodes, n)
|
||||
return true
|
||||
})
|
||||
for _, n := range nodes {
|
||||
switch n.state {
|
||||
case nodeEffective:
|
||||
ns.lru.used -= n.charge
|
||||
n.state = nodeDeleted
|
||||
n.purgefin = fin
|
||||
n.rRemove()
|
||||
n.derefNB()
|
||||
case nodeEvicted:
|
||||
n.state = nodeDeleted
|
||||
n.purgefin = fin
|
||||
case nodeDeleted:
|
||||
default:
|
||||
panic("invalid state")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *lruNs) Purge(fin PurgeFin) {
|
||||
ns.lru.mu.Lock()
|
||||
ns.purgeNB(fin)
|
||||
ns.lru.mu.Unlock()
|
||||
}
|
||||
|
||||
func (ns *lruNs) zapNB() {
|
||||
if ns.state == nsEffective {
|
||||
ns.state = nsZapped
|
||||
|
||||
ns.iterateNodes(func(n *lruNode) bool {
|
||||
if n.state == nodeEffective {
|
||||
ns.lru.used -= n.charge
|
||||
n.rRemove()
|
||||
}
|
||||
ns.lru.size -= n.charge
|
||||
n.state = nodeDeleted
|
||||
n.fin()
|
||||
|
||||
return true
|
||||
})
|
||||
ns.rbRoot = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *lruNs) Zap() {
|
||||
ns.lru.mu.Lock()
|
||||
ns.zapNB()
|
||||
delete(ns.lru.table, ns.id)
|
||||
ns.lru.mu.Unlock()
|
||||
}
|
||||
|
||||
type lruNode struct {
|
||||
ns *lruNs
|
||||
|
||||
rNext, rPrev *lruNode
|
||||
rbLeft, rbRight *lruNode
|
||||
rbBlack bool
|
||||
|
||||
key uint64
|
||||
value interface{}
|
||||
charge int
|
||||
ref int
|
||||
state nodeState
|
||||
delfin DelFin
|
||||
purgefin PurgeFin
|
||||
}
|
||||
|
||||
func (n *lruNode) rInsert(at *lruNode) {
|
||||
x := at.rNext
|
||||
at.rNext = n
|
||||
n.rPrev = at
|
||||
n.rNext = x
|
||||
x.rPrev = n
|
||||
}
|
||||
|
||||
func (n *lruNode) rRemove() bool {
|
||||
if n.rPrev == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
n.rPrev.rNext = n.rNext
|
||||
n.rNext.rPrev = n.rPrev
|
||||
n.rPrev = nil
|
||||
n.rNext = nil
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (n *lruNode) fin() {
|
||||
if r, ok := n.value.(util.Releaser); ok {
|
||||
r.Release()
|
||||
}
|
||||
if n.purgefin != nil {
|
||||
if n.delfin != nil {
|
||||
panic("conflicting delete and purge fin")
|
||||
}
|
||||
n.purgefin(n.ns.id, n.key)
|
||||
n.purgefin = nil
|
||||
} else if n.delfin != nil {
|
||||
n.delfin(true, false)
|
||||
n.delfin = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (n *lruNode) derefNB() {
|
||||
n.ref--
|
||||
if n.ref == 0 {
|
||||
if n.ns.state == nsEffective {
|
||||
// Remove elemement.
|
||||
n.ns.deleteNode(n.key)
|
||||
n.ns.lru.size -= n.charge
|
||||
n.ns.lru.alive--
|
||||
n.fin()
|
||||
}
|
||||
n.value = nil
|
||||
} else if n.ref < 0 {
|
||||
panic("leveldb/cache: lruCache: negative node reference")
|
||||
}
|
||||
}
|
||||
|
||||
func (n *lruNode) deref() {
|
||||
n.ns.lru.mu.Lock()
|
||||
n.derefNB()
|
||||
n.ns.lru.mu.Unlock()
|
||||
}
|
||||
|
||||
type lruHandle struct {
|
||||
node *lruNode
|
||||
once uint32
|
||||
}
|
||||
|
||||
func (h *lruHandle) Value() interface{} {
|
||||
if atomic.LoadUint32(&h.once) == 0 {
|
||||
return h.node.value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *lruHandle) Release() {
|
||||
if !atomic.CompareAndSwapUint32(&h.once, 0, 1) {
|
||||
return
|
||||
}
|
||||
h.node.deref()
|
||||
h.node = nil
|
||||
}
|
||||
|
||||
func rbIsRed(h *lruNode) bool {
|
||||
if h == nil {
|
||||
return false
|
||||
}
|
||||
return !h.rbBlack
|
||||
}
|
||||
|
||||
func rbRotLeft(h *lruNode) *lruNode {
|
||||
x := h.rbRight
|
||||
if x.rbBlack {
|
||||
panic("rotating a black link")
|
||||
}
|
||||
h.rbRight = x.rbLeft
|
||||
x.rbLeft = h
|
||||
x.rbBlack = h.rbBlack
|
||||
h.rbBlack = false
|
||||
return x
|
||||
}
|
||||
|
||||
func rbRotRight(h *lruNode) *lruNode {
|
||||
x := h.rbLeft
|
||||
if x.rbBlack {
|
||||
panic("rotating a black link")
|
||||
}
|
||||
h.rbLeft = x.rbRight
|
||||
x.rbRight = h
|
||||
x.rbBlack = h.rbBlack
|
||||
h.rbBlack = false
|
||||
return x
|
||||
}
|
||||
|
||||
func rbFlip(h *lruNode) {
|
||||
h.rbBlack = !h.rbBlack
|
||||
h.rbLeft.rbBlack = !h.rbLeft.rbBlack
|
||||
h.rbRight.rbBlack = !h.rbRight.rbBlack
|
||||
}
|
||||
|
||||
func rbMoveLeft(h *lruNode) *lruNode {
|
||||
rbFlip(h)
|
||||
if rbIsRed(h.rbRight.rbLeft) {
|
||||
h.rbRight = rbRotRight(h.rbRight)
|
||||
h = rbRotLeft(h)
|
||||
rbFlip(h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func rbMoveRight(h *lruNode) *lruNode {
|
||||
rbFlip(h)
|
||||
if rbIsRed(h.rbLeft.rbLeft) {
|
||||
h = rbRotRight(h)
|
||||
rbFlip(h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func rbFixup(h *lruNode) *lruNode {
|
||||
if rbIsRed(h.rbRight) {
|
||||
h = rbRotLeft(h)
|
||||
}
|
||||
|
||||
if rbIsRed(h.rbLeft) && rbIsRed(h.rbLeft.rbLeft) {
|
||||
h = rbRotRight(h)
|
||||
}
|
||||
|
||||
if rbIsRed(h.rbLeft) && rbIsRed(h.rbRight) {
|
||||
rbFlip(h)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func rbDeleteMin(h *lruNode) (hn, n *lruNode) {
|
||||
if h == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if h.rbLeft == nil {
|
||||
return nil, h
|
||||
}
|
||||
|
||||
if !rbIsRed(h.rbLeft) && !rbIsRed(h.rbLeft.rbLeft) {
|
||||
h = rbMoveLeft(h)
|
||||
}
|
||||
|
||||
h.rbLeft, n = rbDeleteMin(h.rbLeft)
|
||||
|
||||
return rbFixup(h), n
|
||||
}
|
||||
18
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/corrupt_test.go
generated
vendored
18
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/corrupt_test.go
generated
vendored
@@ -9,14 +9,12 @@ package leveldb
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/cache"
|
||||
"github.com/syndtr/goleveldb/leveldb/filter"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
"io"
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const ctValSize = 1000
|
||||
@@ -33,8 +31,8 @@ func newDbCorruptHarnessWopt(t *testing.T, o *opt.Options) *dbCorruptHarness {
|
||||
|
||||
func newDbCorruptHarness(t *testing.T) *dbCorruptHarness {
|
||||
return newDbCorruptHarnessWopt(t, &opt.Options{
|
||||
BlockCache: cache.NewLRUCache(100),
|
||||
Strict: opt.StrictJournalChecksum,
|
||||
BlockCacheCapacity: 100,
|
||||
Strict: opt.StrictJournalChecksum,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -269,9 +267,9 @@ func TestCorruptDB_TableIndex(t *testing.T) {
|
||||
func TestCorruptDB_MissingManifest(t *testing.T) {
|
||||
rnd := rand.New(rand.NewSource(0x0badda7a))
|
||||
h := newDbCorruptHarnessWopt(t, &opt.Options{
|
||||
BlockCache: cache.NewLRUCache(100),
|
||||
Strict: opt.StrictJournalChecksum,
|
||||
WriteBuffer: 1000 * 60,
|
||||
BlockCacheCapacity: 100,
|
||||
Strict: opt.StrictJournalChecksum,
|
||||
WriteBuffer: 1000 * 60,
|
||||
})
|
||||
|
||||
h.build(1000)
|
||||
|
||||
4
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db.go
generated
vendored
4
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db.go
generated
vendored
@@ -823,8 +823,8 @@ func (db *DB) GetProperty(name string) (value string, err error) {
|
||||
case p == "blockpool":
|
||||
value = fmt.Sprintf("%v", db.s.tops.bpool)
|
||||
case p == "cachedblock":
|
||||
if bc := db.s.o.GetBlockCache(); bc != nil {
|
||||
value = fmt.Sprintf("%d", bc.Size())
|
||||
if db.s.tops.bcache != nil {
|
||||
value = fmt.Sprintf("%d", db.s.tops.bcache.Size())
|
||||
} else {
|
||||
value = "<nil>"
|
||||
}
|
||||
|
||||
5
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_snapshot.go
generated
vendored
5
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_snapshot.go
generated
vendored
@@ -8,6 +8,7 @@ package leveldb
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -89,6 +90,10 @@ func (db *DB) newSnapshot() *Snapshot {
|
||||
return snap
|
||||
}
|
||||
|
||||
func (snap *Snapshot) String() string {
|
||||
return fmt.Sprintf("leveldb.Snapshot{%d}", snap.elem.seq)
|
||||
}
|
||||
|
||||
// Get gets the value for the given key. It returns ErrNotFound if
|
||||
// the DB does not contains the key.
|
||||
//
|
||||
|
||||
14
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_test.go
generated
vendored
14
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_test.go
generated
vendored
@@ -1271,7 +1271,7 @@ func TestDB_DeletionMarkers2(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDB_CompactionTableOpenError(t *testing.T) {
|
||||
h := newDbHarnessWopt(t, &opt.Options{CachedOpenFiles: -1})
|
||||
h := newDbHarnessWopt(t, &opt.Options{OpenFilesCacheCapacity: -1})
|
||||
defer h.close()
|
||||
|
||||
im := 10
|
||||
@@ -1629,8 +1629,8 @@ func TestDB_ManualCompaction(t *testing.T) {
|
||||
|
||||
func TestDB_BloomFilter(t *testing.T) {
|
||||
h := newDbHarnessWopt(t, &opt.Options{
|
||||
BlockCache: opt.NoCache,
|
||||
Filter: filter.NewBloomFilter(10),
|
||||
DisableBlockCache: true,
|
||||
Filter: filter.NewBloomFilter(10),
|
||||
})
|
||||
defer h.close()
|
||||
|
||||
@@ -2066,8 +2066,8 @@ func TestDB_GetProperties(t *testing.T) {
|
||||
|
||||
func TestDB_GoleveldbIssue72and83(t *testing.T) {
|
||||
h := newDbHarnessWopt(t, &opt.Options{
|
||||
WriteBuffer: 1 * opt.MiB,
|
||||
CachedOpenFiles: 3,
|
||||
WriteBuffer: 1 * opt.MiB,
|
||||
OpenFilesCacheCapacity: 3,
|
||||
})
|
||||
defer h.close()
|
||||
|
||||
@@ -2200,7 +2200,7 @@ func TestDB_GoleveldbIssue72and83(t *testing.T) {
|
||||
func TestDB_TransientError(t *testing.T) {
|
||||
h := newDbHarnessWopt(t, &opt.Options{
|
||||
WriteBuffer: 128 * opt.KiB,
|
||||
CachedOpenFiles: 3,
|
||||
OpenFilesCacheCapacity: 3,
|
||||
DisableCompactionBackoff: true,
|
||||
})
|
||||
defer h.close()
|
||||
@@ -2410,7 +2410,7 @@ func TestDB_TableCompactionBuilder(t *testing.T) {
|
||||
CompactionTableSize: 43 * opt.KiB,
|
||||
CompactionExpandLimitFactor: 1,
|
||||
CompactionGPOverlapsFactor: 1,
|
||||
BlockCache: opt.NoCache,
|
||||
DisableBlockCache: true,
|
||||
}
|
||||
s, err := newSession(stor, o)
|
||||
if err != nil {
|
||||
|
||||
2
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_write.go
generated
vendored
2
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_write.go
generated
vendored
@@ -112,9 +112,9 @@ func (db *DB) flush(n int) (mem *memDB, nn int, err error) {
|
||||
db.writeDelay += time.Since(start)
|
||||
db.writeDelayN++
|
||||
} else if db.writeDelayN > 0 {
|
||||
db.logf("db@write was delayed N·%d T·%v", db.writeDelayN, db.writeDelay)
|
||||
db.writeDelay = 0
|
||||
db.writeDelayN = 0
|
||||
db.logf("db@write was delayed N·%d T·%v", db.writeDelayN, db.writeDelay)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
16
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/external_test.go
generated
vendored
16
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/external_test.go
generated
vendored
@@ -17,14 +17,14 @@ import (
|
||||
var _ = testutil.Defer(func() {
|
||||
Describe("Leveldb external", func() {
|
||||
o := &opt.Options{
|
||||
BlockCache: opt.NoCache,
|
||||
BlockRestartInterval: 5,
|
||||
BlockSize: 80,
|
||||
Compression: opt.NoCompression,
|
||||
CachedOpenFiles: -1,
|
||||
Strict: opt.StrictAll,
|
||||
WriteBuffer: 1000,
|
||||
CompactionTableSize: 2000,
|
||||
DisableBlockCache: true,
|
||||
BlockRestartInterval: 5,
|
||||
BlockSize: 80,
|
||||
Compression: opt.NoCompression,
|
||||
OpenFilesCacheCapacity: -1,
|
||||
Strict: opt.StrictAll,
|
||||
WriteBuffer: 1000,
|
||||
CompactionTableSize: 2000,
|
||||
}
|
||||
|
||||
Describe("write test", func() {
|
||||
|
||||
4
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/key.go
generated
vendored
4
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/key.go
generated
vendored
@@ -106,7 +106,7 @@ func (ik iKey) assert() {
|
||||
panic("leveldb: nil iKey")
|
||||
}
|
||||
if len(ik) < 8 {
|
||||
panic(fmt.Sprintf("leveldb: iKey %q, len=%d: invalid length", ik, len(ik)))
|
||||
panic(fmt.Sprintf("leveldb: iKey %q, len=%d: invalid length", []byte(ik), len(ik)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ func (ik iKey) parseNum() (seq uint64, kt kType) {
|
||||
num := ik.num()
|
||||
seq, kt = uint64(num>>8), kType(num&0xff)
|
||||
if kt > ktVal {
|
||||
panic(fmt.Sprintf("leveldb: iKey %q, len=%d: invalid type %#x", ik, len(ik), kt))
|
||||
panic(fmt.Sprintf("leveldb: iKey %q, len=%d: invalid type %#x", []byte(ik), len(ik), kt))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
131
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/opt/options.go
generated
vendored
131
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/opt/options.go
generated
vendored
@@ -20,8 +20,9 @@ const (
|
||||
GiB = MiB * 1024
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultBlockCacheSize = 8 * MiB
|
||||
var (
|
||||
DefaultBlockCacher = LRUCacher
|
||||
DefaultBlockCacheCapacity = 8 * MiB
|
||||
DefaultBlockRestartInterval = 16
|
||||
DefaultBlockSize = 4 * KiB
|
||||
DefaultCompactionExpandLimitFactor = 25
|
||||
@@ -33,7 +34,8 @@ const (
|
||||
DefaultCompactionTotalSize = 10 * MiB
|
||||
DefaultCompactionTotalSizeMultiplier = 10.0
|
||||
DefaultCompressionType = SnappyCompression
|
||||
DefaultCachedOpenFiles = 500
|
||||
DefaultOpenFilesCacher = LRUCacher
|
||||
DefaultOpenFilesCacheCapacity = 500
|
||||
DefaultMaxMemCompationLevel = 2
|
||||
DefaultNumLevel = 7
|
||||
DefaultWriteBuffer = 4 * MiB
|
||||
@@ -41,22 +43,33 @@ const (
|
||||
DefaultWriteL0SlowdownTrigger = 8
|
||||
)
|
||||
|
||||
type noCache struct{}
|
||||
// Cacher is a caching algorithm.
|
||||
type Cacher interface {
|
||||
New(capacity int) cache.Cacher
|
||||
}
|
||||
|
||||
func (noCache) SetCapacity(capacity int) {}
|
||||
func (noCache) Capacity() int { return 0 }
|
||||
func (noCache) Used() int { return 0 }
|
||||
func (noCache) Size() int { return 0 }
|
||||
func (noCache) NumObjects() int { return 0 }
|
||||
func (noCache) GetNamespace(id uint64) cache.Namespace { return nil }
|
||||
func (noCache) PurgeNamespace(id uint64, fin cache.PurgeFin) {}
|
||||
func (noCache) ZapNamespace(id uint64) {}
|
||||
func (noCache) Purge(fin cache.PurgeFin) {}
|
||||
func (noCache) Zap() {}
|
||||
type CacherFunc struct {
|
||||
NewFunc func(capacity int) cache.Cacher
|
||||
}
|
||||
|
||||
var NoCache cache.Cache = noCache{}
|
||||
func (f *CacherFunc) New(capacity int) cache.Cacher {
|
||||
if f.NewFunc != nil {
|
||||
return f.NewFunc(capacity)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compression is the per-block compression algorithm to use.
|
||||
func noCacher(int) cache.Cacher { return nil }
|
||||
|
||||
var (
|
||||
// LRUCacher is the LRU-cache algorithm.
|
||||
LRUCacher = &CacherFunc{cache.NewLRU}
|
||||
|
||||
// NoCacher is the value to disable caching algorithm.
|
||||
NoCacher = &CacherFunc{}
|
||||
)
|
||||
|
||||
// Compression is the 'sorted table' block compression algorithm to use.
|
||||
type Compression uint
|
||||
|
||||
func (c Compression) String() string {
|
||||
@@ -133,16 +146,17 @@ type Options struct {
|
||||
// The default value is nil
|
||||
AltFilters []filter.Filter
|
||||
|
||||
// BlockCache provides per-block caching for LevelDB. Specify NoCache to
|
||||
// disable block caching.
|
||||
// BlockCacher provides cache algorithm for LevelDB 'sorted table' block caching.
|
||||
// Specify NoCacher to disable caching algorithm.
|
||||
//
|
||||
// By default LevelDB will create LRU-cache with capacity of BlockCacheSize.
|
||||
BlockCache cache.Cache
|
||||
// The default value is LRUCacher.
|
||||
BlockCacher Cacher
|
||||
|
||||
// BlockCacheSize defines the capacity of the default 'block cache'.
|
||||
// BlockCacheCapacity defines the capacity of the 'sorted table' block caching.
|
||||
// Use -1 for zero, this has same effect with specifying NoCacher to BlockCacher.
|
||||
//
|
||||
// The default value is 8MiB.
|
||||
BlockCacheSize int
|
||||
BlockCacheCapacity int
|
||||
|
||||
// BlockRestartInterval is the number of keys between restart points for
|
||||
// delta encoding of keys.
|
||||
@@ -156,13 +170,6 @@ type Options struct {
|
||||
// The default value is 4KiB.
|
||||
BlockSize int
|
||||
|
||||
// CachedOpenFiles defines number of open files to kept around when not
|
||||
// in-use, the counting includes still in-use files.
|
||||
// Set this to negative value to disable caching.
|
||||
//
|
||||
// The default value is 500.
|
||||
CachedOpenFiles int
|
||||
|
||||
// CompactionExpandLimitFactor limits compaction size after expanded.
|
||||
// This will be multiplied by table size limit at compaction target level.
|
||||
//
|
||||
@@ -237,11 +244,17 @@ type Options struct {
|
||||
// The default value uses the same ordering as bytes.Compare.
|
||||
Comparer comparer.Comparer
|
||||
|
||||
// Compression defines the per-block compression to use.
|
||||
// Compression defines the 'sorted table' block compression to use.
|
||||
//
|
||||
// The default value (DefaultCompression) uses snappy compression.
|
||||
Compression Compression
|
||||
|
||||
// DisableBlockCache allows disable use of cache.Cache functionality on
|
||||
// 'sorted table' block.
|
||||
//
|
||||
// The default value is false.
|
||||
DisableBlockCache bool
|
||||
|
||||
// DisableCompactionBackoff allows disable compaction retry backoff.
|
||||
//
|
||||
// The default value is false.
|
||||
@@ -288,6 +301,18 @@ type Options struct {
|
||||
// The default is 7.
|
||||
NumLevel int
|
||||
|
||||
// OpenFilesCacher provides cache algorithm for open files caching.
|
||||
// Specify NoCacher to disable caching algorithm.
|
||||
//
|
||||
// The default value is LRUCacher.
|
||||
OpenFilesCacher Cacher
|
||||
|
||||
// OpenFilesCacheCapacity defines the capacity of the open files caching.
|
||||
// Use -1 for zero, this has same effect with specifying NoCacher to OpenFilesCacher.
|
||||
//
|
||||
// The default value is 500.
|
||||
OpenFilesCacheCapacity int
|
||||
|
||||
// Strict defines the DB strict level.
|
||||
Strict Strict
|
||||
|
||||
@@ -320,18 +345,22 @@ func (o *Options) GetAltFilters() []filter.Filter {
|
||||
return o.AltFilters
|
||||
}
|
||||
|
||||
func (o *Options) GetBlockCache() cache.Cache {
|
||||
if o == nil {
|
||||
func (o *Options) GetBlockCacher() Cacher {
|
||||
if o == nil || o.BlockCacher == nil {
|
||||
return DefaultBlockCacher
|
||||
} else if o.BlockCacher == NoCacher {
|
||||
return nil
|
||||
}
|
||||
return o.BlockCache
|
||||
return o.BlockCacher
|
||||
}
|
||||
|
||||
func (o *Options) GetBlockCacheSize() int {
|
||||
if o == nil || o.BlockCacheSize <= 0 {
|
||||
return DefaultBlockCacheSize
|
||||
func (o *Options) GetBlockCacheCapacity() int {
|
||||
if o == nil || o.BlockCacheCapacity <= 0 {
|
||||
return DefaultBlockCacheCapacity
|
||||
} else if o.BlockCacheCapacity == -1 {
|
||||
return 0
|
||||
}
|
||||
return o.BlockCacheSize
|
||||
return o.BlockCacheCapacity
|
||||
}
|
||||
|
||||
func (o *Options) GetBlockRestartInterval() int {
|
||||
@@ -348,15 +377,6 @@ func (o *Options) GetBlockSize() int {
|
||||
return o.BlockSize
|
||||
}
|
||||
|
||||
func (o *Options) GetCachedOpenFiles() int {
|
||||
if o == nil || o.CachedOpenFiles == 0 {
|
||||
return DefaultCachedOpenFiles
|
||||
} else if o.CachedOpenFiles < 0 {
|
||||
return 0
|
||||
}
|
||||
return o.CachedOpenFiles
|
||||
}
|
||||
|
||||
func (o *Options) GetCompactionExpandLimit(level int) int {
|
||||
factor := DefaultCompactionExpandLimitFactor
|
||||
if o != nil && o.CompactionExpandLimitFactor > 0 {
|
||||
@@ -494,6 +514,25 @@ func (o *Options) GetNumLevel() int {
|
||||
return o.NumLevel
|
||||
}
|
||||
|
||||
func (o *Options) GetOpenFilesCacher() Cacher {
|
||||
if o == nil || o.OpenFilesCacher == nil {
|
||||
return DefaultOpenFilesCacher
|
||||
}
|
||||
if o.OpenFilesCacher == NoCacher {
|
||||
return nil
|
||||
}
|
||||
return o.OpenFilesCacher
|
||||
}
|
||||
|
||||
func (o *Options) GetOpenFilesCacheCapacity() int {
|
||||
if o == nil || o.OpenFilesCacheCapacity <= 0 {
|
||||
return DefaultOpenFilesCacheCapacity
|
||||
} else if o.OpenFilesCacheCapacity == -1 {
|
||||
return 0
|
||||
}
|
||||
return o.OpenFilesCacheCapacity
|
||||
}
|
||||
|
||||
func (o *Options) GetStrict(strict Strict) bool {
|
||||
if o == nil || o.Strict == 0 {
|
||||
return DefaultStrict&strict != 0
|
||||
|
||||
8
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/options.go
generated
vendored
8
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/options.go
generated
vendored
@@ -7,7 +7,6 @@
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"github.com/syndtr/goleveldb/leveldb/cache"
|
||||
"github.com/syndtr/goleveldb/leveldb/filter"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
@@ -32,13 +31,6 @@ func (s *session) setOptions(o *opt.Options) {
|
||||
no.AltFilters[i] = &iFilter{filter}
|
||||
}
|
||||
}
|
||||
// Block cache.
|
||||
switch o.GetBlockCache() {
|
||||
case nil:
|
||||
no.BlockCache = cache.NewLRUCache(o.GetBlockCacheSize())
|
||||
case opt.NoCache:
|
||||
no.BlockCache = nil
|
||||
}
|
||||
// Comparer.
|
||||
s.icmp = &iComparer{o.GetComparer()}
|
||||
no.Comparer = s.icmp
|
||||
|
||||
5
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/session.go
generated
vendored
5
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/session.go
generated
vendored
@@ -73,7 +73,7 @@ func newSession(stor storage.Storage, o *opt.Options) (s *session, err error) {
|
||||
stCompPtrs: make([]iKey, o.GetNumLevel()),
|
||||
}
|
||||
s.setOptions(o)
|
||||
s.tops = newTableOps(s, s.o.GetCachedOpenFiles())
|
||||
s.tops = newTableOps(s)
|
||||
s.setVersion(newVersion(s))
|
||||
s.log("log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed")
|
||||
return
|
||||
@@ -82,9 +82,6 @@ func newSession(stor storage.Storage, o *opt.Options) (s *session, err error) {
|
||||
// Close session.
|
||||
func (s *session) close() {
|
||||
s.tops.close()
|
||||
if bc := s.o.GetBlockCache(); bc != nil {
|
||||
bc.Purge(nil)
|
||||
}
|
||||
if s.manifest != nil {
|
||||
s.manifest.Close()
|
||||
}
|
||||
|
||||
2
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/storage/file_storage.go
generated
vendored
2
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/storage/file_storage.go
generated
vendored
@@ -221,7 +221,7 @@ func (fs *fileStorage) GetManifest() (f File, err error) {
|
||||
fs.log(fmt.Sprintf("skipping %s: invalid file name", fn))
|
||||
continue
|
||||
}
|
||||
if _, e1 := strconv.ParseUint(fn[7:], 10, 0); e1 != nil {
|
||||
if _, e1 := strconv.ParseUint(fn[8:], 10, 0); e1 != nil {
|
||||
fs.log(fmt.Sprintf("skipping %s: invalid file num: %v", fn, e1))
|
||||
continue
|
||||
}
|
||||
|
||||
70
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table.go
generated
vendored
70
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table.go
generated
vendored
@@ -286,10 +286,10 @@ func (x *tFilesSortByNum) Less(i, j int) bool {
|
||||
|
||||
// Table operations.
|
||||
type tOps struct {
|
||||
s *session
|
||||
cache cache.Cache
|
||||
cacheNS cache.Namespace
|
||||
bpool *util.BufferPool
|
||||
s *session
|
||||
cache *cache.Cache
|
||||
bcache *cache.Cache
|
||||
bpool *util.BufferPool
|
||||
}
|
||||
|
||||
// Creates an empty table and returns table writer.
|
||||
@@ -338,26 +338,28 @@ func (t *tOps) createFrom(src iterator.Iterator) (f *tFile, n int, err error) {
|
||||
|
||||
// Opens table. It returns a cache handle, which should
|
||||
// be released after use.
|
||||
func (t *tOps) open(f *tFile) (ch cache.Handle, err error) {
|
||||
func (t *tOps) open(f *tFile) (ch *cache.Handle, err error) {
|
||||
num := f.file.Num()
|
||||
ch = t.cacheNS.Get(num, func() (charge int, value interface{}) {
|
||||
ch = t.cache.Get(0, num, func() (size int, value cache.Value) {
|
||||
var r storage.Reader
|
||||
r, err = f.file.Open()
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var bcacheNS cache.Namespace
|
||||
if bc := t.s.o.GetBlockCache(); bc != nil {
|
||||
bcacheNS = bc.GetNamespace(num)
|
||||
var bcache *cache.CacheGetter
|
||||
if t.bcache != nil {
|
||||
bcache = &cache.CacheGetter{Cache: t.bcache, NS: num}
|
||||
}
|
||||
|
||||
var tr *table.Reader
|
||||
tr, err = table.NewReader(r, int64(f.size), storage.NewFileInfo(f.file), bcacheNS, t.bpool, t.s.o.Options)
|
||||
tr, err = table.NewReader(r, int64(f.size), storage.NewFileInfo(f.file), bcache, t.bpool, t.s.o.Options)
|
||||
if err != nil {
|
||||
r.Close()
|
||||
return 0, nil
|
||||
}
|
||||
return 1, tr
|
||||
|
||||
})
|
||||
if ch == nil && err == nil {
|
||||
err = ErrClosed
|
||||
@@ -412,16 +414,14 @@ func (t *tOps) newIterator(f *tFile, slice *util.Range, ro *opt.ReadOptions) ite
|
||||
// no one use the the table.
|
||||
func (t *tOps) remove(f *tFile) {
|
||||
num := f.file.Num()
|
||||
t.cacheNS.Delete(num, func(exist, pending bool) {
|
||||
if !pending {
|
||||
if err := f.file.Remove(); err != nil {
|
||||
t.s.logf("table@remove removing @%d %q", num, err)
|
||||
} else {
|
||||
t.s.logf("table@remove removed @%d", num)
|
||||
}
|
||||
if bc := t.s.o.GetBlockCache(); bc != nil {
|
||||
bc.ZapNamespace(num)
|
||||
}
|
||||
t.cache.Delete(0, num, func() {
|
||||
if err := f.file.Remove(); err != nil {
|
||||
t.s.logf("table@remove removing @%d %q", num, err)
|
||||
} else {
|
||||
t.s.logf("table@remove removed @%d", num)
|
||||
}
|
||||
if t.bcache != nil {
|
||||
t.bcache.EvictNS(num)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -429,18 +429,34 @@ func (t *tOps) remove(f *tFile) {
|
||||
// Closes the table ops instance. It will close all tables,
|
||||
// regadless still used or not.
|
||||
func (t *tOps) close() {
|
||||
t.cache.Zap()
|
||||
t.bpool.Close()
|
||||
t.cache.Close()
|
||||
if t.bcache != nil {
|
||||
t.bcache.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Creates new initialized table ops instance.
|
||||
func newTableOps(s *session, cacheCap int) *tOps {
|
||||
c := cache.NewLRUCache(cacheCap)
|
||||
func newTableOps(s *session) *tOps {
|
||||
var (
|
||||
cacher cache.Cacher
|
||||
bcache *cache.Cache
|
||||
)
|
||||
if s.o.GetOpenFilesCacheCapacity() > 0 {
|
||||
cacher = cache.NewLRU(s.o.GetOpenFilesCacheCapacity())
|
||||
}
|
||||
if !s.o.DisableBlockCache {
|
||||
var bcacher cache.Cacher
|
||||
if s.o.GetBlockCacheCapacity() > 0 {
|
||||
bcacher = cache.NewLRU(s.o.GetBlockCacheCapacity())
|
||||
}
|
||||
bcache = cache.NewCache(bcacher)
|
||||
}
|
||||
return &tOps{
|
||||
s: s,
|
||||
cache: c,
|
||||
cacheNS: c.GetNamespace(0),
|
||||
bpool: util.NewBufferPool(s.o.GetBlockSize() + 5),
|
||||
s: s,
|
||||
cache: cache.NewCache(cacher),
|
||||
bcache: bcache,
|
||||
bpool: util.NewBufferPool(s.o.GetBlockSize() + 5),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
60
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table/reader.go
generated
vendored
60
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table/reader.go
generated
vendored
@@ -509,7 +509,7 @@ type Reader struct {
|
||||
mu sync.RWMutex
|
||||
fi *storage.FileInfo
|
||||
reader io.ReaderAt
|
||||
cache cache.Namespace
|
||||
cache *cache.CacheGetter
|
||||
err error
|
||||
bpool *util.BufferPool
|
||||
// Options
|
||||
@@ -613,18 +613,22 @@ func (r *Reader) readBlock(bh blockHandle, verifyChecksum bool) (*block, error)
|
||||
|
||||
func (r *Reader) readBlockCached(bh blockHandle, verifyChecksum, fillCache bool) (*block, util.Releaser, error) {
|
||||
if r.cache != nil {
|
||||
var err error
|
||||
ch := r.cache.Get(bh.offset, func() (charge int, value interface{}) {
|
||||
if !fillCache {
|
||||
return 0, nil
|
||||
}
|
||||
var b *block
|
||||
b, err = r.readBlock(bh, verifyChecksum)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return cap(b.data), b
|
||||
})
|
||||
var (
|
||||
err error
|
||||
ch *cache.Handle
|
||||
)
|
||||
if fillCache {
|
||||
ch = r.cache.Get(bh.offset, func() (size int, value cache.Value) {
|
||||
var b *block
|
||||
b, err = r.readBlock(bh, verifyChecksum)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return cap(b.data), b
|
||||
})
|
||||
} else {
|
||||
ch = r.cache.Get(bh.offset, nil)
|
||||
}
|
||||
if ch != nil {
|
||||
b, ok := ch.Value().(*block)
|
||||
if !ok {
|
||||
@@ -667,18 +671,22 @@ func (r *Reader) readFilterBlock(bh blockHandle) (*filterBlock, error) {
|
||||
|
||||
func (r *Reader) readFilterBlockCached(bh blockHandle, fillCache bool) (*filterBlock, util.Releaser, error) {
|
||||
if r.cache != nil {
|
||||
var err error
|
||||
ch := r.cache.Get(bh.offset, func() (charge int, value interface{}) {
|
||||
if !fillCache {
|
||||
return 0, nil
|
||||
}
|
||||
var b *filterBlock
|
||||
b, err = r.readFilterBlock(bh)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return cap(b.data), b
|
||||
})
|
||||
var (
|
||||
err error
|
||||
ch *cache.Handle
|
||||
)
|
||||
if fillCache {
|
||||
ch = r.cache.Get(bh.offset, func() (size int, value cache.Value) {
|
||||
var b *filterBlock
|
||||
b, err = r.readFilterBlock(bh)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return cap(b.data), b
|
||||
})
|
||||
} else {
|
||||
ch = r.cache.Get(bh.offset, nil)
|
||||
}
|
||||
if ch != nil {
|
||||
b, ok := ch.Value().(*filterBlock)
|
||||
if !ok {
|
||||
@@ -980,7 +988,7 @@ func (r *Reader) Release() {
|
||||
// The fi, cache and bpool is optional and can be nil.
|
||||
//
|
||||
// The returned table reader instance is goroutine-safe.
|
||||
func NewReader(f io.ReaderAt, size int64, fi *storage.FileInfo, cache cache.Namespace, bpool *util.BufferPool, o *opt.Options) (*Reader, error) {
|
||||
func NewReader(f io.ReaderAt, size int64, fi *storage.FileInfo, cache *cache.CacheGetter, bpool *util.BufferPool, o *opt.Options) (*Reader, error) {
|
||||
if f == nil {
|
||||
return nil, errors.New("leveldb/table: nil file")
|
||||
}
|
||||
|
||||
10
Godeps/_workspace/src/golang.org/x/text/transform/transform_test.go
generated
vendored
10
Godeps/_workspace/src/golang.org/x/text/transform/transform_test.go
generated
vendored
@@ -16,7 +16,7 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type lowerCaseASCII struct{ transform.NopResetter }
|
||||
type lowerCaseASCII struct{ NopResetter }
|
||||
|
||||
func (lowerCaseASCII) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
n := len(src)
|
||||
@@ -34,7 +34,7 @@ func (lowerCaseASCII) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, er
|
||||
|
||||
var errYouMentionedX = errors.New("you mentioned X")
|
||||
|
||||
type dontMentionX struct{ transform.NopResetter }
|
||||
type dontMentionX struct{ NopResetter }
|
||||
|
||||
func (dontMentionX) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
n := len(src)
|
||||
@@ -52,7 +52,7 @@ func (dontMentionX) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err
|
||||
|
||||
// doublerAtEOF is a strange Transformer that transforms "this" to "tthhiiss",
|
||||
// but only if atEOF is true.
|
||||
type doublerAtEOF struct{ transform.NopResetter }
|
||||
type doublerAtEOF struct{ NopResetter }
|
||||
|
||||
func (doublerAtEOF) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
if !atEOF {
|
||||
@@ -71,7 +71,7 @@ func (doublerAtEOF) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err
|
||||
// rleDecode and rleEncode implement a toy run-length encoding: "aabbbbbbbbbb"
|
||||
// is encoded as "2a10b". The decoding is assumed to not contain any numbers.
|
||||
|
||||
type rleDecode struct{ transform.NopResetter }
|
||||
type rleDecode struct{ NopResetter }
|
||||
|
||||
func (rleDecode) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) {
|
||||
loop:
|
||||
@@ -104,7 +104,7 @@ loop:
|
||||
}
|
||||
|
||||
type rleEncode struct {
|
||||
transform.NopResetter
|
||||
NopResetter
|
||||
|
||||
// allowStutter means that "xxxxxxxx" can be encoded as "5x3x"
|
||||
// instead of always as "8x".
|
||||
|
||||
39
NICKS
Normal file
39
NICKS
Normal file
@@ -0,0 +1,39 @@
|
||||
# This file maps email addresses used in commits to nicks used the changelog.
|
||||
|
||||
AudriusButkevicius <audrius.butkevicius@gmail.com>
|
||||
Cathryne <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
|
||||
KayoticSully <kayoticsully@gmail.com>
|
||||
Nutomic <me@nutomic.com>
|
||||
Vilbrekin <vilbrekin@gmail.com>
|
||||
Zillode <zillode@zillode.be>
|
||||
alex2108 <register-github@alex-graf.de>
|
||||
andrew-d <andrew@du.nham.ca>
|
||||
asdil12 <dominik@heidler.eu>
|
||||
bigbear2nd <bigbear2nd@gmail.com>
|
||||
brendanlong <self@brendanlong.com>
|
||||
bsidhom <bsidhom@gmail.com>
|
||||
calmh <jakob@nym.se>
|
||||
cdata <chris@scriptolo.gy>
|
||||
ceh <emil@hessman.se>
|
||||
cqcallaw <enlightened.despot@gmail.com>
|
||||
facastagnini <federico.castagnini@gmail.com>
|
||||
filoozoom <philippe@schommers.be>
|
||||
frioux <frew@afoolishmanifesto.com> <frioux@gmail.com>
|
||||
gillisig <gilli@vx.is>
|
||||
jedie <github.com@jensdiemer.de> <git@jensdiemer.de>
|
||||
jpjp <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
||||
kozec <kozec@kozec.com>
|
||||
marcindziadus <dziadus.marcin@gmail.com>
|
||||
mvdan <mvdan@mvdan.cc>
|
||||
peterhoeg <peter@speartail.com>
|
||||
philips <brandon@ifup.org>
|
||||
piobpl <piotrb10@gmail.com>
|
||||
pluby <phill.luby@newredo.com>
|
||||
pyfisch <pyfisch@gmail.com>
|
||||
qbit <qbit@deftly.net>
|
||||
seehuhn <voss@seehuhn.de>
|
||||
snnd <dw@risu.io>
|
||||
timabell <tim@timwise.co.uk>
|
||||
tojrobinson <tully@tojr.org>
|
||||
uok <ueomkail@gmail.com> <uok@users.noreply.github.com>
|
||||
veeti <veeti.paananen@rojekti.fi>
|
||||
26
README.md
26
README.md
@@ -11,7 +11,7 @@ This is the `syncthing` project. The following are the project goals:
|
||||
collaborating devices. The protocol should be well defined, unambiguous,
|
||||
easily understood, free to use, efficient, secure and language neutral.
|
||||
This is the [Block Exchange
|
||||
Protocol](https://github.com/syncthing/syncthing/blob/master/protocol/PROTOCOL.md).
|
||||
Protocol](https://github.com/syncthing/specs/blob/master/BEPv1.md).
|
||||
|
||||
2. Provide the reference implementation to demonstrate the usability of
|
||||
said protocol. This is the `syncthing` utility. It is the hope that
|
||||
@@ -25,7 +25,8 @@ for incompatible changes.
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
Take a look at the [getting started guide](http://discourse.syncthing.net/t/46).
|
||||
Take a look at the [getting started
|
||||
guide](https://github.com/syncthing/syncthing/wiki/Getting-Started).
|
||||
|
||||
There are a few examples for keeping syncthing running in the background
|
||||
on your system in [the etc directory](https://github.com/syncthing/syncthing/blob/master/etc).
|
||||
@@ -37,30 +38,23 @@ Building
|
||||
--------
|
||||
|
||||
Building Syncthing from source is easy, and there's a
|
||||
[guide](http://discourse.syncthing.net/t/44)
|
||||
[guide](https://github.com/syncthing/syncthing/wiki/Building).
|
||||
that describes it for both Unix and Windows.
|
||||
|
||||
Signed Releases
|
||||
---------------
|
||||
|
||||
As of v0.7.0 and onwards, git tags and release binaries are GPG signed with
|
||||
the key BCE524C7 (http://nym.se/gpg.txt). For release binaries, MD5 and
|
||||
SHA1 checksums are calculated and signed, available in the
|
||||
md5sum.txt.asc and sha1sum.txt.asc files.
|
||||
As of v0.10.15 and onwards, git tags and release binaries are GPG signed
|
||||
with the key D26E6ED000654A3E (see http://syncthing.net/security.html).
|
||||
For release binaries, MD5 and SHA1 checksums are calculated and signed,
|
||||
available in the md5sum.txt.asc and sha1sum.txt.asc files.
|
||||
|
||||
Documentation
|
||||
=============
|
||||
|
||||
The [syncthing
|
||||
documentation](http://discourse.syncthing.net/category/documentation) is
|
||||
on the discourse site.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
All documentation and protocol specifications are licensed
|
||||
under the [Creative Commons Attribution 4.0 International
|
||||
License](http://creativecommons.org/licenses/by/4.0/).
|
||||
documentation](https://github.com/syncthing/syncthing/wiki/) is on the
|
||||
Github wiki.
|
||||
|
||||
All code is licensed under the
|
||||
[GPL](https://github.com/syncthing/syncthing/blob/master/LICENSE), v3 or
|
||||
|
||||
83
build.go
83
build.go
@@ -22,6 +22,7 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -72,7 +73,7 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
switch goarch {
|
||||
case "386", "amd64", "arm", "armv5", "armv6", "armv7":
|
||||
case "386", "amd64", "arm":
|
||||
break
|
||||
default:
|
||||
log.Printf("Unknown goarch %q; proceed with caution!", goarch)
|
||||
@@ -190,7 +191,12 @@ func install(pkg string, tags []string) {
|
||||
}
|
||||
|
||||
func build(pkg string, tags []string) {
|
||||
rmr("syncthing", "syncthing.exe")
|
||||
binary := "syncthing"
|
||||
if goos == "windows" {
|
||||
binary += ".exe"
|
||||
}
|
||||
|
||||
rmr(binary, binary+".md5")
|
||||
args := []string{"build", "-ldflags", ldflags()}
|
||||
if len(tags) > 0 {
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
@@ -201,6 +207,13 @@ func build(pkg string, tags []string) {
|
||||
args = append(args, pkg)
|
||||
setBuildEnv()
|
||||
runPrint("go", args...)
|
||||
|
||||
// Create an md5 checksum of the binary, to be included in the archive for
|
||||
// automatic upgrades.
|
||||
err := md5File(binary)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func buildTar() {
|
||||
@@ -213,14 +226,20 @@ func buildTar() {
|
||||
build("./cmd/syncthing", tags)
|
||||
filename := name + ".tar.gz"
|
||||
files := []archiveFile{
|
||||
{"README.md", name + "/README.txt"},
|
||||
{"LICENSE", name + "/LICENSE.txt"},
|
||||
{"AUTHORS", name + "/AUTHORS.txt"},
|
||||
{"syncthing", name + "/syncthing"},
|
||||
{src: "README.md", dst: name + "/README.txt"},
|
||||
{src: "LICENSE", dst: name + "/LICENSE.txt"},
|
||||
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
|
||||
{src: "syncthing", dst: name + "/syncthing"},
|
||||
{src: "syncthing.md5", dst: name + "/syncthing.md5"},
|
||||
}
|
||||
|
||||
for _, file := range listFiles("etc") {
|
||||
files = append(files, archiveFile{file, name + "/" + file})
|
||||
files = append(files, archiveFile{src: file, dst: name + "/" + file})
|
||||
}
|
||||
for _, file := range listFiles("extra") {
|
||||
files = append(files, archiveFile{src: file, dst: name + "/" + filepath.Base(file)})
|
||||
}
|
||||
|
||||
tarGz(filename, files)
|
||||
log.Println(filename)
|
||||
}
|
||||
@@ -235,11 +254,17 @@ func buildZip() {
|
||||
build("./cmd/syncthing", tags)
|
||||
filename := name + ".zip"
|
||||
files := []archiveFile{
|
||||
{"README.md", name + "/README.txt"},
|
||||
{"LICENSE", name + "/LICENSE.txt"},
|
||||
{"AUTHORS", name + "/AUTHORS.txt"},
|
||||
{"syncthing.exe", name + "/syncthing.exe"},
|
||||
{src: "README.md", dst: name + "/README.txt"},
|
||||
{src: "LICENSE", dst: name + "/LICENSE.txt"},
|
||||
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
|
||||
{src: "syncthing.exe", dst: name + "/syncthing.exe"},
|
||||
{src: "syncthing.exe.md5", dst: name + "/syncthing.exe.md5"},
|
||||
}
|
||||
|
||||
for _, file := range listFiles("extra") {
|
||||
files = append(files, archiveFile{src: file, dst: name + "/" + filepath.Base(file)})
|
||||
}
|
||||
|
||||
zipFile(filename, files)
|
||||
log.Println(filename)
|
||||
}
|
||||
@@ -260,15 +285,7 @@ func listFiles(dir string) []string {
|
||||
|
||||
func setBuildEnv() {
|
||||
os.Setenv("GOOS", goos)
|
||||
if strings.HasPrefix(goarch, "armv") {
|
||||
os.Setenv("GOARCH", "arm")
|
||||
os.Setenv("GOARM", goarch[4:])
|
||||
} else {
|
||||
os.Setenv("GOARCH", goarch)
|
||||
}
|
||||
if goarch == "386" {
|
||||
os.Setenv("GO386", "387")
|
||||
}
|
||||
os.Setenv("GOARCH", goarch)
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Println("Warning: can't determine current dir:", err)
|
||||
@@ -554,3 +571,29 @@ func zipFile(out string, files []archiveFile) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func md5File(file string) error {
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
h := md5.New()
|
||||
_, err = io.Copy(h, fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := os.Create(file + ".md5")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(out, "%x\n", h.Sum(nil))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return out.Close()
|
||||
}
|
||||
|
||||
26
build.sh
26
build.sh
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
IFS=$'\n\t'
|
||||
|
||||
DOCKERIMGV=1.4-1
|
||||
DOCKERIMGV=1.4-5
|
||||
|
||||
case "${1:-default}" in
|
||||
default)
|
||||
@@ -52,9 +52,7 @@ case "${1:-default}" in
|
||||
all)
|
||||
go run build.go -goos linux -goarch amd64 tar
|
||||
go run build.go -goos linux -goarch 386 tar
|
||||
go run build.go -goos linux -goarch armv5 tar
|
||||
go run build.go -goos linux -goarch armv6 tar
|
||||
go run build.go -goos linux -goarch armv7 tar
|
||||
go run build.go -goos linux -goarch arm tar
|
||||
|
||||
go run build.go -goos freebsd -goarch amd64 tar
|
||||
go run build.go -goos freebsd -goarch 386 tar
|
||||
@@ -105,7 +103,7 @@ case "${1:-default}" in
|
||||
;;
|
||||
|
||||
docker-init)
|
||||
docker build -q -t syncthing/build:$DOCKERIMGV docker >/dev/null
|
||||
docker build -q -t syncthing/build:$DOCKERIMGV docker
|
||||
;;
|
||||
|
||||
docker-all)
|
||||
@@ -113,22 +111,24 @@ case "${1:-default}" in
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
syncthing/build:$DOCKERIMGV \
|
||||
sh -c './build.sh clean && ./build.sh all && STTRACE=all ./build.sh test-cov'
|
||||
sh -c './build.sh clean \
|
||||
&& go vet ./cmd/... ./internal/... \
|
||||
&& ( golint ./cmd/... ; golint ./internal/... ) | egrep -v "comment on exported|should have comment" \
|
||||
&& ./build.sh all \
|
||||
&& STTRACE=all ./build.sh test-cov'
|
||||
;;
|
||||
|
||||
docker-test)
|
||||
docker run --rm -h syncthing-builder -u $(id -u) -t \
|
||||
-v $(pwd):/tmp/syncthing \
|
||||
-v $(pwd):/go/src/github.com/syncthing/syncthing \
|
||||
-w /go/src/github.com/syncthing/syncthing \
|
||||
syncthing/build:$DOCKERIMGV \
|
||||
sh -euxc 'mkdir -p /go/src/github.com/syncthing \
|
||||
&& cd /go/src/github.com/syncthing \
|
||||
&& cp -r /tmp/syncthing syncthing \
|
||||
&& cd syncthing \
|
||||
&& ./build.sh clean \
|
||||
sh -euxc './build.sh clean \
|
||||
&& go run build.go -race \
|
||||
&& export GOPATH=$(pwd)/Godeps/_workspace:$GOPATH \
|
||||
&& cd test \
|
||||
&& go test -tags integration -v -timeout 60m -short'
|
||||
&& go test -tags integration -v -timeout 60m -short \
|
||||
&& git clean -fxd .'
|
||||
;;
|
||||
|
||||
*)
|
||||
|
||||
9
changelog.sh
Executable file
9
changelog.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
since="$1"
|
||||
if [[ -z $since ]] ; then
|
||||
since="$(git describe --abbrev=0 HEAD^).."
|
||||
fi
|
||||
|
||||
git log --pretty=format:'* %s (%h, @%aN)' "$since" | egrep 'fixes #\d|ref #\d'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
missing-authors() {
|
||||
for email in $(git log --format=%ae master | sort | uniq) ; do
|
||||
for email in $(git log --format=%ae HEAD | sort | uniq) ; do
|
||||
grep -q "$email" AUTHORS || echo $email
|
||||
done
|
||||
}
|
||||
@@ -13,7 +13,10 @@ no-docs-typos() {
|
||||
grep -v f2459ef3319b2f060dbcdacd0c35a1788a94b8bd |\
|
||||
grep -v b61f418bf2d1f7d5a9d7088a20a2a448e5e66801 |\
|
||||
grep -v f0621207e3953711f9ab86d99724f1d0faac45b1 |\
|
||||
grep -v f1120d7aa936c0658429edef0037792520b46334
|
||||
grep -v f1120d7aa936c0658429edef0037792520b46334 |\
|
||||
grep -v a9339d0627fff439879d157c75077f02c9fac61b |\
|
||||
grep -v 254c63763a3ad42fd82259f1767db526cff94a14 |\
|
||||
grep -v 4b76ec40c07078beaa2c5e250ed7d9bd6276a718
|
||||
}
|
||||
|
||||
print-missing-authors() {
|
||||
@@ -23,22 +26,24 @@ print-missing-authors() {
|
||||
}
|
||||
|
||||
print-missing-copyright() {
|
||||
find . -name \*.go | xargs grep -L 'Copyright (C)' | grep -v Godeps
|
||||
find . -name \*.go | xargs egrep -L 'Copyright \(C\)|automatically generated' | grep -v Godeps | grep -v internal/auto/
|
||||
}
|
||||
|
||||
print-line-blame() {
|
||||
for f in $(find . -name \*.go | grep -v Godep) gui/app.js gui/index.html ; do
|
||||
git blame --line-porcelain $f | grep author-mail
|
||||
done | sort | uniq -c | sort -n
|
||||
}
|
||||
echo Author emails missing in AUTHORS file:
|
||||
print-missing-authors
|
||||
echo
|
||||
authors=$(print-missing-authors)
|
||||
if [[ ! -z $authors ]] ; then
|
||||
echo '***'
|
||||
echo Author emails not in AUTHORS:
|
||||
echo $authors
|
||||
echo '***'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo Files missing copyright notice:
|
||||
print-missing-copyright
|
||||
echo
|
||||
|
||||
echo Blame lines per author:
|
||||
print-line-blame
|
||||
copy=$(print-missing-copyright)
|
||||
if [[ ! -z $copy ]] ; then
|
||||
echo ***
|
||||
echo Files missing copyright notice:
|
||||
echo $copy
|
||||
echo ***
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
177
cmd/stcompdirs/main.go
Normal file
177
cmd/stcompdirs/main.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/symlinks"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
log.Println(compareDirectories(flag.Args()...))
|
||||
}
|
||||
|
||||
// Compare a number of directories. Returns nil if the contents are identical,
|
||||
// otherwise an error describing the first found difference.
|
||||
func compareDirectories(dirs ...string) error {
|
||||
chans := make([]chan fileInfo, len(dirs))
|
||||
for i := range chans {
|
||||
chans[i] = make(chan fileInfo)
|
||||
}
|
||||
errcs := make([]chan error, len(dirs))
|
||||
abort := make(chan struct{})
|
||||
|
||||
for i := range dirs {
|
||||
errcs[i] = startWalker(dirs[i], chans[i], abort)
|
||||
}
|
||||
|
||||
res := make([]fileInfo, len(dirs))
|
||||
for {
|
||||
numDone := 0
|
||||
for i := range chans {
|
||||
fi, ok := <-chans[i]
|
||||
if !ok {
|
||||
err, hasError := <-errcs[i]
|
||||
if hasError {
|
||||
close(abort)
|
||||
return err
|
||||
}
|
||||
numDone++
|
||||
}
|
||||
res[i] = fi
|
||||
}
|
||||
|
||||
for i := 1; i < len(res); i++ {
|
||||
if res[i] != res[0] {
|
||||
close(abort)
|
||||
if res[i].name < res[0].name {
|
||||
return fmt.Errorf("%s missing %v (present in %s)", dirs[0], res[i], dirs[i])
|
||||
} else if res[i].name > res[0].name {
|
||||
return fmt.Errorf("%s missing %v (present in %s)", dirs[i], res[0], dirs[0])
|
||||
}
|
||||
return fmt.Errorf("Mismatch; %v (%s) != %v (%s)", res[i], dirs[i], res[0], dirs[0])
|
||||
}
|
||||
}
|
||||
|
||||
if numDone == len(dirs) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
name string
|
||||
mode os.FileMode
|
||||
mod int64
|
||||
hash [16]byte
|
||||
}
|
||||
|
||||
func (f fileInfo) String() string {
|
||||
return fmt.Sprintf("%s %04o %d %x", f.name, f.mode, f.mod, f.hash)
|
||||
}
|
||||
|
||||
func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) chan error {
|
||||
walker := func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rn, _ := filepath.Rel(dir, path)
|
||||
if rn == "." || rn == ".stfolder" {
|
||||
return nil
|
||||
}
|
||||
if rn == ".stversions" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
var f fileInfo
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
f = fileInfo{
|
||||
name: rn,
|
||||
mode: os.ModeSymlink,
|
||||
}
|
||||
|
||||
tgt, _, err := symlinks.Read(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h := md5.New()
|
||||
h.Write([]byte(tgt))
|
||||
hash := h.Sum(nil)
|
||||
|
||||
copy(f.hash[:], hash)
|
||||
} else if info.IsDir() {
|
||||
f = fileInfo{
|
||||
name: rn,
|
||||
mode: info.Mode(),
|
||||
// hash and modtime zero for directories
|
||||
}
|
||||
} else {
|
||||
f = fileInfo{
|
||||
name: rn,
|
||||
mode: info.Mode(),
|
||||
mod: info.ModTime().Unix(),
|
||||
}
|
||||
sum, err := md5file(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.hash = sum
|
||||
}
|
||||
|
||||
select {
|
||||
case res <- f:
|
||||
return nil
|
||||
case <-abort:
|
||||
return errors.New("abort")
|
||||
}
|
||||
}
|
||||
|
||||
errc := make(chan error)
|
||||
go func() {
|
||||
err := filepath.Walk(dir, walker)
|
||||
close(res)
|
||||
if err != nil {
|
||||
errc <- err
|
||||
}
|
||||
close(errc)
|
||||
}()
|
||||
return errc
|
||||
}
|
||||
|
||||
func md5file(fname string) (hash [16]byte, err error) {
|
||||
f, err := os.Open(fname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := md5.New()
|
||||
io.Copy(h, f)
|
||||
hb := h.Sum(nil)
|
||||
copy(hash[:], hb)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -13,44 +13,40 @@
|
||||
// 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 ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/discover"
|
||||
"github.com/syncthing/syncthing/internal/protocol"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
var server string
|
||||
|
||||
flag.StringVar(&server, "server", "udp4://announce.syncthing.net:22026", "Announce server")
|
||||
flag.Parse()
|
||||
path := strings.Split(flag.Arg(0), "/")
|
||||
|
||||
var obj map[string]interface{}
|
||||
dec := json.NewDecoder(os.Stdin)
|
||||
dec.UseNumber()
|
||||
dec.Decode(&obj)
|
||||
|
||||
var v interface{} = obj
|
||||
for _, p := range path {
|
||||
switch tv := v.(type) {
|
||||
case map[string]interface{}:
|
||||
v = tv[p]
|
||||
case []interface{}:
|
||||
i, err := strconv.Atoi(p)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
v = tv[i]
|
||||
default:
|
||||
return // Silence is golden
|
||||
}
|
||||
if len(flag.Args()) != 1 || server == "" {
|
||||
log.Printf("Usage: %s [-server=\"udp4://announce.syncthing.net:22026\"] <device>", os.Args[0])
|
||||
os.Exit(64)
|
||||
}
|
||||
|
||||
id, err := protocol.DeviceIDFromString(flag.Args()[0])
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
discoverer := discover.NewDiscoverer(protocol.LocalDeviceID, nil)
|
||||
discoverer.StartGlobal([]string{server}, 1)
|
||||
for _, addr := range discoverer.Lookup(id) {
|
||||
log.Println(addr)
|
||||
}
|
||||
fmt.Println(v)
|
||||
}
|
||||
@@ -43,8 +43,8 @@ func main() {
|
||||
|
||||
if *device == "" {
|
||||
log.Printf("*** Global index for folder %q", *folder)
|
||||
fs.WithGlobalTruncated(func(fi protocol.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfoTruncated)
|
||||
fs.WithGlobalTruncated(func(fi files.FileIntf) bool {
|
||||
f := fi.(files.FileInfoTruncated)
|
||||
fmt.Println(f)
|
||||
fmt.Println("\t", fs.Availability(f.Name))
|
||||
return true
|
||||
@@ -55,8 +55,8 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("*** Have index for folder %q device %q", *folder, n)
|
||||
fs.WithHaveTruncated(n, func(fi protocol.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfoTruncated)
|
||||
fs.WithHaveTruncated(n, func(fi files.FileIntf) bool {
|
||||
f := fi.(files.FileInfoTruncated)
|
||||
fmt.Println(f)
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/syncthing/syncthing/internal/config"
|
||||
"github.com/syncthing/syncthing/internal/discover"
|
||||
"github.com/syncthing/syncthing/internal/events"
|
||||
"github.com/syncthing/syncthing/internal/files"
|
||||
"github.com/syncthing/syncthing/internal/model"
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
"github.com/syncthing/syncthing/internal/protocol"
|
||||
@@ -70,7 +71,16 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
if err != nil {
|
||||
l.Infoln("Loading HTTPS certificate:", err)
|
||||
l.Infoln("Creating new HTTPS certificate")
|
||||
newCertificate(confDir, "https-")
|
||||
|
||||
// When generating the HTTPS certificate, use the system host name per
|
||||
// default. If that isn't available, use the "syncthing" default.
|
||||
var name string
|
||||
name, err = os.Hostname()
|
||||
if err != nil {
|
||||
name = tlsDefaultCommonName
|
||||
}
|
||||
|
||||
newCertificate(confDir, "https-", name)
|
||||
cert, err = loadCert(confDir, "https-")
|
||||
}
|
||||
if err != nil {
|
||||
@@ -78,7 +88,20 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
}
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ServerName: "syncthing",
|
||||
MinVersion: tls.VersionTLS10, // No SSLv3
|
||||
CipherSuites: []uint16{
|
||||
// No RC4
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
},
|
||||
}
|
||||
|
||||
rawListener, err := net.Listen("tcp", cfg.Address)
|
||||
@@ -108,6 +131,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
|
||||
getRestMux.HandleFunc("/rest/version", restGetVersion)
|
||||
getRestMux.HandleFunc("/rest/stats/device", withModel(m, restGetDeviceStats))
|
||||
getRestMux.HandleFunc("/rest/stats/folder", withModel(m, restGetFolderStats))
|
||||
|
||||
// Debug endpoints, not for general use
|
||||
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
|
||||
@@ -126,6 +150,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
|
||||
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
|
||||
postRestMux.HandleFunc("/rest/scan", withModel(m, restPostScan))
|
||||
postRestMux.HandleFunc("/rest/bump", withModel(m, restPostBump))
|
||||
|
||||
// A handler that splits requests between the two above and disables
|
||||
// caching
|
||||
@@ -291,19 +316,12 @@ func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var folder = qs.Get("folder")
|
||||
|
||||
files := m.NeedFolderFilesLimited(folder, 100) // max 100 files
|
||||
progress, queued, rest := m.NeedFolderFiles(folder, 100)
|
||||
// Convert the struct to a more loose structure, and inject the size.
|
||||
output := make([]map[string]interface{}, 0, len(files))
|
||||
for _, file := range files {
|
||||
output = append(output, map[string]interface{}{
|
||||
"Name": file.Name,
|
||||
"Flags": file.Flags,
|
||||
"Modified": file.Modified,
|
||||
"Version": file.Version,
|
||||
"LocalVersion": file.LocalVersion,
|
||||
"NumBlocks": file.NumBlocks,
|
||||
"Size": protocol.BlocksToSize(file.NumBlocks),
|
||||
})
|
||||
output := map[string][]map[string]interface{}{
|
||||
"progress": toNeedSlice(progress),
|
||||
"queued": toNeedSlice(queued),
|
||||
"rest": toNeedSlice(rest),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
@@ -322,6 +340,12 @@ func restGetDeviceStats(m *model.Model, w http.ResponseWriter, r *http.Request)
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restGetFolderStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var res = m.FolderStatistics()
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func restGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(cfg.Raw())
|
||||
@@ -334,44 +358,44 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
l.Warnln("decoding posted config:", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
} else {
|
||||
if newCfg.GUI.Password != cfg.GUI().Password {
|
||||
if newCfg.GUI.Password != "" {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
|
||||
if err != nil {
|
||||
l.Warnln("bcrypting password:", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
} else {
|
||||
newCfg.GUI.Password = string(hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start or stop usage reporting as appropriate
|
||||
|
||||
if curAcc := cfg.Options().URAccepted; newCfg.Options.URAccepted > curAcc {
|
||||
// UR was enabled
|
||||
newCfg.Options.URAccepted = usageReportVersion
|
||||
newCfg.Options.URUniqueID = randomString(6)
|
||||
err := sendUsageReport(m)
|
||||
if err != nil {
|
||||
l.Infoln("Usage report:", err)
|
||||
}
|
||||
go usageReportingLoop(m)
|
||||
} else if newCfg.Options.URAccepted < curAcc {
|
||||
// UR was disabled
|
||||
newCfg.Options.URAccepted = -1
|
||||
newCfg.Options.URUniqueID = ""
|
||||
stopUsageReporting()
|
||||
}
|
||||
|
||||
// Activate and save
|
||||
|
||||
configInSync = !config.ChangeRequiresRestart(cfg.Raw(), newCfg)
|
||||
cfg.Replace(newCfg)
|
||||
cfg.Save()
|
||||
}
|
||||
|
||||
if newCfg.GUI.Password != cfg.GUI().Password {
|
||||
if newCfg.GUI.Password != "" {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
|
||||
if err != nil {
|
||||
l.Warnln("bcrypting password:", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
newCfg.GUI.Password = string(hash)
|
||||
}
|
||||
}
|
||||
|
||||
// Start or stop usage reporting as appropriate
|
||||
|
||||
if curAcc := cfg.Options().URAccepted; newCfg.Options.URAccepted > curAcc {
|
||||
// UR was enabled
|
||||
newCfg.Options.URAccepted = usageReportVersion
|
||||
newCfg.Options.URUniqueID = randomString(8)
|
||||
err := sendUsageReport(m)
|
||||
if err != nil {
|
||||
l.Infoln("Usage report:", err)
|
||||
}
|
||||
go usageReportingLoop(m)
|
||||
} else if newCfg.Options.URAccepted < curAcc {
|
||||
// UR was disabled
|
||||
newCfg.Options.URAccepted = -1
|
||||
newCfg.Options.URUniqueID = ""
|
||||
stopUsageReporting()
|
||||
}
|
||||
|
||||
// Activate and save
|
||||
|
||||
configInSync = !config.ChangeRequiresRestart(cfg.Raw(), newCfg)
|
||||
cfg.Replace(newCfg)
|
||||
cfg.Save()
|
||||
}
|
||||
|
||||
func restGetConfigInSync(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -425,6 +449,7 @@ func restGetSystem(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
cpuUsageLock.RUnlock()
|
||||
res["cpuPercent"] = cpusum / 10
|
||||
res["pathSeparator"] = string(filepath.Separator)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
@@ -548,6 +573,10 @@ func restGetEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func restGetUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
if noUpgrade {
|
||||
http.Error(w, upgrade.ErrUpgradeUnsupported.Error(), 500)
|
||||
return
|
||||
}
|
||||
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
@@ -598,7 +627,7 @@ func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if upgrade.CompareVersions(rel.Tag, Version) == 1 {
|
||||
err = upgrade.UpgradeTo(rel)
|
||||
err = upgrade.To(rel)
|
||||
if err != nil {
|
||||
l.Warnln("upgrading:", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
@@ -621,6 +650,14 @@ func restPostScan(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func restPostBump(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
file := qs.Get("file")
|
||||
m.BringToFront(folder, file)
|
||||
restGetNeed(m, w, r)
|
||||
}
|
||||
|
||||
func getQR(w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var text = qs.Get("text")
|
||||
@@ -746,3 +783,19 @@ func mimeTypeForFile(file string) string {
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
}
|
||||
|
||||
func toNeedSlice(fs []files.FileInfoTruncated) []map[string]interface{} {
|
||||
output := make([]map[string]interface{}, len(fs))
|
||||
for i, file := range fs {
|
||||
output[i] = map[string]interface{}{
|
||||
"Name": file.Name,
|
||||
"Flags": file.Flags,
|
||||
"Modified": file.Modified,
|
||||
"Version": file.Version,
|
||||
"LocalVersion": file.LocalVersion,
|
||||
"NumBlocks": file.NumBlocks,
|
||||
"Size": files.BlocksToSize(file.NumBlocks),
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
@@ -17,8 +17,6 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -88,7 +86,7 @@ func validCsrfToken(token string) bool {
|
||||
}
|
||||
|
||||
func newCsrfToken() string {
|
||||
token := randomString(30)
|
||||
token := randomString(32)
|
||||
|
||||
csrfMut.Lock()
|
||||
csrfTokens = append(csrfTokens, token)
|
||||
@@ -140,13 +138,3 @@ func loadCsrfTokens() {
|
||||
csrfTokens = append(csrfTokens, s.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func randomString(len int) string {
|
||||
bs := make([]byte, len)
|
||||
_, err := rand.Reader.Read(bs)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(bs)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
@@ -103,10 +102,10 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
cfg *config.ConfigWrapper
|
||||
cfg *config.Wrapper
|
||||
myID protocol.DeviceID
|
||||
confDir string
|
||||
logFlags int = log.Ltime
|
||||
logFlags = log.Ltime
|
||||
writeRateLimit *ratelimit.Bucket
|
||||
readRateLimit *ratelimit.Bucket
|
||||
stop = make(chan int)
|
||||
@@ -177,16 +176,13 @@ are mostly useful for developers. Use with care.
|
||||
available CPU cores.`
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// Command line and environment options
|
||||
var (
|
||||
reset bool
|
||||
showVersion bool
|
||||
doUpgrade bool
|
||||
doUpgradeCheck bool
|
||||
upgradeTo string
|
||||
noBrowser bool
|
||||
noConsole bool
|
||||
generateDir string
|
||||
@@ -232,6 +228,7 @@ func main() {
|
||||
flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade")
|
||||
flag.BoolVar(&doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
|
||||
flag.BoolVar(&showVersion, "version", false, "Show version")
|
||||
flag.StringVar(&upgradeTo, "upgrade-to", upgradeTo, "Force upgrade directly from specified URL")
|
||||
|
||||
flag.Usage = usageFor(flag.CommandLine, usage, fmt.Sprintf(extraUsage, defConfDir))
|
||||
flag.Parse()
|
||||
@@ -266,19 +263,22 @@ func main() {
|
||||
}
|
||||
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
l.Fatalln("generate:", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
if err == nil && !info.IsDir() {
|
||||
l.Fatalln(dir, "is not a directory")
|
||||
}
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
err = os.MkdirAll(dir, 0700)
|
||||
if err != nil {
|
||||
l.Fatalln("generate:", err)
|
||||
}
|
||||
}
|
||||
|
||||
cert, err := loadCert(dir, "")
|
||||
if err == nil {
|
||||
l.Warnln("Key exists; will not overwrite.")
|
||||
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
|
||||
} else {
|
||||
newCertificate(dir, "")
|
||||
newCertificate(dir, "", tlsDefaultCommonName)
|
||||
cert, err = loadCert(dir, "")
|
||||
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||
if err != nil {
|
||||
@@ -317,6 +317,15 @@ func main() {
|
||||
// Ensure that our home directory exists.
|
||||
ensureDir(confDir, 0700)
|
||||
|
||||
if upgradeTo != "" {
|
||||
err := upgrade.ToURL(upgradeTo)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err) // exits 1
|
||||
}
|
||||
l.Okln("Upgraded from", upgradeTo)
|
||||
return
|
||||
}
|
||||
|
||||
if doUpgrade || doUpgradeCheck {
|
||||
rel, err := upgrade.LatestRelease(IsBeta)
|
||||
if err != nil {
|
||||
@@ -332,20 +341,19 @@ func main() {
|
||||
|
||||
if doUpgrade {
|
||||
// Use leveldb database locks to protect against concurrent upgrades
|
||||
_, err = leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{CachedOpenFiles: 100})
|
||||
_, err = leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{OpenFilesCacheCapacity: 100})
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot upgrade, database seems to be locked. Is another copy of Syncthing already running?")
|
||||
}
|
||||
|
||||
err = upgrade.UpgradeTo(rel)
|
||||
err = upgrade.To(rel)
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err) // exits 1
|
||||
}
|
||||
l.Okf("Upgraded to %q", rel.Tag)
|
||||
return
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if reset {
|
||||
@@ -376,13 +384,17 @@ func syncthingMain() {
|
||||
// Ensure that that we have a certificate and key.
|
||||
cert, err = loadCert(confDir, "")
|
||||
if err != nil {
|
||||
newCertificate(confDir, "")
|
||||
newCertificate(confDir, "", tlsDefaultCommonName)
|
||||
cert, err = loadCert(confDir, "")
|
||||
if err != nil {
|
||||
l.Fatalln("load cert:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// We reinitialize the predictable RNG with our device ID, to get a
|
||||
// sequence that is always the same but unique to this syncthing instance.
|
||||
predictableRandom.Seed(seedFromBytes(cert.Certificate[0]))
|
||||
|
||||
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||
l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5]))
|
||||
|
||||
@@ -477,7 +489,7 @@ func syncthingMain() {
|
||||
readRateLimit = ratelimit.NewBucketWithRate(float64(1000*opts.MaxRecvKbps), int64(5*1000*opts.MaxRecvKbps))
|
||||
}
|
||||
|
||||
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{CachedOpenFiles: 100})
|
||||
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{OpenFilesCacheCapacity: 100})
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
|
||||
}
|
||||
@@ -574,7 +586,9 @@ func syncthingMain() {
|
||||
if opts.URUniqueID == "" {
|
||||
// Previously the ID was generated from the node ID. We now need
|
||||
// to generate a new one.
|
||||
opts.URUniqueID = randomString(6)
|
||||
opts.URUniqueID = randomString(8)
|
||||
cfg.SetOptions(opts)
|
||||
cfg.Save()
|
||||
}
|
||||
go usageReportingLoop(m)
|
||||
go func() {
|
||||
@@ -609,7 +623,7 @@ func syncthingMain() {
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func setupGUI(cfg *config.ConfigWrapper, m *model.Model) {
|
||||
func setupGUI(cfg *config.Wrapper, m *model.Model) {
|
||||
opts := cfg.Options()
|
||||
guiCfg := overrideGUIConfig(cfg.GUI(), guiAddress, guiAuthentication, guiAPIKey)
|
||||
|
||||
@@ -650,7 +664,7 @@ func setupGUI(cfg *config.ConfigWrapper, m *model.Model) {
|
||||
}
|
||||
}
|
||||
|
||||
func sanityCheckFolders(cfg *config.ConfigWrapper, m *model.Model) {
|
||||
func sanityCheckFolders(cfg *config.Wrapper, m *model.Model) {
|
||||
nextFolder:
|
||||
for id, folder := range cfg.Folders() {
|
||||
if folder.Invalid != "" {
|
||||
@@ -755,7 +769,7 @@ func setupUPnP() {
|
||||
if len(igds) > 0 {
|
||||
// Configure the first discovered IGD only. This is a work-around until we have a better mechanism
|
||||
// for handling multiple IGDs, which will require changes to the global discovery service
|
||||
igd = igds[0]
|
||||
igd = &igds[0]
|
||||
|
||||
externalPort = setupExternalPort(igd, port)
|
||||
if externalPort == 0 {
|
||||
@@ -779,12 +793,9 @@ func setupExternalPort(igd *upnp.IGD, port int) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// We seed the random number generator with the node ID to get a
|
||||
// repeatable sequence of random external ports.
|
||||
rnd := rand.NewSource(certSeed(cert.Certificate[0]))
|
||||
for i := 0; i < 10; i++ {
|
||||
r := 1024 + int(rnd.Int63()%(65535-1024))
|
||||
err := igd.AddPortMapping(upnp.TCP, r, port, "syncthing", cfg.Options().UPnPLease*60)
|
||||
r := 1024 + predictableRandom.Intn(65535-1024)
|
||||
err := igd.AddPortMapping(upnp.TCP, r, port, fmt.Sprintf("syncthing-%d", r), cfg.Options().UPnPLease*60)
|
||||
if err == nil {
|
||||
return r
|
||||
}
|
||||
@@ -806,7 +817,7 @@ func renewUPnP(port int) {
|
||||
if len(igds) > 0 {
|
||||
// Configure the first discovered IGD only. This is a work-around until we have a better mechanism
|
||||
// for handling multiple IGDs, which will require changes to the global discovery service
|
||||
igd = igds[0]
|
||||
igd = &igds[0]
|
||||
} else {
|
||||
if debugNet {
|
||||
l.Debugln("Failed to discover IGD during UPnP port mapping renewal.")
|
||||
@@ -849,6 +860,17 @@ func renewUPnP(port int) {
|
||||
}
|
||||
|
||||
func resetFolders() {
|
||||
confDir, err := osutil.ExpandTilde(confDir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cfgFile := filepath.Join(confDir, "config.xml")
|
||||
cfg, err := config.Load(cfgFile, myID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
suffix := fmt.Sprintf(".syncthing-reset-%d", time.Now().UnixNano())
|
||||
for _, folder := range cfg.Folders() {
|
||||
if _, err := os.Stat(folder.Path); err == nil {
|
||||
@@ -912,7 +934,7 @@ next:
|
||||
// the certificate and used another name.
|
||||
certName := deviceCfg.CertName
|
||||
if certName == "" {
|
||||
certName = "syncthing"
|
||||
certName = tlsDefaultCommonName
|
||||
}
|
||||
err := remoteCert.VerifyHostname(certName)
|
||||
if err != nil {
|
||||
@@ -926,12 +948,12 @@ next:
|
||||
|
||||
// If rate limiting is set, we wrap the connection in a
|
||||
// limiter.
|
||||
var wr io.Writer = conn
|
||||
wr := io.Writer(conn)
|
||||
if writeRateLimit != nil {
|
||||
wr = &limitedWriter{conn, writeRateLimit}
|
||||
}
|
||||
|
||||
var rd io.Reader = conn
|
||||
rd := io.Reader(conn)
|
||||
if readRateLimit != nil {
|
||||
rd = &limitedReader{conn, readRateLimit}
|
||||
}
|
||||
@@ -953,11 +975,16 @@ next:
|
||||
}
|
||||
}
|
||||
|
||||
events.Default.Log(events.DeviceRejected, map[string]string{
|
||||
"device": remoteID.String(),
|
||||
"address": conn.RemoteAddr().String(),
|
||||
})
|
||||
l.Infof("Connection from %s with unknown device ID %s; ignoring", conn.RemoteAddr(), remoteID)
|
||||
if !cfg.IgnoredDevice(remoteID) {
|
||||
events.Default.Log(events.DeviceRejected, map[string]string{
|
||||
"device": remoteID.String(),
|
||||
"address": conn.RemoteAddr().String(),
|
||||
})
|
||||
l.Infof("Connection from %s with unknown device ID %s", conn.RemoteAddr(), remoteID)
|
||||
} else {
|
||||
l.Infof("Connection from %s with ignored device ID %s", conn.RemoteAddr(), remoteID)
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
@@ -1004,7 +1031,7 @@ func listenTLS(conns chan *tls.Conn, addr string, tlsCfg *tls.Config) {
|
||||
}
|
||||
|
||||
func dialTLS(m *model.Model, conns chan *tls.Conn, tlsCfg *tls.Config) {
|
||||
var delay time.Duration = 1 * time.Second
|
||||
delay := time.Second
|
||||
for {
|
||||
nextDevice:
|
||||
for deviceID, deviceCfg := range cfg.Devices() {
|
||||
@@ -1143,9 +1170,8 @@ func getDefaultConfDir() (string, error) {
|
||||
default:
|
||||
if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" {
|
||||
return filepath.Join(xdgCfg, "syncthing"), nil
|
||||
} else {
|
||||
return osutil.ExpandTilde("~/.config/syncthing")
|
||||
}
|
||||
return osutil.ExpandTilde("~/.config/syncthing")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1233,36 +1259,46 @@ func standbyMonitor() {
|
||||
}
|
||||
|
||||
func autoUpgrade() {
|
||||
var skipped bool
|
||||
interval := time.Duration(cfg.Options().AutoUpgradeIntervalH) * time.Hour
|
||||
timer := time.NewTimer(0)
|
||||
sub := events.Default.Subscribe(events.DeviceConnected)
|
||||
for {
|
||||
if skipped {
|
||||
time.Sleep(interval)
|
||||
} else {
|
||||
skipped = true
|
||||
select {
|
||||
case event := <-sub.C():
|
||||
data, ok := event.Data.(map[string]string)
|
||||
if !ok || data["clientName"] != "syncthing" || upgrade.CompareVersions(data["clientVersion"], Version) != upgrade.Newer {
|
||||
continue
|
||||
}
|
||||
l.Infof("Connected to device %s with a newer version (current %q < remote %q). Checking for upgrades.", data["id"], Version, data["clientVersion"])
|
||||
case <-timer.C:
|
||||
}
|
||||
|
||||
rel, err := upgrade.LatestRelease(IsBeta)
|
||||
if err == upgrade.ErrUpgradeUnsupported {
|
||||
events.Default.Unsubscribe(sub)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
// Don't complain too loudly here; we might simply not have
|
||||
// internet connectivity, or the upgrade server might be down.
|
||||
l.Infoln("Automatic upgrade:", err)
|
||||
timer.Reset(time.Duration(cfg.Options().AutoUpgradeIntervalH) * time.Hour)
|
||||
continue
|
||||
}
|
||||
|
||||
if upgrade.CompareVersions(rel.Tag, Version) <= 0 {
|
||||
if upgrade.CompareVersions(rel.Tag, Version) != upgrade.Newer {
|
||||
// Skip equal, older or majorly newer (incompatible) versions
|
||||
timer.Reset(time.Duration(cfg.Options().AutoUpgradeIntervalH) * time.Hour)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Infof("Automatic upgrade (current %q < latest %q)", Version, rel.Tag)
|
||||
err = upgrade.UpgradeTo(rel)
|
||||
err = upgrade.To(rel)
|
||||
if err != nil {
|
||||
l.Warnln("Automatic upgrade:", err)
|
||||
timer.Reset(time.Duration(cfg.Options().AutoUpgradeIntervalH) * time.Hour)
|
||||
continue
|
||||
}
|
||||
events.Default.Unsubscribe(sub)
|
||||
l.Warnf("Automatically upgraded to version %q. Restarting in 1 minute.", rel.Tag)
|
||||
time.Sleep(time.Minute)
|
||||
stop <- exitUpgrading
|
||||
|
||||
68
cmd/syncthing/random.go
Normal file
68
cmd/syncthing/random.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
cryptoRand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
mathRand "math/rand"
|
||||
)
|
||||
|
||||
// randomCharset contains the characters that can make up a randomString().
|
||||
const randomCharset = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
|
||||
|
||||
// predictableRandom is an RNG that will always have the same sequence. It
|
||||
// will be seeded with the device ID during startup, so that the sequence is
|
||||
// predictable but varies between instances.
|
||||
var predictableRandom = mathRand.New(mathRand.NewSource(42))
|
||||
|
||||
func init() {
|
||||
// The default RNG should be seeded with something good.
|
||||
mathRand.Seed(randomInt64())
|
||||
}
|
||||
|
||||
// randomString returns a string of random characters (taken from
|
||||
// randomCharset) of the specified length.
|
||||
func randomString(l int) string {
|
||||
bs := make([]byte, l)
|
||||
for i := range bs {
|
||||
bs[i] = randomCharset[mathRand.Intn(len(randomCharset))]
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
// randomInt64 returns a strongly random int64, slowly
|
||||
func randomInt64() int64 {
|
||||
var bs [8]byte
|
||||
_, err := io.ReadFull(cryptoRand.Reader, bs[:])
|
||||
if err != nil {
|
||||
panic("randomness failure: " + err.Error())
|
||||
}
|
||||
return seedFromBytes(bs[:])
|
||||
}
|
||||
|
||||
// seedFromBytes calculates a weak 64 bit hash from the given byte slice,
|
||||
// suitable for use a predictable random seed.
|
||||
func seedFromBytes(bs []byte) int64 {
|
||||
h := md5.New()
|
||||
h.Write(bs)
|
||||
s := h.Sum(nil)
|
||||
// The MD5 hash of the byte slice is 16 bytes long. We interpret it as two
|
||||
// uint64s and XOR them together.
|
||||
return int64(binary.BigEndian.Uint64(s[0:]) ^ binary.BigEndian.Uint64(s[8:]))
|
||||
}
|
||||
87
cmd/syncthing/random_test.go
Normal file
87
cmd/syncthing/random_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// 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 (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var predictableRandomTest sync.Once
|
||||
|
||||
func TestPredictableRandom(t *testing.T) {
|
||||
predictableRandomTest.Do(func() {
|
||||
// predictable random sequence is predictable
|
||||
e := 3440579354231278675
|
||||
if v := predictableRandom.Int(); v != e {
|
||||
t.Errorf("Unexpected random value %d != %d", v, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSeedFromBytes(t *testing.T) {
|
||||
// should always return the same seed for the same bytes
|
||||
tcs := []struct {
|
||||
bs []byte
|
||||
v int64
|
||||
}{
|
||||
{[]byte("hello world"), -3639725434188061933},
|
||||
{[]byte("hello worlx"), -2539100776074091088},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
if v := seedFromBytes(tc.bs); v != tc.v {
|
||||
t.Errorf("Unexpected seed value %d != %d", v, tc.v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomString(t *testing.T) {
|
||||
for _, l := range []int{0, 1, 2, 3, 4, 8, 42} {
|
||||
s := randomString(l)
|
||||
if len(s) != l {
|
||||
t.Errorf("Incorrect length %d != %d", len(s), l)
|
||||
}
|
||||
}
|
||||
|
||||
strings := make([]string, 1000)
|
||||
for i := range strings {
|
||||
strings[i] = randomString(8)
|
||||
for j := range strings {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
if strings[i] == strings[j] {
|
||||
t.Errorf("Repeated random string %q", strings[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomInt64(t *testing.T) {
|
||||
ints := make([]int64, 1000)
|
||||
for i := range ints {
|
||||
ints[i] = randomInt64()
|
||||
for j := range ints {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
if ints[i] == ints[j] {
|
||||
t.Errorf("Repeated random int64 %d", ints[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,9 @@ import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"math/big"
|
||||
@@ -35,8 +33,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
tlsRSABits = 3072
|
||||
tlsName = "syncthing"
|
||||
tlsRSABits = 3072
|
||||
tlsDefaultCommonName = "syncthing"
|
||||
)
|
||||
|
||||
func loadCert(dir string, prefix string) (tls.Certificate, error) {
|
||||
@@ -45,15 +43,8 @@ func loadCert(dir string, prefix string) (tls.Certificate, error) {
|
||||
return tls.LoadX509KeyPair(cf, kf)
|
||||
}
|
||||
|
||||
func certSeed(bs []byte) int64 {
|
||||
hf := sha256.New()
|
||||
hf.Write(bs)
|
||||
id := hf.Sum(nil)
|
||||
return int64(binary.BigEndian.Uint64(id))
|
||||
}
|
||||
|
||||
func newCertificate(dir string, prefix string) {
|
||||
l.Infoln("Generating RSA key and certificate...")
|
||||
func newCertificate(dir, prefix, name string) {
|
||||
l.Infof("Generating RSA key and certificate for %s...", name)
|
||||
|
||||
priv, err := rsa.GenerateKey(rand.Reader, tlsRSABits)
|
||||
if err != nil {
|
||||
@@ -66,7 +57,7 @@ func newCertificate(dir string, prefix string) {
|
||||
template := x509.Certificate{
|
||||
SerialNumber: new(big.Int).SetInt64(mr.Int63()),
|
||||
Subject: pkix.Name{
|
||||
CommonName: tlsName,
|
||||
CommonName: name,
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
FROM debian:squeeze
|
||||
MAINTAINER Jakob Borg <jakob@nym.se>
|
||||
|
||||
ENV GOLANG_VERSION 1.4rc2
|
||||
ENV GOLANG_VERSION 1.4
|
||||
|
||||
# SCMs for "go get", gcc for cgo
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates curl gcc libc6-dev make \
|
||||
bzr git mercurial unzip \
|
||||
bzr git mercurial unzip patch \
|
||||
--no-install-recommends \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -17,9 +17,12 @@ RUN curl -sSL https://golang.org/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz \
|
||||
| tar -v -C /usr/local -xz
|
||||
|
||||
ENV PATH /usr/local/go/bin:$PATH
|
||||
RUN mkdir /go
|
||||
ENV GOPATH /go
|
||||
ENV GO386 387
|
||||
ENV GOARM 5
|
||||
ENV PATH /go/bin:$PATH
|
||||
|
||||
RUN mkdir /go
|
||||
WORKDIR /go
|
||||
|
||||
# Use gonative to install native Go for most arch/OS combos
|
||||
@@ -29,15 +32,22 @@ RUN go get github.com/calmh/gonative \
|
||||
&& rm -rf go \
|
||||
&& gonative -version $GOLANG_VERSION
|
||||
|
||||
# Rebuild the special and missing versions
|
||||
# Rebuild the special and missing versions, using patches as appropriate
|
||||
|
||||
RUN mkdir /tmp/patches
|
||||
ADD *.patch /tmp/patches/
|
||||
RUN bash -xec '\
|
||||
cd /usr/local/go ; \
|
||||
for patch in /tmp/patches/*.patch ; do \
|
||||
patch -p0 < "$patch" ; \
|
||||
done \
|
||||
'
|
||||
|
||||
RUN bash -xec '\
|
||||
cd /usr/local/go/src; \
|
||||
for platform in linux/386 freebsd/386 windows/386 linux/arm openbsd/amd64 openbsd/386; do \
|
||||
GOOS=${platform%/*} \
|
||||
GOARCH=${platform##*/} \
|
||||
GOARM=5 \
|
||||
GO386=387 \
|
||||
CGO_ENABLED=0 \
|
||||
./make.bash --no-clean 2>&1; \
|
||||
done \
|
||||
@@ -51,6 +61,11 @@ RUN go get github.com/tools/godep \
|
||||
&& go get github.com/axw/gocov/gocov \
|
||||
&& go get github.com/AlekSi/gocov-xml
|
||||
|
||||
# Install tools "go vet" and "golint"
|
||||
|
||||
RUN go get golang.org/x/tools/cmd/vet \
|
||||
&& go get github.com/golang/lint/golint
|
||||
|
||||
# Build standard library for race
|
||||
|
||||
RUN go install -race std
|
||||
|
||||
22
docker/go-9102.patch
Normal file
22
docker/go-9102.patch
Normal file
@@ -0,0 +1,22 @@
|
||||
--- src/syscall/route_openbsd.go.orig Fri Jul 25 23:38:47 2014
|
||||
+++ src/syscall/route_openbsd.go Fri Jul 25 23:39:20 2014
|
||||
@@ -12,16 +12,16 @@ func (any *anyMessage) toRoutingMessage(b []byte) Rout
|
||||
switch any.Type {
|
||||
case RTM_ADD, RTM_DELETE, RTM_CHANGE, RTM_GET, RTM_LOSING, RTM_REDIRECT, RTM_MISS, RTM_LOCK, RTM_RESOLVE:
|
||||
p := (*RouteMessage)(unsafe.Pointer(any))
|
||||
- return &RouteMessage{Header: p.Header, Data: b[SizeofRtMsghdr:any.Msglen]}
|
||||
+ return &RouteMessage{Header: p.Header, Data: b[p.Header.Hdrlen:any.Msglen]}
|
||||
case RTM_IFINFO:
|
||||
p := (*InterfaceMessage)(unsafe.Pointer(any))
|
||||
- return &InterfaceMessage{Header: p.Header, Data: b[SizeofIfMsghdr:any.Msglen]}
|
||||
+ return &InterfaceMessage{Header: p.Header, Data: b[p.Header.Hdrlen:any.Msglen]}
|
||||
case RTM_IFANNOUNCE:
|
||||
p := (*InterfaceAnnounceMessage)(unsafe.Pointer(any))
|
||||
return &InterfaceAnnounceMessage{Header: p.Header}
|
||||
case RTM_NEWADDR, RTM_DELADDR:
|
||||
p := (*InterfaceAddrMessage)(unsafe.Pointer(any))
|
||||
- return &InterfaceAddrMessage{Header: p.Header, Data: b[SizeofIfaMsghdr:any.Msglen]}
|
||||
+ return &InterfaceAddrMessage{Header: p.Header, Data: b[p.Header.Hdrlen:any.Msglen]}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
27
etc/linux-systemd/README.md
Normal file
27
etc/linux-systemd/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
This directory contains a configuration for running syncthing under the
|
||||
"systemd" service manager on Linux both under either a systemd system service or
|
||||
systemd user service.
|
||||
|
||||
1. Install systemd.
|
||||
|
||||
2. If you are running this as a system level service:
|
||||
|
||||
1. Create the user you will be running the service as (foo in this example).
|
||||
|
||||
2. Copy the syncthing@.service files to /etc/systemd/system
|
||||
|
||||
3. Enable and start the service
|
||||
systemctl enable syncthing@foo.service
|
||||
systemctl start syncthing@foo.service
|
||||
|
||||
3. If you are running this as a user level service:
|
||||
|
||||
1. Log in as the user you will be running the service as
|
||||
|
||||
2. Copy the syncthing.service files to /etc/systemd/user
|
||||
|
||||
3. Enable and start the service
|
||||
systemctl --user enable syncthing.service
|
||||
systemctl --user start syncthing.service
|
||||
|
||||
Log output is sent to the journal.
|
||||
14
etc/linux-systemd/system/syncthing@.service
Normal file
14
etc/linux-systemd/system/syncthing@.service
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Syncthing service for %i
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=%i
|
||||
Environment=STARGS=
|
||||
EnvironmentFile=-/etc/default/syncthing
|
||||
Environment=STNORESTART=yes
|
||||
ExecStart=/usr/bin/syncthing ${STARGS}
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
12
etc/linux-systemd/user/syncthing.service
Normal file
12
etc/linux-systemd/user/syncthing.service
Normal file
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Syncthing service
|
||||
|
||||
[Service]
|
||||
Environment=STARGS=
|
||||
EnvironmentFile=-%h/.config/syncthing/environment
|
||||
Environment=STNORESTART=yes
|
||||
ExecStart=/usr/bin/syncthing ${STARGS}
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=cmdline.target
|
||||
@@ -39,6 +39,8 @@ ul+h5 {
|
||||
|
||||
.panel-title {
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
identicon {
|
||||
@@ -54,6 +56,10 @@ identicon {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.checkbox input[type="checkbox"], .radio input[type="radio"] {
|
||||
float: none; /* issue #1197 */
|
||||
}
|
||||
|
||||
.popover {
|
||||
max-width: none;
|
||||
}
|
||||
@@ -69,6 +75,10 @@ identicon {
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.panel-heading .glyphicon {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -195,3 +205,15 @@ ul.three-columns li, ul.two-columns li {
|
||||
padding-left: 0.5em;
|
||||
text-indent: -0.5em;
|
||||
}
|
||||
|
||||
/** Footer nav on small devices **/
|
||||
|
||||
@media (max-width: 767px) {
|
||||
body {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar-fixed-bottom {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "Ключ API",
|
||||
"About": "Аб праграме",
|
||||
"Add": "Add",
|
||||
"Add Device": "Дадаць прыладу",
|
||||
"Add Folder": "Дадаць каталёг",
|
||||
"Add new folder?": "Add new folder?",
|
||||
"Address": "Адрас",
|
||||
"Addresses": "Адрасы",
|
||||
"Allow Anonymous Usage Reporting?": "Allow Anonymous Usage Reporting?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Automatic upgrades",
|
||||
"Bugs": "Bugs",
|
||||
"CPU Utilization": "CPU Utilization",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Close",
|
||||
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||
"Connection Error": "Connection Error",
|
||||
"Copied from elsewhere": "Copied from elsewhere",
|
||||
"Copied from original": "Copied from original",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
|
||||
"Delete": "Delete",
|
||||
"Device ID": "Device ID",
|
||||
"Device Identification": "Device Identification",
|
||||
"Device Name": "Device Name",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Disconnected",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Download Rate",
|
||||
"Downloaded": "Downloaded",
|
||||
"Downloading": "Downloading",
|
||||
"Edit": "Edit",
|
||||
"Edit Device": "Edit Device",
|
||||
"Edit Folder": "Edit Folder",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "Folder ID",
|
||||
"Folder Master": "Folder Master",
|
||||
"Folder Path": "Folder Path",
|
||||
"Folders": "Folders",
|
||||
"GUI Authentication Password": "GUI Authentication Password",
|
||||
"GUI Authentication User": "GUI Authentication User",
|
||||
"GUI Listen Addresses": "GUI Listen Addresses",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Global Discovery Server",
|
||||
"Global State": "Global State",
|
||||
"Idle": "Idle",
|
||||
"Ignore": "Ignore",
|
||||
"Ignore Patterns": "Ignore Patterns",
|
||||
"Ignore Permissions": "Ignore Permissions",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Introducer": "Introducer",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||
"Keep Versions": "Keep Versions",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Last File Synced",
|
||||
"Last seen": "Last seen",
|
||||
"Later": "Later",
|
||||
"Latest Release": "Latest Release",
|
||||
"Local Discovery": "Local Discovery",
|
||||
"Local State": "Local State",
|
||||
"Maximum Age": "Maximum Age",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||
"Never": "Never",
|
||||
"New Device": "New Device",
|
||||
"New Folder": "New Folder",
|
||||
"No": "No",
|
||||
"No File Versioning": "No File Versioning",
|
||||
"Notice": "Notice",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Restart",
|
||||
"Restart Needed": "Restart Needed",
|
||||
"Restarting": "Restarting",
|
||||
"Reused": "Reused",
|
||||
"Save": "Save",
|
||||
"Scanning": "Scanning",
|
||||
"Select the devices to share this folder with.": "Select the devices to share this folder with.",
|
||||
"Select the folders to share with this device.": "Select the folders to share with this device.",
|
||||
"Settings": "Settings",
|
||||
"Share": "Share",
|
||||
"Share Folder": "Share Folder",
|
||||
"Share Folders With Device": "Share Folders With Device",
|
||||
"Share With Devices": "Share With Devices",
|
||||
"Share this folder?": "Share this folder?",
|
||||
"Shared With": "Shared With",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Short identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Show ID": "Show ID",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Staggered File Versioning",
|
||||
"Start Browser": "Start Browser",
|
||||
"Stopped": "Stopped",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Support / Forum",
|
||||
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
|
||||
"Synchronization": "Synchronization",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Syncthing is restarting.",
|
||||
"Syncthing is upgrading.": "Syncthing is upgrading.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "The aggregated statistics are publicly available at {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
|
||||
"The device ID cannot be blank.": "The device ID cannot be blank.",
|
||||
@@ -124,6 +148,8 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
|
||||
"The rescan interval must be at least 5 seconds.": "The rescan interval must be at least 5 seconds.",
|
||||
"Unknown": "Unknown",
|
||||
"Unshared": "Unshared",
|
||||
"Unused": "Unused",
|
||||
"Up to Date": "Up to Date",
|
||||
"Upgrade To {%version%}": "Upgrade To {{version}}",
|
||||
"Upgrading": "Upgrading",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Yes",
|
||||
"You must keep at least one version.": "You must keep at least one version.",
|
||||
"full documentation": "full documentation",
|
||||
"items": "items"
|
||||
"items": "items",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,28 +1,37 @@
|
||||
{
|
||||
"API Key": "API Ключ",
|
||||
"About": "За Програмата",
|
||||
"Add": "Add",
|
||||
"Add Device": "Добави устройство",
|
||||
"Add Folder": "Добави папка",
|
||||
"Add new folder?": "Add new 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.": "Устройства настроени на introducer компютъра също ще бъдат добавени към този компютър.",
|
||||
"Automatic upgrades": "Automatic upgrades",
|
||||
"Automatic upgrades": "Автоматични ъпдейти",
|
||||
"Bugs": "Бъгове",
|
||||
"CPU Utilization": "Натоварване на Процесора",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Затвори",
|
||||
"Comment, when used at the start of a line": "Коментар, използван в началото на реда",
|
||||
"Compression is recommended in most setups.": "Компресията е препоръчителна в повечето конфигурации.",
|
||||
"Connection Error": "Грешка при Свързването",
|
||||
"Copied from elsewhere": "Копиране от някъде другаде",
|
||||
"Copied from original": "Копиран от оригинала",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Правата запазени © 2014 Jakob Borg и следните Сътрудници:",
|
||||
"Delete": "Изтрий",
|
||||
"Device ID": "Идентификатор на устройство",
|
||||
"Device Identification": "Идентификация на устройство",
|
||||
"Device Name": "Име на устройство",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Прекрати Връзката",
|
||||
"Documentation": "Документация",
|
||||
"Download Rate": "Скорост на Теглене",
|
||||
"Downloaded": "Изтеглен",
|
||||
"Downloading": "Изтегляне",
|
||||
"Edit": "Промени",
|
||||
"Edit Device": "Промени устройство",
|
||||
"Edit Folder": "Промени папка",
|
||||
@@ -34,10 +43,11 @@
|
||||
"File Versioning": "Файлови Версии",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Битовете за права за достъп са игнорирани, когато се проверява за промени. Използвай с файлови системи тип FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Когато syncthing замени или изтрие файл той се премества в .stversions и преименува с дабавени дата и час.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файловете са защитени от промени направени на други устройства, но промени направени на тава устройство ще бъдат синхронизирани до другите устройства.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файловете са защитени от промени направени на други устройства, но промени направени на това устройство ще бъдат синхронизирани с другите устройства.",
|
||||
"Folder ID": "Идентификатор на папка",
|
||||
"Folder Master": "Главна папка",
|
||||
"Folder Path": "Път до папката",
|
||||
"Folders": "Папки",
|
||||
"GUI Authentication Password": "Парола за Потребителския Интерфейс",
|
||||
"GUI Authentication User": "Потребител за Потребителския Интерфейс",
|
||||
"GUI Listen Addresses": "Адрес за Свързване с Потребителския Интерфейс",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Сървър за Глобално Откриване",
|
||||
"Global State": "Глобално състояние",
|
||||
"Idle": "Без Работа",
|
||||
"Ignore": "Ignore",
|
||||
"Ignore Patterns": "Шаблони за Игнориране",
|
||||
"Ignore Permissions": "Игнорирай Права за Достъп",
|
||||
"Incoming Rate Limit (KiB/s)": "Входящ Лимит на Скороста(KiB/s)",
|
||||
"Incoming Rate Limit (KiB/s)": "Входящ Лимит на Скоростта (KiB/s)",
|
||||
"Introducer": "Introducer",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Обратното на даденото условие (пр. не изключвай)",
|
||||
"Keep Versions": "Пази Версии",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Последния синхронизиран файл",
|
||||
"Last seen": "Последно видян",
|
||||
"Later": "Later",
|
||||
"Latest Release": "Най-новата Версия",
|
||||
"Local Discovery": "Локално Откриване",
|
||||
"Local State": "Локално състояние",
|
||||
"Maximum Age": "Максимална Възраст",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Маска на много нива (покрива папки с много нива)",
|
||||
"Never": "Никога",
|
||||
"New Device": "New Device",
|
||||
"New Folder": "New Folder",
|
||||
"No": "Не",
|
||||
"No File Versioning": "Няма Файлови Версии",
|
||||
"Notice": "Известие",
|
||||
@@ -80,23 +96,30 @@
|
||||
"Restart": "Рестартирай",
|
||||
"Restart Needed": "Изискава се Рестартиране",
|
||||
"Restarting": "Рестартиране",
|
||||
"Reused": "Повторно използван",
|
||||
"Save": "Запази",
|
||||
"Scanning": "Сканиране",
|
||||
"Select the devices to share this folder with.": "Избери устройствата, с които да споделиш тази папка.",
|
||||
"Select the folders to share with this device.": "Изберете папките за споделяне с това устройство.",
|
||||
"Settings": "Настройки",
|
||||
"Share": "Share",
|
||||
"Share Folder": "Share Folder",
|
||||
"Share Folders With Device": "Сподели папки с това устройство",
|
||||
"Share With Devices": "Сподели с устройства",
|
||||
"Shared With": "Сподел С",
|
||||
"Share this folder?": "Share this folder?",
|
||||
"Shared With": "Споделена със",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Кратък идентификатор на папката. Трябва да бъде същият на всички компютри.",
|
||||
"Show ID": "Покажи Идентификатора",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Покажи вместо идентификатор на устройството в статус на клъстъра. Ще бъде предлагано на други комютри като име по подразбиране.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Покажи вместо идентификатор на устройството в статус на клъстъра. Ще бъде обновено с името по подразбиране изпратено от другия компютър.",
|
||||
"Shutdown": "Спри Програмата",
|
||||
"Simple File Versioning": "Просто Файлови Версии",
|
||||
"Simple File Versioning": "Опростени Файлови Версии",
|
||||
"Single level wildcard (matches within a directory only)": "Маска на едно ниво (покрива само в папка)",
|
||||
"Source Code": "Сорс Код",
|
||||
"Staggered File Versioning": "Наслагващи се Файлови Версии",
|
||||
"Start Browser": "Стартирай Браузъра",
|
||||
"Stopped": "Спряна",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Помощ / Форум",
|
||||
"Sync Protocol Listen Addresses": "Адрес за слушане на синхронизиращия протокол",
|
||||
"Synchronization": "Синхронизация",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Syncthing се рестартирва",
|
||||
"Syncthing is upgrading.": "Syncthing се обновява.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing изглежда не е включен, или има проблем с интерент връзката. Повторен опит...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"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.": "Полето идентификатор на устройство не може да бъде празно.",
|
||||
@@ -121,9 +145,11 @@
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максималното време да се пазят весрсии (в дни, сложи 0, за да пазиш версии завинаги).",
|
||||
"The number of old versions to keep, per file.": "Броят стари версии, които да бъдат пазени за всеки файл.",
|
||||
"The number of versions must be a number and cannot be blank.": "Броят версии трябва да бъде число и не може да бъде празно.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Интервала на сканиране трябва да бъде не отрицателно число в секунди.",
|
||||
"The rescan interval must be at least 5 seconds.": "Интервала за повторно сканиране трябва да бъде поне 5 секунди.",
|
||||
"Unknown": "Неясен",
|
||||
"Unshared": "Споделянето прекратено",
|
||||
"Unused": "Неизползван",
|
||||
"Up to Date": "Актуален",
|
||||
"Upgrade To {%version%}": "Обновен До {{version}}",
|
||||
"Upgrading": "Обновяване",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Да",
|
||||
"You must keep at least one version.": "Трябва да пазиш поне една версия.",
|
||||
"full documentation": "пълна документация",
|
||||
"items": "артикула"
|
||||
"items": "артикула",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "API klíč",
|
||||
"About": "O aplikaci",
|
||||
"Add": "Přidat",
|
||||
"Add Device": "Přidat přístroj",
|
||||
"Add Folder": "Přidat adresář",
|
||||
"Add new folder?": "Přidat nový adresář?",
|
||||
"Address": "Adresa",
|
||||
"Addresses": "Adresy",
|
||||
"Allow Anonymous Usage Reporting?": "Povolit anonymní hlášení o používání?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Automatický upgrade",
|
||||
"Bugs": "Chyby",
|
||||
"CPU Utilization": "Využití CPU",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Zavřít",
|
||||
"Comment, when used at the start of a line": "Komentář, pokud použito na začátku řádku",
|
||||
"Compression is recommended in most setups.": "Komprese je doporučena pro většinu nastavení.",
|
||||
"Connection Error": "Chyba připojení",
|
||||
"Copied from elsewhere": "Zkopírováno odjinud",
|
||||
"Copied from original": "Zkopírováno z originálu",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg a následující přispěvatelé:",
|
||||
"Delete": "Smazat",
|
||||
"Device ID": "ID přístroje",
|
||||
"Device Identification": "Identifikace přístroje",
|
||||
"Device Name": "Jméno přístroje",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Přístroj {{device}} ({{address}}) žádá o připojení. Chcete ho přidat?",
|
||||
"Devices": "Přístroje",
|
||||
"Disconnected": "Odpojeno",
|
||||
"Documentation": "Dokumentace",
|
||||
"Download Rate": "Rychlost stahování",
|
||||
"Downloaded": "Staženo",
|
||||
"Downloading": "Stahuji",
|
||||
"Edit": "Upravit",
|
||||
"Edit Device": "Upravit přístroj",
|
||||
"Edit Folder": "Upravit adresář",
|
||||
@@ -37,7 +46,8 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Soubory jsou chráněny před změnami na ostatních přístrojích, ale změny provedené z tohoto umístění budou rozeslány na zbytek clusteru.",
|
||||
"Folder ID": "ID adresáře",
|
||||
"Folder Master": "Master adresář",
|
||||
"Folder Path": "Cesta adresáře",
|
||||
"Folder Path": "Cesta k adresáři",
|
||||
"Folders": "Adresáře",
|
||||
"GUI Authentication Password": "Přihlašovací heslo pro GUI",
|
||||
"GUI Authentication User": "Přihlašovací jméno pro GUI",
|
||||
"GUI Listen Addresses": "Adresa naslouchání GUI",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Server globálního oznamování",
|
||||
"Global State": "Všeobecný status",
|
||||
"Idle": "Nečinný",
|
||||
"Ignore": "Ignorovat",
|
||||
"Ignore Patterns": "Ignorované vzory",
|
||||
"Ignore Permissions": "Ignorovat oprávnění",
|
||||
"Incoming Rate Limit (KiB/s)": "Omezení příchozí rychlosti (KiB/s)",
|
||||
"Introducer": "Zavaděč",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Prohození zadané podmínky (např. nevynechat)",
|
||||
"Keep Versions": "Ponechat verze",
|
||||
"Last File Received": "Poslední soubor přijat",
|
||||
"Last File Synced": "Poslední soubor synchronizován",
|
||||
"Last seen": "Naposledy spatřen",
|
||||
"Later": "Později",
|
||||
"Latest Release": "Poslední vydání",
|
||||
"Local Discovery": "Místní oznamování",
|
||||
"Local State": "Místní status",
|
||||
"Maximum Age": "Maximální časový limit",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Víceúrovňový zástupný znak (shoda skrz více úrovní adresářů)",
|
||||
"Never": "Nikdy",
|
||||
"New Device": "Nový přístroj",
|
||||
"New Folder": "Nový adresář",
|
||||
"No": "Ne",
|
||||
"No File Versioning": "Bez verzí souborů",
|
||||
"Notice": "Oznámení",
|
||||
@@ -80,23 +96,30 @@
|
||||
"Restart": "Restart",
|
||||
"Restart Needed": "Je nutný restart",
|
||||
"Restarting": "Restartuji",
|
||||
"Reused": "Opakovaně použité",
|
||||
"Save": "Uložit",
|
||||
"Scanning": "Skenování",
|
||||
"Select the devices to share this folder with.": "Vybrat přístroje se kterými sdílet tento adresář.",
|
||||
"Select the folders to share with this device.": "Vybrat adresáře sdílené tomuto přístroji.",
|
||||
"Settings": "Nastavení",
|
||||
"Share": "Sdílet",
|
||||
"Share Folder": "Sdílet adresář",
|
||||
"Share Folders With Device": "Sdílet adresáře tomuto přístroji",
|
||||
"Share With Devices": "Sdílet s přístroji",
|
||||
"Share this folder?": "Sdílet tento adresář?",
|
||||
"Shared With": "Sdíleno s",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Krátký identifikátor tohoto adresáře. Musí být stejný na všech přístrojích v clusteru.",
|
||||
"Show ID": "Zobrazit ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Zobrazeno místo ID přístroje na náhledu stavu clusteru. Bude odesíláno ostatním přístrojům jako dodatečné výchozí jméno.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Zobrazeno místo ID přístroje na náhledu stavu clusteru. Bude aktualizováno na jméno které přístroj odesílá pokud nebylo vyplněno.",
|
||||
"Shutdown": "Vypnutí",
|
||||
"Shutdown": "Vypnout",
|
||||
"Simple File Versioning": "Jednoduché verze souborů",
|
||||
"Single level wildcard (matches within a directory only)": "Jednoúrovňový zástupný znak (shody pouze uvnitř adresáře)",
|
||||
"Source Code": "Zdrojový kód",
|
||||
"Staggered File Versioning": "Vícenásobné verze souborů",
|
||||
"Start Browser": "Otevřít prohlížeč",
|
||||
"Stopped": "Pozastaveno",
|
||||
"Support": "Podpora",
|
||||
"Support / Forum": "Podpora / Fórum",
|
||||
"Sync Protocol Listen Addresses": "Adresa naslouchání synchronizačního protokolu",
|
||||
"Synchronization": "Synchronizace",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Syncthing se restartuje.",
|
||||
"Syncthing is upgrading.": "Syncthing se aktualizuje.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing se zdá být nefunkční, nebo je problém s připojením k Internetu. Opakuji...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing má nejspíše problém s provedením vašeho požadavku. Pokud problém přetrvává, načtěte znovu data v prohlížeči nebo restartujte Syncthing.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Souhrnné statistiky jsou veřejně dostupné na {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurace byla uložena, ale není aktivována. Pro aktivaci nové konfigurace je třeba restartovat Syncthing.",
|
||||
"The device ID cannot be blank.": "ID přístroje nemůže být prázdné.",
|
||||
@@ -124,6 +148,8 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Interval pro opakování skenování musí být pozitivní číslo.",
|
||||
"The rescan interval must be at least 5 seconds.": "Interval opakování skenování musí být delší než 5 sekund.",
|
||||
"Unknown": "Neznámý",
|
||||
"Unshared": "Nesdílené",
|
||||
"Unused": "Nepoužité",
|
||||
"Up to Date": "Aktuální",
|
||||
"Upgrade To {%version%}": "Aktualizovat na {{version}}",
|
||||
"Upgrading": "Aktualizuji",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Ano",
|
||||
"You must keep at least one version.": "Je třeba ponechat alespoň jednu verzi.",
|
||||
"full documentation": "plná dokumentace",
|
||||
"items": "položky"
|
||||
"items": "položky",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce sdílet adresář \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "API-Schlüssel",
|
||||
"About": "Über Syncthing",
|
||||
"Add": "Hinzufügen",
|
||||
"Add Device": "Gerät hinzufügen",
|
||||
"Add Folder": "Verzeichnis hinzufügen",
|
||||
"Add new folder?": "Neues Verzeichnis hinzufügen?",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adressen",
|
||||
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsberichten erlauben?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Automat. Updates",
|
||||
"Bugs": "Fehler",
|
||||
"CPU Utilization": "Prozessorauslastung",
|
||||
"Changelog": "Versionsinfo",
|
||||
"Close": "Schließen",
|
||||
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
|
||||
"Compression is recommended in most setups.": "Datenkomprimierung ist für die meisten Anwendungen empfohlen",
|
||||
"Connection Error": "Verbindungsfehler",
|
||||
"Copied from elsewhere": "Von woanders kopiert",
|
||||
"Copied from original": "Vom Originial kopiert",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg und folgende Unterstützer:",
|
||||
"Delete": "Löschen",
|
||||
"Device ID": "Geräte ID",
|
||||
"Device Identification": "Gerät Identifikation",
|
||||
"Device Name": "Gerätename",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Gerät {{device}} ({{address}}) möchte sich verbinden. Gerät hinzufügen?",
|
||||
"Devices": "Geräte",
|
||||
"Disconnected": "Getrennt",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Downloadgeschwindigkeit",
|
||||
"Download Rate": "Download",
|
||||
"Downloaded": "Heruntergeladen",
|
||||
"Downloading": "Lädt herunter",
|
||||
"Edit": "Bearbeiten",
|
||||
"Edit Device": "Gerät bearbeiten",
|
||||
"Edit Folder": "Verzeichnis bearbeiten",
|
||||
@@ -33,11 +42,12 @@
|
||||
"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 ein Verzeichnis 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": "Verzeichnispfad",
|
||||
"Folders": "Verzeichnisse",
|
||||
"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",
|
||||
@@ -46,45 +56,57 @@
|
||||
"Global Discovery Server": "Globaler Auffindungsserver",
|
||||
"Global State": "Globaler Status",
|
||||
"Idle": "Untätig",
|
||||
"Ignore": "Ignorieren",
|
||||
"Ignore Patterns": "Ignoriermuster",
|
||||
"Ignore Permissions": "Berechtigungen ignorieren",
|
||||
"Incoming Rate Limit (KiB/s)": "Eingehendes Datenratelimit (KiB/s)",
|
||||
"Introducer": "Verteilergerät",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Umkehrung der angegebenen Bedingung (z.B. schließe nicht aus)",
|
||||
"Keep Versions": "Versionen erhalten",
|
||||
"Last File Received": "Letzte Datei",
|
||||
"Last File Synced": "Letzte Änderung",
|
||||
"Last seen": "Zuletzt online",
|
||||
"Later": "Später",
|
||||
"Latest Release": "Letzte Veröffentlichung",
|
||||
"Local Discovery": "Lokale Auffindung",
|
||||
"Local State": "Lokaler Status",
|
||||
"Maximum Age": "Höchstalter",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Verschachteltes Maskenzeichen (wird für verschachtelte Verzeichnisse verwendet)",
|
||||
"Never": "Nie",
|
||||
"New Device": "Neues Gerät",
|
||||
"New Folder": "Neues Verzeichnis",
|
||||
"No": "Nein",
|
||||
"No File Versioning": "Keine Dateiversionierung",
|
||||
"Notice": "Benachrichtigung",
|
||||
"OK": "Ok",
|
||||
"Notice": "Hinweis",
|
||||
"OK": "OK",
|
||||
"Offline": "Offline",
|
||||
"Online": "Online",
|
||||
"Out Of Sync": "Nicht synchronisiert",
|
||||
"Outgoing Rate Limit (KiB/s)": "Ausgehendes Datenratelimit (KiB/s)",
|
||||
"Override Changes": "Änderungen überschreiben",
|
||||
"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": "Pfad zum Verzeichnis auf dem lokalen Rechner. Wird erzeugt, wenn es nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Pfad in dem die Versionen gespeichert werden sollen (ohne Angabe wird der Ordner .stversions im Verzeichnis verwendet).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Pfad in dem die Versionen gespeichert werden sollen (ohne Angabe wird das Verzeichnis .stversions im Verzeichnis verwendet).",
|
||||
"Please wait": "Bitte warten",
|
||||
"Preview": "Vorschau",
|
||||
"Preview Usage Report": "Vorschau des Nutzungsberichts",
|
||||
"Quick guide to supported patterns": "Schnellanleitung zu den unterstützten Suchstrukturen",
|
||||
"RAM Utilization": "Verwendeter Arbeitsspeicher",
|
||||
"RAM Utilization": "RAM Auslastung",
|
||||
"Rescan": "Überprüfen",
|
||||
"Rescan Interval": "Suchintervall",
|
||||
"Restart": "Neustart",
|
||||
"Restart Needed": "Neustart notwendig",
|
||||
"Restarting": "Wird neu gestartet",
|
||||
"Reused": "Erneut benutzt",
|
||||
"Save": "Speichern",
|
||||
"Scanning": "Suche",
|
||||
"Select the devices to share this folder with.": "Wähle die Geräte aus, mit denen Du dieses Verzeichnis teilen willst.",
|
||||
"Select the folders to share with this device.": "Wähle die Verzeichnisse aus, die du mit diesem Gerät teilen möchtest",
|
||||
"Settings": "Einstellungen",
|
||||
"Share": "Teilen",
|
||||
"Share Folder": "Teile Verzeichnis",
|
||||
"Share Folders With Device": "Teile Verzeichnisse mit diesem Gerät",
|
||||
"Share With Devices": "Teile mit diesen Geräten",
|
||||
"Share this folder?": "Dieses Verzeichnis teilen?",
|
||||
"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",
|
||||
@@ -95,8 +117,9 @@
|
||||
"Single level wildcard (matches within a directory only)": "Einzelnes Maskenzeichen (wird für ein einzelnes Verzeichnis verwendet)",
|
||||
"Source Code": "Quellcode",
|
||||
"Staggered File Versioning": "Stufenweise Dateiversionierung",
|
||||
"Start Browser": "Starte Browser",
|
||||
"Start Browser": "Browser starten",
|
||||
"Stopped": "Gestoppt",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Support / Forum",
|
||||
"Sync Protocol Listen Addresses": "Adresse(n) für das Synchronisierungsprotokoll",
|
||||
"Synchronization": "Synchronisierung",
|
||||
@@ -106,6 +129,7 @@
|
||||
"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 Deiner Internetverbindung. Versuche erneut...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Es scheint als ob Syncthing ein Problem mit der Verarbeitung ihrer Eingabe hat. Bitte laden sie die Seite neu oder führen sie einen Neustart von Syncthing durch, falls das Problem weiterhin besteht.",
|
||||
"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.",
|
||||
@@ -113,7 +137,7 @@
|
||||
"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 a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis ID muss eine kurze Kennung (64 Zeichen oder weniger) sein. Sie kann nur aus Buchstaben, Zahlen und dem Punkt- (.), Bindestrich- (-), und Unterstrich- (_) Zeichen bestehen.",
|
||||
"The folder ID must be unique.": "Die Verzeichnis ID muss eindeutig sein.",
|
||||
"The folder path cannot be blank.": "Der Verzeichnispfad kann nicht leer sein",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Es wird in folgenden Abständen versioniert: in der ersten Stunde wird alle 30 Sekunden eine Version behalten, am ersten Tag eine jede Stunde, in den ersten 30 Tagen eine jeden Tag, danach wird bis zum Höchstalter eine Version pro Woche beibehalten.",
|
||||
@@ -124,12 +148,14 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Das Suchintervall muss eine nicht negative Anzahl von Sekunden sein.",
|
||||
"The rescan interval must be at least 5 seconds.": "Das Suchintervall muss mindestens 5 Sekunden betragen.",
|
||||
"Unknown": "Unbekannt",
|
||||
"Unshared": "Ungeteilt",
|
||||
"Unused": "Ungenutzt",
|
||||
"Up to Date": "Aktuell",
|
||||
"Upgrade To {%version%}": "Update auf {{version}}",
|
||||
"Upgrading": "Wird aktualisiert",
|
||||
"Upload Rate": "Uploadgeschwindigkeit",
|
||||
"Upload Rate": "Upload",
|
||||
"Use Compression": "Benutze Komprimierung",
|
||||
"Use HTTPS for GUI": "Benutze HTTPS für Benutzeroberfläche",
|
||||
"Use HTTPS for GUI": "HTTPS für Benutzeroberfläche benutzen",
|
||||
"Version": "Version",
|
||||
"Versions Path": "Versionierungspfad",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Alte Versionen werden automatisch gelöscht wenn sie älter als das angegebene Höchstalter sind oder die Höchstzahl der Dateien pro Zeitabschnitt überschritten wird.",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du musst mindestens eine Version behalten.",
|
||||
"full documentation": "Komplette Dokumentation",
|
||||
"items": "Einträge"
|
||||
"items": "Einträge",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} möchte das Verzeichnis \"{{folder}}\" teilen."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "API Key",
|
||||
"About": "About",
|
||||
"Add": "Add",
|
||||
"Add Device": "Add Device",
|
||||
"Add Folder": "Add Folder",
|
||||
"Add new folder?": "Add new folder?",
|
||||
"Address": "Address",
|
||||
"Addresses": "Addresses",
|
||||
"Allow Anonymous Usage Reporting?": "Allow Anonymous Usage Reporting?",
|
||||
@@ -11,6 +13,7 @@
|
||||
"Automatic upgrades": "Automatic upgrades",
|
||||
"Bugs": "Bugs",
|
||||
"CPU Utilization": "CPU Utilization",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Close",
|
||||
"Comment, when used at the start of a line": "Comment, when used at the start of a line",
|
||||
"Compression is recommended in most setups.": "Compression is recommended in most setups.",
|
||||
@@ -22,6 +25,8 @@
|
||||
"Device ID": "Device ID",
|
||||
"Device Identification": "Device Identification",
|
||||
"Device Name": "Device Name",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Disconnected",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Download Rate",
|
||||
@@ -51,20 +56,25 @@
|
||||
"Global Discovery Server": "Global Discovery Server",
|
||||
"Global State": "Global State",
|
||||
"Idle": "Idle",
|
||||
"Ignore": "Ignore",
|
||||
"Ignore Patterns": "Ignore Patterns",
|
||||
"Ignore Permissions": "Ignore Permissions",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Introducer": "Introducer",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
|
||||
"Keep Versions": "Keep Versions",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Last File Synced",
|
||||
"Last seen": "Last seen",
|
||||
"Later": "Later",
|
||||
"Latest Release": "Latest Release",
|
||||
"Legend:": "Legend:",
|
||||
"Local Discovery": "Local Discovery",
|
||||
"Local State": "Local State",
|
||||
"Maximum Age": "Maximum Age",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Multi level wildcard (matches multiple directory levels)",
|
||||
"Never": "Never",
|
||||
"New Device": "New Device",
|
||||
"New Folder": "New Folder",
|
||||
"No": "No",
|
||||
"No File Versioning": "No File Versioning",
|
||||
"Notice": "Notice",
|
||||
@@ -72,6 +82,7 @@
|
||||
"Offline": "Offline",
|
||||
"Online": "Online",
|
||||
"Out Of Sync": "Out Of Sync",
|
||||
"Out of Sync Items": "Out of Sync Items",
|
||||
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
|
||||
"Override Changes": "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 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",
|
||||
@@ -90,20 +101,27 @@
|
||||
"Save": "Save",
|
||||
"Scanning": "Scanning",
|
||||
"Select the devices to share this folder with.": "Select the devices to share this folder with.",
|
||||
"Select the folders to share with this device.": "Select the folders to share with this device.",
|
||||
"Settings": "Settings",
|
||||
"Share": "Share",
|
||||
"Share Folder": "Share Folder",
|
||||
"Share Folders With Device": "Share Folders With Device",
|
||||
"Share With Devices": "Share With Devices",
|
||||
"Share this folder?": "Share this folder?",
|
||||
"Shared With": "Shared With",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Short identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Show ID": "Show ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.",
|
||||
"Shutdown": "Shutdown",
|
||||
"Shutdown Complete": "Shutdown Complete",
|
||||
"Simple File Versioning": "Simple File Versioning",
|
||||
"Single level wildcard (matches within a directory only)": "Single level wildcard (matches within a directory only)",
|
||||
"Source Code": "Source Code",
|
||||
"Staggered File Versioning": "Staggered File Versioning",
|
||||
"Start Browser": "Start Browser",
|
||||
"Stopped": "Stopped",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Support / Forum",
|
||||
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
|
||||
"Synchronization": "Synchronization",
|
||||
@@ -113,6 +131,7 @@
|
||||
"Syncthing is restarting.": "Syncthing is restarting.",
|
||||
"Syncthing is upgrading.": "Syncthing is upgrading.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "The aggregated statistics are publicly available at {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
|
||||
"The device ID cannot be blank.": "The device ID cannot be blank.",
|
||||
@@ -147,5 +166,6 @@
|
||||
"Yes": "Yes",
|
||||
"You must keep at least one version.": "You must keep at least one version.",
|
||||
"full documentation": "full documentation",
|
||||
"items": "items"
|
||||
"items": "items",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
}
|
||||
|
||||
169
gui/assets/lang/lang-es.json
Normal file
169
gui/assets/lang/lang-es.json
Normal file
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"API Key": "Clave API",
|
||||
"About": "Acerca de",
|
||||
"Add": "Agregar",
|
||||
"Add Device": "Agregar dispositivo",
|
||||
"Add Folder": "Agregar repositorio",
|
||||
"Add new folder?": "¿Agregar nuevo repositorio?",
|
||||
"Address": "Dirección",
|
||||
"Addresses": "Direcciones",
|
||||
"Allow Anonymous Usage Reporting?": "Permitir reporte anónimo de uso?",
|
||||
"Anonymous Usage Reporting": "Reporte anónimo de uso",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Cualquier dispositivo configurado en un dispositivo introductor será también agregado a este dispositivo.",
|
||||
"Automatic upgrades": "Actualizaciones automáticas",
|
||||
"Bugs": "Errores",
|
||||
"CPU Utilization": "Uso de la CPU",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Cerrar",
|
||||
"Comment, when used at the start of a line": "Comentario, cuando es utilizado al inicio de una línea.",
|
||||
"Compression is recommended in most setups.": "La compresión de datos es recomendada para la mayoría de las configuraciones.",
|
||||
"Connection Error": "Error de conexión",
|
||||
"Copied from elsewhere": "Copiado de otra parte.",
|
||||
"Copied from original": "Copiado del original",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Derechos de autor © 2014 Jakob Borg y los siguientes colaboradores:",
|
||||
"Delete": "Suprimir",
|
||||
"Device ID": "ID del dispositivo",
|
||||
"Device Identification": "Identificación del dispositivo",
|
||||
"Device Name": "Nombre del dispositivo",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositivo {{device}} ({{address}}) se quiere conectar. ¿Agregar nuevo dispositivo?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Desconectado",
|
||||
"Documentation": "Documentación",
|
||||
"Download Rate": "Tasa de descarga",
|
||||
"Downloaded": "Descargado",
|
||||
"Downloading": "Descargando",
|
||||
"Edit": "Editar",
|
||||
"Edit Device": "Editar dispositivo",
|
||||
"Edit Folder": "Editar repositorio",
|
||||
"Editing": "Editando",
|
||||
"Enable UPnP": "Permitir UPnP",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Ingrese las direcciones \"ip:puerto\" separadas por coma, o \"dynamic\" para descubrir automáticamente las direcciones.",
|
||||
"Enter ignore patterns, one per line.": "Añadir patrones de exclusión, uno por línea.",
|
||||
"Error": "Error",
|
||||
"File Versioning": "Control de versiones",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Los permisos de archivo son ignorados al buscar cambios. Usar en sistemas de archivos FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Los archivos son movidos al directorio .stversions y renombrados a versiones marcadas por fecha cuando son reemplazados o eliminados por Syncthing,",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Los archivos están protegidos frente a los cambios realizados en otros dispositivos, peros los cambios realizados en este dispositivo serán envíados al resto del grupo",
|
||||
"Folder ID": "ID del repositorio",
|
||||
"Folder Master": "Repositorio maestro",
|
||||
"Folder Path": "Ruta del repositorio",
|
||||
"Folders": "Repositorios",
|
||||
"GUI Authentication Password": "Contraseña de autenticación de la GUI",
|
||||
"GUI Authentication User": "Usuario de la GUI",
|
||||
"GUI Listen Addresses": "Direcciones de escucha para la GUI.",
|
||||
"Generate": "Generar",
|
||||
"Global Discovery": "Búsqueda en internet",
|
||||
"Global Discovery Server": "Servidor global de identificación",
|
||||
"Global State": "Estado global",
|
||||
"Idle": "Inactivo",
|
||||
"Ignore": "Ignorar",
|
||||
"Ignore Patterns": "Patrones de exclusión",
|
||||
"Ignore Permissions": "Ignorar permisos",
|
||||
"Incoming Rate Limit (KiB/s)": "Límite de velocidad de entrada (KiB/s)",
|
||||
"Introducer": "Introductor",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversión de la condición dada (es decir, no excluir)",
|
||||
"Keep Versions": "Conservar versiones",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Último archivo sincronizado.",
|
||||
"Last seen": "Visto por ultima vez",
|
||||
"Later": "Más tarde",
|
||||
"Latest Release": "Última versión",
|
||||
"Local Discovery": "Búsqueda en red local",
|
||||
"Local State": "Estado local",
|
||||
"Maximum Age": "Edad máxima",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Carácter comodín multinivel (coincide en el directorio y sus subdirectorios)",
|
||||
"Never": "Nunca",
|
||||
"New Device": "Nuevo dispositivo",
|
||||
"New Folder": "Nuevo repositorio",
|
||||
"No": "No",
|
||||
"No File Versioning": "Sin control de versiones de archivos",
|
||||
"Notice": "Aviso",
|
||||
"OK": "OK",
|
||||
"Offline": "Fuera de linea",
|
||||
"Online": "En linea",
|
||||
"Out Of Sync": "Fuera de sincronización",
|
||||
"Outgoing Rate Limit (KiB/s)": "Tasa máxima de envío (KiB/s)",
|
||||
"Override Changes": "Reemplazar los cambios",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta del repositorio en el equipo local. Será creado si no existe. El carácter tilde (~) puede ser utilizado como atajo de ",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta donde serán guardas las versiones (dejar vacío para usar el directorio predifinido \".stversions\" en el repositorio)",
|
||||
"Please wait": "Aguarde por favor",
|
||||
"Preview": "Vista previa",
|
||||
"Preview Usage Report": "Ver reporte de uso",
|
||||
"Quick guide to supported patterns": "Guía rápida sobre los patrones soportados",
|
||||
"RAM Utilization": "Utilización de RAM",
|
||||
"Rescan": "Reescanear",
|
||||
"Rescan Interval": "Intervalo de reescaneo",
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "Es necesario reiniciar",
|
||||
"Restarting": "Reiniciando",
|
||||
"Reused": "Reutilizado",
|
||||
"Save": "Guardar",
|
||||
"Scanning": "Actualización",
|
||||
"Select the devices to share this folder with.": "Seleccione los dispositivos con los cuales compartir este repositorio.",
|
||||
"Select the folders to share with this device.": "Seleccione los repositorios para compartir con este dispositivo.",
|
||||
"Settings": "Configuración",
|
||||
"Share": "Compartir",
|
||||
"Share Folder": "Compartir repositorio",
|
||||
"Share Folders With Device": "Compartir repositorios con dispositivo",
|
||||
"Share With Devices": "Compartir con los dispositivos",
|
||||
"Share this folder?": "¿Compartir este repositorio?",
|
||||
"Shared With": "Compartido con",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador corto para el repositorio. Debe ser el mismo en todos los dispositivos del grupo.",
|
||||
"Show ID": "Mostrar ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrado en lugar de la ID del dispositivo en el estado del grupo. Será sugerido a otros dispositivos como nombre predeterminado opcional.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrado en lugar de la ID del dispositivo en el estado del grupo. Si se deja en blanco, será usado el nombre sugerido por el dispositivo.",
|
||||
"Shutdown": "Apagar",
|
||||
"Simple File Versioning": "Versiones simple de archivos",
|
||||
"Single level wildcard (matches within a directory only)": "Carácter comodín de un solo nivel (coincide sólo dentro de un directorio)",
|
||||
"Source Code": "Código fuente",
|
||||
"Staggered File Versioning": "Versiones del archivo escalonado",
|
||||
"Start Browser": "Iniciar navegador",
|
||||
"Stopped": "Parado",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Soporte / Foro",
|
||||
"Sync Protocol Listen Addresses": "Dirección de escucha del protocolo de sincronización",
|
||||
"Synchronization": "Sincronización",
|
||||
"Syncing": "Sincronización",
|
||||
"Syncthing has been shut down.": "La sincronización esta apagada",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing incluye los siguientes softwares o partes de ellos:",
|
||||
"Syncthing is restarting.": "Syncthing está reiniciando.",
|
||||
"Syncthing is upgrading.": "Syncthing se está actualizando.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece estar apagado, o hay un problema con su conexión de Internet. Reintentando...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing parece estar experimentando un problema al procesar su solicitud. Por favor, recargue el navegador o reinicie Syncthing si el problema persiste.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Las estadísticas acumuladas están disponibles públicamente en {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido guardada pero no activada.\nSyncthing debe reiniciarse para activar la nueva configuración.",
|
||||
"The device ID cannot be blank.": "La ID del dispositivo no puede estar en blanco.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "La ID del dispositivo a introducir se puede encontrar en la opción de menú \"Edición > Mostrar ID\" en el otro dispositivo. Espacios y guiones son opcionales (ignorados).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "El informe de uso se envía encriptado diariamente. Se utiliza para hacer un seguimiento de plataformas comunes, tamaño de repositorios y versiones de la aplicación. Si el conjunto de datos cambia será notificado mediante este dialogo nuevamente.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "La ID del dispositivo introducida no es válida. Debe ser una cadena de 52 o 56 caracteres consistente en letras y números, con espacios y guiones opcionales.",
|
||||
"The folder ID cannot be blank.": "La ID del repositorio no puede estar en blanco.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "La ID del repositorio debe ser un identificador corto (64 caracteres o menos) consistente solamente en letras, números, punto (.), guion (-) y guion bajo (_).",
|
||||
"The folder ID must be unique.": "La ID del repositorio debe ser única.",
|
||||
"The folder path cannot be blank.": "La ruta del repositorio no puede estar vacía.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Los siguientes intervalos se utilizan: para la primera hora una versión se mantiene cada 30 segundos, para el primer día de una versión se mantiene cada hora, durante los primeros 30 días de la versión se mantiene todos los días, hasta que la edad máxima de una versión se mantiene cada semana.",
|
||||
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar en blanco.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El tiempo máximo para mantener una versión (en días, establece en 0 para mantener versiones para siempre).",
|
||||
"The number of old versions to keep, per file.": "El numero de versiones anteriores a conservar, por archivo.",
|
||||
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "El intervalo de reescaneo debe ser un número no negativo de segundos.",
|
||||
"The rescan interval must be at least 5 seconds.": "El intervalo de reescaneo debe ser al menos de 5 segundos.",
|
||||
"Unknown": "Desconocido",
|
||||
"Unshared": "No compartido",
|
||||
"Unused": "No utilizado",
|
||||
"Up to Date": "Actualizado",
|
||||
"Upgrade To {%version%}": "Actualizar a {{version}}",
|
||||
"Upgrading": "Actualizando",
|
||||
"Upload Rate": "Tasa de subida",
|
||||
"Use Compression": "Usar compresión",
|
||||
"Use HTTPS for GUI": "Usar HTTPS para la GUI",
|
||||
"Version": "Versión",
|
||||
"Versions Path": "Ruta de versiones",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Las versiones se eliminan automáticamente si son mayores de la edad máxima o mayor que el número de archivos permitidos en un intervalo.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Al agregar un nuevo dispositivo, tenga en cuenta que este dispositivo se debe agregar en el otro lado también.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Al agregar un nuevo repositorio, tenga en cuenta que la ID del repositorio se utiliza para conectar los repositorios entre dispositivos. Se distingue entre mayúsculas y minúsculas y debe ser exactamente igual en todos los dispositivos.",
|
||||
"Yes": "Sí",
|
||||
"You must keep at least one version.": "Debe mantener al menos una versión",
|
||||
"full documentation": "documentación completa",
|
||||
"items": "Ítems",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir repositorio \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "Clé API",
|
||||
"About": "À propos",
|
||||
"Add": "Ajouter",
|
||||
"Add Device": "Ajouter un périphérique",
|
||||
"Add Folder": "Ajouter un répertoire",
|
||||
"Add new folder?": "Ajouter un nouveau répertoire ?",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresses",
|
||||
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation ?",
|
||||
@@ -11,25 +13,32 @@
|
||||
"Automatic upgrades": "Mises à jour automatiques",
|
||||
"Bugs": "Bugs",
|
||||
"CPU Utilization": "Utilisation du CPU",
|
||||
"Changelog": "Nouveautés",
|
||||
"Close": "Fermer",
|
||||
"Comment, when used at the start of a line": "Commentaire, lorsque utilisé en début de ligne",
|
||||
"Compression is recommended in most setups.": "La compression est recommandée pour la plupart des configurations.",
|
||||
"Connection Error": "Erreur de connexion",
|
||||
"Copied from elsewhere": "Copié d'ailleurs",
|
||||
"Copied from original": "Copié de l'original",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants :",
|
||||
"Delete": "Supprimer",
|
||||
"Device ID": "ID du périphérique",
|
||||
"Device Identification": "Identification de l'appareil",
|
||||
"Device Name": "Nom du périphérique",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "La machine {{device}} ({{address}}) veut se connecter. Voulez-vous ajouter cette machine ?",
|
||||
"Devices": "Machine",
|
||||
"Disconnected": "Déconnecté",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Débit de réception",
|
||||
"Downloaded": "Téléchargé",
|
||||
"Downloading": "En cours de téléchargement",
|
||||
"Edit": "Éditer",
|
||||
"Edit Device": "Éditer le périphérique",
|
||||
"Edit Folder": "Éditer le répertoire",
|
||||
"Editing": "Édition",
|
||||
"Enable UPnP": "Activer l'UPnP",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses \"ip:port\" séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
|
||||
"Enter ignore patterns, one per line.": "Entrer les modèles à ignorer, un par ligne.",
|
||||
"Enter ignore patterns, one per line.": "Entrer les masques de filtrage, un par ligne.",
|
||||
"Error": "Erreur",
|
||||
"File Versioning": "Versions de fichier",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Les permissions de fichier sont ignorées lors de la recherche de changements. À utiliser sur les systèmes de fichiers de type FAT.",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "ID du répertoire",
|
||||
"Folder Master": "Répertoire maître",
|
||||
"Folder Path": "Chemin du répertoire",
|
||||
"Folders": "Répertoires",
|
||||
"GUI Authentication Password": "Mot de passe d'authentification GUI",
|
||||
"GUI Authentication User": "Utilistateur autorisé GUI",
|
||||
"GUI Listen Addresses": "Adresse du GUI",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Serveur global de recherche",
|
||||
"Global State": "État global",
|
||||
"Idle": "Au repos",
|
||||
"Ignore": "Ignorer",
|
||||
"Ignore Patterns": "Modèles à éviter",
|
||||
"Ignore Permissions": "Ignorer les permissions",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite du débit entrant (KiB/s)",
|
||||
"Introducer": "Initiateur",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inverser la condition donnée (i.e. ne pas exclure)",
|
||||
"Keep Versions": "Conserver les versions",
|
||||
"Last File Received": "Dernier fichier reçu",
|
||||
"Last File Synced": "Dernier fichier synchronisé",
|
||||
"Last seen": "Dernière apparition",
|
||||
"Later": "Plus tard",
|
||||
"Latest Release": "Dernière version",
|
||||
"Local Discovery": "Recherche locale",
|
||||
"Local State": "État local",
|
||||
"Maximum Age": "Ancienneté maximum",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Astérisque à plusieurs niveaux (correspond aux répertoires et sous-répertoires)",
|
||||
"Never": "Jamais",
|
||||
"New Device": "Nouvelle machine",
|
||||
"New Folder": "Nouveau répertoire",
|
||||
"No": "Non",
|
||||
"No File Versioning": "Pas de version de fichier",
|
||||
"Notice": "Notification",
|
||||
@@ -73,30 +89,37 @@
|
||||
"Please wait": "Merci de patienter",
|
||||
"Preview": "Aperçu",
|
||||
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
|
||||
"Quick guide to supported patterns": "Guide rapide des modèles supportés",
|
||||
"Quick guide to supported patterns": "Guide rapide des masques supportés",
|
||||
"RAM Utilization": "Utilisation de la RAM",
|
||||
"Rescan": "Rescanner",
|
||||
"Rescan Interval": "Intervalle de scan",
|
||||
"Restart": "Redémarrer",
|
||||
"Restart Needed": "Redémarrage nécessaire",
|
||||
"Restarting": "Redémarrage",
|
||||
"Reused": "Réutilisé",
|
||||
"Save": "Sauver",
|
||||
"Scanning": "En cours de scan",
|
||||
"Select the devices to share this folder with.": "Sélectionner les appareils avec qui partager ce répertoire.",
|
||||
"Select the devices to share this folder with.": "Sélectionner les machines avec qui partager ce répertoire.",
|
||||
"Select the folders to share with this device.": "Sélectionner les répertoires à partager avec cette machine.",
|
||||
"Settings": "Configuration",
|
||||
"Share With Devices": "Partager avec les périphériques",
|
||||
"Share": "Partager",
|
||||
"Share Folder": "Partager le répertoire",
|
||||
"Share Folders With Device": "Partager des répertoires avec des machines",
|
||||
"Share With Devices": "Partager avec les machines",
|
||||
"Share this folder?": "Voulez-vous partager ce répertoire ?",
|
||||
"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.",
|
||||
"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 machines du groupe.",
|
||||
"Show ID": "Montrer l'ID",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de l'appareil dans le statut du groupe. Sera proposé aux autres nœuds comme un nom par défaut optionnel.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de l'appareil dans le statut du groupe. Si laissé vide, il sera changé par le nom proposé par l'appareil.",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de la machine dans le groupe. Sera proposé aux autres machines comme nom optionnel par défaut.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de la machine dans le groupe. Si laissé vide, il sera mis à jour par le nom proposé par la machine distante.",
|
||||
"Shutdown": "Éteindre",
|
||||
"Simple File Versioning": "Versions simples de fichier",
|
||||
"Simple File Versioning": "Suivi simple des versions de fichier",
|
||||
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à l’intérieur du répertoire)",
|
||||
"Source Code": "Code source",
|
||||
"Staggered File Versioning": "Versions échelonnées de fichier",
|
||||
"Start Browser": "Démarrer le navigateur web",
|
||||
"Stopped": "Arrêté",
|
||||
"Support": "Aide",
|
||||
"Support / Forum": "Aide / Forum",
|
||||
"Sync Protocol Listen Addresses": "Adresse du protocole de synchronisation",
|
||||
"Synchronization": "Synchronisation",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
|
||||
"Syncthing is upgrading.": "Syncthing est cours de mise à jour.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble éprouver des difficultés à appliquer votre requête. Si le problème persiste, veuillez rafraîchir votre navigateur ou redémarrer Syncthing.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été sauvée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
|
||||
"The device ID cannot be blank.": "L'ID de l'appareil ne peut être vide.",
|
||||
@@ -120,10 +144,12 @@
|
||||
"The maximum age must be a number and cannot be blank.": "L'ancienneté maximum doit être un nombre et ne peut être vide.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Le temps maximum de conservation d'une version (en jours, mettre à 0 pour conserver les versions pour toujours)",
|
||||
"The 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 number of versions must be a number and cannot be blank.": "Le nombre de versions doit être numérique, et ne peut pas être vide.",
|
||||
"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",
|
||||
"Unshared": "Non partagé",
|
||||
"Unused": "Non utilisé",
|
||||
"Up to Date": "Synchronisation à jour",
|
||||
"Upgrade To {%version%}": "Mettre à jour vers {{version}}",
|
||||
"Upgrading": "Mise à jour de Syncthing",
|
||||
@@ -132,11 +158,12 @@
|
||||
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
|
||||
"Version": "Version",
|
||||
"Versions Path": "Emplacement des versions",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions sont supprimées automatiquement si celles-ci sont plus anciennes que l'ancienneté maximum ou que leur nombre est supérieur au nombre autorisé dans une intervale.",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions seront supprimées automatiquement, si elles dépassent la durée maximum de conservation, ou si leur nombre est supérieur à la valeur autorisée dans l'intervalle.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Lorsqu'un appareil est ajouté, gardez à l'esprit que cet appareil doit aussi être ajouté de l'autre coté.",
|
||||
"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.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que son ID est utilisé pour lier les répertoires à travers les appareils. Les ID sont sensibles à la casse et doivent être identiques à travers tous les nœuds.",
|
||||
"Yes": "Oui",
|
||||
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
||||
"full documentation": "documentation complète",
|
||||
"items": "éléments"
|
||||
"items": "éléments",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le répertoire \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "API kulcs",
|
||||
"About": "Névjegy",
|
||||
"Add": "Add",
|
||||
"Add Device": "Eszköz hozzáadása",
|
||||
"Add Folder": "Mappa hozzáadása",
|
||||
"Add new folder?": "Add new folder?",
|
||||
"Address": "Cím",
|
||||
"Addresses": "Címek",
|
||||
"Allow Anonymous Usage Reporting?": "Engedélyezed a névtelen felhasználási adatok küldését?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Automatikus frissítés",
|
||||
"Bugs": "Hibák",
|
||||
"CPU Utilization": "Processzor használat",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Bezárás",
|
||||
"Comment, when used at the start of a line": "Megjegyzés, a sor elején használva",
|
||||
"Compression is recommended in most setups.": "A tömörítés a a legtöbb esetben ajánlott",
|
||||
"Connection Error": "Kapcsolódási hiba",
|
||||
"Copied from elsewhere": "Másolva máshonnan",
|
||||
"Copied from original": "Másolva az eredetiről",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg és az alábbi Közreműködők",
|
||||
"Delete": "Törlés",
|
||||
"Device ID": "Eszköz azonosító",
|
||||
"Device Identification": "Eszköz azonosító",
|
||||
"Device Name": "Eszköz neve",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Kapcsolat bontva",
|
||||
"Documentation": "Dokumentáció",
|
||||
"Download Rate": "Letöltési sebesség",
|
||||
"Downloaded": "Letöltve",
|
||||
"Downloading": "Letöltés",
|
||||
"Edit": "Szerkesztés",
|
||||
"Edit Device": "Eszköz szerkesztése",
|
||||
"Edit Folder": "Mappa szerkesztése",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "Mappa azonosító",
|
||||
"Folder Master": "Központi mappa",
|
||||
"Folder Path": "Mappa elérési útja",
|
||||
"Folders": "Mappák",
|
||||
"GUI Authentication Password": "Grafikus felület jelszava",
|
||||
"GUI Authentication User": "Grafikus felület felhasználó neve ",
|
||||
"GUI Listen Addresses": "Grafikus felület címe",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Globális felfedező szerver",
|
||||
"Global State": "Globális állapot",
|
||||
"Idle": "Tétlen",
|
||||
"Ignore": "Ignore",
|
||||
"Ignore Patterns": "Figyelmen kívül hagyás",
|
||||
"Ignore Permissions": "Jogosultságok figyelmen kívül hagyása",
|
||||
"Incoming Rate Limit (KiB/s)": "Bejövő sebesség korlát (KIB/mp)",
|
||||
"Introducer": "Bevezető",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "A feltétel ellentéte (pl. ki nem hagyás)",
|
||||
"Keep Versions": "Megtartott verziók",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Utolsó szinkronizált fájl",
|
||||
"Last seen": "Utoljára látva",
|
||||
"Later": "Later",
|
||||
"Latest Release": "Utolsó kiadás",
|
||||
"Local Discovery": "Helyi felfedezés",
|
||||
"Local State": "Helyi állapot",
|
||||
"Maximum Age": "Maximális kor",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Több szintű helyettesítő karakter (több könyvtár szintre érvényesül)",
|
||||
"Never": "Soha",
|
||||
"New Device": "New Device",
|
||||
"New Folder": "New Folder",
|
||||
"No": "Nem",
|
||||
"No File Versioning": "Nincs fájl verziózás",
|
||||
"Notice": "Megjegyzés",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Újraindítás",
|
||||
"Restart Needed": "Újraindítás szükséges",
|
||||
"Restarting": "Újraindulás",
|
||||
"Reused": "Újrafelhasználva",
|
||||
"Save": "Mentés",
|
||||
"Scanning": "Átnézés",
|
||||
"Select the devices to share this folder with.": "Válaszd ki az eszközöket amelyekkel meg szeretnéd osztani a mappát",
|
||||
"Select the folders to share with this device.": "Válaszd ki a mappákat amiket meg szeretnél osztani ezzel az eszközzel",
|
||||
"Settings": "Beállítások",
|
||||
"Share": "Share",
|
||||
"Share Folder": "Share Folder",
|
||||
"Share Folders With Device": "Mappák megosztása az eszközzel",
|
||||
"Share With Devices": "Megosztás más eszközzel",
|
||||
"Share this folder?": "Share this folder?",
|
||||
"Shared With": "Megosztva ezekkel:",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Rövid azonosító. Minden megosztott eszközön azonosnak kell lennie.",
|
||||
"Show ID": "Azonosító mutatása",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Többszintű fájl verziókövetés",
|
||||
"Start Browser": "Böngésző indítása",
|
||||
"Stopped": "Leállítva",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Támogatás / Fórum",
|
||||
"Sync Protocol Listen Addresses": "Szinkronizációs protokoll címe",
|
||||
"Synchronization": "Szinkronizálás",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Syncthing újraindul",
|
||||
"Syncthing is upgrading.": "Syncthing frissül",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Úgy tűnik, hogy a Syncthing nem működik, vagy valami probléma van az hálózati kapcsolattal. Újra próbálom...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Az összevont statisztikák nyilvánosan elérhetők a {{url}} címen.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A beállítások elmentésre kerültek, de nem lettek aktiválva. Indítsd újra a Syncthing-et, hogy aktiváld őket.",
|
||||
"The device ID cannot be blank.": "Az eszköz azonosító nem lehet üres.",
|
||||
@@ -124,6 +148,8 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Az átnézési intervallum nullánál nagyobb másodperc érték kell legyen",
|
||||
"The rescan interval must be at least 5 seconds.": "Az átnézési intervallumnak legalább 5 másodpercnek kell lennie.",
|
||||
"Unknown": "Ismeretlen",
|
||||
"Unshared": "Nincs megosztva",
|
||||
"Unused": "Nincs használatban",
|
||||
"Up to Date": "Friss",
|
||||
"Upgrade To {%version%}": "Frissítés a {{version}} verzióra",
|
||||
"Upgrading": "Frissítés",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Igen",
|
||||
"You must keep at least one version.": "Legalább egy verziót meg kell tartanod",
|
||||
"full documentation": "teljes dokumentáció",
|
||||
"items": "tételek"
|
||||
"items": "tételek",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "Chiave API",
|
||||
"About": "Informazioni",
|
||||
"Add": "Aggiungi",
|
||||
"Add Device": "Aggiungi Dispositivo",
|
||||
"Add Folder": "Aggiungi Cartella",
|
||||
"Add new folder?": "Aggiungere una nuova cartella?",
|
||||
"Address": "Indirizzo",
|
||||
"Addresses": "Indirizzi",
|
||||
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Aggiornamenti automatici",
|
||||
"Bugs": "Bug",
|
||||
"CPU Utilization": "Utilizzo CPU",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Chiudi",
|
||||
"Comment, when used at the start of a line": "Per commentare, va inserito all'inizio di una riga",
|
||||
"Compression is recommended in most setups.": "La compressione è raccomandata nella maggior parte delle configurazioni.",
|
||||
"Connection Error": "Errore di Connessione",
|
||||
"Copied from elsewhere": "Copiato da qualche altra parte",
|
||||
"Copied from original": "Copiato dall'originale",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg e i seguenti Collaboratori:",
|
||||
"Delete": "Elimina",
|
||||
"Device ID": "ID Dispositivo",
|
||||
"Device Identification": "Identificazione Dispositivo",
|
||||
"Device Name": "Nome Dispositivo",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Il dispositivo {{device}} ({{address}}) chiede di connettersi. Aggiungere il nuovo dispositivo?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Disconnesso",
|
||||
"Documentation": "Documentazione",
|
||||
"Download Rate": "Velocità Download",
|
||||
"Downloaded": "Scaricato",
|
||||
"Downloading": "Sto scaricando",
|
||||
"Edit": "Modifica",
|
||||
"Edit Device": "Modifica Dispositivo",
|
||||
"Edit Folder": "Modifica Cartella",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "ID Cartella",
|
||||
"Folder Master": "Cartella Principale",
|
||||
"Folder Path": "Percorso Cartella",
|
||||
"Folders": "Cartelle",
|
||||
"GUI Authentication Password": "Password di Autenticazione dell'Utente",
|
||||
"GUI Authentication User": "Utente dell'Interfaccia Grafica",
|
||||
"GUI Listen Addresses": "Indirizzi dell'Interfaccia Grafica",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Server di Ricerca Globale",
|
||||
"Global State": "Stato Globale",
|
||||
"Idle": "Inattivo",
|
||||
"Ignore": "Ignora",
|
||||
"Ignore Patterns": "Schemi Esclusione File",
|
||||
"Ignore Permissions": "Ignora Permessi",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite Velocità in Ingresso (KiB/s)",
|
||||
"Introducer": "Introduttore",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversione della condizione indicata (ad es. non escludere)",
|
||||
"Keep Versions": "Versioni Mantenute",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Ultimo File Sincronizzato",
|
||||
"Last seen": "Ultima connessione",
|
||||
"Later": "Più Tardi",
|
||||
"Latest Release": "Ultima Versione",
|
||||
"Local Discovery": "Individuazione Locale",
|
||||
"Local State": "Stato Locale",
|
||||
"Maximum Age": "Durata Massima",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Metacarattere multi-livello (corrisponde alle cartelle e alle sotto-cartelle)",
|
||||
"Never": "Mai",
|
||||
"New Device": "Nuovo Dispositivo",
|
||||
"New Folder": "Nuova Cartella",
|
||||
"No": "No",
|
||||
"No File Versioning": "Nessun Controllo Versione",
|
||||
"Notice": "Avviso",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Riavvia",
|
||||
"Restart Needed": "Riavvio Necessario",
|
||||
"Restarting": "Riavvio",
|
||||
"Reused": "Riutilizzato",
|
||||
"Save": "Salva",
|
||||
"Scanning": "Scansione in corso",
|
||||
"Select the devices to share this folder with.": "Seleziona i dispositivi con i quali condividere questa cartella.",
|
||||
"Select the folders to share with this device.": "Seleziona le cartelle da condividere con questo dispositivo.",
|
||||
"Settings": "Impostazioni",
|
||||
"Share": "Condividi",
|
||||
"Share Folder": "Condividi la Cartella",
|
||||
"Share Folders With Device": "Condividi Cartelle con il Dispositivo",
|
||||
"Share With Devices": "Condividi con i Dispositivi",
|
||||
"Share this folder?": "Vuoi condividere questa cartella?",
|
||||
"Shared With": "Condiviso Con",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Breve identificatore della cartella. Deve essere lo stesso su tutti i dispositivi del cluster.",
|
||||
"Show ID": "Mostra ID",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Controllo Versione Cadenzato",
|
||||
"Start Browser": "Avvia Browser",
|
||||
"Stopped": "Fermato",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Supporto / Forum",
|
||||
"Sync Protocol Listen Addresses": "Indirizzi del Protocollo di Sincronizzazione",
|
||||
"Synchronization": "Sincronizzazione",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Riavvio di Syncthing in corso.",
|
||||
"Syncthing is upgrading.": "Aggiornamento di Syncthing in corso.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing sembra inattivo, oppure c'è un problema con la tua connessione a Internet. Nuovo tentativo…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Le statistiche aggregate sono disponibili pubblicamente su {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Devi riavviare Syncthing per attivare la nuova configurazione.",
|
||||
"The device ID cannot be blank.": "L'ID del dispositivo non può essere vuoto.",
|
||||
@@ -121,9 +145,11 @@
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La durata massima di una versione (in giorni, imposta a 0 per mantenere le versioni per sempre).",
|
||||
"The number of old versions to keep, per file.": "Il numero di vecchie versioni da mantenere, per file.",
|
||||
"The number of versions must be a number and cannot be blank.": "Il numero di versioni dev'essere un numero e non può essere vuoto.",
|
||||
"The 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'intervallo di scansione deve essere un numero superiore a zero secondi.",
|
||||
"The rescan interval must be at least 5 seconds.": "L'intervallo di scansione non può essere inferiore a 5 secondi.",
|
||||
"Unknown": "Sconosciuto",
|
||||
"Unshared": "Non Condiviso",
|
||||
"Unused": "Non Utilizzato",
|
||||
"Up to Date": "Sincronizzato",
|
||||
"Upgrade To {%version%}": "Aggiorna alla {{version}}",
|
||||
"Upgrading": "Aggiornamento",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Sì",
|
||||
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
|
||||
"full documentation": "documentazione completa",
|
||||
"items": "elementi"
|
||||
"items": "elementi",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vuole condividere la cartella \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "API raktas",
|
||||
"About": "Apie programą",
|
||||
"Add": "Pridėti",
|
||||
"Add Device": "Pridėti įrenginį",
|
||||
"Add Folder": "Pridėti aplanką",
|
||||
"Add new folder?": "Pridėti naują aplanką?",
|
||||
"Address": "Adresas",
|
||||
"Addresses": "Adresai",
|
||||
"Allow Anonymous Usage Reporting?": "Siųsti anonimišką vartojimo ataskaitą?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Automatiniai atnaujinimai",
|
||||
"Bugs": "Klaidos",
|
||||
"CPU Utilization": "Procesoriaus panaudojimas",
|
||||
"Changelog": "Pasikeitimai",
|
||||
"Close": "Uždaryti",
|
||||
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
|
||||
"Compression is recommended in most setups.": "Daugumoje atvejų spaudimas rekomenduojamas.",
|
||||
"Connection Error": "Susijungimo klaida",
|
||||
"Copied from elsewhere": "Nukopijuota iš betkur",
|
||||
"Copied from original": "Nukopijuota iš originalo",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Visos teisės saugomos © 2014 Jakob Borg ir šių bendraautorių:",
|
||||
"Delete": "Trinti",
|
||||
"Device ID": "Įrenginio ID",
|
||||
"Device Identification": "Įrenginio identifikacija",
|
||||
"Device Name": "Įrenginio pavadinimas",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Įrenginys {{device}} ({{address}}) nori prisijungti. Pridėti naują įrenginį?",
|
||||
"Devices": "Įrenginiai",
|
||||
"Disconnected": "Atsijungęs",
|
||||
"Documentation": "Aprašymas",
|
||||
"Download Rate": "Parsisiuntimo greitis",
|
||||
"Downloaded": "Parsisiųstas",
|
||||
"Downloading": "Siunčiama",
|
||||
"Edit": "Redaguoti",
|
||||
"Edit Device": "Keisti įrenginį",
|
||||
"Edit Folder": "Keisti aplanką",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "Aplanko ID",
|
||||
"Folder Master": "Aplanko vadovas",
|
||||
"Folder Path": "Kelias iki apkanko",
|
||||
"Folders": "Aplankai",
|
||||
"GUI Authentication Password": "Valdymo skydelio slaptažodis",
|
||||
"GUI Authentication User": "Valdymo skydelio vartotojo vardas",
|
||||
"GUI Listen Addresses": "Valdymo skydelio adresas",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Visuotinio matomumo serveris",
|
||||
"Global State": "Visuotinė būsena",
|
||||
"Idle": "Laisvas",
|
||||
"Ignore": "Ignoruoti",
|
||||
"Ignore Patterns": "Nepaisyti šablonų",
|
||||
"Ignore Permissions": "Nepaisyti failų prieigos leidimų",
|
||||
"Incoming Rate Limit (KiB/s)": "Įeinančio srauto maksimalus greitis (KiB/s)",
|
||||
"Introducer": "Supažindintojas",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Apversti sąlygas (pvz.: nenustoti naudoti)",
|
||||
"Keep Versions": "Saugojamų versijų kiekis",
|
||||
"Last File Received": "Paskutinis priimtas failas",
|
||||
"Last File Synced": "Paskutinis gautas failas",
|
||||
"Last seen": "Paskutinį kartą matytas",
|
||||
"Later": "Vėliau",
|
||||
"Latest Release": "Paskutinė versija",
|
||||
"Local Discovery": "Vietinis matomumas",
|
||||
"Local State": "Vietinė būsena",
|
||||
"Maximum Age": "Maksimalus amžius",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Keletos lygių pakaitos (atitinka keletą direktorijų lygių)",
|
||||
"Never": "Niekada",
|
||||
"New Device": "Naujas įrenginys",
|
||||
"New Folder": "Naujas aplankas",
|
||||
"No": "Ne",
|
||||
"No File Versioning": "Nėra versijų valdymo",
|
||||
"Notice": "Įspėjimas",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Perleisti",
|
||||
"Restart Needed": "Reikalingas perleidimas",
|
||||
"Restarting": "Persileidžia",
|
||||
"Reused": "Pakartotinas",
|
||||
"Save": "Išsaugoti",
|
||||
"Scanning": "Skenuojama",
|
||||
"Select the devices to share this folder with.": "Pasirinkite įrenginius, su kuriais dalinsitės šį aplanką.",
|
||||
"Select the folders to share with this device.": "Pasirinkite aplankus kuriais norite dalintis su šiuo įrenginiu.",
|
||||
"Settings": "Nustatymai",
|
||||
"Share": "Dalintis",
|
||||
"Share Folder": "Dalintis aplanku",
|
||||
"Share Folders With Device": "Dalintis aplankalais su šiuo įrenginiu",
|
||||
"Share With Devices": "Dalintis su įrenginiais",
|
||||
"Share this folder?": "Dalintis šiuo aplanku?",
|
||||
"Shared With": "Dalinamasi su",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Trumpas aplanko identifikatorius. Privalo būti toks pat visuose įrenginiuose.",
|
||||
"Show ID": "Rodyti ID",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Pakopinis versijų valdymas",
|
||||
"Start Browser": "Paleisti naršyklę",
|
||||
"Stopped": "Sustabdyta",
|
||||
"Support": "Pagalba",
|
||||
"Support / Forum": "Palaikymas / Diskusijos",
|
||||
"Sync Protocol Listen Addresses": "Sutapatinimo taisyklių adresas",
|
||||
"Synchronization": "Sutapatinimas",
|
||||
@@ -106,11 +129,12 @@
|
||||
"Syncthing is restarting.": "Syncthing perleidžiamas",
|
||||
"Syncthing is upgrading.": "Syncthing atsinaujina.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing išjungta arba problemos su Interneto ryšių. Bandoma iš naujo...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing turi problemų su jūsų užklausa. Perkraukite puslapį arba perkraukite Syncthing jei problema nedings.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Naudojimosi ataskaitą galite peržiūrėti adresu: {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Nauji nustatymai išsaugoti, bet neaktyvuoti. Perleiskite Syncthing programą iš naujo norėdami įgalinti naujus nustatymus.",
|
||||
"The device ID cannot be blank.": "Įrenginio ID negali būti tuščias.",
|
||||
"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).": "Įrenginio ID, kurį čia reikia įvesti, gali būti rastas „Keisti > Rodyti vardą“ dialoge kitame įrenginyje. Tarpai ir brūkšneliai nebūtini (ignoruojami).",
|
||||
"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.": "Kas dieną siunčiama šifruota naudojimo ataskaita. Ji naudojama sekti, kokios platformos naudojamos, aplankų dydžius ir programų versijas. Jei siunčiamų duomenų tipas pasikeis, šis dialogas bus parodytas iš naujo.",
|
||||
"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).": "Įrenginio ID, kurį čia reikia įvesti, gali būti rastas „Redaguoti > Rodyti ID“ dialoge kitame įrenginyje. Tarpai ir brūkšneliai nebūtini (ignoruojami).",
|
||||
"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.": "Kas dieną siunčiama šifruota naudojimo ataskaita. Ji naudojama sekti, kokios platformos naudojamos, aplankų dydžius ir programų versijas. Jei siunčiamų duomenų turinys pasikeis, šis dialogas bus parodytas iš naujo.",
|
||||
"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.": "Įvestas neteisingas įrenginio ID. Turi būti 52 ar 56 simbolių eilutė su raidėmis ir skaičiais kuriuos galima atskirti tarpu arba brūkšneliu.",
|
||||
"The folder ID cannot be blank.": "Aplanko ID negali būti tuščias.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Aplanko vardas negali būti ilgesnis nei 64 simboliai. Galima naudoti tik raides ir skaičius bet tašką (.), brūkšnelį (-) ir pabraukimą (_).",
|
||||
@@ -124,6 +148,8 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
|
||||
"The rescan interval must be at least 5 seconds.": "Nuskaityti galima nedažniau nei kas 5 sekundes.",
|
||||
"Unknown": "Nežinoma",
|
||||
"Unshared": "Nesidalinama",
|
||||
"Unused": "Nenaudojamas",
|
||||
"Up to Date": "Atnaujinta",
|
||||
"Upgrade To {%version%}": "Atnaujinti į {{version}}",
|
||||
"Upgrading": "Atnaujinama",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Taip",
|
||||
"You must keep at least one version.": "Būtina saugoti bent vieną versiją.",
|
||||
"full documentation": "pilna dokumentacija",
|
||||
"items": "įrašai"
|
||||
"items": "įrašai",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} nori dalintis aplanku \"{{folder}}\""
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "API-nøkkel",
|
||||
"About": "Om",
|
||||
"Add": "Add",
|
||||
"Add Device": "Legg Til Enhet",
|
||||
"Add Folder": "Legg Til Mappe",
|
||||
"Add new folder?": "Add new folder?",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresser",
|
||||
"Allow Anonymous Usage Reporting?": "Tillat Anonym Innsamling Av Brukerdata?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Automatiske oppdateringer",
|
||||
"Bugs": "Programfeil",
|
||||
"CPU Utilization": "CPU-utnyttelse",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Lukk",
|
||||
"Comment, when used at the start of a line": "Kommentar, når det blir brukt i starten av en linje.",
|
||||
"Compression is recommended in most setups.": "Komprimering er anbefalt i de fleste tilfeller.",
|
||||
"Connection Error": "Tilkoblingsfeil",
|
||||
"Copied from elsewhere": "Copied from elsewhere",
|
||||
"Copied from original": "Copied from original",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg og følgende Bidragsytere:",
|
||||
"Delete": "Slett",
|
||||
"Device ID": "Enhet ID",
|
||||
"Device Identification": "Enhetskjennemerke",
|
||||
"Device Name": "Navn På Enhet",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Frakoblet",
|
||||
"Documentation": "Dokumentasjon",
|
||||
"Download Rate": "Nedlastingsrate",
|
||||
"Downloaded": "Downloaded",
|
||||
"Downloading": "Downloading",
|
||||
"Edit": "Rediger",
|
||||
"Edit Device": "Rediger Enhet",
|
||||
"Edit Folder": "Rediger Mappe",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "Mappe ID",
|
||||
"Folder Master": "Styrende Mappe",
|
||||
"Folder Path": "Mappeplassering",
|
||||
"Folders": "Folders",
|
||||
"GUI Authentication Password": "GUI Passord",
|
||||
"GUI Authentication User": "GUI Bruker",
|
||||
"GUI Listen Addresses": "GUI Lytteadresse",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Global Søkemotor",
|
||||
"Global State": "Global Tilstand",
|
||||
"Idle": "Pause",
|
||||
"Ignore": "Ignore",
|
||||
"Ignore Patterns": "Utelatelsesmønster",
|
||||
"Ignore Permissions": "Ignorer Tilgangsbit",
|
||||
"Incoming Rate Limit (KiB/s)": "Innkommende Hastighetsbegrensning (KiB/s)",
|
||||
"Introducer": "Introduktør",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Invers av den gitte tilstanden (t.d. ikke ekskluder)",
|
||||
"Keep Versions": "Behold Versjoner",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Last File Synced",
|
||||
"Last seen": "Sist sett",
|
||||
"Later": "Later",
|
||||
"Latest Release": "Nyeste Versjon",
|
||||
"Local Discovery": "Lokal Søking",
|
||||
"Local State": "Lokal Tilstand",
|
||||
"Maximum Age": "Maksimal Levetid",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Multinivåsøk (søker på flere mappenivå)",
|
||||
"Never": "Aldri",
|
||||
"New Device": "New Device",
|
||||
"New Folder": "New Folder",
|
||||
"No": "Nei",
|
||||
"No File Versioning": "Ingen Versjonskontroll",
|
||||
"Notice": "Merknad",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Omstart",
|
||||
"Restart Needed": "Omstart Kreves",
|
||||
"Restarting": "Starter På Ny",
|
||||
"Reused": "Reused",
|
||||
"Save": "Lagre",
|
||||
"Scanning": "Skanner",
|
||||
"Select the devices to share this folder with.": "Velg enhetene du vil dele denne mappen med.",
|
||||
"Select the folders to share with this device.": "Select the folders to share with this device.",
|
||||
"Settings": "Innstillinger",
|
||||
"Share": "Share",
|
||||
"Share Folder": "Share Folder",
|
||||
"Share Folders With Device": "Share Folders With Device",
|
||||
"Share With Devices": "Del Med Enheter",
|
||||
"Share this folder?": "Share this folder?",
|
||||
"Shared With": "Del Med",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort kjennemerke på mappen. Må være det samme på alle enheter i en gruppe.",
|
||||
"Show ID": "Vis ID",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Forskjøvet Versjonskontroll",
|
||||
"Start Browser": "Start Nettleser",
|
||||
"Stopped": "Stoppa",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Brukerstøtte / Forum",
|
||||
"Sync Protocol Listen Addresses": "Lytteadresse For Synkroniseringsprotokoll",
|
||||
"Synchronization": "Synkronisering",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Syncthing starter på ny.",
|
||||
"Syncthing is upgrading.": "Syncthing oppgraderer.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å være nede, eller så er det et problem med nettforbindelsen din. Prøver på ny …",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Samlet statistikk er åpent tilgjengelig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Innstillingene har blitt lagret men ikke aktivert. Syncthing må starte på ny for å aktivere de nye innstillingene.",
|
||||
"The device ID cannot be blank.": "Enhets-ID kan ikke være tom.",
|
||||
@@ -124,6 +148,8 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Antall sekund i skanneintervallet kan ikke være negativt.",
|
||||
"The rescan interval must be at least 5 seconds.": "Skanneintervallet må være minst 5 sekund.",
|
||||
"Unknown": "Ukjent",
|
||||
"Unshared": "Unshared",
|
||||
"Unused": "Unused",
|
||||
"Up to Date": "Oppdatert",
|
||||
"Upgrade To {%version%}": "Oppgrader Til {{version}}",
|
||||
"Upgrading": "Oppgraderer",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du må beholde minst én versjon",
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "element"
|
||||
"items": "element",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "API-sleutel",
|
||||
"About": "Over",
|
||||
"Add": "Toevoegen",
|
||||
"Add Device": "Toestel toevoegen",
|
||||
"Add Folder": "Folder toevoegen",
|
||||
"Add new folder?": "Nieuwe folder toevoegen?",
|
||||
"Address": "Adres",
|
||||
"Addresses": "Adressen",
|
||||
"Allow Anonymous Usage Reporting?": "Bijhouden van anonieme gebruikers statistieken toestaan?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Automatisch bijwerken",
|
||||
"Bugs": "Fouten",
|
||||
"CPU Utilization": "CPU Gebruik",
|
||||
"Changelog": "Changelog",
|
||||
"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",
|
||||
"Copied from elsewhere": "Van elders gekopieerd",
|
||||
"Copied from original": "Gekopieerd van het origineel",
|
||||
"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",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Het toestel {{device}} ({{address}}) wenst te verbinden. Dit toestel toevoegen?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Niet Verbonden",
|
||||
"Documentation": "Documentatie",
|
||||
"Download Rate": "Downloadsnelheid",
|
||||
"Downloaded": "Gedownload",
|
||||
"Downloading": "Bezig met downloaden",
|
||||
"Edit": "Bewerk",
|
||||
"Edit Device": "Toestel aanpassen",
|
||||
"Edit Folder": "Folder aanpassen",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "Folder ID",
|
||||
"Folder Master": "Hoofdfolder",
|
||||
"Folder Path": "Locatie folder",
|
||||
"Folders": "Folders",
|
||||
"GUI Authentication Password": "GUI Authentificatie Wachtwoord",
|
||||
"GUI Authentication User": "GUI Authentificatie Gebruikersnaam",
|
||||
"GUI Listen Addresses": "GUI Inkomend adres",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Globale zoekserver",
|
||||
"Global State": "Globale status",
|
||||
"Idle": "Inactief",
|
||||
"Ignore": "Negeren",
|
||||
"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 File Received": "Last File Received",
|
||||
"Last File Synced": "Laatste Gesynchroniseerde Bestand",
|
||||
"Last seen": "Laatst gezien op",
|
||||
"Later": "Later",
|
||||
"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",
|
||||
"New Device": "Nieuw Toestel",
|
||||
"New Folder": "Nieuwe Folder",
|
||||
"No": "Nee",
|
||||
"No File Versioning": "Geen versiebeheer",
|
||||
"Notice": "Notificatie",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Herstart",
|
||||
"Restart Needed": "Herstart nodig",
|
||||
"Restarting": "Herstarten",
|
||||
"Reused": "Hergebruikt",
|
||||
"Save": "Bewaar",
|
||||
"Scanning": "Aan het zoeken",
|
||||
"Select the devices to share this folder with.": "Selecteer de toestellen om deze folder mee te delen.",
|
||||
"Select the folders to share with this device.": "Selecteer de folders om met dit toestel te delen.",
|
||||
"Settings": "Instellingen",
|
||||
"Share": "Delen",
|
||||
"Share Folder": "Folder Delen",
|
||||
"Share Folders With Device": "Folders delen met toestellen",
|
||||
"Share With Devices": "Delen met toestellen",
|
||||
"Share this folder?": "Deze folder delen?",
|
||||
"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",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Gelaagd versiebeheer",
|
||||
"Start Browser": "Start browser",
|
||||
"Stopped": "Gestopt",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Support / Forum",
|
||||
"Sync Protocol Listen Addresses": "Synchronisatie protocol luister adres",
|
||||
"Synchronization": "Synchronisatie",
|
||||
@@ -106,6 +129,7 @@
|
||||
"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....",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing heeft problemen bij het verwerken van je vraag. Gelieve de pagina opnieuw te laden of Syncthing te herstarten wanneer het probleem blijft opduiken.",
|
||||
"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.",
|
||||
@@ -124,6 +148,8 @@
|
||||
"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",
|
||||
"Unshared": "Niet gedeeld",
|
||||
"Unused": "Ongebruikt",
|
||||
"Up to Date": "Gesynchroniseerd",
|
||||
"Upgrade To {%version%}": "Upgrade naar {{version}}",
|
||||
"Upgrading": "Bezig met upgrade",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Minstens 1 versie moet bewaard blijven.",
|
||||
"full documentation": "volledige documentatie",
|
||||
"items": "objecten"
|
||||
"items": "objecten",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wil de folder \"{{folder}}\" delen."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "API-nøkkel",
|
||||
"About": "Om",
|
||||
"Add": "Add",
|
||||
"Add Device": "Legg Til Eining",
|
||||
"Add Folder": "Legg Til Mappe",
|
||||
"Add new folder?": "Add new folder?",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresser",
|
||||
"Allow Anonymous Usage Reporting?": "Tillat Anonym Innsamling Av Brukardata?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Automatiske oppdateringar",
|
||||
"Bugs": "Programfeil",
|
||||
"CPU Utilization": "CPU-utnytting",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Lukk",
|
||||
"Comment, when used at the start of a line": "Kommentar, når det vert brukt i starten av ei linje.",
|
||||
"Compression is recommended in most setups.": "Komprimering er tilrådd i dei fleste høve.",
|
||||
"Connection Error": "Tilkoplingsfeil",
|
||||
"Copied from elsewhere": "Copied from elsewhere",
|
||||
"Copied from original": "Copied from original",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg og følgjande Bidragsytarar:",
|
||||
"Delete": "Slett",
|
||||
"Device ID": "Eining ID",
|
||||
"Device Identification": "Einingskjennemerke",
|
||||
"Device Name": "Namn På Eining",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Fråkopla",
|
||||
"Documentation": "Dokumentasjon",
|
||||
"Download Rate": "Nedlastingsrate",
|
||||
"Downloaded": "Downloaded",
|
||||
"Downloading": "Downloading",
|
||||
"Edit": "Rediger",
|
||||
"Edit Device": "Rediger Eining",
|
||||
"Edit Folder": "Rediger Mappe",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "Mappe ID",
|
||||
"Folder Master": "Styrande Mappe",
|
||||
"Folder Path": "Mappeplassering",
|
||||
"Folders": "Folders",
|
||||
"GUI Authentication Password": "GUI Passord",
|
||||
"GUI Authentication User": "GUI Brukar",
|
||||
"GUI Listen Addresses": "GUI Lytteadresse",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Global Søkjemotor",
|
||||
"Global State": "Global Tilstand",
|
||||
"Idle": "Pause",
|
||||
"Ignore": "Ignore",
|
||||
"Ignore Patterns": "Utelatingsmønster",
|
||||
"Ignore Permissions": "Ignorer Tilgangsbit",
|
||||
"Incoming Rate Limit (KiB/s)": "Innkomande Hastigheitsgrense (KiB/s)",
|
||||
"Introducer": "Introduktør",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Invers av den gitte tilstanden (t.d. ikkje ekskluder)",
|
||||
"Keep Versions": "Behald Versjonar",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Last File Synced",
|
||||
"Last seen": "Sist sett",
|
||||
"Later": "Later",
|
||||
"Latest Release": "Nyaste Versjon",
|
||||
"Local Discovery": "Lokal Søking",
|
||||
"Local State": "Lokal Tilstand",
|
||||
"Maximum Age": "Maksimal Levetid",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Multinivåsøk (søkjer på fleire mappenivå)",
|
||||
"Never": "Aldri",
|
||||
"New Device": "New Device",
|
||||
"New Folder": "New Folder",
|
||||
"No": "Nei",
|
||||
"No File Versioning": "Ingen Versjonskontroll",
|
||||
"Notice": "Merknad",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Omstart",
|
||||
"Restart Needed": "Omstart Trengs",
|
||||
"Restarting": "Startar På Ny",
|
||||
"Reused": "Reused",
|
||||
"Save": "Lagre",
|
||||
"Scanning": "Skannar",
|
||||
"Select the devices to share this folder with.": "Vel einingane du vil dela denne mappa med.",
|
||||
"Select the folders to share with this device.": "Select the folders to share with this device.",
|
||||
"Settings": "Innstillingar",
|
||||
"Share": "Share",
|
||||
"Share Folder": "Share Folder",
|
||||
"Share Folders With Device": "Share Folders With Device",
|
||||
"Share With Devices": "Del Med Einingar",
|
||||
"Share this folder?": "Share this folder?",
|
||||
"Shared With": "Delt Med",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort kjennemerke på mappa. Må vera det same på alle einingar i ei gruppe.",
|
||||
"Show ID": "Vis ID",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Forskyvd Versjonskontroll",
|
||||
"Start Browser": "Start Nettlesar",
|
||||
"Stopped": "Stoppa",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Brukarstøtte / Forum",
|
||||
"Sync Protocol Listen Addresses": "Lytteadresse For Synkroniseringsprotokoll",
|
||||
"Synchronization": "Synkronisering",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Syncthing startar på ny.",
|
||||
"Syncthing is upgrading.": "Syncthing oppgraderer.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å vera nede, eller så er det eit problem med nettilkoplinga di. Prøvar på ny …",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Samla statistikk er opent tilgjengeleg på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Instillingane har blitt lagra men ikkje aktivert. Syncthing må starta på ny for å aktivera dei nye instillingane.",
|
||||
"The device ID cannot be blank.": "Eining ID kan ikkje vera tom.",
|
||||
@@ -124,6 +148,8 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Talet på sekund i skanneintervallet kan ikkje vera negativt.",
|
||||
"The rescan interval must be at least 5 seconds.": "Skanneintervallet må vera minst 5 sekund.",
|
||||
"Unknown": "Ukjent",
|
||||
"Unshared": "Unshared",
|
||||
"Unused": "Unused",
|
||||
"Up to Date": "Oppdatert",
|
||||
"Upgrade To {%version%}": "Oppgrader Til {{version}}",
|
||||
"Upgrading": "Oppgraderer",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du må behalda minst ein versjon.",
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "element"
|
||||
"items": "element",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "Klucz API",
|
||||
"About": "O Syncthing",
|
||||
"Add": "Add",
|
||||
"Add Device": "Dodaj urządzenie",
|
||||
"Add Folder": "Dodaj folder",
|
||||
"Add new folder?": "Add new folder?",
|
||||
"Address": "Adres",
|
||||
"Addresses": "Adresy",
|
||||
"Allow Anonymous Usage Reporting?": "Zezwalaj na anonimowe statystyki użycia",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Automatyczne aktualizacje",
|
||||
"Bugs": "Błędy",
|
||||
"CPU Utilization": "Użycie CPU",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Zamknij",
|
||||
"Comment, when used at the start of a line": "Komentarz, jeżeli użyty na początku linii",
|
||||
"Compression is recommended in most setups.": "Kompresja jest zalecana w większości przypadków",
|
||||
"Connection Error": "Błąd połączenia",
|
||||
"Copied from elsewhere": "Copied from elsewhere",
|
||||
"Copied from original": "Copied from original",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg i następujący współautorzy:",
|
||||
"Delete": "Usuń",
|
||||
"Device ID": "ID urządzenia",
|
||||
"Device Identification": "Identyfikator urządzenia",
|
||||
"Device Name": "Nazwa urządzenia",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Rozłączony",
|
||||
"Documentation": "Dokumentacja",
|
||||
"Download Rate": "Prędkość pobierania",
|
||||
"Downloaded": "Downloaded",
|
||||
"Downloading": "Downloading",
|
||||
"Edit": "Edytuj",
|
||||
"Edit Device": "Edytuj urządzenie",
|
||||
"Edit Folder": "Edytuj folder",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "ID folderu",
|
||||
"Folder Master": "Główny folder",
|
||||
"Folder Path": "Ścieżka folderu",
|
||||
"Folders": "Folders",
|
||||
"GUI Authentication Password": "Hasło",
|
||||
"GUI Authentication User": "Użytkownik",
|
||||
"GUI Listen Addresses": "Adres nasłuchiwania",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Globalny serwer rozgłoszeniowy",
|
||||
"Global State": "Status globalny",
|
||||
"Idle": "Bezczynny",
|
||||
"Ignore": "Ignore",
|
||||
"Ignore Patterns": "Wzorce ignorowania",
|
||||
"Ignore Permissions": "Ignoruj uprawnienia",
|
||||
"Incoming Rate Limit (KiB/s)": "Ograniczenie prędkości odbierania (KiB/s)",
|
||||
"Introducer": "Wprowadzający",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Odwrócenie podanego wzorca (np. nie wykluczaj)",
|
||||
"Keep Versions": "Zachowuj wersje",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Last File Synced",
|
||||
"Last seen": "Ostatnio widziany",
|
||||
"Later": "Later",
|
||||
"Latest Release": "Najnowsza wersja",
|
||||
"Local Discovery": "Lokalne odnajdywanie",
|
||||
"Local State": "Status lokalny",
|
||||
"Maximum Age": "Maksymalny wiek",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Wieloznaczność na poziomie katalogów i plików (uwzględnia nazwy folderów i plików)",
|
||||
"Never": "Nigdy",
|
||||
"New Device": "New Device",
|
||||
"New Folder": "New Folder",
|
||||
"No": "Nie",
|
||||
"No File Versioning": "Bez wersjonowania pliku",
|
||||
"Notice": "Wskazówka",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Uruchom ponownie",
|
||||
"Restart Needed": "Wymagane ponowne uruchomienie",
|
||||
"Restarting": "Uruchamianie ponowne",
|
||||
"Reused": "Reused",
|
||||
"Save": "Zapisz",
|
||||
"Scanning": "Skanowanie",
|
||||
"Select the devices to share this folder with.": "Wybierz urządzenie, któremu udostępnić folder.",
|
||||
"Select the folders to share with this device.": "Select the folders to share with this device.",
|
||||
"Settings": "Ustawienia",
|
||||
"Share": "Share",
|
||||
"Share Folder": "Share Folder",
|
||||
"Share Folders With Device": "Share Folders With Device",
|
||||
"Share With Devices": "Udostępnij dla urządzenia",
|
||||
"Share this folder?": "Share this folder?",
|
||||
"Shared With": "Współdzielony z",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Krótki identyfikator folderu. Musi być taki sam na wszystkich urządzeniach.",
|
||||
"Show ID": "Pokaż ID",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Rozbudowane wersjonowanie pliku",
|
||||
"Start Browser": "Uruchom przeglądarkę",
|
||||
"Stopped": "Zatrzymany",
|
||||
"Support": "Wsparcie",
|
||||
"Support / Forum": "Wsparcie / Forum",
|
||||
"Sync Protocol Listen Addresses": "Adres nasłuchu protokołu synchronizacji",
|
||||
"Synchronization": "Synchronizacja",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Restart Syncthing",
|
||||
"Syncthing is upgrading.": "Aktualizowanie Syncthing",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing wydaje się być wyłączony lub jest problem z twoim połączeniem internetowym. Próbuje ponownie...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Zebrane statystyki są publicznie dostępna pod adresem {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfiguracja została zapisana lecz nie jest aktywna. Syncthing musi zostać zrestartowany aby aktywować nową konfiguracje.",
|
||||
"The device ID cannot be blank.": "ID urządzenia nie może być puste.",
|
||||
@@ -124,6 +148,8 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
|
||||
"The rescan interval must be at least 5 seconds.": "Interwał skanowania musi wynosić co najmniej 5 sekund.",
|
||||
"Unknown": "Nieznany",
|
||||
"Unshared": "Unshared",
|
||||
"Unused": "Unused",
|
||||
"Up to Date": "Aktualny",
|
||||
"Upgrade To {%version%}": "Aktualizuj do {{version}}",
|
||||
"Upgrading": "Aktualizowanie",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Tak",
|
||||
"You must keep at least one version.": "Musisz posiadać przynajmniej jedną wersję",
|
||||
"full documentation": "pełna dokumentacja",
|
||||
"items": "pozycji"
|
||||
"items": "pozycji",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "Chave da API",
|
||||
"About": "Acerca da aplicação",
|
||||
"Add": "Adicionar",
|
||||
"Add Device": "Adicionar dispositivo",
|
||||
"Add Folder": "Adicionar pasta",
|
||||
"Add new folder?": "Adicionar nova pasta?",
|
||||
"Address": "Endereço",
|
||||
"Addresses": "Endereços",
|
||||
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Actualizações automáticas",
|
||||
"Bugs": "Erros",
|
||||
"CPU Utilization": "Utilização da CPU",
|
||||
"Changelog": "Registo de alterações",
|
||||
"Close": "Fechar",
|
||||
"Comment, when used at the start of a line": "Comentário, quando usado no início de uma linha",
|
||||
"Compression is recommended in most setups.": "A compressão é recomendada na maior parte dos casos.",
|
||||
"Connection Error": "Erro de ligação",
|
||||
"Copied from elsewhere": "Copiado doutro sítio",
|
||||
"Copied from original": "Copiado do original",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Direitos reservados © 2014 Jakob Borg e os seguintes contribuidores:",
|
||||
"Delete": "Eliminar",
|
||||
"Device ID": "ID do dispositivo",
|
||||
"Device Identification": "Identificação do dispositivo",
|
||||
"Device Name": "Nome do dispositivo",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "O dispositivo {{device}} ({{address}}) quer conectar-se. Adiciono este novo dispositivo?",
|
||||
"Devices": "Dispositivos",
|
||||
"Disconnected": "Desconectado",
|
||||
"Documentation": "Documentação",
|
||||
"Download Rate": "Velocidade de recepção",
|
||||
"Downloaded": "Recebido",
|
||||
"Downloading": "Recebendo",
|
||||
"Edit": "Editar",
|
||||
"Edit Device": "Editar dispositivo",
|
||||
"Edit Folder": "Editar pasta",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "ID da pasta",
|
||||
"Folder Master": "Pasta mestre",
|
||||
"Folder Path": "Caminho da pasta",
|
||||
"Folders": "Pastas",
|
||||
"GUI Authentication Password": "Senha da autenticação na interface gráfica",
|
||||
"GUI Authentication User": "Utilizador da autenticação na interface gráfica",
|
||||
"GUI Listen Addresses": "Endereço de escuta da interface gráfica",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Servidor da busca global",
|
||||
"Global State": "Estado global",
|
||||
"Idle": "Em espera",
|
||||
"Ignore": "Ignorar",
|
||||
"Ignore Patterns": "Padrões de exclusão",
|
||||
"Ignore Permissions": "Ignorar permissões",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite de velocidade de recepção (KiB/s)",
|
||||
"Introducer": "Apresentador",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversão de uma dada condição (ou seja, não excluir)",
|
||||
"Keep Versions": "Manter versões",
|
||||
"Last File Received": "Último ficheiro recebido",
|
||||
"Last File Synced": "Último ficheiro sincronizado",
|
||||
"Last seen": "Última vez que foi verificado",
|
||||
"Later": "Mais tarde",
|
||||
"Latest Release": "Última versão",
|
||||
"Local Discovery": "Busca local",
|
||||
"Local State": "Estado local",
|
||||
"Maximum Age": "Idade máxima",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Caractere polivalente multi-nível (faz corresponder a vários níveis de pastas)",
|
||||
"Never": "Nunca",
|
||||
"New Device": "Novo dispositivo",
|
||||
"New Folder": "Nova pasta",
|
||||
"No": "Não",
|
||||
"No File Versioning": "Sem gestão de versões de ficheiros",
|
||||
"Notice": "Avisos",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "É preciso reiniciar",
|
||||
"Restarting": "Reiniciando",
|
||||
"Reused": "Reutilizado",
|
||||
"Save": "Gravar",
|
||||
"Scanning": "Verificando",
|
||||
"Select the devices to share this folder with.": "Seleccione os dispositivos com os quais vai partilhar esta pasta.",
|
||||
"Select the folders to share with this device.": "Seleccione as pastas a partilhar com este dispositivo.",
|
||||
"Settings": "Configurações",
|
||||
"Share": "Partilhar",
|
||||
"Share Folder": "Partilhar pasta",
|
||||
"Share Folders With Device": "Partilhar pastas com dispositivo",
|
||||
"Share With Devices": "Partilhar com os dispositivos",
|
||||
"Share this folder?": "Partilhar esta pasta?",
|
||||
"Shared With": "Partilhado com",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador curto para a pasta. Tem que ser igual em todos os dispositivos do grupo.",
|
||||
"Show ID": "Mostrar ID",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Gestão de versões de ficheiros escalonada",
|
||||
"Start Browser": "Iniciar navegador",
|
||||
"Stopped": "Parado",
|
||||
"Support": "Suporte",
|
||||
"Support / Forum": "Suporte / Fórum",
|
||||
"Sync Protocol Listen Addresses": "Endereços de escuta do protocolo de sincronização",
|
||||
"Synchronization": "Sincronização",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "O Syncthing está a reiniciar.",
|
||||
"Syncthing is upgrading.": "O Syncthing está a actualizar-se.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "O Syncthing parece estar em baixo, ou então existe um problema com a sua ligação à Internet. Tentando novamente...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "O Syncthing parece estar com problemas em processar o seu pedido. Tente recarregar a página ou reiniciar o Syncthing, se o problema persistir.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "As estatísticas agrupadas estão disponíveis publicamente em {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi gravada mas não activada. O Syncthing tem que reiniciar para activar a nova configuração.",
|
||||
"The device ID cannot be blank.": "O ID do dispositivo não pode estar vazio.",
|
||||
@@ -124,7 +148,9 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "O intervalo entre verificações tem que ser um valor não negativo de segundos.",
|
||||
"The rescan interval must be at least 5 seconds.": "O intervalo entre verificações tem que ser pelo menos de 5 segundos.",
|
||||
"Unknown": "Desconhecido",
|
||||
"Up to Date": "Actualizado",
|
||||
"Unshared": "Não partilhada",
|
||||
"Unused": "Não utilizado",
|
||||
"Up to Date": "Sincronizado",
|
||||
"Upgrade To {%version%}": "Actualizar para {{version}}",
|
||||
"Upgrading": "Actualizando",
|
||||
"Upload Rate": "Velocidade de envio",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Sim",
|
||||
"You must keep at least one version.": "Tem que manter pelo menos uma versão.",
|
||||
"full documentation": "documentação completa",
|
||||
"items": "itens"
|
||||
"items": "itens",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer partilhar a pasta \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "Ключ API",
|
||||
"About": "О программе",
|
||||
"Add": "Add",
|
||||
"Add Device": "Добавить устройство",
|
||||
"Add Folder": "Добавить папку",
|
||||
"Add new folder?": "Add new folder?",
|
||||
"Address": "Адрес",
|
||||
"Addresses": "Адреса",
|
||||
"Allow Anonymous Usage Reporting?": "Разрешить сбор анонимной статистики использования?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "Автообновление",
|
||||
"Bugs": "Ошибки",
|
||||
"CPU Utilization": "Загрузка ЦПУ",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Закрыть",
|
||||
"Comment, when used at the start of a line": "Комментарий, если используется в начале строки",
|
||||
"Compression is recommended in most setups.": "Сжатие рекомендуется в большинстве случаев.",
|
||||
"Connection Error": "Ошибка подключения",
|
||||
"Copied from elsewhere": "Скопировано из другого места",
|
||||
"Copied from original": "Скопировано с оригинала",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Все права защищены © 2014 Jakob Borg и следующие участники:",
|
||||
"Delete": "Удалить",
|
||||
"Device ID": "ID устройства",
|
||||
"Device Identification": "Идентификация устройства",
|
||||
"Device Name": "Имя устройства",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Нет соединения",
|
||||
"Documentation": "Документация",
|
||||
"Download Rate": "Скорость загрузки",
|
||||
"Downloaded": "Загружено",
|
||||
"Downloading": "Загрузка",
|
||||
"Edit": "Изменить",
|
||||
"Edit Device": "Изменить устройство",
|
||||
"Edit Folder": "Изменение папки",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "ID папки",
|
||||
"Folder Master": "Папка-оригинал",
|
||||
"Folder Path": "Путь к папке",
|
||||
"Folders": "Папки",
|
||||
"GUI Authentication Password": "Пароль для доступа к панели управления",
|
||||
"GUI Authentication User": "Имя пользователя для доступа к панели управления",
|
||||
"GUI Listen Addresses": "Адрес панели управления",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Сервер глобального обнаружения",
|
||||
"Global State": "Глобальное состояние",
|
||||
"Idle": "Бездействует",
|
||||
"Ignore": "Ignore",
|
||||
"Ignore Patterns": "Шаблоны игнорирования",
|
||||
"Ignore Permissions": "Игнорировать файловые права доступа",
|
||||
"Incoming Rate Limit (KiB/s)": "Ограничение входящего потока (Кбит/сек)",
|
||||
"Introducer": "Рекомендатель",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Инвертировать текущее условие (например, исключить)",
|
||||
"Keep Versions": "Количество хранимых версий",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Последний синхронизированный файл",
|
||||
"Last seen": "Был доступен",
|
||||
"Later": "Later",
|
||||
"Latest Release": "Последняя версия",
|
||||
"Local Discovery": "Локальное обнаружение",
|
||||
"Local State": "Локальное состояние",
|
||||
"Maximum Age": "Максимальный срок",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Многоуровневая маска (поиск совпадений во всех подпапках)",
|
||||
"Never": "Никогда",
|
||||
"New Device": "New Device",
|
||||
"New Folder": "New Folder",
|
||||
"No": "Нет",
|
||||
"No File Versioning": "Без управления версиями файлов",
|
||||
"Notice": "Внимание",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Перезапуск",
|
||||
"Restart Needed": "Требуется перезапуск",
|
||||
"Restarting": "Перезапуск",
|
||||
"Reused": "Повторно использовано",
|
||||
"Save": "Сохранить",
|
||||
"Scanning": "Сканирование",
|
||||
"Select the devices to share this folder with.": "Выберите устройства, для которых будет доступна эта папка.",
|
||||
"Select the folders to share with this device.": "Выберите папку для предоставления доступа данному устройству",
|
||||
"Settings": "Настройки",
|
||||
"Share": "Share",
|
||||
"Share Folder": "Share Folder",
|
||||
"Share Folders With Device": "Предоставить доступ устройству к папкам",
|
||||
"Share With Devices": "Предоставить доступ устройствам",
|
||||
"Share this folder?": "Share this folder?",
|
||||
"Shared With": "Доступ предоставлен",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Короткий идентификатор папки. Должен быть одинаковым на всех устройствах кластера.",
|
||||
"Show ID": "Показать ID",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Ступенчатое управление версиями файлов",
|
||||
"Start Browser": "Открыть браузер",
|
||||
"Stopped": "Остановлено",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Поддержка / Форум",
|
||||
"Sync Protocol Listen Addresses": "Адрес протокола синхронизации",
|
||||
"Synchronization": "Синхронизация",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Перезапуск Syncthing",
|
||||
"Syncthing is upgrading.": "Обновление Syncthing ",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"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 устройства не может быть пустым.",
|
||||
@@ -124,6 +148,8 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Интервал пересканирования должен быть неотрицательным количеством секунд.",
|
||||
"The rescan interval must be at least 5 seconds.": "Интервал пересканирования должен быть хотя бы 5 секунд.",
|
||||
"Unknown": "Неизвестно",
|
||||
"Unshared": "Необщедоступно",
|
||||
"Unused": "Не используется",
|
||||
"Up to Date": "Обновлено",
|
||||
"Upgrade To {%version%}": "Обновить до {{version}}",
|
||||
"Upgrading": "Обновление",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Да",
|
||||
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
|
||||
"full documentation": "полная документация",
|
||||
"items": "элементы"
|
||||
"items": "элементы",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,28 +1,37 @@
|
||||
{
|
||||
"API Key": "API-nyckel",
|
||||
"About": "Om",
|
||||
"Add": "Lägg till",
|
||||
"Add Device": "Lägg till enhet",
|
||||
"Add Folder": "Lägg till katalog",
|
||||
"Add new folder?": "Lägg till katalog?",
|
||||
"Address": "Adress",
|
||||
"Addresses": "Adresser",
|
||||
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistik?",
|
||||
"Anonymous Usage Reporting": "Anonym användarstatistik",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
|
||||
"Automatic upgrades": "Automatic upgrades",
|
||||
"Automatic upgrades": "Automatisk uppgradering",
|
||||
"Bugs": "Buggar",
|
||||
"CPU Utilization": "CPU-användning",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "Stäng",
|
||||
"Comment, when used at the start of a line": "Kommentar, vid början av en rad.",
|
||||
"Compression is recommended in most setups.": "Komprimering är rekommenderat för de flesta.",
|
||||
"Connection Error": "Anslutningsproblem",
|
||||
"Copied from elsewhere": "Kopierat utifrån",
|
||||
"Copied from original": "Oförändrat",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg och de följande medarbetarna:",
|
||||
"Delete": "Radera",
|
||||
"Device ID": "Enhets-ID",
|
||||
"Device Identification": "Enhetsidentifikation",
|
||||
"Device Name": "Enhetsnamn",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "Ej ansluten",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Nedladdningshastighet",
|
||||
"Downloaded": "Nerladdat",
|
||||
"Downloading": "Laddar ner",
|
||||
"Edit": "Redigera",
|
||||
"Edit Device": "Redigera enhet",
|
||||
"Edit Folder": "Redigera katalog",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "Katalog-ID",
|
||||
"Folder Master": "Huvudlagring",
|
||||
"Folder Path": "Sökväg",
|
||||
"Folders": "Kataloger",
|
||||
"GUI Authentication Password": "GUI-lösenord",
|
||||
"GUI Authentication User": "GUI-användare",
|
||||
"GUI Listen Addresses": "GUI-adress",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "Global uppslagningsserver",
|
||||
"Global State": "Global status",
|
||||
"Idle": "Vilande",
|
||||
"Ignore": "Ignorera",
|
||||
"Ignore Patterns": "Filmönster",
|
||||
"Ignore Permissions": "Ignorera filrättigheter",
|
||||
"Incoming Rate Limit (KiB/s)": "Max nedladdningshastighet (KiB/s)",
|
||||
"Introducer": "Introducer",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Vänder på villkoret, d.v.s. exkluderar inte.",
|
||||
"Keep Versions": "Behåll versioner",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "Senast uppdaterad fil",
|
||||
"Last seen": "Senast online",
|
||||
"Later": "Senare",
|
||||
"Latest Release": "Senaste version",
|
||||
"Local Discovery": "Lokal uppslagning",
|
||||
"Local State": "Lokal status",
|
||||
"Maximum Age": "Högsta åldersgräns",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Jokertecken som representerar noll eller fler godtyckliga tecken, även över kataloggränser.",
|
||||
"Never": "Aldrig",
|
||||
"New Device": "Ny enhet",
|
||||
"New Folder": "Ny katalog",
|
||||
"No": "Nej",
|
||||
"No File Versioning": "Ingen versionshantering",
|
||||
"Notice": "OBS",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "Starta om",
|
||||
"Restart Needed": "Omstart behövs",
|
||||
"Restarting": "Startar om",
|
||||
"Reused": "Återanvänt",
|
||||
"Save": "Spara",
|
||||
"Scanning": "Uppdaterar",
|
||||
"Select the devices to share this folder with.": "Ange enheterna att dela den här katalogen med.",
|
||||
"Select the folders to share with this device.": "Välja kataloger att dela med den här enheten",
|
||||
"Settings": "Inställningar",
|
||||
"Share": "Dela",
|
||||
"Share Folder": "Dela katalog",
|
||||
"Share Folders With Device": "Dela kataloger med enhet",
|
||||
"Share With Devices": "Dela med enheter",
|
||||
"Share this folder?": "Dela den här katalogen?",
|
||||
"Shared With": "Delat med",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort identifieringssträng för katalogen. Måste vara samma på alla enheter i klustern.",
|
||||
"Show ID": "Visa ID",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "Versionshantering i intervall",
|
||||
"Start Browser": "Starta browser",
|
||||
"Stopped": "Stoppad",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "Support / Forum",
|
||||
"Sync Protocol Listen Addresses": "Address för inkommande anslutningar",
|
||||
"Synchronization": "Synkronisering",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Syncthing startar om.",
|
||||
"Syncthing is upgrading.": "Syncthing uppgraderas.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller så är där ett problem med din Internetanslutning. Försöker igen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Aggregerad statistik finns publikt tillgänglig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
|
||||
"The device ID cannot be blank.": "Enhets-ID kan inte vara blankt.",
|
||||
@@ -121,9 +145,11 @@
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Den längsta tid att behålla en version (i dagar, sätt till 0 för att behålla versioner för evigt).",
|
||||
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas, per fil.",
|
||||
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett nummer och kan inte lämnas blankt.",
|
||||
"The 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.": "Förnyelseintervallet måste vara ett positivt antal sekunder",
|
||||
"The rescan interval must be at least 5 seconds.": "Uppdateringsintervallet måste vara minst 5 sekunder.",
|
||||
"Unknown": "Okänt",
|
||||
"Unshared": "Odelat",
|
||||
"Unused": "Oanvänd",
|
||||
"Up to Date": "Helt uppdaterad",
|
||||
"Upgrade To {%version%}": "Uppgradera till {{version}}",
|
||||
"Upgrading": "Uppgraderar",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
|
||||
"full documentation": "fullständig dokumentation",
|
||||
"items": "poster"
|
||||
"items": "poster",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"API Key": "API Key",
|
||||
"About": "关于",
|
||||
"Add": "Add",
|
||||
"Add Device": "添加设备",
|
||||
"Add Folder": "添加文件夹",
|
||||
"Add new folder?": "Add new folder?",
|
||||
"Address": "地址",
|
||||
"Addresses": "地址列表",
|
||||
"Allow Anonymous Usage Reporting?": "允许匿名使用报告?",
|
||||
@@ -11,18 +13,25 @@
|
||||
"Automatic upgrades": "自动升级",
|
||||
"Bugs": "Bug汇报",
|
||||
"CPU Utilization": "CPU使用率",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "关闭",
|
||||
"Comment, when used at the start of a line": "注释,在行首使用",
|
||||
"Compression is recommended in most setups.": "在大多数场合,建议开启压缩",
|
||||
"Connection Error": "连接出错",
|
||||
"Copied from elsewhere": "从其他地点复制",
|
||||
"Copied from original": "从源复制",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "版权© 2014 Jakob Borg 及以下贡献者:",
|
||||
"Delete": "删除",
|
||||
"Device ID": "设备ID",
|
||||
"Device Identification": "设备ID",
|
||||
"Device Name": "设备名",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "连接已断开",
|
||||
"Documentation": "文档",
|
||||
"Download Rate": "下载速度",
|
||||
"Downloaded": "已下载",
|
||||
"Downloading": "下载中",
|
||||
"Edit": "选项",
|
||||
"Edit Device": "修改设备选项",
|
||||
"Edit Folder": "修改文件夹选项",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "文件夹ID",
|
||||
"Folder Master": "母文件夹",
|
||||
"Folder Path": "文件夹路径",
|
||||
"Folders": "文件夹",
|
||||
"GUI Authentication Password": "登陆web管理页面的密码",
|
||||
"GUI Authentication User": "登陆web管理页面的用户名",
|
||||
"GUI Listen Addresses": "web管理页面监听地址",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "用以在互联网上寻找节点的Announce服务器地址",
|
||||
"Global State": "全局状态",
|
||||
"Idle": "空闲",
|
||||
"Ignore": "Ignore",
|
||||
"Ignore Patterns": "忽略列表",
|
||||
"Ignore Permissions": "忽略文件权限",
|
||||
"Incoming Rate Limit (KiB/s)": "下载速率限制(千字节/秒)",
|
||||
"Introducer": "介绍人节点",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "对本条件取反(例如:不要排除某项)",
|
||||
"Keep Versions": "保留历史版本数量",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "最近同步的文件",
|
||||
"Last seen": "最后可见",
|
||||
"Later": "Later",
|
||||
"Latest Release": "最新版本",
|
||||
"Local Discovery": "在局域网上寻找节点",
|
||||
"Local State": "本地状态",
|
||||
"Maximum Age": "历史版本最长保留时间",
|
||||
"Multi level wildcard (matches multiple directory levels)": "多级通配符(用以匹配多层文件夹)",
|
||||
"Never": "从未",
|
||||
"New Device": "New Device",
|
||||
"New Folder": "New Folder",
|
||||
"No": "否",
|
||||
"No File Versioning": "不启用版本控制",
|
||||
"Notice": "提示",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "重启syncthing",
|
||||
"Restart Needed": "需要重启Syncthing",
|
||||
"Restarting": "重启中",
|
||||
"Reused": "复用",
|
||||
"Save": "保存",
|
||||
"Scanning": "扫描中",
|
||||
"Select the devices to share this folder with.": "选择将本文件夹共享给哪些设备",
|
||||
"Select the folders to share with this device.": "选择与该设备共享的文件夹。",
|
||||
"Settings": "设置",
|
||||
"Share": "Share",
|
||||
"Share Folder": "Share Folder",
|
||||
"Share Folders With Device": "将指定文件夹共享给设备",
|
||||
"Share With Devices": "共享给",
|
||||
"Share this folder?": "Share this folder?",
|
||||
"Shared With": "共享给",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "文件夹的别名。必须在所有设备上保持一致。",
|
||||
"Show ID": "显示设备ID",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "阶段版本控制",
|
||||
"Start Browser": "启动浏览器",
|
||||
"Stopped": "已停止",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "支持/论坛",
|
||||
"Sync Protocol Listen Addresses": "协议监听地址",
|
||||
"Synchronization": "同步完成度",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Syncthing正在重启",
|
||||
"Syncthing is upgrading.": "Syncthing正在升级",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing似乎关闭了,或者您的网络连接存在故障。重试中...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"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不能为空",
|
||||
@@ -124,6 +148,8 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "扫描间隔单位为秒,且不能为负数。",
|
||||
"The rescan interval must be at least 5 seconds.": "扫描间隔必须至少为5秒。",
|
||||
"Unknown": "未知",
|
||||
"Unshared": "未共享",
|
||||
"Unused": "已共享",
|
||||
"Up to Date": "同步完成",
|
||||
"Upgrade To {%version%}": "升级至版本{{version}}",
|
||||
"Upgrading": "升级中",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "是",
|
||||
"You must keep at least one version.": "您必须保留至少一个版本。",
|
||||
"full documentation": "完整文档",
|
||||
"items": "条目"
|
||||
"items": "条目",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
|
||||
}
|
||||
@@ -1,28 +1,37 @@
|
||||
{
|
||||
"API Key": "API 金鑰",
|
||||
"About": "關於",
|
||||
"Add": "增加",
|
||||
"Add Device": "增加裝置",
|
||||
"Add Folder": "增加資料夾",
|
||||
"Add new 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": "Automatic upgrades",
|
||||
"Automatic upgrades": "自動升級",
|
||||
"Bugs": "程式錯誤",
|
||||
"CPU Utilization": "CPU 使用率",
|
||||
"Changelog": "Changelog",
|
||||
"Close": "關閉",
|
||||
"Comment, when used at the start of a line": "註解,當輸入在一行的開頭時",
|
||||
"Compression is recommended in most setups.": "建議在大多數的設置中使用壓縮。",
|
||||
"Connection Error": "連線錯誤",
|
||||
"Copied from elsewhere": "從別處複製",
|
||||
"Copied from original": "Copied from original",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "版權所有 © 2014 Jakob Borg 及以下貢獻者:",
|
||||
"Delete": "刪除",
|
||||
"Device ID": "裝置識別碼",
|
||||
"Device Identification": "裝置識別",
|
||||
"Device Name": "裝置名稱",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "裝置 {{device}} ({{address}}) 想要連線。要新增裝置嗎?",
|
||||
"Devices": "Devices",
|
||||
"Disconnected": "斷線",
|
||||
"Documentation": "說明文件",
|
||||
"Download Rate": "下載速率",
|
||||
"Downloaded": "已下載",
|
||||
"Downloading": "正在下載",
|
||||
"Edit": "編輯",
|
||||
"Edit Device": "編輯裝置",
|
||||
"Edit Folder": "編輯資料夾",
|
||||
@@ -38,6 +47,7 @@
|
||||
"Folder ID": "資料夾識別碼",
|
||||
"Folder Master": "主資料夾",
|
||||
"Folder Path": "資料夾路徑",
|
||||
"Folders": "資料夾",
|
||||
"GUI Authentication Password": "GUI 認證密碼",
|
||||
"GUI Authentication User": "GUI 認證使用者名稱",
|
||||
"GUI Listen Addresses": "GUI 監聽位址",
|
||||
@@ -46,19 +56,25 @@
|
||||
"Global Discovery Server": "全域探索伺服器",
|
||||
"Global State": "全域狀態",
|
||||
"Idle": "閒置",
|
||||
"Ignore": "忽略",
|
||||
"Ignore Patterns": "忽略樣式",
|
||||
"Ignore Permissions": "忽略權限",
|
||||
"Incoming Rate Limit (KiB/s)": "連入速率限制 (KiB/s)",
|
||||
"Introducer": "引入者",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "反轉給定條件 (即:不要排除)",
|
||||
"Keep Versions": "保留歷史版本數",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last File Synced": "最後同步檔案",
|
||||
"Last seen": "最後發現時間",
|
||||
"Later": "稍後",
|
||||
"Latest Release": "最新發佈",
|
||||
"Local Discovery": "本地探索",
|
||||
"Local State": "本地狀態",
|
||||
"Maximum Age": "最長保留時間",
|
||||
"Multi level wildcard (matches multiple directory levels)": "多階層萬用字元 (可比對多層資料夾)",
|
||||
"Never": "從未",
|
||||
"New Device": "新裝置",
|
||||
"New Folder": "新資料夾",
|
||||
"No": "否",
|
||||
"No File Versioning": "無檔案版本控制",
|
||||
"Notice": "注意",
|
||||
@@ -80,11 +96,17 @@
|
||||
"Restart": "重新啟動",
|
||||
"Restart Needed": "需要重新啟動",
|
||||
"Restarting": "正在重新啟動",
|
||||
"Reused": "Reused",
|
||||
"Save": "儲存",
|
||||
"Scanning": "正在掃描",
|
||||
"Select the devices to share this folder with.": "選擇要共享這個資料夾的裝置。",
|
||||
"Select the folders to share with this device.": "選擇要共享這個資料夾的裝置。",
|
||||
"Settings": "設定",
|
||||
"Share": "分享",
|
||||
"Share Folder": "分享資料夾",
|
||||
"Share Folders With Device": "與裝置共享資料夾",
|
||||
"Share With Devices": "與這些裝置共享",
|
||||
"Share this folder?": "分享此資料夾?",
|
||||
"Shared With": "與誰共享",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "資料夾的簡短識別字。必須在叢集內所有的裝置上皆相同。",
|
||||
"Show ID": "顯示識別碼",
|
||||
@@ -97,6 +119,7 @@
|
||||
"Staggered File Versioning": "變動式檔案版本控制",
|
||||
"Start Browser": "啟動瀏覽器",
|
||||
"Stopped": "已停止",
|
||||
"Support": "Support",
|
||||
"Support / Forum": "支援 / 論壇",
|
||||
"Sync Protocol Listen Addresses": "同步通訊協定監聽位址",
|
||||
"Synchronization": "同步作業",
|
||||
@@ -106,6 +129,7 @@
|
||||
"Syncthing is restarting.": "Syncthing 正在重新啟動。",
|
||||
"Syncthing is upgrading.": "Syncthing 正在進行升級。",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎下線了,或者您的網際網路連線出現問題。正在重試...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please reload your browser or restart Syncthing if the problem persists.",
|
||||
"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.": "裝置識別碼不能為空白。",
|
||||
@@ -121,9 +145,11 @@
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "一個版本被保留的最長時間 (單位為天,若設定為 0 則表示永遠保留)。",
|
||||
"The number of old versions to keep, per file.": "每個檔案要保留的舊版本數量。",
|
||||
"The number of versions must be a number and cannot be blank.": "每個檔案要保留的舊版本數量必須是數字且不能為空白。",
|
||||
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "重新掃描間隔必須為一個非負數的秒數。",
|
||||
"The rescan interval must be at least 5 seconds.": "重新掃描間隔至少須為 5 秒。",
|
||||
"Unknown": "未知",
|
||||
"Unshared": "未共享",
|
||||
"Unused": "未使用",
|
||||
"Up to Date": "最新",
|
||||
"Upgrade To {%version%}": "升級至 {{version}}",
|
||||
"Upgrading": "正在升級",
|
||||
@@ -138,5 +164,6 @@
|
||||
"Yes": "是",
|
||||
"You must keep at least one version.": "您必須保留至少一個版本。",
|
||||
"full documentation": "完整說明文件",
|
||||
"items": "個項目"
|
||||
"items": "個項目",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想要分享資料夾 \"{{folder}}\"。"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
var validLangs = ["be","bg","cs","de","en","fr","hu","it","lt","nb","nl","nn","pl","pt-PT","ru","sv","zh-CN","zh-TW"]
|
||||
var validLangs = ["be","bg","cs","de","en","es","fr","hu","it","lt","nb","nl","nn","pl","pt-PT","ru","sv","zh-CN","zh-TW"]
|
||||
|
||||
216
gui/index.html
216
gui/index.html
@@ -89,19 +89,20 @@
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="panel-group" id="folders">
|
||||
<div class="visible-xs"><h3><span translate>Folders</span></h3><hr></div>
|
||||
<div class="panel panel-default" ng-repeat="folder in folderList()">
|
||||
<div class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer">
|
||||
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.ID)}}%"></div>
|
||||
<h3 class="panel-title">
|
||||
<span class="glyphicon glyphicon-hdd"></span> {{folder.ID}}
|
||||
<span class="pull-right hidden-xs text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
|
||||
<span translate ng-switch-when="unknown">Unknown</span>
|
||||
<span translate ng-switch-when="unshared">Unshared</span>
|
||||
<span translate ng-switch-when="stopped">Stopped</span>
|
||||
<span translate ng-switch-when="scanning">Scanning</span>
|
||||
<span translate ng-switch-when="idle">Up to Date</span>
|
||||
<span class="glyphicon glyphicon-hdd hidden-xs"></span>{{folder.ID}}
|
||||
<span class="pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
|
||||
<span ng-switch-when="unknown"><span class="hidden-xs" translate>Unknown</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="unshared"><span class="hidden-xs" translate>Unshared</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="stopped"><span class="hidden-xs" translate>Stopped</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="scanning"><span class="hidden-xs" translate>Scanning</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="idle"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="syncing">
|
||||
<span translate>Syncing</span>
|
||||
<span class="hidden-xs" translate>Syncing</span>
|
||||
({{syncPercentage(folder.ID)}}%)
|
||||
</span>
|
||||
</span>
|
||||
@@ -153,6 +154,14 @@
|
||||
<th><span class="glyphicon glyphicon-share-alt"></span> <span translate>Shared With</span></th>
|
||||
<td class="text-right">{{sharesFolder(folder)}}</td>
|
||||
</tr>
|
||||
<tr ng-if="folderStats[folder.ID].LastFile">
|
||||
<th><span class="glyphicon glyphicon-transfer"></span> <span translate>Last File Received</span></th>
|
||||
<td class="text-right">
|
||||
<span title="{{folderStats[folder.ID].LastFile.Filename}} @ {{folderStats[folder.ID].LastFile.At | date:'yyyy-MM-dd HH:mm'}}">
|
||||
{{folderStats[folder.ID].LastFile.Filename | basename}}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -179,6 +188,7 @@
|
||||
<!-- This device -->
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="visible-xs"><h3><span translate>Devices</span></h3><hr></div>
|
||||
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
|
||||
<div class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer">
|
||||
<h3 class="panel-title">
|
||||
@@ -236,13 +246,13 @@
|
||||
<div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.DeviceID]._total | number:0}}%"></div>
|
||||
<h3 class="panel-title">
|
||||
<identicon data-value="deviceCfg.DeviceID"></identicon> {{deviceName(deviceCfg)}}
|
||||
<span ng-switch="deviceStatus(deviceCfg)" class="pull-right hidden-xs text-{{deviceClass(deviceCfg)}}">
|
||||
<span translate ng-switch-when="insync">Up to Date</span>
|
||||
<span ng-switch="deviceStatus(deviceCfg)" class="pull-right text-{{deviceClass(deviceCfg)}}">
|
||||
<span ng-switch-when="insync"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="syncing">
|
||||
<span translate>Syncing</span> ({{completion[deviceCfg.DeviceID]._total | number:0}}%)
|
||||
<span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.DeviceID]._total | number:0}}%)
|
||||
</span>
|
||||
<span translate ng-switch-when="disconnected">Disconnected</span>
|
||||
<span translate ng-switch-when="unused">Unused</span>
|
||||
<span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">◼</span></span>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
@@ -276,8 +286,8 @@
|
||||
</tr>
|
||||
<tr ng-if="!connections[deviceCfg.DeviceID]">
|
||||
<th><span class="glyphicon glyphicon-eye-open"></span> <span translate>Last seen</span></th>
|
||||
<td translate ng-if="!stats[deviceCfg.DeviceID].LastSeenDays || stats[deviceCfg.DeviceID].LastSeenDays >= 365" class="text-right">Never</td>
|
||||
<td ng-if="stats[deviceCfg.DeviceID].LastSeenDays < 365" class="text-right">{{stats[deviceCfg.DeviceID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
|
||||
<td translate ng-if="!deviceStats[deviceCfg.DeviceID].LastSeenDays || deviceStats[deviceCfg.DeviceID].LastSeenDays >= 365" class="text-right">Never</td>
|
||||
<td ng-if="deviceStats[deviceCfg.DeviceID].LastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.DeviceID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
|
||||
</tr>
|
||||
<tr ng-if="deviceFolders(deviceCfg).length > 0">
|
||||
<th><span class="glyphicon glyphicon-hdd"></span> <span translate>Folders</span></th>
|
||||
@@ -300,6 +310,73 @@
|
||||
</div>
|
||||
</div> <!-- /row -->
|
||||
|
||||
<!-- Device Rejections -->
|
||||
|
||||
<div ng-repeat="(device, event) in deviceRejections" class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<identicon data-value="device"></identicon> <span translate>New Device</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
<small>{{ event.time | date:"H:mm:ss" }}:</small>
|
||||
<span translate translate-value-device="{{ device }}" translate-value-address="{{ event.data.address }}">
|
||||
Device {%device%} ({%address%}) wants to connect. Add new device?
|
||||
<span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="panel-footer clearfix">
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-sm btn-success" ng-click="addNewDeviceID(device)"><span class="glyphicon glyphicon-ok"></span> <span translate>Add</span></button>
|
||||
<button class="btn btn-sm btn-danger" ng-click="ignoreRejectedDevice(device)"><span class="glyphicon glyphicon-remove"></span> <span translate>Ignore</span></button>
|
||||
<button class="btn btn-sm btn-default" ng-click="dismissDeviceRejection(device)"><span class="glyphicon glyphicon-time"></span> <span translate>Later</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Folder Rejections -->
|
||||
|
||||
<div ng-repeat="(key, event) in folderRejections" class="row reject">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<span translate ng-if="!folders[event.data.folder]">New Folder</span>
|
||||
<span translate ng-if="folders[event.data.folder]">Share Folder</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
<small>{{ event.time | date:"H:mm:ss" }}:</small>
|
||||
<span translate translate-value-device="{{ deviceName(findDevice(event.data.device)) }}" translate-value-folder="{{ event.data.folder }}">
|
||||
{%device%} wants to share folder "{%folder%}".
|
||||
</span>
|
||||
<span translate ng-if="folders[event.data.folder]">Share this folder?</span>
|
||||
<span translate ng-if="!folders[event.data.folder]">Add new folder?</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="panel-footer clearfix">
|
||||
<div class="pull-right">
|
||||
<button class="btn btn-sm btn-success" ng-click="addFolderAndShare(event.data.folder, event.data.device)" ng-if="!folders[event.data.folder]">
|
||||
<span class="glyphicon glyphicon-ok"></span> <span translate>Add</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-success" ng-click="shareFolderWithDevice(event.data.folder, event.data.device)" ng-if="folders[event.data.folder]">
|
||||
<span class="glyphicon glyphicon-ok"></span> <span translate>Share</span>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-default" ng-click="dismissFolderRejection(event.data.folder, event.data.device)">
|
||||
<span class="glyphicon glyphicon-time"></span> <span translate>Later</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Errors -->
|
||||
|
||||
<div ng-if="errorList().length > 0" class="row">
|
||||
@@ -321,14 +398,14 @@
|
||||
|
||||
<!-- Bottom bar -->
|
||||
|
||||
<nav class="navbar navbar-default navbar-fixed-bottom hidden-xs">
|
||||
<nav class="navbar navbar-default navbar-fixed-bottom">
|
||||
<div class="container">
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a class="navbar-link" href="http://discourse.syncthing.net/"><span translate>Support / Forum</span></a></li>
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/releases"><span translate>Latest Release</span></a></li>
|
||||
<li><a class="navbar-link" href="http://discourse.syncthing.net/category/documentation"><span translate>Documentation</span></a></li>
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/issues"><span translate>Bugs</span></a></li>
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing"><span translate>Source Code</span></a></li>
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/wiki"><span class="glyphicon glyphicon-book"></span> <span translate>Documentation</span></a></li>
|
||||
<li><a class="navbar-link" href="https://discourse.syncthing.net"><span class="glyphicon glyphicon-question-sign"></span> <span translate>Support</span></a></li>
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/releases"><span class="glyphicon glyphicon-info-sign"></span> <span translate>Changelog</span></a></li>
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/issues"><span class="glyphicon glyphicon-warning-sign"></span> <span translate>Bugs</span></a></li>
|
||||
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing"><span class="glyphicon glyphicon-wrench"></span> <span translate>Source Code</span></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -341,6 +418,14 @@
|
||||
</p>
|
||||
</modal>
|
||||
|
||||
<!-- HTTP error modal -->
|
||||
|
||||
<modal id="httpError" status="danger" icon="exclamation-sign" title="{{'Connection Error' | translate}}">
|
||||
<p translate>
|
||||
Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.
|
||||
</p>
|
||||
</modal>
|
||||
|
||||
<!-- Restarting modal -->
|
||||
|
||||
<modal id="restarting" icon="refresh" title="{{'Restarting' | translate}}" status="info">
|
||||
@@ -355,7 +440,7 @@
|
||||
|
||||
<!-- Shutdown modal -->
|
||||
|
||||
<modal id="shutdown" icon="off" status="success" title="Shutdown Complete">
|
||||
<modal id="shutdown" icon="off" status="success" title="{{'Shutdown Complete' | translate}}">
|
||||
<p translate>Syncthing has been shut down.</p>
|
||||
</modal>
|
||||
|
||||
@@ -598,7 +683,7 @@
|
||||
|
||||
<hr/>
|
||||
|
||||
<p class="small"><span translate>Quick guide to supported patterns</span> (<a href="https://discourse.syncthing.net/t/80" translate>full documentation</a>):</p>
|
||||
<p class="small"><span translate>Quick guide to supported patterns</span> (<a href="https://github.com/syncthing/syncthing/wiki/Ignoring-Files" target="_blank" translate>full documentation</a>):</p>
|
||||
<dl class="dl-horizontal dl-narrow small">
|
||||
<dt><code>!</code></dt> <dd><span translate>Inversion of the given condition (i.e. do not exclude)</span></dd>
|
||||
<dt><code>*</code></dt> <dd><span translate>Single level wildcard (matches within a directory only)</span></dd>
|
||||
@@ -607,7 +692,7 @@
|
||||
</dl>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="pull-left"><span translate>Editing</span> <code>{{currentFolder.Path}}/.stignore</code></div>
|
||||
<div class="pull-left"><span translate>Editing</span> <code>{{currentFolder.Path}}{{system.pathSeparator}}.stignore</code></div>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-dismiss="modal" ng-click="saveIgnores()"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
||||
</div>
|
||||
@@ -648,14 +733,14 @@
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<span translate>Enable UPnP</span> <input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.UPnPEnabled">
|
||||
<input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.UPnPEnabled"> <span translate>Enable UPnP</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<span translate>Global Discovery</span> <input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.GlobalAnnEnabled">
|
||||
<input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.GlobalAnnEnabled"> <span translate>Global Discovery</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -664,14 +749,14 @@
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label ng-if="upgradeInfo">
|
||||
<span translate>Automatic upgrades</span> <input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.AutoUpgradeEnabled">
|
||||
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.AutoUpgradeEnabled"> <span translate>Automatic upgrades</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<span translate>Local Discovery</span> <input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.LocalAnnEnabled">
|
||||
<input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.LocalAnnEnabled"> <span translate>Local Discovery</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -698,21 +783,21 @@
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<span translate>Use HTTPS for GUI</span> <input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS">
|
||||
<input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS"> <span translate>Use HTTPS for GUI</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<span translate>Start Browser</span> <input id="StartBrowser" type="checkbox" ng-model="tmpOptions.StartBrowser">
|
||||
<input id="StartBrowser" type="checkbox" ng-model="tmpOptions.StartBrowser"> <span translate>Start Browser</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<span translate>Anonymous Usage Reporting</span> <input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled"> (<a translate ng-click="showURPreview()" href="#">Preview</a>)
|
||||
<input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled"> <span translate>Anonymous Usage Reporting</span> (<a translate ng-click="showURPreview()" href="#">Preview</a>)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -781,39 +866,57 @@
|
||||
|
||||
<!-- Needed files modal -->
|
||||
|
||||
<modal id="needed" large="yes" status="info" icon="cloud-download" close="yes" title="Out of Sync Items">
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr ng-repeat="f in needed" ng-init="a = needAction(f)">
|
||||
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
|
||||
<td title="{{f.Name}}">{{f.Name | basename}}</td>
|
||||
<td>
|
||||
<span ng-if="a == 'sync' && progress[neededFolder] && progress[neededFolder][f.Name]">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.Name].Reused}}%"></div>
|
||||
<div class="progress-bar" style="width: {{progress[neededFolder][f.Name].CopiedFromOrigin}}%"></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.Name].CopiedFromElsewhere}}%"></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.Name].Pulled}}%"></div>
|
||||
<div class="progress-bar progress-bar-danger" style="width: {{progress[neededFolder][f.Name].Pulling}}%"></div>
|
||||
<span class="show frontal">{{progress[neededFolder][f.Name].BytesDone | binary}}B / {{progress[neededFolder][f.Name].BytesTotal | binary}}B</span>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<span translate>Legend:</span>
|
||||
<div class="progress progress-striped active">
|
||||
<modal id="needed" large="yes" status="info" icon="cloud-download" close="yes" title="{{'Out of Sync Items' | translate}}">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" style="width: 20%"><span translate class="show">Reused</span></div>
|
||||
<div class="progress-bar" style="width: 20%"><span translate class="show">Copied from original</span></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: 20%"><span translate class="show">Copied from elsewhere</span></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: 20%"><span translate class="show">Downloaded</span></div>
|
||||
<div class="progress-bar progress-bar-danger" style="width: 20%"><span translate class="show">Downloading</span></div>
|
||||
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: 20%"><span translate class="show">Downloading</span></div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr ng-repeat="f in needed.progress" ng-init="a = needAction(f)">
|
||||
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
|
||||
<td title="{{f.Name}}">{{f.Name | basename}}</td>
|
||||
<td ng-if="a == 'sync' && progress[neededFolder] && progress[neededFolder][f.Name]">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.Name].Reused}}%"></div>
|
||||
<div class="progress-bar" style="width: {{progress[neededFolder][f.Name].CopiedFromOrigin}}%"></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.Name].CopiedFromElsewhere}}%"></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.Name].Pulled}}%"></div>
|
||||
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: {{progress[neededFolder][f.Name].Pulling}}%"></div>
|
||||
<span class="show frontal">
|
||||
{{progress[neededFolder][f.Name].BytesDone | binary}}B / {{progress[neededFolder][f.Name].BytesTotal | binary}}B
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right small-data" ng-if="a != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.Name]">
|
||||
<span ng-if="f.Size > 0">{{f.Size | binary}}B</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="f in needed.queued" ng-init="a = needAction(f)">
|
||||
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
|
||||
<td title="{{f.Name}}">{{f.Name | basename}}</td>
|
||||
<td class="text-right small-data">
|
||||
<span ng-if="$index != 0" class="glyphicon glyphicon-chevron-up" ng-click="bumpFile(neededFolder, f.Name)"></span>
|
||||
<span ng-if="f.Size > 0">{{f.Size | binary}}B</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-repeat="f in needed.rest" ng-init="a = needAction(f)">
|
||||
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
|
||||
<td title="{{f.Name}}">{{f.Name | basename}}</td>
|
||||
<td class="text-right small-data"><span ng-if="f.Size > 0">{{f.Size | binary}}B</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</modal>
|
||||
|
||||
<!-- About modal -->
|
||||
|
||||
<modal id="about" large="yes" close="yes" status="info" title="About">
|
||||
<modal id="about" large="yes" close="yes" status="info" title="{{'About' | translate}}">
|
||||
<h1 class="text-center"><img alt="Syncthing" title="Syncthing" src="assets/img/logo-text-256.png" style="vertical-align: -16px" height="100" width="366"/><br/><small>{{version}}</small></h1>
|
||||
<hr/>
|
||||
|
||||
@@ -829,12 +932,15 @@
|
||||
<li>Ben Schulz</li>
|
||||
<li>Ben Sidhom</li>
|
||||
<li>Brandon Philips</li>
|
||||
<li>Brendan Long</li>
|
||||
<li>Caleb Callaway</li>
|
||||
<li>Cathryne Linenweaver</li>
|
||||
<li>Chris Joel</li>
|
||||
<li>Daniel Martí</li>
|
||||
<li>Dennis Wilson</li>
|
||||
<li>Dominik Heidler</li>
|
||||
<li>Emil Hessman</li>
|
||||
<li>Federico Castagnini</li>
|
||||
<li>Felix Ableitner</li>
|
||||
<li>Felix Unterpaintner</li>
|
||||
<li>Gilli Sigurdsson</li>
|
||||
@@ -844,10 +950,12 @@
|
||||
<li>Lode Hoste</li>
|
||||
<li>Marcin Dziadus</li>
|
||||
<li>Michael Tilli</li>
|
||||
<li>Peter Hoeg</li>
|
||||
<li>Philippe Schommers</li>
|
||||
<li>Phill Luby</li>
|
||||
<li>Piotr Bejda</li>
|
||||
<li>Ryan Sullivan</li>
|
||||
<li>Tim Abell</li>
|
||||
<li>Tomas Cerveny</li>
|
||||
<li>Tully Robinson</li>
|
||||
<li>Veeti Paananen</li>
|
||||
|
||||
@@ -104,15 +104,12 @@ function decimals(val, num) {
|
||||
return decs;
|
||||
}
|
||||
|
||||
function randomString(len, bits) {
|
||||
bits = bits || 36;
|
||||
var outStr = "",
|
||||
newStr;
|
||||
while (outStr.length < len) {
|
||||
newStr = Math.random().toString(bits).slice(2);
|
||||
outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
|
||||
function randomString(len) {
|
||||
var i, result = '', chars = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-';
|
||||
for (i = 0; i < len; i++) {
|
||||
result += chars[Math.round(Math.random() * (chars.length - 1))];
|
||||
}
|
||||
return outStr.toLowerCase();
|
||||
return result;
|
||||
}
|
||||
|
||||
function isEmptyObject(obj) {
|
||||
|
||||
@@ -10,21 +10,21 @@ angular.module('syncthing.core')
|
||||
var restarting = false;
|
||||
|
||||
function initController() {
|
||||
|
||||
LocaleService.autoConfigLocale();
|
||||
|
||||
refreshSystem();
|
||||
refreshConfig();
|
||||
refreshConnectionStats();
|
||||
refreshDeviceStats();
|
||||
refreshFolderStats();
|
||||
|
||||
$http.get(urlbase + '/version').success(function (data) {
|
||||
$scope.version = data.version;
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
|
||||
$http.get(urlbase + '/report').success(function (data) {
|
||||
$scope.reportData = data;
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
|
||||
$http.get(urlbase + '/upgrade').success(function (data) {
|
||||
$scope.upgradeInfo = data;
|
||||
@@ -46,13 +46,16 @@ angular.module('syncthing.core')
|
||||
$scope.model = {};
|
||||
$scope.myID = '';
|
||||
$scope.devices = [];
|
||||
$scope.deviceRejections = {};
|
||||
$scope.folderRejections = {};
|
||||
$scope.protocolChanged = false;
|
||||
$scope.reportData = {};
|
||||
$scope.reportPreview = false;
|
||||
$scope.folders = {};
|
||||
$scope.seenError = '';
|
||||
$scope.upgradeInfo = null;
|
||||
$scope.stats = {};
|
||||
$scope.deviceStats = {};
|
||||
$scope.folderStats = {};
|
||||
$scope.progress = {};
|
||||
|
||||
$(window).bind('beforeunload', function () {
|
||||
@@ -102,6 +105,30 @@ angular.module('syncthing.core')
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('HTTPError', function (event, arg) {
|
||||
// Emitted when a HTTP call fails. We use the status code to try
|
||||
// to figure out what's wrong.
|
||||
|
||||
if (navigatingAway || !online) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('HTTPError', arg);
|
||||
online = false;
|
||||
if (!restarting) {
|
||||
if (arg.status === 0) {
|
||||
// A network error, not an HTTP error
|
||||
$scope.$emit('UIOffline');
|
||||
} else if (arg.status >= 400 && arg.status <= 599) {
|
||||
// A genuine HTTP error
|
||||
$('#networkError').modal('hide');
|
||||
$('#restarting').modal('hide');
|
||||
$('#shutdown').modal('hide');
|
||||
$('#httpError').modal();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('StateChanged', function (event, arg) {
|
||||
var data = arg.data;
|
||||
if ($scope.model[data.folder]) {
|
||||
@@ -112,6 +139,7 @@ angular.module('syncthing.core')
|
||||
$scope.$on('LocalIndexUpdated', function (event, arg) {
|
||||
var data = arg.data;
|
||||
refreshFolder(data.folder);
|
||||
refreshFolderStats();
|
||||
|
||||
// Update completion status for all devices that we share this folder with.
|
||||
$scope.folders[data.folder].Devices.forEach(function (deviceCfg) {
|
||||
@@ -165,12 +193,20 @@ angular.module('syncthing.core')
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('DeviceRejected', function (event, arg) {
|
||||
$scope.deviceRejections[arg.data.device] = arg;
|
||||
});
|
||||
|
||||
$scope.$on('FolderRejected', function (event, arg) {
|
||||
$scope.folderRejections[arg.data.folder + "-" + arg.data.device] = arg;
|
||||
});
|
||||
|
||||
$scope.$on('ConfigSaved', function (event, arg) {
|
||||
updateLocalConfig(arg.data);
|
||||
|
||||
$http.get(urlbase + '/config/sync').success(function (data) {
|
||||
$scope.configInSync = data.configInSync;
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
});
|
||||
|
||||
$scope.$on('DownloadProgress', function (event, arg) {
|
||||
@@ -220,6 +256,10 @@ angular.module('syncthing.core')
|
||||
console.log("DownloadProgress", $scope.progress);
|
||||
});
|
||||
|
||||
$scope.emitHTTPError = function (data, status, headers, config) {
|
||||
$scope.$emit('HTTPError', {data: data, status: status, headers: headers, config: config});
|
||||
};
|
||||
|
||||
var debouncedFuncs = {};
|
||||
|
||||
function refreshFolder(folder) {
|
||||
@@ -229,7 +269,7 @@ angular.module('syncthing.core')
|
||||
$http.get(urlbase + '/model?folder=' + encodeURIComponent(folder)).success(function (data) {
|
||||
$scope.model[folder] = data;
|
||||
console.log("refreshFolder", folder, data);
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
}, 1000, true);
|
||||
}
|
||||
debouncedFuncs[key]();
|
||||
@@ -276,7 +316,7 @@ angular.module('syncthing.core')
|
||||
}
|
||||
$scope.announceServersFailed = failed;
|
||||
console.log("refreshSystem", data);
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
}
|
||||
|
||||
function refreshCompletion(device, folder) {
|
||||
@@ -305,7 +345,7 @@ angular.module('syncthing.core')
|
||||
$scope.completion[device]._total = tot / cnt;
|
||||
|
||||
console.log("refreshCompletion", device, folder, $scope.completion[device]);
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
}, 1000, true);
|
||||
}
|
||||
debouncedFuncs[key]();
|
||||
@@ -332,25 +372,25 @@ angular.module('syncthing.core')
|
||||
}
|
||||
$scope.connections = data;
|
||||
console.log("refreshConnections", data);
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
}
|
||||
|
||||
function refreshErrors() {
|
||||
$http.get(urlbase + '/errors').success(function (data) {
|
||||
$scope.errors = data.errors;
|
||||
console.log("refreshErrors", data);
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
}
|
||||
|
||||
function refreshConfig() {
|
||||
$http.get(urlbase + '/config').success(function (data) {
|
||||
updateLocalConfig(data);
|
||||
console.log("refreshConfig", data);
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
|
||||
$http.get(urlbase + '/config/sync').success(function (data) {
|
||||
$scope.configInSync = data.configInSync;
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
}
|
||||
|
||||
function refreshNeed(folder) {
|
||||
@@ -359,18 +399,30 @@ angular.module('syncthing.core')
|
||||
console.log("refreshNeed", folder, data);
|
||||
$scope.needed = data;
|
||||
}
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
}
|
||||
|
||||
var refreshDeviceStats = debounce(function () {
|
||||
$http.get(urlbase + "/stats/device").success(function (data) {
|
||||
$scope.stats = data;
|
||||
for (var device in $scope.stats) {
|
||||
$scope.stats[device].LastSeen = new Date($scope.stats[device].LastSeen);
|
||||
$scope.stats[device].LastSeenDays = (new Date() - $scope.stats[device].LastSeen) / 1000 / 86400;
|
||||
$scope.deviceStats = data;
|
||||
for (var device in $scope.deviceStats) {
|
||||
$scope.deviceStats[device].LastSeen = new Date($scope.deviceStats[device].LastSeen);
|
||||
$scope.deviceStats[device].LastSeenDays = (new Date() - $scope.deviceStats[device].LastSeen) / 1000 / 86400;
|
||||
}
|
||||
console.log("refreshDeviceStats", data);
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
}, 500);
|
||||
|
||||
var refreshFolderStats = debounce(function () {
|
||||
$http.get(urlbase + "/stats/folder").success(function (data) {
|
||||
$scope.folderStats = data;
|
||||
for (var folder in $scope.folderStats) {
|
||||
if ($scope.folderStats[folder].LastFile) {
|
||||
$scope.folderStats[folder].LastFile.At = new Date($scope.folderStats[folder].LastFile.At);
|
||||
}
|
||||
}
|
||||
console.log("refreshfolderStats", data);
|
||||
}).error($scope.emitHTTPError);
|
||||
}, 500);
|
||||
|
||||
$scope.refresh = function () {
|
||||
@@ -551,7 +603,7 @@ angular.module('syncthing.core')
|
||||
$http.get(urlbase + '/config/sync').success(function (data) {
|
||||
$scope.configInSync = data.configInSync;
|
||||
});
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
};
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
@@ -631,7 +683,7 @@ angular.module('syncthing.core')
|
||||
restarting = true;
|
||||
$http.post(urlbase + '/shutdown').success(function () {
|
||||
$('#shutdown').modal();
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
$scope.configInSync = true;
|
||||
};
|
||||
|
||||
@@ -683,6 +735,11 @@ angular.module('syncthing.core')
|
||||
return n.DeviceID !== $scope.currentDevice.DeviceID;
|
||||
});
|
||||
$scope.config.Devices = $scope.devices;
|
||||
// In case we later added the device manually, remove the ignoral
|
||||
// record.
|
||||
$scope.config.IgnoredDevices = $scope.config.IgnoredDevices.filter(function (id) {
|
||||
return id !== $scope.currentDevice.DeviceID;
|
||||
});
|
||||
|
||||
for (var id in $scope.folders) {
|
||||
$scope.folders[id].Devices = $scope.folders[id].Devices.filter(function (n) {
|
||||
@@ -694,10 +751,24 @@ angular.module('syncthing.core')
|
||||
};
|
||||
|
||||
$scope.saveDevice = function () {
|
||||
var deviceCfg, done, i;
|
||||
|
||||
$('#editDevice').modal('hide');
|
||||
deviceCfg = $scope.currentDevice;
|
||||
$scope.saveDeviceConfig($scope.currentDevice);
|
||||
};
|
||||
|
||||
$scope.addNewDeviceID = function (device) {
|
||||
var deviceCfg = {
|
||||
DeviceID: device,
|
||||
AddressesStr: 'dynamic',
|
||||
Compression: true,
|
||||
Introducer: false,
|
||||
selectedFolders: {}
|
||||
};
|
||||
$scope.saveDeviceConfig(deviceCfg);
|
||||
$scope.dismissDeviceRejection(device);
|
||||
};
|
||||
|
||||
$scope.saveDeviceConfig = function (deviceCfg) {
|
||||
var done, i;
|
||||
deviceCfg.Addresses = deviceCfg.AddressesStr.split(',').map(function (x) {
|
||||
return x.trim();
|
||||
});
|
||||
@@ -717,6 +788,11 @@ angular.module('syncthing.core')
|
||||
|
||||
$scope.devices.sort(deviceCompare);
|
||||
$scope.config.Devices = $scope.devices;
|
||||
// In case we are adding the device manually, remove the ignoral
|
||||
// record.
|
||||
$scope.config.IgnoredDevices = $scope.config.IgnoredDevices.filter(function (id) {
|
||||
return id !== deviceCfg.DeviceID;
|
||||
});
|
||||
|
||||
if (!$scope.editingSelf) {
|
||||
for (var id in deviceCfg.selectedFolders) {
|
||||
@@ -734,7 +810,6 @@ angular.module('syncthing.core')
|
||||
DeviceID: deviceCfg.DeviceID
|
||||
});
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
$scope.folders[id].Devices = $scope.folders[id].Devices.filter(function (n) {
|
||||
return n.DeviceID != deviceCfg.DeviceID;
|
||||
@@ -746,6 +821,16 @@ angular.module('syncthing.core')
|
||||
$scope.saveConfig();
|
||||
};
|
||||
|
||||
$scope.dismissDeviceRejection = function (device) {
|
||||
delete $scope.deviceRejections[device];
|
||||
};
|
||||
|
||||
$scope.ignoreRejectedDevice = function (device) {
|
||||
$scope.config.IgnoredDevices.push(device);
|
||||
$scope.saveConfig();
|
||||
$scope.dismissDeviceRejection(device);
|
||||
};
|
||||
|
||||
$scope.otherDevices = function () {
|
||||
return $scope.devices.filter(function (n) {
|
||||
return n.DeviceID !== $scope.myID;
|
||||
@@ -799,11 +884,14 @@ angular.module('syncthing.core')
|
||||
params: { current: newvalue }
|
||||
}).success(function (data) {
|
||||
$scope.directoryList = data;
|
||||
});
|
||||
}).error($scope.emitHTTPError);
|
||||
});
|
||||
|
||||
$scope.editFolder = function (deviceCfg) {
|
||||
$scope.currentFolder = angular.copy(deviceCfg);
|
||||
$scope.editFolder = function (folderCfg) {
|
||||
$scope.currentFolder = angular.copy(folderCfg);
|
||||
if ($scope.currentFolder.Path.slice(-1) == $scope.system.pathSeparator) {
|
||||
$scope.currentFolder.Path = $scope.currentFolder.Path.slice(0, -1);
|
||||
}
|
||||
$scope.currentFolder.selectedDevices = {};
|
||||
$scope.currentFolder.Devices.forEach(function (n) {
|
||||
$scope.currentFolder.selectedDevices[n.DeviceID] = true;
|
||||
@@ -852,6 +940,34 @@ angular.module('syncthing.core')
|
||||
$('#editFolder').modal();
|
||||
};
|
||||
|
||||
$scope.addFolderAndShare = function (folder, device) {
|
||||
$scope.dismissFolderRejection(folder, device);
|
||||
$scope.currentFolder = {
|
||||
ID: folder,
|
||||
selectedDevices: {}
|
||||
};
|
||||
$scope.currentFolder.selectedDevices[device] = true;
|
||||
|
||||
$scope.currentFolder.RescanIntervalS = 60;
|
||||
$scope.currentFolder.FileVersioningSelector = "none";
|
||||
$scope.currentFolder.simpleKeep = 5;
|
||||
$scope.currentFolder.staggeredMaxAge = 365;
|
||||
$scope.currentFolder.staggeredCleanInterval = 3600;
|
||||
$scope.currentFolder.staggeredVersionsPath = "";
|
||||
$scope.editingExisting = false;
|
||||
$scope.folderEditor.$setPristine();
|
||||
$('#editFolder').modal();
|
||||
};
|
||||
|
||||
$scope.shareFolderWithDevice = function (folder, device) {
|
||||
$scope.folders[folder].Devices.push({
|
||||
DeviceID: device
|
||||
});
|
||||
$scope.config.Folders = folderList($scope.folders);
|
||||
$scope.saveConfig();
|
||||
$scope.dismissFolderRejection(folder, device);
|
||||
};
|
||||
|
||||
$scope.saveFolder = function () {
|
||||
var folderCfg, done, i;
|
||||
|
||||
@@ -901,6 +1017,10 @@ angular.module('syncthing.core')
|
||||
$scope.saveConfig();
|
||||
};
|
||||
|
||||
$scope.dismissFolderRejection = function (folder, device) {
|
||||
delete $scope.folderRejections[folder + "-" + device];
|
||||
};
|
||||
|
||||
$scope.sharesFolder = function (folderCfg) {
|
||||
var names = [];
|
||||
folderCfg.Devices.forEach(function (device) {
|
||||
@@ -979,7 +1099,7 @@ angular.module('syncthing.core')
|
||||
};
|
||||
|
||||
$scope.setAPIKey = function (cfg) {
|
||||
cfg.APIKey = randomString(30, 32);
|
||||
cfg.APIKey = randomString(32);
|
||||
};
|
||||
|
||||
$scope.showURPreview = function () {
|
||||
@@ -1041,6 +1161,15 @@ angular.module('syncthing.core')
|
||||
$http.post(urlbase + "/scan?folder=" + encodeURIComponent(folder));
|
||||
};
|
||||
|
||||
$scope.bumpFile = function (folder, file) {
|
||||
$http.post(urlbase + "/bump?folder=" + encodeURIComponent(folder) + "&file=" + encodeURIComponent(file)).success(function (data) {
|
||||
if ($scope.neededFolder == folder) {
|
||||
console.log("bumpFile", folder, data);
|
||||
$scope.needed = data;
|
||||
}
|
||||
}).error($scope.emitHTTPError);
|
||||
};
|
||||
|
||||
// pseudo main. called on all definitions assigned
|
||||
initController();
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -20,6 +20,7 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -37,12 +38,13 @@ var l = logger.DefaultLogger
|
||||
const CurrentVersion = 7
|
||||
|
||||
type Configuration struct {
|
||||
Version int `xml:"version,attr"`
|
||||
Folders []FolderConfiguration `xml:"folder"`
|
||||
Devices []DeviceConfiguration `xml:"device"`
|
||||
GUI GUIConfiguration `xml:"gui"`
|
||||
Options OptionsConfiguration `xml:"options"`
|
||||
XMLName xml.Name `xml:"configuration" json:"-"`
|
||||
Version int `xml:"version,attr"`
|
||||
Folders []FolderConfiguration `xml:"folder"`
|
||||
Devices []DeviceConfiguration `xml:"device"`
|
||||
GUI GUIConfiguration `xml:"gui"`
|
||||
Options OptionsConfiguration `xml:"options"`
|
||||
IgnoredDevices []protocol.DeviceID `xml:"ignoredDevice"`
|
||||
XMLName xml.Name `xml:"configuration" json:"-"`
|
||||
|
||||
OriginalVersion int `xml:"-" json:"-"` // The version we read from disk, before any conversion
|
||||
Deprecated_Repositories []FolderConfiguration `xml:"repository" json:"-"`
|
||||
@@ -58,9 +60,9 @@ type FolderConfiguration struct {
|
||||
IgnorePerms bool `xml:"ignorePerms,attr"`
|
||||
Versioning VersioningConfiguration `xml:"versioning"`
|
||||
LenientMtimes bool `xml:"lenientMtimes"`
|
||||
Copiers int `xml:"copiers" default:"1"` // This defines how many files are handled concurrently.
|
||||
Pullers int `xml:"pullers" default:"16"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
|
||||
Finishers int `xml:"finishers" default:"1"` // Most of the time, should be equal to the number of copiers. These are CPU bound due to hashing.
|
||||
Copiers int `xml:"copiers" default:"1"` // This defines how many files are handled concurrently.
|
||||
Pullers int `xml:"pullers" default:"16"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
|
||||
Hashers int `xml:"hashers" default:"0"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
|
||||
|
||||
Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
|
||||
|
||||
@@ -92,13 +94,13 @@ func (f *FolderConfiguration) HasMarker() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
|
||||
if r.deviceIDs == nil {
|
||||
for _, n := range r.Devices {
|
||||
r.deviceIDs = append(r.deviceIDs, n.DeviceID)
|
||||
func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
|
||||
if f.deviceIDs == nil {
|
||||
for _, n := range f.Devices {
|
||||
f.deviceIDs = append(f.deviceIDs, n.DeviceID)
|
||||
}
|
||||
}
|
||||
return r.deviceIDs
|
||||
return f.deviceIDs
|
||||
}
|
||||
|
||||
type VersioningConfiguration struct {
|
||||
@@ -240,10 +242,13 @@ func (cfg *Configuration) WriteXML(w io.Writer) error {
|
||||
func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
fillNilSlices(&cfg.Options)
|
||||
|
||||
// Initialize an empty slice for folders if the config has none
|
||||
// Initialize an empty slices
|
||||
if cfg.Folders == nil {
|
||||
cfg.Folders = []FolderConfiguration{}
|
||||
}
|
||||
if cfg.IgnoredDevices == nil {
|
||||
cfg.IgnoredDevices = []protocol.DeviceID{}
|
||||
}
|
||||
|
||||
// Check for missing, bad or duplicate folder ID:s
|
||||
var seenFolders = map[string]*FolderConfiguration{}
|
||||
@@ -353,9 +358,6 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
if cfg.Folders[i].Pullers == 0 {
|
||||
cfg.Folders[i].Pullers = 16
|
||||
}
|
||||
if cfg.Folders[i].Finishers == 0 {
|
||||
cfg.Folders[i].Finishers = 1
|
||||
}
|
||||
sort.Sort(FolderDeviceConfigurationList(cfg.Folders[i].Devices))
|
||||
}
|
||||
|
||||
@@ -369,6 +371,10 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
|
||||
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
|
||||
cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers)
|
||||
|
||||
if cfg.GUI.APIKey == "" {
|
||||
cfg.GUI.APIKey = randomString(32)
|
||||
}
|
||||
}
|
||||
|
||||
// ChangeRequiresRestart returns true if updating the configuration requires a
|
||||
@@ -674,3 +680,16 @@ func (l FolderDeviceConfigurationList) Swap(a, b int) {
|
||||
func (l FolderDeviceConfigurationList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
// randomCharset contains the characters that can make up a randomString().
|
||||
const randomCharset = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
|
||||
|
||||
// randomString returns a string of random characters (taken from
|
||||
// randomCharset) of the specified length.
|
||||
func randomString(l int) string {
|
||||
bs := make([]byte, l)
|
||||
for i := range bs {
|
||||
bs[i] = randomCharset[rand.Intn(len(randomCharset))]
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestDeviceConfig(t *testing.T) {
|
||||
RescanIntervalS: 600,
|
||||
Copiers: 1,
|
||||
Pullers: 16,
|
||||
Finishers: 1,
|
||||
Hashers: 0,
|
||||
},
|
||||
}
|
||||
expectedDevices := []DeviceConfiguration{
|
||||
|
||||
@@ -42,7 +42,7 @@ func (fn HandlerFunc) Changed(cfg Configuration) error {
|
||||
// A wrapper around a Configuration that manages loads, saves and published
|
||||
// notifications of changes to registered Handlers
|
||||
|
||||
type ConfigWrapper struct {
|
||||
type Wrapper struct {
|
||||
cfg Configuration
|
||||
path string
|
||||
|
||||
@@ -57,8 +57,8 @@ type ConfigWrapper struct {
|
||||
|
||||
// Wrap wraps an existing Configuration structure and ties it to a file on
|
||||
// disk.
|
||||
func Wrap(path string, cfg Configuration) *ConfigWrapper {
|
||||
w := &ConfigWrapper{cfg: cfg, path: path}
|
||||
func Wrap(path string, cfg Configuration) *Wrapper {
|
||||
w := &Wrapper{cfg: cfg, path: path}
|
||||
w.replaces = make(chan Configuration)
|
||||
go w.Serve()
|
||||
return w
|
||||
@@ -66,7 +66,7 @@ func Wrap(path string, cfg Configuration) *ConfigWrapper {
|
||||
|
||||
// Load loads an existing file on disk and returns a new configuration
|
||||
// wrapper.
|
||||
func Load(path string, myID protocol.DeviceID) (*ConfigWrapper, error) {
|
||||
func Load(path string, myID protocol.DeviceID) (*Wrapper, error) {
|
||||
fd, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -84,7 +84,7 @@ func Load(path string, myID protocol.DeviceID) (*ConfigWrapper, error) {
|
||||
// Serve handles configuration replace events and calls any interested
|
||||
// handlers. It is started automatically by Wrap() and Load() and should not
|
||||
// be run manually.
|
||||
func (w *ConfigWrapper) Serve() {
|
||||
func (w *Wrapper) Serve() {
|
||||
for cfg := range w.replaces {
|
||||
w.sMut.Lock()
|
||||
subs := w.subs
|
||||
@@ -97,25 +97,25 @@ func (w *ConfigWrapper) Serve() {
|
||||
|
||||
// Stop stops the Serve() loop. Set and Replace operations will panic after a
|
||||
// Stop.
|
||||
func (w *ConfigWrapper) Stop() {
|
||||
func (w *Wrapper) Stop() {
|
||||
close(w.replaces)
|
||||
}
|
||||
|
||||
// Subscribe registers the given handler to be called on any future
|
||||
// configuration changes.
|
||||
func (w *ConfigWrapper) Subscribe(h Handler) {
|
||||
func (w *Wrapper) Subscribe(h Handler) {
|
||||
w.sMut.Lock()
|
||||
w.subs = append(w.subs, h)
|
||||
w.sMut.Unlock()
|
||||
}
|
||||
|
||||
// Raw returns the currently wrapped Configuration object.
|
||||
func (w *ConfigWrapper) Raw() Configuration {
|
||||
func (w *Wrapper) Raw() Configuration {
|
||||
return w.cfg
|
||||
}
|
||||
|
||||
// Replace swaps the current configuration object for the given one.
|
||||
func (w *ConfigWrapper) Replace(cfg Configuration) {
|
||||
func (w *Wrapper) Replace(cfg Configuration) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -127,7 +127,7 @@ func (w *ConfigWrapper) Replace(cfg Configuration) {
|
||||
|
||||
// Devices returns a map of devices. Device structures should not be changed,
|
||||
// other than for the purpose of updating via SetDevice().
|
||||
func (w *ConfigWrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
||||
func (w *Wrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
if w.deviceMap == nil {
|
||||
@@ -141,7 +141,7 @@ func (w *ConfigWrapper) Devices() map[protocol.DeviceID]DeviceConfiguration {
|
||||
|
||||
// SetDevice adds a new device to the configuration, or overwrites an existing
|
||||
// device with the same ID.
|
||||
func (w *ConfigWrapper) SetDevice(dev DeviceConfiguration) {
|
||||
func (w *Wrapper) SetDevice(dev DeviceConfiguration) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -161,7 +161,7 @@ func (w *ConfigWrapper) SetDevice(dev DeviceConfiguration) {
|
||||
|
||||
// Devices returns a map of folders. Folder structures should not be changed,
|
||||
// other than for the purpose of updating via SetFolder().
|
||||
func (w *ConfigWrapper) Folders() map[string]FolderConfiguration {
|
||||
func (w *Wrapper) Folders() map[string]FolderConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
if w.folderMap == nil {
|
||||
@@ -181,7 +181,7 @@ func (w *ConfigWrapper) Folders() map[string]FolderConfiguration {
|
||||
|
||||
// SetFolder adds a new folder to the configuration, or overwrites an existing
|
||||
// folder with the same ID.
|
||||
func (w *ConfigWrapper) SetFolder(fld FolderConfiguration) {
|
||||
func (w *Wrapper) SetFolder(fld FolderConfiguration) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -200,14 +200,14 @@ func (w *ConfigWrapper) SetFolder(fld FolderConfiguration) {
|
||||
}
|
||||
|
||||
// Options returns the current options configuration object.
|
||||
func (w *ConfigWrapper) Options() OptionsConfiguration {
|
||||
func (w *Wrapper) Options() OptionsConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
return w.cfg.Options
|
||||
}
|
||||
|
||||
// SetOptions replaces the current options configuration object.
|
||||
func (w *ConfigWrapper) SetOptions(opts OptionsConfiguration) {
|
||||
func (w *Wrapper) SetOptions(opts OptionsConfiguration) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
w.cfg.Options = opts
|
||||
@@ -215,14 +215,14 @@ func (w *ConfigWrapper) SetOptions(opts OptionsConfiguration) {
|
||||
}
|
||||
|
||||
// GUI returns the current GUI configuration object.
|
||||
func (w *ConfigWrapper) GUI() GUIConfiguration {
|
||||
func (w *Wrapper) GUI() GUIConfiguration {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
return w.cfg.GUI
|
||||
}
|
||||
|
||||
// SetGUI replaces the current GUI configuration object.
|
||||
func (w *ConfigWrapper) SetGUI(gui GUIConfiguration) {
|
||||
func (w *Wrapper) SetGUI(gui GUIConfiguration) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
w.cfg.GUI = gui
|
||||
@@ -230,7 +230,7 @@ func (w *ConfigWrapper) SetGUI(gui GUIConfiguration) {
|
||||
}
|
||||
|
||||
// InvalidateFolder sets the invalid marker on the given folder.
|
||||
func (w *ConfigWrapper) InvalidateFolder(id string, err string) {
|
||||
func (w *Wrapper) InvalidateFolder(id string, err string) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
@@ -245,8 +245,21 @@ func (w *ConfigWrapper) InvalidateFolder(id string, err string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns whether or not connection attempts from the given device should be
|
||||
// silently ignored.
|
||||
func (w *Wrapper) IgnoredDevice(id protocol.DeviceID) bool {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
for _, device := range w.cfg.IgnoredDevices {
|
||||
if device == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Save writes the configuration to disk, and generates a ConfigSaved event.
|
||||
func (w *ConfigWrapper) Save() error {
|
||||
func (w *Wrapper) Save() error {
|
||||
fd, err := ioutil.TempFile(filepath.Dir(w.path), "cfg")
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -94,7 +94,7 @@ func (d *UDPClient) broadcast(pkt []byte) {
|
||||
|
||||
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)
|
||||
l.Warnf("discover %s: broadcast: %v; trying again in %v", d.url, err, d.errorRetryInterval)
|
||||
select {
|
||||
case <-d.stop:
|
||||
return
|
||||
@@ -106,7 +106,7 @@ func (d *UDPClient) broadcast(pkt []byte) {
|
||||
|
||||
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)
|
||||
l.Warnf("discover %s: broadcast: %v; trying again in %v", d.url, err, d.errorRetryInterval)
|
||||
select {
|
||||
case <-d.stop:
|
||||
return
|
||||
@@ -125,13 +125,13 @@ func (d *UDPClient) broadcast(pkt []byte) {
|
||||
var ok bool
|
||||
|
||||
if debug {
|
||||
l.Debugf("Global UDP discovery (%s): send announcement -> %v\n%s", d.url, remote, hex.Dump(pkt))
|
||||
l.Debugf("discover %s: broadcast: Sending self announcement to %v", d.url, remote)
|
||||
}
|
||||
|
||||
_, err := conn.WriteTo(pkt, remote)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: warning: %s", d.url, err)
|
||||
l.Debugf("discover %s: broadcast: Failed to send self announcement: %s", d.url, err)
|
||||
}
|
||||
ok = false
|
||||
} else {
|
||||
@@ -141,7 +141,7 @@ func (d *UDPClient) broadcast(pkt []byte) {
|
||||
|
||||
res := d.Lookup(d.id)
|
||||
if debug {
|
||||
l.Debugf("discover %s: external lookup check: %v", d.url, res)
|
||||
l.Debugf("discover %s: broadcast: Self-lookup returned: %v", d.url, res)
|
||||
}
|
||||
ok = len(res) > 0
|
||||
}
|
||||
@@ -163,7 +163,7 @@ 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)
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -171,7 +171,7 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
||||
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)
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
||||
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)
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -189,7 +189,7 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
||||
_, err = conn.Write(buf)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("discover %s: %v; no external lookup", d.url, err)
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -202,20 +202,16 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
||||
return nil
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("discover %s: %v; no external lookup", d.url, err)
|
||||
l.Debugf("discover %s: Lookup(%s): %s", d.url, device, 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)
|
||||
l.Debugf("discover %s: Lookup(%s): %s\n%s", d.url, device, err, hex.Dump(buf[:n]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -225,6 +221,9 @@ func (d *UDPClient) Lookup(device protocol.DeviceID) []string {
|
||||
deviceAddr := net.JoinHostPort(net.IP(a.IP).String(), strconv.Itoa(int(a.Port)))
|
||||
addrs = append(addrs, deviceAddr)
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("discover %s: Lookup(%s) result: %v", d.url, device, addrs)
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
|
||||
bb, err := beacon.NewBroadcast(localPort)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
l.Debugln("discover: Start local v4:", err)
|
||||
}
|
||||
l.Infoln("Local discovery over IPv4 unavailable")
|
||||
} else {
|
||||
@@ -85,7 +85,7 @@ func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
|
||||
mb, err := beacon.NewMulticast(localMCAddr)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
l.Debugln("discover: Start local v6:", err)
|
||||
}
|
||||
l.Infoln("Local discovery over IPv6 unavailable")
|
||||
} else {
|
||||
@@ -247,10 +247,10 @@ func (d *Discoverer) announcementPkt() *Announce {
|
||||
for _, astr := range d.listenAddrs {
|
||||
addr, err := net.ResolveTCPAddr("tcp", astr)
|
||||
if err != nil {
|
||||
l.Warnln("%v: not announcing %s", err, astr)
|
||||
l.Warnln("discover: %v: not announcing %s", err, astr)
|
||||
continue
|
||||
} else if debug {
|
||||
l.Debugf("discover: announcing %s: %#v", astr, addr)
|
||||
l.Debugf("discover: resolved %s as %#v", astr, addr)
|
||||
}
|
||||
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
||||
addrs = append(addrs, Address{Port: uint16(addr.Port)})
|
||||
@@ -295,16 +295,19 @@ func (d *Discoverer) recvAnnouncements(b beacon.Interface) {
|
||||
for {
|
||||
buf, addr := b.Recv()
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover: read announcement from %s:\n%s", addr, hex.Dump(buf))
|
||||
}
|
||||
|
||||
var pkt Announce
|
||||
err := pkt.UnmarshalXDR(buf)
|
||||
if err != nil && err != io.EOF {
|
||||
if debug {
|
||||
l.Debugf("discover: Failed to unmarshal local announcement from %s:\n%s", addr, hex.Dump(buf))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover: Received local announcement from %s for %s", addr, protocol.DeviceIDFromBytes(pkt.This.ID))
|
||||
}
|
||||
|
||||
var newDevice bool
|
||||
if bytes.Compare(pkt.This.ID, d.myID[:]) != 0 {
|
||||
newDevice = d.registerDevice(addr, pkt.This)
|
||||
@@ -352,7 +355,7 @@ func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool {
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("discover: register: %v -> %v", id, current)
|
||||
l.Debugf("discover: Caching %s addresses: %v", id, current)
|
||||
}
|
||||
|
||||
d.registry[id] = current
|
||||
@@ -375,7 +378,7 @@ func (d *Discoverer) filterCached(c []CacheEntry) []CacheEntry {
|
||||
for i := 0; i < len(c); {
|
||||
if ago := time.Since(c[i].Seen); ago > d.cacheLifetime {
|
||||
if debug {
|
||||
l.Debugf("removing cached address %s: seen %v ago", c[i].Address, ago)
|
||||
l.Debugf("discover: Removing cached address %s - seen %v ago", c[i].Address, ago)
|
||||
}
|
||||
c[i] = c[len(c)-1]
|
||||
c = c[:len(c)-1]
|
||||
|
||||
@@ -86,7 +86,7 @@ const BufferSize = 64
|
||||
|
||||
type Logger struct {
|
||||
subs map[int]*Subscription
|
||||
nextId int
|
||||
nextID int
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
@@ -120,15 +120,15 @@ func NewLogger() *Logger {
|
||||
func (l *Logger) Log(t EventType, data interface{}) {
|
||||
l.mutex.Lock()
|
||||
if debug {
|
||||
dl.Debugln("log", l.nextId, t.String(), data)
|
||||
dl.Debugln("log", l.nextID, t.String(), data)
|
||||
}
|
||||
e := Event{
|
||||
ID: l.nextId,
|
||||
ID: l.nextID,
|
||||
Time: time.Now(),
|
||||
Type: t,
|
||||
Data: data,
|
||||
}
|
||||
l.nextId++
|
||||
l.nextID++
|
||||
for _, s := range l.subs {
|
||||
if s.mask&t != 0 {
|
||||
select {
|
||||
@@ -148,10 +148,10 @@ func (l *Logger) Subscribe(mask EventType) *Subscription {
|
||||
}
|
||||
s := &Subscription{
|
||||
mask: mask,
|
||||
id: l.nextId,
|
||||
id: l.nextID,
|
||||
events: make(chan Event, BufferSize),
|
||||
}
|
||||
l.nextId++
|
||||
l.nextID++
|
||||
l.subs[s.id] = s
|
||||
l.mutex.Unlock()
|
||||
return s
|
||||
@@ -187,6 +187,10 @@ func (s *Subscription) Poll(timeout time.Duration) (Event, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Subscription) C() <-chan Event {
|
||||
return s.events
|
||||
}
|
||||
|
||||
type BufferedSubscription struct {
|
||||
sub *Subscription
|
||||
buf []Event
|
||||
|
||||
@@ -126,7 +126,7 @@ type BlockFinder struct {
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
func NewBlockFinder(db *leveldb.DB, cfg *config.ConfigWrapper) *BlockFinder {
|
||||
func NewBlockFinder(db *leveldb.DB, cfg *config.Wrapper) *BlockFinder {
|
||||
if blockFinder != nil {
|
||||
return blockFinder
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ func TestConcurrentSetClear(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
os.RemoveAll("testdata/concurrent-set-clear.db")
|
||||
db, err := leveldb.OpenFile("testdata/concurrent-set-clear.db", &opt.Options{CachedOpenFiles: 10})
|
||||
db, err := leveldb.OpenFile("testdata/concurrent-set-clear.db", &opt.Options{OpenFilesCacheCapacity: 10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func TestConcurrentSetOnly(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
os.RemoveAll("testdata/concurrent-set-only.db")
|
||||
db, err := leveldb.OpenFile("testdata/concurrent-set-only.db", &opt.Options{CachedOpenFiles: 10})
|
||||
db, err := leveldb.OpenFile("testdata/concurrent-set-only.db", &opt.Options{OpenFilesCacheCapacity: 10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -166,8 +166,6 @@ func globalKeyFolder(key []byte) []byte {
|
||||
|
||||
type deletionHandler func(db dbReader, batch dbWriter, folder, device, name []byte, dbi iterator.Iterator) uint64
|
||||
|
||||
type fileIterator func(f protocol.FileIntf) bool
|
||||
|
||||
func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo, deleteFn deletionHandler) uint64 {
|
||||
runtime.GC()
|
||||
|
||||
@@ -246,7 +244,7 @@ func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.File
|
||||
if debugDB {
|
||||
l.Debugln("generic replace; exists - compare")
|
||||
}
|
||||
var ef protocol.FileInfoTruncated
|
||||
var ef FileInfoTruncated
|
||||
ef.UnmarshalXDR(dbi.Value())
|
||||
if fs[fsi].Version > ef.Version ||
|
||||
(fs[fsi].Version == ef.Version && fs[fsi].Flags != ef.Flags) {
|
||||
@@ -308,7 +306,7 @@ func ldbReplace(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo) u
|
||||
|
||||
func ldbReplaceWithDelete(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo) uint64 {
|
||||
return ldbGenericReplace(db, folder, device, fs, func(db dbReader, batch dbWriter, folder, device, name []byte, dbi iterator.Iterator) uint64 {
|
||||
var tf protocol.FileInfoTruncated
|
||||
var tf FileInfoTruncated
|
||||
err := tf.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -378,7 +376,7 @@ func ldbUpdate(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo) ui
|
||||
continue
|
||||
}
|
||||
|
||||
var ef protocol.FileInfoTruncated
|
||||
var ef FileInfoTruncated
|
||||
err = ef.UnmarshalXDR(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -528,7 +526,7 @@ func ldbRemoveFromGlobal(db dbReader, batch dbWriter, folder, device, file []byt
|
||||
}
|
||||
}
|
||||
|
||||
func ldbWithHave(db *leveldb.DB, folder, device []byte, truncate bool, fn fileIterator) {
|
||||
func ldbWithHave(db *leveldb.DB, folder, device []byte, truncate bool, fn Iterator) {
|
||||
start := deviceKey(folder, device, nil) // before all folder/device files
|
||||
limit := deviceKey(folder, device, []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
|
||||
snap, err := db.GetSnapshot()
|
||||
@@ -559,7 +557,7 @@ func ldbWithHave(db *leveldb.DB, folder, device []byte, truncate bool, fn fileIt
|
||||
}
|
||||
}
|
||||
|
||||
func ldbWithAllFolderTruncated(db *leveldb.DB, folder []byte, fn func(device []byte, f protocol.FileInfoTruncated) bool) {
|
||||
func ldbWithAllFolderTruncated(db *leveldb.DB, folder []byte, fn func(device []byte, f FileInfoTruncated) bool) {
|
||||
runtime.GC()
|
||||
|
||||
start := deviceKey(folder, nil, nil) // before all folder/device files
|
||||
@@ -583,7 +581,7 @@ func ldbWithAllFolderTruncated(db *leveldb.DB, folder []byte, fn func(device []b
|
||||
|
||||
for dbi.Next() {
|
||||
device := deviceKeyDevice(dbi.Key())
|
||||
var f protocol.FileInfoTruncated
|
||||
var f FileInfoTruncated
|
||||
err := f.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -594,11 +592,11 @@ func ldbWithAllFolderTruncated(db *leveldb.DB, folder []byte, fn func(device []b
|
||||
}
|
||||
}
|
||||
|
||||
func ldbGet(db *leveldb.DB, folder, device, file []byte) protocol.FileInfo {
|
||||
func ldbGet(db *leveldb.DB, folder, device, file []byte) (protocol.FileInfo, bool) {
|
||||
nk := deviceKey(folder, device, file)
|
||||
bs, err := db.Get(nk, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return protocol.FileInfo{}
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -609,10 +607,10 @@ func ldbGet(db *leveldb.DB, folder, device, file []byte) protocol.FileInfo {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return f
|
||||
return f, true
|
||||
}
|
||||
|
||||
func ldbGetGlobal(db *leveldb.DB, folder, file []byte) protocol.FileInfo {
|
||||
func ldbGetGlobal(db *leveldb.DB, folder, file []byte, truncate bool) (FileIntf, bool) {
|
||||
k := globalKey(folder, file)
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
@@ -633,7 +631,7 @@ func ldbGetGlobal(db *leveldb.DB, folder, file []byte) protocol.FileInfo {
|
||||
}
|
||||
bs, err := snap.Get(k, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return protocol.FileInfo{}
|
||||
return nil, false
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -658,15 +656,14 @@ func ldbGetGlobal(db *leveldb.DB, folder, file []byte) protocol.FileInfo {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var f protocol.FileInfo
|
||||
err = f.UnmarshalXDR(bs)
|
||||
fi, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return f
|
||||
return fi, true
|
||||
}
|
||||
|
||||
func ldbWithGlobal(db *leveldb.DB, folder []byte, truncate bool, fn fileIterator) {
|
||||
func ldbWithGlobal(db *leveldb.DB, folder []byte, truncate bool, fn Iterator) {
|
||||
runtime.GC()
|
||||
|
||||
start := globalKey(folder, nil)
|
||||
@@ -754,7 +751,7 @@ func ldbAvailability(db *leveldb.DB, folder, file []byte) []protocol.DeviceID {
|
||||
return devices
|
||||
}
|
||||
|
||||
func ldbWithNeed(db *leveldb.DB, folder, device []byte, truncate bool, fn fileIterator) {
|
||||
func ldbWithNeed(db *leveldb.DB, folder, device []byte, truncate bool, fn Iterator) {
|
||||
runtime.GC()
|
||||
|
||||
start := globalKey(folder, nil)
|
||||
@@ -938,9 +935,9 @@ func ldbDropFolder(db *leveldb.DB, folder []byte) {
|
||||
dbi.Release()
|
||||
}
|
||||
|
||||
func unmarshalTrunc(bs []byte, truncate bool) (protocol.FileIntf, error) {
|
||||
func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
|
||||
if truncate {
|
||||
var tf protocol.FileInfoTruncated
|
||||
var tf FileInfoTruncated
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
return tf, err
|
||||
} else {
|
||||
|
||||
@@ -30,14 +30,6 @@ import (
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type fileRecord struct {
|
||||
File protocol.FileInfo
|
||||
Usage int
|
||||
Global bool
|
||||
}
|
||||
|
||||
type bitset uint64
|
||||
|
||||
type Set struct {
|
||||
localVersion map[protocol.DeviceID]uint64
|
||||
mutex sync.Mutex
|
||||
@@ -46,6 +38,22 @@ type Set struct {
|
||||
blockmap *BlockMap
|
||||
}
|
||||
|
||||
// FileIntf is the set of methods implemented by both protocol.FileInfo and
|
||||
// protocol.FileInfoTruncated.
|
||||
type FileIntf interface {
|
||||
Size() int64
|
||||
IsDeleted() bool
|
||||
IsInvalid() bool
|
||||
IsDirectory() bool
|
||||
IsSymlink() bool
|
||||
HasPermissionBits() bool
|
||||
}
|
||||
|
||||
// The Iterator is called with either a protocol.FileInfo or a
|
||||
// protocol.FileInfoTruncated (depending on the method) and returns true to
|
||||
// continue iteration, false to stop.
|
||||
type Iterator func(f FileIntf) bool
|
||||
|
||||
func NewSet(folder string, db *leveldb.DB) *Set {
|
||||
var s = Set{
|
||||
localVersion: make(map[protocol.DeviceID]uint64),
|
||||
@@ -57,7 +65,7 @@ func NewSet(folder string, db *leveldb.DB) *Set {
|
||||
ldbCheckGlobals(db, []byte(folder))
|
||||
|
||||
var deviceID protocol.DeviceID
|
||||
ldbWithAllFolderTruncated(db, []byte(folder), func(device []byte, f protocol.FileInfoTruncated) bool {
|
||||
ldbWithAllFolderTruncated(db, []byte(folder), func(device []byte, f FileInfoTruncated) bool {
|
||||
copy(deviceID[:], device)
|
||||
if f.LocalVersion > s.localVersion[deviceID] {
|
||||
s.localVersion[deviceID] = f.LocalVersion
|
||||
@@ -118,8 +126,8 @@ func (s *Set) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
discards := make([]protocol.FileInfo, 0, len(fs))
|
||||
updates := make([]protocol.FileInfo, 0, len(fs))
|
||||
for _, newFile := range fs {
|
||||
existingFile := ldbGet(s.db, []byte(s.folder), device[:], []byte(newFile.Name))
|
||||
if existingFile.Version <= newFile.Version {
|
||||
existingFile, ok := ldbGet(s.db, []byte(s.folder), device[:], []byte(newFile.Name))
|
||||
if !ok || existingFile.Version <= newFile.Version {
|
||||
discards = append(discards, existingFile)
|
||||
updates = append(updates, newFile)
|
||||
}
|
||||
@@ -132,58 +140,72 @@ func (s *Set) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Set) WithNeed(device protocol.DeviceID, fn fileIterator) {
|
||||
func (s *Set) WithNeed(device protocol.DeviceID, fn Iterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithNeed(%v)", s.folder, device)
|
||||
}
|
||||
ldbWithNeed(s.db, []byte(s.folder), device[:], false, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *Set) WithNeedTruncated(device protocol.DeviceID, fn fileIterator) {
|
||||
func (s *Set) WithNeedTruncated(device protocol.DeviceID, fn Iterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithNeedTruncated(%v)", s.folder, device)
|
||||
}
|
||||
ldbWithNeed(s.db, []byte(s.folder), device[:], true, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *Set) WithHave(device protocol.DeviceID, fn fileIterator) {
|
||||
func (s *Set) WithHave(device protocol.DeviceID, fn Iterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithHave(%v)", s.folder, device)
|
||||
}
|
||||
ldbWithHave(s.db, []byte(s.folder), device[:], false, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *Set) WithHaveTruncated(device protocol.DeviceID, fn fileIterator) {
|
||||
func (s *Set) WithHaveTruncated(device protocol.DeviceID, fn Iterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithHaveTruncated(%v)", s.folder, device)
|
||||
}
|
||||
ldbWithHave(s.db, []byte(s.folder), device[:], true, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *Set) WithGlobal(fn fileIterator) {
|
||||
func (s *Set) WithGlobal(fn Iterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithGlobal()", s.folder)
|
||||
}
|
||||
ldbWithGlobal(s.db, []byte(s.folder), false, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *Set) WithGlobalTruncated(fn fileIterator) {
|
||||
func (s *Set) WithGlobalTruncated(fn Iterator) {
|
||||
if debug {
|
||||
l.Debugf("%s WithGlobalTruncated()", s.folder)
|
||||
}
|
||||
ldbWithGlobal(s.db, []byte(s.folder), true, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *Set) Get(device protocol.DeviceID, file string) protocol.FileInfo {
|
||||
f := ldbGet(s.db, []byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file)))
|
||||
func (s *Set) Get(device protocol.DeviceID, file string) (protocol.FileInfo, bool) {
|
||||
f, ok := ldbGet(s.db, []byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file)))
|
||||
f.Name = osutil.NativeFilename(f.Name)
|
||||
return f
|
||||
return f, ok
|
||||
}
|
||||
|
||||
func (s *Set) GetGlobal(file string) protocol.FileInfo {
|
||||
f := ldbGetGlobal(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(file)))
|
||||
func (s *Set) GetGlobal(file string) (protocol.FileInfo, bool) {
|
||||
fi, ok := ldbGetGlobal(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(file)), false)
|
||||
if !ok {
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
f := fi.(protocol.FileInfo)
|
||||
f.Name = osutil.NativeFilename(f.Name)
|
||||
return f
|
||||
return f, true
|
||||
}
|
||||
|
||||
func (s *Set) GetGlobalTruncated(file string) (FileInfoTruncated, bool) {
|
||||
fi, ok := ldbGetGlobal(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(file)), true)
|
||||
if !ok {
|
||||
return FileInfoTruncated{}, false
|
||||
}
|
||||
f := fi.(FileInfoTruncated)
|
||||
f.Name = osutil.NativeFilename(f.Name)
|
||||
return f, true
|
||||
}
|
||||
|
||||
func (s *Set) Availability(file string) []protocol.DeviceID {
|
||||
@@ -218,13 +240,13 @@ func normalizeFilenames(fs []protocol.FileInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
func nativeFileIterator(fn fileIterator) fileIterator {
|
||||
return func(fi protocol.FileIntf) bool {
|
||||
func nativeFileIterator(fn Iterator) Iterator {
|
||||
return func(fi FileIntf) bool {
|
||||
switch f := fi.(type) {
|
||||
case protocol.FileInfo:
|
||||
f.Name = osutil.NativeFilename(f.Name)
|
||||
return fn(f)
|
||||
case protocol.FileInfoTruncated:
|
||||
case FileInfoTruncated:
|
||||
f.Name = osutil.NativeFilename(f.Name)
|
||||
return fn(f)
|
||||
default:
|
||||
|
||||
@@ -51,7 +51,7 @@ func genBlocks(n int) []protocol.BlockInfo {
|
||||
|
||||
func globalList(s *files.Set) []protocol.FileInfo {
|
||||
var fs []protocol.FileInfo
|
||||
s.WithGlobal(func(fi protocol.FileIntf) bool {
|
||||
s.WithGlobal(func(fi files.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfo)
|
||||
fs = append(fs, f)
|
||||
return true
|
||||
@@ -61,7 +61,7 @@ func globalList(s *files.Set) []protocol.FileInfo {
|
||||
|
||||
func haveList(s *files.Set, n protocol.DeviceID) []protocol.FileInfo {
|
||||
var fs []protocol.FileInfo
|
||||
s.WithHave(n, func(fi protocol.FileIntf) bool {
|
||||
s.WithHave(n, func(fi files.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfo)
|
||||
fs = append(fs, f)
|
||||
return true
|
||||
@@ -71,7 +71,7 @@ func haveList(s *files.Set, n protocol.DeviceID) []protocol.FileInfo {
|
||||
|
||||
func needList(s *files.Set, n protocol.DeviceID) []protocol.FileInfo {
|
||||
var fs []protocol.FileInfo
|
||||
s.WithNeed(n, func(fi protocol.FileIntf) bool {
|
||||
s.WithNeed(n, func(fi files.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfo)
|
||||
fs = append(fs, f)
|
||||
return true
|
||||
@@ -209,27 +209,42 @@ func TestGlobalSet(t *testing.T) {
|
||||
t.Errorf("Need incorrect;\n A: %v !=\n E: %v", n, expectedRemoteNeed)
|
||||
}
|
||||
|
||||
f := m.Get(protocol.LocalDeviceID, "b")
|
||||
f, ok := m.Get(protocol.LocalDeviceID, "b")
|
||||
if !ok {
|
||||
t.Error("Unexpectedly not OK")
|
||||
}
|
||||
if fmt.Sprint(f) != fmt.Sprint(localTot[1]) {
|
||||
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, localTot[1])
|
||||
}
|
||||
|
||||
f = m.Get(remoteDevice0, "b")
|
||||
f, ok = m.Get(remoteDevice0, "b")
|
||||
if !ok {
|
||||
t.Error("Unexpectedly not OK")
|
||||
}
|
||||
if fmt.Sprint(f) != fmt.Sprint(remote1[0]) {
|
||||
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, remote1[0])
|
||||
}
|
||||
|
||||
f = m.GetGlobal("b")
|
||||
f, ok = m.GetGlobal("b")
|
||||
if !ok {
|
||||
t.Error("Unexpectedly not OK")
|
||||
}
|
||||
if fmt.Sprint(f) != fmt.Sprint(remote1[0]) {
|
||||
t.Errorf("GetGlobal incorrect;\n A: %v !=\n E: %v", f, remote1[0])
|
||||
}
|
||||
|
||||
f = m.Get(protocol.LocalDeviceID, "zz")
|
||||
f, ok = m.Get(protocol.LocalDeviceID, "zz")
|
||||
if ok {
|
||||
t.Error("Unexpectedly OK")
|
||||
}
|
||||
if f.Name != "" {
|
||||
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, protocol.FileInfo{})
|
||||
}
|
||||
|
||||
f = m.GetGlobal("zz")
|
||||
f, ok = m.GetGlobal("zz")
|
||||
if ok {
|
||||
t.Error("Unexpectedly OK")
|
||||
}
|
||||
if f.Name != "" {
|
||||
t.Errorf("GetGlobal incorrect;\n A: %v !=\n E: %v", f, protocol.FileInfo{})
|
||||
}
|
||||
|
||||
75
internal/files/truncated.go
Normal file
75
internal/files/truncated.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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/>.
|
||||
|
||||
//go:generate -command genxdr go run ../../Godeps/_workspace/src/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||
//go:generate genxdr -o truncated_xdr.go truncated.go
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/protocol"
|
||||
)
|
||||
|
||||
// Used for unmarshalling a FileInfo structure but skipping the block list.
|
||||
type FileInfoTruncated struct {
|
||||
Name string // max:8192
|
||||
Flags uint32
|
||||
Modified int64
|
||||
Version uint64
|
||||
LocalVersion uint64
|
||||
NumBlocks uint32
|
||||
}
|
||||
|
||||
func (f FileInfoTruncated) String() string {
|
||||
return fmt.Sprintf("File{Name:%q, Flags:0%o, Modified:%d, Version:%d, Size:%d, NumBlocks:%d}",
|
||||
f.Name, f.Flags, f.Modified, f.Version, f.Size(), f.NumBlocks)
|
||||
}
|
||||
|
||||
// Returns a statistical guess on the size, not the exact figure
|
||||
func (f FileInfoTruncated) Size() int64 {
|
||||
if f.IsDeleted() || f.IsDirectory() {
|
||||
return 128
|
||||
}
|
||||
return BlocksToSize(f.NumBlocks)
|
||||
}
|
||||
|
||||
func (f FileInfoTruncated) IsDeleted() bool {
|
||||
return f.Flags&protocol.FlagDeleted != 0
|
||||
}
|
||||
|
||||
func (f FileInfoTruncated) IsInvalid() bool {
|
||||
return f.Flags&protocol.FlagInvalid != 0
|
||||
}
|
||||
|
||||
func (f FileInfoTruncated) IsDirectory() bool {
|
||||
return f.Flags&protocol.FlagDirectory != 0
|
||||
}
|
||||
|
||||
func (f FileInfoTruncated) IsSymlink() bool {
|
||||
return f.Flags&protocol.FlagSymlink != 0
|
||||
}
|
||||
|
||||
func (f FileInfoTruncated) HasPermissionBits() bool {
|
||||
return f.Flags&protocol.FlagNoPermBits == 0
|
||||
}
|
||||
|
||||
func BlocksToSize(num uint32) int64 {
|
||||
if num < 2 {
|
||||
return protocol.BlockSize / 2
|
||||
}
|
||||
return int64(num-1)*protocol.BlockSize + protocol.BlockSize/2
|
||||
}
|
||||
112
internal/files/truncated_xdr.go
Normal file
112
internal/files/truncated_xdr.go
Normal file
@@ -0,0 +1,112 @@
|
||||
// ************************************************************
|
||||
// This file is automatically generated by genxdr. Do not edit.
|
||||
// ************************************************************
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/calmh/xdr"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
FileInfoTruncated Structure:
|
||||
|
||||
0 1 2 3
|
||||
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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of Name |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Name (variable length) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Flags |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
+ Modified (64 bits) +
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
+ Version (64 bits) +
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| |
|
||||
+ Local Version (64 bits) +
|
||||
| |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Num Blocks |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct FileInfoTruncated {
|
||||
string Name<8192>;
|
||||
unsigned int Flags;
|
||||
hyper Modified;
|
||||
unsigned hyper Version;
|
||||
unsigned hyper LocalVersion;
|
||||
unsigned int NumBlocks;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (o FileInfoTruncated) EncodeXDR(w io.Writer) (int, error) {
|
||||
var xw = xdr.NewWriter(w)
|
||||
return o.encodeXDR(xw)
|
||||
}
|
||||
|
||||
func (o FileInfoTruncated) MarshalXDR() ([]byte, error) {
|
||||
return o.AppendXDR(make([]byte, 0, 128))
|
||||
}
|
||||
|
||||
func (o FileInfoTruncated) MustMarshalXDR() []byte {
|
||||
bs, err := o.MarshalXDR()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (o FileInfoTruncated) AppendXDR(bs []byte) ([]byte, error) {
|
||||
var aw = xdr.AppendWriter(bs)
|
||||
var xw = xdr.NewWriter(&aw)
|
||||
_, err := o.encodeXDR(xw)
|
||||
return []byte(aw), err
|
||||
}
|
||||
|
||||
func (o FileInfoTruncated) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
if l := len(o.Name); l > 8192 {
|
||||
return xw.Tot(), xdr.ElementSizeExceeded("Name", l, 8192)
|
||||
}
|
||||
xw.WriteString(o.Name)
|
||||
xw.WriteUint32(o.Flags)
|
||||
xw.WriteUint64(uint64(o.Modified))
|
||||
xw.WriteUint64(o.Version)
|
||||
xw.WriteUint64(o.LocalVersion)
|
||||
xw.WriteUint32(o.NumBlocks)
|
||||
return xw.Tot(), xw.Error()
|
||||
}
|
||||
|
||||
func (o *FileInfoTruncated) DecodeXDR(r io.Reader) error {
|
||||
xr := xdr.NewReader(r)
|
||||
return o.decodeXDR(xr)
|
||||
}
|
||||
|
||||
func (o *FileInfoTruncated) UnmarshalXDR(bs []byte) error {
|
||||
var br = bytes.NewReader(bs)
|
||||
var xr = xdr.NewReader(br)
|
||||
return o.decodeXDR(xr)
|
||||
}
|
||||
|
||||
func (o *FileInfoTruncated) decodeXDR(xr *xdr.Reader) error {
|
||||
o.Name = xr.ReadStringMax(8192)
|
||||
o.Flags = xr.ReadUint32()
|
||||
o.Modified = int64(xr.ReadUint64())
|
||||
o.Version = xr.ReadUint64()
|
||||
o.LocalVersion = xr.ReadUint64()
|
||||
o.NumBlocks = xr.ReadUint32()
|
||||
return xr.Error()
|
||||
}
|
||||
@@ -15,26 +15,11 @@
|
||||
|
||||
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)
|
||||
}
|
||||
import "time"
|
||||
|
||||
type cache struct {
|
||||
patterns []Pattern
|
||||
entries map[string]cacheEntry
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
@@ -50,46 +35,27 @@ func newCache(patterns []Pattern) *cache {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func TestCache(t *testing.T) {
|
||||
|
||||
res, ok := c.get("nonexistent")
|
||||
if res != false || ok != false {
|
||||
t.Error("res %v, ok %v for nonexistent item", res, ok)
|
||||
t.Errorf("res %v, ok %v for nonexistent item", res, ok)
|
||||
}
|
||||
|
||||
// Set and check some items
|
||||
|
||||
@@ -17,6 +17,8 @@ package ignore
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -24,6 +26,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/fnmatch"
|
||||
)
|
||||
@@ -33,51 +36,76 @@ type Pattern struct {
|
||||
include bool
|
||||
}
|
||||
|
||||
func (p Pattern) String() string {
|
||||
if p.include {
|
||||
return p.match.String()
|
||||
} else {
|
||||
return "(?exclude)" + p.match.String()
|
||||
}
|
||||
}
|
||||
|
||||
type Matcher struct {
|
||||
patterns []Pattern
|
||||
matches *cache
|
||||
mut sync.Mutex
|
||||
patterns []Pattern
|
||||
withCache bool
|
||||
matches *cache
|
||||
curHash string
|
||||
stop chan struct{}
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
func Load(file string, cache bool) (*Matcher, error) {
|
||||
seen := make(map[string]bool)
|
||||
matcher, err := loadIgnoreFile(file, seen)
|
||||
if !cache || err != nil {
|
||||
return matcher, err
|
||||
func New(withCache bool) *Matcher {
|
||||
m := &Matcher{
|
||||
withCache: withCache,
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
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) {
|
||||
// Nothing in cache or a cache mismatch, create a new cache which will
|
||||
// store matches for the given set of patterns.
|
||||
// Initialize oldMatches to indicate that we are interested in
|
||||
// caching.
|
||||
cached = newCache(matcher.patterns)
|
||||
matcher.matches = cached
|
||||
caches[file] = cached
|
||||
return matcher, nil
|
||||
if withCache {
|
||||
go m.clean(2 * time.Hour)
|
||||
}
|
||||
|
||||
// Patterns haven't changed, so we can reuse the old matches, create a new
|
||||
// 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.matches = cached
|
||||
return matcher, nil
|
||||
return m
|
||||
}
|
||||
|
||||
func Parse(r io.Reader, file string) (*Matcher, error) {
|
||||
seen := map[string]bool{
|
||||
file: true,
|
||||
func (m *Matcher) Load(file string) error {
|
||||
// No locking, Parse() does the locking
|
||||
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
// We do a parse with empty patterns to clear out the hash, cache etc.
|
||||
m.Parse(&bytes.Buffer{}, file)
|
||||
return err
|
||||
}
|
||||
return parseIgnoreFile(r, file, seen)
|
||||
defer fd.Close()
|
||||
|
||||
return m.Parse(fd, file)
|
||||
}
|
||||
|
||||
func (m *Matcher) Parse(r io.Reader, file string) error {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
seen := map[string]bool{file: true}
|
||||
patterns, err := parseIgnoreFile(r, file, seen)
|
||||
// Error is saved and returned at the end. We process the patterns
|
||||
// (possibly blank) anyway.
|
||||
|
||||
newHash := hashPatterns(patterns)
|
||||
if newHash == m.curHash {
|
||||
// We've already loaded exactly these patterns.
|
||||
return err
|
||||
}
|
||||
|
||||
m.curHash = newHash
|
||||
m.patterns = patterns
|
||||
if m.withCache {
|
||||
m.matches = newCache(patterns)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Matcher) Match(file string) (result bool) {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
if len(m.patterns) == 0 {
|
||||
return false
|
||||
}
|
||||
@@ -108,18 +136,53 @@ func (m *Matcher) Match(file string) (result bool) {
|
||||
|
||||
// Patterns return a list of the loaded regexp patterns, as strings
|
||||
func (m *Matcher) Patterns() []string {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
patterns := make([]string, len(m.patterns))
|
||||
for i, pat := range m.patterns {
|
||||
if pat.include {
|
||||
patterns[i] = pat.match.String()
|
||||
} else {
|
||||
patterns[i] = "(?exclude)" + pat.match.String()
|
||||
}
|
||||
patterns[i] = pat.String()
|
||||
}
|
||||
return patterns
|
||||
}
|
||||
|
||||
func loadIgnoreFile(file string, seen map[string]bool) (*Matcher, error) {
|
||||
func (m *Matcher) Hash() string {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
return m.curHash
|
||||
}
|
||||
|
||||
func (m *Matcher) Stop() {
|
||||
close(m.stop)
|
||||
}
|
||||
|
||||
func (m *Matcher) clean(d time.Duration) {
|
||||
t := time.NewTimer(d / 2)
|
||||
for {
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
case <-t.C:
|
||||
m.mut.Lock()
|
||||
if m.matches != nil {
|
||||
m.matches.clean(d)
|
||||
}
|
||||
t.Reset(d / 2)
|
||||
m.mut.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hashPatterns(patterns []Pattern) string {
|
||||
h := md5.New()
|
||||
for _, pat := range patterns {
|
||||
h.Write([]byte(pat.String()))
|
||||
h.Write([]byte("\n"))
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) {
|
||||
if seen[file] {
|
||||
return nil, fmt.Errorf("Multiple include of ignore file %q", file)
|
||||
}
|
||||
@@ -134,8 +197,8 @@ func loadIgnoreFile(file string, seen map[string]bool) (*Matcher, error) {
|
||||
return parseIgnoreFile(fd, file, seen)
|
||||
}
|
||||
|
||||
func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (*Matcher, error) {
|
||||
var exps Matcher
|
||||
func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]Pattern, error) {
|
||||
var patterns []Pattern
|
||||
|
||||
addPattern := func(line string) error {
|
||||
include := true
|
||||
@@ -150,28 +213,27 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (*M
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid pattern %q in ignore file", line)
|
||||
}
|
||||
exps.patterns = append(exps.patterns, Pattern{exp, include})
|
||||
patterns = append(patterns, Pattern{exp, include})
|
||||
} else if strings.HasPrefix(line, "**/") {
|
||||
// Add the pattern as is, and without **/ so it matches in current dir
|
||||
exp, err := fnmatch.Convert(line, fnmatch.FNM_PATHNAME)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid pattern %q in ignore file", line)
|
||||
}
|
||||
exps.patterns = append(exps.patterns, Pattern{exp, include})
|
||||
patterns = append(patterns, Pattern{exp, include})
|
||||
|
||||
exp, err = fnmatch.Convert(line[3:], fnmatch.FNM_PATHNAME)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid pattern %q in ignore file", line)
|
||||
}
|
||||
exps.patterns = append(exps.patterns, Pattern{exp, include})
|
||||
patterns = append(patterns, Pattern{exp, include})
|
||||
} else if strings.HasPrefix(line, "#include ") {
|
||||
includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
|
||||
includes, err := loadIgnoreFile(includeFile, seen)
|
||||
if err != nil {
|
||||
return err
|
||||
} else {
|
||||
exps.patterns = append(exps.patterns, includes.patterns...)
|
||||
}
|
||||
patterns = append(patterns, includes...)
|
||||
} else {
|
||||
// Path name or pattern, add it so it matches files both in
|
||||
// current directory and subdirs.
|
||||
@@ -179,13 +241,13 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (*M
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid pattern %q in ignore file", line)
|
||||
}
|
||||
exps.patterns = append(exps.patterns, Pattern{exp, include})
|
||||
patterns = append(patterns, Pattern{exp, include})
|
||||
|
||||
exp, err = fnmatch.Convert("**/"+line, fnmatch.FNM_PATHNAME)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid pattern %q in ignore file", line)
|
||||
}
|
||||
exps.patterns = append(exps.patterns, Pattern{exp, include})
|
||||
patterns = append(patterns, Pattern{exp, include})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -219,17 +281,5 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (*M
|
||||
}
|
||||
}
|
||||
|
||||
return &exps, nil
|
||||
}
|
||||
|
||||
func patternsEqual(a, b []Pattern) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i].include != b[i].include || a[i].match.String() != b[i].match.String() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return patterns, nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ import (
|
||||
)
|
||||
|
||||
func TestIgnore(t *testing.T) {
|
||||
pats, err := Load("testdata/.stignore", true)
|
||||
pats := New(true)
|
||||
err := pats.Load("testdata/.stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -74,7 +75,8 @@ func TestExcludes(t *testing.T) {
|
||||
i*2
|
||||
!ign2
|
||||
`
|
||||
pats, err := Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
pats := New(true)
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -114,15 +116,19 @@ func TestBadPatterns(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, pat := range badPatterns {
|
||||
parsed, err := Parse(bytes.NewBufferString(pat), ".stignore")
|
||||
err := New(true).Parse(bytes.NewBufferString(pat), ".stignore")
|
||||
if err == nil {
|
||||
t.Errorf("No error for pattern %q: %v", pat, parsed)
|
||||
t.Errorf("No error for pattern %q", pat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCaseSensitivity(t *testing.T) {
|
||||
ign, _ := Parse(bytes.NewBufferString("test"), ".stignore")
|
||||
ign := New(true)
|
||||
err := ign.Parse(bytes.NewBufferString("test"), ".stignore")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
match := []string{"test"}
|
||||
dontMatch := []string{"foo"}
|
||||
@@ -170,7 +176,8 @@ func TestCaching(t *testing.T) {
|
||||
|
||||
fd2.WriteString("/y/\n")
|
||||
|
||||
pats, err := Load(fd1.Name(), true)
|
||||
pats := New(true)
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -193,9 +200,9 @@ func TestCaching(t *testing.T) {
|
||||
t.Fatal("Expected 4 cached results")
|
||||
}
|
||||
|
||||
// Reload file, expect old outcomes to be provided
|
||||
// Reload file, expect old outcomes to be preserved
|
||||
|
||||
pats, err = Load(fd1.Name(), true)
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -207,7 +214,7 @@ func TestCaching(t *testing.T) {
|
||||
|
||||
fd2.WriteString("/z/\n")
|
||||
|
||||
pats, err = Load(fd1.Name(), true)
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -222,9 +229,9 @@ func TestCaching(t *testing.T) {
|
||||
pats.Match(letter)
|
||||
}
|
||||
|
||||
// Verify that outcomes provided on next laod
|
||||
// Verify that outcomes preserved on next laod
|
||||
|
||||
pats, err = Load(fd1.Name(), true)
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -236,7 +243,7 @@ func TestCaching(t *testing.T) {
|
||||
|
||||
fd1.WriteString("/a/\n")
|
||||
|
||||
pats, err = Load(fd1.Name(), true)
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -252,7 +259,7 @@ func TestCaching(t *testing.T) {
|
||||
|
||||
// Verify that outcomes provided on next laod
|
||||
|
||||
pats, err = Load(fd1.Name(), true)
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -273,7 +280,11 @@ func TestCommentsAndBlankLines(t *testing.T) {
|
||||
|
||||
|
||||
`
|
||||
pats, _ := Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
pats := New(true)
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(pats.patterns) > 0 {
|
||||
t.Errorf("Expected no patterns")
|
||||
}
|
||||
@@ -297,7 +308,11 @@ flamingo
|
||||
*.crow
|
||||
*.crow
|
||||
`
|
||||
pats, _ := Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
pats := New(false)
|
||||
err := pats.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -335,7 +350,8 @@ flamingo
|
||||
}
|
||||
|
||||
// Load the patterns
|
||||
pats, err := Load(fd.Name(), true)
|
||||
pats := New(true)
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -344,7 +360,7 @@ flamingo
|
||||
|
||||
// This load should now load the cached outcomes as the set of patterns
|
||||
// has not changed.
|
||||
pats, err = Load(fd.Name(), true)
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -353,3 +369,152 @@ flamingo
|
||||
result = pats.Match("filename")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheReload(t *testing.T) {
|
||||
fd, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer fd.Close()
|
||||
defer os.Remove(fd.Name())
|
||||
|
||||
// Ignore file matches f1 and f2
|
||||
|
||||
_, err = fd.WriteString("f1\nf2\n")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pats := New(true)
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that both are ignored
|
||||
|
||||
if !pats.Match("f1") {
|
||||
t.Error("Unexpected non-match for f1")
|
||||
}
|
||||
if !pats.Match("f2") {
|
||||
t.Error("Unexpected non-match for f2")
|
||||
}
|
||||
if pats.Match("f3") {
|
||||
t.Error("Unexpected match for f3")
|
||||
}
|
||||
|
||||
// Rewrite file to match f1 and f3
|
||||
|
||||
err = fd.Truncate(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = fd.Seek(0, os.SEEK_SET)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = fd.WriteString("f1\nf3\n")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that the new patterns are in effect
|
||||
|
||||
if !pats.Match("f1") {
|
||||
t.Error("Unexpected non-match for f1")
|
||||
}
|
||||
if pats.Match("f2") {
|
||||
t.Error("Unexpected match for f2")
|
||||
}
|
||||
if !pats.Match("f3") {
|
||||
t.Error("Unexpected non-match for f3")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHash(t *testing.T) {
|
||||
p1 := New(true)
|
||||
err := p1.Load("testdata/.stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Same list of patterns as testdata/.stignore, after expansion
|
||||
stignore := `
|
||||
dir2/dfile
|
||||
dir3
|
||||
bfile
|
||||
dir1/cfile
|
||||
**/efile
|
||||
/ffile
|
||||
lost+found
|
||||
`
|
||||
p2 := New(true)
|
||||
err = p2.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Not same list of patterns
|
||||
stignore = `
|
||||
dir2/dfile
|
||||
dir3
|
||||
bfile
|
||||
dir1/cfile
|
||||
/ffile
|
||||
lost+found
|
||||
`
|
||||
p3 := New(true)
|
||||
err = p3.Parse(bytes.NewBufferString(stignore), ".stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if p1.Hash() == "" {
|
||||
t.Error("p1 hash blank")
|
||||
}
|
||||
if p2.Hash() == "" {
|
||||
t.Error("p2 hash blank")
|
||||
}
|
||||
if p3.Hash() == "" {
|
||||
t.Error("p3 hash blank")
|
||||
}
|
||||
if p1.Hash() != p2.Hash() {
|
||||
t.Error("p1-p2 hashes differ")
|
||||
}
|
||||
if p1.Hash() == p3.Hash() {
|
||||
t.Error("p1-p3 hashes same")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashOfEmpty(t *testing.T) {
|
||||
p1 := New(true)
|
||||
err := p1.Load("testdata/.stignore")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
firstHash := p1.Hash()
|
||||
|
||||
// Reloading with a non-existent file should empty the patterns and
|
||||
// recalculate the hash. d41d8cd98f00b204e9800998ecf8427e is the md5 of
|
||||
// nothing.
|
||||
|
||||
p1.Load("file/does/not/exist")
|
||||
secondHash := p1.Hash()
|
||||
|
||||
if firstHash == secondHash {
|
||||
t.Error("hash did not change")
|
||||
}
|
||||
if secondHash != "d41d8cd98f00b204e9800998ecf8427e" {
|
||||
t.Error("second hash is not hash of empty string")
|
||||
}
|
||||
if len(p1.patterns) != 0 {
|
||||
t.Error("there are more than zero patterns")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func newDeviceActivity() *deviceActivity {
|
||||
|
||||
func (m *deviceActivity) leastBusy(availability []protocol.DeviceID) protocol.DeviceID {
|
||||
m.mut.Lock()
|
||||
var low int = 2<<30 - 1
|
||||
low := 2<<30 - 1
|
||||
var selected protocol.DeviceID
|
||||
for _, device := range availability {
|
||||
if usage := m.act[device]; usage < low {
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
)
|
||||
|
||||
func TestDeviceActivity(t *testing.T) {
|
||||
n0 := protocol.DeviceID{1, 2, 3, 4}
|
||||
n1 := protocol.DeviceID{5, 6, 7, 8}
|
||||
n2 := protocol.DeviceID{9, 10, 11, 12}
|
||||
n0 := protocol.DeviceID([32]byte{1, 2, 3, 4})
|
||||
n1 := protocol.DeviceID([32]byte{5, 6, 7, 8})
|
||||
n2 := protocol.DeviceID([32]byte{9, 10, 11, 12})
|
||||
devices := []protocol.DeviceID{n0, n1, n2}
|
||||
na := newDeviceActivity()
|
||||
|
||||
|
||||
@@ -41,8 +41,6 @@ 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"
|
||||
)
|
||||
|
||||
@@ -81,14 +79,15 @@ const (
|
||||
type service interface {
|
||||
Serve()
|
||||
Stop()
|
||||
Jobs() ([]string, []string) // In progress, Queued
|
||||
BringToFront(string)
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
cfg *config.ConfigWrapper
|
||||
cfg *config.Wrapper
|
||||
db *leveldb.DB
|
||||
finder *files.BlockFinder
|
||||
progressEmitter *ProgressEmitter
|
||||
cache *lrufdcache.FileCache
|
||||
|
||||
deviceName string
|
||||
clientName string
|
||||
@@ -101,6 +100,7 @@ type Model struct {
|
||||
deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
|
||||
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
|
||||
folderRunners map[string]service // folder -> puller or scanner
|
||||
folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef
|
||||
fmut sync.RWMutex // protects the above
|
||||
|
||||
folderState map[string]folderState // folder -> state
|
||||
@@ -126,11 +126,10 @@ var (
|
||||
// NewModel creates and starts a new model. The model starts in read-only mode,
|
||||
// where it sends index information to connected peers and responds to requests
|
||||
// for file data without altering the local folder in any way.
|
||||
func NewModel(cfg *config.ConfigWrapper, deviceName, clientName, clientVersion string, db *leveldb.DB) *Model {
|
||||
func NewModel(cfg *config.Wrapper, deviceName, clientName, clientVersion string, db *leveldb.DB) *Model {
|
||||
m := &Model{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
cache: lrufdcache.NewCache(25),
|
||||
deviceName: deviceName,
|
||||
clientName: clientName,
|
||||
clientVersion: clientVersion,
|
||||
@@ -141,6 +140,7 @@ func NewModel(cfg *config.ConfigWrapper, deviceName, clientName, clientVersion s
|
||||
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
|
||||
folderIgnores: make(map[string]*ignore.Matcher),
|
||||
folderRunners: make(map[string]service),
|
||||
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
||||
folderState: make(map[string]folderState),
|
||||
folderStateChanged: make(map[string]time.Time),
|
||||
protoConn: make(map[protocol.DeviceID]protocol.Connection),
|
||||
@@ -190,7 +190,7 @@ func (m *Model) StartFolderRW(folder string) {
|
||||
progressEmitter: m.progressEmitter,
|
||||
copiers: cfg.Copiers,
|
||||
pullers: cfg.Pullers,
|
||||
finishers: cfg.Finishers,
|
||||
queue: newJobQueue(),
|
||||
}
|
||||
m.folderRunners[folder] = p
|
||||
m.fmut.Unlock()
|
||||
@@ -287,6 +287,15 @@ func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
|
||||
return res
|
||||
}
|
||||
|
||||
// Returns statistics about each folder
|
||||
func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
|
||||
var res = make(map[string]stats.FolderStatistics)
|
||||
for id := range m.cfg.Folders() {
|
||||
res[id] = m.folderStatRef(id).GetStatistics()
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Returns the completion status, in percent, for the given device and folder.
|
||||
func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
defer m.leveldbPanicWorkaround()
|
||||
@@ -300,7 +309,7 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
return 0 // Folder doesn't exist, so we hardly have any of it
|
||||
}
|
||||
|
||||
rf.WithGlobalTruncated(func(f protocol.FileIntf) bool {
|
||||
rf.WithGlobalTruncated(func(f files.FileIntf) bool {
|
||||
if !f.IsDeleted() {
|
||||
tot += f.Size()
|
||||
}
|
||||
@@ -312,7 +321,7 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
}
|
||||
|
||||
var need int64
|
||||
rf.WithNeedTruncated(device, func(f protocol.FileIntf) bool {
|
||||
rf.WithNeedTruncated(device, func(f files.FileIntf) bool {
|
||||
if !f.IsDeleted() {
|
||||
need += f.Size()
|
||||
}
|
||||
@@ -337,7 +346,7 @@ func sizeOf(fs []protocol.FileInfo) (files, deleted int, bytes int64) {
|
||||
return
|
||||
}
|
||||
|
||||
func sizeOfFile(f protocol.FileIntf) (files, deleted int, bytes int64) {
|
||||
func sizeOfFile(f files.FileIntf) (files, deleted int, bytes int64) {
|
||||
if !f.IsDeleted() {
|
||||
files++
|
||||
} else {
|
||||
@@ -349,15 +358,15 @@ func sizeOfFile(f protocol.FileIntf) (files, deleted int, bytes int64) {
|
||||
|
||||
// GlobalSize returns the number of files, deleted files and total bytes for all
|
||||
// files in the global model.
|
||||
func (m *Model) GlobalSize(folder string) (files, deleted int, bytes int64) {
|
||||
func (m *Model) GlobalSize(folder string) (nfiles, deleted int, bytes int64) {
|
||||
defer m.leveldbPanicWorkaround()
|
||||
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
rf.WithGlobalTruncated(func(f protocol.FileIntf) bool {
|
||||
rf.WithGlobalTruncated(func(f files.FileIntf) bool {
|
||||
fs, de, by := sizeOfFile(f)
|
||||
files += fs
|
||||
nfiles += fs
|
||||
deleted += de
|
||||
bytes += by
|
||||
return true
|
||||
@@ -368,18 +377,18 @@ func (m *Model) GlobalSize(folder string) (files, deleted int, bytes int64) {
|
||||
|
||||
// LocalSize returns the number of files, deleted files and total bytes for all
|
||||
// files in the local folder.
|
||||
func (m *Model) LocalSize(folder string) (files, deleted int, bytes int64) {
|
||||
func (m *Model) LocalSize(folder string) (nfiles, deleted int, bytes int64) {
|
||||
defer m.leveldbPanicWorkaround()
|
||||
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
rf.WithHaveTruncated(protocol.LocalDeviceID, func(f protocol.FileIntf) bool {
|
||||
rf.WithHaveTruncated(protocol.LocalDeviceID, func(f files.FileIntf) bool {
|
||||
if f.IsInvalid() {
|
||||
return true
|
||||
}
|
||||
fs, de, by := sizeOfFile(f)
|
||||
files += fs
|
||||
nfiles += fs
|
||||
deleted += de
|
||||
bytes += by
|
||||
return true
|
||||
@@ -389,42 +398,74 @@ func (m *Model) LocalSize(folder string) (files, deleted int, bytes int64) {
|
||||
}
|
||||
|
||||
// NeedSize returns the number and total size of currently needed files.
|
||||
func (m *Model) NeedSize(folder string) (files int, bytes int64) {
|
||||
func (m *Model) NeedSize(folder string) (nfiles int, bytes int64) {
|
||||
defer m.leveldbPanicWorkaround()
|
||||
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f protocol.FileIntf) bool {
|
||||
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f files.FileIntf) bool {
|
||||
fs, de, by := sizeOfFile(f)
|
||||
files += fs + de
|
||||
nfiles += fs + de
|
||||
bytes += by
|
||||
return true
|
||||
})
|
||||
}
|
||||
bytes -= m.progressEmitter.BytesCompleted(folder)
|
||||
if debug {
|
||||
l.Debugf("%v NeedSize(%q): %d %d", m, folder, files, bytes)
|
||||
l.Debugf("%v NeedSize(%q): %d %d", m, folder, nfiles, bytes)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NeedFiles returns the list of currently needed files, stopping at maxFiles
|
||||
// files. Limit <= 0 is ignored.
|
||||
func (m *Model) NeedFolderFilesLimited(folder string, maxFiles int) []protocol.FileInfoTruncated {
|
||||
// NeedFiles returns the list of currently needed files in progress, queued,
|
||||
// and to be queued on next puller iteration. Also takes a soft cap which is
|
||||
// only respected when adding files from the model rather than the runner queue.
|
||||
func (m *Model) NeedFolderFiles(folder string, max int) ([]files.FileInfoTruncated, []files.FileInfoTruncated, []files.FileInfoTruncated) {
|
||||
defer m.leveldbPanicWorkaround()
|
||||
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
fs := make([]protocol.FileInfoTruncated, 0, maxFiles)
|
||||
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f protocol.FileIntf) bool {
|
||||
fs = append(fs, f.(protocol.FileInfoTruncated))
|
||||
return maxFiles <= 0 || len(fs) < maxFiles
|
||||
})
|
||||
return fs
|
||||
var progress, queued, rest []files.FileInfoTruncated
|
||||
var seen map[string]bool
|
||||
|
||||
runner, ok := m.folderRunners[folder]
|
||||
if ok {
|
||||
progressNames, queuedNames := runner.Jobs()
|
||||
|
||||
progress = make([]files.FileInfoTruncated, len(progressNames))
|
||||
queued = make([]files.FileInfoTruncated, len(queuedNames))
|
||||
seen = make(map[string]bool, len(progressNames)+len(queuedNames))
|
||||
|
||||
for i, name := range progressNames {
|
||||
if f, ok := rf.GetGlobalTruncated(name); ok {
|
||||
progress[i] = f
|
||||
seen[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
for i, name := range queuedNames {
|
||||
if f, ok := rf.GetGlobalTruncated(name); ok {
|
||||
queued[i] = f
|
||||
seen[name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
left := max - len(progress) - len(queued)
|
||||
if max < 1 || left > 0 {
|
||||
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f files.FileIntf) bool {
|
||||
left--
|
||||
ft := f.(files.FileInfoTruncated)
|
||||
if !seen[ft.Name] {
|
||||
rest = append(rest, ft)
|
||||
}
|
||||
return max < 1 || left > 0
|
||||
})
|
||||
}
|
||||
return progress, queued, rest
|
||||
}
|
||||
return nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Index is called when a new device is connected and we receive their full index.
|
||||
@@ -439,13 +480,12 @@ func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.F
|
||||
"folder": folder,
|
||||
"device": deviceID.String(),
|
||||
})
|
||||
l.Warnf("Unexpected folder ID %q sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder, deviceID)
|
||||
l.Infof("Unexpected folder ID %q sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder, deviceID)
|
||||
return
|
||||
}
|
||||
|
||||
m.fmut.RLock()
|
||||
files, ok := m.folderFiles[folder]
|
||||
ignores, _ := m.folderIgnores[folder]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
if !ok {
|
||||
@@ -454,9 +494,9 @@ func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.F
|
||||
|
||||
for i := 0; i < len(fs); {
|
||||
lamport.Default.Tick(fs[i].Version)
|
||||
if (ignores != nil && ignores.Match(fs[i].Name)) || symlinkInvalid(fs[i].IsSymlink()) {
|
||||
if symlinkInvalid(fs[i].IsSymlink()) {
|
||||
if debug {
|
||||
l.Debugln("dropping update for ignored/unsupported symlink", fs[i])
|
||||
l.Debugln("dropping update for unsupported symlink", fs[i])
|
||||
}
|
||||
fs[i] = fs[len(fs)-1]
|
||||
fs = fs[:len(fs)-1]
|
||||
@@ -489,7 +529,6 @@ func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []prot
|
||||
|
||||
m.fmut.RLock()
|
||||
files, ok := m.folderFiles[folder]
|
||||
ignores, _ := m.folderIgnores[folder]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
if !ok {
|
||||
@@ -498,9 +537,9 @@ func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []prot
|
||||
|
||||
for i := 0; i < len(fs); {
|
||||
lamport.Default.Tick(fs[i].Version)
|
||||
if (ignores != nil && ignores.Match(fs[i].Name)) || symlinkInvalid(fs[i].IsSymlink()) {
|
||||
if symlinkInvalid(fs[i].IsSymlink()) {
|
||||
if debug {
|
||||
l.Debugln("dropping update for ignored/unsupported symlink", fs[i])
|
||||
l.Debugln("dropping update for unsupported symlink", fs[i])
|
||||
}
|
||||
fs[i] = fs[len(fs)-1]
|
||||
fs = fs[:len(fs)-1]
|
||||
@@ -537,8 +576,21 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
} else {
|
||||
m.deviceVer[deviceID] = cm.ClientName + " " + cm.ClientVersion
|
||||
}
|
||||
|
||||
event := map[string]string{
|
||||
"id": deviceID.String(),
|
||||
"clientName": cm.ClientName,
|
||||
"clientVersion": cm.ClientVersion,
|
||||
}
|
||||
|
||||
if conn, ok := m.rawConn[deviceID].(*tls.Conn); ok {
|
||||
event["addr"] = conn.RemoteAddr().String()
|
||||
}
|
||||
|
||||
m.pmut.Unlock()
|
||||
|
||||
events.Default.Log(events.DeviceConnected, event)
|
||||
|
||||
l.Infof(`Device %s client is "%s %s"`, deviceID, cm.ClientName, cm.ClientVersion)
|
||||
|
||||
var changed bool
|
||||
@@ -576,7 +628,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
l.Infof("Adding device %v to config (vouched for by introducer %v)", id, deviceID)
|
||||
newDeviceCfg := config.DeviceConfiguration{
|
||||
DeviceID: id,
|
||||
Compression: true,
|
||||
Compression: m.cfg.Devices()[deviceID].Compression,
|
||||
Addresses: []string{"dynamic"},
|
||||
}
|
||||
|
||||
@@ -668,7 +720,11 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
return nil, ErrNoSuchFile
|
||||
}
|
||||
|
||||
lf := r.Get(protocol.LocalDeviceID, name)
|
||||
lf, ok := r.Get(protocol.LocalDeviceID, name)
|
||||
if !ok {
|
||||
return nil, ErrNoSuchFile
|
||||
}
|
||||
|
||||
if lf.IsInvalid() || lf.IsDeleted() {
|
||||
if debug {
|
||||
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d; invalid: %v", m, deviceID, folder, name, offset, size, lf)
|
||||
@@ -699,11 +755,12 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
}
|
||||
reader = strings.NewReader(target)
|
||||
} else {
|
||||
reader, err = m.cache.Open(fn)
|
||||
reader, err = os.Open(fn) // XXX: Inefficient, should cache fd?
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.(*lrufdcache.CachedFile).Close()
|
||||
|
||||
defer reader.(*os.File).Close()
|
||||
}
|
||||
|
||||
buf := make([]byte, size)
|
||||
@@ -722,18 +779,18 @@ func (m *Model) ReplaceLocal(folder string, fs []protocol.FileInfo) {
|
||||
m.fmut.RUnlock()
|
||||
}
|
||||
|
||||
func (m *Model) CurrentFolderFile(folder string, file string) protocol.FileInfo {
|
||||
func (m *Model) CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
m.fmut.RLock()
|
||||
f := m.folderFiles[folder].Get(protocol.LocalDeviceID, file)
|
||||
f, ok := m.folderFiles[folder].Get(protocol.LocalDeviceID, file)
|
||||
m.fmut.RUnlock()
|
||||
return f
|
||||
return f, ok
|
||||
}
|
||||
|
||||
func (m *Model) CurrentGlobalFile(folder string, file string) protocol.FileInfo {
|
||||
func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
m.fmut.RLock()
|
||||
f := m.folderFiles[folder].GetGlobal(file)
|
||||
f, ok := m.folderFiles[folder].GetGlobal(file)
|
||||
m.fmut.RUnlock()
|
||||
return f
|
||||
return f, ok
|
||||
}
|
||||
|
||||
type cFiler struct {
|
||||
@@ -742,7 +799,7 @@ type cFiler struct {
|
||||
}
|
||||
|
||||
// Implements scanner.CurrentFiler
|
||||
func (cf cFiler) CurrentFile(file string) protocol.FileInfo {
|
||||
func (cf cFiler) CurrentFile(file string) (protocol.FileInfo, bool) {
|
||||
return cf.m.CurrentFolderFile(cf.r, file)
|
||||
}
|
||||
|
||||
@@ -865,17 +922,33 @@ func (m *Model) deviceStatRef(deviceID protocol.DeviceID) *stats.DeviceStatistic
|
||||
|
||||
if sr, ok := m.deviceStatRefs[deviceID]; ok {
|
||||
return sr
|
||||
} else {
|
||||
sr = stats.NewDeviceStatisticsReference(m.db, deviceID)
|
||||
m.deviceStatRefs[deviceID] = sr
|
||||
return sr
|
||||
}
|
||||
|
||||
sr := stats.NewDeviceStatisticsReference(m.db, deviceID)
|
||||
m.deviceStatRefs[deviceID] = sr
|
||||
return sr
|
||||
}
|
||||
|
||||
func (m *Model) deviceWasSeen(deviceID protocol.DeviceID) {
|
||||
m.deviceStatRef(deviceID).WasSeen()
|
||||
}
|
||||
|
||||
func (m *Model) folderStatRef(folder string) *stats.FolderStatisticsReference {
|
||||
m.fmut.Lock()
|
||||
defer m.fmut.Unlock()
|
||||
|
||||
sr, ok := m.folderStatRefs[folder]
|
||||
if !ok {
|
||||
sr = stats.NewFolderStatisticsReference(m.db, folder)
|
||||
m.folderStatRefs[folder] = sr
|
||||
}
|
||||
return sr
|
||||
}
|
||||
|
||||
func (m *Model) receivedFile(folder, filename string) {
|
||||
m.folderStatRef(folder).ReceivedFile(filename)
|
||||
}
|
||||
|
||||
func sendIndexes(conn protocol.Connection, folder string, fs *files.Set, ignores *ignore.Matcher) {
|
||||
deviceID := conn.ID()
|
||||
name := conn.Name()
|
||||
@@ -909,7 +982,7 @@ func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, fol
|
||||
maxLocalVer := uint64(0)
|
||||
var err error
|
||||
|
||||
fs.WithHave(protocol.LocalDeviceID, func(fi protocol.FileIntf) bool {
|
||||
fs.WithHave(protocol.LocalDeviceID, func(fi files.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfo)
|
||||
if f.LocalVersion <= minLocalVer {
|
||||
return true
|
||||
@@ -1016,7 +1089,8 @@ func (m *Model) AddFolder(cfg config.FolderConfiguration) {
|
||||
m.deviceFolders[device.DeviceID] = append(m.deviceFolders[device.DeviceID], cfg.ID)
|
||||
}
|
||||
|
||||
ignores, _ := ignore.Load(filepath.Join(cfg.Path, ".stignore"), m.cfg.Options().CacheIgnoredFiles)
|
||||
ignores := ignore.New(m.cfg.Options().CacheIgnoredFiles)
|
||||
_ = ignores.Load(filepath.Join(cfg.Path, ".stignore")) // Ignore error, there might not be an .stignore
|
||||
m.folderIgnores[cfg.ID] = ignores
|
||||
|
||||
m.addedFolder = true
|
||||
@@ -1057,26 +1131,28 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
|
||||
|
||||
m.fmut.Lock()
|
||||
fs, ok := m.folderFiles[folder]
|
||||
dir := m.folderCfgs[folder].Path
|
||||
|
||||
ignores, _ := ignore.Load(filepath.Join(dir, ".stignore"), m.cfg.Options().CacheIgnoredFiles)
|
||||
m.folderIgnores[folder] = ignores
|
||||
|
||||
w := &scanner.Walker{
|
||||
Dir: dir,
|
||||
Sub: sub,
|
||||
Matcher: ignores,
|
||||
BlockSize: protocol.BlockSize,
|
||||
TempNamer: defTempNamer,
|
||||
CurrentFiler: cFiler{m, folder},
|
||||
IgnorePerms: m.folderCfgs[folder].IgnorePerms,
|
||||
}
|
||||
folderCfg := m.folderCfgs[folder]
|
||||
ignores := m.folderIgnores[folder]
|
||||
m.fmut.Unlock()
|
||||
|
||||
if !ok {
|
||||
return errors.New("no such folder")
|
||||
}
|
||||
|
||||
_ = ignores.Load(filepath.Join(folderCfg.Path, ".stignore")) // Ignore error, there might not be an .stignore
|
||||
|
||||
w := &scanner.Walker{
|
||||
Dir: folderCfg.Path,
|
||||
Sub: sub,
|
||||
Matcher: ignores,
|
||||
BlockSize: protocol.BlockSize,
|
||||
TempNamer: defTempNamer,
|
||||
TempLifetime: time.Duration(m.cfg.Options().KeepTemporariesH) * time.Hour,
|
||||
CurrentFiler: cFiler{m, folder},
|
||||
IgnorePerms: folderCfg.IgnorePerms,
|
||||
Hashers: folderCfg.Hashers,
|
||||
}
|
||||
|
||||
m.setState(folder, FolderScanning)
|
||||
fchan, err := w.Walk()
|
||||
|
||||
@@ -1106,8 +1182,8 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
|
||||
batch = batch[:0]
|
||||
// TODO: We should limit the Have scanning to start at sub
|
||||
seenPrefix := false
|
||||
fs.WithHaveTruncated(protocol.LocalDeviceID, func(fi protocol.FileIntf) bool {
|
||||
f := fi.(protocol.FileInfoTruncated)
|
||||
fs.WithHaveTruncated(protocol.LocalDeviceID, func(fi files.FileIntf) bool {
|
||||
f := fi.(files.FileInfoTruncated)
|
||||
if !strings.HasPrefix(f.Name, sub) {
|
||||
// Return true so that we keep iterating, until we get to the part
|
||||
// of the tree we are interested in. Then return false so we stop
|
||||
@@ -1145,7 +1221,7 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
|
||||
"size": f.Size(),
|
||||
})
|
||||
batch = append(batch, nf)
|
||||
} else if _, err := os.Lstat(filepath.Join(dir, f.Name)); err != nil && os.IsNotExist(err) {
|
||||
} else if _, err := os.Lstat(filepath.Join(folderCfg.Path, f.Name)); err != nil && os.IsNotExist(err) {
|
||||
// File has been deleted
|
||||
nf := protocol.FileInfo{
|
||||
Name: f.Name,
|
||||
@@ -1247,15 +1323,15 @@ func (m *Model) Override(folder string) {
|
||||
|
||||
m.setState(folder, FolderScanning)
|
||||
batch := make([]protocol.FileInfo, 0, indexBatchSize)
|
||||
fs.WithNeed(protocol.LocalDeviceID, func(fi protocol.FileIntf) bool {
|
||||
fs.WithNeed(protocol.LocalDeviceID, func(fi files.FileIntf) bool {
|
||||
need := fi.(protocol.FileInfo)
|
||||
if len(batch) == indexBatchSize {
|
||||
fs.Update(protocol.LocalDeviceID, batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
|
||||
have := fs.Get(protocol.LocalDeviceID, need.Name)
|
||||
if have.Name != need.Name {
|
||||
have, ok := fs.Get(protocol.LocalDeviceID, need.Name)
|
||||
if !ok || have.Name != need.Name {
|
||||
// We are missing the file
|
||||
need.Flags |= protocol.FlagDeleted
|
||||
need.Blocks = nil
|
||||
@@ -1312,9 +1388,9 @@ func (m *Model) RemoteLocalVersion(folder string) uint64 {
|
||||
return ver
|
||||
}
|
||||
|
||||
func (m *Model) availability(folder string, file string) []protocol.DeviceID {
|
||||
func (m *Model) availability(folder, file string) []protocol.DeviceID {
|
||||
// Acquire this lock first, as the value returned from foldersFiles can
|
||||
// gen heavily modified on Close()
|
||||
// get heavily modified on Close()
|
||||
m.pmut.RLock()
|
||||
defer m.pmut.RUnlock()
|
||||
|
||||
@@ -1335,6 +1411,17 @@ func (m *Model) availability(folder string, file string) []protocol.DeviceID {
|
||||
return availableDevices
|
||||
}
|
||||
|
||||
// Bump the given files priority in the job queue
|
||||
func (m *Model) BringToFront(folder, file string) {
|
||||
m.pmut.RLock()
|
||||
defer m.pmut.RUnlock()
|
||||
|
||||
runner, ok := m.folderRunners[folder]
|
||||
if ok {
|
||||
runner.BringToFront(file)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) String() string {
|
||||
return fmt.Sprintf("model@%p", m)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user