Compare commits

..

36 Commits

Author SHA1 Message Date
Jakob Borg
9812305bb9 Translation update 2014-09-10 14:21:44 +02:00
Jakob Borg
f680a63a1f Woops, broke LastSeen 2014-09-10 11:29:01 +02:00
Jakob Borg
781d63cb2a UI Tweaks 2014-09-10 11:27:21 +02:00
Jakob Borg
9ff04ee3d8 Don't start when the config dir is not a dir 2014-09-10 10:36:05 +02:00
Jakob Borg
5d85a24977 Don't potentially block forever in Close() (fixes #655) 2014-09-10 08:48:15 +02:00
Jakob Borg
1e51fca0b0 Don't crash on new nodes (fixes #656) 2014-09-10 08:31:30 +02:00
Jakob Borg
5537d53f9a Timestamp the panic log 2014-09-10 08:25:56 +02:00
Jakob Borg
50a4170541 Announce actual port when UPnP is disabled (fixes #657) 2014-09-10 08:22:38 +02:00
Jakob Borg
3a8255bda1 Update lang-en.json 2014-09-10 07:48:35 +02:00
Jakob Borg
a617846f0f Merge pull request #654 from AudriusButkevicius/disco
Check if global discovery was actually started before trying to stop it (fixes #653)
2014-09-10 07:41:49 +02:00
Audrius Butkevicius
5772588c29 Check if global discovery was actually started before trying to stop it (fixes #653) 2014-09-10 00:05:28 +01:00
Jakob Borg
9d0dc45f74 Clarify clickability of top Edit menu (ref #651) 2014-09-08 19:54:11 +02:00
Jakob Borg
c6aefbc9a0 Entire panel title should be clickable (ref #651) 2014-09-08 19:46:33 +02:00
Jakob Borg
dbbafb0cc9 Hide irrelevant fields for disconnected nodes (ref #592) 2014-09-08 19:41:20 +02:00
Jakob Borg
6e8272f78f Implement incoming rate limit (fixes #613) 2014-09-08 17:25:55 +02:00
Jakob Borg
baf8a63121 Announce Server -> Discovery Server 2014-09-08 09:42:33 +02:00
Jakob Borg
fc4a76ee50 Only add one instance of a file to the need list (fixes #592) 2014-09-08 09:37:42 +02:00
Jakob Borg
2117d1d035 Tone down insignificant discovery error messages (ref #241) 2014-09-08 09:14:21 +02:00
Jakob Borg
0a70e0b7b6 Remove orphaned temp files before attempting to remove directories (fixes #492) 2014-09-07 21:29:06 +02:00
Jakob Borg
64ffac5671 Update goleveldb (fixes #644, closes #648) 2014-09-07 14:18:00 +02:00
Jakob Borg
ac384e8a9c Merge remote-tracking branch 'origin/pr/647'
* origin/pr/647:
  Listen for ConfigSaved event in the UI (fixes #244)
  Emit ConfigSaved event
  Save config after updating node name
2014-09-07 14:15:17 +02:00
Jakob Borg
f97c8222c7 Cleanup imports in previous 2014-09-07 14:08:49 +02:00
Jakob Borg
728289ee3a Merge remote-tracking branch 'origin/pr/646'
* origin/pr/646:
  Add session support (fixes #611)
2014-09-07 14:07:21 +02:00
Alexander Graf
5faa16f9ee use modification time for version timestamp; change version format to filename~yyyymmdd-hhmmss 2014-09-07 13:40:22 +02:00
Audrius Butkevicius
4e608b116a Add session support (fixes #611) 2014-09-07 12:10:17 +01:00
Audrius Butkevicius
521b49166e Listen for ConfigSaved event in the UI (fixes #244) 2014-09-07 12:07:25 +01:00
Audrius Butkevicius
8f32decf2d Emit ConfigSaved event 2014-09-07 12:04:40 +01:00
Audrius Butkevicius
0d51f83d2d Save config after updating node name 2014-09-07 12:04:40 +01:00
Audrius Butkevicius
78c6a68db9 Merge pull request #645 from AudriusButkevicius/cfg
Allow saving config from anywhere
2014-09-07 12:02:35 +01:00
Audrius Butkevicius
2949ab73e2 Add tests 2014-09-07 12:00:41 +01:00
Audrius Butkevicius
223741820d Fix tests 2014-09-07 12:00:41 +01:00
Audrius Butkevicius
4b57821f52 Allow saving config from anywhere 2014-09-07 12:00:37 +01:00
Audrius Butkevicius
74271a479f Silence failing ulimit calls 2014-09-06 15:04:49 +01:00
Audrius Butkevicius
c377177108 Fix tests on Windows 2014-09-06 14:56:12 +01:00
Jakob Borg
84eb729bd4 Don't start the browser on restarts (fixes #636) 2014-09-06 07:35:30 +02:00
Jakob Borg
14aea365c5 Don't stop permanently on exit (fixes #637) 2014-09-06 07:28:57 +02:00
66 changed files with 1568 additions and 892 deletions

2
Godeps/Godeps.json generated
View File

@@ -49,7 +49,7 @@
},
{
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
"Rev": "2b99e8d4757bf06eeab1b0485d80b8ae1c088874"
"Rev": "457e6f75905f7c1316afd8c43ad323f4c32b31c2"
},
{
"ImportPath": "github.com/vitrun/qart/coding",

View File

@@ -128,7 +128,8 @@ const (
type nodeState int
const (
nodeEffective nodeState = iota
nodeZero nodeState = iota
nodeEffective
nodeEvicted
nodeDeleted
)

View File

@@ -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()
}
}

View File

@@ -13,6 +13,13 @@ import (
"github.com/syndtr/goleveldb/leveldb/util"
)
// The LLRB implementation were taken from https://github.com/petar/GoLLRB,
// which conatins 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
@@ -74,11 +81,7 @@ 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
}
@@ -130,130 +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
n.value = value
n.charge = charge
n.state = nodeEvicted
ns.lru.size += charge
ns.lru.alive++
if charge > 0 {
node.ref++
node.rInsert(&ns.lru.recent)
ns.lru.used += charge
ns.lru.evict()
}
}
ns.lru.mu.Unlock()
return &lruHandle{node: node}
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++
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")
}
}
}
}
@@ -265,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() {
@@ -293,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{}
@@ -344,7 +486,7 @@ 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()
@@ -380,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
}

View File

@@ -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

View File

@@ -21,7 +21,7 @@ var _ = testutil.Defer(func() {
BlockRestartInterval: 5,
BlockSize: 50,
Compression: opt.NoCompression,
MaxOpenFiles: 0,
CachedOpenFiles: -1,
Strict: opt.StrictAll,
WriteBuffer: 1000,
}

View File

@@ -24,7 +24,7 @@ const (
DefaultBlockRestartInterval = 16
DefaultBlockSize = 4 * KiB
DefaultCompressionType = SnappyCompression
DefaultMaxOpenFiles = 1000
DefaultCachedOpenFiles = 500
DefaultWriteBuffer = 4 * MiB
)
@@ -125,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.
@@ -165,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
@@ -213,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
@@ -248,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

View File

@@ -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

View File

@@ -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}

View File

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -5,13 +5,10 @@
package main
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"mime"
"net"
"net/http"
@@ -44,7 +41,6 @@ var (
configInSync = true
guiErrors = []guiError{}
guiErrorsMut sync.Mutex
apiKey string
modt = time.Now().UTC().Format(http.TimeFormat)
eventSub *events.BufferedSubscription
)
@@ -88,9 +84,6 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
}
}
apiKey = cfg.APIKey
loadCsrfTokens()
// The GET handlers
getRestMux := http.NewServeMux()
getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion))
@@ -141,14 +134,14 @@ 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)
handler = basicAuthAndSessionMiddleware(cfg, handler)
}
go http.Serve(listener, handler)
@@ -361,8 +354,9 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
// Activate and save
newCfg.Location = cfg.Location
newCfg.Save()
cfg = newCfg
saveConfig()
}
}
@@ -599,56 +593,6 @@ 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()

