mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-03 03:19:04 -05:00
Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c07b39e58b | ||
|
|
384c543ab9 | ||
|
|
592b13d7db | ||
|
|
6fdba3c02e | ||
|
|
cbf758ead9 | ||
|
|
d1ad778a64 | ||
|
|
ce5ad296ae | ||
|
|
797e105786 | ||
|
|
d17d80747e | ||
|
|
55ea207a55 | ||
|
|
6384d1e5a3 | ||
|
|
aba01cdace | ||
|
|
517b7a14b4 | ||
|
|
2927de7cf9 | ||
|
|
6471ba70e4 | ||
|
|
9f9de01c51 | ||
|
|
3662decb8b | ||
|
|
583bcfb3c7 | ||
|
|
c45e3fa4d5 | ||
|
|
24cbcef620 | ||
|
|
e2a520ff49 | ||
|
|
a5e3317e28 | ||
|
|
5638c4ba87 | ||
|
|
bf7a128142 | ||
|
|
c5243cd4d5 | ||
|
|
db868ed29d | ||
|
|
450c7d80f8 | ||
|
|
abbb001975 | ||
|
|
f35d83ae48 | ||
|
|
a2315dc95e | ||
|
|
4e2feb6fbc | ||
|
|
13602b6769 | ||
|
|
85dba25246 | ||
|
|
66432672b3 | ||
|
|
e6d96e4c18 | ||
|
|
9812305bb9 | ||
|
|
f680a63a1f | ||
|
|
781d63cb2a | ||
|
|
9ff04ee3d8 | ||
|
|
5d85a24977 | ||
|
|
1e51fca0b0 | ||
|
|
5537d53f9a | ||
|
|
50a4170541 | ||
|
|
3a8255bda1 | ||
|
|
a617846f0f | ||
|
|
5772588c29 | ||
|
|
9d0dc45f74 | ||
|
|
c6aefbc9a0 | ||
|
|
dbbafb0cc9 | ||
|
|
6e8272f78f | ||
|
|
baf8a63121 | ||
|
|
fc4a76ee50 | ||
|
|
2117d1d035 | ||
|
|
0a70e0b7b6 | ||
|
|
64ffac5671 | ||
|
|
ac384e8a9c | ||
|
|
f97c8222c7 | ||
|
|
728289ee3a | ||
|
|
5faa16f9ee | ||
|
|
4e608b116a | ||
|
|
521b49166e | ||
|
|
8f32decf2d | ||
|
|
0d51f83d2d | ||
|
|
78c6a68db9 | ||
|
|
2949ab73e2 | ||
|
|
223741820d | ||
|
|
4b57821f52 | ||
|
|
74271a479f | ||
|
|
c377177108 | ||
|
|
84eb729bd4 | ||
|
|
14aea365c5 | ||
|
|
97cb3fa5a5 | ||
|
|
b5368db704 | ||
|
|
8c442b72f3 | ||
|
|
f8f6791d39 | ||
|
|
0c09f077aa | ||
|
|
af2831d7b6 | ||
|
|
64d5d4aec7 | ||
|
|
619a6b2adb | ||
|
|
33a26bc0cf | ||
|
|
b445a7c4d3 | ||
|
|
e6892d0c3e | ||
|
|
33e9a88b56 | ||
|
|
df00a2251e | ||
|
|
92c44c8abe | ||
|
|
8e4f7bbd3e | ||
|
|
a40217cf07 | ||
|
|
e586fda5f2 | ||
|
|
a58564ff88 | ||
|
|
89885b9fb9 | ||
|
|
5c7d977ae0 | ||
|
|
2cd3ee9698 | ||
|
|
dd3080e018 | ||
|
|
5915e8e86a | ||
|
|
3c67c06654 | ||
|
|
76232ca573 | ||
|
|
5235e82bda | ||
|
|
10f0713257 | ||
|
|
e9c7970ea4 | ||
|
|
1a6ac4aeb1 | ||
|
|
f633bdddf0 | ||
|
|
de0b91d157 | ||
|
|
2e77e498f5 | ||
|
|
4ac67eb1f9 | ||
|
|
2b536de37f | ||
|
|
2ffa92ba1b | ||
|
|
6ecddd8388 | ||
|
|
bd2772ea4c | ||
|
|
92bf79d53b | ||
|
|
eebe0eeb71 | ||
|
|
c2daedbd11 | ||
|
|
7c604beb73 | ||
|
|
8c42aea827 |
@@ -9,6 +9,7 @@ Gilli Sigurdsson <gilli@vx.is>
|
||||
James Patterson <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
||||
Jens Diemer <github.com@jensdiemer.de> <git@jensdiemer.de>
|
||||
Marcin Dziadus <dziadus.marcin@gmail.com>
|
||||
Michael Tilli <pyfisch@gmail.com>
|
||||
Philippe Schommers <philippe@schommers.be>
|
||||
Ryan Sullivan <kayoticsully@gmail.com>
|
||||
Tully Robinson <tully@tojr.org>
|
||||
|
||||
4
Godeps/Godeps.json
generated
4
Godeps/Godeps.json
generated
@@ -37,7 +37,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/bkaradzic/go-lz4",
|
||||
"Rev": "77e2ba877bde9da31213bec75dbbe197fa507c21"
|
||||
"Rev": "93a831dcee242be64a9cc9803dda84af25932de7"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/xdr",
|
||||
@@ -49,7 +49,7 @@
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||
"Rev": "59d87758aeaab5ab6ed289c773349500228a1557"
|
||||
"Rev": "9bca75c48d6c31becfbb127702b425e7226052e3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vitrun/qart/coding",
|
||||
|
||||
12
Godeps/_workspace/src/github.com/bkaradzic/go-lz4/lz4-example/main.go
generated
vendored
12
Godeps/_workspace/src/github.com/bkaradzic/go-lz4/lz4-example/main.go
generated
vendored
@@ -72,10 +72,18 @@ func main() {
|
||||
|
||||
if *decompress {
|
||||
data, _ = ioutil.ReadAll(input)
|
||||
data, _ = lz4.Decode(nil, data)
|
||||
data, err = lz4.Decode(nil, data)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to decode:", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
data, _ = ioutil.ReadAll(input)
|
||||
data, _ = lz4.Encode(nil, data)
|
||||
data, err = lz4.Encode(nil, data)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to encode:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(args[1], data, 0644)
|
||||
|
||||
4
Godeps/_workspace/src/github.com/bkaradzic/go-lz4/writer.go
generated
vendored
4
Godeps/_workspace/src/github.com/bkaradzic/go-lz4/writer.go
generated
vendored
@@ -121,7 +121,7 @@ func Encode(dst, src []byte) ([]byte, error) {
|
||||
)
|
||||
|
||||
for {
|
||||
if int(e.pos)+4 >= len(e.src) {
|
||||
if int(e.pos)+12 >= len(e.src) {
|
||||
e.writeLiterals(uint32(len(e.src))-e.anchor, 0, e.anchor)
|
||||
return e.dst[:e.dpos], nil
|
||||
}
|
||||
@@ -158,7 +158,7 @@ func Encode(dst, src []byte) ([]byte, error) {
|
||||
ref += minMatch
|
||||
e.anchor = e.pos
|
||||
|
||||
for int(e.pos) < len(e.src) && e.src[e.pos] == e.src[ref] {
|
||||
for int(e.pos) < len(e.src)-5 && e.src[e.pos] == e.src[ref] {
|
||||
e.pos++
|
||||
ref++
|
||||
}
|
||||
|
||||
14
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache.go
generated
vendored
14
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache.go
generated
vendored
@@ -40,10 +40,21 @@ type Cache interface {
|
||||
// Size returns entire alive cache objects size.
|
||||
Size() int
|
||||
|
||||
// NumObjects returns number of alive objects.
|
||||
NumObjects() int
|
||||
|
||||
// GetNamespace gets cache namespace with the given id.
|
||||
// GetNamespace is never return nil.
|
||||
GetNamespace(id uint64) Namespace
|
||||
|
||||
// PurgeNamespace purges cache namespace with the given id from this cache tree.
|
||||
// Also read Namespace.Purge.
|
||||
PurgeNamespace(id uint64, fin PurgeFin)
|
||||
|
||||
// ZapNamespace detaches cache namespace with the given id from this cache tree.
|
||||
// Also read Namespace.Zap.
|
||||
ZapNamespace(id uint64)
|
||||
|
||||
// Purge purges all cache namespace from this cache tree.
|
||||
// This is behave the same as calling Namespace.Purge method on all cache namespace.
|
||||
Purge(fin PurgeFin)
|
||||
@@ -117,7 +128,8 @@ const (
|
||||
type nodeState int
|
||||
|
||||
const (
|
||||
nodeEffective nodeState = iota
|
||||
nodeZero nodeState = iota
|
||||
nodeEffective
|
||||
nodeEvicted
|
||||
nodeDeleted
|
||||
)
|
||||
|
||||
56
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache_test.go
generated
vendored
56
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache_test.go
generated
vendored
@@ -512,6 +512,56 @@ func TestLRUCache_Finalizer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -521,7 +571,7 @@ func BenchmarkLRUCache_SetRelease(b *testing.B) {
|
||||
ns := c.GetNamespace(0)
|
||||
b.ResetTimer()
|
||||
for i := uint64(0); i < uint64(b.N); i++ {
|
||||
set(ns, i, nil, 1, nil).Release()
|
||||
set(ns, i, "", 1, nil).Release()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,10 +588,10 @@ func BenchmarkLRUCache_SetReleaseTwice(b *testing.B) {
|
||||
nb := b.N - na
|
||||
|
||||
for i := uint64(0); i < uint64(na); i++ {
|
||||
set(ns, i, nil, 1, nil).Release()
|
||||
set(ns, i, "", 1, nil).Release()
|
||||
}
|
||||
|
||||
for i := uint64(0); i < uint64(nb); i++ {
|
||||
set(ns, i, nil, 1, nil).Release()
|
||||
set(ns, i, "", 1, nil).Release()
|
||||
}
|
||||
}
|
||||
|
||||
453
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/lru_cache.go
generated
vendored
453
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/lru_cache.go
generated
vendored
@@ -13,13 +13,20 @@ import (
|
||||
"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 int
|
||||
mu sync.Mutex
|
||||
recent lruNode
|
||||
table map[uint64]*lruNs
|
||||
capacity int
|
||||
used, size, alive int
|
||||
}
|
||||
|
||||
// NewLRUCache creates a new initialized LRU cache with the given capacity.
|
||||
@@ -51,6 +58,12 @@ func (c *lruCache) Size() int {
|
||||
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()
|
||||
@@ -68,15 +81,28 @@ func (c *lruCache) GetNamespace(id uint64) Namespace {
|
||||
return ns
|
||||
}
|
||||
|
||||
ns := &lruNs{
|
||||
lru: c,
|
||||
id: id,
|
||||
table: make(map[uint64]*lruNode),
|
||||
}
|
||||
ns := &lruNs{lru: c, id: id}
|
||||
c.table[id] = ns
|
||||
return ns
|
||||
}
|
||||
|
||||
func (c *lruCache) ZapNamespace(id uint64) {
|
||||
c.mu.Lock()
|
||||
if ns, exist := c.table[id]; exist {
|
||||
ns.zapNB()
|
||||
delete(c.table, id)
|
||||
}
|
||||
c.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()
|
||||
@@ -107,129 +133,267 @@ func (c *lruCache) evict() {
|
||||
}
|
||||
|
||||
type lruNs struct {
|
||||
lru *lruCache
|
||||
id uint64
|
||||
table map[uint64]*lruNode
|
||||
state nsState
|
||||
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 {
|
||||
ns.lru.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
node, ok := ns.table[key]
|
||||
if ok {
|
||||
switch node.state {
|
||||
case nodeEvicted:
|
||||
// Insert to recent list.
|
||||
node.state = nodeEffective
|
||||
node.ref++
|
||||
ns.lru.used += node.charge
|
||||
ns.lru.evict()
|
||||
fallthrough
|
||||
case nodeEffective:
|
||||
// Bump to front.
|
||||
node.rRemove()
|
||||
node.rInsert(&ns.lru.recent)
|
||||
}
|
||||
node.ref++
|
||||
} else {
|
||||
if setf == nil {
|
||||
ns.lru.mu.Unlock()
|
||||
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.lru.mu.Unlock()
|
||||
ns.deleteNode(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
node = &lruNode{
|
||||
ns: ns,
|
||||
key: key,
|
||||
value: value,
|
||||
charge: charge,
|
||||
ref: 1,
|
||||
if charge < 0 {
|
||||
charge = 0
|
||||
}
|
||||
ns.table[key] = node
|
||||
|
||||
if charge > 0 {
|
||||
node.ref++
|
||||
node.rInsert(&ns.lru.recent)
|
||||
ns.lru.used += charge
|
||||
ns.lru.size += charge
|
||||
ns.lru.evict()
|
||||
n.value = value
|
||||
n.charge = charge
|
||||
n.state = nodeEvicted
|
||||
|
||||
ns.lru.size += charge
|
||||
ns.lru.alive++
|
||||
|
||||
fallthrough
|
||||
case nodeEvicted:
|
||||
if n.charge == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Insert to recent list.
|
||||
n.state = nodeEffective
|
||||
n.ref++
|
||||
ns.lru.used += n.charge
|
||||
ns.lru.evict()
|
||||
|
||||
fallthrough
|
||||
case nodeEffective:
|
||||
// Bump to front.
|
||||
n.rRemove()
|
||||
n.rInsert(&ns.lru.recent)
|
||||
}
|
||||
n.ref++
|
||||
|
||||
ns.lru.mu.Unlock()
|
||||
return &lruHandle{node: node}
|
||||
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)
|
||||
}
|
||||
ns.lru.mu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
node, exist := ns.table[key]
|
||||
if !exist {
|
||||
n := ns.getNode(key)
|
||||
if n == nil {
|
||||
if fin != nil {
|
||||
fin(false, false)
|
||||
}
|
||||
ns.lru.mu.Unlock()
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
switch node.state {
|
||||
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)
|
||||
}
|
||||
ns.lru.mu.Unlock()
|
||||
return false
|
||||
case nodeEffective:
|
||||
ns.lru.used -= node.charge
|
||||
node.state = nodeDeleted
|
||||
node.delfin = fin
|
||||
node.rRemove()
|
||||
node.derefNB()
|
||||
default:
|
||||
node.state = nodeDeleted
|
||||
node.delfin = fin
|
||||
panic("invalid state")
|
||||
}
|
||||
|
||||
ns.lru.mu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
func (ns *lruNs) purgeNB(fin PurgeFin) {
|
||||
if ns.state != nsEffective {
|
||||
return
|
||||
}
|
||||
|
||||
for _, node := range ns.table {
|
||||
switch node.state {
|
||||
case nodeDeleted:
|
||||
case nodeEffective:
|
||||
ns.lru.used -= node.charge
|
||||
node.state = nodeDeleted
|
||||
node.purgefin = fin
|
||||
node.rRemove()
|
||||
node.derefNB()
|
||||
default:
|
||||
node.state = nodeDeleted
|
||||
node.purgefin = fin
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,22 +405,22 @@ func (ns *lruNs) Purge(fin PurgeFin) {
|
||||
}
|
||||
|
||||
func (ns *lruNs) zapNB() {
|
||||
if ns.state != nsEffective {
|
||||
return
|
||||
}
|
||||
if ns.state == nsEffective {
|
||||
ns.state = nsZapped
|
||||
|
||||
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()
|
||||
|
||||
for _, node := range ns.table {
|
||||
if node.state == nodeEffective {
|
||||
ns.lru.used -= node.charge
|
||||
node.rRemove()
|
||||
}
|
||||
ns.lru.size -= node.charge
|
||||
node.state = nodeDeleted
|
||||
node.fin()
|
||||
return true
|
||||
})
|
||||
ns.rbRoot = nil
|
||||
}
|
||||
ns.table = nil
|
||||
}
|
||||
|
||||
func (ns *lruNs) Zap() {
|
||||
@@ -269,7 +433,9 @@ func (ns *lruNs) Zap() {
|
||||
type lruNode struct {
|
||||
ns *lruNs
|
||||
|
||||
rNext, rPrev *lruNode
|
||||
rNext, rPrev *lruNode
|
||||
rbLeft, rbRight *lruNode
|
||||
rbBlack bool
|
||||
|
||||
key uint64
|
||||
value interface{}
|
||||
@@ -320,10 +486,12 @@ func (n *lruNode) derefNB() {
|
||||
if n.ref == 0 {
|
||||
if n.ns.state == nsEffective {
|
||||
// Remove elemement.
|
||||
delete(n.ns.table, n.key)
|
||||
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")
|
||||
}
|
||||
@@ -354,3 +522,92 @@ func (h *lruHandle) Release() {
|
||||
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
|
||||
}
|
||||
|
||||
23
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db.go
generated
vendored
23
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db.go
generated
vendored
@@ -14,6 +14,7 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
@@ -35,7 +36,7 @@ type DB struct {
|
||||
|
||||
// MemDB.
|
||||
memMu sync.RWMutex
|
||||
memPool *util.Pool
|
||||
memPool chan *memdb.DB
|
||||
mem, frozenMem *memDB
|
||||
journal *journal.Writer
|
||||
journalWriter storage.Writer
|
||||
@@ -47,6 +48,9 @@ type DB struct {
|
||||
snapsMu sync.Mutex
|
||||
snapsRoot snapshotElement
|
||||
|
||||
// Stats.
|
||||
aliveSnaps, aliveIters int32
|
||||
|
||||
// Write.
|
||||
writeC chan *Batch
|
||||
writeMergedC chan bool
|
||||
@@ -80,7 +84,7 @@ func openDB(s *session) (*DB, error) {
|
||||
// Initial sequence
|
||||
seq: s.stSeq,
|
||||
// MemDB
|
||||
memPool: util.NewPool(1),
|
||||
memPool: make(chan *memdb.DB, 1),
|
||||
// Write
|
||||
writeC: make(chan *Batch),
|
||||
writeMergedC: make(chan bool),
|
||||
@@ -122,6 +126,7 @@ func openDB(s *session) (*DB, error) {
|
||||
go db.tCompaction()
|
||||
go db.mCompaction()
|
||||
go db.jWriter()
|
||||
go db.mpoolDrain()
|
||||
|
||||
s.logf("db@open done T·%v", time.Since(start))
|
||||
|
||||
@@ -568,7 +573,7 @@ func (db *DB) get(key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, er
|
||||
}
|
||||
defer m.decref()
|
||||
|
||||
mk, mv, me := m.db.Find(ikey)
|
||||
mk, mv, me := m.mdb.Find(ikey)
|
||||
if me == nil {
|
||||
ukey, _, t, ok := parseIkey(mk)
|
||||
if ok && db.s.icmp.uCompare(ukey, key) == 0 {
|
||||
@@ -657,6 +662,14 @@ func (db *DB) GetSnapshot() (*Snapshot, error) {
|
||||
// Returns sstables list for each level.
|
||||
// leveldb.blockpool
|
||||
// Returns block pool stats.
|
||||
// leveldb.cachedblock
|
||||
// Returns size of cached block.
|
||||
// leveldb.openedtables
|
||||
// Returns number of opened tables.
|
||||
// leveldb.alivesnaps
|
||||
// Returns number of alive snapshots.
|
||||
// leveldb.aliveiters
|
||||
// Returns number of alive iterators.
|
||||
func (db *DB) GetProperty(name string) (value string, err error) {
|
||||
err = db.ok()
|
||||
if err != nil {
|
||||
@@ -712,6 +725,10 @@ func (db *DB) GetProperty(name string) (value string, err error) {
|
||||
}
|
||||
case p == "openedtables":
|
||||
value = fmt.Sprintf("%d", db.s.tops.cache.Size())
|
||||
case p == "alivesnaps":
|
||||
value = fmt.Sprintf("%d", atomic.LoadInt32(&db.aliveSnaps))
|
||||
case p == "aliveiters":
|
||||
value = fmt.Sprintf("%d", atomic.LoadInt32(&db.aliveIters))
|
||||
default:
|
||||
err = errors.New("leveldb: GetProperty: unknown property: " + name)
|
||||
}
|
||||
|
||||
6
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_compaction.go
generated
vendored
6
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_compaction.go
generated
vendored
@@ -221,10 +221,10 @@ func (db *DB) memCompaction() {
|
||||
c := newCMem(db.s)
|
||||
stats := new(cStatsStaging)
|
||||
|
||||
db.logf("mem@flush N·%d S·%s", mem.db.Len(), shortenb(mem.db.Size()))
|
||||
db.logf("mem@flush N·%d S·%s", mem.mdb.Len(), shortenb(mem.mdb.Size()))
|
||||
|
||||
// Don't compact empty memdb.
|
||||
if mem.db.Len() == 0 {
|
||||
if mem.mdb.Len() == 0 {
|
||||
db.logf("mem@flush skipping")
|
||||
// drop frozen mem
|
||||
db.dropFrozenMem()
|
||||
@@ -242,7 +242,7 @@ func (db *DB) memCompaction() {
|
||||
db.compactionTransact("mem@flush", func(cnt *compactionTransactCounter) (err error) {
|
||||
stats.startTimer()
|
||||
defer stats.stopTimer()
|
||||
return c.flush(mem.db, -1)
|
||||
return c.flush(mem.mdb, -1)
|
||||
}, func() error {
|
||||
for _, r := range c.rec.addedTables {
|
||||
db.logf("mem@flush rollback @%d", r.num)
|
||||
|
||||
11
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_iter.go
generated
vendored
11
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_iter.go
generated
vendored
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
@@ -38,11 +39,11 @@ func (db *DB) newRawIterator(slice *util.Range, ro *opt.ReadOptions) iterator.It
|
||||
ti := v.getIterators(slice, ro)
|
||||
n := len(ti) + 2
|
||||
i := make([]iterator.Iterator, 0, n)
|
||||
emi := em.db.NewIterator(slice)
|
||||
emi := em.mdb.NewIterator(slice)
|
||||
emi.SetReleaser(&memdbReleaser{m: em})
|
||||
i = append(i, emi)
|
||||
if fm != nil {
|
||||
fmi := fm.db.NewIterator(slice)
|
||||
fmi := fm.mdb.NewIterator(slice)
|
||||
fmi.SetReleaser(&memdbReleaser{m: fm})
|
||||
i = append(i, fmi)
|
||||
}
|
||||
@@ -66,6 +67,7 @@ func (db *DB) newIterator(seq uint64, slice *util.Range, ro *opt.ReadOptions) *d
|
||||
}
|
||||
rawIter := db.newRawIterator(islice, ro)
|
||||
iter := &dbIter{
|
||||
db: db,
|
||||
icmp: db.s.icmp,
|
||||
iter: rawIter,
|
||||
seq: seq,
|
||||
@@ -73,6 +75,7 @@ func (db *DB) newIterator(seq uint64, slice *util.Range, ro *opt.ReadOptions) *d
|
||||
key: make([]byte, 0),
|
||||
value: make([]byte, 0),
|
||||
}
|
||||
atomic.AddInt32(&db.aliveIters, 1)
|
||||
runtime.SetFinalizer(iter, (*dbIter).Release)
|
||||
return iter
|
||||
}
|
||||
@@ -89,6 +92,7 @@ const (
|
||||
|
||||
// dbIter represent an interator states over a database session.
|
||||
type dbIter struct {
|
||||
db *DB
|
||||
icmp *iComparer
|
||||
iter iterator.Iterator
|
||||
seq uint64
|
||||
@@ -303,6 +307,7 @@ func (i *dbIter) Release() {
|
||||
|
||||
if i.releaser != nil {
|
||||
i.releaser.Release()
|
||||
i.releaser = nil
|
||||
}
|
||||
|
||||
i.dir = dirReleased
|
||||
@@ -310,6 +315,8 @@ func (i *dbIter) Release() {
|
||||
i.value = nil
|
||||
i.iter.Release()
|
||||
i.iter = nil
|
||||
atomic.AddInt32(&i.db.aliveIters, -1)
|
||||
i.db = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
9
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_snapshot.go
generated
vendored
9
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_snapshot.go
generated
vendored
@@ -9,6 +9,7 @@ package leveldb
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
@@ -81,7 +82,7 @@ func (db *DB) minSeq() uint64 {
|
||||
type Snapshot struct {
|
||||
db *DB
|
||||
elem *snapshotElement
|
||||
mu sync.Mutex
|
||||
mu sync.RWMutex
|
||||
released bool
|
||||
}
|
||||
|
||||
@@ -91,6 +92,7 @@ func (db *DB) newSnapshot() *Snapshot {
|
||||
db: db,
|
||||
elem: db.acquireSnapshot(),
|
||||
}
|
||||
atomic.AddInt32(&db.aliveSnaps, 1)
|
||||
runtime.SetFinalizer(snap, (*Snapshot).Release)
|
||||
return snap
|
||||
}
|
||||
@@ -105,8 +107,8 @@ func (snap *Snapshot) Get(key []byte, ro *opt.ReadOptions) (value []byte, err er
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
snap.mu.Lock()
|
||||
defer snap.mu.Unlock()
|
||||
snap.mu.RLock()
|
||||
defer snap.mu.RUnlock()
|
||||
if snap.released {
|
||||
err = ErrSnapshotReleased
|
||||
return
|
||||
@@ -160,6 +162,7 @@ func (snap *Snapshot) Release() {
|
||||
|
||||
snap.released = true
|
||||
snap.db.releaseSnapshot(snap.elem)
|
||||
atomic.AddInt32(&snap.db.aliveSnaps, -1)
|
||||
snap.db = nil
|
||||
snap.elem = nil
|
||||
}
|
||||
|
||||
70
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_state.go
generated
vendored
70
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_state.go
generated
vendored
@@ -8,16 +8,16 @@ package leveldb
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/journal"
|
||||
"github.com/syndtr/goleveldb/leveldb/memdb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type memDB struct {
|
||||
pool *util.Pool
|
||||
db *memdb.DB
|
||||
ref int32
|
||||
db *DB
|
||||
mdb *memdb.DB
|
||||
ref int32
|
||||
}
|
||||
|
||||
func (m *memDB) incref() {
|
||||
@@ -26,7 +26,13 @@ func (m *memDB) incref() {
|
||||
|
||||
func (m *memDB) decref() {
|
||||
if ref := atomic.AddInt32(&m.ref, -1); ref == 0 {
|
||||
m.pool.Put(m)
|
||||
// Only put back memdb with std capacity.
|
||||
if m.mdb.Capacity() == m.db.s.o.GetWriteBuffer() {
|
||||
m.mdb.Reset()
|
||||
m.db.mpoolPut(m.mdb)
|
||||
}
|
||||
m.db = nil
|
||||
m.mdb = nil
|
||||
} else if ref < 0 {
|
||||
panic("negative memdb ref")
|
||||
}
|
||||
@@ -42,6 +48,41 @@ func (db *DB) addSeq(delta uint64) {
|
||||
atomic.AddUint64(&db.seq, delta)
|
||||
}
|
||||
|
||||
func (db *DB) mpoolPut(mem *memdb.DB) {
|
||||
defer func() {
|
||||
recover()
|
||||
}()
|
||||
select {
|
||||
case db.memPool <- mem:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DB) mpoolGet() *memdb.DB {
|
||||
select {
|
||||
case mem := <-db.memPool:
|
||||
return mem
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DB) mpoolDrain() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
select {
|
||||
case <-db.memPool:
|
||||
default:
|
||||
}
|
||||
case _, _ = <-db.closeC:
|
||||
close(db.memPool)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create new memdb and froze the old one; need external synchronization.
|
||||
// newMem only called synchronously by the writer.
|
||||
func (db *DB) newMem(n int) (mem *memDB, err error) {
|
||||
@@ -70,18 +111,15 @@ func (db *DB) newMem(n int) (mem *memDB, err error) {
|
||||
db.journalWriter = w
|
||||
db.journalFile = file
|
||||
db.frozenMem = db.mem
|
||||
mem, ok := db.memPool.Get().(*memDB)
|
||||
if ok && mem.db.Capacity() >= n {
|
||||
mem.db.Reset()
|
||||
mem.incref()
|
||||
} else {
|
||||
mem = &memDB{
|
||||
pool: db.memPool,
|
||||
db: memdb.New(db.s.icmp, maxInt(db.s.o.GetWriteBuffer(), n)),
|
||||
ref: 1,
|
||||
}
|
||||
mdb := db.mpoolGet()
|
||||
if mdb == nil || mdb.Capacity() < n {
|
||||
mdb = memdb.New(db.s.icmp, maxInt(db.s.o.GetWriteBuffer(), n))
|
||||
}
|
||||
mem = &memDB{
|
||||
db: db,
|
||||
mdb: mdb,
|
||||
ref: 2,
|
||||
}
|
||||
mem.incref()
|
||||
db.mem = mem
|
||||
// The seq only incremented by the writer. And whoever called newMem
|
||||
// should hold write lock, so no need additional synchronization here.
|
||||
|
||||
12
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_test.go
generated
vendored
12
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_test.go
generated
vendored
@@ -1210,7 +1210,7 @@ func TestDb_DeletionMarkers2(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDb_CompactionTableOpenError(t *testing.T) {
|
||||
h := newDbHarnessWopt(t, &opt.Options{MaxOpenFiles: 0})
|
||||
h := newDbHarnessWopt(t, &opt.Options{CachedOpenFiles: -1})
|
||||
defer h.close()
|
||||
|
||||
im := 10
|
||||
@@ -1577,7 +1577,11 @@ func TestDb_BloomFilter(t *testing.T) {
|
||||
return fmt.Sprintf("key%06d", i)
|
||||
}
|
||||
|
||||
n := 10000
|
||||
const (
|
||||
n = 10000
|
||||
indexOverhead = 19898
|
||||
filterOverhead = 19799
|
||||
)
|
||||
|
||||
// Populate multiple layers
|
||||
for i := 0; i < n; i++ {
|
||||
@@ -1601,7 +1605,7 @@ func TestDb_BloomFilter(t *testing.T) {
|
||||
cnt := int(h.stor.ReadCounter())
|
||||
t.Logf("lookup of %d present keys yield %d sstable I/O reads", n, cnt)
|
||||
|
||||
if min, max := n, n+2*n/100; cnt < min || cnt > max {
|
||||
if min, max := n+indexOverhead+filterOverhead, n+indexOverhead+filterOverhead+2*n/100; cnt < min || cnt > max {
|
||||
t.Errorf("num of sstable I/O reads of present keys not in range of %d - %d, got %d", min, max, cnt)
|
||||
}
|
||||
|
||||
@@ -1612,7 +1616,7 @@ func TestDb_BloomFilter(t *testing.T) {
|
||||
}
|
||||
cnt = int(h.stor.ReadCounter())
|
||||
t.Logf("lookup of %d missing keys yield %d sstable I/O reads", n, cnt)
|
||||
if max := 3 * n / 100; cnt > max {
|
||||
if max := 3*n/100 + indexOverhead + filterOverhead; cnt > max {
|
||||
t.Errorf("num of sstable I/O reads of missing keys was more than %d, got %d", max, cnt)
|
||||
}
|
||||
|
||||
|
||||
14
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_write.go
generated
vendored
14
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_write.go
generated
vendored
@@ -75,7 +75,7 @@ func (db *DB) flush(n int) (mem *memDB, nn int, err error) {
|
||||
mem = nil
|
||||
}
|
||||
}()
|
||||
nn = mem.db.Free()
|
||||
nn = mem.mdb.Free()
|
||||
switch {
|
||||
case v.tLen(0) >= kL0_SlowdownWritesTrigger && !delayed:
|
||||
delayed = true
|
||||
@@ -90,13 +90,13 @@ func (db *DB) flush(n int) (mem *memDB, nn int, err error) {
|
||||
}
|
||||
default:
|
||||
// Allow memdb to grow if it has no entry.
|
||||
if mem.db.Len() == 0 {
|
||||
if mem.mdb.Len() == 0 {
|
||||
nn = n
|
||||
} else {
|
||||
mem.decref()
|
||||
mem, err = db.rotateMem(n)
|
||||
if err == nil {
|
||||
nn = mem.db.Free()
|
||||
nn = mem.mdb.Free()
|
||||
} else {
|
||||
nn = 0
|
||||
}
|
||||
@@ -190,7 +190,7 @@ drain:
|
||||
return
|
||||
case db.journalC <- b:
|
||||
// Write into memdb
|
||||
b.memReplay(mem.db)
|
||||
b.memReplay(mem.mdb)
|
||||
}
|
||||
// Wait for journal writer
|
||||
select {
|
||||
@@ -200,7 +200,7 @@ drain:
|
||||
case err = <-db.journalAckC:
|
||||
if err != nil {
|
||||
// Revert memdb if error detected
|
||||
b.revertMemReplay(mem.db)
|
||||
b.revertMemReplay(mem.mdb)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -209,7 +209,7 @@ drain:
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
b.memReplay(mem.db)
|
||||
b.memReplay(mem.mdb)
|
||||
}
|
||||
|
||||
// Set last seq number.
|
||||
@@ -271,7 +271,7 @@ func (db *DB) CompactRange(r util.Range) error {
|
||||
// Check for overlaps in memdb.
|
||||
mem := db.getEffectiveMem()
|
||||
defer mem.decref()
|
||||
if isMemOverlaps(db.s.icmp, mem.db, r.Start, r.Limit) {
|
||||
if isMemOverlaps(db.s.icmp, mem.mdb, r.Start, r.Limit) {
|
||||
// Memdb compaction.
|
||||
if _, err := db.rotateMem(0); err != nil {
|
||||
<-db.writeLockC
|
||||
|
||||
9
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/external_test.go
generated
vendored
9
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/external_test.go
generated
vendored
@@ -21,7 +21,7 @@ var _ = testutil.Defer(func() {
|
||||
BlockRestartInterval: 5,
|
||||
BlockSize: 50,
|
||||
Compression: opt.NoCompression,
|
||||
MaxOpenFiles: 0,
|
||||
CachedOpenFiles: -1,
|
||||
Strict: opt.StrictAll,
|
||||
WriteBuffer: 1000,
|
||||
}
|
||||
@@ -40,18 +40,17 @@ var _ = testutil.Defer(func() {
|
||||
})
|
||||
|
||||
Describe("read test", func() {
|
||||
testutil.AllKeyValueTesting(nil, func(kv testutil.KeyValue) testutil.DB {
|
||||
testutil.AllKeyValueTesting(nil, nil, func(kv testutil.KeyValue) testutil.DB {
|
||||
// Building the DB.
|
||||
db := newTestingDB(o, nil, nil)
|
||||
kv.IterateShuffled(nil, func(i int, key, value []byte) {
|
||||
err := db.TestPut(key, value)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
testutil.Defer("teardown", func() {
|
||||
db.TestClose()
|
||||
})
|
||||
|
||||
return db
|
||||
}, func(db testutil.DB) {
|
||||
db.(*testingDB).TestClose()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
2
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/memdb/memdb_test.go
generated
vendored
2
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/memdb/memdb_test.go
generated
vendored
@@ -129,7 +129,7 @@ var _ = testutil.Defer(func() {
|
||||
}
|
||||
|
||||
return db
|
||||
})
|
||||
}, nil, nil)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
49
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/opt/options.go
generated
vendored
49
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/opt/options.go
generated
vendored
@@ -24,19 +24,22 @@ const (
|
||||
DefaultBlockRestartInterval = 16
|
||||
DefaultBlockSize = 4 * KiB
|
||||
DefaultCompressionType = SnappyCompression
|
||||
DefaultMaxOpenFiles = 1000
|
||||
DefaultCachedOpenFiles = 500
|
||||
DefaultWriteBuffer = 4 * MiB
|
||||
)
|
||||
|
||||
type noCache struct{}
|
||||
|
||||
func (noCache) SetCapacity(capacity int) {}
|
||||
func (noCache) Capacity() int { return 0 }
|
||||
func (noCache) Used() int { return 0 }
|
||||
func (noCache) Size() int { return 0 }
|
||||
func (noCache) GetNamespace(id uint64) cache.Namespace { return nil }
|
||||
func (noCache) Purge(fin cache.PurgeFin) {}
|
||||
func (noCache) Zap() {}
|
||||
func (noCache) SetCapacity(capacity int) {}
|
||||
func (noCache) Capacity() int { return 0 }
|
||||
func (noCache) Used() int { return 0 }
|
||||
func (noCache) Size() int { return 0 }
|
||||
func (noCache) NumObjects() int { return 0 }
|
||||
func (noCache) GetNamespace(id uint64) cache.Namespace { return nil }
|
||||
func (noCache) PurgeNamespace(id uint64, fin cache.PurgeFin) {}
|
||||
func (noCache) ZapNamespace(id uint64) {}
|
||||
func (noCache) Purge(fin cache.PurgeFin) {}
|
||||
func (noCache) Zap() {}
|
||||
|
||||
var NoCache cache.Cache = noCache{}
|
||||
|
||||
@@ -122,6 +125,13 @@ type Options struct {
|
||||
// The default value is 4KiB.
|
||||
BlockSize int
|
||||
|
||||
// CachedOpenFiles defines number of open files to kept around when not
|
||||
// in-use, the counting includes still in-use files.
|
||||
// Set this to negative value to disable caching.
|
||||
//
|
||||
// The default value is 500.
|
||||
CachedOpenFiles int
|
||||
|
||||
// Comparer defines a total ordering over the space of []byte keys: a 'less
|
||||
// than' relationship. The same comparison algorithm must be used for reads
|
||||
// and writes over the lifetime of the DB.
|
||||
@@ -162,13 +172,6 @@ type Options struct {
|
||||
// The default value is nil.
|
||||
Filter filter.Filter
|
||||
|
||||
// MaxOpenFiles defines maximum number of open files to kept around
|
||||
// (cached). This is not an hard limit, actual open files may exceed
|
||||
// the defined value.
|
||||
//
|
||||
// The default value is 1000.
|
||||
MaxOpenFiles int
|
||||
|
||||
// Strict defines the DB strict level.
|
||||
Strict Strict
|
||||
|
||||
@@ -210,6 +213,15 @@ func (o *Options) GetBlockSize() int {
|
||||
return o.BlockSize
|
||||
}
|
||||
|
||||
func (o *Options) GetCachedOpenFiles() int {
|
||||
if o == nil || o.CachedOpenFiles == 0 {
|
||||
return DefaultCachedOpenFiles
|
||||
} else if o.CachedOpenFiles < 0 {
|
||||
return 0
|
||||
}
|
||||
return o.CachedOpenFiles
|
||||
}
|
||||
|
||||
func (o *Options) GetComparer() comparer.Comparer {
|
||||
if o == nil || o.Comparer == nil {
|
||||
return comparer.DefaultComparer
|
||||
@@ -245,13 +257,6 @@ func (o *Options) GetFilter() filter.Filter {
|
||||
return o.Filter
|
||||
}
|
||||
|
||||
func (o *Options) GetMaxOpenFiles() int {
|
||||
if o == nil || o.MaxOpenFiles <= 0 {
|
||||
return DefaultMaxOpenFiles
|
||||
}
|
||||
return o.MaxOpenFiles
|
||||
}
|
||||
|
||||
func (o *Options) GetStrict(strict Strict) bool {
|
||||
if o == nil || o.Strict == 0 {
|
||||
return DefaultStrict&strict != 0
|
||||
|
||||
2
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/session.go
generated
vendored
2
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/session.go
generated
vendored
@@ -58,7 +58,7 @@ func newSession(stor storage.Storage, o *opt.Options) (s *session, err error) {
|
||||
storLock: storLock,
|
||||
}
|
||||
s.setOptions(o)
|
||||
s.tops = newTableOps(s, s.o.GetMaxOpenFiles())
|
||||
s.tops = newTableOps(s, s.o.GetCachedOpenFiles())
|
||||
s.setVersion(&version{s: s})
|
||||
s.log("log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock D·DeletedEntry L·Level Q·SeqNum T·TimeElapsed")
|
||||
return
|
||||
|
||||
26
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table.go
generated
vendored
26
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table.go
generated
vendored
@@ -7,7 +7,6 @@
|
||||
package leveldb
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -323,15 +322,6 @@ func (t *tOps) createFrom(src iterator.Iterator) (f *tFile, n int, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
type tableWrapper struct {
|
||||
*table.Reader
|
||||
closer io.Closer
|
||||
}
|
||||
|
||||
func (tr tableWrapper) Release() {
|
||||
tr.closer.Close()
|
||||
}
|
||||
|
||||
// Opens table. It returns a cache handle, which should
|
||||
// be released after use.
|
||||
func (t *tOps) open(f *tFile) (ch cache.Handle, err error) {
|
||||
@@ -347,7 +337,7 @@ func (t *tOps) open(f *tFile) (ch cache.Handle, err error) {
|
||||
if bc := t.s.o.GetBlockCache(); bc != nil {
|
||||
bcacheNS = bc.GetNamespace(num)
|
||||
}
|
||||
return 1, tableWrapper{table.NewReader(r, int64(f.size), bcacheNS, t.bpool, t.s.o), r}
|
||||
return 1, table.NewReader(r, int64(f.size), bcacheNS, t.bpool, t.s.o)
|
||||
})
|
||||
if ch == nil && err == nil {
|
||||
err = ErrClosed
|
||||
@@ -363,7 +353,7 @@ func (t *tOps) find(f *tFile, key []byte, ro *opt.ReadOptions) (rkey, rvalue []b
|
||||
return nil, nil, err
|
||||
}
|
||||
defer ch.Release()
|
||||
return ch.Value().(tableWrapper).Find(key, ro)
|
||||
return ch.Value().(*table.Reader).Find(key, ro)
|
||||
}
|
||||
|
||||
// Returns approximate offset of the given key.
|
||||
@@ -372,10 +362,9 @@ func (t *tOps) offsetOf(f *tFile, key []byte) (offset uint64, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_offset, err := ch.Value().(tableWrapper).OffsetOf(key)
|
||||
offset = uint64(_offset)
|
||||
ch.Release()
|
||||
return
|
||||
defer ch.Release()
|
||||
offset_, err := ch.Value().(*table.Reader).OffsetOf(key)
|
||||
return uint64(offset_), err
|
||||
}
|
||||
|
||||
// Creates an iterator from the given table.
|
||||
@@ -384,7 +373,7 @@ func (t *tOps) newIterator(f *tFile, slice *util.Range, ro *opt.ReadOptions) ite
|
||||
if err != nil {
|
||||
return iterator.NewEmptyIterator(err)
|
||||
}
|
||||
iter := ch.Value().(tableWrapper).NewIterator(slice, ro)
|
||||
iter := ch.Value().(*table.Reader).NewIterator(slice, ro)
|
||||
iter.SetReleaser(ch)
|
||||
return iter
|
||||
}
|
||||
@@ -401,7 +390,7 @@ func (t *tOps) remove(f *tFile) {
|
||||
t.s.logf("table@remove removed @%d", num)
|
||||
}
|
||||
if bc := t.s.o.GetBlockCache(); bc != nil {
|
||||
bc.GetNamespace(num).Zap()
|
||||
bc.ZapNamespace(num)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -411,6 +400,7 @@ func (t *tOps) remove(f *tFile) {
|
||||
// regadless still used or not.
|
||||
func (t *tOps) close() {
|
||||
t.cache.Zap()
|
||||
t.bpool.Close()
|
||||
}
|
||||
|
||||
// Creates new initialized table ops instance.
|
||||
|
||||
4
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table/block_test.go
generated
vendored
4
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table/block_test.go
generated
vendored
@@ -40,7 +40,7 @@ var _ = testutil.Defer(func() {
|
||||
data := bw.buf.Bytes()
|
||||
restartsLen := int(binary.LittleEndian.Uint32(data[len(data)-4:]))
|
||||
return &block{
|
||||
cmp: comparer.DefaultComparer,
|
||||
tr: &Reader{cmp: comparer.DefaultComparer},
|
||||
data: data,
|
||||
restartsLen: restartsLen,
|
||||
restartsOffset: len(data) - (restartsLen+1)*4,
|
||||
@@ -59,7 +59,7 @@ var _ = testutil.Defer(func() {
|
||||
// Make block.
|
||||
br := Build(kv, restartInterval)
|
||||
// Do testing.
|
||||
testutil.KeyValueTesting(nil, br, kv.Clone())
|
||||
testutil.KeyValueTesting(nil, kv.Clone(), br, nil, nil)
|
||||
}
|
||||
|
||||
Describe(Text(), Test)
|
||||
|
||||
217
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table/reader.go
generated
vendored
217
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table/reader.go
generated
vendored
@@ -37,8 +37,7 @@ func max(x, y int) int {
|
||||
}
|
||||
|
||||
type block struct {
|
||||
bpool *util.BufferPool
|
||||
cmp comparer.BasicComparer
|
||||
tr *Reader
|
||||
data []byte
|
||||
restartsLen int
|
||||
restartsOffset int
|
||||
@@ -47,31 +46,25 @@ type block struct {
|
||||
}
|
||||
|
||||
func (b *block) seek(rstart, rlimit int, key []byte) (index, offset int, err error) {
|
||||
n := b.restartsOffset
|
||||
data := b.data
|
||||
cmp := b.cmp
|
||||
|
||||
index = sort.Search(b.restartsLen-rstart-(b.restartsLen-rlimit), func(i int) bool {
|
||||
offset := int(binary.LittleEndian.Uint32(data[n+4*(rstart+i):]))
|
||||
offset += 1 // shared always zero, since this is a restart point
|
||||
v1, n1 := binary.Uvarint(data[offset:]) // key length
|
||||
_, n2 := binary.Uvarint(data[offset+n1:]) // value length
|
||||
offset := int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*(rstart+i):]))
|
||||
offset += 1 // shared always zero, since this is a restart point
|
||||
v1, n1 := binary.Uvarint(b.data[offset:]) // key length
|
||||
_, n2 := binary.Uvarint(b.data[offset+n1:]) // value length
|
||||
m := offset + n1 + n2
|
||||
return cmp.Compare(data[m:m+int(v1)], key) > 0
|
||||
return b.tr.cmp.Compare(b.data[m:m+int(v1)], key) > 0
|
||||
}) + rstart - 1
|
||||
if index < rstart {
|
||||
// The smallest key is greater-than key sought.
|
||||
index = rstart
|
||||
}
|
||||
offset = int(binary.LittleEndian.Uint32(data[n+4*index:]))
|
||||
offset = int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*index:]))
|
||||
return
|
||||
}
|
||||
|
||||
func (b *block) restartIndex(rstart, rlimit, offset int) int {
|
||||
n := b.restartsOffset
|
||||
data := b.data
|
||||
return sort.Search(b.restartsLen-rstart-(b.restartsLen-rlimit), func(i int) bool {
|
||||
return int(binary.LittleEndian.Uint32(data[n+4*(rstart+i):])) > offset
|
||||
return int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*(rstart+i):])) > offset
|
||||
}) + rstart - 1
|
||||
}
|
||||
|
||||
@@ -141,10 +134,10 @@ func (b *block) newIterator(slice *util.Range, inclLimit bool, cache util.Releas
|
||||
}
|
||||
|
||||
func (b *block) Release() {
|
||||
if b.bpool != nil {
|
||||
b.bpool.Put(b.data)
|
||||
b.bpool = nil
|
||||
if b.tr.bpool != nil {
|
||||
b.tr.bpool.Put(b.data)
|
||||
}
|
||||
b.tr = nil
|
||||
b.data = nil
|
||||
}
|
||||
|
||||
@@ -270,7 +263,7 @@ func (i *blockIter) Seek(key []byte) bool {
|
||||
i.dir = dirForward
|
||||
}
|
||||
for i.Next() {
|
||||
if i.block.cmp.Compare(i.key, key) >= 0 {
|
||||
if i.block.tr.cmp.Compare(i.key, key) >= 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -479,7 +472,7 @@ func (i *blockIter) Error() error {
|
||||
}
|
||||
|
||||
type filterBlock struct {
|
||||
filter filter.Filter
|
||||
tr *Reader
|
||||
data []byte
|
||||
oOffset int
|
||||
baseLg uint
|
||||
@@ -493,7 +486,7 @@ func (b *filterBlock) contains(offset uint64, key []byte) bool {
|
||||
n := int(binary.LittleEndian.Uint32(o))
|
||||
m := int(binary.LittleEndian.Uint32(o[4:]))
|
||||
if n < m && m <= b.oOffset {
|
||||
return b.filter.Contains(b.data[n:m], key)
|
||||
return b.tr.filter.Contains(b.data[n:m], key)
|
||||
} else if n == m {
|
||||
return false
|
||||
}
|
||||
@@ -501,10 +494,17 @@ func (b *filterBlock) contains(offset uint64, key []byte) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *filterBlock) Release() {
|
||||
if b.tr.bpool != nil {
|
||||
b.tr.bpool.Put(b.data)
|
||||
}
|
||||
b.tr = nil
|
||||
b.data = nil
|
||||
}
|
||||
|
||||
type indexIter struct {
|
||||
blockIter
|
||||
tableReader *Reader
|
||||
slice *util.Range
|
||||
*blockIter
|
||||
slice *util.Range
|
||||
// Options
|
||||
checksum bool
|
||||
fillCache bool
|
||||
@@ -523,7 +523,7 @@ func (i *indexIter) Get() iterator.Iterator {
|
||||
if i.slice != nil && (i.blockIter.isFirst() || i.blockIter.isLast()) {
|
||||
slice = i.slice
|
||||
}
|
||||
return i.tableReader.getDataIter(dataBH, slice, i.checksum, i.fillCache)
|
||||
return i.blockIter.block.tr.getDataIter(dataBH, slice, i.checksum, i.fillCache)
|
||||
}
|
||||
|
||||
// Reader is a table reader.
|
||||
@@ -538,9 +538,8 @@ type Reader struct {
|
||||
checksum bool
|
||||
strictIter bool
|
||||
|
||||
dataEnd int64
|
||||
indexBlock *block
|
||||
filterBlock *filterBlock
|
||||
dataEnd int64
|
||||
indexBH, filterBH blockHandle
|
||||
}
|
||||
|
||||
func verifyChecksum(data []byte) bool {
|
||||
@@ -557,6 +556,7 @@ func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
|
||||
}
|
||||
if checksum || r.checksum {
|
||||
if !verifyChecksum(data) {
|
||||
r.bpool.Put(data)
|
||||
return nil, errors.New("leveldb/table: Reader: invalid block (checksum mismatch)")
|
||||
}
|
||||
}
|
||||
@@ -575,6 +575,7 @@ func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
r.bpool.Put(data)
|
||||
return nil, fmt.Errorf("leveldb/table: Reader: unknown block compression type: %d", data[bh.length])
|
||||
}
|
||||
return data, nil
|
||||
@@ -587,7 +588,7 @@ func (r *Reader) readBlock(bh blockHandle, checksum bool) (*block, error) {
|
||||
}
|
||||
restartsLen := int(binary.LittleEndian.Uint32(data[len(data)-4:]))
|
||||
b := &block{
|
||||
cmp: r.cmp,
|
||||
tr: r,
|
||||
data: data,
|
||||
restartsLen: restartsLen,
|
||||
restartsOffset: len(data) - (restartsLen+1)*4,
|
||||
@@ -596,7 +597,44 @@ func (r *Reader) readBlock(bh blockHandle, checksum bool) (*block, error) {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterBlock, error) {
|
||||
func (r *Reader) readBlockCached(bh blockHandle, checksum, fillCache bool) (*block, util.Releaser, error) {
|
||||
if r.cache != nil {
|
||||
var err error
|
||||
ch := r.cache.Get(bh.offset, func() (charge int, value interface{}) {
|
||||
if !fillCache {
|
||||
return 0, nil
|
||||
}
|
||||
var b *block
|
||||
b, err = r.readBlock(bh, checksum)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return cap(b.data), b
|
||||
})
|
||||
if ch != nil {
|
||||
b, ok := ch.Value().(*block)
|
||||
if !ok {
|
||||
ch.Release()
|
||||
return nil, nil, errors.New("leveldb/table: Reader: inconsistent block type")
|
||||
}
|
||||
if !b.checksum && (r.checksum || checksum) {
|
||||
if !verifyChecksum(b.data) {
|
||||
ch.Release()
|
||||
return nil, nil, errors.New("leveldb/table: Reader: invalid block (checksum mismatch)")
|
||||
}
|
||||
b.checksum = true
|
||||
}
|
||||
return b, ch, err
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := r.readBlock(bh, checksum)
|
||||
return b, b, err
|
||||
}
|
||||
|
||||
func (r *Reader) readFilterBlock(bh blockHandle) (*filterBlock, error) {
|
||||
data, err := r.readRawBlock(bh, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -611,7 +649,7 @@ func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterB
|
||||
return nil, errors.New("leveldb/table: Reader: invalid filter block (invalid offset)")
|
||||
}
|
||||
b := &filterBlock{
|
||||
filter: filter,
|
||||
tr: r,
|
||||
data: data,
|
||||
oOffset: oOffset,
|
||||
baseLg: uint(data[n-1]),
|
||||
@@ -620,42 +658,42 @@ func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterB
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fillCache bool) iterator.Iterator {
|
||||
func (r *Reader) readFilterBlockCached(bh blockHandle, fillCache bool) (*filterBlock, util.Releaser, error) {
|
||||
if r.cache != nil {
|
||||
// Get/set block cache.
|
||||
var err error
|
||||
cache := r.cache.Get(dataBH.offset, func() (charge int, value interface{}) {
|
||||
ch := r.cache.Get(bh.offset, func() (charge int, value interface{}) {
|
||||
if !fillCache {
|
||||
return 0, nil
|
||||
}
|
||||
var dataBlock *block
|
||||
dataBlock, err = r.readBlock(dataBH, checksum)
|
||||
var b *filterBlock
|
||||
b, err = r.readFilterBlock(bh)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
return int(dataBH.length), dataBlock
|
||||
return cap(b.data), b
|
||||
})
|
||||
if err != nil {
|
||||
return iterator.NewEmptyIterator(err)
|
||||
}
|
||||
if cache != nil {
|
||||
dataBlock := cache.Value().(*block)
|
||||
if !dataBlock.checksum && (r.checksum || checksum) {
|
||||
if !verifyChecksum(dataBlock.data) {
|
||||
return iterator.NewEmptyIterator(errors.New("leveldb/table: Reader: invalid block (checksum mismatch)"))
|
||||
}
|
||||
dataBlock.checksum = true
|
||||
if ch != nil {
|
||||
b, ok := ch.Value().(*filterBlock)
|
||||
if !ok {
|
||||
ch.Release()
|
||||
return nil, nil, errors.New("leveldb/table: Reader: inconsistent block type")
|
||||
}
|
||||
iter := dataBlock.newIterator(slice, false, cache)
|
||||
return iter
|
||||
return b, ch, err
|
||||
} else if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
dataBlock, err := r.readBlock(dataBH, checksum)
|
||||
|
||||
b, err := r.readFilterBlock(bh)
|
||||
return b, b, err
|
||||
}
|
||||
|
||||
func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fillCache bool) iterator.Iterator {
|
||||
b, rel, err := r.readBlockCached(dataBH, checksum, fillCache)
|
||||
if err != nil {
|
||||
return iterator.NewEmptyIterator(err)
|
||||
}
|
||||
iter := dataBlock.newIterator(slice, false, dataBlock)
|
||||
return iter
|
||||
return b.newIterator(slice, false, rel)
|
||||
}
|
||||
|
||||
// NewIterator creates an iterator from the table.
|
||||
@@ -669,18 +707,21 @@ func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fi
|
||||
// when not used.
|
||||
//
|
||||
// Also read Iterator documentation of the leveldb/iterator package.
|
||||
|
||||
func (r *Reader) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
|
||||
if r.err != nil {
|
||||
return iterator.NewEmptyIterator(r.err)
|
||||
}
|
||||
|
||||
fillCache := !ro.GetDontFillCache()
|
||||
b, rel, err := r.readBlockCached(r.indexBH, true, fillCache)
|
||||
if err != nil {
|
||||
return iterator.NewEmptyIterator(err)
|
||||
}
|
||||
index := &indexIter{
|
||||
blockIter: *r.indexBlock.newIterator(slice, true, nil),
|
||||
tableReader: r,
|
||||
slice: slice,
|
||||
checksum: ro.GetStrict(opt.StrictBlockChecksum),
|
||||
fillCache: !ro.GetDontFillCache(),
|
||||
blockIter: b.newIterator(slice, true, rel),
|
||||
slice: slice,
|
||||
checksum: ro.GetStrict(opt.StrictBlockChecksum),
|
||||
fillCache: !ro.GetDontFillCache(),
|
||||
}
|
||||
return iterator.NewIndexedIterator(index, r.strictIter || ro.GetStrict(opt.StrictIterator), false)
|
||||
}
|
||||
@@ -697,7 +738,13 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
|
||||
return
|
||||
}
|
||||
|
||||
index := r.indexBlock.newIterator(nil, true, nil)
|
||||
indexBlock, rel, err := r.readBlockCached(r.indexBH, true, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rel.Release()
|
||||
|
||||
index := indexBlock.newIterator(nil, true, nil)
|
||||
defer index.Release()
|
||||
if !index.Seek(key) {
|
||||
err = index.Error()
|
||||
@@ -711,9 +758,15 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
|
||||
err = errors.New("leveldb/table: Reader: invalid table (bad data block handle)")
|
||||
return
|
||||
}
|
||||
if r.filterBlock != nil && !r.filterBlock.contains(dataBH.offset, key) {
|
||||
err = ErrNotFound
|
||||
return
|
||||
if r.filter != nil {
|
||||
filterBlock, rel, ferr := r.readFilterBlockCached(r.filterBH, true)
|
||||
if ferr == nil {
|
||||
if !filterBlock.contains(dataBH.offset, key) {
|
||||
rel.Release()
|
||||
return nil, nil, ErrNotFound
|
||||
}
|
||||
rel.Release()
|
||||
}
|
||||
}
|
||||
data := r.getDataIter(dataBH, nil, ro.GetStrict(opt.StrictBlockChecksum), !ro.GetDontFillCache())
|
||||
defer data.Release()
|
||||
@@ -760,7 +813,13 @@ func (r *Reader) OffsetOf(key []byte) (offset int64, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
index := r.indexBlock.newIterator(nil, true, nil)
|
||||
indexBlock, rel, err := r.readBlockCached(r.indexBH, true, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rel.Release()
|
||||
|
||||
index := indexBlock.newIterator(nil, true, nil)
|
||||
defer index.Release()
|
||||
if index.Seek(key) {
|
||||
dataBH, n := decodeBlockHandle(index.Value())
|
||||
@@ -778,6 +837,17 @@ func (r *Reader) OffsetOf(key []byte) (offset int64, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Release implements util.Releaser.
|
||||
// It also close the file if it is an io.Closer.
|
||||
func (r *Reader) Release() {
|
||||
if closer, ok := r.reader.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
r.reader = nil
|
||||
r.cache = nil
|
||||
r.bpool = nil
|
||||
}
|
||||
|
||||
// NewReader creates a new initialized table reader for the file.
|
||||
// The cache and bpool is optional and can be nil.
|
||||
//
|
||||
@@ -817,16 +887,11 @@ func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, bpool *util.Buf
|
||||
return r
|
||||
}
|
||||
// Decode the index block handle.
|
||||
indexBH, n := decodeBlockHandle(footer[n:])
|
||||
r.indexBH, n = decodeBlockHandle(footer[n:])
|
||||
if n == 0 {
|
||||
r.err = errors.New("leveldb/table: Reader: invalid table (bad index block handle)")
|
||||
return r
|
||||
}
|
||||
// Read index block.
|
||||
r.indexBlock, r.err = r.readBlock(indexBH, true)
|
||||
if r.err != nil {
|
||||
return r
|
||||
}
|
||||
// Read metaindex block.
|
||||
metaBlock, err := r.readBlock(metaBH, true)
|
||||
if err != nil {
|
||||
@@ -842,32 +907,28 @@ func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, bpool *util.Buf
|
||||
continue
|
||||
}
|
||||
fn := key[7:]
|
||||
var filter filter.Filter
|
||||
if f0 := o.GetFilter(); f0 != nil && f0.Name() == fn {
|
||||
filter = f0
|
||||
r.filter = f0
|
||||
} else {
|
||||
for _, f0 := range o.GetAltFilters() {
|
||||
if f0.Name() == fn {
|
||||
filter = f0
|
||||
r.filter = f0
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if filter != nil {
|
||||
if r.filter != nil {
|
||||
filterBH, n := decodeBlockHandle(metaIter.Value())
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
r.filterBH = filterBH
|
||||
// Update data end.
|
||||
r.dataEnd = int64(filterBH.offset)
|
||||
filterBlock, err := r.readFilterBlock(filterBH, filter)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
r.filterBlock = filterBlock
|
||||
break
|
||||
}
|
||||
}
|
||||
metaIter.Release()
|
||||
metaBlock.Release()
|
||||
return r
|
||||
}
|
||||
|
||||
8
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table/table_test.go
generated
vendored
8
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/table/table_test.go
generated
vendored
@@ -104,14 +104,16 @@ var _ = testutil.Defer(func() {
|
||||
if body != nil {
|
||||
body(db.(tableWrapper).Reader)
|
||||
}
|
||||
testutil.KeyValueTesting(nil, db, *kv)
|
||||
testutil.KeyValueTesting(nil, *kv, db, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
testutil.AllKeyValueTesting(nil, Build)
|
||||
testutil.AllKeyValueTesting(nil, Build, nil, nil)
|
||||
Describe("with one key per block", Test(testutil.KeyValue_Generate(nil, 9, 1, 10, 512, 512), func(r *Reader) {
|
||||
It("should have correct blocks number", func() {
|
||||
Expect(r.indexBlock.restartsLen).Should(Equal(9))
|
||||
indexBlock, err := r.readBlock(r.indexBH, true)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(indexBlock.restartsLen).Should(Equal(9))
|
||||
})
|
||||
}))
|
||||
})
|
||||
|
||||
100
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/testutil/kvtest.go
generated
vendored
100
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/testutil/kvtest.go
generated
vendored
@@ -16,13 +16,22 @@ import (
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
|
||||
func KeyValueTesting(rnd *rand.Rand, kv KeyValue, p DB, setup func(KeyValue) DB, teardown func(DB)) {
|
||||
if rnd == nil {
|
||||
rnd = NewRand()
|
||||
}
|
||||
|
||||
if db, ok := p.(Find); ok {
|
||||
It("Should find all keys with Find", func() {
|
||||
if p == nil {
|
||||
BeforeEach(func() {
|
||||
p = setup(kv)
|
||||
})
|
||||
AfterEach(func() {
|
||||
teardown(p)
|
||||
})
|
||||
}
|
||||
|
||||
It("Should find all keys with Find", func() {
|
||||
if db, ok := p.(Find); ok {
|
||||
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
|
||||
key_, key, value := kv.IndexInexact(i)
|
||||
|
||||
@@ -38,9 +47,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
|
||||
Expect(rkey).Should(Equal(key))
|
||||
Expect(rvalue).Should(Equal(value), "Value for key %q (%q)", key_, key)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
It("Should return error if the key is not present", func() {
|
||||
It("Should return error if the key is not present", func() {
|
||||
if db, ok := p.(Find); ok {
|
||||
var key []byte
|
||||
if kv.Len() > 0 {
|
||||
key_, _ := kv.Index(kv.Len() - 1)
|
||||
@@ -49,11 +60,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
|
||||
rkey, _, err := db.TestFind(key)
|
||||
Expect(err).Should(HaveOccurred(), "Find for key %q yield key %q", key, rkey)
|
||||
Expect(err).Should(Equal(util.ErrNotFound))
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if db, ok := p.(Get); ok {
|
||||
It("Should only find exact key with Get", func() {
|
||||
It("Should only find exact key with Get", func() {
|
||||
if db, ok := p.(Get); ok {
|
||||
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
|
||||
key_, key, value := kv.IndexInexact(i)
|
||||
|
||||
@@ -69,11 +80,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
|
||||
Expect(err).Should(Equal(util.ErrNotFound))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if db, ok := p.(NewIterator); ok {
|
||||
TestIter := func(r *util.Range, _kv KeyValue) {
|
||||
TestIter := func(r *util.Range, _kv KeyValue) {
|
||||
if db, ok := p.(NewIterator); ok {
|
||||
iter := db.TestNewIterator(r)
|
||||
Expect(iter.Error()).ShouldNot(HaveOccurred())
|
||||
|
||||
@@ -84,45 +95,48 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
|
||||
|
||||
DoIteratorTesting(&t)
|
||||
}
|
||||
}
|
||||
|
||||
It("Should iterates and seeks correctly", func(done Done) {
|
||||
TestIter(nil, kv.Clone())
|
||||
done <- true
|
||||
}, 3.0)
|
||||
It("Should iterates and seeks correctly", func(done Done) {
|
||||
TestIter(nil, kv.Clone())
|
||||
done <- true
|
||||
}, 3.0)
|
||||
|
||||
RandomIndex(rnd, kv.Len(), kv.Len(), func(i int) {
|
||||
type slice struct {
|
||||
r *util.Range
|
||||
start, limit int
|
||||
}
|
||||
RandomIndex(rnd, kv.Len(), kv.Len(), func(i int) {
|
||||
type slice struct {
|
||||
r *util.Range
|
||||
start, limit int
|
||||
}
|
||||
|
||||
key_, _, _ := kv.IndexInexact(i)
|
||||
for _, x := range []slice{
|
||||
{&util.Range{Start: key_, Limit: nil}, i, kv.Len()},
|
||||
{&util.Range{Start: nil, Limit: key_}, 0, i},
|
||||
} {
|
||||
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", x.start, x.limit), func(done Done) {
|
||||
TestIter(x.r, kv.Slice(x.start, x.limit))
|
||||
done <- true
|
||||
}, 3.0)
|
||||
}
|
||||
})
|
||||
|
||||
RandomRange(rnd, kv.Len(), kv.Len(), func(start, limit int) {
|
||||
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", start, limit), func(done Done) {
|
||||
r := kv.Range(start, limit)
|
||||
TestIter(&r, kv.Slice(start, limit))
|
||||
key_, _, _ := kv.IndexInexact(i)
|
||||
for _, x := range []slice{
|
||||
{&util.Range{Start: key_, Limit: nil}, i, kv.Len()},
|
||||
{&util.Range{Start: nil, Limit: key_}, 0, i},
|
||||
} {
|
||||
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", x.start, x.limit), func(done Done) {
|
||||
TestIter(x.r, kv.Slice(x.start, x.limit))
|
||||
done <- true
|
||||
}, 3.0)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
RandomRange(rnd, kv.Len(), kv.Len(), func(start, limit int) {
|
||||
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", start, limit), func(done Done) {
|
||||
r := kv.Range(start, limit)
|
||||
TestIter(&r, kv.Slice(start, limit))
|
||||
done <- true
|
||||
}, 3.0)
|
||||
})
|
||||
}
|
||||
|
||||
func AllKeyValueTesting(rnd *rand.Rand, body func(kv KeyValue) DB) {
|
||||
func AllKeyValueTesting(rnd *rand.Rand, body, setup func(KeyValue) DB, teardown func(DB)) {
|
||||
Test := func(kv *KeyValue) func() {
|
||||
return func() {
|
||||
db := body(*kv)
|
||||
KeyValueTesting(rnd, db, *kv)
|
||||
var p DB
|
||||
if body != nil {
|
||||
p = body(*kv)
|
||||
}
|
||||
KeyValueTesting(rnd, *kv, p, setup, teardown)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,15 +19,21 @@ type buffer struct {
|
||||
|
||||
// BufferPool is a 'buffer pool'.
|
||||
type BufferPool struct {
|
||||
pool [4]chan []byte
|
||||
size [3]uint32
|
||||
sizeMiss [3]uint32
|
||||
baseline0 int
|
||||
baseline1 int
|
||||
baseline2 int
|
||||
pool [6]chan []byte
|
||||
size [5]uint32
|
||||
sizeMiss [5]uint32
|
||||
sizeHalf [5]uint32
|
||||
baseline [4]int
|
||||
baselinex0 int
|
||||
baselinex1 int
|
||||
baseline0 int
|
||||
baseline1 int
|
||||
baseline2 int
|
||||
close chan struct{}
|
||||
|
||||
get uint32
|
||||
put uint32
|
||||
half uint32
|
||||
less uint32
|
||||
equal uint32
|
||||
greater uint32
|
||||
@@ -35,16 +41,15 @@ type BufferPool struct {
|
||||
}
|
||||
|
||||
func (p *BufferPool) poolNum(n int) int {
|
||||
switch {
|
||||
case n <= p.baseline0:
|
||||
if n <= p.baseline0 && n > p.baseline0/2 {
|
||||
return 0
|
||||
case n <= p.baseline1:
|
||||
return 1
|
||||
case n <= p.baseline2:
|
||||
return 2
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
for i, x := range p.baseline {
|
||||
if n <= x {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
return len(p.baseline) + 1
|
||||
}
|
||||
|
||||
// Get returns buffer with length of n.
|
||||
@@ -59,13 +64,22 @@ func (p *BufferPool) Get(n int) []byte {
|
||||
case b := <-pool:
|
||||
switch {
|
||||
case cap(b) > n:
|
||||
atomic.AddUint32(&p.less, 1)
|
||||
return b[:n]
|
||||
if cap(b)-n >= n {
|
||||
atomic.AddUint32(&p.half, 1)
|
||||
select {
|
||||
case pool <- b:
|
||||
default:
|
||||
}
|
||||
return make([]byte, n)
|
||||
} else {
|
||||
atomic.AddUint32(&p.less, 1)
|
||||
return b[:n]
|
||||
}
|
||||
case cap(b) == n:
|
||||
atomic.AddUint32(&p.equal, 1)
|
||||
return b[:n]
|
||||
default:
|
||||
panic("not reached")
|
||||
atomic.AddUint32(&p.greater, 1)
|
||||
}
|
||||
default:
|
||||
atomic.AddUint32(&p.miss, 1)
|
||||
@@ -79,8 +93,23 @@ func (p *BufferPool) Get(n int) []byte {
|
||||
case b := <-pool:
|
||||
switch {
|
||||
case cap(b) > n:
|
||||
atomic.AddUint32(&p.less, 1)
|
||||
return b[:n]
|
||||
if cap(b)-n >= n {
|
||||
atomic.AddUint32(&p.half, 1)
|
||||
sizeHalfPtr := &p.sizeHalf[poolNum-1]
|
||||
if atomic.AddUint32(sizeHalfPtr, 1) == 20 {
|
||||
atomic.StoreUint32(sizePtr, uint32(cap(b)/2))
|
||||
atomic.StoreUint32(sizeHalfPtr, 0)
|
||||
} else {
|
||||
select {
|
||||
case pool <- b:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return make([]byte, n)
|
||||
} else {
|
||||
atomic.AddUint32(&p.less, 1)
|
||||
return b[:n]
|
||||
}
|
||||
case cap(b) == n:
|
||||
atomic.AddUint32(&p.equal, 1)
|
||||
return b[:n]
|
||||
@@ -126,20 +155,34 @@ func (p *BufferPool) Put(b []byte) {
|
||||
|
||||
}
|
||||
|
||||
func (p *BufferPool) Close() {
|
||||
select {
|
||||
case p.close <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BufferPool) String() string {
|
||||
return fmt.Sprintf("BufferPool{B·%d Z·%v Zm·%v G·%d P·%d <·%d =·%d >·%d M·%d}",
|
||||
p.baseline0, p.size, p.sizeMiss, p.get, p.put, p.less, p.equal, p.greater, p.miss)
|
||||
return fmt.Sprintf("BufferPool{B·%d Z·%v Zm·%v Zh·%v G·%d P·%d H·%d <·%d =·%d >·%d M·%d}",
|
||||
p.baseline0, p.size, p.sizeMiss, p.sizeHalf, p.get, p.put, p.half, p.less, p.equal, p.greater, p.miss)
|
||||
}
|
||||
|
||||
func (p *BufferPool) drain() {
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
select {
|
||||
case <-p.pool[0]:
|
||||
case <-p.pool[1]:
|
||||
case <-p.pool[2]:
|
||||
case <-p.pool[3]:
|
||||
default:
|
||||
case <-ticker.C:
|
||||
for _, ch := range p.pool {
|
||||
select {
|
||||
case <-ch:
|
||||
default:
|
||||
}
|
||||
}
|
||||
case <-p.close:
|
||||
for _, ch := range p.pool {
|
||||
close(ch)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,10 +194,10 @@ func NewBufferPool(baseline int) *BufferPool {
|
||||
}
|
||||
p := &BufferPool{
|
||||
baseline0: baseline,
|
||||
baseline1: baseline * 2,
|
||||
baseline2: baseline * 4,
|
||||
baseline: [...]int{baseline / 4, baseline / 2, baseline * 2, baseline * 4},
|
||||
close: make(chan struct{}, 1),
|
||||
}
|
||||
for i, cap := range []int{6, 6, 3, 1} {
|
||||
for i, cap := range []int{2, 2, 4, 4, 2, 1} {
|
||||
p.pool[i] = make(chan []byte, cap)
|
||||
}
|
||||
go p.drain()
|
||||
1
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/util/range.go
generated
vendored
1
Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/util/range.go
generated
vendored
@@ -25,6 +25,7 @@ func BytesPrefix(prefix []byte) *Range {
|
||||
limit = make([]byte, i+1)
|
||||
copy(limit, prefix)
|
||||
limit[i] = c + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
return &Range{prefix, limit}
|
||||
|
||||
@@ -4,4 +4,20 @@
|
||||
|
||||
package auto_test
|
||||
|
||||
// Empty test file to generate 0% coverage rather than no coverage
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/auto"
|
||||
)
|
||||
|
||||
func TestAssets(t *testing.T) {
|
||||
assets := auto.Assets()
|
||||
idx, ok := assets["index.html"]
|
||||
if !ok {
|
||||
t.Fatal("No index.html in compiled in assets")
|
||||
}
|
||||
if !bytes.Contains(idx, []byte("<html")) {
|
||||
t.Fatal("No html in index.html")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package auto contains auto generated files for web assets.
|
||||
package auto
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,11 +11,6 @@ type recv struct {
|
||||
src net.Addr
|
||||
}
|
||||
|
||||
type dst struct {
|
||||
intf string
|
||||
conn *net.UDPConn
|
||||
}
|
||||
|
||||
type Interface interface {
|
||||
Send(data []byte)
|
||||
Recv() ([]byte, net.Addr)
|
||||
|
||||
@@ -9,7 +9,6 @@ import "net"
|
||||
type Broadcast struct {
|
||||
conn *net.UDPConn
|
||||
port int
|
||||
conns []dst
|
||||
inbox chan []byte
|
||||
outbox chan recv
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import "net"
|
||||
type Multicast struct {
|
||||
conn *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
conns []dst
|
||||
inbox chan []byte
|
||||
outbox chan recv
|
||||
}
|
||||
|
||||
14
build.go
14
build.go
@@ -163,7 +163,7 @@ func setup() {
|
||||
}
|
||||
|
||||
func test(pkg string) {
|
||||
runPrint("godep", "go", "test", pkg)
|
||||
runPrint("godep", "go", "test", "-short", "-timeout", "10s", pkg)
|
||||
}
|
||||
|
||||
func install(pkg string) {
|
||||
@@ -243,20 +243,20 @@ func xdr() {
|
||||
}
|
||||
|
||||
func translate() {
|
||||
os.Chdir("gui")
|
||||
runPipe("lang-en-new.json", "go", "run", "../cmd/translate/main.go", "lang-en.json", "index.html")
|
||||
os.Chdir("gui/lang")
|
||||
runPipe("lang-en-new.json", "go", "run", "../../cmd/translate/main.go", "lang-en.json", "../index.html")
|
||||
os.Remove("lang-en.json")
|
||||
err := os.Rename("lang-en-new.json", "lang-en.json")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
os.Chdir("..")
|
||||
os.Chdir("../..")
|
||||
}
|
||||
|
||||
func transifex() {
|
||||
os.Chdir("gui")
|
||||
runPrint("go", "run", "../cmd/transifexdl/main.go")
|
||||
os.Chdir("..")
|
||||
os.Chdir("gui/lang")
|
||||
runPrint("go", "run", "../../cmd/transifexdl/main.go")
|
||||
os.Chdir("../..")
|
||||
assets()
|
||||
}
|
||||
|
||||
|
||||
12
build.sh
12
build.sh
@@ -12,9 +12,9 @@ case "${1:-default}" in
|
||||
;;
|
||||
|
||||
test)
|
||||
ulimit -t 60 || true
|
||||
ulimit -d 512000 || true
|
||||
ulimit -m 512000 || true
|
||||
ulimit -t 60 &>/dev/null || true
|
||||
ulimit -d 512000 &>/dev/null || true
|
||||
ulimit -m 512000 &>/dev/null || true
|
||||
|
||||
go run build.go "$1"
|
||||
;;
|
||||
@@ -68,9 +68,9 @@ case "${1:-default}" in
|
||||
;;
|
||||
|
||||
test-cov)
|
||||
ulimit -t 60 || true
|
||||
ulimit -d 512000 || true
|
||||
ulimit -m 512000 || true
|
||||
ulimit -t 60 &>/dev/null || true
|
||||
ulimit -d 512000 &>/dev/null || true
|
||||
ulimit -m 512000 &>/dev/null || true
|
||||
|
||||
go get github.com/axw/gocov/gocov
|
||||
go get github.com/AlekSi/gocov-xml
|
||||
|
||||
@@ -14,7 +14,16 @@ no-docs-typos() {
|
||||
grep -v f1120d7aa936c0658429edef0037792520b46334
|
||||
}
|
||||
|
||||
for email in $(missing-contribs) ; do
|
||||
git log --author="$email" --format="%H %ae %s" | no-docs-typos
|
||||
done
|
||||
print-missing-contribs() {
|
||||
for email in $(missing-contribs) ; do
|
||||
git log --author="$email" --format="%H %ae %s" | no-docs-typos
|
||||
done
|
||||
}
|
||||
|
||||
print-missing-copyright() {
|
||||
find . -name \*.go | xargs grep -L 'Copyright (C)' | grep -v Godeps
|
||||
}
|
||||
|
||||
print-missing-contribs
|
||||
print-missing-copyright
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io"
|
||||
"os"
|
||||
@@ -23,27 +23,27 @@ var tpl = template.Must(template.New("assets").Parse(`package auto
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/hex"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
var Assets = make(map[string][]byte)
|
||||
|
||||
func init() {
|
||||
func Assets() map[string][]byte {
|
||||
var assets = make(map[string][]byte, {{.assets | len}})
|
||||
var bs []byte
|
||||
var gr *gzip.Reader
|
||||
{{range $asset := .assets}}
|
||||
bs, _ = hex.DecodeString("{{$asset.HexData}}")
|
||||
bs, _ = base64.StdEncoding.DecodeString("{{$asset.Data}}")
|
||||
gr, _ = gzip.NewReader(bytes.NewBuffer(bs))
|
||||
bs, _ = ioutil.ReadAll(gr)
|
||||
Assets["{{$asset.Name}}"] = bs
|
||||
assets["{{$asset.Name}}"] = bs
|
||||
{{end}}
|
||||
return assets
|
||||
}
|
||||
`))
|
||||
|
||||
type asset struct {
|
||||
Name string
|
||||
HexData string
|
||||
Name string
|
||||
Data string
|
||||
}
|
||||
|
||||
var assets []asset
|
||||
@@ -69,8 +69,8 @@ func walkerFor(basePath string) filepath.WalkFunc {
|
||||
|
||||
name, _ = filepath.Rel(basePath, name)
|
||||
assets = append(assets, asset{
|
||||
Name: filepath.ToSlash(name),
|
||||
HexData: fmt.Sprintf("%x", buf.Bytes()),
|
||||
Name: filepath.ToSlash(name),
|
||||
Data: base64.StdEncoding.EncodeToString(buf.Bytes()),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -45,16 +41,10 @@ var (
|
||||
configInSync = true
|
||||
guiErrors = []guiError{}
|
||||
guiErrorsMut sync.Mutex
|
||||
static func(http.ResponseWriter, *http.Request, *log.Logger)
|
||||
apiKey string
|
||||
modt = time.Now().UTC().Format(http.TimeFormat)
|
||||
eventSub *events.BufferedSubscription
|
||||
)
|
||||
|
||||
const (
|
||||
unchangedPassword = "--password-unchanged--"
|
||||
)
|
||||
|
||||
func init() {
|
||||
l.AddHandler(logger.LevelWarn, showGuiError)
|
||||
sub := events.Default.Subscribe(events.AllEvents)
|
||||
@@ -62,36 +52,28 @@ func init() {
|
||||
}
|
||||
|
||||
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
|
||||
var listener net.Listener
|
||||
var err error
|
||||
if cfg.UseTLS {
|
||||
cert, err := loadCert(confDir, "https-")
|
||||
if err != nil {
|
||||
l.Infoln("Loading HTTPS certificate:", err)
|
||||
l.Infoln("Creating new HTTPS certificate")
|
||||
newCertificate(confDir, "https-")
|
||||
cert, err = loadCert(confDir, "https-")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ServerName: "syncthing",
|
||||
}
|
||||
listener, err = tls.Listen("tcp", cfg.Address, tlsCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
listener, err = net.Listen("tcp", cfg.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert, err := loadCert(confDir, "https-")
|
||||
if err != nil {
|
||||
l.Infoln("Loading HTTPS certificate:", err)
|
||||
l.Infoln("Creating new HTTPS certificate")
|
||||
newCertificate(confDir, "https-")
|
||||
cert, err = loadCert(confDir, "https-")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
ServerName: "syncthing",
|
||||
}
|
||||
|
||||
apiKey = cfg.APIKey
|
||||
loadCsrfTokens()
|
||||
rawListener, err := net.Listen("tcp", cfg.Address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listener := &DowngradingListener{rawListener, tlsCfg}
|
||||
|
||||
// The GET handlers
|
||||
getRestMux := http.NewServeMux()
|
||||
@@ -143,14 +125,19 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
||||
|
||||
// Wrap everything in CSRF protection. The /rest prefix should be
|
||||
// protected, other requests will grant cookies.
|
||||
handler := csrfMiddleware("/rest", mux)
|
||||
handler := csrfMiddleware("/rest", cfg.APIKey, mux)
|
||||
|
||||
// Add our version as a header to responses
|
||||
handler = withVersionMiddleware(handler)
|
||||
|
||||
// Wrap everything in basic auth, if user/password is set.
|
||||
if len(cfg.User) > 0 {
|
||||
handler = basicAuthMiddleware(cfg.User, cfg.Password, handler)
|
||||
if len(cfg.User) > 0 && len(cfg.Password) > 0 {
|
||||
handler = basicAuthAndSessionMiddleware(cfg, handler)
|
||||
}
|
||||
|
||||
// Redirect to HTTPS if we are supposed to
|
||||
if cfg.UseTLS {
|
||||
handler = redirectToHTTPSMiddleware(handler)
|
||||
}
|
||||
|
||||
go http.Serve(listener, handler)
|
||||
@@ -170,6 +157,23 @@ func getPostHandler(get, post http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func redirectToHTTPSMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Add a generous access-control-allow-origin header since we may be
|
||||
// redirecting REST requests over protocols
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
|
||||
if r.TLS == nil {
|
||||
// Redirect HTTP requests to HTTPS
|
||||
r.URL.Host = r.Host
|
||||
r.URL.Scheme = "https"
|
||||
http.Redirect(w, r, r.URL.String(), http.StatusFound)
|
||||
} else {
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func noCacheMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
@@ -257,14 +261,14 @@ func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
func restPostOverride(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var repo = qs.Get("repo")
|
||||
m.Override(repo)
|
||||
go m.Override(repo)
|
||||
}
|
||||
|
||||
func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var repo = qs.Get("repo")
|
||||
|
||||
files := m.NeedFilesRepo(repo)
|
||||
files := m.NeedFilesRepoLimited(repo, 100, 2500) // max 100 files or 2500 blocks
|
||||
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(files)
|
||||
@@ -283,12 +287,8 @@ func restGetNodeStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func restGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
encCfg := cfg
|
||||
if encCfg.GUI.Password != "" {
|
||||
encCfg.GUI.Password = unchangedPassword
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(encCfg)
|
||||
json.NewEncoder(w).Encode(cfg)
|
||||
}
|
||||
|
||||
func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -299,18 +299,16 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
} else {
|
||||
if newCfg.GUI.Password == "" {
|
||||
// Leave it empty
|
||||
} else if newCfg.GUI.Password == unchangedPassword {
|
||||
newCfg.GUI.Password = cfg.GUI.Password
|
||||
} else {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
|
||||
if err != nil {
|
||||
l.Warnln("bcrypting password:", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
} else {
|
||||
newCfg.GUI.Password = string(hash)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,8 +361,9 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Activate and save
|
||||
|
||||
newCfg.Location = cfg.Location
|
||||
newCfg.Save()
|
||||
cfg = newCfg
|
||||
saveConfig()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,7 +548,9 @@ func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
restPostRestart(w, r)
|
||||
flushResponse(`{"ok": "restarting"}`, w)
|
||||
l.Infoln("Upgrading")
|
||||
stop <- exitUpgrading
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,57 +602,9 @@ func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Reques
|
||||
json.NewEncoder(w).Encode(comp)
|
||||
}
|
||||
|
||||
func basicAuthMiddleware(username string, passhash string, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if validAPIKey(r.Header.Get("X-API-Key")) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
error := func() {
|
||||
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
|
||||
http.Error(w, "Not Authorized", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
hdr := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(hdr, "Basic ") {
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
hdr = hdr[6:]
|
||||
bs, err := base64.StdEncoding.DecodeString(hdr)
|
||||
if err != nil {
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
fields := bytes.SplitN(bs, []byte(":"), 2)
|
||||
if len(fields) != 2 {
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
if string(fields[0]) != username {
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(passhash), fields[1]); err != nil {
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func validAPIKey(k string) bool {
|
||||
return len(apiKey) > 0 && k == apiKey
|
||||
}
|
||||
|
||||
func embeddedStatic(assetDir string) http.Handler {
|
||||
assets := auto.Assets()
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
file := r.URL.Path
|
||||
|
||||
@@ -672,13 +625,13 @@ func embeddedStatic(assetDir string) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
bs, ok := auto.Assets[file]
|
||||
bs, ok := assets[file]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
mtype := mime.TypeByExtension(filepath.Ext(r.URL.Path))
|
||||
mtype := mimeTypeForFile(file)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
@@ -688,3 +641,28 @@ func embeddedStatic(assetDir string) http.Handler {
|
||||
w.Write(bs)
|
||||
})
|
||||
}
|
||||
|
||||
func mimeTypeForFile(file string) string {
|
||||
// We use a built in table of the common types since the system
|
||||
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||
// to the system.
|
||||
ext := filepath.Ext(file)
|
||||
switch ext {
|
||||
case ".htm", ".html":
|
||||
return "text/html"
|
||||
case ".css":
|
||||
return "text/css"
|
||||
case ".js":
|
||||
return "application/javascript"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".ttf":
|
||||
return "application/x-font-ttf"
|
||||
case ".woff":
|
||||
return "application/x-font-woff"
|
||||
default:
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
}
|
||||
|
||||
90
cmd/syncthing/gui_auth.go
Executable file
90
cmd/syncthing/gui_auth.go
Executable file
@@ -0,0 +1,90 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.google.com/p/go.crypto/bcrypt"
|
||||
"github.com/syncthing/syncthing/config"
|
||||
)
|
||||
|
||||
var (
|
||||
sessions = make(map[string]bool)
|
||||
sessionsMut sync.Mutex
|
||||
)
|
||||
|
||||
func basicAuthAndSessionMiddleware(cfg config.GUIConfiguration, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if cfg.APIKey != "" && r.Header.Get("X-API-Key") == cfg.APIKey {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
cookie, err := r.Cookie("sessionid")
|
||||
if err == nil && cookie != nil {
|
||||
sessionsMut.Lock()
|
||||
_, ok := sessions[cookie.Value]
|
||||
sessionsMut.Unlock()
|
||||
if ok {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
error := func() {
|
||||
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
|
||||
http.Error(w, "Not Authorized", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
hdr := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(hdr, "Basic ") {
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
hdr = hdr[6:]
|
||||
bs, err := base64.StdEncoding.DecodeString(hdr)
|
||||
if err != nil {
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
fields := bytes.SplitN(bs, []byte(":"), 2)
|
||||
if len(fields) != 2 {
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
if string(fields[0]) != cfg.User {
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(cfg.Password), fields[1]); err != nil {
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
sessionid := randomString(32)
|
||||
sessionsMut.Lock()
|
||||
sessions[sessionid] = true
|
||||
sessionsMut.Unlock()
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "sessionid",
|
||||
Value: sessionid,
|
||||
MaxAge: 0,
|
||||
})
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -21,10 +25,11 @@ var csrfMut sync.Mutex
|
||||
// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
|
||||
// the request with 403. For / and /index.html, set a new CSRF cookie if none
|
||||
// is currently set.
|
||||
func csrfMiddleware(prefix string, next http.Handler) http.Handler {
|
||||
func csrfMiddleware(prefix, apiKey string, next http.Handler) http.Handler {
|
||||
loadCsrfTokens()
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Allow requests carrying a valid API key
|
||||
if validAPIKey(r.Header.Get("X-API-Key")) {
|
||||
if apiKey != "" && r.Header.Get("X-API-Key") == apiKey {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -72,13 +77,7 @@ func validCsrfToken(token string) bool {
|
||||
}
|
||||
|
||||
func newCsrfToken() string {
|
||||
bs := make([]byte, 30)
|
||||
_, err := rand.Reader.Read(bs)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
|
||||
token := base64.StdEncoding.EncodeToString(bs)
|
||||
token := randomString(30)
|
||||
|
||||
csrfMut.Lock()
|
||||
csrfTokens = append(csrfTokens, token)
|
||||
@@ -130,3 +129,13 @@ func loadCsrfTokens() {
|
||||
csrfTokens = append(csrfTokens, s.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func randomString(len int) string {
|
||||
bs := make([]byte, len)
|
||||
_, err := rand.Reader.Read(bs)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(bs)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
if os.Getenv("STHEAPPROFILE") != "" {
|
||||
if innerProcess && os.Getenv("STHEAPPROFILE") != "" {
|
||||
l.Debugln("Starting heap profiling")
|
||||
go saveHeapProfiles()
|
||||
}
|
||||
}
|
||||
|
||||
24
cmd/syncthing/limitedreader.go
Normal file
24
cmd/syncthing/limitedreader.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/juju/ratelimit"
|
||||
)
|
||||
|
||||
type limitedReader struct {
|
||||
r io.Reader
|
||||
bucket *ratelimit.Bucket
|
||||
}
|
||||
|
||||
func (r *limitedReader) Read(buf []byte) (int, error) {
|
||||
n, err := r.r.Read(buf)
|
||||
if r.bucket != nil {
|
||||
r.bucket.Wait(int64(n))
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
@@ -34,11 +33,11 @@ import (
|
||||
"github.com/syncthing/syncthing/files"
|
||||
"github.com/syncthing/syncthing/logger"
|
||||
"github.com/syncthing/syncthing/model"
|
||||
"github.com/syncthing/syncthing/osutil"
|
||||
"github.com/syncthing/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/upgrade"
|
||||
"github.com/syncthing/syncthing/upnp"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -52,7 +51,16 @@ var (
|
||||
GoArchExtra string // "", "v5", "v6", "v7"
|
||||
)
|
||||
|
||||
const (
|
||||
exitSuccess = 0
|
||||
exitError = 1
|
||||
exitNoUpgradeAvailable = 2
|
||||
exitRestarting = 3
|
||||
exitUpgrading = 4
|
||||
)
|
||||
|
||||
var l = logger.DefaultLogger
|
||||
var innerProcess = os.Getenv("STNORESTART") != ""
|
||||
|
||||
func init() {
|
||||
if Version != "unknown-dev" {
|
||||
@@ -75,17 +83,16 @@ func init() {
|
||||
}
|
||||
|
||||
var (
|
||||
cfg config.Configuration
|
||||
myID protocol.NodeID
|
||||
confDir string
|
||||
logFlags int = log.Ltime
|
||||
rateBucket *ratelimit.Bucket
|
||||
stop = make(chan bool)
|
||||
discoverer *discover.Discoverer
|
||||
lockConn *net.TCPListener
|
||||
lockPort int
|
||||
externalPort int
|
||||
cert tls.Certificate
|
||||
cfg config.Configuration
|
||||
myID protocol.NodeID
|
||||
confDir string
|
||||
logFlags int = log.Ltime
|
||||
writeRateLimit *ratelimit.Bucket
|
||||
readRateLimit *ratelimit.Bucket
|
||||
stop = make(chan int)
|
||||
discoverer *discover.Discoverer
|
||||
externalPort int
|
||||
cert tls.Certificate
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -152,16 +159,20 @@ func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// Command line options
|
||||
var (
|
||||
reset bool
|
||||
showVersion bool
|
||||
doUpgrade bool
|
||||
doUpgradeCheck bool
|
||||
noBrowser bool
|
||||
generateDir string
|
||||
guiAddress string
|
||||
guiAuthentication string
|
||||
guiAPIKey string
|
||||
)
|
||||
|
||||
func main() {
|
||||
var reset bool
|
||||
var showVersion bool
|
||||
var doUpgrade bool
|
||||
var doUpgradeCheck bool
|
||||
var noBrowser bool
|
||||
var generateDir string
|
||||
var guiAddress string
|
||||
var guiAuthentication string
|
||||
var guiAPIKey string
|
||||
flag.StringVar(&confDir, "home", getDefaultConfDir(), "Set configuration directory")
|
||||
flag.BoolVar(&reset, "reset", false, "Prepare to resync from cluster")
|
||||
flag.BoolVar(&showVersion, "version", false, "Show version")
|
||||
@@ -216,7 +227,7 @@ func main() {
|
||||
|
||||
if upgrade.CompareVersions(rel.Tag, Version) <= 0 {
|
||||
l.Infof("No upgrade available (current %q >= latest %q).", Version, rel.Tag)
|
||||
os.Exit(2)
|
||||
os.Exit(exitNoUpgradeAvailable)
|
||||
}
|
||||
|
||||
l.Infof("Upgrade available (current %q < latest %q)", Version, rel.Tag)
|
||||
@@ -233,12 +244,27 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
lockPort, err = getLockPort()
|
||||
if err != nil {
|
||||
l.Fatalln("Opening lock port:", err)
|
||||
if reset {
|
||||
resetRepositories()
|
||||
return
|
||||
}
|
||||
|
||||
confDir = expandTilde(confDir)
|
||||
|
||||
if info, err := os.Stat(confDir); err == nil && !info.IsDir() {
|
||||
l.Fatalln("Config directory", confDir, "is not a directory")
|
||||
}
|
||||
|
||||
if os.Getenv("STNORESTART") != "" {
|
||||
syncthingMain()
|
||||
} else {
|
||||
monitorMain()
|
||||
}
|
||||
}
|
||||
|
||||
func syncthingMain() {
|
||||
var err error
|
||||
|
||||
if len(os.Getenv("GOGC")) == 0 {
|
||||
debug.SetGCPercent(25)
|
||||
}
|
||||
@@ -247,11 +273,9 @@ func main() {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
}
|
||||
|
||||
confDir = expandTilde(confDir)
|
||||
|
||||
events.Default.Log(events.Starting, map[string]string{"home": confDir})
|
||||
|
||||
if _, err := os.Stat(confDir); err != nil && confDir == getDefaultConfDir() {
|
||||
if _, err = os.Stat(confDir); err != nil && confDir == getDefaultConfDir() {
|
||||
// We are supposed to use the default configuration directory. It
|
||||
// doesn't exist. In the past our default has been ~/.syncthing, so if
|
||||
// that directory exists we move it to the new default location and
|
||||
@@ -291,21 +315,14 @@ func main() {
|
||||
// Prepare to be able to save configuration
|
||||
|
||||
cfgFile := filepath.Join(confDir, "config.xml")
|
||||
go saveConfigLoop(cfgFile)
|
||||
|
||||
var myName string
|
||||
|
||||
// Load the configuration file, if it exists.
|
||||
// If it does not, create a template.
|
||||
|
||||
cf, err := os.Open(cfgFile)
|
||||
cfg, err = config.Load(cfgFile, myID)
|
||||
if err == nil {
|
||||
// Read config.xml
|
||||
cfg, err = config.Load(cf, myID)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
cf.Close()
|
||||
myCfg := cfg.GetNodeConfiguration(myID)
|
||||
if myCfg == nil || myCfg.Name == "" {
|
||||
myName, _ = os.Hostname()
|
||||
@@ -317,7 +334,7 @@ func main() {
|
||||
myName, _ = os.Hostname()
|
||||
defaultRepo := filepath.Join(getHomeDir(), "Sync")
|
||||
|
||||
cfg, err = config.Load(nil, myID)
|
||||
cfg = config.New(cfgFile, myID)
|
||||
cfg.Repositories = []config.RepositoryConfiguration{
|
||||
{
|
||||
ID: "default",
|
||||
@@ -342,19 +359,10 @@ func main() {
|
||||
l.FatalErr(err)
|
||||
cfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)}
|
||||
|
||||
saveConfig()
|
||||
cfg.Save()
|
||||
l.Infof("Edit %s to taste or use the GUI\n", cfgFile)
|
||||
}
|
||||
|
||||
if reset {
|
||||
resetRepositories()
|
||||
return
|
||||
}
|
||||
|
||||
if len(os.Getenv("STRESTART")) > 0 {
|
||||
waitForParentExit()
|
||||
}
|
||||
|
||||
if profiler := os.Getenv("STPROFILER"); len(profiler) > 0 {
|
||||
go func() {
|
||||
l.Debugln("Starting profiler on", profiler)
|
||||
@@ -379,17 +387,20 @@ func main() {
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
|
||||
// If the write rate should be limited, set up a rate limiter for it.
|
||||
// If the read or write rate should be limited, set up a rate limiter for it.
|
||||
// This will be used on connections created in the connect and listen routines.
|
||||
|
||||
if cfg.Options.MaxSendKbps > 0 {
|
||||
rateBucket = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps))
|
||||
writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps))
|
||||
}
|
||||
if cfg.Options.MaxRecvKbps > 0 {
|
||||
readRateLimit = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxRecvKbps), int64(5*1000*cfg.Options.MaxRecvKbps))
|
||||
}
|
||||
|
||||
// If this is the first time the user runs v0.9, archive the old indexes and config.
|
||||
archiveLegacyConfig()
|
||||
|
||||
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), nil)
|
||||
db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{CachedOpenFiles: 100})
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
|
||||
}
|
||||
@@ -420,6 +431,7 @@ nextRepo:
|
||||
// that all files have been deleted which might not be the case,
|
||||
// so mark it as invalid instead.
|
||||
if err != nil || !fi.IsDir() {
|
||||
l.Warnf("Stopping repository %q - directory missing, but has files in index", repo.ID)
|
||||
cfg.Repositories[i].Invalid = "repo directory missing"
|
||||
continue nextRepo
|
||||
}
|
||||
@@ -432,6 +444,7 @@ nextRepo:
|
||||
if err != nil {
|
||||
// If there was another error or we could not create the
|
||||
// directory, the repository is invalid.
|
||||
l.Warnf("Stopping repository %q - %v", err)
|
||||
cfg.Repositories[i].Invalid = err.Error()
|
||||
continue nextRepo
|
||||
}
|
||||
@@ -466,7 +479,7 @@ nextRepo:
|
||||
proto = "https"
|
||||
}
|
||||
|
||||
l.Infof("Starting web GUI on %s://%s:%d/", proto, hostShow, addr.Port)
|
||||
l.Infof("Starting web GUI on %s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
|
||||
err := startGUI(guiCfg, os.Getenv("STGUIASSETS"), m)
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot start GUI:", err)
|
||||
@@ -481,7 +494,13 @@ nextRepo:
|
||||
// start needing a bunch of files which are nowhere to be found. This
|
||||
// needs to be changed when we correctly do persistent indexes.
|
||||
for _, repoCfg := range cfg.Repositories {
|
||||
if repoCfg.Invalid != "" {
|
||||
continue
|
||||
}
|
||||
for _, node := range repoCfg.NodeIDs() {
|
||||
if node == myID {
|
||||
continue
|
||||
}
|
||||
m.Index(node, repoCfg.ID, nil)
|
||||
}
|
||||
}
|
||||
@@ -516,6 +535,14 @@ nextRepo:
|
||||
}
|
||||
}
|
||||
|
||||
// The default port we announce, possibly modified by setupUPnP next.
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", cfg.Options.ListenAddress[0])
|
||||
if err != nil {
|
||||
l.Fatalln("Bad listen address:", err)
|
||||
}
|
||||
externalPort = addr.Port
|
||||
|
||||
// UPnP
|
||||
|
||||
if cfg.Options.UPnPEnabled {
|
||||
@@ -572,14 +599,17 @@ nextRepo:
|
||||
}()
|
||||
}
|
||||
|
||||
go standbyMonitor()
|
||||
if cfg.Options.RestartOnWakeup {
|
||||
go standbyMonitor()
|
||||
}
|
||||
|
||||
events.Default.Log(events.StartupComplete, nil)
|
||||
go generateEvents()
|
||||
|
||||
<-stop
|
||||
code := <-stop
|
||||
|
||||
l.Okln("Exiting")
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func generateEvents() {
|
||||
@@ -589,25 +619,6 @@ func generateEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
func waitForParentExit() {
|
||||
l.Infoln("Waiting for parent to exit...")
|
||||
lockPortStr := os.Getenv("STRESTART")
|
||||
lockPort, err := strconv.Atoi(lockPortStr)
|
||||
if err != nil {
|
||||
l.Warnln("Invalid lock port %q: %v", lockPortStr, err)
|
||||
}
|
||||
// Wait for the listen address to become free, indicating that the parent has exited.
|
||||
for {
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", lockPort))
|
||||
if err == nil {
|
||||
ln.Close()
|
||||
break
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
l.Infoln("Continuing")
|
||||
}
|
||||
|
||||
func setupUPnP() {
|
||||
if len(cfg.Options.ListenAddress) == 1 {
|
||||
_, portStr, err := net.SplitHostPort(cfg.Options.ListenAddress[0])
|
||||
@@ -663,13 +674,16 @@ func renewUPnP(port int) {
|
||||
}
|
||||
|
||||
// Just renew the same port that we already have
|
||||
err = igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", cfg.Options.UPnPLease*60)
|
||||
if err == nil {
|
||||
l.Infoln("Renewed UPnP port mapping - external port", externalPort)
|
||||
continue
|
||||
if externalPort != 0 {
|
||||
err = igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", cfg.Options.UPnPLease*60)
|
||||
if err == nil {
|
||||
l.Infoln("Renewed UPnP port mapping - external port", externalPort)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Something strange has happened. Perhaps the gateway has changed?
|
||||
// Something strange has happened. We didn't have an external port before?
|
||||
// Or perhaps the gateway has changed?
|
||||
// Retry the same port sequence from the beginning.
|
||||
r := setupExternalPort(igd, port)
|
||||
if r != 0 {
|
||||
@@ -725,7 +739,7 @@ func archiveLegacyConfig() {
|
||||
l.Warnf("Cannot archive config:", err)
|
||||
return
|
||||
}
|
||||
defer src.Close()
|
||||
defer dst.Close()
|
||||
|
||||
l.Infoln("Archiving config.xml")
|
||||
io.Copy(dst, src)
|
||||
@@ -734,74 +748,12 @@ func archiveLegacyConfig() {
|
||||
|
||||
func restart() {
|
||||
l.Infoln("Restarting")
|
||||
if os.Getenv("SMF_FMRI") != "" || os.Getenv("STNORESTART") != "" {
|
||||
// Solaris SMF
|
||||
l.Infoln("Service manager detected; exit instead of restart")
|
||||
stop <- true
|
||||
return
|
||||
}
|
||||
|
||||
env := os.Environ()
|
||||
newEnv := make([]string, 0, len(env))
|
||||
for _, s := range env {
|
||||
if !strings.HasPrefix(s, "STRESTART=") {
|
||||
newEnv = append(newEnv, s)
|
||||
}
|
||||
}
|
||||
newEnv = append(newEnv, fmt.Sprintf("STRESTART=%d", lockPort))
|
||||
|
||||
pgm, err := exec.LookPath(os.Args[0])
|
||||
if err != nil {
|
||||
l.Warnln("Cannot restart:", err)
|
||||
return
|
||||
}
|
||||
proc, err := os.StartProcess(pgm, os.Args, &os.ProcAttr{
|
||||
Env: newEnv,
|
||||
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
|
||||
})
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
proc.Release()
|
||||
stop <- true
|
||||
stop <- exitRestarting
|
||||
}
|
||||
|
||||
func shutdown() {
|
||||
stop <- true
|
||||
}
|
||||
|
||||
var saveConfigCh = make(chan struct{})
|
||||
|
||||
func saveConfigLoop(cfgFile string) {
|
||||
for _ = range saveConfigCh {
|
||||
fd, err := os.Create(cfgFile + ".tmp")
|
||||
if err != nil {
|
||||
l.Warnln("Saving config:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = config.Save(fd, cfg)
|
||||
if err != nil {
|
||||
l.Warnln("Saving config:", err)
|
||||
fd.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
err = fd.Close()
|
||||
if err != nil {
|
||||
l.Warnln("Saving config:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
err = osutil.Rename(cfgFile+".tmp", cfgFile)
|
||||
if err != nil {
|
||||
l.Warnln("Saving config:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveConfig() {
|
||||
saveConfigCh <- struct{}{}
|
||||
l.Infoln("Shutting down")
|
||||
stop <- exitSuccess
|
||||
}
|
||||
|
||||
func listenConnect(myID protocol.NodeID, m *model.Model, tlsCfg *tls.Config) {
|
||||
@@ -857,15 +809,20 @@ next:
|
||||
continue next
|
||||
}
|
||||
|
||||
// If rate limiting is set, we wrap the write side of the
|
||||
// connection in a limiter.
|
||||
// If rate limiting is set, we wrap the connection in a
|
||||
// limiter.
|
||||
var wr io.Writer = conn
|
||||
if rateBucket != nil {
|
||||
wr = &limitedWriter{conn, rateBucket}
|
||||
if writeRateLimit != nil {
|
||||
wr = &limitedWriter{conn, writeRateLimit}
|
||||
}
|
||||
|
||||
var rd io.Reader = conn
|
||||
if readRateLimit != nil {
|
||||
rd = &limitedReader{conn, readRateLimit}
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
|
||||
protoConn := protocol.NewConnection(remoteID, conn, wr, m, name, nodeCfg.Compression)
|
||||
protoConn := protocol.NewConnection(remoteID, rd, wr, m, name, nodeCfg.Compression)
|
||||
|
||||
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
||||
if debugNet {
|
||||
@@ -1121,16 +1078,6 @@ func getFreePort(host string, ports ...int) (int, error) {
|
||||
return addr.Port, nil
|
||||
}
|
||||
|
||||
func getLockPort() (int, error) {
|
||||
var err error
|
||||
lockConn, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.IP{127, 0, 0, 1}})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
addr := lockConn.Addr().(*net.TCPAddr)
|
||||
return addr.Port, nil
|
||||
}
|
||||
|
||||
func overrideGUIConfig(originalCfg config.GUIConfiguration, address, authentication, apikey string) config.GUIConfiguration {
|
||||
// Make a copy of the config
|
||||
cfg := originalCfg
|
||||
@@ -1181,12 +1128,20 @@ func overrideGUIConfig(originalCfg config.GUIConfiguration, address, authenticat
|
||||
}
|
||||
|
||||
func standbyMonitor() {
|
||||
restartDelay := time.Duration(60 * time.Second)
|
||||
now := time.Now()
|
||||
for {
|
||||
time.Sleep(10 * time.Second)
|
||||
if time.Since(now) > 2*time.Minute {
|
||||
l.Infoln("Paused state detected, possibly woke up from standby.")
|
||||
l.Infoln("Paused state detected, possibly woke up from standby. Restarting in", restartDelay)
|
||||
|
||||
// We most likely just woke from standby. If we restart
|
||||
// immediately chances are we won't have networking ready. Give
|
||||
// things a moment to stabilize.
|
||||
time.Sleep(restartDelay)
|
||||
|
||||
restart()
|
||||
return
|
||||
}
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build solaris
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build freebsd
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
182
cmd/syncthing/monitor.go
Normal file
182
cmd/syncthing/monitor.go
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
stdoutFirstLines []string // The first 10 lines of stdout
|
||||
stdoutLastLines []string // The last 50 lines of stdout
|
||||
stdoutMut sync.Mutex
|
||||
)
|
||||
|
||||
const (
|
||||
countRestarts = 5
|
||||
loopThreshold = 15 * time.Second
|
||||
)
|
||||
|
||||
func monitorMain() {
|
||||
os.Setenv("STNORESTART", "yes")
|
||||
l.SetPrefix("[monitor] ")
|
||||
|
||||
args := os.Args
|
||||
var restarts [countRestarts]time.Time
|
||||
|
||||
sign := make(chan os.Signal, 1)
|
||||
sigTerm := syscall.Signal(0xf)
|
||||
signal.Notify(sign, os.Interrupt, sigTerm, os.Kill)
|
||||
|
||||
for {
|
||||
if t := time.Since(restarts[0]); t < loopThreshold {
|
||||
l.Warnf("%d restarts in %v; not retrying further", countRestarts, t)
|
||||
os.Exit(exitError)
|
||||
}
|
||||
|
||||
copy(restarts[0:], restarts[1:])
|
||||
restarts[len(restarts)-1] = time.Now()
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
|
||||
l.Infoln("Starting syncthing")
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
|
||||
stdoutMut.Lock()
|
||||
stdoutFirstLines = make([]string, 0, 10)
|
||||
stdoutLastLines = make([]string, 0, 50)
|
||||
stdoutMut.Unlock()
|
||||
|
||||
go copyStderr(stderr)
|
||||
go copyStdout(stdout)
|
||||
|
||||
exit := make(chan error)
|
||||
|
||||
go func() {
|
||||
exit <- cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case s := <-sign:
|
||||
l.Infof("Signal %d received; exiting", s)
|
||||
cmd.Process.Kill()
|
||||
<-exit
|
||||
return
|
||||
|
||||
case err = <-exit:
|
||||
if err == nil {
|
||||
// Successfull exit indicates an intentional shutdown
|
||||
return
|
||||
} else if exiterr, ok := err.(*exec.ExitError); ok {
|
||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||
switch status.ExitStatus() {
|
||||
case exitUpgrading:
|
||||
// Restart the monitor process to release the .old
|
||||
// binary as part of the upgrade process.
|
||||
l.Infoln("Restarting monitor...")
|
||||
os.Setenv("STNORESTART", "")
|
||||
err := exec.Command(args[0], args[1:]...).Start()
|
||||
if err != nil {
|
||||
l.Warnln("restart:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
l.Infoln("Syncthing exited:", err)
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Let the next child process know that this is not the first time
|
||||
// it's starting up.
|
||||
os.Setenv("STRESTART", "yes")
|
||||
}
|
||||
}
|
||||
|
||||
func copyStderr(stderr io.ReadCloser) {
|
||||
br := bufio.NewReader(stderr)
|
||||
|
||||
var panicFd *os.File
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if panicFd == nil {
|
||||
os.Stderr.WriteString(line)
|
||||
|
||||
if strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") {
|
||||
panicFd, err = os.Create(filepath.Join(confDir, time.Now().Format("panic-20060102-150405.log")))
|
||||
if err != nil {
|
||||
l.Warnln("Create panic log:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Warnf("Panic detected, writing to \"%s\"", panicFd.Name())
|
||||
l.Warnln("Please create an issue at https://github.com/syncthing/syncthing/issues/ with the panic log attached")
|
||||
|
||||
panicFd.WriteString("Panic at " + time.Now().Format(time.RFC1123) + "\n")
|
||||
stdoutMut.Lock()
|
||||
for _, line := range stdoutFirstLines {
|
||||
panicFd.WriteString(line)
|
||||
}
|
||||
panicFd.WriteString("...\n")
|
||||
for _, line := range stdoutLastLines {
|
||||
panicFd.WriteString(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if panicFd != nil {
|
||||
panicFd.WriteString(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyStdout(stderr io.ReadCloser) {
|
||||
br := bufio.NewReader(stderr)
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stdoutMut.Lock()
|
||||
if len(stdoutFirstLines) < cap(stdoutFirstLines) {
|
||||
stdoutFirstLines = append(stdoutFirstLines, line)
|
||||
}
|
||||
if l := len(stdoutLastLines); l == cap(stdoutLastLines) {
|
||||
stdoutLastLines = stdoutLastLines[:l-1]
|
||||
}
|
||||
stdoutLastLines = append(stdoutLastLines, line)
|
||||
stdoutMut.Unlock()
|
||||
|
||||
os.Stdout.WriteString(line)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !windows
|
||||
// +build !solaris,!windows
|
||||
|
||||
package main
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
if os.Getenv("STPERFSTATS") != "" {
|
||||
if innerProcess && os.Getenv("STPERFSTATS") != "" {
|
||||
go savePerfStats(fmt.Sprintf("perfstats-%d.csv", syscall.Getpid()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
@@ -13,8 +14,10 @@ import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"math/big"
|
||||
mr "math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
@@ -73,3 +76,40 @@ func newCertificate(dir string, prefix string) {
|
||||
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
keyOut.Close()
|
||||
}
|
||||
|
||||
type DowngradingListener struct {
|
||||
net.Listener
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
type WrappedConnection struct {
|
||||
io.Reader
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (l *DowngradingListener) Accept() (net.Conn, error) {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
bs, err := br.Peek(1)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wrapper := &WrappedConnection{br, conn}
|
||||
|
||||
// 0x16 is the first byte of a TLS handshake
|
||||
if bs[0] == 0x16 {
|
||||
return tls.Server(wrapper, l.TLSConfig), nil
|
||||
}
|
||||
|
||||
return wrapper, nil
|
||||
}
|
||||
|
||||
func (c *WrappedConnection) Read(b []byte) (n int, err error) {
|
||||
return c.Reader.Read(b)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -8,20 +8,22 @@ package config
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"code.google.com/p/go.crypto/bcrypt"
|
||||
"github.com/syncthing/syncthing/events"
|
||||
"github.com/syncthing/syncthing/logger"
|
||||
"github.com/syncthing/syncthing/osutil"
|
||||
"github.com/syncthing/syncthing/protocol"
|
||||
)
|
||||
|
||||
var l = logger.DefaultLogger
|
||||
|
||||
type Configuration struct {
|
||||
Location string `xml:"-" json:"-"`
|
||||
Version int `xml:"version,attr" default:"3"`
|
||||
Repositories []RepositoryConfiguration `xml:"repository"`
|
||||
Nodes []NodeConfiguration `xml:"node"`
|
||||
@@ -117,12 +119,14 @@ type OptionsConfiguration struct {
|
||||
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" default:"[ff32::5222]:21026"`
|
||||
ParallelRequests int `xml:"parallelRequests" default:"16"`
|
||||
MaxSendKbps int `xml:"maxSendKbps"`
|
||||
MaxRecvKbps int `xml:"maxRecvKbps"`
|
||||
ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60"`
|
||||
StartBrowser bool `xml:"startBrowser" default:"true"`
|
||||
UPnPEnabled bool `xml:"upnpEnabled" default:"true"`
|
||||
UPnPLease int `xml:"upnpLeaseMinutes" default:"0"`
|
||||
UPnPRenewal int `xml:"upnpRenewalMinutes" default:"30"`
|
||||
URAccepted int `xml:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
|
||||
RestartOnWakeup bool `xml:"restartOnWakeup" default:"true"`
|
||||
|
||||
Deprecated_RescanIntervalS int `xml:"rescanIntervalS,omitempty" json:"-"`
|
||||
Deprecated_UREnabled bool `xml:"urEnabled,omitempty" json:"-"`
|
||||
@@ -227,14 +231,39 @@ func fillNilSlices(data interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Save(wr io.Writer, cfg Configuration) error {
|
||||
e := xml.NewEncoder(wr)
|
||||
e.Indent("", " ")
|
||||
err := e.Encode(cfg)
|
||||
func (cfg *Configuration) Save() error {
|
||||
fd, err := os.Create(cfg.Location + ".tmp")
|
||||
if err != nil {
|
||||
l.Warnln("Saving config:", err)
|
||||
return err
|
||||
}
|
||||
_, err = wr.Write([]byte("\n"))
|
||||
|
||||
e := xml.NewEncoder(fd)
|
||||
e.Indent("", " ")
|
||||
err = e.Encode(cfg)
|
||||
if err != nil {
|
||||
fd.Close()
|
||||
return err
|
||||
}
|
||||
_, err = fd.Write([]byte("\n"))
|
||||
|
||||
if err != nil {
|
||||
l.Warnln("Saving config:", err)
|
||||
fd.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
err = fd.Close()
|
||||
if err != nil {
|
||||
l.Warnln("Saving config:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
err = osutil.Rename(cfg.Location+".tmp", cfg.Location)
|
||||
if err != nil {
|
||||
l.Warnln("Saving config:", err)
|
||||
}
|
||||
events.Default.Log(events.ConfigSaved, cfg)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -252,18 +281,7 @@ func uniqueStrings(ss []string) []string {
|
||||
return us
|
||||
}
|
||||
|
||||
func Load(rd io.Reader, myID protocol.NodeID) (Configuration, error) {
|
||||
var cfg Configuration
|
||||
|
||||
setDefaults(&cfg)
|
||||
setDefaults(&cfg.Options)
|
||||
setDefaults(&cfg.GUI)
|
||||
|
||||
var err error
|
||||
if rd != nil {
|
||||
err = xml.NewDecoder(rd).Decode(&cfg)
|
||||
}
|
||||
|
||||
func (cfg *Configuration) prepare(myID protocol.NodeID) {
|
||||
fillNilSlices(&cfg.Options)
|
||||
|
||||
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
|
||||
@@ -312,17 +330,17 @@ func Load(rd io.Reader, myID protocol.NodeID) (Configuration, error) {
|
||||
|
||||
// Upgrade to v2 configuration if appropriate
|
||||
if cfg.Version == 1 {
|
||||
convertV1V2(&cfg)
|
||||
convertV1V2(cfg)
|
||||
}
|
||||
|
||||
// Upgrade to v3 configuration if appropriate
|
||||
if cfg.Version == 2 {
|
||||
convertV2V3(&cfg)
|
||||
convertV2V3(cfg)
|
||||
}
|
||||
|
||||
// Upgrade to v4 configuration if appropriate
|
||||
if cfg.Version == 3 {
|
||||
convertV3V4(&cfg)
|
||||
convertV3V4(cfg)
|
||||
}
|
||||
|
||||
// Hash old cleartext passwords
|
||||
@@ -368,6 +386,39 @@ func Load(rd io.Reader, myID protocol.NodeID) (Configuration, error) {
|
||||
n.Addresses = []string{"dynamic"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func New(location string, myID protocol.NodeID) Configuration {
|
||||
var cfg Configuration
|
||||
|
||||
cfg.Location = location
|
||||
|
||||
setDefaults(&cfg)
|
||||
setDefaults(&cfg.Options)
|
||||
setDefaults(&cfg.GUI)
|
||||
|
||||
cfg.prepare(myID)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func Load(location string, myID protocol.NodeID) (Configuration, error) {
|
||||
var cfg Configuration
|
||||
|
||||
cfg.Location = location
|
||||
|
||||
setDefaults(&cfg)
|
||||
setDefaults(&cfg.Options)
|
||||
setDefaults(&cfg.GUI)
|
||||
|
||||
fd, err := os.Open(location)
|
||||
if err != nil {
|
||||
return Configuration{}, err
|
||||
}
|
||||
err = xml.NewDecoder(fd).Decode(&cfg)
|
||||
fd.Close()
|
||||
|
||||
cfg.prepare(myID)
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -33,17 +31,16 @@ func TestDefaultValues(t *testing.T) {
|
||||
LocalAnnMCAddr: "[ff32::5222]:21026",
|
||||
ParallelRequests: 16,
|
||||
MaxSendKbps: 0,
|
||||
MaxRecvKbps: 0,
|
||||
ReconnectIntervalS: 60,
|
||||
StartBrowser: true,
|
||||
UPnPEnabled: true,
|
||||
UPnPLease: 0,
|
||||
UPnPRenewal: 30,
|
||||
RestartOnWakeup: true,
|
||||
}
|
||||
|
||||
cfg, err := Load(bytes.NewReader(nil), node1)
|
||||
if err != io.EOF {
|
||||
t.Error(err)
|
||||
}
|
||||
cfg := New("test", node1)
|
||||
|
||||
if !reflect.DeepEqual(cfg.Options, expected) {
|
||||
t.Errorf("Default config differs;\n E: %#v\n A: %#v", expected, cfg.Options)
|
||||
@@ -51,84 +48,8 @@ func TestDefaultValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNodeConfig(t *testing.T) {
|
||||
v1data := []byte(`
|
||||
<configuration version="1">
|
||||
<repository id="test" directory="~/Sync">
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
|
||||
<address>b</address>
|
||||
</node>
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
|
||||
<address>b</address>
|
||||
</node>
|
||||
</repository>
|
||||
<options>
|
||||
<readOnly>true</readOnly>
|
||||
<rescanIntervalS>600</rescanIntervalS>
|
||||
</options>
|
||||
</configuration>
|
||||
`)
|
||||
|
||||
v2data := []byte(`
|
||||
<configuration version="2">
|
||||
<repository id="test" directory="~/Sync" ro="true">
|
||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
|
||||
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
|
||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
|
||||
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
|
||||
</repository>
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
|
||||
<address>b</address>
|
||||
</node>
|
||||
<options>
|
||||
<rescanIntervalS>600</rescanIntervalS>
|
||||
</options>
|
||||
</configuration>
|
||||
`)
|
||||
|
||||
v3data := []byte(`
|
||||
<configuration version="3">
|
||||
<repository id="test" directory="~/Sync" ro="true" ignorePerms="false">
|
||||
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" compression="false"></node>
|
||||
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" compression="false"></node>
|
||||
</repository>
|
||||
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="true">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="true">
|
||||
<address>b</address>
|
||||
</node>
|
||||
<options>
|
||||
<rescanIntervalS>600</rescanIntervalS>
|
||||
</options>
|
||||
</configuration>`)
|
||||
|
||||
v4data := []byte(`
|
||||
<configuration version="4">
|
||||
<repository id="test" directory="~/Sync" ro="true" ignorePerms="false" rescanIntervalS="600">
|
||||
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></node>
|
||||
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></node>
|
||||
</repository>
|
||||
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="true">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="true">
|
||||
<address>b</address>
|
||||
</node>
|
||||
</configuration>`)
|
||||
|
||||
for i, data := range [][]byte{v1data, v2data, v3data, v4data} {
|
||||
cfg, err := Load(bytes.NewReader(data), node1)
|
||||
for i, ver := range []string{"v1", "v2", "v3", "v4"} {
|
||||
cfg, err := Load("testdata/"+ver+".xml", node1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -181,14 +102,7 @@ func TestNodeConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNoListenAddress(t *testing.T) {
|
||||
data := []byte(`<configuration version="1">
|
||||
<options>
|
||||
<listenAddress></listenAddress>
|
||||
</options>
|
||||
</configuration>
|
||||
`)
|
||||
|
||||
cfg, err := Load(bytes.NewReader(data), node1)
|
||||
cfg, err := Load("testdata/nolistenaddress.xml", node1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -200,26 +114,6 @@ func TestNoListenAddress(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOverriddenValues(t *testing.T) {
|
||||
data := []byte(`<configuration version="2">
|
||||
<options>
|
||||
<listenAddress>:23000</listenAddress>
|
||||
<allowDelete>false</allowDelete>
|
||||
<globalAnnounceServer>syncthing.nym.se:22026</globalAnnounceServer>
|
||||
<globalAnnounceEnabled>false</globalAnnounceEnabled>
|
||||
<localAnnounceEnabled>false</localAnnounceEnabled>
|
||||
<localAnnouncePort>42123</localAnnouncePort>
|
||||
<localAnnounceMCAddr>quux:3232</localAnnounceMCAddr>
|
||||
<parallelRequests>32</parallelRequests>
|
||||
<maxSendKbps>1234</maxSendKbps>
|
||||
<reconnectionIntervalS>6000</reconnectionIntervalS>
|
||||
<startBrowser>false</startBrowser>
|
||||
<upnpEnabled>false</upnpEnabled>
|
||||
<upnpLeaseMinutes>60</upnpLeaseMinutes>
|
||||
<upnpRenewalMinutes>15</upnpRenewalMinutes>
|
||||
</options>
|
||||
</configuration>
|
||||
`)
|
||||
|
||||
expected := OptionsConfiguration{
|
||||
ListenAddress: []string{":23000"},
|
||||
GlobalAnnServer: "syncthing.nym.se:22026",
|
||||
@@ -229,14 +123,16 @@ func TestOverriddenValues(t *testing.T) {
|
||||
LocalAnnMCAddr: "quux:3232",
|
||||
ParallelRequests: 32,
|
||||
MaxSendKbps: 1234,
|
||||
MaxRecvKbps: 2341,
|
||||
ReconnectIntervalS: 6000,
|
||||
StartBrowser: false,
|
||||
UPnPEnabled: false,
|
||||
UPnPLease: 60,
|
||||
UPnPRenewal: 15,
|
||||
RestartOnWakeup: false,
|
||||
}
|
||||
|
||||
cfg, err := Load(bytes.NewReader(data), node1)
|
||||
cfg, err := Load("testdata/overridenvalues.xml", node1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -247,19 +143,6 @@ func TestOverriddenValues(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNodeAddressesDynamic(t *testing.T) {
|
||||
data := []byte(`
|
||||
<configuration version="2">
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
|
||||
<address></address>
|
||||
</node>
|
||||
<node id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
|
||||
</node>
|
||||
<node id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
|
||||
<address>dynamic</address>
|
||||
</node>
|
||||
</configuration>
|
||||
`)
|
||||
|
||||
name, _ := os.Hostname()
|
||||
expected := []NodeConfiguration{
|
||||
{
|
||||
@@ -284,7 +167,7 @@ func TestNodeAddressesDynamic(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := Load(bytes.NewReader(data), node4)
|
||||
cfg, err := Load("testdata/nodeaddressesdynamic.xml", node4)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -295,23 +178,6 @@ func TestNodeAddressesDynamic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNodeAddressesStatic(t *testing.T) {
|
||||
data := []byte(`
|
||||
<configuration version="3">
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
|
||||
<address>192.0.2.1</address>
|
||||
<address>192.0.2.2</address>
|
||||
</node>
|
||||
<node id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
|
||||
<address>192.0.2.3:6070</address>
|
||||
<address>[2001:db8::42]:4242</address>
|
||||
</node>
|
||||
<node id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
|
||||
<address>[2001:db8::44]:4444</address>
|
||||
<address>192.0.2.4:6090</address>
|
||||
</node>
|
||||
</configuration>
|
||||
`)
|
||||
|
||||
name, _ := os.Hostname()
|
||||
expected := []NodeConfiguration{
|
||||
{
|
||||
@@ -333,7 +199,7 @@ func TestNodeAddressesStatic(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := Load(bytes.NewReader(data), node4)
|
||||
cfg, err := Load("testdata/nodeaddressesstatic.xml", node4)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -344,18 +210,7 @@ func TestNodeAddressesStatic(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestVersioningConfig(t *testing.T) {
|
||||
data := []byte(`
|
||||
<configuration version="2">
|
||||
<repository id="test" directory="~/Sync" ro="true">
|
||||
<versioning type="simple">
|
||||
<param key="foo" val="bar"/>
|
||||
<param key="baz" val="quux"/>
|
||||
</versioning>
|
||||
</repository>
|
||||
</configuration>
|
||||
`)
|
||||
|
||||
cfg, err := Load(bytes.NewReader(data), node4)
|
||||
cfg, err := Load("testdata/versioningconfig.xml", node4)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -376,3 +231,67 @@ func TestVersioningConfig(t *testing.T) {
|
||||
t.Errorf("vc.Params differ;\n E: %#v\n A: %#v", expected, vc.Params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSaveLoad(t *testing.T) {
|
||||
path := "testdata/temp.xml"
|
||||
os.Remove(path)
|
||||
|
||||
exists := func(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
cfg := New(path, node1)
|
||||
|
||||
// To make the equality pass later
|
||||
cfg.XMLName.Local = "configuration"
|
||||
|
||||
if exists(path) {
|
||||
t.Error(path, "exists")
|
||||
}
|
||||
|
||||
err := cfg.Save()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if !exists(path) {
|
||||
t.Error(path, "does not exist")
|
||||
}
|
||||
|
||||
cfg2, err := Load(path, node1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cfg, cfg2) {
|
||||
t.Errorf("Configs are not equal;\n E: %#v\n A: %#v", cfg, cfg2)
|
||||
}
|
||||
|
||||
cfg.GUI.User = "test"
|
||||
cfg.Save()
|
||||
|
||||
cfg2, err = Load(path, node1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if cfg2.GUI.User != "test" || !reflect.DeepEqual(cfg, cfg2) {
|
||||
t.Errorf("Configs are not equal;\n E: %#v\n A: %#v", cfg, cfg2)
|
||||
}
|
||||
|
||||
os.Remove(path)
|
||||
}
|
||||
|
||||
func TestPrepare(t *testing.T) {
|
||||
var cfg Configuration
|
||||
|
||||
if cfg.Repositories != nil || cfg.Nodes != nil || cfg.Options.ListenAddress != nil {
|
||||
t.Error("Expected nil")
|
||||
}
|
||||
|
||||
cfg.prepare(node1)
|
||||
|
||||
if cfg.Repositories == nil || cfg.Nodes == nil || cfg.Options.ListenAddress == nil {
|
||||
t.Error("Unexpected nil")
|
||||
}
|
||||
}
|
||||
|
||||
10
config/testdata/nodeaddressesdynamic.xml
vendored
Executable file
10
config/testdata/nodeaddressesdynamic.xml
vendored
Executable file
@@ -0,0 +1,10 @@
|
||||
<configuration version="2">
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
|
||||
<address></address>
|
||||
</node>
|
||||
<node id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
|
||||
</node>
|
||||
<node id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
|
||||
<address>dynamic</address>
|
||||
</node>
|
||||
</configuration>
|
||||
14
config/testdata/nodeaddressesstatic.xml
vendored
Executable file
14
config/testdata/nodeaddressesstatic.xml
vendored
Executable file
@@ -0,0 +1,14 @@
|
||||
<configuration version="3">
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ">
|
||||
<address>192.0.2.1</address>
|
||||
<address>192.0.2.2</address>
|
||||
</node>
|
||||
<node id="GYRZZQBIRNPV4T7TC52WEQYJ3TFDQW6MWDFLMU4SSSU6EMFBK2VA">
|
||||
<address>192.0.2.3:6070</address>
|
||||
<address>[2001:db8::42]:4242</address>
|
||||
</node>
|
||||
<node id="LGFPDIT7SKNNJVJZA4FC7QNCRKCE753K72BW5QD2FOZ7FRFEP57Q">
|
||||
<address>[2001:db8::44]:4444</address>
|
||||
<address>192.0.2.4:6090</address>
|
||||
</node>
|
||||
</configuration>
|
||||
5
config/testdata/nolistenaddress.xml
vendored
Executable file
5
config/testdata/nolistenaddress.xml
vendored
Executable file
@@ -0,0 +1,5 @@
|
||||
<configuration version="1">
|
||||
<options>
|
||||
<listenAddress></listenAddress>
|
||||
</options>
|
||||
</configuration>
|
||||
20
config/testdata/overridenvalues.xml
vendored
Executable file
20
config/testdata/overridenvalues.xml
vendored
Executable file
@@ -0,0 +1,20 @@
|
||||
<configuration version="2">
|
||||
<options>
|
||||
<listenAddress>:23000</listenAddress>
|
||||
<allowDelete>false</allowDelete>
|
||||
<globalAnnounceServer>syncthing.nym.se:22026</globalAnnounceServer>
|
||||
<globalAnnounceEnabled>false</globalAnnounceEnabled>
|
||||
<localAnnounceEnabled>false</localAnnounceEnabled>
|
||||
<localAnnouncePort>42123</localAnnouncePort>
|
||||
<localAnnounceMCAddr>quux:3232</localAnnounceMCAddr>
|
||||
<parallelRequests>32</parallelRequests>
|
||||
<maxSendKbps>1234</maxSendKbps>
|
||||
<maxRecvKbps>2341</maxRecvKbps>
|
||||
<reconnectionIntervalS>6000</reconnectionIntervalS>
|
||||
<startBrowser>false</startBrowser>
|
||||
<upnpEnabled>false</upnpEnabled>
|
||||
<upnpLeaseMinutes>60</upnpLeaseMinutes>
|
||||
<upnpRenewalMinutes>15</upnpRenewalMinutes>
|
||||
<restartOnWakeup>false</restartOnWakeup>
|
||||
</options>
|
||||
</configuration>
|
||||
20
config/testdata/v1.xml
vendored
Executable file
20
config/testdata/v1.xml
vendored
Executable file
@@ -0,0 +1,20 @@
|
||||
<configuration version="1">
|
||||
<repository id="test" directory="~/Sync">
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
|
||||
<address>b</address>
|
||||
</node>
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
|
||||
<address>b</address>
|
||||
</node>
|
||||
</repository>
|
||||
<options>
|
||||
<readOnly>true</readOnly>
|
||||
<rescanIntervalS>600</rescanIntervalS>
|
||||
</options>
|
||||
</configuration>
|
||||
19
config/testdata/v2.xml
vendored
Executable file
19
config/testdata/v2.xml
vendored
Executable file
@@ -0,0 +1,19 @@
|
||||
<configuration version="2">
|
||||
<repository id="test" directory="~/Sync" ro="true">
|
||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
|
||||
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
|
||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
|
||||
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
|
||||
</repository>
|
||||
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
|
||||
<address>b</address>
|
||||
</node>
|
||||
<options>
|
||||
<rescanIntervalS>600</rescanIntervalS>
|
||||
</options>
|
||||
</configuration>
|
||||
15
config/testdata/v3.xml
vendored
Executable file
15
config/testdata/v3.xml
vendored
Executable file
@@ -0,0 +1,15 @@
|
||||
<configuration version="3">
|
||||
<repository id="test" directory="~/Sync" ro="true" ignorePerms="false">
|
||||
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" compression="false"></node>
|
||||
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" compression="false"></node>
|
||||
</repository>
|
||||
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="true">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="true">
|
||||
<address>b</address>
|
||||
</node>
|
||||
<options>
|
||||
<rescanIntervalS>600</rescanIntervalS>
|
||||
</options>
|
||||
</configuration>
|
||||
12
config/testdata/v4.xml
vendored
Executable file
12
config/testdata/v4.xml
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
<configuration version="4">
|
||||
<repository id="test" directory="~/Sync" ro="true" ignorePerms="false" rescanIntervalS="600">
|
||||
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></node>
|
||||
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></node>
|
||||
</repository>
|
||||
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="true">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="true">
|
||||
<address>b</address>
|
||||
</node>
|
||||
</configuration>
|
||||
8
config/testdata/versioningconfig.xml
vendored
Executable file
8
config/testdata/versioningconfig.xml
vendored
Executable file
@@ -0,0 +1,8 @@
|
||||
<configuration version="2">
|
||||
<repository id="test" directory="~/Sync" ro="true">
|
||||
<versioning type="simple">
|
||||
<param key="foo" val="bar"/>
|
||||
<param key="baz" val="quux"/>
|
||||
</versioning>
|
||||
</repository>
|
||||
</configuration>
|
||||
@@ -36,7 +36,7 @@ The Announcement packet has the following 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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Magic (0x029E4C77) |
|
||||
| Magic (0x9D79BC39) |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Node Structure \
|
||||
@@ -121,7 +121,7 @@ The Query packet has the following 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
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Magic Number (0x23D63A9A) |
|
||||
| Magic Number (0x2CA856F5) |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Length of Node ID |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
@@ -38,7 +38,6 @@ type Discoverer struct {
|
||||
forcedBcastTick chan time.Time
|
||||
extAnnounceOK bool
|
||||
extAnnounceOKmut sync.Mutex
|
||||
globalBcastStop chan bool
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
@@ -50,11 +49,6 @@ var (
|
||||
ErrIncorrectMagic = errors.New("incorrect magic number")
|
||||
)
|
||||
|
||||
// We tolerate a certain amount of errors because we might be running on
|
||||
// laptops that sleep and wake, have intermittent network connectivity, etc.
|
||||
// When we hit this many errors in succession, we stop.
|
||||
const maxErrors = 30
|
||||
|
||||
func NewDiscoverer(id protocol.NodeID, addresses []string) *Discoverer {
|
||||
return &Discoverer{
|
||||
myID: id,
|
||||
@@ -71,7 +65,10 @@ func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
|
||||
if localPort > 0 {
|
||||
bb, err := beacon.NewBroadcast(localPort)
|
||||
if err != nil {
|
||||
l.Infof("No IPv4 discovery possible (%v)", err)
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
l.Infoln("Local discovery over IPv4 unavailable")
|
||||
} else {
|
||||
d.broadcastBeacon = bb
|
||||
go d.recvAnnouncements(bb)
|
||||
@@ -81,7 +78,10 @@ func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
|
||||
if len(localMCAddr) > 0 {
|
||||
mb, err := beacon.NewMulticast(localMCAddr)
|
||||
if err != nil {
|
||||
l.Infof("No IPv6 discovery possible (%v)", err)
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
l.Infoln("Local discovery over IPv6 unavailable")
|
||||
} else {
|
||||
d.multicastBeacon = mb
|
||||
go d.recvAnnouncements(mb)
|
||||
@@ -89,7 +89,7 @@ func (d *Discoverer) StartLocal(localPort int, localMCAddr string) {
|
||||
}
|
||||
|
||||
if d.broadcastBeacon == nil && d.multicastBeacon == nil {
|
||||
l.Warnln("No local discovery method available")
|
||||
l.Warnln("Local discovery unavailable")
|
||||
} else {
|
||||
d.localBcastTick = time.Tick(d.localBcastIntv)
|
||||
d.forcedBcastTick = make(chan time.Time)
|
||||
@@ -108,8 +108,10 @@ func (d *Discoverer) StartGlobal(server string, extPort uint16) {
|
||||
}
|
||||
|
||||
func (d *Discoverer) StopGlobal() {
|
||||
close(d.stopGlobal)
|
||||
d.globalWG.Wait()
|
||||
if d.stopGlobal != nil {
|
||||
close(d.stopGlobal)
|
||||
d.globalWG.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Discoverer) ExtAnnounceOK() bool {
|
||||
|
||||
@@ -26,6 +26,7 @@ const (
|
||||
ItemStarted
|
||||
StateChanged
|
||||
RepoRejected
|
||||
ConfigSaved
|
||||
|
||||
AllEvents = ^EventType(0)
|
||||
)
|
||||
@@ -56,6 +57,8 @@ func (t EventType) String() string {
|
||||
return "StateChanged"
|
||||
case RepoRejected:
|
||||
return "RepoRejected"
|
||||
case ConfigSaved:
|
||||
return "ConfigSaved"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package files
|
||||
|
||||
import "code.google.com/p/go.text/unicode/norm"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !windows,!darwin
|
||||
|
||||
package files
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package files
|
||||
|
||||
import (
|
||||
@@ -182,18 +186,28 @@ func ldbGenericReplace(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo
|
||||
if lv := ldbInsert(batch, repo, node, newName, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
|
||||
if fs[fsi].IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, repo, node, newName)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
|
||||
}
|
||||
fsi++
|
||||
|
||||
case moreFs && moreDb && cmp == 0:
|
||||
// File exists on both sides - compare versions.
|
||||
// File exists on both sides - compare versions. We might get an
|
||||
// update with the same version and different flags if a node has
|
||||
// marked a file as invalid, so handle that too.
|
||||
var ef protocol.FileInfoTruncated
|
||||
ef.UnmarshalXDR(dbi.Value())
|
||||
if fs[fsi].Version > ef.Version {
|
||||
if fs[fsi].Version > ef.Version || fs[fsi].Version != ef.Version {
|
||||
if lv := ldbInsert(batch, repo, node, newName, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
|
||||
if fs[fsi].IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, repo, node, newName)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, repo, node, newName, fs[fsi].Version)
|
||||
}
|
||||
}
|
||||
// Iterate both sides.
|
||||
fsi++
|
||||
@@ -276,7 +290,11 @@ func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64
|
||||
if lv := ldbInsert(batch, repo, node, name, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
|
||||
if f.IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, repo, node, name)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -285,11 +303,17 @@ func ldbUpdate(db *leveldb.DB, repo, node []byte, fs []protocol.FileInfo) uint64
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if ef.Version != f.Version {
|
||||
// Flags might change without the version being bumped when we set the
|
||||
// invalid flag on an existing file.
|
||||
if ef.Version != f.Version || ef.Flags != f.Flags {
|
||||
if lv := ldbInsert(batch, repo, node, name, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
|
||||
if f.IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, repo, node, name)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, repo, node, name, f.Version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,7 +405,9 @@ func ldbRemoveFromGlobal(db dbReader, batch dbWriter, repo, node, file []byte) {
|
||||
gk := globalKey(repo, file)
|
||||
svl, err := db.Get(gk, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
// We might be called to "remove" a global version that doesn't exist
|
||||
// if the first update for the file is already marked invalid.
|
||||
return
|
||||
}
|
||||
|
||||
var fl versionList
|
||||
@@ -591,6 +617,7 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, truncate bool, fn fileIterat
|
||||
dbi := snap.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
outer:
|
||||
for dbi.Next() {
|
||||
var vl versionList
|
||||
err := vl.UnmarshalXDR(dbi.Value())
|
||||
@@ -616,28 +643,44 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, truncate bool, fn fileIterat
|
||||
|
||||
if need || !have {
|
||||
name := globalKeyName(dbi.Key())
|
||||
fk := nodeKey(repo, vl.versions[0].node, name)
|
||||
bs, err := snap.Get(fk, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
needVersion := vl.versions[0].version
|
||||
inner:
|
||||
for i := range vl.versions {
|
||||
if vl.versions[i].version != needVersion {
|
||||
// We haven't found a valid copy of the file with the needed version.
|
||||
continue outer
|
||||
}
|
||||
fk := nodeKey(repo, vl.versions[i].node, name)
|
||||
bs, err := snap.Get(fk, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
gf, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
gf, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if gf.IsDeleted() && !have {
|
||||
// We don't need deleted files that we don't have
|
||||
continue
|
||||
}
|
||||
if gf.IsInvalid() {
|
||||
// The file is marked invalid for whatever reason, don't use it.
|
||||
continue inner
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("need repo=%q node=%v name=%q need=%v have=%v haveV=%d globalV=%d", repo, protocol.NodeIDFromBytes(node), name, need, have, haveVersion, vl.versions[0].version)
|
||||
}
|
||||
if gf.IsDeleted() && !have {
|
||||
// We don't need deleted files that we don't have
|
||||
continue outer
|
||||
}
|
||||
|
||||
if cont := fn(gf); !cont {
|
||||
return
|
||||
if debug {
|
||||
l.Debugf("need repo=%q node=%v name=%q need=%v have=%v haveV=%d globalV=%d", repo, protocol.NodeIDFromBytes(node), name, need, have, haveVersion, vl.versions[0].version)
|
||||
}
|
||||
|
||||
if cont := fn(gf); !cont {
|
||||
return
|
||||
}
|
||||
|
||||
// This file is handled, no need to look further in the version list
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,11 @@ import (
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
var remoteNode protocol.NodeID
|
||||
var remoteNode0, remoteNode1 protocol.NodeID
|
||||
|
||||
func init() {
|
||||
remoteNode, _ = protocol.NodeIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
|
||||
remoteNode0, _ = protocol.NodeIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
|
||||
remoteNode1, _ = protocol.NodeIDFromString("I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU")
|
||||
}
|
||||
|
||||
func genBlocks(n int) []protocol.BlockInfo {
|
||||
@@ -81,6 +82,16 @@ func (l fileList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
|
||||
func (l fileList) String() string {
|
||||
var b bytes.Buffer
|
||||
b.WriteString("[]protocol.FileList{\n")
|
||||
for _, f := range l {
|
||||
fmt.Fprintf(&b, " %q: #%d, %d bytes, %d blocks, flags=%o\n", f.Name, f.Version, f.Size(), len(f.Blocks), f.Flags)
|
||||
}
|
||||
b.WriteString("}")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func TestGlobalSet(t *testing.T) {
|
||||
lamport.Default = lamport.Clock{}
|
||||
|
||||
@@ -91,20 +102,20 @@ func TestGlobalSet(t *testing.T) {
|
||||
|
||||
m := files.NewSet("test", db)
|
||||
|
||||
local0 := []protocol.FileInfo{
|
||||
local0 := fileList{
|
||||
protocol.FileInfo{Name: "a", Version: 1000, Blocks: genBlocks(1)},
|
||||
protocol.FileInfo{Name: "b", Version: 1000, Blocks: genBlocks(2)},
|
||||
protocol.FileInfo{Name: "c", Version: 1000, Blocks: genBlocks(3)},
|
||||
protocol.FileInfo{Name: "d", Version: 1000, Blocks: genBlocks(4)},
|
||||
protocol.FileInfo{Name: "z", Version: 1000, Blocks: genBlocks(8)},
|
||||
}
|
||||
local1 := []protocol.FileInfo{
|
||||
local1 := fileList{
|
||||
protocol.FileInfo{Name: "a", Version: 1000, Blocks: genBlocks(1)},
|
||||
protocol.FileInfo{Name: "b", Version: 1000, Blocks: genBlocks(2)},
|
||||
protocol.FileInfo{Name: "c", Version: 1000, Blocks: genBlocks(3)},
|
||||
protocol.FileInfo{Name: "d", Version: 1000, Blocks: genBlocks(4)},
|
||||
}
|
||||
localTot := []protocol.FileInfo{
|
||||
localTot := fileList{
|
||||
local0[0],
|
||||
local0[1],
|
||||
local0[2],
|
||||
@@ -112,76 +123,76 @@ func TestGlobalSet(t *testing.T) {
|
||||
protocol.FileInfo{Name: "z", Version: 1001, Flags: protocol.FlagDeleted},
|
||||
}
|
||||
|
||||
remote0 := []protocol.FileInfo{
|
||||
remote0 := fileList{
|
||||
protocol.FileInfo{Name: "a", Version: 1000, Blocks: genBlocks(1)},
|
||||
protocol.FileInfo{Name: "b", Version: 1000, Blocks: genBlocks(2)},
|
||||
protocol.FileInfo{Name: "c", Version: 1002, Blocks: genBlocks(5)},
|
||||
}
|
||||
remote1 := []protocol.FileInfo{
|
||||
remote1 := fileList{
|
||||
protocol.FileInfo{Name: "b", Version: 1001, Blocks: genBlocks(6)},
|
||||
protocol.FileInfo{Name: "e", Version: 1000, Blocks: genBlocks(7)},
|
||||
}
|
||||
remoteTot := []protocol.FileInfo{
|
||||
remoteTot := fileList{
|
||||
remote0[0],
|
||||
remote1[0],
|
||||
remote0[2],
|
||||
remote1[1],
|
||||
}
|
||||
|
||||
expectedGlobal := []protocol.FileInfo{
|
||||
remote0[0],
|
||||
remote1[0],
|
||||
remote0[2],
|
||||
localTot[3],
|
||||
remote1[1],
|
||||
localTot[4],
|
||||
expectedGlobal := fileList{
|
||||
remote0[0], // a
|
||||
remote1[0], // b
|
||||
remote0[2], // c
|
||||
localTot[3], // d
|
||||
remote1[1], // e
|
||||
localTot[4], // z
|
||||
}
|
||||
|
||||
expectedLocalNeed := []protocol.FileInfo{
|
||||
expectedLocalNeed := fileList{
|
||||
remote1[0],
|
||||
remote0[2],
|
||||
remote1[1],
|
||||
}
|
||||
|
||||
expectedRemoteNeed := []protocol.FileInfo{
|
||||
expectedRemoteNeed := fileList{
|
||||
local0[3],
|
||||
}
|
||||
|
||||
m.ReplaceWithDelete(protocol.LocalNodeID, local0)
|
||||
m.ReplaceWithDelete(protocol.LocalNodeID, local1)
|
||||
m.Replace(remoteNode, remote0)
|
||||
m.Update(remoteNode, remote1)
|
||||
m.Replace(remoteNode0, remote0)
|
||||
m.Update(remoteNode0, remote1)
|
||||
|
||||
g := globalList(m)
|
||||
sort.Sort(fileList(g))
|
||||
g := fileList(globalList(m))
|
||||
sort.Sort(g)
|
||||
|
||||
if fmt.Sprint(g) != fmt.Sprint(expectedGlobal) {
|
||||
t.Errorf("Global incorrect;\n A: %v !=\n E: %v", g, expectedGlobal)
|
||||
}
|
||||
|
||||
h := haveList(m, protocol.LocalNodeID)
|
||||
sort.Sort(fileList(h))
|
||||
h := fileList(haveList(m, protocol.LocalNodeID))
|
||||
sort.Sort(h)
|
||||
|
||||
if fmt.Sprint(h) != fmt.Sprint(localTot) {
|
||||
t.Errorf("Have incorrect;\n A: %v !=\n E: %v", h, localTot)
|
||||
}
|
||||
|
||||
h = haveList(m, remoteNode)
|
||||
sort.Sort(fileList(h))
|
||||
h = fileList(haveList(m, remoteNode0))
|
||||
sort.Sort(h)
|
||||
|
||||
if fmt.Sprint(h) != fmt.Sprint(remoteTot) {
|
||||
t.Errorf("Have incorrect;\n A: %v !=\n E: %v", h, remoteTot)
|
||||
}
|
||||
|
||||
n := needList(m, protocol.LocalNodeID)
|
||||
sort.Sort(fileList(n))
|
||||
n := fileList(needList(m, protocol.LocalNodeID))
|
||||
sort.Sort(n)
|
||||
|
||||
if fmt.Sprint(n) != fmt.Sprint(expectedLocalNeed) {
|
||||
t.Errorf("Need incorrect;\n A: %v !=\n E: %v", n, expectedLocalNeed)
|
||||
}
|
||||
|
||||
n = needList(m, remoteNode)
|
||||
sort.Sort(fileList(n))
|
||||
n = fileList(needList(m, remoteNode0))
|
||||
sort.Sort(n)
|
||||
|
||||
if fmt.Sprint(n) != fmt.Sprint(expectedRemoteNeed) {
|
||||
t.Errorf("Need incorrect;\n A: %v !=\n E: %v", n, expectedRemoteNeed)
|
||||
@@ -192,7 +203,7 @@ func TestGlobalSet(t *testing.T) {
|
||||
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, localTot[1])
|
||||
}
|
||||
|
||||
f = m.Get(remoteNode, "b")
|
||||
f = m.Get(remoteNode0, "b")
|
||||
if fmt.Sprint(f) != fmt.Sprint(remote1[0]) {
|
||||
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, remote1[0])
|
||||
}
|
||||
@@ -212,14 +223,14 @@ func TestGlobalSet(t *testing.T) {
|
||||
t.Errorf("GetGlobal incorrect;\n A: %v !=\n E: %v", f, protocol.FileInfo{})
|
||||
}
|
||||
|
||||
av := []protocol.NodeID{protocol.LocalNodeID, remoteNode}
|
||||
av := []protocol.NodeID{protocol.LocalNodeID, remoteNode0}
|
||||
a := m.Availability("a")
|
||||
if !(len(a) == 2 && (a[0] == av[0] && a[1] == av[1] || a[0] == av[1] && a[1] == av[0])) {
|
||||
t.Errorf("Availability incorrect;\n A: %v !=\n E: %v", a, av)
|
||||
}
|
||||
a = m.Availability("b")
|
||||
if len(a) != 1 || a[0] != remoteNode {
|
||||
t.Errorf("Availability incorrect;\n A: %v !=\n E: %v", a, remoteNode)
|
||||
if len(a) != 1 || a[0] != remoteNode0 {
|
||||
t.Errorf("Availability incorrect;\n A: %v !=\n E: %v", a, remoteNode0)
|
||||
}
|
||||
a = m.Availability("d")
|
||||
if len(a) != 1 || a[0] != protocol.LocalNodeID {
|
||||
@@ -227,6 +238,128 @@ func TestGlobalSet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedWithInvalid(t *testing.T) {
|
||||
lamport.Default = lamport.Clock{}
|
||||
|
||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := files.NewSet("test", db)
|
||||
|
||||
localHave := fileList{
|
||||
protocol.FileInfo{Name: "a", Version: 1000, Blocks: genBlocks(1)},
|
||||
}
|
||||
remote0Have := fileList{
|
||||
protocol.FileInfo{Name: "b", Version: 1001, Blocks: genBlocks(2)},
|
||||
protocol.FileInfo{Name: "c", Version: 1002, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
|
||||
protocol.FileInfo{Name: "d", Version: 1003, Blocks: genBlocks(7)},
|
||||
}
|
||||
remote1Have := fileList{
|
||||
protocol.FileInfo{Name: "c", Version: 1002, Blocks: genBlocks(7)},
|
||||
protocol.FileInfo{Name: "d", Version: 1003, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
|
||||
protocol.FileInfo{Name: "e", Version: 1004, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
|
||||
}
|
||||
|
||||
expectedNeed := fileList{
|
||||
protocol.FileInfo{Name: "b", Version: 1001, Blocks: genBlocks(2)},
|
||||
protocol.FileInfo{Name: "c", Version: 1002, Blocks: genBlocks(7)},
|
||||
protocol.FileInfo{Name: "d", Version: 1003, Blocks: genBlocks(7)},
|
||||
}
|
||||
|
||||
s.ReplaceWithDelete(protocol.LocalNodeID, localHave)
|
||||
s.Replace(remoteNode0, remote0Have)
|
||||
s.Replace(remoteNode1, remote1Have)
|
||||
|
||||
need := fileList(needList(s, protocol.LocalNodeID))
|
||||
sort.Sort(need)
|
||||
|
||||
if fmt.Sprint(need) != fmt.Sprint(expectedNeed) {
|
||||
t.Errorf("Need incorrect;\n A: %v !=\n E: %v", need, expectedNeed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateToInvalid(t *testing.T) {
|
||||
lamport.Default = lamport.Clock{}
|
||||
|
||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := files.NewSet("test", db)
|
||||
|
||||
localHave := fileList{
|
||||
protocol.FileInfo{Name: "a", Version: 1000, Blocks: genBlocks(1)},
|
||||
protocol.FileInfo{Name: "b", Version: 1001, Blocks: genBlocks(2)},
|
||||
protocol.FileInfo{Name: "c", Version: 1002, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
|
||||
protocol.FileInfo{Name: "d", Version: 1003, Blocks: genBlocks(7)},
|
||||
}
|
||||
|
||||
s.ReplaceWithDelete(protocol.LocalNodeID, localHave)
|
||||
|
||||
have := fileList(haveList(s, protocol.LocalNodeID))
|
||||
sort.Sort(have)
|
||||
|
||||
if fmt.Sprint(have) != fmt.Sprint(localHave) {
|
||||
t.Errorf("Have incorrect before invalidation;\n A: %v !=\n E: %v", have, localHave)
|
||||
}
|
||||
|
||||
localHave[1] = protocol.FileInfo{Name: "b", Version: 1001, Flags: protocol.FlagInvalid}
|
||||
s.Update(protocol.LocalNodeID, localHave[1:2])
|
||||
|
||||
have = fileList(haveList(s, protocol.LocalNodeID))
|
||||
sort.Sort(have)
|
||||
|
||||
if fmt.Sprint(have) != fmt.Sprint(localHave) {
|
||||
t.Errorf("Have incorrect after invalidation;\n A: %v !=\n E: %v", have, localHave)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidAvailability(t *testing.T) {
|
||||
lamport.Default = lamport.Clock{}
|
||||
|
||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := files.NewSet("test", db)
|
||||
|
||||
remote0Have := fileList{
|
||||
protocol.FileInfo{Name: "both", Version: 1001, Blocks: genBlocks(2)},
|
||||
protocol.FileInfo{Name: "r1only", Version: 1002, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
|
||||
protocol.FileInfo{Name: "r0only", Version: 1003, Blocks: genBlocks(7)},
|
||||
protocol.FileInfo{Name: "none", Version: 1004, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
|
||||
}
|
||||
remote1Have := fileList{
|
||||
protocol.FileInfo{Name: "both", Version: 1001, Blocks: genBlocks(2)},
|
||||
protocol.FileInfo{Name: "r1only", Version: 1002, Blocks: genBlocks(7)},
|
||||
protocol.FileInfo{Name: "r0only", Version: 1003, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
|
||||
protocol.FileInfo{Name: "none", Version: 1004, Blocks: genBlocks(5), Flags: protocol.FlagInvalid},
|
||||
}
|
||||
|
||||
s.Replace(remoteNode0, remote0Have)
|
||||
s.Replace(remoteNode1, remote1Have)
|
||||
|
||||
if av := s.Availability("both"); len(av) != 2 {
|
||||
t.Error("Incorrect availability for 'both':", av)
|
||||
}
|
||||
|
||||
if av := s.Availability("r0only"); len(av) != 1 || av[0] != remoteNode0 {
|
||||
t.Error("Incorrect availability for 'r0only':", av)
|
||||
}
|
||||
|
||||
if av := s.Availability("r1only"); len(av) != 1 || av[0] != remoteNode1 {
|
||||
t.Error("Incorrect availability for 'r1only':", av)
|
||||
}
|
||||
|
||||
if av := s.Availability("none"); len(av) != 0 {
|
||||
t.Error("Incorrect availability for 'none':", av)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalDeleted(t *testing.T) {
|
||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
@@ -332,7 +465,7 @@ func Benchmark10kUpdateChg(b *testing.B) {
|
||||
}
|
||||
|
||||
m := files.NewSet("test", db)
|
||||
m.Replace(remoteNode, remote)
|
||||
m.Replace(remoteNode0, remote)
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 10000; i++ {
|
||||
@@ -363,7 +496,7 @@ func Benchmark10kUpdateSme(b *testing.B) {
|
||||
b.Fatal(err)
|
||||
}
|
||||
m := files.NewSet("test", db)
|
||||
m.Replace(remoteNode, remote)
|
||||
m.Replace(remoteNode0, remote)
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 10000; i++ {
|
||||
@@ -390,7 +523,7 @@ func Benchmark10kNeed2k(b *testing.B) {
|
||||
}
|
||||
|
||||
m := files.NewSet("test", db)
|
||||
m.Replace(remoteNode, remote)
|
||||
m.Replace(remoteNode0, remote)
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 8000; i++ {
|
||||
@@ -423,7 +556,7 @@ func Benchmark10kHaveFullList(b *testing.B) {
|
||||
}
|
||||
|
||||
m := files.NewSet("test", db)
|
||||
m.Replace(remoteNode, remote)
|
||||
m.Replace(remoteNode0, remote)
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 2000; i++ {
|
||||
@@ -456,7 +589,7 @@ func Benchmark10kGlobal(b *testing.B) {
|
||||
}
|
||||
|
||||
m := files.NewSet("test", db)
|
||||
m.Replace(remoteNode, remote)
|
||||
m.Replace(remoteNode0, remote)
|
||||
|
||||
var local []protocol.FileInfo
|
||||
for i := 0; i < 2000; i++ {
|
||||
@@ -507,8 +640,8 @@ func TestGlobalReset(t *testing.T) {
|
||||
t.Errorf("Global incorrect;\n%v !=\n%v", g, local)
|
||||
}
|
||||
|
||||
m.Replace(remoteNode, remote)
|
||||
m.Replace(remoteNode, nil)
|
||||
m.Replace(remoteNode0, remote)
|
||||
m.Replace(remoteNode0, nil)
|
||||
|
||||
g = globalList(m)
|
||||
sort.Sort(fileList(g))
|
||||
@@ -547,7 +680,7 @@ func TestNeed(t *testing.T) {
|
||||
}
|
||||
|
||||
m.ReplaceWithDelete(protocol.LocalNodeID, local)
|
||||
m.Replace(remoteNode, remote)
|
||||
m.Replace(remoteNode0, remote)
|
||||
|
||||
need := needList(m, protocol.LocalNodeID)
|
||||
|
||||
@@ -618,7 +751,7 @@ func TestListDropRepo(t *testing.T) {
|
||||
protocol.FileInfo{Name: "e", Version: 1002},
|
||||
protocol.FileInfo{Name: "f", Version: 1002},
|
||||
}
|
||||
s1.Replace(remoteNode, local2)
|
||||
s1.Replace(remoteNode0, local2)
|
||||
|
||||
// Check that we have both repos and their data is in the global list
|
||||
|
||||
@@ -649,6 +782,46 @@ func TestListDropRepo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalNeedWithInvalid(t *testing.T) {
|
||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
s := files.NewSet("test1", db)
|
||||
|
||||
rem0 := fileList{
|
||||
protocol.FileInfo{Name: "a", Version: 1002, Blocks: genBlocks(4)},
|
||||
protocol.FileInfo{Name: "b", Version: 1002, Flags: protocol.FlagInvalid},
|
||||
protocol.FileInfo{Name: "c", Version: 1002, Blocks: genBlocks(4)},
|
||||
}
|
||||
s.Replace(remoteNode0, rem0)
|
||||
|
||||
rem1 := fileList{
|
||||
protocol.FileInfo{Name: "a", Version: 1002, Blocks: genBlocks(4)},
|
||||
protocol.FileInfo{Name: "b", Version: 1002, Blocks: genBlocks(4)},
|
||||
protocol.FileInfo{Name: "c", Version: 1002, Flags: protocol.FlagInvalid},
|
||||
}
|
||||
s.Replace(remoteNode1, rem1)
|
||||
|
||||
total := fileList{
|
||||
// There's a valid copy of each file, so it should be merged
|
||||
protocol.FileInfo{Name: "a", Version: 1002, Blocks: genBlocks(4)},
|
||||
protocol.FileInfo{Name: "b", Version: 1002, Blocks: genBlocks(4)},
|
||||
protocol.FileInfo{Name: "c", Version: 1002, Blocks: genBlocks(4)},
|
||||
}
|
||||
|
||||
need := fileList(needList(s, protocol.LocalNodeID))
|
||||
if fmt.Sprint(need) != fmt.Sprint(total) {
|
||||
t.Errorf("Need incorrect;\n A: %v !=\n E: %v", need, total)
|
||||
}
|
||||
|
||||
global := fileList(globalList(s))
|
||||
if fmt.Sprint(global) != fmt.Sprint(total) {
|
||||
t.Errorf("Global incorrect;\n A: %v !=\n E: %v", global, total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongPath(t *testing.T) {
|
||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
@@ -704,7 +877,7 @@ func TestStressGlobalVersion(t *testing.T) {
|
||||
m := files.NewSet("test", db)
|
||||
|
||||
done := make(chan struct{})
|
||||
go stressWriter(m, remoteNode, set1, nil, done)
|
||||
go stressWriter(m, remoteNode0, set1, nil, done)
|
||||
go stressWriter(m, protocol.LocalNodeID, set2, nil, done)
|
||||
|
||||
t0 := time.Now()
|
||||
|
||||
@@ -20,15 +20,22 @@ const (
|
||||
func Convert(pattern string, flags int) (*regexp.Regexp, error) {
|
||||
any := "."
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
flags |= FNM_NOESCAPE
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
flags |= FNM_NOESCAPE | FNM_CASEFOLD
|
||||
pattern = filepath.FromSlash(pattern)
|
||||
if flags&FNM_PATHNAME != 0 {
|
||||
any = "[^\\\\]"
|
||||
}
|
||||
} else if flags&FNM_PATHNAME != 0 {
|
||||
any = "[^/]"
|
||||
case "darwin":
|
||||
flags |= FNM_CASEFOLD
|
||||
fallthrough
|
||||
default:
|
||||
if flags&FNM_PATHNAME != 0 {
|
||||
any = "[^/]"
|
||||
}
|
||||
}
|
||||
|
||||
if flags&FNM_NOESCAPE != 0 {
|
||||
pattern = strings.Replace(pattern, "\\", "\\\\", -1)
|
||||
} else {
|
||||
|
||||
@@ -50,12 +50,17 @@ var testcases = []testcase{
|
||||
{"**/foo.txt", "bar/baz/foo.txt", 0, true},
|
||||
{"**/foo.txt", "bar/baz/foo.txt", FNM_PATHNAME, true},
|
||||
|
||||
{"foo.txt", "foo.TXT", 0, false},
|
||||
{"foo.txt", "foo.TXT", FNM_CASEFOLD, true},
|
||||
}
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
testcases = append(testcases, testcase{"foo.txt", "foo.TXT", 0, true})
|
||||
case "darwin":
|
||||
testcases = append(testcases, testcase{"foo.txt", "foo.TXT", 0, true})
|
||||
fallthrough
|
||||
default:
|
||||
testcases = append(testcases, testcase{"f\\[ab\\]o.txt", "f[ab]o.txt", 0, true})
|
||||
testcases = append(testcases, testcase{"foo\\.txt", "foo.txt", 0, true})
|
||||
testcases = append(testcases, testcase{"foo\\*.txt", "foo*.txt", 0, true})
|
||||
|
||||
149
gui/app.js
149
gui/app.js
@@ -15,7 +15,7 @@ syncthing.config(function ($httpProvider, $translateProvider) {
|
||||
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token';
|
||||
|
||||
$translateProvider.useStaticFilesLoader({
|
||||
prefix: 'lang-',
|
||||
prefix: 'lang/lang-',
|
||||
suffix: '.json'
|
||||
});
|
||||
});
|
||||
@@ -153,13 +153,17 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('UIOnline');
|
||||
$scope.init();
|
||||
online = true;
|
||||
restarting = false;
|
||||
$('#networkError').modal('hide');
|
||||
$('#restarting').modal('hide');
|
||||
$('#shutdown').modal('hide');
|
||||
if (restarting){
|
||||
document.location.reload(true);
|
||||
} else {
|
||||
console.log('UIOnline');
|
||||
$scope.init();
|
||||
online = true;
|
||||
restarting = false;
|
||||
$('#networkError').modal('hide');
|
||||
$('#restarting').modal('hide');
|
||||
$('#shutdown').modal('hide');
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('UIOffline', function (event, arg) {
|
||||
@@ -237,6 +241,14 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
}
|
||||
})
|
||||
|
||||
$scope.$on('ConfigSaved', function (event, arg) {
|
||||
updateLocalConfig(arg.data);
|
||||
|
||||
$http.get(urlbase + '/config/sync').success(function (data) {
|
||||
$scope.configInSync = data.configInSync;
|
||||
});
|
||||
});
|
||||
|
||||
var debouncedFuncs = {};
|
||||
|
||||
function refreshRepo(repo) {
|
||||
@@ -252,6 +264,33 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
debouncedFuncs[key]();
|
||||
}
|
||||
|
||||
function updateLocalConfig(config) {
|
||||
var hasConfig = !isEmptyObject($scope.config);
|
||||
|
||||
$scope.config = config;
|
||||
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
|
||||
|
||||
$scope.nodes = $scope.config.Nodes;
|
||||
$scope.nodes.forEach(function (nodeCfg) {
|
||||
$scope.completion[nodeCfg.NodeID] = {
|
||||
_total: 100,
|
||||
};
|
||||
});
|
||||
$scope.nodes.sort(nodeCompare);
|
||||
|
||||
$scope.repos = repoMap($scope.config.Repositories);
|
||||
Object.keys($scope.repos).forEach(function (repo) {
|
||||
refreshRepo(repo);
|
||||
$scope.repos[repo].Nodes.forEach(function (nodeCfg) {
|
||||
refreshCompletion(nodeCfg.NodeID, repo);
|
||||
});
|
||||
});
|
||||
|
||||
if (!hasConfig) {
|
||||
$scope.$emit('ConfigLoaded');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshSystem() {
|
||||
$http.get(urlbase + '/system').success(function (data) {
|
||||
$scope.myID = data.myID;
|
||||
@@ -324,31 +363,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
|
||||
function refreshConfig() {
|
||||
$http.get(urlbase + '/config').success(function (data) {
|
||||
var hasConfig = !isEmptyObject($scope.config);
|
||||
|
||||
$scope.config = data;
|
||||
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
|
||||
|
||||
$scope.nodes = $scope.config.Nodes;
|
||||
$scope.nodes.forEach(function (nodeCfg) {
|
||||
$scope.completion[nodeCfg.NodeID] = {
|
||||
_total: 100,
|
||||
};
|
||||
});
|
||||
$scope.nodes.sort(nodeCompare);
|
||||
|
||||
$scope.repos = repoMap($scope.config.Repositories);
|
||||
Object.keys($scope.repos).forEach(function (repo) {
|
||||
refreshRepo(repo);
|
||||
$scope.repos[repo].Nodes.forEach(function (nodeCfg) {
|
||||
refreshCompletion(nodeCfg.NodeID, repo);
|
||||
});
|
||||
});
|
||||
|
||||
if (!hasConfig) {
|
||||
$scope.$emit('ConfigLoaded');
|
||||
}
|
||||
|
||||
updateLocalConfig(data);
|
||||
console.log("refreshConfig", data);
|
||||
});
|
||||
|
||||
@@ -360,6 +375,10 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
var refreshNodeStats = debounce(function () {
|
||||
$http.get(urlbase+"/stats/node").success(function (data) {
|
||||
$scope.stats = data;
|
||||
for (var node in $scope.stats) {
|
||||
$scope.stats[node].LastSeen = new Date($scope.stats[node].LastSeen);
|
||||
$scope.stats[node].LastSeenDays = (new Date() - $scope.stats[node].LastSeen) / 1000 / 86400;
|
||||
}
|
||||
console.log("refreshNodeStats", data);
|
||||
});
|
||||
}, 500);
|
||||
@@ -477,17 +496,6 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
return '';
|
||||
};
|
||||
|
||||
$scope.nodeVer = function (nodeCfg) {
|
||||
if (nodeCfg.NodeID === $scope.myID) {
|
||||
return $scope.version;
|
||||
}
|
||||
var conn = $scope.connections[nodeCfg.NodeID];
|
||||
if (conn) {
|
||||
return conn.ClientVersion;
|
||||
}
|
||||
return '?';
|
||||
};
|
||||
|
||||
$scope.findNode = function (nodeID) {
|
||||
var matches = $scope.nodes.filter(function (n) { return n.NodeID == nodeID; });
|
||||
if (matches.length != 1) {
|
||||
@@ -521,6 +529,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
// Make a working copy
|
||||
$scope.tmpOptions = angular.copy($scope.config.Options);
|
||||
$scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
|
||||
$scope.tmpOptions.NodeName = $scope.thisNode().Name;
|
||||
$scope.tmpGUI = angular.copy($scope.config.GUI);
|
||||
$('#settings').modal();
|
||||
};
|
||||
@@ -553,6 +562,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
}
|
||||
|
||||
// Apply new settings locally
|
||||
$scope.thisNode().Name = $scope.tmpOptions.NodeName;
|
||||
$scope.config.Options = angular.copy($scope.tmpOptions);
|
||||
$scope.config.GUI = angular.copy($scope.tmpGUI);
|
||||
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
|
||||
@@ -579,7 +589,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
|
||||
setTimeout(function(){
|
||||
window.location.protocol = protocol;
|
||||
}, 1000);
|
||||
}, 2500);
|
||||
|
||||
$scope.protocolChanged = false;
|
||||
}
|
||||
@@ -837,6 +847,13 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
|
||||
cfg.APIKey = randomString(30, 32);
|
||||
};
|
||||
|
||||
$scope.showURPreview = function () {
|
||||
$('#settings').modal('hide');
|
||||
$('#urPreview').modal().on('hidden.bs.modal', function () {
|
||||
$('#settings').modal();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.acceptUR = function () {
|
||||
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
|
||||
$scope.saveConfig();
|
||||
@@ -906,10 +923,10 @@ function nodeCompare(a, b) {
|
||||
}
|
||||
|
||||
function repoCompare(a, b) {
|
||||
if (a.Directory < b.Directory) {
|
||||
if (a.ID < b.ID) {
|
||||
return -1;
|
||||
}
|
||||
return a.Directory > b.Directory;
|
||||
return a.ID > b.ID;
|
||||
}
|
||||
|
||||
function repoMap(l) {
|
||||
@@ -971,9 +988,9 @@ function debounce(func, wait) {
|
||||
} else {
|
||||
timeout = null;
|
||||
if (again) {
|
||||
again = false;
|
||||
result = func.apply(context, args);
|
||||
context = args = null;
|
||||
again = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1043,12 +1060,6 @@ syncthing.filter('metric', function () {
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.filter('short', function () {
|
||||
return function (input) {
|
||||
return input.substr(0, 6);
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.filter('alwaysNumber', function () {
|
||||
return function (input) {
|
||||
if (input === undefined) {
|
||||
@@ -1058,18 +1069,6 @@ syncthing.filter('alwaysNumber', function () {
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.filter('shortPath', function () {
|
||||
return function (input) {
|
||||
if (input === undefined)
|
||||
return "";
|
||||
var parts = input.split(/[\/\\]/);
|
||||
if (!parts || parts.length <= 3) {
|
||||
return input;
|
||||
}
|
||||
return ".../" + parts.slice(parts.length-2).join("/");
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.filter('basename', function () {
|
||||
return function (input) {
|
||||
if (input === undefined)
|
||||
@@ -1082,24 +1081,6 @@ syncthing.filter('basename', function () {
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.filter('clean', function () {
|
||||
return function (input) {
|
||||
return encodeURIComponent(input).replace(/%/g, '');
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.directive('optionEditor', function () {
|
||||
return {
|
||||
restrict: 'C',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
scope: {
|
||||
setting: '=setting',
|
||||
},
|
||||
template: '<input type="text" ng-model="config.Options[setting.id]"></input>',
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.directive('uniqueRepo', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
|
||||
BIN
gui/font/raleway-500.woff
Normal file
BIN
gui/font/raleway-500.woff
Normal file
Binary file not shown.
@@ -2,5 +2,5 @@
|
||||
font-family: 'Raleway';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local('Raleway'), url(raleway-500.ttf) format('truetype');
|
||||
src: local('Raleway'), url(raleway-500.woff) format('woff');
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
522
gui/index.html
522
gui/index.html
@@ -11,93 +11,12 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<link rel="shortcut icon" href="favicon.png">
|
||||
<link rel="shortcut icon" href="img/favicon.png">
|
||||
|
||||
<title>Syncthing | {{thisNodeName()}}</title>
|
||||
<link href="bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="raleway.css" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
body {
|
||||
padding-bottom: 70px;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
ul+h5 {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.text-monospace {
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.table-condensed>thead>tr>th, .table-condensed>tbody>tr>th, .table-condensed>tfoot>tr>th, .table-condensed>thead>tr>td, .table-condensed>tbody>tr>td, .table-condensed>tfoot>tr>td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
top: -5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.list-no-bullet {
|
||||
list-style-type: none
|
||||
}
|
||||
|
||||
.li-column {
|
||||
display: inline-block;
|
||||
min-width: 7em;
|
||||
margin-right: 1em;
|
||||
background-color: rgb(236, 240, 241);
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
margin: 2px 2px;
|
||||
}
|
||||
.li-column span.data {
|
||||
margin-left: 0.5em;
|
||||
min-width: 10em;
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ng-cloak {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.table th {
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
|
||||
.table td.small-data {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table.table-condensed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
table.table-condensed td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width:767px) {
|
||||
table.table-condensed td {
|
||||
/* for mobile phones to allow linebreaks in long repro folder/shared with
|
||||
* columns. */
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link href="font/raleway.css" rel="stylesheet">
|
||||
<link href="overrides.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -107,21 +26,18 @@
|
||||
|
||||
<nav class="navbar navbar-top navbar-default" role="navigation">
|
||||
<div class="container">
|
||||
<span class="navbar-brand"><img class="logo" src="logo-text-64.png" height="32" width="117"/></span>
|
||||
<span class="navbar-brand"><img class="logo" src="img/logo-text-64.png" height="32" width="117"/></span>
|
||||
<p class="navbar-text hidden-xs">{{thisNodeName()}}</p>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li ng-if="upgradeInfo.newer">
|
||||
<button type="button" class="btn navbar-btn btn-default" href="" ng-click="upgrade()">
|
||||
<span class="glyphicon glyphicon-chevron-up"></span> 
|
||||
<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
|
||||
<button type="button" class="btn navbar-btn btn-primary btn-sm" href="" ng-click="upgrade()">
|
||||
<span class="glyphicon glyphicon-chevron-up"></span> 
|
||||
<span translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span translate>Edit</spanq <b class="caret"></b></a>
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-cog"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="" ng-click="addRepo()"><span class="glyphicon glyphicon-hdd"></span> <span translate>Add Repository</span></a></li>
|
||||
<li><a href="" ng-click="addNode()"><span class="glyphicon glyphicon-retweet"></span> <span translate>Add Node</span></a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span> <span translate>Settings</span></a></li>
|
||||
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span> <span translate>Show ID</span></a></li>
|
||||
<li class="divider"></li>
|
||||
@@ -163,199 +79,208 @@
|
||||
<div class="col-md-6">
|
||||
<div class="panel-group" id="repositories">
|
||||
<div class="panel panel-{{repoClass(repo.ID)}}" ng-repeat="repo in repoList()">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-heading" data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}" style="cursor: pointer">
|
||||
<h3 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}">
|
||||
<span class="glyphicon glyphicon-hdd"></span> {{repo.ID}}
|
||||
<span class="pull-right hidden-xs" ng-switch="repoStatus(repo.ID)">
|
||||
<span translate ng-switch-when="unknown">Unknown</span>
|
||||
<span translate ng-switch-when="stopped">Stopped</span>
|
||||
<span translate ng-switch-when="scanning">Scanning</span>
|
||||
<span ng-switch-when="syncing">
|
||||
<span translate>Syncing</span>
|
||||
({{syncPercentage(repo.ID)}}%)
|
||||
</span>
|
||||
<span ng-switch-when="idle">
|
||||
<span translate>Idle</span>
|
||||
({{syncPercentage(repo.ID)}}%)
|
||||
</span>
|
||||
<span class="glyphicon glyphicon-hdd"></span> {{repo.ID}}
|
||||
<span class="pull-right hidden-xs" ng-switch="repoStatus(repo.ID)">
|
||||
<span translate ng-switch-when="unknown">Unknown</span>
|
||||
<span translate ng-switch-when="stopped">Stopped</span>
|
||||
<span translate ng-switch-when="scanning">Scanning</span>
|
||||
<span ng-switch-when="syncing">
|
||||
<span translate>Syncing</span>
|
||||
({{syncPercentage(repo.ID)}}%)
|
||||
</span>
|
||||
</a>
|
||||
<span ng-switch-when="idle">
|
||||
<span translate>Idle</span>
|
||||
({{syncPercentage(repo.ID)}}%)
|
||||
</span>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="repo-{{$index}}" class="panel-collapse collapse">
|
||||
<div id="repo-{{$index}}" class="panel-collapse collapse" ng-class="{in: $index === 0}">
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Repository ID</span></th>
|
||||
<td class="text-right">{{repo.ID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-folder-open"></span> <span translate>Folder</span></th>
|
||||
<td class="text-right">{{repo.Directory}}</td>
|
||||
</tr>
|
||||
<tr ng-if="model[repo.ID].invalid">
|
||||
<th><span class="glyphicon glyphicon-warning-sign"></span> <span translate>Error</span></th>
|
||||
<td class="text-right">{{model[repo.ID].invalid}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-globe"></span> <span translate>Global Repository</span></th>
|
||||
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].globalBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-home"></span> <span translate>Local Repository</span></th>
|
||||
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].localBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Out Of Sync</span></th>
|
||||
<td class="text-right">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Repository ID</span></th>
|
||||
<td class="text-right">{{repo.ID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-folder-open"></span> <span translate>Folder</span></th>
|
||||
<td class="text-right">{{repo.Directory}}</td>
|
||||
</tr>
|
||||
<tr ng-if="model[repo.ID].invalid">
|
||||
<th><span class="glyphicon glyphicon-warning-sign"></span> <span translate>Error</span></th>
|
||||
<td class="text-right">{{model[repo.ID].invalid}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-globe"></span> <span translate>Global Repository</span></th>
|
||||
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].globalBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-home"></span> <span translate>Local Repository</span></th>
|
||||
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].localBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Out Of Sync</span></th>
|
||||
<td class="text-right">
|
||||
<a ng-if="model[repo.ID].needFiles > 0" ng-click="showNeed(repo.ID)" href="">{{model[repo.ID].needFiles | alwaysNumber}} <span translate>items</span>, ~{{model[repo.ID].needBytes | binary}}B</a>
|
||||
<span ng-if="model[repo.ID].needFiles == 0">0 <span translate>items</span>, 0 B</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-lock"></span> <span translate>Master Repo</span></th>
|
||||
<td class="text-right">
|
||||
<span translate ng-if="repo.ReadOnly">Yes</span>
|
||||
<span translate ng-if="!repo.ReadOnly">No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-unchecked"></span> <span translate>Ignore Permissions</span></th>
|
||||
<td class="text-right">
|
||||
<span translate ng-if="repo.IgnorePerms">Yes</span>
|
||||
<span translate ng-if="!repo.IgnorePerms">No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-refresh"></span> <span translate>Rescan Interval</span></th>
|
||||
<td class="text-right">{{repo.RescanIntervalS}} s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-share-alt"></span> <span translate>Shared With</span></th>
|
||||
<td class="text-right">{{sharesRepo(repo)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-lock"></span> <span translate>Master Repo</span></th>
|
||||
<td class="text-right">
|
||||
<span translate ng-if="repo.ReadOnly">Yes</span>
|
||||
<span translate ng-if="!repo.ReadOnly">No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-unchecked"></span> <span translate>Ignore Permissions</span></th>
|
||||
<td class="text-right">
|
||||
<span translate ng-if="repo.IgnorePerms">Yes</span>
|
||||
<span translate ng-if="!repo.IgnorePerms">No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-refresh"></span> <span translate>Rescan Interval</span></th>
|
||||
<td class="text-right">{{repo.RescanIntervalS}} s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-share-alt"></span> <span translate>Shared With</span></th>
|
||||
<td class="text-right">{{sharesRepo(repo)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button class="btn btn-sm btn-danger" ng-if="repo.ReadOnly && model[repo.ID].needFiles > 0" ng-click="override(repo.ID)" href=""><span class="glyphicon glyphicon-upload"></span> <span translate>Override Changes</span></button>
|
||||
<span class="pull-right">
|
||||
<a class="btn btn-sm btn-default" href="" ng-show="repoStatus(repo.ID) == 'idle'" ng-click="rescanRepo(repo.ID)"><span class="glyphicon glyphicon-refresh"></span> <span translate>Rescan</span></a>
|
||||
<a class="btn btn-sm btn-primary" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span> <span translate>Edit</span></a>
|
||||
<a class="btn btn-sm btn-danger" ng-if="repo.ReadOnly && model[repo.ID].needFiles > 0" ng-click="override(repo.ID)" href=""><span class="glyphicon glyphicon-upload"></span> <span translate>Override Changes</span></a>
|
||||
<button class="btn btn-sm btn-default" href="" ng-show="repoStatus(repo.ID) == 'idle'" ng-click="rescanRepo(repo.ID)"><span class="glyphicon glyphicon-refresh"></span> <span translate>Rescan</span></button>
|
||||
<button class="btn btn-sm btn-default" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span> <span translate>Edit</span></button>
|
||||
</span>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-default pull-right" ng-click="addRepo()"><span class="glyphicon glyphicon-plus"></span> <span translate>Add Repository</span></button>
|
||||
<div class="clearfix"></div>
|
||||
<hr class="visible-sm"/>
|
||||
</div>
|
||||
|
||||
<!-- Node list (top right) -->
|
||||
|
||||
<!-- This node -->
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="panel-group" id="nodes">
|
||||
<div class="panel panel-default" ng-repeat="nodeCfg in [thisNode()]">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#nodes" href="#node-this"><span class="glyphicon glyphicon-home"></span> {{nodeName(nodeCfg)}}</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="node-this" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-th"></span> <span translate>RAM Utilization</span></th>
|
||||
<td class="text-right">{{system.sys | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tasks"></span> <span translate>CPU Utilization</span></th>
|
||||
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Download Rate</span></th>
|
||||
<td class="text-right">{{connections['total'].inbps | metric}}bps ({{connections['total'].InBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
||||
<td class="text-right">{{connections['total'].outbps | metric}}bps ({{connections['total'].OutBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr ng-if="system.extAnnounceOK != undefined">
|
||||
<th><span class="glyphicon glyphicon-bullhorn"></span> <span translate>Announce Server</span></th>
|
||||
<td class="text-right">
|
||||
<span class="data text-success" ng-if="system.extAnnounceOK"><span translate>Online</span></span>
|
||||
<span class="data text-danger" ng-if="!system.extAnnounceOK"><span translate>Offline</span></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
||||
<td class="text-right">{{version}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span> <span translate>Edit</span></a></span>
|
||||
</div>
|
||||
<div class="panel panel-default" ng-repeat="nodeCfg in [thisNode()]">
|
||||
<div class="panel-heading" data-toggle="collapse" href="#node-this" style="cursor: pointer">
|
||||
<h3 class="panel-title">
|
||||
<span class="glyphicon glyphicon-home"></span> {{nodeName(nodeCfg)}}
|
||||
</h3>
|
||||
</div>
|
||||
<div id="node-this" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Download Rate</span></th>
|
||||
<td class="text-right">{{connections['total'].inbps | metric}}bps ({{connections['total'].InBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
||||
<td class="text-right">{{connections['total'].outbps | metric}}bps ({{connections['total'].OutBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-th"></span> <span translate>RAM Utilization</span></th>
|
||||
<td class="text-right">{{system.sys | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-dashboard"></span> <span translate>CPU Utilization</span></th>
|
||||
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
||||
</tr>
|
||||
<tr ng-if="system.extAnnounceOK != undefined">
|
||||
<th><span class="glyphicon glyphicon-bullhorn"></span> <span translate>Global Discovery Server</span></th>
|
||||
<td class="text-right">
|
||||
<span class="data text-success" ng-if="system.extAnnounceOK"><span translate>Online</span></span>
|
||||
<span class="data text-danger" ng-if="!system.extAnnounceOK"><span translate>Offline</span></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
||||
<td class="text-right">{{version}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remote nodes -->
|
||||
|
||||
<div class="panel-group" id="nodes">
|
||||
<div class="panel panel-{{nodeClass(nodeCfg)}}" ng-repeat="nodeCfg in otherNodes()">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-heading" data-toggle="collapse" data-parent="#nodes" href="#node-{{$index}}" style="cursor: pointer">
|
||||
<h3 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{$index}}">
|
||||
<span class="glyphicon glyphicon-retweet"></span>
|
||||
{{nodeName(nodeCfg)}}
|
||||
<span class="pull-right hidden-xs">
|
||||
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total == 100">
|
||||
<span translate>Up to Date</span> (100%)
|
||||
</span>
|
||||
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total < 100">
|
||||
<span translate>Syncing</span> ({{completion[nodeCfg.NodeID]._total | number:0}}%)
|
||||
</span>
|
||||
<span translate ng-if="!connections[nodeCfg.NodeID]">Disconnected</span>
|
||||
<span class="glyphicon glyphicon-retweet"></span> {{nodeName(nodeCfg)}}
|
||||
<span class="pull-right hidden-xs">
|
||||
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total == 100">
|
||||
<span translate>Up to Date</span> (100%)
|
||||
</span>
|
||||
</a>
|
||||
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total < 100">
|
||||
<span translate>Syncing</span> ({{completion[nodeCfg.NodeID]._total | number:0}}%)
|
||||
</span>
|
||||
<span translate ng-if="!connections[nodeCfg.NodeID]">Disconnected</span>
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="node-{{$index}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-link"></span> <span translate>Address</span></th>
|
||||
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-comment"></span> <span translate>Synchronization</span></th>
|
||||
<td class="text-right">{{completion[nodeCfg.NodeID]._total | alwaysNumber | number:0}}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-compressed"></span> <span translate>Use Compression</span></th>
|
||||
<td translate ng-if="nodeCfg.Compression" class="text-right">Yes</td>
|
||||
<td translate ng-if="!nodeCfg.Compression" class="text-right">No</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Download Rate</span></th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps ({{connections[nodeCfg.NodeID].InBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps ({{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
||||
<td class="text-right">{{nodeVer(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr ng-if="!connections[nodeCfg.NodeID]">
|
||||
<th><span class="glyphicon glyphicon-eye-open"></span> <span translate>Last seen</span></th>
|
||||
<td translate ng-if="stats[nodeCfg.NodeID].LastSeen.indexOf('1970') > -1" class="text-right">Never</td>
|
||||
<td ng-if="stats[nodeCfg.NodeID].LastSeen.indexOf('1970') < 0" class="text-right">{{stats[nodeCfg.NodeID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span> <span translate>Edit</span></a></span>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr ng-if="connections[nodeCfg.NodeID]">
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> <span translate>Download Rate</span></th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps ({{connections[nodeCfg.NodeID].InBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr ng-if="connections[nodeCfg.NodeID]">
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> <span translate>Upload Rate</span></th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps ({{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-link"></span> <span translate>Address</span></th>
|
||||
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr ng-if="connections[nodeCfg.NodeID]">
|
||||
<th><span class="glyphicon glyphicon-comment"></span> <span translate>Synchronization</span></th>
|
||||
<td class="text-right">{{completion[nodeCfg.NodeID]._total | alwaysNumber | number:0}}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-compressed"></span> <span translate>Use Compression</span></th>
|
||||
<td translate ng-if="nodeCfg.Compression" class="text-right">Yes</td>
|
||||
<td translate ng-if="!nodeCfg.Compression" class="text-right">No</td>
|
||||
</tr>
|
||||
<tr ng-if="connections[nodeCfg.NodeID]">
|
||||
<th><span class="glyphicon glyphicon-tag"></span> <span translate>Version</span></th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].ClientVersion}}</td>
|
||||
</tr>
|
||||
<tr ng-if="!connections[nodeCfg.NodeID]">
|
||||
<th><span class="glyphicon glyphicon-eye-open"></span> <span translate>Last seen</span></th>
|
||||
<td translate ng-if="!stats[nodeCfg.NodeID].LastSeenDays || stats[nodeCfg.NodeID].LastSeenDays >= 365" class="text-right">Never</td>
|
||||
<td ng-if="stats[nodeCfg.NodeID].LastSeenDays < 365" class="text-right">{{stats[nodeCfg.NodeID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="pull-right"><a class="btn btn-sm btn-default" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span> <span translate>Edit</span></a></span>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-default pull-right" ng-click="addNode()"><span class="glyphicon glyphicon-plus"></span> <span translate>Add Node</span></button>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div> <!-- /row -->
|
||||
|
||||
@@ -468,9 +393,9 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="saveNode()" ng-disabled="nodeEditor.$invalid"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
||||
<button ng-if="editingExisting && !editingSelf" type="button" class="btn btn-danger pull-left" ng-click="deleteNode()"><span class="glyphicon glyphicon-minus"></span> <span translate>Delete</span></button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveNode()" ng-disabled="nodeEditor.$invalid"><span class="glyphicon glyphicon-ok"></span> <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>
|
||||
<button ng-if="editingExisting && !editingSelf" type="button" class="btn btn-danger pull-left btn-sm" ng-click="deleteNode()"><span class="glyphicon glyphicon-minus"></span> <span translate>Delete</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -594,9 +519,9 @@
|
||||
<div translate ng-show="!editingExisting">When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="saveRepo()" ng-disabled="repoEditor.$invalid"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
||||
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left" ng-click="deleteRepo()"><span class="glyphicon glyphicon-minus"></span> <span translate>Delete</span></button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveRepo()" ng-disabled="repoEditor.$invalid"><span class="glyphicon glyphicon-ok"></span> <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>
|
||||
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left btn-sm" ng-click="deleteRepo()"><span class="glyphicon glyphicon-minus"></span> <span translate>Delete</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -615,28 +540,22 @@
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate for="NodeName">Node Name</label>
|
||||
<input id="NodeName" class="form-control" type="text" ng-model="tmpOptions.NodeName">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="ListenStr">Sync Protocol Listen Addresses</label>
|
||||
<input id="ListenStr" class="form-control" type="text" ng-model="tmpOptions.ListenStr">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
|
||||
<input id="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.MaxRecvKbps">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
|
||||
<input id="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.MaxSendKbps">
|
||||
</div>
|
||||
<!--
|
||||
<div class="form-group">
|
||||
<label translate for="ReconnectIntervalS">Reconnect Interval (s)</label>
|
||||
<input id="ReconnectIntervalS" class="form-control" type="number" ng-model="tmpOptions.ReconnectIntervalS">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="ParallelRequests">Max Outstanding Requests</label>
|
||||
<input id="ParallelRequests" class="form-control" type="number" ng-model="tmpOptions.ParallelRequests">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="MaxChangeKbps">Max File Change Rate (KiB/s)</label>
|
||||
<input id="MaxChangeKbps" class="form-control" type="number" ng-model="tmpOptions.MaxChangeKbps">
|
||||
</div>
|
||||
-->
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
@@ -681,7 +600,7 @@
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<span translate>Use HTTPS for GUI</span> <input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS">
|
||||
<span translate>Use HTTPS for GUI</span> <input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -695,7 +614,7 @@
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<span translate>Anonymous Usage Reporting</span> <input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled">
|
||||
<span translate>Anonymous Usage Reporting</span> <input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled"> (<a translate ng-click="showURPreview()" href="#">Preview</a>)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -713,8 +632,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="saveSettings()"><span class="glyphicon glyphicon-ok"></span> <span translate>Save</span></button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> <span translate>Close</span></button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()"><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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -729,14 +648,34 @@
|
||||
<h4 translate class="modal-title">Allow Anonymous Usage Reporting?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
|
||||
<p translate translate-value-url="https://data.syncthing.net">The aggregated statistics are publicly available at {%url%}.</p>
|
||||
<button translate type="button" class="btn btn-default" ng-show="!reportPreview" ng-click="showReportPreview()">Preview Usage Report</button>
|
||||
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
|
||||
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
|
||||
<p translate translate-value-url="https://data.syncthing.net">The aggregated statistics are publicly available at {%url%}.</p>
|
||||
<button translate type="button" class="btn btn-default btn-sm" ng-show="!reportPreview" ng-click="showReportPreview()">Preview Usage Report</button>
|
||||
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success" ng-click="acceptUR()"><span class="glyphicon glyphicon-ok"></span> <span translate>Yes</span></button>
|
||||
<button type="button" class="btn btn-danger" ng-click="declineUR()"><span class="glyphicon glyphicon-remove"></span> <span translate>No</span></button>
|
||||
<button type="button" class="btn btn-success btn-sm" ng-click="acceptUR()"><span class="glyphicon glyphicon-ok"></span> <span translate>Yes</span></button>
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="declineUR()"><span class="glyphicon glyphicon-remove"></span> <span translate>No</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage report preview modal -->
|
||||
|
||||
<div id="urPreview" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-success">
|
||||
<h4 translate class="modal-title">Anonymous Usage Reporting</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
|
||||
<p translate translate-value-url="https://data.syncthing.net">The aggregated statistics are publicly available at {%url%}.</p>
|
||||
<pre><small>{{reportData | json}}</small></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-ok"></span> <span translate>OK</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -757,7 +696,7 @@
|
||||
<!-- About modal -->
|
||||
|
||||
<modal id="about" large="yes" close="yes" status="info" title="About">
|
||||
<h1 class="text-center"><img alt="Syncthing" title="Syncthing" src="logo-text-256.png" style="vertical-align: -16px" height="100" width="366"/><br/><small>{{version}}</small></h1>
|
||||
<h1 class="text-center"><img alt="Syncthing" title="Syncthing" src="img/logo-text-256.png" style="vertical-align: -16px" height="100" width="366"/><br/><small>{{version}}</small></h1>
|
||||
<hr/>
|
||||
|
||||
<p translate>Copyright © 2014 Jakob Borg and the following Contributors:</p>
|
||||
@@ -773,18 +712,19 @@
|
||||
<li>Brandon Philips</li>
|
||||
<li>Gilli Sigurdsson</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<ul>
|
||||
<li>James Patterson</li>
|
||||
<li>Jens Diemer</li>
|
||||
<li>Marcin Dziadus</li>
|
||||
<li>Michael Tilli</li>
|
||||
<li>Philippe Schommers</li>
|
||||
<li>Ryan Sullivan</li>
|
||||
<li>Tully Robinson</li>
|
||||
<li>Veeti Paananen</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
@@ -803,12 +743,12 @@
|
||||
</modal>
|
||||
|
||||
|
||||
<script src="angular.min.js"></script>
|
||||
<script src="angular-translate.min.js"></script>
|
||||
<script src="angular-translate-loader.min.js"></script>
|
||||
<script src="jquery-2.0.3.min.js"></script>
|
||||
<script src="angular/angular.min.js"></script>
|
||||
<script src="angular/angular-translate.min.js"></script>
|
||||
<script src="angular/angular-translate-loader.min.js"></script>
|
||||
<script src="jquery/jquery-2.0.3.min.js"></script>
|
||||
<script src="bootstrap/js/bootstrap.min.js"></script>
|
||||
<script src="valid-langs.js"></script>
|
||||
<script src="lang/valid-langs.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
7
gui/lang/README-FIRST.txt
Normal file
7
gui/lang/README-FIRST.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
All files in this directory are auto generated. Do not change any of
|
||||
them. To contribute translations, please head over to
|
||||
|
||||
https://www.transifex.com/projects/p/syncthing/
|
||||
|
||||
Any updates made on Transifex will be automatically pulled into these
|
||||
files.
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Глобална Папка",
|
||||
"Idle": "Без Работа",
|
||||
"Ignore Permissions": "Игнорирай Права за Достъп",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Пази Версии",
|
||||
"Last seen": "Last seen",
|
||||
"Latest Release": "Най-новата Версия",
|
||||
138
gui/lang/lang-ca.json
Normal file
138
gui/lang/lang-ca.json
Normal file
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"API Key": "Clau API",
|
||||
"About": "Sobre",
|
||||
"Add Node": "Afegir Node",
|
||||
"Add Repository": "Afegir Repositori",
|
||||
"Address": "Adreça",
|
||||
"Addresses": "Adreces",
|
||||
"Allow Anonymous Usage Reporting?": "Permetre l'enviament anònim d'informes d'ús?",
|
||||
"Announce Server": "Servidor d'anunciament",
|
||||
"Anonymous Usage Reporting": "Informe anònim d'ús",
|
||||
"Bugs": "Bugs",
|
||||
"CPU Utilization": "Utilització del CPU",
|
||||
"Close": "Tancar",
|
||||
"Connection Error": "Error de connexió",
|
||||
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg i els següents contribuïdors",
|
||||
"Delete": "Esborrar",
|
||||
"Disconnected": "Desconnectat",
|
||||
"Documentation": "Documentació",
|
||||
"Download Rate": "Tasa de descarrega",
|
||||
"Edit": "Editar",
|
||||
"Edit Node": "Editar Node",
|
||||
"Edit Repository": "Editar Repositori",
|
||||
"Enable UPnP": "Habilitat UPnP",
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduir, separat per comes, adreces \"ip:port\" o \"dynamic\" per descobrir automàticament les adreces.",
|
||||
"Error": "Error",
|
||||
"File Versioning": "Versionat de Fitxers",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Els bits de permisos dels fitxers son ignorats quan es cerquen canvis. Utilitzar en sistemes de fitxers FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Els fitxers es mouen amb l'estampat de la data a la carpeta .stversions quan son substituïts o esborrats per syncthing.",
|
||||
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Els fitxers estan protegits de canvis fets per altres nodes, però els canvis fets en aquest node seran enviats a la resta del cluster.",
|
||||
"Folder": "Carpeta",
|
||||
"GUI Authentication Password": "Contrasenya d'autenticació GUI",
|
||||
"GUI Authentication User": "Usuari d'autenticació GUI",
|
||||
"GUI Listen Addresses": "Adreça d'escolta del GUI",
|
||||
"Generate": "Generar",
|
||||
"Global Discovery": "Descobriment Global",
|
||||
"Global Discovery Server": "Servidor de Descobriment Global",
|
||||
"Global Repository": "Repositori Global",
|
||||
"Idle": "Inactiu",
|
||||
"Ignore Permissions": "Ignora Permisos",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Mantenir Versions",
|
||||
"Last seen": "Vist per última vegada",
|
||||
"Latest Release": "Última publicació",
|
||||
"Local Discovery": "Descobriment Local",
|
||||
"Local Discovery Port": "Port de Descobriment Local",
|
||||
"Local Repository": "Repositori Local",
|
||||
"Master Repo": "Rep Master",
|
||||
"Max File Change Rate (KiB/s)": "Tasa Màxima d'intercanvi de fitxer (KiB/s)",
|
||||
"Max Outstanding Requests": "Màxim de Peticions Pendents",
|
||||
"Maximum Age": "Antiguitat Màxima",
|
||||
"Never": "Mai",
|
||||
"No": "No",
|
||||
"No File Versioning": "Sense Versionat de Fitxer",
|
||||
"Node ID": "ID del Node",
|
||||
"Node Identification": "Identificació del Node",
|
||||
"Node Name": "Nom Del Node",
|
||||
"Notice": "Avís",
|
||||
"OK": "OK",
|
||||
"Offline": "Desconnectat",
|
||||
"Online": "Connectat",
|
||||
"Out Of Sync": "Fora de la Sincronització",
|
||||
"Outgoing Rate Limit (KiB/s)": "Tasa Límit de Sortida (KiB/s)",
|
||||
"Override Changes": "Sobreescriure Canvis",
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta del repositori a l'equip local. Si no existeix serà creada. El caràcter (~) es pot fer servir com a drecera de",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Ruta on les versions s'haurien de guardar (deixa-ho buit per fer servir el directori .stversions per defecte al repositori)",
|
||||
"Please wait": "Si-us-plau espera",
|
||||
"Preview Usage Report": "Vista Prèvia de l'Informe d'Ús",
|
||||
"RAM Utilization": "Utilització de la RAM",
|
||||
"Reconnect Interval (s)": "Interval de Reconnexió (s)",
|
||||
"Repository ID": "ID del Repositori",
|
||||
"Repository Master": "Repositori Mestre",
|
||||
"Repository Path": "Ruta del Repositori",
|
||||
"Rescan": "Re-escanejar",
|
||||
"Rescan Interval": "Interval de re-escaneig",
|
||||
"Rescan Interval (s)": "Interval de re-escaneig (s)",
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "És Necessari Reiniciar",
|
||||
"Restarting": "Reiniciant",
|
||||
"Save": "Guardar",
|
||||
"Scanning": "Escanejant",
|
||||
"Select the nodes to share this repository with.": "Seleccionar els nodes amb els que es comparteix el repositori.",
|
||||
"Settings": "Preferències",
|
||||
"Share With Nodes": "Compartir Amb Els Nodes",
|
||||
"Shared With": "Compartir Amb",
|
||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identificador curt pel repositori. Ha de ser el mateix per tots els nodes del cluster.",
|
||||
"Show ID": "Mostrar ID",
|
||||
"Shown instead of Node ID in the cluster status.": "Mostrat en comptes del ID del Node en l'estat del cluster.",
|
||||
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Mostrat en comptes del ID del Node en l'estat del cluster. Serà advertit als altres nodes com un nom opcional per defecte.",
|
||||
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Mostrat en comptes del ID del Node en l'estat del cluster. S'actualitzara al nom del node si es deixa buit.",
|
||||
"Shutdown": "Apagar",
|
||||
"Simple File Versioning": "Versionat de Fitxers Senzill",
|
||||
"Source Code": "Codi Font",
|
||||
"Staggered File Versioning": "Versionat de Fitxers Esglaonat",
|
||||
"Start Browser": "Arrancar Navegador",
|
||||
"Stopped": "Aturat",
|
||||
"Support / Forum": "Suport / Fòrum",
|
||||
"Sync Protocol Listen Addresses": "Adreça d'escolta del Protocol Sync",
|
||||
"Synchronization": "Sincronització",
|
||||
"Syncing": "Synthing",
|
||||
"Syncthing has been shut down.": "S'ha aturat el synthing.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing inclou el següent programari o parts dels mateixos:",
|
||||
"Syncthing is restarting.": "Reiniciant syncthing.",
|
||||
"Syncthing is upgrading.": "Actualitzant syncthing.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Synthing sembla parat, o hi ha algun problema amb la connexió a Internet. Reintentant...",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan públicament disponibles a {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració s'ha guardar però no s'ha activat. S'ha de reiniciar el synthing per activar la nova configuració.",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "L'informe d'ús encriptat s'envia diàriament. Es fa servir per rastrejar plataformes habituals, mides de repositoris i versions de l'aplicació. Si es canvia el conjunt de dades reportades es demanarà amb aquest diàleg de nou.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "El ID del Node introduït no sembla vàlid. Hauria de tenir 52 caràcters amb lletres i números, els espais i les barres son opcionals.",
|
||||
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "El ID del Node introduït no sembla vàlid. Hauria de tenir 52 o 56 caràcters amb lletres i números, els espais i les barres son opcionals.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Es fan servir els següents intervals: per la primera hora es manté una versió cada 30 segons, pel primer dia es manté una versió cada hora, pel primer cada 30 dies es manté una versió cada dia, fins el màxim d'antiguitat es manté una versió cada setmana.",
|
||||
"The maximum age must be a number and cannot be blank.": "La màxima antiguitat ha de ser un número i no pot estar en blanc.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Temps màxim en mantenir una versió (en dies, si es deixa en 0 es mantenen les versions per sempre).",
|
||||
"The node ID cannot be blank.": "El ID del node no pot estar en blanc.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "El ID del node per introduir aquí es pot trobar al diàleg \"Editar > Mostrar ID\" en l'altre node. Els espais i les barres son opcionals (s'ignoren).",
|
||||
"The number of old versions to keep, per file.": "El nombre de versions antigues que es mantenen per fitxer.",
|
||||
"The number of versions must be a number and cannot be blank.": "El nombre de versions ha de ser un número i no es pot deixar en blanc.",
|
||||
"The repository ID cannot be blank.": "El ID del repositori no pot estar en blanc.",
|
||||
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "El ID del repositori ha de ser un identificador curt (64 caràcters o menys) format només per lletres, nombres i el punt (.), barra (-) i barra baixa (_).",
|
||||
"The repository ID must be unique.": "El ID del repositori ha de ser únic",
|
||||
"The repository path cannot be blank.": "La carpeta del repositori no pot estar en blanc.",
|
||||
"The rescan interval must be at least 5 seconds.": "El interval de re-escaneig ha de ser com a mínim de 5 segons.",
|
||||
"Unknown": "Desconegut",
|
||||
"Up to Date": "Actualitzat",
|
||||
"Upgrade To {%version%}": "Actualitzar a {{version}}",
|
||||
"Upgrading": "Actualitzant",
|
||||
"Upload Rate": "Tasa de Pujada",
|
||||
"Usage": "Ús",
|
||||
"Use Compression": "Utilitza compressió",
|
||||
"Use HTTPS for GUI": "Utilitzar HTTPS pel GUI",
|
||||
"Version": "Versió",
|
||||
"Versions Path": "Carpeta de les Versions",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions son automàticament eliminades si son més antigues que el màxim d'antiguitat o si excedeixen del nombre de fitxers permesos en un interval.",
|
||||
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quan s'afegeix un nou node recorda que aquest node s'ha d'afegir tambe a l'altre banda.",
|
||||
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Quan s'afegeix un nou repositori recorda que el ID del repositori s'utilitza per lligar repositoris entre nodes. Es distingeix entre majúscules i minúscules i ha de ser exactament iguals entre tots els nodes.",
|
||||
"Yes": "Si",
|
||||
"You must keep at least one version.": "Has de mantenir com a mínim una versió.",
|
||||
"items": "Elements"
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Global lagring",
|
||||
"Idle": "Inaktiv",
|
||||
"Ignore Permissions": "Ignorér filrettigheder",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Behold versioner",
|
||||
"Last seen": "Last seen",
|
||||
"Latest Release": "Seneste udgivelse",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Globales Verzeichnis",
|
||||
"Idle": "Untätig",
|
||||
"Ignore Permissions": "Berechtigungen ignorieren",
|
||||
"Incoming Rate Limit (KiB/s)": "Eingehendes Datenratelimit (KiB/s)",
|
||||
"Keep Versions": "Versionen erhalten",
|
||||
"Last seen": "Zuletzt online",
|
||||
"Latest Release": "Letzte Veröffentlichung",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Global Repository",
|
||||
"Idle": "Ανενεργό",
|
||||
"Ignore Permissions": "Αγνόησε Δικαιώματα",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Διατήρησε Εκδόσεις",
|
||||
"Last seen": "Last seen",
|
||||
"Latest Release": "Τελευταία Έκδοση",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Global Repository",
|
||||
"Idle": "Idle",
|
||||
"Ignore Permissions": "Ignore Permissions",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Keep Versions",
|
||||
"Last seen": "Last seen",
|
||||
"Latest Release": "Latest Release",
|
||||
@@ -63,6 +64,7 @@
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Path where versions should be stored (leave empty for the default .stversions folder in the repository).",
|
||||
"Please wait": "Please wait",
|
||||
"Preview": "Preview",
|
||||
"Preview Usage Report": "Preview Usage Report",
|
||||
"RAM Utilization": "RAM Utilization",
|
||||
"Reconnect Interval (s)": "Reconnect Interval (s)",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Repositorio global",
|
||||
"Idle": "Inactivo",
|
||||
"Ignore Permissions": "Ignorar permisos",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Conservar versiones",
|
||||
"Last seen": "Visto por ultima vez",
|
||||
"Latest Release": "Última versión",
|
||||
@@ -37,8 +37,9 @@
|
||||
"Global Repository": "Répertoire global",
|
||||
"Idle": "Au repos",
|
||||
"Ignore Permissions": "Ignorer les permissions",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Conserver les versions",
|
||||
"Last seen": "Dernière appartition",
|
||||
"Last seen": "Dernière apparition",
|
||||
"Latest Release": "Dernière version",
|
||||
"Local Discovery": "Recherche locale",
|
||||
"Local Discovery Port": "Port de recherche locale",
|
||||
@@ -61,7 +62,7 @@
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite du débit sortant (KiB/s)",
|
||||
"Override Changes": "Écraser les changements",
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Chemin du répertoire sur l'ordinateur local. Il sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Path where versions should be stored (leave empty for the default .stversions folder in the repository).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
|
||||
"Please wait": "Merci de patienter",
|
||||
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
|
||||
"RAM Utilization": "Utilisation de la RAM",
|
||||
@@ -84,8 +85,8 @@
|
||||
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identifiant court pour le répertoire. Il doit être le même sur l'ensemble des nœuds du cluster.",
|
||||
"Show ID": "Montrer l'ID",
|
||||
"Shown instead of Node ID in the cluster status.": "Affiché à la place de l'ID du nœud au sein du cluster.",
|
||||
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.",
|
||||
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.",
|
||||
"Shown instead of Node ID in the cluster status. Will be advertised to other nodes as an optional default name.": "Affiché à la place de l'ID du nœud dans le statut du cluster. Sera annoncé aux autres nœuds comme un nom par défaut optionnel.",
|
||||
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Affiché à la place de l'ID du nœud dans le statut du cluster. Sera mis à jour par le nom que le nœud annonce si laissé vide.",
|
||||
"Shutdown": "Éteindre",
|
||||
"Simple File Versioning": "Versions simples de fichier",
|
||||
"Source Code": "Code source",
|
||||
@@ -97,7 +98,7 @@
|
||||
"Synchronization": "Synchronisation",
|
||||
"Syncing": "En cours de synchronisation",
|
||||
"Syncthing has been shut down.": "Syncthing a été éteint.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing inclut les logiciels, ou portion de ceux-ci, suivants:",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing intègre les logiciels suivants (ou des éléments provenant de ces logiciels) :",
|
||||
"Syncthing is restarting.": "Syncthing est cours de redémarrage.",
|
||||
"Syncthing is upgrading.": "Syncthing est cours de mise à jour.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
|
||||
@@ -106,9 +107,9 @@
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des répertoires et les versions de l'application. Si le jeu de données rapportées devait être changé, il vous sera demandé de le valider de nouveau via ce dialogue.",
|
||||
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID du nœud ne semble pas être valide. Il devrait ressembler à une chaine de 52 caractères comprenant lettres et chiffres, avec des espaces et des traits d'union optionnels.",
|
||||
"The entered node ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID du nœud inséré ne semble pas être valide. Il devrait ressembler à une chaîne de 52 ou 56 comprenant lettres et chiffres, avec des espaces et des traits d'union optionnels.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Les intervalles suivant sont utilisés: la première heure une version est conservée chaque 30 secondes, le premier jour une version est conservée chaque heure, les premiers 30 jours une version est conservée chaque jour, jusqu'à la limite d'âge maximum une version est conservée chaque semaine.",
|
||||
"The maximum age must be a number and cannot be blank.": "L'ancienneté maximum doit être un nombre et ne peut être vide.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Le temps maximum de conservation d'une version (en jours, mettre à 0 pour conserver les versions pour toujours)",
|
||||
"The node ID cannot be blank.": "L'ID du nœud ne peut être vide.",
|
||||
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "L'ID du nœud à insérer peut être trouvé à travers le menu \"Éditer > Montrer l'ID\" des autres nœuds. Les espaces et les traits d'union sont optionnels (ils seront ignorés).",
|
||||
"The number of old versions to keep, per file.": "Le nombre d'anciennes versions à garder, par fichier.",
|
||||
@@ -120,7 +121,7 @@
|
||||
"The rescan interval must be at least 5 seconds.": "L'intervalle de scan doit être d'au minimum 5 secondes.",
|
||||
"Unknown": "Inconnu",
|
||||
"Up to Date": "Synchronisation à jour",
|
||||
"Upgrade To {%version%}": "Upgrader vers {{version}}",
|
||||
"Upgrade To {%version%}": "Mettre à jour vers {{version}}",
|
||||
"Upgrading": "Mise à jour de Syncthing",
|
||||
"Upload Rate": "Débit d'envoi",
|
||||
"Usage": "Utilisation",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Globális tároló",
|
||||
"Idle": "Tétlen",
|
||||
"Ignore Permissions": "Jogosultságok figyelmen kívül hagyása",
|
||||
"Incoming Rate Limit (KiB/s)": "Bejövő sebesség korlát (KIB/mp)",
|
||||
"Keep Versions": "Verziók megtartása",
|
||||
"Last seen": "Utoljára látva",
|
||||
"Latest Release": "Utolsó kiadás",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Deposito Globale",
|
||||
"Idle": "Inattivo",
|
||||
"Ignore Permissions": "Ignora Permessi",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Versioni Mantenute",
|
||||
"Last seen": "Last seen",
|
||||
"Latest Release": "Ultima Versione",
|
||||
@@ -61,7 +62,7 @@
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite di Velocità in Uscita (KiB/s)",
|
||||
"Override Changes": "Ignora Modifiche",
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Percorso del deposito nel computer locale. Verrà creato se non esiste già. Il carattere tilde (~) può essere utilizzato come scorciatoia per",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Path where versions should be stored (leave empty for the default .stversions folder in the repository).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Percorso di salvataggio delle versioni (lasciare vuoto per utilizzare la cartella predefinita .stversions nel deposito).",
|
||||
"Please wait": "Attendere prego",
|
||||
"Preview Usage Report": "Anteprima Statistiche di Utilizzo",
|
||||
"RAM Utilization": "Utilizzo RAM",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Visuotinė saugykla",
|
||||
"Idle": "Laisvas",
|
||||
"Ignore Permissions": "Nepaisyti failų prieigos leidimų",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Saugojamų versijų kiekis",
|
||||
"Last seen": "Paskutinį kartą matytas",
|
||||
"Latest Release": "Paskutinė versija",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Globale repository",
|
||||
"Idle": "Inactief",
|
||||
"Ignore Permissions": "Rechten negeren",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Versies behouden",
|
||||
"Last seen": "Last seen",
|
||||
"Latest Release": "Laatste uitgave",
|
||||
@@ -7,7 +7,7 @@
|
||||
"Addresses": "Endereços",
|
||||
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
|
||||
"Announce Server": "Servidor de anúncios",
|
||||
"Anonymous Usage Reporting": "Enviar de relatórios anónimos de utilização",
|
||||
"Anonymous Usage Reporting": "Enviar relatórios anónimos de utilização",
|
||||
"Bugs": "Erros",
|
||||
"CPU Utilization": "Utilização da CPU",
|
||||
"Close": "Fechar",
|
||||
@@ -37,8 +37,9 @@
|
||||
"Global Repository": "Repositório global",
|
||||
"Idle": "Em espera",
|
||||
"Ignore Permissions": "Ignorar permissões",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite de velocidade de recepção (KiB/s)",
|
||||
"Keep Versions": "Manter versões",
|
||||
"Last seen": "Última vez que foi visto",
|
||||
"Last seen": "Última vez que foi verificado",
|
||||
"Latest Release": "Última versão",
|
||||
"Local Discovery": "Busca local",
|
||||
"Local Discovery Port": "Porto da busca local",
|
||||
@@ -71,7 +72,7 @@
|
||||
"Repository Path": "Caminho do repositório",
|
||||
"Rescan": "Verificar agora",
|
||||
"Rescan Interval": "Intervalo entre verificações",
|
||||
"Rescan Interval (s)": "Intervalo entre verificações (s)",
|
||||
"Rescan Interval (s)": "Intervalo entre verificações (em segundos)",
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "É preciso reiniciar",
|
||||
"Restarting": "Reiniciando",
|
||||
@@ -122,7 +123,7 @@
|
||||
"Up to Date": "Actualizado",
|
||||
"Upgrade To {%version%}": "Actualizar para {{version}}",
|
||||
"Upgrading": "Actualizando",
|
||||
"Upload Rate": "Taxa de envio",
|
||||
"Upload Rate": "Velocidade de envio",
|
||||
"Usage": "Utilização",
|
||||
"Use Compression": "Usar compressão",
|
||||
"Use HTTPS for GUI": "Utilizar HTTPS na interface gráfica",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Глобальный репозиторий",
|
||||
"Idle": "Бездействует",
|
||||
"Ignore Permissions": "Игнорировать файловые права доступа",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Количество хранимых версий",
|
||||
"Last seen": "Last seen",
|
||||
"Latest Release": "Последняя версия",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Global lagring",
|
||||
"Idle": "Vilande",
|
||||
"Ignore Permissions": "Ignorera filrättigheter",
|
||||
"Incoming Rate Limit (KiB/s)": "Max nedladdningshastighet (KiB/s)",
|
||||
"Keep Versions": "Behåll versioner",
|
||||
"Last seen": "Senast online",
|
||||
"Latest Release": "Senaste version",
|
||||
@@ -58,7 +59,7 @@
|
||||
"Offline": "Ej tillgänglig",
|
||||
"Online": "Tillgänglig",
|
||||
"Out Of Sync": "Ur synk",
|
||||
"Outgoing Rate Limit (KiB/s)": "Utgående hastighetsbegränsning (KiB/s)",
|
||||
"Outgoing Rate Limit (KiB/s)": "Max uppladdningshastighet (KiB/s)",
|
||||
"Override Changes": "Skriv över ändringar",
|
||||
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the repository).": "Katalog där versioner sparas (lämna blankt för att använda .stversions i lagringskatalogen).",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Global Depo",
|
||||
"Idle": "Boşta",
|
||||
"Ignore Permissions": "İzinleri yoksay",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Sürüm tut",
|
||||
"Last seen": "Last seen",
|
||||
"Latest Release": "Son sürüm",
|
||||
@@ -37,6 +37,7 @@
|
||||
"Global Repository": "Глобальний репозиторій",
|
||||
"Idle": "Очікування",
|
||||
"Ignore Permissions": "Ігнорувати права доступу до файлів",
|
||||
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
|
||||
"Keep Versions": "Зберігати версії",
|
||||
"Last seen": "З’являвся останній раз",
|
||||
"Latest Release": "Останній реліз",
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user