90
cmd/syncthing/gui_auth.go Executable file
View 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)
})
}

View File

@@ -25,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
}
@@ -76,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)
@@ -134,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)
}

View File

@@ -14,7 +14,8 @@ import (
)
func init() {
if os.Getenv("STHEAPPROFILE") != "" {
if innerProcess && os.Getenv("STHEAPPROFILE") != "" {
l.Debugln("Starting heap profiling")
go saveHeapProfiles()
}
}

View 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
}

View File

@@ -16,7 +16,6 @@ import (
"net/http"
_ "net/http/pprof"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
@@ -34,7 +33,6 @@ 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"
@@ -53,7 +51,15 @@ var (
GoArchExtra string // "", "v5", "v6", "v7"
)
const (
exitSuccess = 0
exitError = 1
exitNoUpgradeAvailable = 2
exitRestarting = 3
)
var l = logger.DefaultLogger
var innerProcess = os.Getenv("STNORESTART") != ""
func init() {
if Version != "unknown-dev" {
@@ -76,17 +82,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 (
@@ -153,16 +158,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")
@@ -217,7 +226,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)
@@ -234,12 +243,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)
}
@@ -248,11 +272,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
@@ -292,21 +314,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()
@@ -318,7 +333,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",
@@ -343,19 +358,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)
@@ -380,17 +386,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"), &opt.Options{MaxOpenFiles: 100})
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?")
}
@@ -525,6 +534,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 {
@@ -586,9 +603,10 @@ nextRepo:
events.Default.Log(events.StartupComplete, nil)
go generateEvents()
<-stop
code := <-stop
l.Okln("Exiting")
os.Exit(code)
}
func generateEvents() {
@@ -598,25 +616,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])
@@ -746,74 +745,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) {
@@ -869,15 +806,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 {
@@ -1133,16 +1075,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

167
cmd/syncthing/monitor.go Normal file
View File

@@ -0,0 +1,167 @@
// 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
}
}
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/syncting/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)
}
}

View File

@@ -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()))
}
}

View File

@@ -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,6 +119,7 @@ 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"`
@@ -227,14 +230,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 +280,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 +329,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 +385,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
}

View File

@@ -5,8 +5,6 @@
package config
import (
"bytes"
"io"
"os"
"reflect"
"testing"
@@ -33,6 +31,7 @@ func TestDefaultValues(t *testing.T) {
LocalAnnMCAddr: "[ff32::5222]:21026",
ParallelRequests: 16,
MaxSendKbps: 0,
MaxRecvKbps: 0,
ReconnectIntervalS: 60,
StartBrowser: true,
UPnPEnabled: true,
@@ -40,10 +39,7 @@ func TestDefaultValues(t *testing.T) {
UPnPRenewal: 30,
}
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 +47,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 +101,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 +113,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,6 +122,7 @@ func TestOverriddenValues(t *testing.T) {
LocalAnnMCAddr: "quux:3232",
ParallelRequests: 32,
MaxSendKbps: 1234,
MaxRecvKbps: 2341,
ReconnectIntervalS: 6000,
StartBrowser: false,
UPnPEnabled: false,
@@ -236,7 +130,7 @@ func TestOverriddenValues(t *testing.T) {
UPnPRenewal: 15,
}
cfg, err := Load(bytes.NewReader(data), node1)
cfg, err := Load("testdata/overridenvalues.xml", node1)
if err != nil {
t.Error(err)
}
@@ -247,19 +141,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 +165,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 +176,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 +197,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 +208,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 +229,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
View 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
View 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
View File

@@ -0,0 +1,5 @@
<configuration version="1">
<options>
<listenAddress></listenAddress>
</options>
</configuration>

19
config/testdata/overridenvalues.xml vendored Executable file
View File

@@ -0,0 +1,19 @@
<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>
</options>
</configuration>

20
config/testdata/v1.xml vendored Executable file
View 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
View 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
View 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
View 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
View 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>

View File

@@ -65,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)
@@ -75,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)
@@ -83,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)
@@ -102,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 {

View File

@@ -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"
}

View File

@@ -678,6 +678,9 @@ outer:
if cont := fn(gf); !cont {
return
}
// This file is handled, no need to look further in the version list
continue outer
}
}
}

View File

@@ -782,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 {

View File

@@ -237,6 +237,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 +260,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 +359,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);
});
@@ -477,17 +488,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 +521,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 +554,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(); });

View File

@@ -30,17 +30,14 @@
<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>&emsp;
<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>&emsp;
<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&nbsp;<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>&emsp;<span translate>Add Repository</span></a></li>
<li><a href="" ng-click="addNode()"><span class="glyphicon glyphicon-retweet"></span>&emsp;<span translate>Add Node</span></a></li>
<li class="divider"></li>
<li><a href="" ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span>&emsp;<span translate>Settings</span></a></li>
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span>&emsp;<span translate>Show ID</span></a></li>
<li class="divider"></li>
@@ -82,199 +79,205 @@
<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>&emsp;{{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>&emsp;<span translate>Repository ID</span></th>
<td class="text-right">{{repo.ID}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-folder-open"></span>&emsp;<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>&emsp;<span translate>Error</span></th>
<td class="text-right">{{model[repo.ID].invalid}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-globe"></span>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<span translate>Repository ID</span></th>
<td class="text-right">{{repo.ID}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-folder-open"></span>&emsp;<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>&emsp;<span translate>Error</span></th>
<td class="text-right">{{model[repo.ID].invalid}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-globe"></span>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<span translate>Rescan Interval</span></th>
<td class="text-right">{{repo.RescanIntervalS}} s</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-share-alt"></span>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<span translate>Rescan Interval</span></th>
<td class="text-right">{{repo.RescanIntervalS}} s</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-share-alt"></span>&emsp;<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>&emsp;<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>&emsp;<span translate>Rescan</span></a>
<a class="btn btn-sm btn-primary" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span>&emsp;<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>&emsp;<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>&emsp;<span translate>Rescan</span></button>
<button class="btn btn-sm btn-default" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span>&emsp;<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>&emsp;<span translate>Add Repository</span></button>
</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>&emsp;<span translate>RAM Utilization</span></th>
<td class="text-right">{{system.sys | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-tasks"></span>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;{{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>&emsp;<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>&emsp;<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>&emsp;<span translate>RAM Utilization</span></th>
<td class="text-right">{{system.sys | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-dashboard"></span>&emsp;<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>&emsp;<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>&emsp;<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">
<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>
<h3 class="panel-title" data-toggle="collapse" data-parent="#nodes" href="#node-{{$index}}" style="cursor: pointer">
<span class="glyphicon glyphicon-retweet"></span>&emsp;{{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>&emsp;<span translate>Address</span></th>
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-comment"></span>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>&emsp;<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>
</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>&emsp;<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>&emsp;<span translate>Add Node</span></button>
</div>
</div> <!-- /row -->
@@ -387,9 +390,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>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<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>&emsp;<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>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<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>&emsp;<span translate>Delete</span></button>
</div>
</div>
</div>
@@ -513,9 +516,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>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<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>&emsp;<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>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<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>&emsp;<span translate>Delete</span></button>
</div>
</div>
</div>
@@ -534,28 +537,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>
@@ -600,7 +597,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>
@@ -632,8 +629,8 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="saveSettings()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
</div>
</div>
</div>
@@ -648,14 +645,14 @@
<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>&emsp;<span translate>Yes</span></button>
<button type="button" class="btn btn-danger" ng-click="declineUR()"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>No</span></button>
<button type="button" class="btn btn-success btn-sm" ng-click="acceptUR()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Yes</span></button>
<button type="button" class="btn btn-danger btn-sm" ng-click="declineUR()"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>No</span></button>
</div>
</div>
</div>
@@ -692,7 +689,7 @@
<li>Brandon Philips</li>
<li>Gilli Sigurdsson</li>
</ul>
</div>
</div>
<div class="col-md-6">
<ul>
<li>James Patterson</li>
@@ -704,7 +701,7 @@
<li>Tully Robinson</li>
<li>Veeti Paananen</li>
</ul>
</div>
</div>
</div>
<hr/>

View File

@@ -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": "Най-новата Версия",

View File

@@ -37,6 +37,7 @@
"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ó",

View File

@@ -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",

View File

@@ -37,6 +37,7 @@
"Global Repository": "Globales Verzeichnis",
"Idle": "Untätig",
"Ignore Permissions": "Berechtigungen ignorieren",
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
"Keep Versions": "Versionen erhalten",
"Last seen": "Zuletzt online",
"Latest Release": "Letzte Veröffentlichung",

View File

@@ -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": "Τελευταία Έκδοση",

View File

@@ -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",

View File

@@ -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",

View File

@@ -37,6 +37,7 @@
"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 apparition",
"Latest Release": "Dernière version",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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)": "Incoming Rate Limit (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",

View File

@@ -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": "Последняя версия",

View File

@@ -37,6 +37,7 @@
"Global Repository": "Global lagring",
"Idle": "Vilande",
"Ignore Permissions": "Ignorera filrättigheter",
"Incoming Rate Limit (KiB/s)": "Max nerladdningshastighet (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).",

View File

@@ -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",

View File

@@ -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": "Останній реліз",

View File

@@ -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": "最新版本",

View File

@@ -37,6 +37,7 @@
"Global Repository": "全域儲存庫",
"Idle": "閒置",
"Ignore Permissions": "忽略權限",
"Incoming Rate Limit (KiB/s)": "連入速率限制 (KiB/s)",
"Keep Versions": "保留版本數",
"Last seen": "最後發現時間",
"Latest Release": "最新發佈",

View File

@@ -10,7 +10,7 @@
<div class="modal-body" ng-transclude>
</div>
<div ng-if="close" class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
</div>
</div>
</div>

View File

@@ -1,80 +1,89 @@
body {
padding-bottom: 70px;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
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;
}
h1, h2, h3, h4, h5 {
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
ul+h5 {
margin-top: 1.5em;
}
ul+h5 {
margin-top: 1.5em;
}
.text-monospace {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
.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;
}
.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;
}
.logo {
margin: 0;
padding: 0;
top: -5px;
position: relative;
}
.list-no-bullet {
list-style-type: none
}
.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;
}
.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;
}
.ng-cloak {
display: none !important;
}
.table th {
white-space: nowrap;
font-weight: 400;
}
.table th {
white-space: nowrap;
font-weight: 400;
}
.table td {
padding-left: 20px !important;
}
.table td {
padding-left: 20px !important;
}
.table td.small-data {
white-space: nowrap;
}
.table td.small-data {
white-space: nowrap;
}
table.table-condensed {
table-layout: fixed;
}
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 {
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. */
* columns. */
white-space: normal;
}
}
}
.navbar-right {
/* to align with panel */
padding-right: 15px;
}
.panel-body .table-condensed {
margin-bottom: 0;
}

View File

@@ -73,13 +73,13 @@ func TestExcludes(t *testing.T) {
{"ign2", true},
{"ibla2", true},
{"iex2", false},
{"ign1/ign", true},
{"ign1/ex", false},
{"ign1/iex2", false},
{"iex2/ign", false},
{"foo/bar/ign1", true},
{"foo/bar/ign2", true},
{"foo/bar/iex2", false},
{filepath.Join("ign1", "ign"), true},
{filepath.Join("ign1", "ex"), false},
{filepath.Join("ign1", "iex2"), false},
{filepath.Join("iex2", "ign"), false},
{filepath.Join("foo", "bar", "ign1"), true},
{filepath.Join("foo", "bar", "ign2"), true},
{filepath.Join("foo", "bar", "iex2"), false},
}
for _, tc := range tests {

View File

@@ -2,7 +2,7 @@
set -euo pipefail
IFS=$'\n\t'
#go test -tags integration -v
./test-http.sh
./test-merge.sh
./test-delupd.sh
go test -tags integration -v

View File

@@ -19,6 +19,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"time"
)
@@ -54,7 +55,11 @@ func (p *syncthingProcess) start() error {
}
func (p *syncthingProcess) stop() {
p.cmd.Process.Kill()
if runtime.GOOS != "windows" {
p.cmd.Process.Signal(os.Interrupt)
} else {
p.cmd.Process.Kill()
}
p.cmd.Wait()
}

View File

@@ -24,13 +24,6 @@ var env = []string{
"STTRACE=model",
}
func TestRestartBothDuringTransfer(t *testing.T) {
// Give the receiver some time to rot with needed files but
// without any peer. This triggers
// https://github.com/syncthing/syncthing/issues/463
testRestartDuringTransfer(t, true, true, 10*time.Second, 0)
}
func TestRestartReceiverDuringTransfer(t *testing.T) {
testRestartDuringTransfer(t, false, true, 0, 0)
}
@@ -39,6 +32,13 @@ func TestRestartSenderDuringTransfer(t *testing.T) {
testRestartDuringTransfer(t, true, false, 0, 0)
}
func TestRestartSenderAndReceiverDuringTransfer(t *testing.T) {
// // Give the receiver some time to rot with needed files but
// // without any peer. This triggers
// // https://github.com/syncthing/syncthing/issues/463
testRestartDuringTransfer(t, true, true, 10*time.Second, 0)
}
func testRestartDuringTransfer(t *testing.T, restartSender, restartReceiver bool, senderDelay, receiverDelay time.Duration) {
log.Println("Cleaning...")
err := removeAll("s1", "s2", "f1/index", "f2/index")
@@ -134,6 +134,9 @@ func testRestartDuringTransfer(t *testing.T, restartSender, restartReceiver bool
time.Sleep(1 * time.Second)
}
sender.stop()
receiver.stop()
log.Println("Comparing directories...")
err = compareDirectories("s1", "s2")
if err != nil {

View File

@@ -5,6 +5,7 @@
package model
import (
"crypto/tls"
"errors"
"fmt"
"io"
@@ -451,6 +452,7 @@ func (m *Model) ClusterConfig(nodeID protocol.NodeID, config protocol.ClusterCon
node := m.cfg.GetNodeConfiguration(nodeID)
if node != nil && node.Name == "" {
node.Name = name
m.cfg.Save()
}
}
}
@@ -473,6 +475,13 @@ func (m *Model) Close(node protocol.NodeID, err error) {
conn, ok := m.rawConn[node]
if ok {
if conn, ok := conn.(*tls.Conn); ok {
// If the underlying connection is a *tls.Conn, Close() does more
// than it says on the tin. Specifically, it sends a TLS alert
// message, which might block forever if the connection is dead
// and we don't have a deadline site.
conn.SetWriteDeadline(time.Now().Add(250 * time.Millisecond))
}
conn.Close()
}
delete(m.protoConn, node)
@@ -566,7 +575,9 @@ func (m *Model) ConnectedTo(nodeID protocol.NodeID) bool {
m.pmut.RLock()
_, ok := m.protoConn[nodeID]
if ok {
m.nodeStatRefs[nodeID].WasSeen()
if statRef, ok := m.nodeStatRefs[nodeID]; ok {
statRef.WasSeen()
}
}
m.pmut.RUnlock()
return ok

View File

@@ -266,7 +266,7 @@ func TestNodeRename(t *testing.T) {
ClientVersion: "v0.9.4",
}
cfg, _ := config.Load(nil, node1)
cfg := config.New("test", node1)
cfg.Nodes = []config.NodeConfiguration{
{
NodeID: node1,

View File

@@ -222,7 +222,7 @@ func (p *puller) run() {
if changed {
p.model.setState(p.repoCfg.ID, RepoCleaning)
p.fixupDirectories()
p.clean()
changed = false
}
@@ -261,7 +261,9 @@ func (p *puller) runRO() {
}
}
func (p *puller) fixupDirectories() {
// clean deletes orphaned temporary files and directories that should no
// longer exist.
func (p *puller) clean() {
var deleteDirs []string
var changed = 0
@@ -270,6 +272,10 @@ func (p *puller) fixupDirectories() {
return err
}
if info.Mode().IsRegular() && defTempNamer.IsTemporary(path) {
os.Remove(path)
}
if !info.IsDir() {
return nil
}

View File

@@ -9,7 +9,6 @@ import (
"path/filepath"
"sort"
"strconv"
"time"
"github.com/syncthing/syncthing/osutil"
)
@@ -46,7 +45,7 @@ func NewSimple(repoID, repoPath string, params map[string]string) Versioner {
// Move away the named file to a version archive. If this function returns
// nil, the named file does not exist any more (has been archived).
func (v Simple) Archive(filePath string) error {
_, err := os.Stat(filePath)
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
if debug {
@@ -88,7 +87,7 @@ func (v Simple) Archive(filePath string) error {
return err
}
ver := file + "~" + time.Now().Format("20060102-150405")
ver := file + "~" + fileInfo.ModTime().Format("20060102-150405")
dst := filepath.Join(dir, ver)
if debug {
l.Debugln("moving to", dst)
@@ -98,7 +97,7 @@ func (v Simple) Archive(filePath string) error {
return err
}
versions, err := filepath.Glob(filepath.Join(dir, file+"~*"))
versions, err := filepath.Glob(filepath.Join(dir, file+"~[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]"))
if err != nil {
l.Warnln("globbing:", err)
return nil

View File

@@ -5,7 +5,6 @@
package versioner
import (
"fmt"
"os"
"path/filepath"
"sort"
@@ -46,6 +45,40 @@ func isFile(path string) bool {
return fileInfo.Mode().IsRegular()
}
const TimeLayout = "20060102-150405"
func versionExt(path string) string {
pathSplit := strings.Split(path, "~")
if len(pathSplit) > 1 {
return pathSplit[len(pathSplit)-1]
} else {
return ""
}
}
// Rename versions with old version format
func (v Staggered) renameOld() {
err := filepath.Walk(v.versionsPath, func(path string, f os.FileInfo, err error) error {
if f.Mode().IsRegular() {
versionUnix, err := strconv.ParseInt(strings.Replace(filepath.Ext(path), ".v", "", 1), 10, 0)
if err == nil {
l.Infoln("Renaming file", path, "from old to new version format")
versiondate := time.Unix(versionUnix, 0)
name := path[:len(path)-len(filepath.Ext(path))]
err = osutil.Rename(path, name+"~"+versiondate.Format(TimeLayout))
if err != nil {
l.Infoln("Error renaming to new format", err)
}
}
}
return nil
})
if err != nil {
l.Infoln("Versioner: error scanning versions dir", err)
return
}
}
// The constructor function takes a map of parameters and creates the type.
func NewStaggered(repoID, repoPath string, params map[string]string) Versioner {
maxAge, err := strconv.ParseInt(params["maxAge"], 10, 0)
@@ -89,6 +122,9 @@ func NewStaggered(repoID, repoPath string, params map[string]string) Versioner {
l.Debugf("instantiated %#v", s)
}
// Rename version with old version format
s.renameOld()
go func() {
s.clean()
for _ = range time.Tick(time.Duration(cleanInterval) * time.Second) {
@@ -134,9 +170,9 @@ func (v Staggered) clean() {
filesPerDir[dir]++
}
case mode.IsRegular():
extension := filepath.Ext(path)
extension := versionExt(path)
dir := filepath.Dir(path)
name := path[:len(path)-len(extension)]
name := path[:len(path)-len(extension)-1]
filesPerDir[dir]++
versionsPerFile[name] = append(versionsPerFile[name], path)
@@ -158,7 +194,7 @@ func (v Staggered) clean() {
if numFiles > 0 {
continue
}
if path == v.versionsPath {
if debug {
l.Debugln("Cleaner: versions dir is empty, don't delete", path)
@@ -166,7 +202,6 @@ func (v Staggered) clean() {
continue
}
if debug {
l.Debugln("Cleaner: deleting empty directory", path)
}
@@ -184,17 +219,16 @@ func (v Staggered) expire(versions []string) {
if debug {
l.Debugln("Versioner: Expiring versions", versions)
}
now := time.Now().Unix()
var prevAge int64
firstFile := true
for _, file := range versions {
if isFile(file) {
versiondate, err := strconv.ParseInt(strings.Replace(filepath.Ext(file), ".v", "", 1), 10, 0)
versionTime, err := time.Parse(TimeLayout, versionExt(file))
if err != nil {
l.Infof("Versioner: file name %q is invalid: %v", file, err)
continue
}
age := now - versiondate
age := int64(time.Since(versionTime).Seconds())
// If the file is older than the max age of the last interval, remove it
if lastIntv := v.interval[len(v.interval)-1]; lastIntv.end > 0 && age > lastIntv.end {
@@ -250,7 +284,7 @@ func (v Staggered) Archive(filePath string) error {
v.mutex.Lock()
defer v.mutex.Unlock()
_, err := os.Stat(filePath)
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
if debug {
@@ -291,7 +325,7 @@ func (v Staggered) Archive(filePath string) error {
return err
}
ver := file + ".v" + fmt.Sprintf("%010d", time.Now().Unix())
ver := file + "~" + fileInfo.ModTime().Format(TimeLayout)
dst := filepath.Join(dir, ver)
if debug {
l.Debugln("moving to", dst)
@@ -301,7 +335,7 @@ func (v Staggered) Archive(filePath string) error {
return err
}
versions, err := filepath.Glob(filepath.Join(dir, file+".v[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]"))
versions, err := filepath.Glob(filepath.Join(dir, file+"~[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]"))
if err != nil {
l.Warnln("Versioner: error finding versions for", file, err)
return nil