Compare commits
103 Commits
v0.12.0-rc2
...
v0.12.3
| Author | SHA1 | Date | |
|---|---|---|---|
| ad8c266f76 | |||
| 807c3bdcc7 | |||
| 62efbd17de | |||
| d99350fd61 | |||
| 40c70a9a2a | |||
| 8fb7f40a6a | |||
| 2f12d41d9d | |||
| 9fbdb6b305 | |||
| 73285cadb6 | |||
| 56f1c295b6 | |||
| 7f76ed8413 | |||
| f7edd36931 | |||
| c39f2b7c05 | |||
| 342036408e | |||
| f4904fce17 | |||
| 2abb2de753 | |||
| ef0a0db07e | |||
| a45795efec | |||
| 88ae353aef | |||
| 97b9690711 | |||
| 48ce356d5c | |||
| 92451f94bc | |||
| 593632045d | |||
| e3c55ef307 | |||
| edf1730fd2 | |||
| 34cd8e3f95 | |||
| 242cce022a | |||
| 19bf51cefb | |||
| 74a2e80142 | |||
| b9b630e3b6 | |||
| f0bdf833d1 | |||
| 81bc6bf34b | |||
| 7de736e8d0 | |||
| cd097e0fce | |||
| cc81a7ccfe | |||
| 58d320c270 | |||
| 59565fd1d1 | |||
| 55592137a2 | |||
| 58523060f0 | |||
| 07d53be9fc | |||
| d4b0235a8b | |||
| 34aa41e17b | |||
| 36f6a9347c | |||
| d49d386ef2 | |||
| 00c363829c | |||
| a9691dbdf4 | |||
| 9df701906f | |||
| 283671fa9d | |||
| 435c29755d | |||
| 686f91777c | |||
| 2aa028facb | |||
| b4bbd050c2 | |||
| 2a4fc28318 | |||
| 313485e406 | |||
| faf4267c73 | |||
| e6277d799f | |||
| cdbc8004fb | |||
| 1fac2f686d | |||
| 08c8d679ac | |||
| 48c34b7234 | |||
| 28603f0d2c | |||
| b2855f02fe | |||
| bef3d88076 | |||
| e1a8ea7dec | |||
| c4ad97136f | |||
| eab1d6782b | |||
| fd7b8ec77e | |||
| e28c991331 | |||
| a52811dfa3 | |||
| 9e210d705d | |||
| c42f1b53ab | |||
| d171173e90 | |||
| 679f0f9363 | |||
| 724c1e297f | |||
| 83154569b1 | |||
| e3c0fba34b | |||
| 2b6a6b91f3 | |||
| 09a555fdd2 | |||
| dc32f7f0a3 | |||
| 1efd8d6c75 | |||
| 898fc72313 | |||
| 21c5806cbf | |||
| 464e6bec95 | |||
| 2ae832d919 | |||
| 5b03c2d949 | |||
| f629a998a0 | |||
| fe88781bc8 | |||
| e725c97967 | |||
| 63caf22671 | |||
| 44790b1333 | |||
| b40bb64612 | |||
| 7b5ab29a6d | |||
| 4fd614be09 | |||
| 73236e58c5 | |||
| 32414853c6 | |||
| f3dc78d457 | |||
| a32ac62208 | |||
| d7a934cf0e | |||
| 503491392d | |||
| b3a2bf367b | |||
| c19eff4872 | |||
| 2941a813c2 | |||
| 0a022d38fa |
@@ -36,6 +36,7 @@ Frank Isemann <frank@isemann.name>
|
||||
Gilli Sigurdsson <gilli@vx.is>
|
||||
Jacek Szafarkiewicz <szafar@linux.pl>
|
||||
Jakob Borg <jakob@nym.se>
|
||||
Jake Peterson <jake@acogdev.com>
|
||||
James Patterson <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
||||
Jaroslav Malec <dzardacz@gmail.com>
|
||||
Jens Diemer <github.com@jensdiemer.de> <git@jensdiemer.de>
|
||||
@@ -60,10 +61,12 @@ Piotr Bejda <piotrb10@gmail.com>
|
||||
Ryan Sullivan <kayoticsully@gmail.com>
|
||||
Sergey Mishin <ralder@yandex.ru>
|
||||
Stefan Tatschner <stefan@sevenbyte.org> <rumpelsepp@sevenbyte.org>
|
||||
Stefan Kuntz <stefan.github@gmail.com> <Stefan.github@gmail.com>
|
||||
Tim Abell <tim@timwise.co.uk>
|
||||
Tobias Nygren <tnn@nygren.pp.se>
|
||||
Tomas Cerveny <kozec@kozec.com>
|
||||
Tully Robinson <tully@tojr.org>
|
||||
Tyler Brazier <tyler@tylerbrazier.com>
|
||||
Veeti Paananen <veeti.paananen@rojekti.fi>
|
||||
Vil Brekin <vilbrekin@gmail.com>
|
||||
Yannic A. <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>
|
||||
|
||||
@@ -7,8 +7,10 @@ LordLandon <lordlandon@gmail.com>
|
||||
Moter8 <moter8@gmail.com>
|
||||
Nutomic <me@nutomic.com>
|
||||
Rewt0r <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
||||
Stefan-Code <stefan.github@gmail.com> <Stefan.github@gmail.com>
|
||||
Vilbrekin <vilbrekin@gmail.com>
|
||||
Zillode <zillode@zillode.be>
|
||||
acogdev <jake@acogdev.com>
|
||||
alex2108 <register-github@alex-graf.de>
|
||||
andrew-d <andrew@du.nham.ca>
|
||||
asdil12 <dominik@heidler.eu>
|
||||
@@ -63,6 +65,7 @@ simplypeachy <aD@simplypeachy.co.uk> <simplypeachy@users.noreply.github.com>
|
||||
timabell <tim@timwise.co.uk>
|
||||
tnn2 <tnn@nygren.pp.se>
|
||||
tojrobinson <tully@tojr.org>
|
||||
tylerbrazier <tyler@tylerbrazier.com>
|
||||
uok <ueomkail@gmail.com> <uok@users.noreply.github.com>
|
||||
veeti <veeti.paananen@rojekti.fi>
|
||||
wsgcsysadmin <e.meitner@willystreet.coo>
|
||||
|
||||
@@ -108,7 +108,7 @@ case "${1:-default}" in
|
||||
# For every package in the repo
|
||||
for dir in $(go list ./...) ; do
|
||||
# run the tests
|
||||
godep go test -race -coverprofile=profile.out $dir
|
||||
GOPATH="$(pwd)/Godeps/_workspace:$GOPATH" go test -race -coverprofile=profile.out $dir
|
||||
if [ -f profile.out ] ; then
|
||||
# and if there was test output, append it to coverage.out
|
||||
grep -v "mode: " profile.out >> coverage.out
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
func dump(ldb *leveldb.DB) {
|
||||
it := ldb.NewIterator(nil, nil)
|
||||
var dev protocol.DeviceID
|
||||
for it.Next() {
|
||||
key := it.Key()
|
||||
switch key[0] {
|
||||
case db.KeyTypeDevice:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
devBytes := key[1+64 : 1+64+32]
|
||||
name := nulString(key[1+64+32:])
|
||||
copy(dev[:], devBytes)
|
||||
fmt.Printf("[device] F:%q N:%q D:%v\n", folder, name, dev)
|
||||
|
||||
var f protocol.FileInfo
|
||||
err := f.UnmarshalXDR(it.Value())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf(" N:%q\n F:%#o\n M:%d\n V:%v\n S:%d\n B:%d\n", f.Name, f.Flags, f.Modified, f.Version, f.Size(), len(f.Blocks))
|
||||
|
||||
case db.KeyTypeGlobal:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
name := nulString(key[1+64:])
|
||||
fmt.Printf("[global] F:%q N:%q V:%x\n", folder, name, it.Value())
|
||||
|
||||
case db.KeyTypeBlock:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
hash := key[1+64 : 1+64+32]
|
||||
name := nulString(key[1+64+32:])
|
||||
fmt.Printf("[block] F:%q H:%x N:%q I:%d\n", folder, hash, name, binary.BigEndian.Uint32(it.Value()))
|
||||
|
||||
case db.KeyTypeDeviceStatistic:
|
||||
fmt.Printf("[dstat]\n %x\n %x\n", it.Key(), it.Value())
|
||||
|
||||
case db.KeyTypeFolderStatistic:
|
||||
fmt.Printf("[fstat]\n %x\n %x\n", it.Key(), it.Value())
|
||||
|
||||
case db.KeyTypeVirtualMtime:
|
||||
fmt.Printf("[mtime]\n %x\n %x\n", it.Key(), it.Value())
|
||||
|
||||
default:
|
||||
fmt.Printf("[???]\n %x\n %x\n", it.Key(), it.Value())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
// An IntHeap is a min-heap of ints.
|
||||
type SizedElement struct {
|
||||
key string
|
||||
size int
|
||||
}
|
||||
|
||||
type ElementHeap []SizedElement
|
||||
|
||||
func (h ElementHeap) Len() int { return len(h) }
|
||||
func (h ElementHeap) Less(i, j int) bool { return h[i].size > h[j].size }
|
||||
func (h ElementHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||
|
||||
func (h *ElementHeap) Push(x interface{}) {
|
||||
*h = append(*h, x.(SizedElement))
|
||||
}
|
||||
|
||||
func (h *ElementHeap) Pop() interface{} {
|
||||
old := *h
|
||||
n := len(old)
|
||||
x := old[n-1]
|
||||
*h = old[0 : n-1]
|
||||
return x
|
||||
}
|
||||
|
||||
func dumpsize(ldb *leveldb.DB) {
|
||||
h := &ElementHeap{}
|
||||
heap.Init(h)
|
||||
|
||||
it := ldb.NewIterator(nil, nil)
|
||||
var dev protocol.DeviceID
|
||||
var ele SizedElement
|
||||
for it.Next() {
|
||||
key := it.Key()
|
||||
switch key[0] {
|
||||
case db.KeyTypeDevice:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
devBytes := key[1+64 : 1+64+32]
|
||||
name := nulString(key[1+64+32:])
|
||||
copy(dev[:], devBytes)
|
||||
ele.key = fmt.Sprintf("DEVICE:%s:%s:%s", dev, folder, name)
|
||||
|
||||
case db.KeyTypeGlobal:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
name := nulString(key[1+64:])
|
||||
ele.key = fmt.Sprintf("GLOBAL:%s:%s", folder, name)
|
||||
|
||||
case db.KeyTypeBlock:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
hash := key[1+64 : 1+64+32]
|
||||
name := nulString(key[1+64+32:])
|
||||
ele.key = fmt.Sprintf("BLOCK:%s:%x:%s", folder, hash, name)
|
||||
|
||||
case db.KeyTypeDeviceStatistic:
|
||||
ele.key = fmt.Sprintf("DEVICESTATS:%s", key[1:])
|
||||
|
||||
case db.KeyTypeFolderStatistic:
|
||||
ele.key = fmt.Sprintf("FOLDERSTATS:%s", key[1:])
|
||||
|
||||
case db.KeyTypeVirtualMtime:
|
||||
ele.key = fmt.Sprintf("MTIME:%s", key[1:])
|
||||
|
||||
default:
|
||||
ele.key = fmt.Sprintf("UNKNOWN:%x", key)
|
||||
}
|
||||
ele.size = len(it.Value())
|
||||
heap.Push(h, ele)
|
||||
}
|
||||
|
||||
for h.Len() > 0 {
|
||||
ele = heap.Pop(h).(SizedElement)
|
||||
fmt.Println(ele.key, ele.size)
|
||||
}
|
||||
}
|
||||
+18
-52
@@ -7,25 +7,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var mode string
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
flag.StringVar(&mode, "mode", "dump", "Mode of operation: dump, dumpsize")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
ldb, err := leveldb.OpenFile(flag.Arg(0), &opt.Options{
|
||||
path := flag.Arg(0)
|
||||
if path == "" {
|
||||
path = filepath.Join(defaultConfigDir(), "index-v0.11.0.db")
|
||||
}
|
||||
|
||||
fmt.Println("Path:", path)
|
||||
|
||||
ldb, err := leveldb.OpenFile(path, &opt.Options{
|
||||
ErrorIfMissing: true,
|
||||
Strict: opt.StrictAll,
|
||||
OpenFilesCacheCapacity: 100,
|
||||
@@ -34,53 +42,11 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
it := ldb.NewIterator(nil, nil)
|
||||
var dev protocol.DeviceID
|
||||
for it.Next() {
|
||||
key := it.Key()
|
||||
switch key[0] {
|
||||
case db.KeyTypeDevice:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
devBytes := key[1+64 : 1+64+32]
|
||||
name := nulString(key[1+64+32:])
|
||||
copy(dev[:], devBytes)
|
||||
fmt.Printf("[device] F:%q N:%q D:%v\n", folder, name, dev)
|
||||
|
||||
var f protocol.FileInfo
|
||||
err := f.UnmarshalXDR(it.Value())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf(" N:%q\n F:%#o\n M:%d\n V:%v\n S:%d\n B:%d\n", f.Name, f.Flags, f.Modified, f.Version, f.Size(), len(f.Blocks))
|
||||
|
||||
case db.KeyTypeGlobal:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
name := nulString(key[1+64:])
|
||||
fmt.Printf("[global] F:%q N:%q V:%x\n", folder, name, it.Value())
|
||||
|
||||
case db.KeyTypeBlock:
|
||||
folder := nulString(key[1 : 1+64])
|
||||
hash := key[1+64 : 1+64+32]
|
||||
name := nulString(key[1+64+32:])
|
||||
fmt.Printf("[block] F:%q H:%x N:%q I:%d\n", folder, hash, name, binary.BigEndian.Uint32(it.Value()))
|
||||
|
||||
case db.KeyTypeDeviceStatistic:
|
||||
fmt.Printf("[dstat]\n %x\n %x\n", it.Key(), it.Value())
|
||||
|
||||
case db.KeyTypeFolderStatistic:
|
||||
fmt.Printf("[fstat]\n %x\n %x\n", it.Key(), it.Value())
|
||||
|
||||
default:
|
||||
fmt.Printf("[???]\n %x\n %x\n", it.Key(), it.Value())
|
||||
}
|
||||
if mode == "dump" {
|
||||
dump(ldb)
|
||||
} else if mode == "dumpsize" {
|
||||
dumpsize(ldb)
|
||||
} else {
|
||||
fmt.Println("Unknown mode")
|
||||
}
|
||||
}
|
||||
|
||||
func nulString(bs []byte) string {
|
||||
for i := range bs {
|
||||
if bs[i] == 0 {
|
||||
return string(bs[:i])
|
||||
}
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
func nulString(bs []byte) string {
|
||||
for i := range bs {
|
||||
if bs[i] == 0 {
|
||||
return string(bs[:i])
|
||||
}
|
||||
}
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func defaultConfigDir() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if p := os.Getenv("LocalAppData"); p != "" {
|
||||
return filepath.Join(p, "Syncthing")
|
||||
}
|
||||
return filepath.Join(os.Getenv("AppData"), "Syncthing")
|
||||
|
||||
case "darwin":
|
||||
dir, err := osutil.ExpandTilde("~/Library/Application Support/Syncthing")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
|
||||
default:
|
||||
if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" {
|
||||
return filepath.Join(xdgCfg, "syncthing")
|
||||
}
|
||||
dir, err := osutil.ExpandTilde("~/.config/syncthing")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return dir
|
||||
}
|
||||
}
|
||||
@@ -63,13 +63,13 @@ func (e *addressLister) addresses(includePrivateIPV4 bool) []string {
|
||||
|
||||
if addr.IP == nil || addr.IP.IsUnspecified() {
|
||||
// Address like 0.0.0.0:22000 or [::]:22000 or :22000; include as is.
|
||||
addrs = append(addrs, "tcp://"+addr.String())
|
||||
addrs = append(addrs, tcpAddr(addr.String()))
|
||||
} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
|
||||
// A public address; include as is.
|
||||
addrs = append(addrs, "tcp://"+addr.String())
|
||||
addrs = append(addrs, tcpAddr(addr.String()))
|
||||
} else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() {
|
||||
// A private IPv4 address.
|
||||
addrs = append(addrs, "tcp://"+addr.String())
|
||||
addrs = append(addrs, tcpAddr(addr.String()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,3 +117,11 @@ func isPublicIPv6(ip net.IP) bool {
|
||||
|
||||
return ip.IsGlobalUnicast()
|
||||
}
|
||||
|
||||
func tcpAddr(host string) string {
|
||||
u := url.URL{
|
||||
Scheme: "tcp",
|
||||
Host: host,
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
@@ -22,11 +22,3 @@ func init() {
|
||||
l.SetDebug("main", strings.Contains(os.Getenv("STTRACE"), "main") || os.Getenv("STTRACE") == "all")
|
||||
l.SetDebug("http", strings.Contains(os.Getenv("STTRACE"), "http") || os.Getenv("STTRACE") == "all")
|
||||
}
|
||||
|
||||
func shouldDebugMain() bool {
|
||||
return l.ShouldDebug("main")
|
||||
}
|
||||
|
||||
func shouldDebugHTTP() bool {
|
||||
return l.ShouldDebug("http")
|
||||
}
|
||||
|
||||
@@ -562,8 +562,7 @@ func (s *apiSvc) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
s.systemConfigMut.Lock()
|
||||
defer s.systemConfigMut.Unlock()
|
||||
|
||||
var to config.Configuration
|
||||
err := json.NewDecoder(r.Body).Decode(&to)
|
||||
to, err := config.ReadJSON(r.Body, myID)
|
||||
if err != nil {
|
||||
l.Warnln("decoding posted config:", err)
|
||||
http.Error(w, err.Error(), 500)
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -24,6 +25,13 @@ var (
|
||||
sessionsMut = sync.NewMutex()
|
||||
)
|
||||
|
||||
func emitLoginAttempt(success bool, username string) {
|
||||
events.Default.Log(events.LoginAttempt, map[string]interface{}{
|
||||
"success": success,
|
||||
"username": username,
|
||||
})
|
||||
}
|
||||
|
||||
func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguration, next http.Handler) http.Handler {
|
||||
apiKey := cfg.APIKey()
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -70,12 +78,15 @@ func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguratio
|
||||
return
|
||||
}
|
||||
|
||||
if string(fields[0]) != cfg.User {
|
||||
username := string(fields[0])
|
||||
if username != cfg.User {
|
||||
emitLoginAttempt(false, username)
|
||||
error()
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(cfg.Password), fields[1]); err != nil {
|
||||
emitLoginAttempt(false, username)
|
||||
error()
|
||||
return
|
||||
}
|
||||
@@ -90,6 +101,7 @@ func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguratio
|
||||
MaxAge: 0,
|
||||
})
|
||||
|
||||
emitLoginAttempt(true, username)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
+26
-92
@@ -9,6 +9,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -42,9 +43,6 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
@@ -371,7 +369,7 @@ func main() {
|
||||
|
||||
if doUpgrade {
|
||||
// Use leveldb database locks to protect against concurrent upgrades
|
||||
_, err = leveldb.OpenFile(locations[locDatabase], &opt.Options{OpenFilesCacheCapacity: 100})
|
||||
_, err = db.Open(locations[locDatabase])
|
||||
if err != nil {
|
||||
l.Infoln("Attempting upgrade through running Syncthing...")
|
||||
err = upgradeViaRest()
|
||||
@@ -600,48 +598,38 @@ func syncthingMain() {
|
||||
|
||||
if (opts.MaxRecvKbps > 0 || opts.MaxSendKbps > 0) && !opts.LimitBandwidthInLan {
|
||||
lans, _ = osutil.GetLans()
|
||||
networks := make([]string, 0, len(lans))
|
||||
for _, lan := range lans {
|
||||
networks = append(networks, lan.String())
|
||||
}
|
||||
for _, lan := range opts.AlwaysLocalNets {
|
||||
_, ipnet, err := net.ParseCIDR(lan)
|
||||
if err != nil {
|
||||
l.Infoln("Network", lan, "is malformed:", err)
|
||||
continue
|
||||
}
|
||||
networks = append(networks, ipnet.String())
|
||||
lans = append(lans, ipnet)
|
||||
}
|
||||
|
||||
networks := make([]string, len(lans))
|
||||
for i, lan := range lans {
|
||||
networks[i] = lan.String()
|
||||
}
|
||||
l.Infoln("Local networks:", strings.Join(networks, ", "))
|
||||
}
|
||||
|
||||
dbFile := locations[locDatabase]
|
||||
dbOpts := dbOpts(cfg)
|
||||
ldb, err := leveldb.OpenFile(dbFile, dbOpts)
|
||||
if leveldbIsCorrupted(err) {
|
||||
ldb, err = leveldb.RecoverFile(dbFile, dbOpts)
|
||||
}
|
||||
if leveldbIsCorrupted(err) {
|
||||
// The database is corrupted, and we've tried to recover it but it
|
||||
// didn't work. At this point there isn't much to do beyond dropping
|
||||
// the database and reindexing...
|
||||
l.Infoln("Database corruption detected, unable to recover. Reinitializing...")
|
||||
if err := resetDB(); err != nil {
|
||||
l.Fatalln("Remove database:", err)
|
||||
}
|
||||
ldb, err = leveldb.OpenFile(dbFile, dbOpts)
|
||||
}
|
||||
ldb, err := db.Open(dbFile)
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
|
||||
}
|
||||
|
||||
protectedFiles := []string{
|
||||
locations[locDatabase], locations[locConfigFile], locations[locCertFile], locations[locKeyFile],
|
||||
locations[locDatabase],
|
||||
locations[locConfigFile],
|
||||
locations[locCertFile],
|
||||
locations[locKeyFile],
|
||||
}
|
||||
|
||||
// Remove database entries for folders that no longer exist in the config
|
||||
folders := cfg.Folders()
|
||||
for _, folder := range db.ListFolders(ldb) {
|
||||
for _, folder := range ldb.ListFolders() {
|
||||
if _, ok := folders[folder]; !ok {
|
||||
l.Infof("Cleaning data for dropped folder %q", folder)
|
||||
db.DropFolder(ldb, folder)
|
||||
@@ -880,40 +868,6 @@ func loadConfig(cfgFile string) (*config.Wrapper, string, error) {
|
||||
return cfg, myName, nil
|
||||
}
|
||||
|
||||
func dbOpts(cfg *config.Wrapper) *opt.Options {
|
||||
// Calculate a suitable database block cache capacity.
|
||||
|
||||
// Default is 8 MiB.
|
||||
blockCacheCapacity := 8 << 20
|
||||
// Increase block cache up to this maximum:
|
||||
const maxCapacity = 64 << 20
|
||||
// ... which we reach when the box has this much RAM:
|
||||
const maxAtRAM = 8 << 30
|
||||
|
||||
if v := cfg.Options().DatabaseBlockCacheMiB; v != 0 {
|
||||
// Use the value from the config, if it's set.
|
||||
blockCacheCapacity = v << 20
|
||||
} else if bytes, err := memorySize(); err == nil {
|
||||
// We start at the default of 8 MiB and use larger values for machines
|
||||
// with more memory.
|
||||
|
||||
if bytes > maxAtRAM {
|
||||
// Cap the cache at maxCapacity when we reach maxAtRam amount of memory
|
||||
blockCacheCapacity = maxCapacity
|
||||
} else if bytes > maxAtRAM/maxCapacity*int64(blockCacheCapacity) {
|
||||
// Grow from the default to maxCapacity at maxAtRam amount of memory
|
||||
blockCacheCapacity = int(bytes * maxCapacity / maxAtRAM)
|
||||
}
|
||||
l.Infoln("Database block cache capacity", blockCacheCapacity/1024, "KiB")
|
||||
}
|
||||
|
||||
return &opt.Options{
|
||||
OpenFilesCacheCapacity: 100,
|
||||
BlockCacheCapacity: blockCacheCapacity,
|
||||
WriteBuffer: 4 << 20,
|
||||
}
|
||||
}
|
||||
|
||||
func startAuditing(mainSvc *suture.Supervisor) {
|
||||
auditFile := timestampedLoc(locAuditLog)
|
||||
fd, err := os.OpenFile(auditFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||
@@ -953,23 +907,19 @@ func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, a
|
||||
}
|
||||
|
||||
func defaultConfig(myName string) config.Configuration {
|
||||
defaultFolder := config.NewFolderConfiguration("default", locations[locDefFolder])
|
||||
defaultFolder.RescanIntervalS = 60
|
||||
defaultFolder.MinDiskFreePct = 1
|
||||
defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}}
|
||||
defaultFolder.AutoNormalize = true
|
||||
defaultFolder.MaxConflicts = -1
|
||||
|
||||
thisDevice := config.NewDeviceConfiguration(myID, myName)
|
||||
thisDevice.Addresses = []string{"dynamic"}
|
||||
|
||||
newCfg := config.New(myID)
|
||||
newCfg.Folders = []config.FolderConfiguration{
|
||||
{
|
||||
ID: "default",
|
||||
RawPath: locations[locDefFolder],
|
||||
RescanIntervalS: 60,
|
||||
MinDiskFreePct: 1,
|
||||
Devices: []config.FolderDeviceConfiguration{{DeviceID: myID}},
|
||||
},
|
||||
}
|
||||
newCfg.Devices = []config.DeviceConfiguration{
|
||||
{
|
||||
DeviceID: myID,
|
||||
Addresses: []string{"dynamic"},
|
||||
Name: myName,
|
||||
},
|
||||
}
|
||||
newCfg.Folders = []config.FolderConfiguration{defaultFolder}
|
||||
newCfg.Devices = []config.DeviceConfiguration{thisDevice}
|
||||
|
||||
port, err := getFreePort("127.0.0.1", 8384)
|
||||
if err != nil {
|
||||
@@ -1163,19 +1113,3 @@ func checkShortIDs(cfg *config.Wrapper) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// A "better" version of leveldb's errors.IsCorrupted.
|
||||
func leveldbIsCorrupted(err error) bool {
|
||||
switch {
|
||||
case err == nil:
|
||||
return false
|
||||
|
||||
case errors.IsCorrupted(err):
|
||||
return true
|
||||
|
||||
case strings.Contains(err.Error(), "corrupted"):
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -14,9 +14,6 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
func TestFolderErrors(t *testing.T) {
|
||||
@@ -38,7 +35,7 @@ func TestFolderErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ldb, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
// Case 1 - new folder, directory and marker created
|
||||
|
||||
|
||||
+116
-9
@@ -28,8 +28,10 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
countRestarts = 4
|
||||
loopThreshold = 60 * time.Second
|
||||
countRestarts = 4
|
||||
loopThreshold = 60 * time.Second
|
||||
logFileAutoCloseDelay = 5 * time.Second
|
||||
logFileMaxOpenTime = time.Minute
|
||||
)
|
||||
|
||||
func monitorMain() {
|
||||
@@ -37,16 +39,10 @@ func monitorMain() {
|
||||
os.Setenv("STMONITORED", "yes")
|
||||
l.SetPrefix("[monitor] ")
|
||||
|
||||
var err error
|
||||
var dst io.Writer = os.Stdout
|
||||
|
||||
if logFile != "-" {
|
||||
var fileDst io.Writer
|
||||
|
||||
fileDst, err = os.Create(logFile)
|
||||
if err != nil {
|
||||
l.Fatalln("log file:", err)
|
||||
}
|
||||
var fileDst io.Writer = newAutoclosedFile(logFile, logFileAutoCloseDelay, logFileMaxOpenTime)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Translate line breaks to Windows standard
|
||||
@@ -262,3 +258,114 @@ func restartMonitorWindows(args []string) error {
|
||||
cmd.Stdin = os.Stdin
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
// An autoclosedFile is an io.WriteCloser that opens itself for appending on
|
||||
// Write() and closes itself after an interval of no writes (closeDelay) or
|
||||
// when the file has been open for too long (maxOpenTime). A call to Write()
|
||||
// will return any error that happens on the resulting Open() call too. Errors
|
||||
// on automatic Close() calls are silently swallowed...
|
||||
type autoclosedFile struct {
|
||||
name string // path to write to
|
||||
closeDelay time.Duration // close after this long inactivity
|
||||
maxOpenTime time.Duration // or this long after opening
|
||||
|
||||
fd io.WriteCloser // underlying WriteCloser
|
||||
opened time.Time // timestamp when the file was last opened
|
||||
closed chan struct{} // closed on Close(), stops the closerLoop
|
||||
closeTimer *time.Timer // fires closeDelay after a write
|
||||
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
func newAutoclosedFile(name string, closeDelay, maxOpenTime time.Duration) *autoclosedFile {
|
||||
f := &autoclosedFile{
|
||||
name: name,
|
||||
closeDelay: closeDelay,
|
||||
maxOpenTime: maxOpenTime,
|
||||
mut: sync.NewMutex(),
|
||||
closed: make(chan struct{}),
|
||||
closeTimer: time.NewTimer(time.Minute),
|
||||
}
|
||||
go f.closerLoop()
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *autoclosedFile) Write(bs []byte) (int, error) {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
|
||||
// Make sure the file is open for appending
|
||||
if err := f.ensureOpen(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// If we haven't run into the maxOpenTime, postpone close for another
|
||||
// closeDelay
|
||||
if time.Since(f.opened) < f.maxOpenTime {
|
||||
f.closeTimer.Reset(f.closeDelay)
|
||||
}
|
||||
|
||||
return f.fd.Write(bs)
|
||||
}
|
||||
|
||||
func (f *autoclosedFile) Close() error {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
|
||||
// Stop the timer and closerLoop() routine
|
||||
f.closeTimer.Stop()
|
||||
close(f.closed)
|
||||
|
||||
// Close the file, if it's open
|
||||
if f.fd != nil {
|
||||
return f.fd.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Must be called with f.mut held!
|
||||
func (f *autoclosedFile) ensureOpen() error {
|
||||
if f.fd != nil {
|
||||
// File is already open
|
||||
return nil
|
||||
}
|
||||
|
||||
// We open the file for write only, and create it if it doesn't exist.
|
||||
flags := os.O_WRONLY | os.O_CREATE
|
||||
if f.opened.IsZero() {
|
||||
// This is the first time we are opening the file. We should truncate
|
||||
// it to better emulate an os.Create() call.
|
||||
flags |= os.O_TRUNC
|
||||
} else {
|
||||
// The file was already opened once, so we should append to it.
|
||||
flags |= os.O_APPEND
|
||||
}
|
||||
|
||||
fd, err := os.OpenFile(f.name, flags, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.fd = fd
|
||||
f.opened = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *autoclosedFile) closerLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-f.closeTimer.C:
|
||||
// Close the file when the timer expires.
|
||||
f.mut.Lock()
|
||||
if f.fd != nil {
|
||||
f.fd.Close() // errors, schmerrors
|
||||
f.fd = nil
|
||||
}
|
||||
f.mut.Unlock()
|
||||
|
||||
case <-f.closed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
|
||||
|
||||
defaultAnnounceServersDNS, defaultAnnounceServersIP, otherAnnounceServers := 0, 0, 0
|
||||
for _, addr := range cfg.Options().GlobalAnnServers {
|
||||
if addr == "default" {
|
||||
if addr == "default" || addr == "default-v4" || addr == "default-v6" {
|
||||
defaultAnnounceServersDNS++
|
||||
} else if stringIn(addr, config.DefaultDiscoveryServersIP) {
|
||||
defaultAnnounceServersIP++
|
||||
@@ -201,7 +201,7 @@ func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
|
||||
defaultRelayServers, otherRelayServers := 0, 0
|
||||
for _, addr := range cfg.Options().RelayServers {
|
||||
switch addr {
|
||||
case "dynamic+https://relays.syncthing.net":
|
||||
case "dynamic+https://relays.syncthing.net/endpoint":
|
||||
defaultRelayServers++
|
||||
default:
|
||||
otherRelayServers++
|
||||
|
||||
@@ -149,6 +149,16 @@ func (s *verboseSvc) formatEvent(ev events.Event) string {
|
||||
data := ev.Data.(map[string][]string)
|
||||
newRelays := data["new"]
|
||||
return fmt.Sprintf("Relay state changed; connected relay(s) are %s.", strings.Join(newRelays, ", "))
|
||||
case events.LoginAttempt:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
username := data["username"].(string)
|
||||
var success string
|
||||
if data["success"].(bool) {
|
||||
success = "successful"
|
||||
} else {
|
||||
success = "failed"
|
||||
}
|
||||
return fmt.Sprintf("Login %s for username %s.", success, username)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Upstart Configuration
|
||||
|
||||
This directory contains example configuration files for running Syncthing under
|
||||
the "Upstart" service manager on Linux. To have syncthing start when you login
|
||||
place "user/syncthing.conf" in the "/home/[username]/.config/upstart/" folder.
|
||||
To have syncthing start when the system boots place "system/syncthing.conf"
|
||||
in the "/etc/init/" folder.
|
||||
To manualy start syncthing via Upstart when using the system configuration use:
|
||||
|
||||
```
|
||||
sudo initctl start syncthing
|
||||
```
|
||||
|
||||
For further documentation see [http://docs.syncthing.net/users/autostart.html][1].
|
||||
|
||||
[1]: http://docs.syncthing.net/users/autostart.html#Upstart
|
||||
@@ -0,0 +1,13 @@
|
||||
description "Syncthing"
|
||||
|
||||
start on (local-filesystems and net-device-up IFACE!=lo)
|
||||
stop on runlevel [!2345]
|
||||
|
||||
env STNORESTART=yes
|
||||
env HOME=/home/$USER
|
||||
setuid "$USER"
|
||||
setgid "$USER"
|
||||
|
||||
exec /usr/local/bin/syncthing
|
||||
|
||||
respawn
|
||||
@@ -0,0 +1,21 @@
|
||||
# Location of the syncthing executable
|
||||
env SYNCTHING_EXE="/usr/local/bin"
|
||||
|
||||
# Set the name of the application
|
||||
description "Syncthing"
|
||||
|
||||
# Start syncthing you login to your desktop
|
||||
start on desktop-start
|
||||
|
||||
# Stop syncthing you logout of your desktop
|
||||
stop on desktop-end
|
||||
|
||||
# Set STNORESTART to yes to have Upstart monitor the process instead
|
||||
# of having a separate syncthing process do the monitoring
|
||||
env STNORESTART=yes
|
||||
|
||||
# If Upstart detects syncthing has failed - it should restart it
|
||||
respawn
|
||||
|
||||
# the syncthing command Upstart is to execute when it is started up
|
||||
exec $SYNCTHING_EXE -no-browser
|
||||
Binary file not shown.
@@ -2,5 +2,5 @@
|
||||
font-family: 'Raleway';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: local('Raleway'), url(raleway-500.woff) format('woff');
|
||||
src: local('Raleway Medium'), local('Raleway-Medium'), url(raleway-500.woff) format('woff');
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"A negative number of days doesn't make sense.": "Няма логика на зададен отрицателен брой дни.",
|
||||
"A negative number of days doesn't make sense.": "Няма логика в задаването на отрицателен брой дни.",
|
||||
"A new major version may not be compatible with previous versions.": "Нова основна версия, която може да не е съвмеситима с предишни версии.",
|
||||
"API Key": "API Ключ",
|
||||
"About": "За Програмата",
|
||||
"About": "За програмата",
|
||||
"Actions": "Действия",
|
||||
"Add": "Добави",
|
||||
"Add Device": "Добави устройство",
|
||||
@@ -13,36 +13,36 @@
|
||||
"Advanced": "Допълнителни",
|
||||
"Advanced Configuration": "Допълнителни настройки",
|
||||
"All Data": "Всички данни",
|
||||
"Allow Anonymous Usage Reporting?": "Разреши анонимен доклад за ползване на програмата?",
|
||||
"Allow Anonymous Usage Reporting?": "Разреши анонимно докладване за употребата на програмата?",
|
||||
"Alphabetic": "Азбучен ред",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Друга команда се занимава с версиите. Тази команда трябва да премахни файла от синхронизираната папка.",
|
||||
"Anonymous Usage Reporting": "Анонимен Доклад",
|
||||
"Anonymous Usage Reporting": "Анонимен доклад",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Устройства настроени на introducer компютъра също ще бъдат добавени към този компютър.",
|
||||
"Automatic upgrades": "Автоматични ъпдейти",
|
||||
"Be careful!": "Внимавай!",
|
||||
"Automatic upgrades": "Автоматично обновяване",
|
||||
"Be careful!": "Внимание!",
|
||||
"Bugs": "Бъгове",
|
||||
"CPU Utilization": "Натоварване на Процесора",
|
||||
"Changelog": "Сипъск с промени",
|
||||
"CPU Utilization": "Процесор в употреба",
|
||||
"Changelog": "Списък с промени",
|
||||
"Clean out after": "Изчисти след",
|
||||
"Close": "Затвори",
|
||||
"Command": "Команда",
|
||||
"Comment, when used at the start of a line": "Коментар, използван в началото на реда",
|
||||
"Compression": "Компресия",
|
||||
"Connection Error": "Грешка при Свързването",
|
||||
"Compression": "Компресиране",
|
||||
"Connection Error": "Грешка при свързването",
|
||||
"Copied from elsewhere": "Копиране от някъде другаде",
|
||||
"Copied from original": "Копиран от оригинала",
|
||||
"Copyright © 2015 the following Contributors:": "Правата запазени © 2015 Сътрудници:",
|
||||
"Copyright © 2015 the following Contributors:": "Всички правата запазени © 2015 Сътрудници:",
|
||||
"Delete": "Изтрий",
|
||||
"Deleted": "Изтрито",
|
||||
"Device ID": "Идентификатор на устройство",
|
||||
"Device Identification": "Идентификация на устройство",
|
||||
"Device Identification": "Идентификатор на устройство",
|
||||
"Device Name": "Име на устройство",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Устройство {{device}} ({{address}}) желае да се свърже. Добави ново устройство?",
|
||||
"Devices": "Устройства",
|
||||
"Disconnected": "Прекрати Връзката",
|
||||
"Disconnected": "Не е свързано",
|
||||
"Discovery": "Откриване",
|
||||
"Documentation": "Документация",
|
||||
"Download Rate": "Скорост на Теглене",
|
||||
"Download Rate": "Скорост на сваляне",
|
||||
"Downloaded": "Изтеглен",
|
||||
"Downloading": "Изтегляне",
|
||||
"Edit": "Промени",
|
||||
@@ -50,49 +50,49 @@
|
||||
"Edit Folder": "Промени папка",
|
||||
"Editing": "Променяне",
|
||||
"Enable UPnP": "Включи UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Въведи адреси разделени със запетая (\"tcp://ip:port\", \"tcp://host:port\") или \"dynamic\", за да извършиш автоматична връзка на адреси.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Въведете адреси разделени със запетая (\"tcp://ip:port\", \"tcp://host:port\") или \"dynamic\", за да автоматично откриване на наличните адреси.",
|
||||
"Enter ignore patterns, one per line.": "Добави шаблони за игнориране, по един на ред.",
|
||||
"Error": "Грешка",
|
||||
"External File Versioning": "Външно упраление на версиите",
|
||||
"External File Versioning": "Външно управление на версиите",
|
||||
"Failed Items": "Неуспешни",
|
||||
"File Pull Order": "По ред на дърпане",
|
||||
"File Versioning": "Файлови Версии",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Битовете за права за достъп са игнорирани, когато се проверява за промени. Използвай с файлови системи тип FAT.",
|
||||
"File Pull Order": "Ред на сваляне",
|
||||
"File Versioning": "Версии на файловете",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Битовете за права за достъп ще бъдат игнорирани, когато се проверява за промени. Ползвайте за файлови системи тип FAT.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Файловете биват преместени в .stversions папка, когато са заменен или изтрити от Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Когато syncthing замени или изтрие файл той се премества в .stversions и преименува с дабавени дата и час.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файловете са защитени от промени направени на други устройства, но промени направени на това устройство ще бъдат синхронизирани с другите устройства.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Когато syncthing замени или изтрие файл той се премества в .stversions и преименува с набавени дата и час.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Защитава файловете от промени направени на други устройства, но промените направени на това устройство ще бъдат синхронизирани с останалите устройства.",
|
||||
"Folder": "Папка",
|
||||
"Folder ID": "Идентификатор на папка",
|
||||
"Folder ID": "Идентификатор на папката",
|
||||
"Folder Master": "Главна папка",
|
||||
"Folder Path": "Път до папката",
|
||||
"Folders": "Папки",
|
||||
"GUI": "Потребителски интефейс",
|
||||
"GUI Authentication Password": "Парола за Потребителския Интерфейс",
|
||||
"GUI Authentication User": "Потребител за Потребителския Интерфейс",
|
||||
"GUI Listen Addresses": "Адрес за Свързване с Потребителския Интерфейс",
|
||||
"GUI": "Потребителски интерфейс",
|
||||
"GUI Authentication Password": "Парола за потребителския интерфейс",
|
||||
"GUI Authentication User": "Потребител за потребителския интерфейс",
|
||||
"GUI Listen Addresses": "Адрес за свързване с потребителския интерфейс",
|
||||
"Generate": "Генерирай",
|
||||
"Global Discovery": "Глобавно Откриване",
|
||||
"Global Discovery Server": "Сървър за Глобално Откриване",
|
||||
"Global Discovery": "Глобално откриване",
|
||||
"Global Discovery Server": "Сървър за глобално откриване",
|
||||
"Global State": "Глобално състояние",
|
||||
"Help": "Помощ",
|
||||
"Home page": "Начална страница",
|
||||
"Ignore": "Игнорирай",
|
||||
"Ignore Patterns": "Шаблони за Игнориране",
|
||||
"Ignore Permissions": "Игнорирай Права за Достъп",
|
||||
"Incoming Rate Limit (KiB/s)": "Входящ Лимит на Скоростта (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Неправилни настройки могат да повредят съдържанието на папката и да попречат на по-нататъшно синхронизиране.",
|
||||
"Ignore Patterns": "Шаблони за игнориране",
|
||||
"Ignore Permissions": "Игнорирай правата за достъп",
|
||||
"Incoming Rate Limit (KiB/s)": "Входящ лимит на скоростта (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Неправилни настройки могат да повредят файловете и да попречат на синхронизирането.",
|
||||
"Introducer": "Introducer",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Обратното на даденото условие (пр. не изключвай)",
|
||||
"Keep Versions": "Пази Версии",
|
||||
"Largest First": "Най-големите първо",
|
||||
"Keep Versions": "Пази версии",
|
||||
"Largest First": " Първо най-големите",
|
||||
"Last File Received": "Последния получен файл",
|
||||
"Last seen": "Последно видян",
|
||||
"Later": "По-късно",
|
||||
"Local Discovery": "Локално Откриване",
|
||||
"Local Discovery": "Локално откриване",
|
||||
"Local State": "Локално състояние",
|
||||
"Local State (Total)": "Локално Състояние (Общо)",
|
||||
"Local State (Total)": "Локално състояние (Общо)",
|
||||
"Major Upgrade": "Основно Обновяване",
|
||||
"Maximum Age": "Максимална Възраст",
|
||||
"Maximum Age": "Максимална възраст",
|
||||
"Metadata Only": "Само мета информация",
|
||||
"Minimum Free Disk Space": "Минимално свободно дисково пространство",
|
||||
"Move to top of queue": "Премести в началото на опашката",
|
||||
@@ -100,66 +100,66 @@
|
||||
"Never": "Никога",
|
||||
"New Device": "Ново устройство",
|
||||
"New Folder": "Нова папка",
|
||||
"Newest First": "Най-новите първо",
|
||||
"Newest First": "Първо най-новите",
|
||||
"No": "Не",
|
||||
"No File Versioning": "Няма Файлови Версии",
|
||||
"No File Versioning": "Без версии",
|
||||
"Notice": "Известие",
|
||||
"OK": "ОК",
|
||||
"Off": "Изключено",
|
||||
"Oldest First": "Най-старите първо",
|
||||
"Oldest First": "Първо най-старите",
|
||||
"Options": "Настройки",
|
||||
"Out of Sync": "Не синхронизиран",
|
||||
"Out of Sync": "Несинхронизирано",
|
||||
"Out of Sync Items": "Несинхронизирани елементи",
|
||||
"Outgoing Rate Limit (KiB/s)": "Лимит на Изходящата Скорост (KiB/s)",
|
||||
"Override Changes": "Замени Промените",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Пътят до папката на този компютър. Ще бъде създадена ако не съществува. Символът тилда (~) може да бъде използван като заместител на",
|
||||
"Outgoing Rate Limit (KiB/s)": "Лимит на изходящата скорост (KiB/s)",
|
||||
"Override Changes": "Наложи локалните промени",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Път до папката на това устройство. Ако не съществува ще бъде създадена. Символът тилда (~) може да бъде използван като заместител на",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Пътят, където версиите да бъдат складирани(остави празно за папката .stversions).",
|
||||
"Pause": "Пауза",
|
||||
"Paused": "На пауза",
|
||||
"Please consult the release notes before performing a major upgrade.": "Моля прочети бележките по обновяването преди да започнеш.",
|
||||
"Please wait": "Моля изчакай",
|
||||
"Preview": "Преглед",
|
||||
"Preview Usage Report": "Разгледай Доклада за Използване",
|
||||
"Preview Usage Report": "Разгледай доклада за използване",
|
||||
"Quick guide to supported patterns": "Бърз наръчник към поддържаните шаблони",
|
||||
"RAM Utilization": "RAM Натоварване",
|
||||
"Random": "Произволно",
|
||||
"RAM Utilization": "RAM в употреба",
|
||||
"Random": "Произволен",
|
||||
"Relayed via": "Препратено през",
|
||||
"Relays": "Препращачи",
|
||||
"Release Notes": "Бележки по обновяването",
|
||||
"Remove": "Премахни",
|
||||
"Rescan": "Повторно Сканиране",
|
||||
"Rescan All": "Пълно повторно сканиране",
|
||||
"Rescan Interval": "Интервал за Повторно Сканиране",
|
||||
"Rescan": "Сканирай повторно",
|
||||
"Rescan All": "Сканирай повторно всички",
|
||||
"Rescan Interval": "Интервал за повторно сканиране",
|
||||
"Restart": "Рестартирай",
|
||||
"Restart Needed": "Изискава се Рестартиране",
|
||||
"Restart Needed": "Изисква се рестартиране",
|
||||
"Restarting": "Рестартиране",
|
||||
"Resume": "Пусни",
|
||||
"Reused": "Повторно използван",
|
||||
"Save": "Запази",
|
||||
"Scanning": "Сканиране",
|
||||
"Select the devices to share this folder with.": "Избери устройствата, с които да споделиш тази папка.",
|
||||
"Select the devices to share this folder with.": "Изберете устройствата, с които да споделите папката.",
|
||||
"Select the folders to share with this device.": "Изберете папките за споделяне с това устройство.",
|
||||
"Settings": "Настройки",
|
||||
"Share": "Сподели",
|
||||
"Share Folder": "Сподели папка",
|
||||
"Share Folders With Device": "Сподели папки с това устройство",
|
||||
"Share With Devices": "Сподели с устройства",
|
||||
"Share With Devices": "Споделяне с устройства",
|
||||
"Share this folder?": "Сподели тази папка?",
|
||||
"Shared With": "Споделена със",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Кратък идентификатор на папката. Трябва да бъде същият на всички компютри.",
|
||||
"Show ID": "Покажи Идентификатора",
|
||||
"Shared With": "Споделена с",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Идентификаторът на папката трябва да бъде еднакъв на всички устройства.",
|
||||
"Show ID": "Покажи идентификатора",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Покажи вместо идентификатор на устройството в статус на клъстъра. Ще бъде предлагано на други комютри като име по подразбиране.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Покажи вместо идентификатор на устройството в статус на клъстъра. Ще бъде обновено с името по подразбиране изпратено от другия компютър.",
|
||||
"Shutdown": "Спри Програмата",
|
||||
"Shutdown": "Спри програмата",
|
||||
"Shutdown Complete": "Спирането завършено",
|
||||
"Simple File Versioning": "Опростени Файлови Версии",
|
||||
"Simple File Versioning": "Опростени версии",
|
||||
"Single level wildcard (matches within a directory only)": "Маска на едно ниво (покрива само в папка)",
|
||||
"Smallest First": "Най-малките първо",
|
||||
"Source Code": "Сорс Код",
|
||||
"Staggered File Versioning": "Наслагващи се Файлови Версии",
|
||||
"Start Browser": "Стартирай Браузъра",
|
||||
"Smallest First": "Първо най-малките",
|
||||
"Source Code": "Сорс код",
|
||||
"Staggered File Versioning": "Наслагващи се версии",
|
||||
"Start Browser": "Стартирай браузъра",
|
||||
"Statistics": "Статистика",
|
||||
"Stopped": "Спряна",
|
||||
"Stopped": "Без синхронизиране",
|
||||
"Support": "Помощ",
|
||||
"Sync Protocol Listen Addresses": "Адрес за слушане на синхронизиращия протокол",
|
||||
"Syncing": "Синхронизиране",
|
||||
@@ -172,12 +172,12 @@
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Сумарната статистика е публично достъпна на {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурацията е запазена, но не е активирана. Syncthing трябва да рестартира, за да се активира новата конфигурация.",
|
||||
"The device ID cannot be blank.": "Полето идентификатор на устройство не може да бъде празно.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор на устройство за въвеждане тук, може да бъде намерен в \"Промени > Покажи Идентификатора\". Интервалите и тиретата са пожелание(биват прескачани).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Криптираният доклад се изпраща дневно. Използва се, за да следи общи платформи, размери на папки и версии на приложението. Ако събираните данни се променят, ще бъдете информиран с подобен на този диалог.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Въведни идентификатор на устройство не е валиден. Трябва да бъде 52 или 56 символа и да се състои от букви и цифри, като интервалите и тиретата са пожелание.",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор на устройство за въвеждане тук, може да бъде намерен в \"Промени > Покажи идентификатора\". Интервалите и тиретата са пожелание (биват прескачани).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Криптираният доклад се изпраща ежедневно. Използва се, за отичане на ползваните платформи, размер на папки и версии на приложението. Ако събираните данни се променят, ще бъдете информиран с подобен на този диалог.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Въведеният идентификатор на устройство не е валиден. Трябва да бъде 52 или 56 символа и да се състои от букви и цифри, като интервалите и тиретата са пожелание.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Първият параметър за командата е пътя до папката, а вторият е релативния път в самата папка.",
|
||||
"The folder ID cannot be blank.": "Полето идентификатор на папка неможе да бъде празно.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Идентификаторът на папка трябва да бъде къс (64 символа или по-малко) състоящ се само от букви, цифри, точка(.), тире(-) и подчерта (_).",
|
||||
"The folder ID cannot be blank.": "Полето идентификатор на папка не може да бъде празно.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Идентификаторът на папка трябва да бъде къс (64 символа или по-малко) състоящ се само от букви, цифри, точка (.), тире (-) и подчерта (_).",
|
||||
"The folder ID must be unique.": "Идентификаторът на папката тряба да бъде уникален.",
|
||||
"The folder path cannot be blank.": "Пътят до папката не може да бъде празен.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Използва се следния интервал: за първия час се пази версия всеки 30 секунди, за първия ден се пази версия всеки час, за първите 30 дена се пази версия всеки ден, до максимума се пази една версия всяка седмица.",
|
||||
@@ -185,29 +185,29 @@
|
||||
"The maximum age must be a number and cannot be blank.": "Максималната възраст трябва да е число и не може д ае празна.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максималното време да се пазят весрсии (в дни, сложи 0, за да пазиш версии завинаги).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Минималното свободно дисково пространство в проценти трябва да е между 0 и 100 (включително).",
|
||||
"The number of days must be a number and cannot be blank.": "Броят дни трябва да бъде число и неможе да бъде празно.",
|
||||
"The number of days must be a number and cannot be blank.": "Броят дни трябва да бъде число и не може да бъде празно.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Броят дни за запазване на файловете в кошчето. Нула значи завинаги.",
|
||||
"The number of old versions to keep, per file.": "Броят стари версии, които да бъдат пазени за всеки файл.",
|
||||
"The number of versions must be a number and cannot be blank.": "Броят версии трябва да бъде число и не може да бъде празно.",
|
||||
"The path cannot be blank.": "Пътят неможе да бъде празен.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Ограничението на скорста трябва да бъде положително число (0: неограничено)",
|
||||
"The path cannot be blank.": "Пътят не може да бъде празен.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Ограничението на скоростта трябва да бъде положително число (0: неограничено)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Интервала на сканиране трябва да бъде не отрицателно число в секунди.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Ще бъдат спрени и автоматично синхронизирани, когато грешката бъде оправена.",
|
||||
"This is a major version upgrade.": "Това е нова основна версия.",
|
||||
"Trash Can File Versioning": "Версии на файлове в кошчето",
|
||||
"Unknown": "Неясен",
|
||||
"Unshared": "Споделянето прекратено",
|
||||
"Trash Can File Versioning": "Само на файловете в кошчето",
|
||||
"Unknown": "Неясно",
|
||||
"Unshared": "Несподелена",
|
||||
"Unused": "Неизползван",
|
||||
"Up to Date": "Актуален",
|
||||
"Up to Date": "Синхронизирано",
|
||||
"Updated": "Обновено",
|
||||
"Upgrade": "Обнови",
|
||||
"Upgrade To {%version%}": "Обновен До {{version}}",
|
||||
"Upgrade To {%version%}": "Обновен до {{version}}",
|
||||
"Upgrading": "Обновяване",
|
||||
"Upload Rate": "Скорост на Качване",
|
||||
"Uptime": "Работи вече",
|
||||
"Use HTTPS for GUI": "Използвай HTTPS за Потребителския Интерфейс",
|
||||
"Upload Rate": "Скорост на качване",
|
||||
"Uptime": "Работи от",
|
||||
"Use HTTPS for GUI": "Използвай HTTPS за потребителския интерфейс",
|
||||
"Version": "Версия",
|
||||
"Versions Path": "Път до Версиите",
|
||||
"Versions Path": "Път до версиите",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версиите биват изтривани автоматично, когато са по-стари от максималната възраст или надминават броя файлове разрешени в даден интервал.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Когато добавяш ново устройство помни, че твоето устройство също трябва да бъде добавено от другата страна.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Когато добавяш нов идентификатор на папка помни, че той се използва за свързване на папките на различни устройства. Главни/малки букви са от значение и трябва да са еднакви на всички устройства.",
|
||||
@@ -215,6 +215,6 @@
|
||||
"You must keep at least one version.": "Трябва да пазиш поне една версия.",
|
||||
"days": "дни",
|
||||
"full documentation": "пълна документация",
|
||||
"items": "артикула",
|
||||
"items": "елемента",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} желае да сподели папка \"{{folder}}\"."
|
||||
}
|
||||
@@ -42,7 +42,7 @@
|
||||
"Disconnected": "Getrennt",
|
||||
"Discovery": "Gerätesuche",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Download-Rate",
|
||||
"Download Rate": "Download",
|
||||
"Downloaded": "Heruntergeladen",
|
||||
"Downloading": "Lädt herunter",
|
||||
"Edit": "Bearbeiten",
|
||||
@@ -90,7 +90,7 @@
|
||||
"Later": "Später",
|
||||
"Local Discovery": "Lokale Gerätesuche",
|
||||
"Local State": "Lokaler Status",
|
||||
"Local State (Total)": "Lokaler Status (total)",
|
||||
"Local State (Total)": "Lokaler Status (Gesamt)",
|
||||
"Major Upgrade": "Hauptversionsupgrade",
|
||||
"Maximum Age": "Höchstalter",
|
||||
"Metadata Only": "Nur Metadaten",
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"Add new folder?": "¿Añadir una nueva carpeta?",
|
||||
"Address": "Dirección",
|
||||
"Addresses": "Direcciones",
|
||||
"Advanced": "Advanced",
|
||||
"Advanced Configuration": "Advanced Configuration",
|
||||
"Advanced": "Avanzado",
|
||||
"Advanced Configuration": "Configuración Avanzada",
|
||||
"All Data": "Todos los datos",
|
||||
"Allow Anonymous Usage Reporting?": "¿Deseas permitir el envío anónimo de informes de uso?",
|
||||
"Alphabetic": "Alfabético",
|
||||
@@ -19,7 +19,7 @@
|
||||
"Anonymous Usage Reporting": "Informe anónimo de uso",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Cualquier dispositivo configurado en un dispositivo de introducción será añadido también.",
|
||||
"Automatic upgrades": "Actualizaciones automáticas",
|
||||
"Be careful!": "Be careful!",
|
||||
"Be careful!": "¡Ten cuidado!",
|
||||
"Bugs": "Errores (bugs)",
|
||||
"CPU Utilization": "Uso de CPU",
|
||||
"Changelog": "Informe de cambios",
|
||||
@@ -40,7 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositivo {{device}} ({{address}}) quiere conectarse. ¿Añadir nuevo dispositivo?",
|
||||
"Devices": "Dispositivos",
|
||||
"Disconnected": "Desconectado",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "Descubrimiento",
|
||||
"Documentation": "Documentación",
|
||||
"Download Rate": "Velocidad de descarga",
|
||||
"Downloaded": "Descargado",
|
||||
@@ -50,18 +50,18 @@
|
||||
"Edit Folder": "Editar repositorio",
|
||||
"Editing": "Editando",
|
||||
"Enable UPnP": "Habilitar UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduzca separados por comas (\"tcp://ip:port\", \"tcp://host:port\") direcciones o \"dynamic\" para llevar a cabo la detección automática de la dirección.",
|
||||
"Enter ignore patterns, one per line.": "Introducir patrones a ignorar, uno por línea.",
|
||||
"Error": "Error",
|
||||
"External File Versioning": "Versionado externo de fichero",
|
||||
"Failed Items": "Failed Items",
|
||||
"Failed Items": "Elementos fallidos",
|
||||
"File Pull Order": "Orden de ficheros del pull",
|
||||
"File Versioning": "Versionado de ficheros",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Los bits de permiso de ficheros son ignorados cuando se buscan cambios. Utilizar en sistemas de ficheros FAT.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Los archivos serán movidos a la carpeta .stversions cuando sean reemplazados o borrados por Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Los ficheros son cambiados a versiones con indicación de fecha en una carpeta \".stversions\" cuando son reemplazados o borrados por Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Los ficheros son protegidos por los cambios hechos en otros dispositivos, pero los cambios hechos en este dispositivo serán enviados al resto del grupo (cluster).",
|
||||
"Folder": "Folder",
|
||||
"Folder": "Carpeta",
|
||||
"Folder ID": "ID de carpeta",
|
||||
"Folder Master": "Carpeta principal",
|
||||
"Folder Path": "Ruta de la carpeta",
|
||||
@@ -75,12 +75,12 @@
|
||||
"Global Discovery Server": "Servidor de descubrimiento global",
|
||||
"Global State": "Estado global",
|
||||
"Help": "Ayuda",
|
||||
"Home page": "Home page",
|
||||
"Home page": "Página de inicio",
|
||||
"Ignore": "Ignorar",
|
||||
"Ignore Patterns": "Patrones a ignorar",
|
||||
"Ignore Permissions": "Permisos a ignorar",
|
||||
"Incoming Rate Limit (KiB/s)": "Límite de descarga (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configuración incorrecta puede dañar los contenidos de la carpeta y hacer que Syncthing no funcione.",
|
||||
"Introducer": "Presentador",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversión de la condición dada (por ejemplo, \"no excluir\")",
|
||||
"Keep Versions": "Mantener versiones",
|
||||
@@ -94,7 +94,7 @@
|
||||
"Major Upgrade": "Actualización importante",
|
||||
"Maximum Age": "Edad máxima",
|
||||
"Metadata Only": "Sólo metadatos",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "Espacio mínimo libre en disco",
|
||||
"Move to top of queue": "Mover al principio de la cola",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Comodín multinivel (coincide con múltiples niveles de directorio)",
|
||||
"Never": "Nunca",
|
||||
@@ -107,15 +107,15 @@
|
||||
"OK": "OK",
|
||||
"Off": "Desconectar",
|
||||
"Oldest First": "El más antiguo primero",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Out of Sync",
|
||||
"Options": "Opciones",
|
||||
"Out of Sync": "No sincronizado",
|
||||
"Out of Sync Items": "Elementos no sincronizados",
|
||||
"Outgoing Rate Limit (KiB/s)": "Límite de subida (KiB/s)",
|
||||
"Override Changes": "Anular cambios",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta a la carpeta en la máquina local. Se creará si no existe. El carácter de la tilde (~) puede usarse como atajo.",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ruta donde se almacenarán las versiones (dejar vacío para usar la carpeta por defecto \".stversions\").",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Pause": "Pausar",
|
||||
"Paused": "Pausado",
|
||||
"Please consult the release notes before performing a major upgrade.": "Por favor, consultar las notas de la versión antes de realizar una actualización importante.",
|
||||
"Please wait": "Por favor, espere",
|
||||
"Preview": "Vista previa",
|
||||
@@ -123,17 +123,17 @@
|
||||
"Quick guide to supported patterns": "Guía rápida de patrones soportados",
|
||||
"RAM Utilization": "Uso de RAM",
|
||||
"Random": "Aleatorio",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Relayed via": "Respaldada a través",
|
||||
"Relays": "Respaldos",
|
||||
"Release Notes": "Notas de la versión",
|
||||
"Remove": "Remove",
|
||||
"Remove": "Eliminar",
|
||||
"Rescan": "Volver a buscar",
|
||||
"Rescan All": "Volver a buscar todo",
|
||||
"Rescan Interval": "Intervalo de nueva búsqueda",
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "Reinicio necesario",
|
||||
"Restarting": "Reiniciando",
|
||||
"Resume": "Resume",
|
||||
"Resume": "Continuar",
|
||||
"Reused": "Reutilizado",
|
||||
"Save": "Guardar",
|
||||
"Scanning": "Rastreando",
|
||||
@@ -158,7 +158,7 @@
|
||||
"Source Code": "Código fuente",
|
||||
"Staggered File Versioning": "Versionado escalonado de fichero",
|
||||
"Start Browser": "Iniciar el navegador",
|
||||
"Statistics": "Statistics",
|
||||
"Statistics": "Estadísticas",
|
||||
"Stopped": "Detenido",
|
||||
"Support": "Soporte",
|
||||
"Sync Protocol Listen Addresses": "Direcciones de escucha del protocolo de sincronización",
|
||||
@@ -181,18 +181,18 @@
|
||||
"The folder ID must be unique.": "La ID de la carpeta debe ser única.",
|
||||
"The folder path cannot be blank.": "La ruta de la carpeta no puede estar en blanco.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Se utilizan los siguientes intervalos: para la primera hora se mantiene una versión cada 30 segundos, para el primer día se mantiene una versión cada hora, para los primeros 30 días se mantiene una versión diaria hasta la edad máxima de una semana.",
|
||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
||||
"The following items could not be synchronized.": "Los siguientes artículos no pueden ser sincronizados.",
|
||||
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar vacía.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El tiempo máximo para mantener una versión en días (introducir 0 para mantener las versiones indefinidamente).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "El porcentaje de espacio libre mínimo debe ser un número no negativo entre 0 y 100 (ambos inclusive).",
|
||||
"The number of days must be a number and cannot be blank.": "El número de días debe ser un número y no puede estar en blanco.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "El número de días para mantener los archivos en la papelera. Cero significa \"para siempre\".",
|
||||
"The number of old versions to keep, per file.": "El número de versiones a antiguas a mantener para cada fichero.",
|
||||
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
|
||||
"The path cannot be blank.": "La ruta no puede estar vacía.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "El límite de velocidad debe ser un número no negativo (0: sin límite)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "El intervalo de actualización debe ser un número positivo de segundos.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Se reintentarán de forma automática y se sincronizarán cuando se resuelva el error.",
|
||||
"This is a major version upgrade.": "Hay una actualización importante.",
|
||||
"Trash Can File Versioning": "Versionado de archivos de la papelera",
|
||||
"Unknown": "Desconocido",
|
||||
@@ -213,7 +213,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Cuando añada una nueva carpeta, tenga en cuenta que su ID se usa para unir carpetas entre dispositivos. Son sensibles a las mayúsculas y deben coincidir exactamente entre todos los dispositivos.",
|
||||
"Yes": "Si",
|
||||
"You must keep at least one version.": "Debes mantener al menos una versión.",
|
||||
"days": "days",
|
||||
"days": "días",
|
||||
"full documentation": "Documentación completa",
|
||||
"items": "Elementos",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{folder}}\"."
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "El dispositivo {{device}} ({{address}}) se quiere conectar. ¿Agregar nuevo dispositivo?",
|
||||
"Devices": "Dispositivos",
|
||||
"Disconnected": "Desconectado",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "Búsqueda",
|
||||
"Documentation": "Documentación",
|
||||
"Download Rate": "Tasa de descarga",
|
||||
"Downloaded": "Descargado",
|
||||
@@ -75,12 +75,12 @@
|
||||
"Global Discovery Server": "Servidor global de identificación",
|
||||
"Global State": "Estado global",
|
||||
"Help": "Ayuda",
|
||||
"Home page": "Home page",
|
||||
"Home page": "Pagina de inicio",
|
||||
"Ignore": "Ignorar",
|
||||
"Ignore Patterns": "Patrones de exclusión",
|
||||
"Ignore Permissions": "Ignorar permisos",
|
||||
"Incoming Rate Limit (KiB/s)": "Límite de velocidad de entrada (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Configuración incorrecta puede dañar los contenidos de la carpeta y hacer Syncthing inoperable.",
|
||||
"Introducer": "Introductor",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversión de la condición dada (es decir, no excluir)",
|
||||
"Keep Versions": "Conservar versiones",
|
||||
@@ -94,7 +94,7 @@
|
||||
"Major Upgrade": "Actualización mayor",
|
||||
"Maximum Age": "Edad máxima",
|
||||
"Metadata Only": "Sólo metadatos",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "Espacio mínimo libre en disco",
|
||||
"Move to top of queue": "Mover al principio de la cola.",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Carácter comodín multinivel (coincide en el directorio y sus subdirectorios)",
|
||||
"Never": "Nunca",
|
||||
@@ -108,7 +108,7 @@
|
||||
"Off": "Apagado",
|
||||
"Oldest First": "Antiguo primero",
|
||||
"Options": "Opciones",
|
||||
"Out of Sync": "Out of Sync",
|
||||
"Out of Sync": "Fuera de sincronización",
|
||||
"Out of Sync Items": "Ítems no sincronizados",
|
||||
"Outgoing Rate Limit (KiB/s)": "Tasa máxima de envío (KiB/s)",
|
||||
"Override Changes": "Reemplazar los cambios",
|
||||
@@ -123,8 +123,8 @@
|
||||
"Quick guide to supported patterns": "Guía rápida sobre los patrones soportados",
|
||||
"RAM Utilization": "Utilización de RAM",
|
||||
"Random": "Aleatorio",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Relayed via": "retransmitida vía",
|
||||
"Relays": "Retransmisores",
|
||||
"Release Notes": "Notas de lanzamiento",
|
||||
"Remove": "Eliminar",
|
||||
"Rescan": "Reescanear",
|
||||
@@ -133,7 +133,7 @@
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "Es necesario reiniciar",
|
||||
"Restarting": "Reiniciando",
|
||||
"Resume": "Resume",
|
||||
"Resume": "Reanudar",
|
||||
"Reused": "Reutilizado",
|
||||
"Save": "Guardar",
|
||||
"Scanning": "Actualización",
|
||||
@@ -213,7 +213,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Al agregar un nuevo repositorio, tenga en cuenta que la ID del repositorio se utiliza para conectar los repositorios entre dispositivos. Se distingue entre mayúsculas y minúsculas y debe ser exactamente igual en todos los dispositivos.",
|
||||
"Yes": "Sí",
|
||||
"You must keep at least one version.": "Debe mantener al menos una versión",
|
||||
"days": "days",
|
||||
"days": "días",
|
||||
"full documentation": "documentación completa",
|
||||
"items": "ítems",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir repositorio \"{{folder}}\"."
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
|
||||
"A negative number of days doesn't make sense.": "Negatiivinen määrä päiviä ei ole järjellinen.",
|
||||
"A new major version may not be compatible with previous versions.": "A new major version may not be compatible with previous versions.",
|
||||
"API Key": "API-avain",
|
||||
"About": "Tietoja",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Edit Folder": "Muokkaa kansiota",
|
||||
"Editing": "Muokkaus",
|
||||
"Enable UPnP": "Ota UPnP käyttöön",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Syötä osoitteet pilkuilla erotettuina (\"tcp://ip:portti, tcp://nimi:portti\") tai \"dynamic\" käyttääksesi osoitteen automaattista selvitystä.",
|
||||
"Enter ignore patterns, one per line.": "Syötä ohituslausekkeet, yksi riviä kohden.",
|
||||
"Error": "Virhe",
|
||||
"External File Versioning": "Ulkoinen tiedostoversionti",
|
||||
@@ -58,10 +58,10 @@
|
||||
"File Pull Order": "File Pull Order",
|
||||
"File Versioning": "Tiedostoversiointi",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Tiedostojen oikeusbitit jätetään huomiotta etsittäessä muutoksia. Käytä FAT-tiedostojärjestelmissä.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Kun Syncthing poistaa tai korvaa tiedostoja, ne siirretään .stversions-hakemistoon.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Tiedostot siirretään päivämäärällä merkityiksi versioiksi .stversions-kansioon, kun Syncthing korvaa tai poistaa ne.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Tiedostot on suojattu muilla laitteilla tehdyiltä muutoksilta, mutta tällä laitteella tehdyt muutokset lähetetään muuhun ryhmään.",
|
||||
"Folder": "Folder",
|
||||
"Folder": "Kansio",
|
||||
"Folder ID": "Kansion ID",
|
||||
"Folder Master": "Hallitsijakansio",
|
||||
"Folder Path": "Kansion polku",
|
||||
@@ -94,7 +94,7 @@
|
||||
"Major Upgrade": "Major Upgrade",
|
||||
"Maximum Age": "Maksimi-ikä",
|
||||
"Metadata Only": "Vain metadata",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "Vapaan levytilan vähimmäismäärä",
|
||||
"Move to top of queue": "Siirrä jonon alkuun",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Monitasoinen jokerimerkki (vaikuttaa useassa kansiotasossa)",
|
||||
"Never": "Ei koskaan",
|
||||
@@ -114,8 +114,8 @@
|
||||
"Override Changes": "Ohita muutokset",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Polku kansioon paikallisella tietokoneella. Kansio luodaan, ellei sitä ole olemassa. Tilde-merkkiä (~) voidaan käyttää oikotienä polulle",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Polku jonne versiot tullaan tallentamaan (jätä tyhjäksi oletusvalintaa .stversions varten).",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Pause": "Keskeytä",
|
||||
"Paused": "Keskeytetty",
|
||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
||||
"Please wait": "Ole hyvä ja odota",
|
||||
"Preview": "Esikatselu",
|
||||
@@ -124,16 +124,16 @@
|
||||
"RAM Utilization": "RAM:n käyttö",
|
||||
"Random": "Satunnaien",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Release Notes": "Release Notes",
|
||||
"Remove": "Remove",
|
||||
"Relays": "Välityspalvelimet",
|
||||
"Release Notes": "Julkaisutiedot",
|
||||
"Remove": "Poista",
|
||||
"Rescan": "Skannaa uudelleen",
|
||||
"Rescan All": "Skannaa kaikki uudelleen",
|
||||
"Rescan Interval": "Uudelleenskannauksen aikaväli",
|
||||
"Restart": "Käynnistä uudelleen",
|
||||
"Restart Needed": "Uudelleenkäynnistys tarvitaan",
|
||||
"Restarting": "Käynnistetään uudelleen",
|
||||
"Resume": "Resume",
|
||||
"Resume": "Jatka",
|
||||
"Reused": "Uudelleenkäytetty",
|
||||
"Save": "Tallenna",
|
||||
"Scanning": "Skannataan",
|
||||
@@ -154,11 +154,11 @@
|
||||
"Shutdown Complete": "Sammutus valmis",
|
||||
"Simple File Versioning": "Yksinkertainen tiedostoversiointi",
|
||||
"Single level wildcard (matches within a directory only)": "Yksitasoinen jokerimerkki (vaikuttaa vain kyseisen kansion sisällä)",
|
||||
"Smallest First": "Smallest First",
|
||||
"Smallest First": "Pienin ensin",
|
||||
"Source Code": "Lähdekoodi",
|
||||
"Staggered File Versioning": "Porrastettu tiedostoversiointi",
|
||||
"Start Browser": "Käynnistä selain",
|
||||
"Statistics": "Statistics",
|
||||
"Statistics": "Tilastot",
|
||||
"Stopped": "Pysäytetty",
|
||||
"Support": "Tuki",
|
||||
"Sync Protocol Listen Addresses": "Synkronointiprotokollan kuunteluosoite",
|
||||
@@ -181,16 +181,16 @@
|
||||
"The folder ID must be unique.": "Kansion ID:n tulee olla uniikki.",
|
||||
"The folder path cannot be blank.": "Kansion polku ei voi olla tyhjä.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Seuraavat aikavälit ovat käytössä: ensimmäisen tunnin ajalta uusi versio säilytetään joka 30 sekunti, ensimmäisen päivän ajalta uusi versio säilytetään tunneittain ja ensimmäisen 30 päivän aikana uusi versio säilytetään päivittäin. Lopulta uusi versio säilytetään viikoittain, kunnes maksimi-ikä saavutetaan.",
|
||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
||||
"The following items could not be synchronized.": "Seuraavia nimikkeitä ei voitu synkronoida.",
|
||||
"The maximum age must be a number and cannot be blank.": "Maksimi-iän tulee olla numero, eikä se voi olla tyhjä.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimiaika versioiden säilytykseen (päivissä, aseta 0 säilyttääksesi versiot ikuisesti).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Vapaan levytilan vähimmäismäärä prosentteina tulee olla positiivinen luku (suljetulta) väliltä 0-100.",
|
||||
"The number of days must be a number and cannot be blank.": "Päivien määrän tulee olla numero, eikä se voi olla tyhjä.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Montako päivää tiedostoja säilytetään roskakorissa. Nolla (0) = ikuisesti.",
|
||||
"The number of old versions to keep, per file.": "Säilytettävien vanhojen versioiden määrä tiedostoa kohden.",
|
||||
"The number of versions must be a number and cannot be blank.": "Versioiden määrän rulee olla numero, eikä se voi olla tyhjä.",
|
||||
"The path cannot be blank.": "Polku ei voi olla tyhjä.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Nopeusrajan tulee olla positiivinen luku tai nolla. (0: ei rajaa)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Uudelleenskannauksen aikavälin tulee olla ei-negatiivinen numero sekunteja.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
|
||||
"This is a major version upgrade.": "This is a major version upgrade.",
|
||||
@@ -204,7 +204,7 @@
|
||||
"Upgrade To {%version%}": "Päivitä versioon {{version}}",
|
||||
"Upgrading": "Päivitetään",
|
||||
"Upload Rate": "Lähetysmäärä",
|
||||
"Uptime": "Uptime",
|
||||
"Uptime": "Päälläoloaika",
|
||||
"Use HTTPS for GUI": "Käytä HTTPS:ää GUI:n kanssa",
|
||||
"Version": "Versio",
|
||||
"Versions Path": "Versioiden polku",
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
"Device Identification": "Identification de l'appareil",
|
||||
"Device Name": "Nom du périphérique",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "L'appareil {{device}} ({{address}}) veut se connecter. Voulez-vous ajouter cette appareil ?",
|
||||
"Devices": "Appareil",
|
||||
"Devices": "Appareils",
|
||||
"Disconnected": "Déconnecté",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "Découverte",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Débit de réception",
|
||||
"Downloaded": "Téléchargé",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"A negative number of days doesn't make sense.": "Et negativt antall dager gir ikke mening.",
|
||||
"A new major version may not be compatible with previous versions.": "En ny hovedversjon kan bli ikke-kompatibel med en eldre versjon.",
|
||||
"A new major version may not be compatible with previous versions.": "En ny hovedversjon er ikke nødvendigvis kompatibel med eldre versjoner.",
|
||||
"API Key": "API-nøkkel",
|
||||
"About": "Om",
|
||||
"Actions": "Handlinger",
|
||||
@@ -40,7 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enhet {{device}} ({{address}}) ønsker å koble seg til. Legg til ny enhet?",
|
||||
"Devices": "Enheter",
|
||||
"Disconnected": "Frakoblet",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "Oppdagelse",
|
||||
"Documentation": "Dokumentasjon",
|
||||
"Download Rate": "Nedlastingsrate",
|
||||
"Downloaded": "Lastet ned",
|
||||
@@ -57,8 +57,8 @@
|
||||
"Failed Items": "Elementsynkronisering som har feilet",
|
||||
"File Pull Order": "Filenes Henterekkefølge",
|
||||
"File Versioning": "Versjonskontroll",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Fil bit-rettigheter ignoreres når forandringer oppdages. Bruk FAT filsystem. ",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer som slettes eller erstattes av Syncthing flyttes til katalogen .stversions",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Informasjon om filrettigheter ignoreres når det letes etter endringer. Bruk på FAT filsystem. ",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer som slettes eller erstattes av Syncthing flyttes til katalogen .stversions.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttes til en datostemplet versjon i .stversions-katalogen når den oppdateres eller slettes av Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskyttet mot endringer som er gjort på andre enheter, men endringer som er gjort på denne enheten blir sendt til resten av gruppen.",
|
||||
"Folder": "Katalog",
|
||||
@@ -80,7 +80,7 @@
|
||||
"Ignore Patterns": "Utelatelsesmønster",
|
||||
"Ignore Permissions": "Ignorer Tilgangsbit",
|
||||
"Incoming Rate Limit (KiB/s)": "Innkommende Hastighetsbegrensning (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Feilaktige innstillinger kan skade innholdet i dine delte kataloger og hindre Syncthing i å fungere. ",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Feilaktige innstillinger kan skade innholdet i dine delte kataloger og hindre Syncthing i å fungere.",
|
||||
"Introducer": "Introduktør",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Invers av den gitte tilstanden (t.d. ikke ekskluder)",
|
||||
"Keep Versions": "Behold Versjoner",
|
||||
@@ -194,7 +194,7 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Antall sekund i skanneintervallet kan ikke være negativt.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Disse hentes automatisk og vil synkroniseres når feilen er blitt utbedret.",
|
||||
"This is a major version upgrade.": "Dette er en hovedoppgradering",
|
||||
"Trash Can File Versioning": "Fil Versjonskontroll i papirkurven",
|
||||
"Trash Can File Versioning": "Papirkurv Versjonskontroll",
|
||||
"Unknown": "Ukjent",
|
||||
"Unshared": "Ikke delt",
|
||||
"Unused": "Ikke i bruk",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
|
||||
"A new major version may not be compatible with previous versions.": "A new major version may not be compatible with previous versions.",
|
||||
"A negative number of days doesn't make sense.": "Eit negativt tal dagar har ikkje meining.",
|
||||
"A new major version may not be compatible with previous versions.": "Ein ny hovudversjon er ikkje nødvendigvis kompatibel med eldre versjonar. ",
|
||||
"API Key": "API-nøkkel",
|
||||
"About": "Om",
|
||||
"Actions": "Handlingar",
|
||||
@@ -15,7 +15,7 @@
|
||||
"All Data": "Alle data",
|
||||
"Allow Anonymous Usage Reporting?": "Tillata anonymisert bruksrapportering?",
|
||||
"Alphabetic": "Alfabetisk",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein ekstern kommando håndterer filutgåver. Den må sørge for at fila blir fjerna frå den synkroniserte mappa.",
|
||||
"Anonymous Usage Reporting": "Anonymisert bruksrapportering",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Einingar konfigurert på ei introduksjonseining vil òg verta lagt til denne eininga.",
|
||||
"Automatic upgrades": "Automatiske oppdateringar",
|
||||
@@ -23,7 +23,7 @@
|
||||
"Bugs": "Programfeil",
|
||||
"CPU Utilization": "CPU-utnytting",
|
||||
"Changelog": "Endringslogg",
|
||||
"Clean out after": "Clean out after",
|
||||
"Clean out after": "Tøm etter",
|
||||
"Close": "Lukk",
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, når brukt i starten av linja",
|
||||
@@ -40,7 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Eininga {{device}} ({{address}}) vil kopla seg til. Vil du leggja ho til?",
|
||||
"Devices": "Einingar",
|
||||
"Disconnected": "Fråkopla",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "Oppdaging",
|
||||
"Documentation": "Dokumentasjon",
|
||||
"Download Rate": "Nedlastingsfart",
|
||||
"Downloaded": "Lasta ned",
|
||||
@@ -54,12 +54,12 @@
|
||||
"Enter ignore patterns, one per line.": "Skriv inn mønster som skal utelatast, eitt per linje.",
|
||||
"Error": "Feilmelding",
|
||||
"External File Versioning": "Ekstern filutgåvehandtering",
|
||||
"Failed Items": "Failed Items",
|
||||
"File Pull Order": "File Pull Order",
|
||||
"Failed Items": "Feilande element",
|
||||
"File Pull Order": "Henterekkefølge for filer",
|
||||
"File Versioning": "Filutgåvekontroll",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Informasjon om filrettar vert ignorert når det blir leita etter endringar. Bruk på FAT filsystem. ",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer som Syncthing slettar eller skriv over vert flytta til katalogen .stversions.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer som Syncthing oppdaterar eller sletter vert flytta til ein datostempla versjon i .stversions-katalogen.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskytta mot endringar gjort på andre einingar, men endringar gjort på denne eininga vert sende til resten av klyngja.",
|
||||
"Folder": "Mappe",
|
||||
"Folder ID": "Mappe ID",
|
||||
@@ -80,7 +80,7 @@
|
||||
"Ignore Patterns": "Utelatingsmønster",
|
||||
"Ignore Permissions": "Ignorer tilgangar",
|
||||
"Incoming Rate Limit (KiB/s)": "Innkomande hastigheitsgrense (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Feil innstillingar kan skade innhaldet i dine delte katalogar og hindre Syncthing i å fungere.",
|
||||
"Introducer": "Introduktør",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Det motsette av den gitte tilstanden (dvs. ekskluder ikkje)",
|
||||
"Keep Versions": "Behald Versjonar",
|
||||
@@ -90,11 +90,11 @@
|
||||
"Later": "Seinare",
|
||||
"Local Discovery": "Lokal oppdaging",
|
||||
"Local State": "Lokal Tilstand",
|
||||
"Local State (Total)": "Local State (Total)",
|
||||
"Major Upgrade": "Major Upgrade",
|
||||
"Local State (Total)": "Lokal tilstand (total)",
|
||||
"Major Upgrade": "Hovudoppgradering",
|
||||
"Maximum Age": "Maksimal Levetid",
|
||||
"Metadata Only": "Berre metadata",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "Naudsynt ledig diskplass",
|
||||
"Move to top of queue": "Flytt øvst i køen",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Fleirnivå-jokerteikn (søkjer på fleire mappenivå)",
|
||||
"Never": "Aldri",
|
||||
@@ -116,15 +116,15 @@
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Plasseringa for lagra versjonar (la denne vera tom for å bruka standard .stversions-mappa i mappa).",
|
||||
"Pause": "Stans",
|
||||
"Paused": "Stansa",
|
||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
||||
"Please consult the release notes before performing a major upgrade.": "Sjå \"release notes\" før ei hovudoppgradering vert utført.",
|
||||
"Please wait": "Gjer vel og vent",
|
||||
"Preview": "Førehandsvisning",
|
||||
"Preview Usage Report": "Førehandsvis bruksrapporten",
|
||||
"Quick guide to supported patterns": "Kjapp innføring i godkjente mønster",
|
||||
"RAM Utilization": "Minnebruk",
|
||||
"Random": "Tilfeldig",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relays": "Relays",
|
||||
"Relayed via": "Relé via",
|
||||
"Relays": "Reléer",
|
||||
"Release Notes": "Utgivingsnotat",
|
||||
"Remove": "Fjern",
|
||||
"Rescan": "Skann På Ny",
|
||||
@@ -168,7 +168,7 @@
|
||||
"Syncthing is restarting.": "Syncthing startar på ny.",
|
||||
"Syncthing is upgrading.": "Syncthing oppgraderer.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å vera nede, eller så er det eit problem med nettilkoplinga di. Prøvar på ny …",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ser ut til å ha støtt på eit problem under behandling av din førespurnad. Vær vennleg å oppfrisk nettlesaren eller start Syncthing på nytt om problemet vedvarer.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Samla statistikk er opent tilgjengeleg på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Instillingane har blitt lagra men ikkje aktivert. Syncthing må starta på ny for å aktivera dei nye instillingane.",
|
||||
"The device ID cannot be blank.": "Eining ID kan ikkje vera tom.",
|
||||
@@ -184,17 +184,17 @@
|
||||
"The following items could not be synchronized.": "Fyljande filer kunne ikkje synkroniserast.",
|
||||
"The maximum age must be a number and cannot be blank.": "Maksimal levetid må vera eit tal og kan ikkje vera tomt.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Høgaste tidsrom å behalda ei utgåve (i dagar, set til 0 for å behalda versjonane for alltid).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Nødvendig ledig diskplass må vere eit tal mellom 0 og 100.",
|
||||
"The number of days must be a number and cannot be blank.": "Tal på dagar må ver eit tal og kan ikkje vera tomt.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Antall dagar å behalda filer i søppelkorga. Null betyr for alltid.",
|
||||
"The number of old versions to keep, per file.": "Tal på gamle versjonar ein skal behalda, per fil.",
|
||||
"The number of versions must be a number and cannot be blank.": "Tal på versjonar må vera eit tal og kan ikkje vera tomt.",
|
||||
"The path cannot be blank.": "Bana kan ikkje vera tom.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Hastigheitsgrensa må ver eit positivt tall (0: ingen grensa)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Talet på sekund i skanneintervallet kan ikkje vera negativt.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
|
||||
"This is a major version upgrade.": "This is a major version upgrade.",
|
||||
"Trash Can File Versioning": "Trash Can File Versioning",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Desse vil bli prøvd på nytt automatisk og vil bli synkronisert når feilen har blitt utbetra.",
|
||||
"This is a major version upgrade.": "Dette er ei hovudoppgradering",
|
||||
"Trash Can File Versioning": "Papirkorg filutgåvehandtering",
|
||||
"Unknown": "Ukjent",
|
||||
"Unshared": "Ikkje delt",
|
||||
"Unused": "Ubrukt",
|
||||
@@ -213,7 +213,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Hugs at når ei ny mappe vert lagt til, vert mappe-ID-en brukt til å binda saman mappene mellom einingane. Det er skilnad på store og små bokstavar, så ID-ane må vera identiske på alle einingane.",
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du må behalda minst ein versjon.",
|
||||
"days": "days",
|
||||
"days": "dagar",
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "element",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønskjer å dela mappa \"{{folder}}\"."
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Urządzenie {{device}} ({{address}}) chce się połączyć. Zezwolić?",
|
||||
"Devices": "Urządzenia",
|
||||
"Disconnected": "Rozłączony",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "Odnajdywanie",
|
||||
"Documentation": "Dokumentacja",
|
||||
"Download Rate": "Prędkość pobierania",
|
||||
"Downloaded": "Pobrane",
|
||||
@@ -124,7 +124,7 @@
|
||||
"RAM Utilization": "Użycie pamięci RAM",
|
||||
"Random": "Losowo",
|
||||
"Relayed via": "Przekazane przez",
|
||||
"Relays": "Przekaźnik",
|
||||
"Relays": "Przekaźniki",
|
||||
"Release Notes": "Informacje o wydaniu",
|
||||
"Remove": "Usuń",
|
||||
"Rescan": "Skanuj ponownie",
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"Oldest First": "Mais antigo primeiro",
|
||||
"Options": "Opções",
|
||||
"Out of Sync": "Fora de sincronia",
|
||||
"Out of Sync Items": "Itens fora de sincronia",
|
||||
"Out of Sync Items": "Fora de sincronia",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite de velocidade de envio (KiB/s)",
|
||||
"Override Changes": "Sobrescrever mudanças",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Caminho para a pasta na máquina local. Será criado caso não exista. O caractere til (~) pode ser usado como um atalho para",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "O dispositivo {{device}} ({{address}}) quer conectar-se. Adiciono este novo dispositivo?",
|
||||
"Devices": "Dispositivos",
|
||||
"Disconnected": "Desconectado",
|
||||
"Discovery": "Busca",
|
||||
"Discovery": "Detecção",
|
||||
"Documentation": "Documentação",
|
||||
"Download Rate": "Velocidade de recepção",
|
||||
"Downloaded": "Recebido",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Edit Folder": "Editar pasta",
|
||||
"Editing": "Editando",
|
||||
"Enable UPnP": "Activar UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços separados por vírgulas (\"tcp://ip:porto\", \"tcp://máquina:porto\") ou \"dynamic\" para descobrir automaticamente os endereços.",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços separados por vírgulas (\"tcp://ip:porto\", \"tcp://máquina:porto\") ou \"dynamic\" para detectar automaticamente os endereços.",
|
||||
"Enter ignore patterns, one per line.": "Escreva os padrões de exclusão, um por linha.",
|
||||
"Error": "Erro",
|
||||
"External File Versioning": "Externa",
|
||||
@@ -71,8 +71,8 @@
|
||||
"GUI Authentication User": "Utilizador da autenticação na interface gráfica",
|
||||
"GUI Listen Addresses": "Endereço de escuta da interface gráfica",
|
||||
"Generate": "Gerar",
|
||||
"Global Discovery": "Busca global",
|
||||
"Global Discovery Server": "Servidor da busca global",
|
||||
"Global Discovery": "Detecção global",
|
||||
"Global Discovery Server": "Servidor de detecção global",
|
||||
"Global State": "Estado global",
|
||||
"Help": "Ajuda",
|
||||
"Home page": "Página do projecto",
|
||||
@@ -88,7 +88,7 @@
|
||||
"Last File Received": "Último ficheiro recebido",
|
||||
"Last seen": "Última vez que foi verificado",
|
||||
"Later": "Mais tarde",
|
||||
"Local Discovery": "Busca local",
|
||||
"Local Discovery": "Detecção local",
|
||||
"Local State": "Estado local",
|
||||
"Local State (Total)": "Estado local (total)",
|
||||
"Major Upgrade": "Actualização importante",
|
||||
@@ -123,8 +123,8 @@
|
||||
"Quick guide to supported patterns": "Guia rápido dos padrões suportados",
|
||||
"RAM Utilization": "Utilização da RAM",
|
||||
"Random": "Aleatória",
|
||||
"Relayed via": "Transmitido via",
|
||||
"Relays": "Transmissores",
|
||||
"Relayed via": "Retransmitido via",
|
||||
"Relays": "Retransmissores",
|
||||
"Release Notes": "Notas de lançamento",
|
||||
"Remove": "Remover",
|
||||
"Rescan": "Verificar agora",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"Address": "Adresă",
|
||||
"Addresses": "Adrese",
|
||||
"Advanced": "Advanced",
|
||||
"Advanced Configuration": "Advanced Configuration",
|
||||
"Advanced Configuration": "Configurari avansate",
|
||||
"All Data": "Toate Datele",
|
||||
"Allow Anonymous Usage Reporting?": "Permiteţi raportarea anonimă de folosire a aplicaţiei?",
|
||||
"Alphabetic": "Alphabetic",
|
||||
@@ -19,7 +19,7 @@
|
||||
"Anonymous Usage Reporting": "Raport Anonim despre Folosirea Aplicației",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Toate dispozitivele configurate pe un dispozitiv iniţiator vor fi adăugate şi pe acest dispozitiv. ",
|
||||
"Automatic upgrades": "Actualizare automată",
|
||||
"Be careful!": "Be careful!",
|
||||
"Be careful!": "Fii atent!",
|
||||
"Bugs": "Bug-uri",
|
||||
"CPU Utilization": "CPU ",
|
||||
"Changelog": "Noutăți",
|
||||
@@ -33,7 +33,7 @@
|
||||
"Copied from original": "Copiat din original",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright ©2015 Următorii Contribuitori:",
|
||||
"Delete": "Şterge",
|
||||
"Deleted": "Deleted",
|
||||
"Deleted": "Șters",
|
||||
"Device ID": "ID Dispozitiv",
|
||||
"Device Identification": "Identificare Dispozitiv",
|
||||
"Device Name": "Nume Dispozitiv",
|
||||
@@ -61,7 +61,7 @@
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Documentele sînt mutate într-un fișier .stversions conținînd versiuni datate atunci cînd sînt șterse sau înlocuite de Syncthing. ",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Fișierele sunt protejate de schimbările făcute pe alte dispozitive dar schimbările efectuate pe acest dispozitiv vor fi trimise catre restul grupului.",
|
||||
"Folder": "Folder",
|
||||
"Folder": "Mapă",
|
||||
"Folder ID": "ID Mapă",
|
||||
"Folder Master": "Master Măpi",
|
||||
"Folder Path": "Locaţie Mapei",
|
||||
@@ -107,14 +107,14 @@
|
||||
"OK": "OK",
|
||||
"Off": "Închis",
|
||||
"Oldest First": "Oldest First",
|
||||
"Options": "Options",
|
||||
"Options": "Opțiuni",
|
||||
"Out of Sync": "Out of Sync",
|
||||
"Out of Sync Items": "Elemente Nesincronizate",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limită Viteză de Upload (KB/s)",
|
||||
"Override Changes": "Suprascrie Schimbări",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Localizarea fișierului în acest computer. Dacă nu există, va fi creat. Tilda (~) înlocuiește ",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Locul unde vor fi stocate versiunile (a se lăsa neschimbat pentru fișierul .stversions din fișier). ",
|
||||
"Pause": "Pause",
|
||||
"Pause": "Pauză",
|
||||
"Paused": "Paused",
|
||||
"Please consult the release notes before performing a major upgrade.": "Please consult the release notes before performing a major upgrade.",
|
||||
"Please wait": "Aşteaptă",
|
||||
@@ -158,7 +158,7 @@
|
||||
"Source Code": "Cod Sursă",
|
||||
"Staggered File Versioning": "Versiuni eşalonate ale documentelor",
|
||||
"Start Browser": "Lansează Browser",
|
||||
"Statistics": "Statistics",
|
||||
"Statistics": "Statistici",
|
||||
"Stopped": "Oprit",
|
||||
"Support": "Suport Tehnic",
|
||||
"Sync Protocol Listen Addresses": "Adresa protocolului de sincronizare",
|
||||
@@ -186,7 +186,7 @@
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Câte zile să se păstreze o versiune (setează 0 pentru nelimitat)",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Numărul de zile pentru a păstra fișierele in urnă.Zero înseamnă permanent.",
|
||||
"The number of old versions to keep, per file.": "Numărul de versiuni vechi de salvat per fişier.",
|
||||
"The number of versions must be a number and cannot be blank.": "Numărul de versiuni trebuie să fie un număr şi nu poate fi gol.",
|
||||
"The path cannot be blank.": "Locația nu poate fi goală.",
|
||||
@@ -213,7 +213,7 @@
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Cînd adăugaţi un fişier nou, nu uitaţi că ID-ul fişierului va rămîne acelaşi pe toate dispozitivele. Iar literele mari sînt diferite de literele mici. ",
|
||||
"Yes": "Da",
|
||||
"You must keep at least one version.": "Trebuie să păstrezi cel puţin o versiune.",
|
||||
"days": "days",
|
||||
"days": "Zile",
|
||||
"full documentation": "toată documentaţia",
|
||||
"items": "obiecte",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{Dispozitivul}} vrea să transmită mapa {{Mapa}}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"A negative number of days doesn't make sense.": "一個負的天數並不合理。",
|
||||
"A new major version may not be compatible with previous versions.": "A new major version may not be compatible with previous versions.",
|
||||
"A new major version may not be compatible with previous versions.": "一個新的主要版本可能與以前的版本並不相容。",
|
||||
"API Key": "API 金鑰",
|
||||
"About": "關於",
|
||||
"Actions": "操作",
|
||||
@@ -10,8 +10,8 @@
|
||||
"Add new folder?": "新增資料夾?",
|
||||
"Address": "位址",
|
||||
"Addresses": "位址",
|
||||
"Advanced": "Advanced",
|
||||
"Advanced Configuration": "Advanced Configuration",
|
||||
"Advanced": "進階",
|
||||
"Advanced Configuration": "進階設定",
|
||||
"All Data": "全部資料",
|
||||
"Allow Anonymous Usage Reporting?": "允許匿名的使用資訊回報?",
|
||||
"Alphabetic": "字母順序",
|
||||
@@ -19,18 +19,18 @@
|
||||
"Anonymous Usage Reporting": "匿名的使用資訊回報",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "任何在引入者裝置所設置的裝置將會一併新增至此裝置",
|
||||
"Automatic upgrades": "自動升級",
|
||||
"Be careful!": "Be careful!",
|
||||
"Be careful!": "請小心!",
|
||||
"Bugs": "程式錯誤",
|
||||
"CPU Utilization": "CPU 使用",
|
||||
"Changelog": "更新日誌",
|
||||
"Clean out after": "Clean out after",
|
||||
"Clean out after": "於之後清空",
|
||||
"Close": "關閉",
|
||||
"Command": "指令",
|
||||
"Comment, when used at the start of a line": "註解,當輸入在一行的開頭時",
|
||||
"Compression": "壓縮",
|
||||
"Connection Error": "連線錯誤",
|
||||
"Copied from elsewhere": "從別處複製",
|
||||
"Copied from original": "Copied from original",
|
||||
"Copied from original": "從原來複製",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 下列貢獻者:",
|
||||
"Delete": "刪除",
|
||||
"Deleted": "已刪除",
|
||||
@@ -54,10 +54,10 @@
|
||||
"Enter ignore patterns, one per line.": "輸入忽略樣式,每行一種。",
|
||||
"Error": "錯誤",
|
||||
"External File Versioning": "外部檔案版本控制",
|
||||
"Failed Items": "Failed Items",
|
||||
"Failed Items": "失敗的項目",
|
||||
"File Pull Order": "提取檔案的順序",
|
||||
"File Versioning": "檔案版本控制",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "當改變時,檔案權限位元 File permission bits 會被忽略。用於 FAT 檔案系統上。",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "其他裝置做的改變不會影響到此裝置的檔案,但在此裝置上的變化將被發送到叢集中的其他部分。",
|
||||
@@ -80,7 +80,7 @@
|
||||
"Ignore Patterns": "忽略樣式",
|
||||
"Ignore Permissions": "忽略權限",
|
||||
"Incoming Rate Limit (KiB/s)": "連入速率限制 (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "不正確的設定可能會損壞你的資料夾內容,並引致 Syncthing 的不正當運作。",
|
||||
"Introducer": "引入者",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "反轉給定條件 (即:不要排除)",
|
||||
"Keep Versions": "保留歷史版本數",
|
||||
@@ -90,11 +90,11 @@
|
||||
"Later": "稍後",
|
||||
"Local Discovery": "本地探索",
|
||||
"Local State": "本地狀態",
|
||||
"Local State (Total)": "Local State (Total)",
|
||||
"Local State (Total)": "本地狀態 (總結)",
|
||||
"Major Upgrade": "重大更新",
|
||||
"Maximum Age": "最長保留時間",
|
||||
"Metadata Only": "僅中繼資料",
|
||||
"Minimum Free Disk Space": "Minimum Free Disk Space",
|
||||
"Minimum Free Disk Space": "最少閒置磁碟空間",
|
||||
"Move to top of queue": "移到隊列頂端",
|
||||
"Multi level wildcard (matches multiple directory levels)": "多階層萬用字元 (可比對多層資料夾)",
|
||||
"Never": "從未",
|
||||
@@ -114,8 +114,8 @@
|
||||
"Override Changes": "置換改變",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "資料夾在本地電腦的路徑。若資料夾不存在則會建立。波浪符號 (~) 可用作下列資料夾的捷徑:",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "儲存歷史版本的路徑 (若為空,則預設使用資料夾中的 .stversions 資料夾)。",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Paused",
|
||||
"Pause": "暫停",
|
||||
"Paused": "暫停",
|
||||
"Please consult the release notes before performing a major upgrade.": "執行重大升級前請先參閱版本資訊。",
|
||||
"Please wait": "請稍後",
|
||||
"Preview": "預覽",
|
||||
@@ -123,7 +123,7 @@
|
||||
"Quick guide to supported patterns": "可支援樣式的快速指南",
|
||||
"RAM Utilization": "記憶體使用",
|
||||
"Random": "隨機",
|
||||
"Relayed via": "Relayed via",
|
||||
"Relayed via": "中繼於",
|
||||
"Relays": "中繼點",
|
||||
"Release Notes": "版本資訊",
|
||||
"Remove": "移除",
|
||||
@@ -133,8 +133,8 @@
|
||||
"Restart": "重新啟動",
|
||||
"Restart Needed": "需要重新啟動",
|
||||
"Restarting": "正在重新啟動",
|
||||
"Resume": "Resume",
|
||||
"Reused": "Reused",
|
||||
"Resume": "繼續",
|
||||
"Reused": "重用",
|
||||
"Save": "儲存",
|
||||
"Scanning": "正在掃描",
|
||||
"Select the devices to share this folder with.": "選擇要共享這個資料夾的裝置。",
|
||||
@@ -181,18 +181,18 @@
|
||||
"The folder ID must be unique.": "資料夾識別碼必須為獨一無二的。",
|
||||
"The folder path cannot be blank.": "資料夾路徑不能空白。",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "使用下列的間隔:在第一個小時內每 30 秒保留一個版本,在第一天內每小時保留一個版本,在第 30 天內每一天保留一個版本,在達到最長保留時間前每一星期保留一個版本。",
|
||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
||||
"The following items could not be synchronized.": "以下項目不能被同步。",
|
||||
"The maximum age must be a number and cannot be blank.": "最長保留時間必須為一個數字且不得為空。",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "一個版本被保留的最長時間 (單位為天,若設定為 0 則表示永遠保留)。",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
"The number of days must be a number and cannot be blank.": "天數必須必須為一個數字且不得為空。",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "檔案在 trash can 中保留的日子。零表示永遠地保留。",
|
||||
"The number of old versions to keep, per file.": "每個檔案要保留的舊版本數量。",
|
||||
"The number of versions must be a number and cannot be blank.": "每個檔案要保留的舊版本數量必須是數字且不能為空白。",
|
||||
"The path cannot be blank.": "路徑不能空白。",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "The rate limit must be a non-negative number (0: no limit)",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "限制速率必須為非負的數字 (0: 不設限制)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "重新掃描間隔必須為一個非負數的秒數。",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "解決間題後,將會自動重試和同步。",
|
||||
"This is a major version upgrade.": "這是一個主要版本更新。",
|
||||
"Trash Can File Versioning": "Trash Can File Versioning",
|
||||
"Unknown": "未知",
|
||||
@@ -209,11 +209,11 @@
|
||||
"Version": "版本",
|
||||
"Versions Path": "歷史版本路徑",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "當檔案歷史版本的存留時間大於設定的最大值,或是其數量在一段時間內超出允許值時,則會被刪除。",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "當新增一個裝置時,請記住,這個裝置也必須被添加至另一邊。",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "當新增一個裝置時,務必記住,當前的這個裝置也同樣必須被添加至另一邊。",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "當新增一個資料夾時,請記住,資料夾識別碼是用來將裝置之間的資料夾綁定在一起的。它們有區分大小寫,且必須在所有裝置之間完全相同。",
|
||||
"Yes": "是",
|
||||
"You must keep at least one version.": "您必須保留至少一個版本。",
|
||||
"days": "days",
|
||||
"days": "日",
|
||||
"full documentation": "完整說明文件",
|
||||
"items": "個項目",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想要分享資料夾 \"{{folder}}\"。"
|
||||
|
||||
+1
-1
@@ -469,7 +469,7 @@
|
||||
<th><span class="fa fa-fw fa-thumbs-o-up"></span> <span translate>Introducer</span></th>
|
||||
<td translate class="text-right">Yes</td>
|
||||
</tr>
|
||||
<tr ng-if="connections[deviceCfg.deviceID].version">
|
||||
<tr ng-if="connections[deviceCfg.deviceID].clientVersion">
|
||||
<th><span class="fa fa-fw fa-tag"></span> <span translate>Version</span></th>
|
||||
<td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
|
||||
</tr>
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
<li class="auto-generated">Frank Isemann</li>
|
||||
<li class="auto-generated">Gilli Sigurdsson</li>
|
||||
<li class="auto-generated">Jacek Szafarkiewicz</li>
|
||||
<li class="auto-generated">Jake Peterson</li>
|
||||
<li class="auto-generated">Jakob Borg</li>
|
||||
<li class="auto-generated">James Patterson</li>
|
||||
<li class="auto-generated">Jaroslav Malec</li>
|
||||
@@ -71,11 +72,13 @@
|
||||
<li class="auto-generated">Piotr Bejda</li>
|
||||
<li class="auto-generated">Ryan Sullivan</li>
|
||||
<li class="auto-generated">Sergey Mishin</li>
|
||||
<li class="auto-generated">Stefan Kuntz</li>
|
||||
<li class="auto-generated">Stefan Tatschner</li>
|
||||
<li class="auto-generated">Tim Abell</li>
|
||||
<li class="auto-generated">Tobias Nygren</li>
|
||||
<li class="auto-generated">Tomas Cerveny</li>
|
||||
<li class="auto-generated">Tully Robinson</li>
|
||||
<li class="auto-generated">Tyler Brazier</li>
|
||||
<li class="auto-generated">Veeti Paananen</li>
|
||||
<li class="auto-generated">Vil Brekin</li>
|
||||
<li class="auto-generated">Yannic A.</li>
|
||||
|
||||
+18
-18
File diff suppressed because one or more lines are too long
@@ -25,10 +25,6 @@ type Interface interface {
|
||||
Error() error
|
||||
}
|
||||
|
||||
type readerFrom interface {
|
||||
ReadFrom([]byte) (int, net.Addr, error)
|
||||
}
|
||||
|
||||
type errorHolder struct {
|
||||
err error
|
||||
mut stdsync.Mutex // uses stdlib sync as I want this to be trivially embeddable, and there is no risk of blocking
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
|
||||
type Multicast struct {
|
||||
*suture.Supervisor
|
||||
addr *net.UDPAddr
|
||||
inbox chan []byte
|
||||
outbox chan recv
|
||||
mr *multicastReader
|
||||
|
||||
+70
-511
@@ -8,43 +8,46 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
OldestHandledVersion = 5
|
||||
OldestHandledVersion = 10
|
||||
CurrentVersion = 12
|
||||
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultDiscoveryServers should be substituted when the configuration
|
||||
// contains <globalAnnounceServer>default</globalAnnounceServer>. This is
|
||||
// DefaultDiscoveryServersV4 should be substituted when the configuration
|
||||
// contains <globalAnnounceServer>default-v4</globalAnnounceServer>. This is
|
||||
// done by the "consumer" of the configuration, as we don't want these
|
||||
// saved to the config.
|
||||
DefaultDiscoveryServers = []string{
|
||||
DefaultDiscoveryServersV4 = []string{
|
||||
"https://discovery-v4-1.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 194.126.249.5, Sweden
|
||||
"https://discovery-v4-2.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 45.55.230.38, USA
|
||||
"https://discovery-v4-2.syncthing.net/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 45.55.230.38, USA
|
||||
"https://discovery-v4-3.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 128.199.95.124, Singapore
|
||||
}
|
||||
// DefaultDiscoveryServersV6 should be substituted when the configuration
|
||||
// contains <globalAnnounceServer>default-v6</globalAnnounceServer>.
|
||||
DefaultDiscoveryServersV6 = []string{
|
||||
"https://discovery-v6-1.syncthing.net/?id=SR7AARM-TCBUZ5O-VFAXY4D-CECGSDE-3Q6IZ4G-XG7AH75-OBIXJQV-QJ6NLQA", // 2001:470:28:4d6::5, Sweden
|
||||
"https://discovery-v6-2.syncthing.net/?id=AQEHEO2-XOS7QRA-X2COH5K-PO6OPVA-EWOSEGO-KZFMD32-XJ4ZV46-CUUVKAS", // 2604:a880:800:10::182:a001, USA
|
||||
"https://discovery-v6-2.syncthing.net/?id=DVU36WY-H3LVZHW-E6LLFRE-YAFN5EL-HILWRYP-OC2M47J-Z4PE62Y-ADIBDQC", // 2604:a880:800:10::182:a001, USA
|
||||
"https://discovery-v6-3.syncthing.net/?id=7WT2BVR-FX62ZOW-TNVVW25-6AHFJGD-XEXQSBW-VO3MPL2-JBTLL4T-P4572Q4", // 2400:6180:0:d0::d9:d001, Singapore
|
||||
}
|
||||
// DefaultDiscoveryServers should be substituted when the configuration
|
||||
// contains <globalAnnounceServer>default</globalAnnounceServer>.
|
||||
DefaultDiscoveryServers = append(DefaultDiscoveryServersV4, DefaultDiscoveryServersV6...)
|
||||
|
||||
// DefaultDiscoveryServersIP is used by the usage reporting.
|
||||
// XXX: Detect Android, and use this is we still don't have working DNS?
|
||||
@@ -58,6 +61,48 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func New(myID protocol.DeviceID) Configuration {
|
||||
var cfg Configuration
|
||||
cfg.Version = CurrentVersion
|
||||
cfg.OriginalVersion = CurrentVersion
|
||||
|
||||
setDefaults(&cfg)
|
||||
setDefaults(&cfg.Options)
|
||||
setDefaults(&cfg.GUI)
|
||||
|
||||
cfg.prepare(myID)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
|
||||
var cfg Configuration
|
||||
|
||||
setDefaults(&cfg)
|
||||
setDefaults(&cfg.Options)
|
||||
setDefaults(&cfg.GUI)
|
||||
|
||||
err := xml.NewDecoder(r).Decode(&cfg)
|
||||
cfg.OriginalVersion = cfg.Version
|
||||
|
||||
cfg.prepare(myID)
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func ReadJSON(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
|
||||
var cfg Configuration
|
||||
|
||||
setDefaults(&cfg)
|
||||
setDefaults(&cfg.Options)
|
||||
setDefaults(&cfg.GUI)
|
||||
|
||||
err := json.NewDecoder(r).Decode(&cfg)
|
||||
cfg.OriginalVersion = cfg.Version
|
||||
|
||||
cfg.prepare(myID)
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
Version int `xml:"version,attr" json:"version"`
|
||||
Folders []FolderConfiguration `xml:"folder" json:"folders"`
|
||||
@@ -94,298 +139,6 @@ func (cfg Configuration) Copy() Configuration {
|
||||
return newCfg
|
||||
}
|
||||
|
||||
type FolderConfiguration struct {
|
||||
ID string `xml:"id,attr" json:"id"`
|
||||
RawPath string `xml:"path,attr" json:"path"`
|
||||
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
|
||||
ReadOnly bool `xml:"ro,attr" json:"readOnly"`
|
||||
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
|
||||
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
|
||||
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
|
||||
MinDiskFreePct float64 `xml:"minDiskFreePct" json:"minDiskFreePct"`
|
||||
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
|
||||
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
|
||||
Pullers int `xml:"pullers" json:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
|
||||
Hashers int `xml:"hashers" json:"hashers"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
|
||||
Order PullOrder `xml:"order" json:"order"`
|
||||
IgnoreDelete bool `xml:"ignoreDelete" json:"ignoreDelete"`
|
||||
ScanProgressIntervalS int `xml:"scanProgressIntervalS" json:"scanProgressIntervalS"` // Set to a negative value to disable. Value of 0 will get replaced with value of 2 (default value)
|
||||
PullerSleepS int `xml:"pullerSleepS" json:"pullerSleepS"`
|
||||
PullerPauseS int `xml:"pullerPauseS" json:"pullerPauseS"`
|
||||
MaxConflicts int `xml:"maxConflicts" json:"maxConflicts"`
|
||||
|
||||
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
|
||||
}
|
||||
|
||||
func (f FolderConfiguration) Copy() FolderConfiguration {
|
||||
c := f
|
||||
c.Devices = make([]FolderDeviceConfiguration, len(f.Devices))
|
||||
copy(c.Devices, f.Devices)
|
||||
return c
|
||||
}
|
||||
|
||||
func (f FolderConfiguration) Path() string {
|
||||
// This is intentionally not a pointer method, because things like
|
||||
// cfg.Folders["default"].Path() should be valid.
|
||||
|
||||
// Attempt tilde expansion; leave unchanged in case of error
|
||||
if path, err := osutil.ExpandTilde(f.RawPath); err == nil {
|
||||
f.RawPath = path
|
||||
}
|
||||
|
||||
// Attempt absolutification; leave unchanged in case of error
|
||||
if !filepath.IsAbs(f.RawPath) {
|
||||
// Abs() looks like a fairly expensive syscall on Windows, while
|
||||
// IsAbs() is a whole bunch of string mangling. I think IsAbs() may be
|
||||
// somewhat faster in the general case, hence the outer if...
|
||||
if path, err := filepath.Abs(f.RawPath); err == nil {
|
||||
f.RawPath = path
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to enable long filename support on Windows. We may still not
|
||||
// have an absolute path here if the previous steps failed.
|
||||
if runtime.GOOS == "windows" && filepath.IsAbs(f.RawPath) && !strings.HasPrefix(f.RawPath, `\\`) {
|
||||
return `\\?\` + f.RawPath
|
||||
}
|
||||
|
||||
return f.RawPath
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) CreateMarker() error {
|
||||
if !f.HasMarker() {
|
||||
marker := filepath.Join(f.Path(), ".stfolder")
|
||||
fd, err := os.Create(marker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fd.Close()
|
||||
osutil.HideFile(marker)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) HasMarker() bool {
|
||||
_, err := os.Stat(filepath.Join(f.Path(), ".stfolder"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
|
||||
deviceIDs := make([]protocol.DeviceID, len(f.Devices))
|
||||
for i, n := range f.Devices {
|
||||
deviceIDs[i] = n.DeviceID
|
||||
}
|
||||
return deviceIDs
|
||||
}
|
||||
|
||||
type VersioningConfiguration struct {
|
||||
Type string `xml:"type,attr" json:"type"`
|
||||
Params map[string]string `json:"params"`
|
||||
}
|
||||
|
||||
type InternalVersioningConfiguration struct {
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
Params []InternalParam `xml:"param"`
|
||||
}
|
||||
|
||||
type InternalParam struct {
|
||||
Key string `xml:"key,attr"`
|
||||
Val string `xml:"val,attr"`
|
||||
}
|
||||
|
||||
func (c *VersioningConfiguration) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
var tmp InternalVersioningConfiguration
|
||||
tmp.Type = c.Type
|
||||
for k, v := range c.Params {
|
||||
tmp.Params = append(tmp.Params, InternalParam{k, v})
|
||||
}
|
||||
|
||||
return e.EncodeElement(tmp, start)
|
||||
|
||||
}
|
||||
|
||||
func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var tmp InternalVersioningConfiguration
|
||||
err := d.DecodeElement(&tmp, &start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type = tmp.Type
|
||||
c.Params = make(map[string]string, len(tmp.Params))
|
||||
for _, p := range tmp.Params {
|
||||
c.Params[p.Key] = p.Val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DeviceConfiguration struct {
|
||||
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
|
||||
Name string `xml:"name,attr,omitempty" json:"name"`
|
||||
Addresses []string `xml:"address,omitempty" json:"addresses"`
|
||||
Compression protocol.Compression `xml:"compression,attr" json:"compression"`
|
||||
CertName string `xml:"certName,attr,omitempty" json:"certName"`
|
||||
Introducer bool `xml:"introducer,attr" json:"introducer"`
|
||||
}
|
||||
|
||||
func (orig DeviceConfiguration) Copy() DeviceConfiguration {
|
||||
c := orig
|
||||
c.Addresses = make([]string, len(orig.Addresses))
|
||||
copy(c.Addresses, orig.Addresses)
|
||||
return c
|
||||
}
|
||||
|
||||
type FolderDeviceConfiguration struct {
|
||||
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
|
||||
}
|
||||
|
||||
type OptionsConfiguration struct {
|
||||
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
|
||||
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"`
|
||||
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
|
||||
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
|
||||
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
|
||||
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027"`
|
||||
RelayServers []string `xml:"relayServer" json:"relayServers" default:"dynamic+https://relays.syncthing.net/endpoint"`
|
||||
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
|
||||
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
|
||||
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
|
||||
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
|
||||
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
|
||||
RelayWithoutGlobalAnn bool `xml:"relayWithoutGlobalAnn" json:"relayWithoutGlobalAnn" default:"false"`
|
||||
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
|
||||
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
|
||||
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"`
|
||||
UPnPRenewalM int `xml:"upnpRenewalMinutes" json:"upnpRenewalMinutes" default:"30"`
|
||||
UPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"upnpTimeoutSeconds" default:"10"`
|
||||
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
|
||||
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
|
||||
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
|
||||
URPostInsecurely bool `xml:"urPostInsecurely" json:"urPostInsecurely" default:"false"` // For testing
|
||||
URInitialDelayS int `xml:"urInitialDelayS" json:"urInitialDelayS" default:"1800"`
|
||||
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
|
||||
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12"` // 0 for off
|
||||
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
|
||||
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"true"`
|
||||
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
|
||||
SymlinksEnabled bool `xml:"symlinksEnabled" json:"symlinksEnabled" default:"true"`
|
||||
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
|
||||
DatabaseBlockCacheMiB int `xml:"databaseBlockCacheMiB" json:"databaseBlockCacheMiB" default:"0"`
|
||||
MinHomeDiskFreePct float64 `xml:"minHomeDiskFreePct" json:"minHomeDiskFreePct" default:"1"`
|
||||
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=30"`
|
||||
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
|
||||
}
|
||||
|
||||
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
|
||||
c := orig
|
||||
c.ListenAddress = make([]string, len(orig.ListenAddress))
|
||||
copy(c.ListenAddress, orig.ListenAddress)
|
||||
c.GlobalAnnServers = make([]string, len(orig.GlobalAnnServers))
|
||||
copy(c.GlobalAnnServers, orig.GlobalAnnServers)
|
||||
return c
|
||||
}
|
||||
|
||||
type GUIConfiguration struct {
|
||||
Enabled bool `xml:"enabled,attr" json:"enabled" default:"true"`
|
||||
RawAddress string `xml:"address" json:"address" default:"127.0.0.1:8384"`
|
||||
User string `xml:"user,omitempty" json:"user"`
|
||||
Password string `xml:"password,omitempty" json:"password"`
|
||||
RawUseTLS bool `xml:"tls,attr" json:"useTLS"`
|
||||
RawAPIKey string `xml:"apikey,omitempty" json:"apiKey"`
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) Address() string {
|
||||
if override := os.Getenv("STGUIADDRESS"); override != "" {
|
||||
// This value may be of the form "scheme://address:port" or just
|
||||
// "address:port". We need to chop off the scheme. We try to parse it as
|
||||
// an URL if it contains a slash. If that fails, return it as is and let
|
||||
// some other error handling handle it.
|
||||
|
||||
if strings.Contains(override, "/") {
|
||||
url, err := url.Parse(override)
|
||||
if err != nil {
|
||||
return override
|
||||
}
|
||||
return url.Host
|
||||
}
|
||||
|
||||
return override
|
||||
}
|
||||
|
||||
return c.RawAddress
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) UseTLS() bool {
|
||||
if override := os.Getenv("STGUIADDRESS"); override != "" {
|
||||
return strings.HasPrefix(override, "https:")
|
||||
}
|
||||
return c.RawUseTLS
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) URL() string {
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: c.Address(),
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
if c.UseTLS() {
|
||||
u.Scheme = "https"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(u.Host, ":") {
|
||||
// Empty host, i.e. ":port", use IPv4 localhost
|
||||
u.Host = "127.0.0.1" + u.Host
|
||||
} else if strings.HasPrefix(u.Host, "0.0.0.0:") {
|
||||
// IPv4 all zeroes host, convert to IPv4 localhost
|
||||
u.Host = "127.0.0.1" + u.Host[7:]
|
||||
} else if strings.HasPrefix(u.Host, "[::]:") {
|
||||
// IPv6 all zeroes host, convert to IPv6 localhost
|
||||
u.Host = "[::1]" + u.Host[4:]
|
||||
}
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) APIKey() string {
|
||||
if override := os.Getenv("STGUIAPIKEY"); override != "" {
|
||||
return override
|
||||
}
|
||||
return c.RawAPIKey
|
||||
}
|
||||
|
||||
func New(myID protocol.DeviceID) Configuration {
|
||||
var cfg Configuration
|
||||
cfg.Version = CurrentVersion
|
||||
cfg.OriginalVersion = CurrentVersion
|
||||
|
||||
setDefaults(&cfg)
|
||||
setDefaults(&cfg.Options)
|
||||
setDefaults(&cfg.GUI)
|
||||
|
||||
cfg.prepare(myID)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func ReadXML(r io.Reader, myID protocol.DeviceID) (Configuration, error) {
|
||||
var cfg Configuration
|
||||
|
||||
setDefaults(&cfg)
|
||||
setDefaults(&cfg.Options)
|
||||
setDefaults(&cfg.GUI)
|
||||
|
||||
err := xml.NewDecoder(r).Decode(&cfg)
|
||||
cfg.OriginalVersion = cfg.Version
|
||||
|
||||
cfg.prepare(myID)
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (cfg *Configuration) WriteXML(w io.Writer) error {
|
||||
e := xml.NewEncoder(w)
|
||||
e.Indent("", " ")
|
||||
@@ -400,44 +153,22 @@ func (cfg *Configuration) WriteXML(w io.Writer) error {
|
||||
func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
fillNilSlices(&cfg.Options)
|
||||
|
||||
// Initialize an empty slices
|
||||
// Initialize any empty slices
|
||||
if cfg.Folders == nil {
|
||||
cfg.Folders = []FolderConfiguration{}
|
||||
}
|
||||
if cfg.IgnoredDevices == nil {
|
||||
cfg.IgnoredDevices = []protocol.DeviceID{}
|
||||
}
|
||||
if cfg.Options.AlwaysLocalNets == nil {
|
||||
cfg.Options.AlwaysLocalNets = []string{}
|
||||
}
|
||||
|
||||
// Check for missing, bad or duplicate folder ID:s
|
||||
var seenFolders = map[string]*FolderConfiguration{}
|
||||
for i := range cfg.Folders {
|
||||
folder := &cfg.Folders[i]
|
||||
|
||||
if len(folder.RawPath) == 0 {
|
||||
folder.Invalid = "no directory configured"
|
||||
continue
|
||||
}
|
||||
|
||||
// The reason it's done like this:
|
||||
// C: -> C:\ -> C:\ (issue that this is trying to fix)
|
||||
// C:\somedir -> C:\somedir\ -> C:\somedir
|
||||
// C:\somedir\ -> C:\somedir\\ -> C:\somedir
|
||||
// This way in the tests, we get away without OS specific separators
|
||||
// in the test configs.
|
||||
folder.RawPath = filepath.Dir(folder.RawPath + string(filepath.Separator))
|
||||
if folder.RawPath[len(folder.RawPath)-1] != filepath.Separator {
|
||||
folder.RawPath = folder.RawPath + string(filepath.Separator)
|
||||
}
|
||||
|
||||
if folder.ID == "" {
|
||||
folder.ID = "default"
|
||||
}
|
||||
|
||||
if folder.RescanIntervalS > MaxRescanIntervalS {
|
||||
folder.RescanIntervalS = MaxRescanIntervalS
|
||||
} else if folder.RescanIntervalS < 0 {
|
||||
folder.RescanIntervalS = 0
|
||||
}
|
||||
folder.prepare()
|
||||
|
||||
if seen, ok := seenFolders[folder.ID]; ok {
|
||||
l.Warnf("Multiple folders with ID %q; disabling", folder.ID)
|
||||
@@ -451,43 +182,18 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
|
||||
cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers)
|
||||
|
||||
if cfg.Version < OldestHandledVersion {
|
||||
if cfg.Version > 0 && cfg.Version < OldestHandledVersion {
|
||||
l.Warnf("Configuration version %d is deprecated. Attempting best effort conversion, but please verify manually.", cfg.Version)
|
||||
}
|
||||
|
||||
// Upgrade configuration versions as appropriate
|
||||
if cfg.Version <= 5 {
|
||||
convertV5V6(cfg)
|
||||
}
|
||||
if cfg.Version == 6 {
|
||||
convertV6V7(cfg)
|
||||
}
|
||||
if cfg.Version == 7 {
|
||||
convertV7V8(cfg)
|
||||
}
|
||||
if cfg.Version == 8 {
|
||||
convertV8V9(cfg)
|
||||
}
|
||||
if cfg.Version == 9 {
|
||||
convertV9V10(cfg)
|
||||
}
|
||||
if cfg.Version == 10 {
|
||||
if cfg.Version <= 10 {
|
||||
convertV10V11(cfg)
|
||||
}
|
||||
if cfg.Version == 11 {
|
||||
convertV11V12(cfg)
|
||||
}
|
||||
|
||||
// Hash old cleartext passwords
|
||||
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0)
|
||||
if err != nil {
|
||||
l.Warnln("bcrypting password:", err)
|
||||
} else {
|
||||
cfg.GUI.Password = string(hash)
|
||||
}
|
||||
}
|
||||
|
||||
// Build a list of available devices
|
||||
existingDevices := make(map[protocol.DeviceID]bool)
|
||||
for _, device := range cfg.Devices {
|
||||
@@ -533,49 +239,18 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
|
||||
}
|
||||
}
|
||||
|
||||
// ChangeRequiresRestart returns true if updating the configuration requires a
|
||||
// complete restart.
|
||||
func ChangeRequiresRestart(from, to Configuration) bool {
|
||||
// Adding, removing or changing folders requires restart
|
||||
if !reflect.DeepEqual(from.Folders, to.Folders) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Removing a device requres restart
|
||||
toDevs := make(map[protocol.DeviceID]bool, len(from.Devices))
|
||||
for _, dev := range to.Devices {
|
||||
toDevs[dev.DeviceID] = true
|
||||
}
|
||||
for _, dev := range from.Devices {
|
||||
if _, ok := toDevs[dev.DeviceID]; !ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Changing usage reporting to on or off does not require a restart.
|
||||
to.Options.URAccepted = from.Options.URAccepted
|
||||
to.Options.URUniqueID = from.Options.URUniqueID
|
||||
|
||||
// All of the generic options require restart
|
||||
if !reflect.DeepEqual(from.Options, to.Options) || !reflect.DeepEqual(from.GUI, to.GUI) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func convertV11V12(cfg *Configuration) {
|
||||
// Change listen address schema
|
||||
for i, addr := range cfg.Options.ListenAddress {
|
||||
if len(addr) > 0 && !strings.HasPrefix(addr, "tcp://") {
|
||||
cfg.Options.ListenAddress[i] = fmt.Sprintf("tcp://%s", addr)
|
||||
cfg.Options.ListenAddress[i] = tcpAddr(addr)
|
||||
}
|
||||
}
|
||||
|
||||
for i, device := range cfg.Devices {
|
||||
for j, addr := range device.Addresses {
|
||||
if addr != "dynamic" && addr != "" {
|
||||
cfg.Devices[i].Addresses[j] = fmt.Sprintf("tcp://%s", addr)
|
||||
cfg.Devices[i].Addresses[j] = tcpAddr(addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -623,52 +298,6 @@ func convertV10V11(cfg *Configuration) {
|
||||
cfg.Version = 11
|
||||
}
|
||||
|
||||
func convertV9V10(cfg *Configuration) {
|
||||
// Enable auto normalization on existing folders.
|
||||
for i := range cfg.Folders {
|
||||
cfg.Folders[i].AutoNormalize = true
|
||||
}
|
||||
cfg.Version = 10
|
||||
}
|
||||
|
||||
func convertV8V9(cfg *Configuration) {
|
||||
// Compression is interpreted and serialized differently, but no enforced
|
||||
// changes. Still need a new version number since the compression stuff
|
||||
// isn't understandable by earlier versions.
|
||||
cfg.Version = 9
|
||||
}
|
||||
|
||||
func convertV7V8(cfg *Configuration) {
|
||||
// Add IPv6 announce server
|
||||
if len(cfg.Options.GlobalAnnServers) == 1 && cfg.Options.GlobalAnnServers[0] == "udp4://announce.syncthing.net:22026" {
|
||||
cfg.Options.GlobalAnnServers = append(cfg.Options.GlobalAnnServers, "udp6://announce-v6.syncthing.net:22026")
|
||||
}
|
||||
|
||||
cfg.Version = 8
|
||||
}
|
||||
|
||||
func convertV6V7(cfg *Configuration) {
|
||||
// Migrate announce server addresses to the new URL based format
|
||||
for i := range cfg.Options.GlobalAnnServers {
|
||||
cfg.Options.GlobalAnnServers[i] = "udp4://" + cfg.Options.GlobalAnnServers[i]
|
||||
}
|
||||
|
||||
cfg.Version = 7
|
||||
}
|
||||
|
||||
func convertV5V6(cfg *Configuration) {
|
||||
// Added ".stfolder" file at folder roots to identify mount issues
|
||||
// Doesn't affect the config itself, but uses config migrations to identify
|
||||
// the migration point.
|
||||
for _, folder := range Wrap("", *cfg).Folders() {
|
||||
// Best attempt, if it fails, it fails, the user will have to fix
|
||||
// it up manually, as the repo will not get started.
|
||||
folder.CreateMarker()
|
||||
}
|
||||
|
||||
cfg.Version = 6
|
||||
}
|
||||
|
||||
func setDefaults(data interface{}) error {
|
||||
s := reflect.ValueOf(data).Elem()
|
||||
t := s.Type()
|
||||
@@ -808,30 +437,6 @@ loop:
|
||||
return devices[0:count]
|
||||
}
|
||||
|
||||
type DeviceConfigurationList []DeviceConfiguration
|
||||
|
||||
func (l DeviceConfigurationList) Less(a, b int) bool {
|
||||
return l[a].DeviceID.Compare(l[b].DeviceID) == -1
|
||||
}
|
||||
func (l DeviceConfigurationList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
func (l DeviceConfigurationList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
type FolderDeviceConfigurationList []FolderDeviceConfiguration
|
||||
|
||||
func (l FolderDeviceConfigurationList) Less(a, b int) bool {
|
||||
return l[a].DeviceID.Compare(l[b].DeviceID) == -1
|
||||
}
|
||||
func (l FolderDeviceConfigurationList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
func (l FolderDeviceConfigurationList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
// randomCharset contains the characters that can make up a randomString().
|
||||
const randomCharset = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
|
||||
|
||||
@@ -845,56 +450,10 @@ func randomString(l int) string {
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
type PullOrder int
|
||||
|
||||
const (
|
||||
OrderRandom PullOrder = iota // default is random
|
||||
OrderAlphabetic
|
||||
OrderSmallestFirst
|
||||
OrderLargestFirst
|
||||
OrderOldestFirst
|
||||
OrderNewestFirst
|
||||
)
|
||||
|
||||
func (o PullOrder) String() string {
|
||||
switch o {
|
||||
case OrderRandom:
|
||||
return "random"
|
||||
case OrderAlphabetic:
|
||||
return "alphabetic"
|
||||
case OrderSmallestFirst:
|
||||
return "smallestFirst"
|
||||
case OrderLargestFirst:
|
||||
return "largestFirst"
|
||||
case OrderOldestFirst:
|
||||
return "oldestFirst"
|
||||
case OrderNewestFirst:
|
||||
return "newestFirst"
|
||||
default:
|
||||
return "unknown"
|
||||
func tcpAddr(host string) string {
|
||||
u := url.URL{
|
||||
Scheme: "tcp",
|
||||
Host: host,
|
||||
}
|
||||
}
|
||||
|
||||
func (o PullOrder) MarshalText() ([]byte, error) {
|
||||
return []byte(o.String()), nil
|
||||
}
|
||||
|
||||
func (o *PullOrder) UnmarshalText(bs []byte) error {
|
||||
switch string(bs) {
|
||||
case "random":
|
||||
*o = OrderRandom
|
||||
case "alphabetic":
|
||||
*o = OrderAlphabetic
|
||||
case "smallestFirst":
|
||||
*o = OrderSmallestFirst
|
||||
case "largestFirst":
|
||||
*o = OrderLargestFirst
|
||||
case "oldestFirst":
|
||||
*o = OrderOldestFirst
|
||||
case "newestFirst":
|
||||
*o = OrderNewestFirst
|
||||
default:
|
||||
*o = OrderRandom
|
||||
}
|
||||
return nil
|
||||
return u.String()
|
||||
}
|
||||
|
||||
+15
-80
@@ -56,12 +56,12 @@ func TestDefaultValues(t *testing.T) {
|
||||
ProgressUpdateIntervalS: 5,
|
||||
SymlinksEnabled: true,
|
||||
LimitBandwidthInLan: false,
|
||||
DatabaseBlockCacheMiB: 0,
|
||||
MinHomeDiskFreePct: 1,
|
||||
URURL: "https://data.syncthing.net/newdata",
|
||||
URInitialDelayS: 1800,
|
||||
URPostInsecurely: false,
|
||||
ReleasesURL: "https://api.github.com/repos/syncthing/syncthing/releases?per_page=30",
|
||||
AlwaysLocalNets: []string{},
|
||||
}
|
||||
|
||||
cfg := New(device1)
|
||||
@@ -91,7 +91,7 @@ func TestDeviceConfig(t *testing.T) {
|
||||
expectedFolders := []FolderConfiguration{
|
||||
{
|
||||
ID: "test",
|
||||
RawPath: "testdata" + string(filepath.Separator),
|
||||
RawPath: "testdata",
|
||||
Devices: []FolderDeviceConfiguration{{DeviceID: device1}, {DeviceID: device4}},
|
||||
ReadOnly: true,
|
||||
RescanIntervalS: 600,
|
||||
@@ -103,6 +103,18 @@ func TestDeviceConfig(t *testing.T) {
|
||||
MaxConflicts: -1,
|
||||
},
|
||||
}
|
||||
|
||||
// The cachedPath will have been resolved to an absolute path,
|
||||
// depending on where the tests are running. Zero it out so we don't
|
||||
// fail based on that.
|
||||
for i := range cfg.Folders {
|
||||
cfg.Folders[i].cachedPath = ""
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
expectedFolders[0].RawPath += string(filepath.Separator)
|
||||
}
|
||||
|
||||
expectedDevices := []DeviceConfiguration{
|
||||
{
|
||||
DeviceID: device1,
|
||||
@@ -174,12 +186,12 @@ func TestOverriddenValues(t *testing.T) {
|
||||
ProgressUpdateIntervalS: 10,
|
||||
SymlinksEnabled: false,
|
||||
LimitBandwidthInLan: true,
|
||||
DatabaseBlockCacheMiB: 42,
|
||||
MinHomeDiskFreePct: 5.2,
|
||||
URURL: "https://localhost/newdata",
|
||||
URInitialDelayS: 800,
|
||||
URPostInsecurely: true,
|
||||
ReleasesURL: "https://localhost/releases",
|
||||
AlwaysLocalNets: []string{},
|
||||
}
|
||||
|
||||
cfg, err := Load("testdata/overridenvalues.xml", device1)
|
||||
@@ -458,83 +470,6 @@ func TestPrepare(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiresRestart(t *testing.T) {
|
||||
wr, err := Load("testdata/v6.xml", device1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg := wr.cfg
|
||||
|
||||
if ChangeRequiresRestart(cfg, cfg) {
|
||||
t.Error("No change does not require restart")
|
||||
}
|
||||
|
||||
newCfg := cfg
|
||||
newCfg.Devices = append(newCfg.Devices, DeviceConfiguration{
|
||||
DeviceID: device3,
|
||||
})
|
||||
if ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("Adding a device does not require restart")
|
||||
}
|
||||
|
||||
newCfg = cfg
|
||||
newCfg.Devices = newCfg.Devices[:len(newCfg.Devices)-1]
|
||||
if !ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("Removing a device requires restart")
|
||||
}
|
||||
|
||||
newCfg = cfg
|
||||
newCfg.Folders = append(newCfg.Folders, FolderConfiguration{
|
||||
ID: "t1",
|
||||
RawPath: "t1",
|
||||
})
|
||||
if !ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("Adding a folder requires restart")
|
||||
}
|
||||
|
||||
newCfg = cfg
|
||||
newCfg.Folders = newCfg.Folders[:len(newCfg.Folders)-1]
|
||||
if !ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("Removing a folder requires restart")
|
||||
}
|
||||
|
||||
newCfg = cfg
|
||||
newFolders := make([]FolderConfiguration, len(cfg.Folders))
|
||||
copy(newFolders, cfg.Folders)
|
||||
newCfg.Folders = newFolders
|
||||
if ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("No changes done yet")
|
||||
}
|
||||
newCfg.Folders[0].RawPath = "different"
|
||||
if !ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("Changing a folder requires restart")
|
||||
}
|
||||
|
||||
newCfg = cfg
|
||||
newDevices := make([]DeviceConfiguration, len(cfg.Devices))
|
||||
copy(newDevices, cfg.Devices)
|
||||
newCfg.Devices = newDevices
|
||||
if ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("No changes done yet")
|
||||
}
|
||||
newCfg.Devices[0].Name = "different"
|
||||
if ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("Changing a device does not require restart")
|
||||
}
|
||||
|
||||
newCfg = cfg
|
||||
newCfg.Options.GlobalAnnEnabled = !cfg.Options.GlobalAnnEnabled
|
||||
if !ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("Changing general options requires restart")
|
||||
}
|
||||
|
||||
newCfg = cfg
|
||||
newCfg.GUI.RawUseTLS = !cfg.GUI.RawUseTLS
|
||||
if !ChangeRequiresRestart(cfg, newCfg) {
|
||||
t.Error("Changing GUI options requires restart")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
wrapper, err := Load("testdata/example.xml", device1)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package config
|
||||
|
||||
import "github.com/syncthing/syncthing/lib/protocol"
|
||||
|
||||
type DeviceConfiguration struct {
|
||||
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
|
||||
Name string `xml:"name,attr,omitempty" json:"name"`
|
||||
Addresses []string `xml:"address,omitempty" json:"addresses"`
|
||||
Compression protocol.Compression `xml:"compression,attr" json:"compression"`
|
||||
CertName string `xml:"certName,attr,omitempty" json:"certName"`
|
||||
Introducer bool `xml:"introducer,attr" json:"introducer"`
|
||||
}
|
||||
|
||||
func NewDeviceConfiguration(id protocol.DeviceID, name string) DeviceConfiguration {
|
||||
return DeviceConfiguration{
|
||||
DeviceID: id,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (orig DeviceConfiguration) Copy() DeviceConfiguration {
|
||||
c := orig
|
||||
c.Addresses = make([]string, len(orig.Addresses))
|
||||
copy(c.Addresses, orig.Addresses)
|
||||
return c
|
||||
}
|
||||
|
||||
type DeviceConfigurationList []DeviceConfiguration
|
||||
|
||||
func (l DeviceConfigurationList) Less(a, b int) bool {
|
||||
return l[a].DeviceID.Compare(l[b].DeviceID) == -1
|
||||
}
|
||||
|
||||
func (l DeviceConfigurationList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
|
||||
func (l DeviceConfigurationList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type FolderConfiguration struct {
|
||||
ID string `xml:"id,attr" json:"id"`
|
||||
RawPath string `xml:"path,attr" json:"path"`
|
||||
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
|
||||
ReadOnly bool `xml:"ro,attr" json:"readOnly"`
|
||||
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
|
||||
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
|
||||
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
|
||||
MinDiskFreePct float64 `xml:"minDiskFreePct" json:"minDiskFreePct"`
|
||||
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
|
||||
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
|
||||
Pullers int `xml:"pullers" json:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
|
||||
Hashers int `xml:"hashers" json:"hashers"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
|
||||
Order PullOrder `xml:"order" json:"order"`
|
||||
IgnoreDelete bool `xml:"ignoreDelete" json:"ignoreDelete"`
|
||||
ScanProgressIntervalS int `xml:"scanProgressIntervalS" json:"scanProgressIntervalS"` // Set to a negative value to disable. Value of 0 will get replaced with value of 2 (default value)
|
||||
PullerSleepS int `xml:"pullerSleepS" json:"pullerSleepS"`
|
||||
PullerPauseS int `xml:"pullerPauseS" json:"pullerPauseS"`
|
||||
MaxConflicts int `xml:"maxConflicts" json:"maxConflicts"`
|
||||
|
||||
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
|
||||
cachedPath string
|
||||
}
|
||||
|
||||
type FolderDeviceConfiguration struct {
|
||||
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
|
||||
}
|
||||
|
||||
func NewFolderConfiguration(id, path string) FolderConfiguration {
|
||||
f := FolderConfiguration{
|
||||
ID: id,
|
||||
RawPath: path,
|
||||
}
|
||||
f.prepare()
|
||||
return f
|
||||
}
|
||||
|
||||
func (f FolderConfiguration) Copy() FolderConfiguration {
|
||||
c := f
|
||||
c.Devices = make([]FolderDeviceConfiguration, len(f.Devices))
|
||||
copy(c.Devices, f.Devices)
|
||||
c.Versioning = f.Versioning.Copy()
|
||||
return c
|
||||
}
|
||||
|
||||
func (f FolderConfiguration) Path() string {
|
||||
// This is intentionally not a pointer method, because things like
|
||||
// cfg.Folders["default"].Path() should be valid.
|
||||
|
||||
if f.cachedPath == "" {
|
||||
l.Infoln("bug: uncached path call (should only happen in tests)")
|
||||
return f.cleanedPath()
|
||||
}
|
||||
return f.cachedPath
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) CreateMarker() error {
|
||||
if !f.HasMarker() {
|
||||
marker := filepath.Join(f.Path(), ".stfolder")
|
||||
fd, err := os.Create(marker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fd.Close()
|
||||
osutil.HideFile(marker)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) HasMarker() bool {
|
||||
_, err := os.Stat(filepath.Join(f.Path(), ".stfolder"))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
|
||||
deviceIDs := make([]protocol.DeviceID, len(f.Devices))
|
||||
for i, n := range f.Devices {
|
||||
deviceIDs[i] = n.DeviceID
|
||||
}
|
||||
return deviceIDs
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) prepare() {
|
||||
if len(f.RawPath) == 0 {
|
||||
f.Invalid = "no directory configured"
|
||||
return
|
||||
}
|
||||
|
||||
// The reason it's done like this:
|
||||
// C: -> C:\ -> C:\ (issue that this is trying to fix)
|
||||
// C:\somedir -> C:\somedir\ -> C:\somedir
|
||||
// C:\somedir\ -> C:\somedir\\ -> C:\somedir
|
||||
// This way in the tests, we get away without OS specific separators
|
||||
// in the test configs.
|
||||
f.RawPath = filepath.Dir(f.RawPath + string(filepath.Separator))
|
||||
|
||||
// If we're not on Windows, we want the path to end with a slash to
|
||||
// penetrate symlinks. On Windows, paths must not end with a slash.
|
||||
if runtime.GOOS != "windows" && f.RawPath[len(f.RawPath)-1] != filepath.Separator {
|
||||
f.RawPath = f.RawPath + string(filepath.Separator)
|
||||
}
|
||||
|
||||
f.cachedPath = f.cleanedPath()
|
||||
|
||||
if f.ID == "" {
|
||||
f.ID = "default"
|
||||
}
|
||||
|
||||
if f.RescanIntervalS > MaxRescanIntervalS {
|
||||
f.RescanIntervalS = MaxRescanIntervalS
|
||||
} else if f.RescanIntervalS < 0 {
|
||||
f.RescanIntervalS = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) cleanedPath() string {
|
||||
cleaned := f.RawPath
|
||||
|
||||
// Attempt tilde expansion; leave unchanged in case of error
|
||||
if path, err := osutil.ExpandTilde(cleaned); err == nil {
|
||||
cleaned = path
|
||||
}
|
||||
|
||||
// Attempt absolutification; leave unchanged in case of error
|
||||
if !filepath.IsAbs(cleaned) {
|
||||
// Abs() looks like a fairly expensive syscall on Windows, while
|
||||
// IsAbs() is a whole bunch of string mangling. I think IsAbs() may be
|
||||
// somewhat faster in the general case, hence the outer if...
|
||||
if path, err := filepath.Abs(cleaned); err == nil {
|
||||
cleaned = path
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to enable long filename support on Windows. We may still not
|
||||
// have an absolute path here if the previous steps failed.
|
||||
if runtime.GOOS == "windows" && filepath.IsAbs(cleaned) && !strings.HasPrefix(f.RawPath, `\\`) {
|
||||
return `\\?\` + cleaned
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
type FolderDeviceConfigurationList []FolderDeviceConfiguration
|
||||
|
||||
func (l FolderDeviceConfigurationList) Less(a, b int) bool {
|
||||
return l[a].DeviceID.Compare(l[b].DeviceID) == -1
|
||||
}
|
||||
|
||||
func (l FolderDeviceConfigurationList) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
|
||||
func (l FolderDeviceConfigurationList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GUIConfiguration struct {
|
||||
Enabled bool `xml:"enabled,attr" json:"enabled" default:"true"`
|
||||
RawAddress string `xml:"address" json:"address" default:"127.0.0.1:8384"`
|
||||
User string `xml:"user,omitempty" json:"user"`
|
||||
Password string `xml:"password,omitempty" json:"password"`
|
||||
RawUseTLS bool `xml:"tls,attr" json:"useTLS"`
|
||||
RawAPIKey string `xml:"apikey,omitempty" json:"apiKey"`
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) Address() string {
|
||||
if override := os.Getenv("STGUIADDRESS"); override != "" {
|
||||
// This value may be of the form "scheme://address:port" or just
|
||||
// "address:port". We need to chop off the scheme. We try to parse it as
|
||||
// an URL if it contains a slash. If that fails, return it as is and let
|
||||
// some other error handling handle it.
|
||||
|
||||
if strings.Contains(override, "/") {
|
||||
url, err := url.Parse(override)
|
||||
if err != nil {
|
||||
return override
|
||||
}
|
||||
return url.Host
|
||||
}
|
||||
|
||||
return override
|
||||
}
|
||||
|
||||
return c.RawAddress
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) UseTLS() bool {
|
||||
if override := os.Getenv("STGUIADDRESS"); override != "" {
|
||||
return strings.HasPrefix(override, "https:")
|
||||
}
|
||||
return c.RawUseTLS
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) URL() string {
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: c.Address(),
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
if c.UseTLS() {
|
||||
u.Scheme = "https"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(u.Host, ":") {
|
||||
// Empty host, i.e. ":port", use IPv4 localhost
|
||||
u.Host = "127.0.0.1" + u.Host
|
||||
} else if strings.HasPrefix(u.Host, "0.0.0.0:") {
|
||||
// IPv4 all zeroes host, convert to IPv4 localhost
|
||||
u.Host = "127.0.0.1" + u.Host[7:]
|
||||
} else if strings.HasPrefix(u.Host, "[::]:") {
|
||||
// IPv6 all zeroes host, convert to IPv6 localhost
|
||||
u.Host = "[::1]" + u.Host[4:]
|
||||
}
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) APIKey() string {
|
||||
if override := os.Getenv("STGUIAPIKEY"); override != "" {
|
||||
return override
|
||||
}
|
||||
return c.RawAPIKey
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package config
|
||||
|
||||
type OptionsConfiguration struct {
|
||||
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"tcp://0.0.0.0:22000"`
|
||||
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default"`
|
||||
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
|
||||
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
|
||||
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027"`
|
||||
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027"`
|
||||
RelayServers []string `xml:"relayServer" json:"relayServers" default:"dynamic+https://relays.syncthing.net/endpoint"`
|
||||
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
|
||||
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
|
||||
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
|
||||
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
|
||||
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
|
||||
RelayWithoutGlobalAnn bool `xml:"relayWithoutGlobalAnn" json:"relayWithoutGlobalAnn" default:"false"`
|
||||
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
|
||||
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
|
||||
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"`
|
||||
UPnPRenewalM int `xml:"upnpRenewalMinutes" json:"upnpRenewalMinutes" default:"30"`
|
||||
UPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"upnpTimeoutSeconds" default:"10"`
|
||||
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
|
||||
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
|
||||
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
|
||||
URPostInsecurely bool `xml:"urPostInsecurely" json:"urPostInsecurely" default:"false"` // For testing
|
||||
URInitialDelayS int `xml:"urInitialDelayS" json:"urInitialDelayS" default:"1800"`
|
||||
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
|
||||
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12"` // 0 for off
|
||||
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
|
||||
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"true"`
|
||||
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
|
||||
SymlinksEnabled bool `xml:"symlinksEnabled" json:"symlinksEnabled" default:"true"`
|
||||
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
|
||||
MinHomeDiskFreePct float64 `xml:"minHomeDiskFreePct" json:"minHomeDiskFreePct" default:"1"`
|
||||
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=30"`
|
||||
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
|
||||
}
|
||||
|
||||
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
|
||||
c := orig
|
||||
c.ListenAddress = make([]string, len(orig.ListenAddress))
|
||||
copy(c.ListenAddress, orig.ListenAddress)
|
||||
c.GlobalAnnServers = make([]string, len(orig.GlobalAnnServers))
|
||||
copy(c.GlobalAnnServers, orig.GlobalAnnServers)
|
||||
c.RelayServers = make([]string, len(orig.RelayServers))
|
||||
copy(c.RelayServers, orig.RelayServers)
|
||||
c.AlwaysLocalNets = make([]string, len(orig.AlwaysLocalNets))
|
||||
copy(c.AlwaysLocalNets, orig.AlwaysLocalNets)
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package config
|
||||
|
||||
type PullOrder int
|
||||
|
||||
const (
|
||||
OrderRandom PullOrder = iota // default is random
|
||||
OrderAlphabetic
|
||||
OrderSmallestFirst
|
||||
OrderLargestFirst
|
||||
OrderOldestFirst
|
||||
OrderNewestFirst
|
||||
)
|
||||
|
||||
func (o PullOrder) String() string {
|
||||
switch o {
|
||||
case OrderRandom:
|
||||
return "random"
|
||||
case OrderAlphabetic:
|
||||
return "alphabetic"
|
||||
case OrderSmallestFirst:
|
||||
return "smallestFirst"
|
||||
case OrderLargestFirst:
|
||||
return "largestFirst"
|
||||
case OrderOldestFirst:
|
||||
return "oldestFirst"
|
||||
case OrderNewestFirst:
|
||||
return "newestFirst"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (o PullOrder) MarshalText() ([]byte, error) {
|
||||
return []byte(o.String()), nil
|
||||
}
|
||||
|
||||
func (o *PullOrder) UnmarshalText(bs []byte) error {
|
||||
switch string(bs) {
|
||||
case "random":
|
||||
*o = OrderRandom
|
||||
case "alphabetic":
|
||||
*o = OrderAlphabetic
|
||||
case "smallestFirst":
|
||||
*o = OrderSmallestFirst
|
||||
case "largestFirst":
|
||||
*o = OrderLargestFirst
|
||||
case "oldestFirst":
|
||||
*o = OrderOldestFirst
|
||||
case "newestFirst":
|
||||
*o = OrderNewestFirst
|
||||
default:
|
||||
*o = OrderRandom
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package config
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
type VersioningConfiguration struct {
|
||||
Type string `xml:"type,attr" json:"type"`
|
||||
Params map[string]string `json:"params"`
|
||||
}
|
||||
|
||||
type InternalVersioningConfiguration struct {
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
Params []InternalParam `xml:"param"`
|
||||
}
|
||||
|
||||
type InternalParam struct {
|
||||
Key string `xml:"key,attr"`
|
||||
Val string `xml:"val,attr"`
|
||||
}
|
||||
|
||||
func (c VersioningConfiguration) Copy() VersioningConfiguration {
|
||||
cp := c
|
||||
cp.Params = make(map[string]string, len(c.Params))
|
||||
for k, v := range c.Params {
|
||||
cp.Params[k] = v
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
func (c *VersioningConfiguration) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
var tmp InternalVersioningConfiguration
|
||||
tmp.Type = c.Type
|
||||
for k, v := range c.Params {
|
||||
tmp.Params = append(tmp.Params, InternalParam{k, v})
|
||||
}
|
||||
|
||||
return e.EncodeElement(tmp, start)
|
||||
|
||||
}
|
||||
|
||||
func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var tmp InternalVersioningConfiguration
|
||||
err := d.DecodeElement(&tmp, &start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type = tmp.Type
|
||||
c.Params = make(map[string]string, len(tmp.Params))
|
||||
for _, p := range tmp.Params {
|
||||
c.Params[p.Key] = p.Val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
// If all verification calls returns nil, CommitConfiguration() is called for
|
||||
// each subscribing object. The callee returns true if the new configuration
|
||||
// has been successfully applied, otherwise false. Any Commit() call returning
|
||||
// false will result in a "restart needed" respone to the API/user. Note that
|
||||
// false will result in a "restart needed" response to the API/user. Note that
|
||||
// the new configuration will still have been applied by those who were
|
||||
// capable of doing so.
|
||||
type Committer interface {
|
||||
@@ -310,9 +310,14 @@ func (w *Wrapper) Save() error {
|
||||
func (w *Wrapper) GlobalDiscoveryServers() []string {
|
||||
var servers []string
|
||||
for _, srv := range w.cfg.Options.GlobalAnnServers {
|
||||
if srv == "default" {
|
||||
switch srv {
|
||||
case "default":
|
||||
servers = append(servers, DefaultDiscoveryServers...)
|
||||
} else {
|
||||
case "default-v4":
|
||||
servers = append(servers, DefaultDiscoveryServersV4...)
|
||||
case "default-v6":
|
||||
servers = append(servers, DefaultDiscoveryServersV6...)
|
||||
default:
|
||||
servers = append(servers, srv)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,49 +280,18 @@ func (s *connectionSvc) connect() {
|
||||
continue
|
||||
}
|
||||
|
||||
var addrs []string
|
||||
var relays []discover.Relay
|
||||
for _, addr := range deviceCfg.Addresses {
|
||||
if addr == "dynamic" {
|
||||
if s.discoverer != nil {
|
||||
if t, r, err := s.discoverer.Lookup(deviceID); err == nil {
|
||||
addrs = append(addrs, t...)
|
||||
relays = append(relays, r...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
addrs, relays := s.resolveAddresses(deviceID, deviceCfg.Addresses)
|
||||
|
||||
for _, addr := range addrs {
|
||||
uri, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
l.Infoln("Failed to parse connection url:", addr, err)
|
||||
continue
|
||||
if conn := s.connectDirect(deviceID, addr); conn != nil {
|
||||
if connected {
|
||||
s.model.Close(deviceID, fmt.Errorf("switching connections"))
|
||||
}
|
||||
s.conns <- model.IntermediateConnection{
|
||||
conn, model.ConnectionTypeDirectDial,
|
||||
}
|
||||
continue nextDevice
|
||||
}
|
||||
|
||||
dialer, ok := dialers[uri.Scheme]
|
||||
if !ok {
|
||||
l.Infoln("Unknown address schema", uri)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Debugln("dial", deviceCfg.DeviceID, uri)
|
||||
conn, err := dialer(uri, s.tlsCfg)
|
||||
if err != nil {
|
||||
l.Debugln("dial failed", deviceCfg.DeviceID, uri, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if connected {
|
||||
s.model.Close(deviceID, fmt.Errorf("switching connections"))
|
||||
}
|
||||
|
||||
s.conns <- model.IntermediateConnection{
|
||||
conn, model.ConnectionTypeDirectDial,
|
||||
}
|
||||
continue nextDevice
|
||||
}
|
||||
|
||||
// Only connect via relays if not already connected
|
||||
@@ -345,45 +314,12 @@ func (s *connectionSvc) connect() {
|
||||
s.lastRelayCheck[deviceID] = time.Now()
|
||||
|
||||
for _, addr := range relays {
|
||||
uri, err := url.Parse(addr.URL)
|
||||
if err != nil {
|
||||
l.Infoln("Failed to parse relay connection url:", addr, err)
|
||||
continue
|
||||
if conn := s.connectViaRelay(deviceID, addr); conn != nil {
|
||||
s.conns <- model.IntermediateConnection{
|
||||
conn, model.ConnectionTypeRelayDial,
|
||||
}
|
||||
continue nextDevice
|
||||
}
|
||||
|
||||
inv, err := client.GetInvitationFromRelay(uri, deviceID, s.tlsCfg.Certificates)
|
||||
if err != nil {
|
||||
l.Debugf("Failed to get invitation for %s from %s: %v", deviceID, uri, err)
|
||||
continue
|
||||
} else {
|
||||
l.Debugln("Succesfully retrieved relay invitation", inv, "from", uri)
|
||||
}
|
||||
|
||||
conn, err := client.JoinSession(inv)
|
||||
if err != nil {
|
||||
l.Debugf("Failed to join relay session %s: %v", inv, err)
|
||||
continue
|
||||
} else {
|
||||
l.Debugln("Sucessfully joined relay session", inv)
|
||||
}
|
||||
|
||||
var tc *tls.Conn
|
||||
|
||||
if inv.ServerSocket {
|
||||
tc = tls.Server(conn, s.tlsCfg)
|
||||
} else {
|
||||
tc = tls.Client(conn, s.tlsCfg)
|
||||
}
|
||||
err = tc.Handshake()
|
||||
if err != nil {
|
||||
l.Infof("TLS handshake (BEP/relay %s): %v", inv, err)
|
||||
tc.Close()
|
||||
continue
|
||||
}
|
||||
s.conns <- model.IntermediateConnection{
|
||||
tc, model.ConnectionTypeRelayDial,
|
||||
}
|
||||
continue nextDevice
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,6 +331,84 @@ func (s *connectionSvc) connect() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *connectionSvc) resolveAddresses(deviceID protocol.DeviceID, inAddrs []string) (addrs []string, relays []discover.Relay) {
|
||||
for _, addr := range inAddrs {
|
||||
if addr == "dynamic" {
|
||||
if s.discoverer != nil {
|
||||
if t, r, err := s.discoverer.Lookup(deviceID); err == nil {
|
||||
addrs = append(addrs, t...)
|
||||
relays = append(relays, r...)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *connectionSvc) connectDirect(deviceID protocol.DeviceID, addr string) *tls.Conn {
|
||||
uri, err := url.Parse(addr)
|
||||
if err != nil {
|
||||
l.Infoln("Failed to parse connection url:", addr, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
dialer, ok := dialers[uri.Scheme]
|
||||
if !ok {
|
||||
l.Infoln("Unknown address schema", uri)
|
||||
return nil
|
||||
}
|
||||
|
||||
l.Debugln("dial", deviceID, uri)
|
||||
conn, err := dialer(uri, s.tlsCfg)
|
||||
if err != nil {
|
||||
l.Debugln("dial failed", deviceID, uri, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
func (s *connectionSvc) connectViaRelay(deviceID protocol.DeviceID, addr discover.Relay) *tls.Conn {
|
||||
uri, err := url.Parse(addr.URL)
|
||||
if err != nil {
|
||||
l.Infoln("Failed to parse relay connection url:", addr, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
inv, err := client.GetInvitationFromRelay(uri, deviceID, s.tlsCfg.Certificates)
|
||||
if err != nil {
|
||||
l.Debugf("Failed to get invitation for %s from %s: %v", deviceID, uri, err)
|
||||
return nil
|
||||
}
|
||||
l.Debugln("Succesfully retrieved relay invitation", inv, "from", uri)
|
||||
|
||||
conn, err := client.JoinSession(inv)
|
||||
if err != nil {
|
||||
l.Debugf("Failed to join relay session %s: %v", inv, err)
|
||||
return nil
|
||||
}
|
||||
l.Debugln("Successfully joined relay session", inv)
|
||||
|
||||
var tc *tls.Conn
|
||||
|
||||
if inv.ServerSocket {
|
||||
tc = tls.Server(conn, s.tlsCfg)
|
||||
} else {
|
||||
tc = tls.Client(conn, s.tlsCfg)
|
||||
}
|
||||
|
||||
err = tc.Handshake()
|
||||
if err != nil {
|
||||
l.Infof("TLS handshake (BEP/relay %s): %v", inv, err)
|
||||
tc.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
return tc
|
||||
}
|
||||
|
||||
func (s *connectionSvc) acceptRelayConns() {
|
||||
for {
|
||||
conn := s.relaySvc.Accept()
|
||||
|
||||
@@ -17,8 +17,6 @@ import (
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
var files, oneFile, firstHalf, secondHalf []protocol.FileInfo
|
||||
@@ -44,16 +42,16 @@ func init() {
|
||||
fs.Replace(protocol.LocalDeviceID, firstHalf)
|
||||
}
|
||||
|
||||
func tempDB() (*leveldb.DB, string) {
|
||||
func tempDB() (*db.Instance, string) {
|
||||
dir, err := ioutil.TempDir("", "syncthing")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
db, err := leveldb.OpenFile(filepath.Join(dir, "db"), &opt.Options{OpenFilesCacheCapacity: 100})
|
||||
dbi, err := db.Open(filepath.Join(dir, "db"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return db, dir
|
||||
return dbi, dir
|
||||
}
|
||||
|
||||
func BenchmarkReplaceAll(b *testing.B) {
|
||||
|
||||
+7
-4
@@ -29,11 +29,11 @@ var blockFinder *BlockFinder
|
||||
const maxBatchSize = 256 << 10
|
||||
|
||||
type BlockMap struct {
|
||||
db *leveldb.DB
|
||||
db *Instance
|
||||
folder string
|
||||
}
|
||||
|
||||
func NewBlockMap(db *leveldb.DB, folder string) *BlockMap {
|
||||
func NewBlockMap(db *Instance, folder string) *BlockMap {
|
||||
return &BlockMap{
|
||||
db: db,
|
||||
folder: folder,
|
||||
@@ -146,10 +146,10 @@ func (m *BlockMap) blockKeyInto(o, hash []byte, file string) []byte {
|
||||
}
|
||||
|
||||
type BlockFinder struct {
|
||||
db *leveldb.DB
|
||||
db *Instance
|
||||
}
|
||||
|
||||
func NewBlockFinder(db *leveldb.DB) *BlockFinder {
|
||||
func NewBlockFinder(db *Instance) *BlockFinder {
|
||||
if blockFinder != nil {
|
||||
return blockFinder
|
||||
}
|
||||
@@ -214,6 +214,9 @@ func blockKeyInto(o, hash []byte, folder, file string) []byte {
|
||||
}
|
||||
o[0] = KeyTypeBlock
|
||||
copy(o[1:], []byte(folder))
|
||||
for i := len(folder); i < 64; i++ {
|
||||
o[1+i] = 0
|
||||
}
|
||||
copy(o[1+64:], []byte(hash))
|
||||
copy(o[1+64+32:], []byte(file))
|
||||
return o
|
||||
|
||||
@@ -10,9 +10,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
func genBlocks(n int) []protocol.BlockInfo {
|
||||
@@ -50,17 +47,14 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
func setup() (*leveldb.DB, *BlockFinder) {
|
||||
func setup() (*Instance, *BlockFinder) {
|
||||
// Setup
|
||||
|
||||
db, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
db := OpenMemory()
|
||||
return db, NewBlockFinder(db)
|
||||
}
|
||||
|
||||
func dbEmpty(db *leveldb.DB) bool {
|
||||
func dbEmpty(db *Instance) bool {
|
||||
iter := db.NewIterator(nil, nil)
|
||||
defer iter.Release()
|
||||
if iter.Next() {
|
||||
|
||||
+2
-912
@@ -12,14 +12,11 @@ package db
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -89,555 +86,11 @@ type dbReader interface {
|
||||
Get([]byte, *opt.ReadOptions) ([]byte, error)
|
||||
}
|
||||
|
||||
type dbWriter interface {
|
||||
Put([]byte, []byte)
|
||||
Delete([]byte)
|
||||
}
|
||||
|
||||
// Flush batches to disk when they contain this many records.
|
||||
const batchFlushSize = 64
|
||||
|
||||
// deviceKey returns a byte slice encoding the following information:
|
||||
// keyTypeDevice (1 byte)
|
||||
// folder (64 bytes)
|
||||
// device (32 bytes)
|
||||
// name (variable size)
|
||||
func deviceKey(folder, device, file []byte) []byte {
|
||||
return deviceKeyInto(nil, folder, device, file)
|
||||
}
|
||||
|
||||
func deviceKeyInto(k []byte, folder, device, file []byte) []byte {
|
||||
reqLen := 1 + 64 + 32 + len(file)
|
||||
if len(k) < reqLen {
|
||||
k = make([]byte, reqLen)
|
||||
}
|
||||
k[0] = KeyTypeDevice
|
||||
if len(folder) > 64 {
|
||||
panic("folder name too long")
|
||||
}
|
||||
copy(k[1:], []byte(folder))
|
||||
copy(k[1+64:], device[:])
|
||||
copy(k[1+64+32:], []byte(file))
|
||||
return k[:reqLen]
|
||||
}
|
||||
|
||||
func deviceKeyName(key []byte) []byte {
|
||||
return key[1+64+32:]
|
||||
}
|
||||
|
||||
func deviceKeyFolder(key []byte) []byte {
|
||||
folder := key[1 : 1+64]
|
||||
izero := bytes.IndexByte(folder, 0)
|
||||
if izero < 0 {
|
||||
return folder
|
||||
}
|
||||
return folder[:izero]
|
||||
}
|
||||
|
||||
func deviceKeyDevice(key []byte) []byte {
|
||||
return key[1+64 : 1+64+32]
|
||||
}
|
||||
|
||||
// globalKey returns a byte slice encoding the following information:
|
||||
// keyTypeGlobal (1 byte)
|
||||
// folder (64 bytes)
|
||||
// name (variable size)
|
||||
func globalKey(folder, file []byte) []byte {
|
||||
k := make([]byte, 1+64+len(file))
|
||||
k[0] = KeyTypeGlobal
|
||||
if len(folder) > 64 {
|
||||
panic("folder name too long")
|
||||
}
|
||||
copy(k[1:], []byte(folder))
|
||||
copy(k[1+64:], []byte(file))
|
||||
return k
|
||||
}
|
||||
|
||||
func globalKeyName(key []byte) []byte {
|
||||
return key[1+64:]
|
||||
}
|
||||
|
||||
func globalKeyFolder(key []byte) []byte {
|
||||
folder := key[1 : 1+64]
|
||||
izero := bytes.IndexByte(folder, 0)
|
||||
if izero < 0 {
|
||||
return folder
|
||||
}
|
||||
return folder[:izero]
|
||||
}
|
||||
|
||||
type deletionHandler func(db dbReader, batch dbWriter, folder, device, name []byte, dbi iterator.Iterator) int64
|
||||
|
||||
func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker, deleteFn deletionHandler) int64 {
|
||||
sort.Sort(fileList(fs)) // sort list on name, same as in the database
|
||||
|
||||
start := deviceKey(folder, device, nil) // before all folder/device files
|
||||
limit := deviceKey(folder, device, []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
|
||||
|
||||
batch := new(leveldb.Batch)
|
||||
l.Debugf("new batch %p", batch)
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.Debugf("created snapshot %p", snap)
|
||||
defer func() {
|
||||
l.Debugf("close snapshot %p", snap)
|
||||
snap.Release()
|
||||
}()
|
||||
|
||||
dbi := snap.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
moreDb := dbi.Next()
|
||||
fsi := 0
|
||||
var maxLocalVer int64
|
||||
|
||||
isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
|
||||
for {
|
||||
var newName, oldName []byte
|
||||
moreFs := fsi < len(fs)
|
||||
|
||||
if !moreDb && !moreFs {
|
||||
break
|
||||
}
|
||||
|
||||
if moreFs {
|
||||
newName = []byte(fs[fsi].Name)
|
||||
}
|
||||
|
||||
if moreDb {
|
||||
oldName = deviceKeyName(dbi.Key())
|
||||
}
|
||||
|
||||
cmp := bytes.Compare(newName, oldName)
|
||||
|
||||
l.Debugf("generic replace; folder=%q device=%v moreFs=%v moreDb=%v cmp=%d newName=%q oldName=%q", folder, protocol.DeviceIDFromBytes(device), moreFs, moreDb, cmp, newName, oldName)
|
||||
|
||||
switch {
|
||||
case moreFs && (!moreDb || cmp == -1):
|
||||
l.Debugln("generic replace; missing - insert")
|
||||
// Database is missing this file. Insert it.
|
||||
if lv := ldbInsert(batch, folder, device, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if isLocalDevice {
|
||||
localSize.addFile(fs[fsi])
|
||||
}
|
||||
if fs[fsi].IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, newName, globalSize)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, folder, device, fs[fsi], globalSize)
|
||||
}
|
||||
fsi++
|
||||
|
||||
case moreFs && moreDb && cmp == 0:
|
||||
// File exists on both sides - compare versions. We might get an
|
||||
// update with the same version and different flags if a device has
|
||||
// marked a file as invalid, so handle that too.
|
||||
l.Debugln("generic replace; exists - compare")
|
||||
var ef FileInfoTruncated
|
||||
ef.UnmarshalXDR(dbi.Value())
|
||||
if !fs[fsi].Version.Equal(ef.Version) || fs[fsi].Flags != ef.Flags {
|
||||
l.Debugln("generic replace; differs - insert")
|
||||
if lv := ldbInsert(batch, folder, device, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if isLocalDevice {
|
||||
localSize.removeFile(ef)
|
||||
localSize.addFile(fs[fsi])
|
||||
}
|
||||
if fs[fsi].IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, newName, globalSize)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, folder, device, fs[fsi], globalSize)
|
||||
}
|
||||
} else {
|
||||
l.Debugln("generic replace; equal - ignore")
|
||||
}
|
||||
|
||||
fsi++
|
||||
moreDb = dbi.Next()
|
||||
|
||||
case moreDb && (!moreFs || cmp == 1):
|
||||
l.Debugln("generic replace; exists - remove")
|
||||
if lv := deleteFn(snap, batch, folder, device, oldName, dbi); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
moreDb = dbi.Next()
|
||||
}
|
||||
|
||||
// Write out and reuse the batch every few records, to avoid the batch
|
||||
// growing too large and thus allocating unnecessarily much memory.
|
||||
if batch.Len() > batchFlushSize {
|
||||
l.Debugf("db.Write %p", batch)
|
||||
|
||||
err = db.Write(batch, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
batch.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
l.Debugf("db.Write %p", batch)
|
||||
err = db.Write(batch, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return maxLocalVer
|
||||
}
|
||||
|
||||
func ldbReplace(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) int64 {
|
||||
// TODO: Return the remaining maxLocalVer?
|
||||
return ldbGenericReplace(db, folder, device, fs, localSize, globalSize, func(db dbReader, batch dbWriter, folder, device, name []byte, dbi iterator.Iterator) int64 {
|
||||
// Database has a file that we are missing. Remove it.
|
||||
l.Debugf("delete; folder=%q device=%v name=%q", folder, protocol.DeviceIDFromBytes(device), name)
|
||||
ldbRemoveFromGlobal(db, batch, folder, device, name, globalSize)
|
||||
l.Debugf("batch.Delete %p %x", batch, dbi.Key())
|
||||
batch.Delete(dbi.Key())
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
func ldbUpdate(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) int64 {
|
||||
batch := new(leveldb.Batch)
|
||||
l.Debugf("new batch %p", batch)
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.Debugf("created snapshot %p", snap)
|
||||
defer func() {
|
||||
l.Debugf("close snapshot %p", snap)
|
||||
snap.Release()
|
||||
}()
|
||||
|
||||
var maxLocalVer int64
|
||||
var fk []byte
|
||||
isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
|
||||
for _, f := range fs {
|
||||
name := []byte(f.Name)
|
||||
fk = deviceKeyInto(fk[:cap(fk)], folder, device, name)
|
||||
l.Debugf("snap.Get %p %x", snap, fk)
|
||||
bs, err := snap.Get(fk, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
if isLocalDevice {
|
||||
localSize.addFile(f)
|
||||
}
|
||||
|
||||
if lv := ldbInsert(batch, folder, device, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if f.IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, name, globalSize)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, folder, device, f, globalSize)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var ef FileInfoTruncated
|
||||
err = ef.UnmarshalXDR(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Flags might change without the version being bumped when we set the
|
||||
// invalid flag on an existing file.
|
||||
if !ef.Version.Equal(f.Version) || ef.Flags != f.Flags {
|
||||
if isLocalDevice {
|
||||
localSize.removeFile(ef)
|
||||
localSize.addFile(f)
|
||||
}
|
||||
|
||||
if lv := ldbInsert(batch, folder, device, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if f.IsInvalid() {
|
||||
ldbRemoveFromGlobal(snap, batch, folder, device, name, globalSize)
|
||||
} else {
|
||||
ldbUpdateGlobal(snap, batch, folder, device, f, globalSize)
|
||||
}
|
||||
}
|
||||
|
||||
// Write out and reuse the batch every few records, to avoid the batch
|
||||
// growing too large and thus allocating unnecessarily much memory.
|
||||
if batch.Len() > batchFlushSize {
|
||||
l.Debugf("db.Write %p", batch)
|
||||
|
||||
err = db.Write(batch, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
batch.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
l.Debugf("db.Write %p", batch)
|
||||
err = db.Write(batch, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return maxLocalVer
|
||||
}
|
||||
|
||||
func ldbInsert(batch dbWriter, folder, device []byte, file protocol.FileInfo) int64 {
|
||||
l.Debugf("insert; folder=%q device=%v %v", folder, protocol.DeviceIDFromBytes(device), file)
|
||||
|
||||
if file.LocalVersion == 0 {
|
||||
file.LocalVersion = clock(0)
|
||||
}
|
||||
|
||||
name := []byte(file.Name)
|
||||
nk := deviceKey(folder, device, name)
|
||||
l.Debugf("batch.Put %p %x", batch, nk)
|
||||
batch.Put(nk, file.MustMarshalXDR())
|
||||
|
||||
return file.LocalVersion
|
||||
}
|
||||
|
||||
// ldbUpdateGlobal adds this device+version to the version list for the given
|
||||
// file. If the device is already present in the list, the version is updated.
|
||||
// If the file does not have an entry in the global list, it is created.
|
||||
func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device []byte, file protocol.FileInfo, globalSize *sizeTracker) bool {
|
||||
l.Debugf("update global; folder=%q device=%v file=%q version=%d", folder, protocol.DeviceIDFromBytes(device), file.Name, file.Version)
|
||||
name := []byte(file.Name)
|
||||
gk := globalKey(folder, name)
|
||||
svl, err := db.Get(gk, nil)
|
||||
if err != nil && err != leveldb.ErrNotFound {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var fl versionList
|
||||
var oldFile protocol.FileInfo
|
||||
var hasOldFile bool
|
||||
// Remove the device from the current version list
|
||||
if svl != nil {
|
||||
err = fl.UnmarshalXDR(svl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := range fl.versions {
|
||||
if bytes.Compare(fl.versions[i].device, device) == 0 {
|
||||
if fl.versions[i].version.Equal(file.Version) {
|
||||
// No need to do anything
|
||||
return false
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
// Keep the current newest file around so we can subtract it from
|
||||
// the globalSize if we replace it.
|
||||
oldFile, hasOldFile = ldbGet(db, folder, fl.versions[0].device, name)
|
||||
}
|
||||
|
||||
fl.versions = append(fl.versions[:i], fl.versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nv := fileVersion{
|
||||
device: device,
|
||||
version: file.Version,
|
||||
}
|
||||
|
||||
insertedAt := -1
|
||||
// Find a position in the list to insert this file. The file at the front
|
||||
// of the list is the newer, the "global".
|
||||
for i := range fl.versions {
|
||||
switch fl.versions[i].version.Compare(file.Version) {
|
||||
case protocol.Equal, protocol.Lesser:
|
||||
// The version at this point in the list is equal to or lesser
|
||||
// ("older") than us. We insert ourselves in front of it.
|
||||
fl.versions = insertVersion(fl.versions, i, nv)
|
||||
insertedAt = i
|
||||
goto done
|
||||
|
||||
case protocol.ConcurrentLesser, protocol.ConcurrentGreater:
|
||||
// The version at this point is in conflict with us. We must pull
|
||||
// the actual file metadata to determine who wins. If we win, we
|
||||
// insert ourselves in front of the loser here. (The "Lesser" and
|
||||
// "Greater" in the condition above is just based on the device
|
||||
// IDs in the version vector, which is not the only thing we use
|
||||
// to determine the winner.)
|
||||
of, ok := ldbGet(db, folder, fl.versions[i].device, name)
|
||||
if !ok {
|
||||
panic("file referenced in version list does not exist")
|
||||
}
|
||||
if file.WinsConflict(of) {
|
||||
fl.versions = insertVersion(fl.versions, i, nv)
|
||||
insertedAt = i
|
||||
goto done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find a position for an insert above, so append to the end.
|
||||
fl.versions = append(fl.versions, nv)
|
||||
insertedAt = len(fl.versions) - 1
|
||||
|
||||
done:
|
||||
if insertedAt == 0 {
|
||||
// We just inserted a new newest version. Fixup the global size
|
||||
// calculation.
|
||||
if !file.Version.Equal(oldFile.Version) {
|
||||
globalSize.addFile(file)
|
||||
if hasOldFile {
|
||||
// We have the old file that was removed at the head of the list.
|
||||
globalSize.removeFile(oldFile)
|
||||
} else if len(fl.versions) > 1 {
|
||||
// The previous newest version is now at index 1, grab it from there.
|
||||
oldFile, ok := ldbGet(db, folder, fl.versions[1].device, name)
|
||||
if !ok {
|
||||
panic("file referenced in version list does not exist")
|
||||
}
|
||||
globalSize.removeFile(oldFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
l.Debugf("batch.Put %p %x", batch, gk)
|
||||
l.Debugf("new global after update: %v", fl)
|
||||
batch.Put(gk, fl.MustMarshalXDR())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func insertVersion(vl []fileVersion, i int, v fileVersion) []fileVersion {
|
||||
t := append(vl, fileVersion{})
|
||||
copy(t[i+1:], t[i:])
|
||||
t[i] = v
|
||||
return t
|
||||
}
|
||||
|
||||
// ldbRemoveFromGlobal removes the device from the global version list for the
|
||||
// given file. If the version list is empty after this, the file entry is
|
||||
// removed entirely.
|
||||
func ldbRemoveFromGlobal(db dbReader, batch dbWriter, folder, device, file []byte, globalSize *sizeTracker) {
|
||||
l.Debugf("remove from global; folder=%q device=%v file=%q", folder, protocol.DeviceIDFromBytes(device), file)
|
||||
|
||||
gk := globalKey(folder, file)
|
||||
svl, err := db.Get(gk, nil)
|
||||
if err != nil {
|
||||
// We might be called to "remove" a global version that doesn't exist
|
||||
// if the first update for the file is already marked invalid.
|
||||
return
|
||||
}
|
||||
|
||||
var fl versionList
|
||||
err = fl.UnmarshalXDR(svl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
removed := false
|
||||
for i := range fl.versions {
|
||||
if bytes.Compare(fl.versions[i].device, device) == 0 {
|
||||
if i == 0 && globalSize != nil {
|
||||
f, ok := ldbGet(db, folder, device, file)
|
||||
if !ok {
|
||||
panic("removing nonexistent file")
|
||||
}
|
||||
globalSize.removeFile(f)
|
||||
removed = true
|
||||
}
|
||||
fl.versions = append(fl.versions[:i], fl.versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(fl.versions) == 0 {
|
||||
l.Debugf("batch.Delete %p %x", batch, gk)
|
||||
batch.Delete(gk)
|
||||
} else {
|
||||
l.Debugf("batch.Put %p %x", batch, gk)
|
||||
l.Debugf("new global after remove: %v", fl)
|
||||
batch.Put(gk, fl.MustMarshalXDR())
|
||||
if removed {
|
||||
f, ok := ldbGet(db, folder, fl.versions[0].device, file)
|
||||
if !ok {
|
||||
panic("new global is nonexistent file")
|
||||
}
|
||||
globalSize.addFile(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ldbWithHave(db *leveldb.DB, folder, device []byte, truncate bool, fn Iterator) {
|
||||
start := deviceKey(folder, device, nil) // before all folder/device files
|
||||
limit := deviceKey(folder, device, []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.Debugf("created snapshot %p", snap)
|
||||
defer func() {
|
||||
l.Debugf("close snapshot %p", snap)
|
||||
snap.Release()
|
||||
}()
|
||||
|
||||
dbi := snap.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
f, err := unmarshalTrunc(dbi.Value(), truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if cont := fn(f); !cont {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ldbWithAllFolderTruncated(db *leveldb.DB, folder []byte, fn func(device []byte, f FileInfoTruncated) bool) {
|
||||
start := deviceKey(folder, nil, nil) // before all folder/device files
|
||||
limit := deviceKey(folder, protocol.LocalDeviceID[:], []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.Debugf("created snapshot %p", snap)
|
||||
defer func() {
|
||||
l.Debugf("close snapshot %p", snap)
|
||||
snap.Release()
|
||||
}()
|
||||
|
||||
dbi := snap.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
device := deviceKeyDevice(dbi.Key())
|
||||
var f FileInfoTruncated
|
||||
err := f.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch f.Name {
|
||||
case "", ".", "..", "/": // A few obviously invalid filenames
|
||||
l.Infof("Dropping invalid filename %q from database", f.Name)
|
||||
batch := new(leveldb.Batch)
|
||||
ldbRemoveFromGlobal(db, batch, folder, device, nil, nil)
|
||||
batch.Delete(dbi.Key())
|
||||
db.Write(batch, nil)
|
||||
continue
|
||||
}
|
||||
|
||||
if cont := fn(device, f); !cont {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ldbGet(db dbReader, folder, device, file []byte) (protocol.FileInfo, bool) {
|
||||
nk := deviceKey(folder, device, file)
|
||||
bs, err := db.Get(nk, nil)
|
||||
func getFile(db dbReader, key []byte) (protocol.FileInfo, bool) {
|
||||
bs, err := db.Get(key, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
@@ -652,366 +105,3 @@ func ldbGet(db dbReader, folder, device, file []byte) (protocol.FileInfo, bool)
|
||||
}
|
||||
return f, true
|
||||
}
|
||||
|
||||
func ldbGetGlobal(db *leveldb.DB, folder, file []byte, truncate bool) (FileIntf, bool) {
|
||||
k := globalKey(folder, file)
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.Debugf("created snapshot %p", snap)
|
||||
defer func() {
|
||||
l.Debugf("close snapshot %p", snap)
|
||||
snap.Release()
|
||||
}()
|
||||
|
||||
l.Debugf("snap.Get %p %x", snap, k)
|
||||
bs, err := snap.Get(k, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return nil, false
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var vl versionList
|
||||
err = vl.UnmarshalXDR(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(vl.versions) == 0 {
|
||||
l.Debugln(k)
|
||||
panic("no versions?")
|
||||
}
|
||||
|
||||
k = deviceKey(folder, vl.versions[0].device, file)
|
||||
l.Debugf("snap.Get %p %x", snap, k)
|
||||
bs, err = snap.Get(k, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fi, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fi, true
|
||||
}
|
||||
|
||||
func ldbWithGlobal(db *leveldb.DB, folder, prefix []byte, truncate bool, fn Iterator) {
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.Debugf("created snapshot %p", snap)
|
||||
defer func() {
|
||||
l.Debugf("close snapshot %p", snap)
|
||||
snap.Release()
|
||||
}()
|
||||
|
||||
dbi := snap.NewIterator(util.BytesPrefix(globalKey(folder, prefix)), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
var vl versionList
|
||||
err := vl.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(vl.versions) == 0 {
|
||||
l.Debugln(dbi.Key())
|
||||
panic("no versions?")
|
||||
}
|
||||
name := globalKeyName(dbi.Key())
|
||||
fk = deviceKeyInto(fk[:cap(fk)], folder, vl.versions[0].device, name)
|
||||
l.Debugf("snap.Get %p %x", snap, fk)
|
||||
bs, err := snap.Get(fk, nil)
|
||||
if err != nil {
|
||||
l.Debugf("folder: %q (%x)", folder, folder)
|
||||
l.Debugf("key: %q (%x)", dbi.Key(), dbi.Key())
|
||||
l.Debugf("vl: %v", vl)
|
||||
l.Debugf("vl.versions[0].device: %x", vl.versions[0].device)
|
||||
l.Debugf("name: %q (%x)", name, name)
|
||||
l.Debugf("fk: %q", fk)
|
||||
l.Debugf("fk: %x %x %x", fk[1:1+64], fk[1+64:1+64+32], fk[1+64+32:])
|
||||
panic(err)
|
||||
}
|
||||
|
||||
f, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if cont := fn(f); !cont {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ldbAvailability(db *leveldb.DB, folder, file []byte) []protocol.DeviceID {
|
||||
k := globalKey(folder, file)
|
||||
bs, err := db.Get(k, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var vl versionList
|
||||
err = vl.UnmarshalXDR(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var devices []protocol.DeviceID
|
||||
for _, v := range vl.versions {
|
||||
if !v.version.Equal(vl.versions[0].version) {
|
||||
break
|
||||
}
|
||||
n := protocol.DeviceIDFromBytes(v.device)
|
||||
devices = append(devices, n)
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
||||
|
||||
func ldbWithNeed(db *leveldb.DB, folder, device []byte, truncate bool, fn Iterator) {
|
||||
start := globalKey(folder, nil)
|
||||
limit := globalKey(folder, []byte{0xff, 0xff, 0xff, 0xff})
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.Debugf("created snapshot %p", snap)
|
||||
defer func() {
|
||||
l.Debugf("close snapshot %p", snap)
|
||||
snap.Release()
|
||||
}()
|
||||
|
||||
dbi := snap.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
nextFile:
|
||||
for dbi.Next() {
|
||||
var vl versionList
|
||||
err := vl.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(vl.versions) == 0 {
|
||||
l.Debugln(dbi.Key())
|
||||
panic("no versions?")
|
||||
}
|
||||
|
||||
have := false // If we have the file, any version
|
||||
need := false // If we have a lower version of the file
|
||||
var haveVersion protocol.Vector
|
||||
for _, v := range vl.versions {
|
||||
if bytes.Compare(v.device, device) == 0 {
|
||||
have = true
|
||||
haveVersion = v.version
|
||||
// XXX: This marks Concurrent (i.e. conflicting) changes as
|
||||
// needs. Maybe we should do that, but it needs special
|
||||
// handling in the puller.
|
||||
need = !v.version.GreaterEqual(vl.versions[0].version)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if need || !have {
|
||||
name := globalKeyName(dbi.Key())
|
||||
needVersion := vl.versions[0].version
|
||||
|
||||
nextVersion:
|
||||
for i := range vl.versions {
|
||||
if !vl.versions[i].version.Equal(needVersion) {
|
||||
// We haven't found a valid copy of the file with the needed version.
|
||||
continue nextFile
|
||||
}
|
||||
fk = deviceKeyInto(fk[:cap(fk)], folder, vl.versions[i].device, name)
|
||||
l.Debugf("snap.Get %p %x", snap, fk)
|
||||
bs, err := snap.Get(fk, nil)
|
||||
if err != nil {
|
||||
var id protocol.DeviceID
|
||||
copy(id[:], device)
|
||||
l.Debugf("device: %v", id)
|
||||
l.Debugf("need: %v, have: %v", need, have)
|
||||
l.Debugf("key: %q (%x)", dbi.Key(), dbi.Key())
|
||||
l.Debugf("vl: %v", vl)
|
||||
l.Debugf("i: %v", i)
|
||||
l.Debugf("fk: %q (%x)", fk, fk)
|
||||
l.Debugf("name: %q (%x)", name, name)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
gf, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if gf.IsInvalid() {
|
||||
// The file is marked invalid for whatever reason, don't use it.
|
||||
continue nextVersion
|
||||
}
|
||||
|
||||
if gf.IsDeleted() && !have {
|
||||
// We don't need deleted files that we don't have
|
||||
continue nextFile
|
||||
}
|
||||
|
||||
l.Debugf("need folder=%q device=%v name=%q need=%v have=%v haveV=%d globalV=%d", folder, protocol.DeviceIDFromBytes(device), name, need, have, haveVersion, vl.versions[0].version)
|
||||
|
||||
if cont := fn(gf); !cont {
|
||||
return
|
||||
}
|
||||
|
||||
// This file is handled, no need to look further in the version list
|
||||
continue nextFile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ldbListFolders(db *leveldb.DB) []string {
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.Debugf("created snapshot %p", snap)
|
||||
defer func() {
|
||||
l.Debugf("close snapshot %p", snap)
|
||||
snap.Release()
|
||||
}()
|
||||
|
||||
dbi := snap.NewIterator(util.BytesPrefix([]byte{KeyTypeGlobal}), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
folderExists := make(map[string]bool)
|
||||
for dbi.Next() {
|
||||
folder := string(globalKeyFolder(dbi.Key()))
|
||||
if !folderExists[folder] {
|
||||
folderExists[folder] = true
|
||||
}
|
||||
}
|
||||
|
||||
folders := make([]string, 0, len(folderExists))
|
||||
for k := range folderExists {
|
||||
folders = append(folders, k)
|
||||
}
|
||||
|
||||
sort.Strings(folders)
|
||||
return folders
|
||||
}
|
||||
|
||||
func ldbDropFolder(db *leveldb.DB, folder []byte) {
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.Debugf("created snapshot %p", snap)
|
||||
defer func() {
|
||||
l.Debugf("close snapshot %p", snap)
|
||||
snap.Release()
|
||||
}()
|
||||
|
||||
// Remove all items related to the given folder from the device->file bucket
|
||||
dbi := snap.NewIterator(util.BytesPrefix([]byte{KeyTypeDevice}), nil)
|
||||
for dbi.Next() {
|
||||
itemFolder := deviceKeyFolder(dbi.Key())
|
||||
if bytes.Compare(folder, itemFolder) == 0 {
|
||||
db.Delete(dbi.Key(), nil)
|
||||
}
|
||||
}
|
||||
dbi.Release()
|
||||
|
||||
// Remove all items related to the given folder from the global bucket
|
||||
dbi = snap.NewIterator(util.BytesPrefix([]byte{KeyTypeGlobal}), nil)
|
||||
for dbi.Next() {
|
||||
itemFolder := globalKeyFolder(dbi.Key())
|
||||
if bytes.Compare(folder, itemFolder) == 0 {
|
||||
db.Delete(dbi.Key(), nil)
|
||||
}
|
||||
}
|
||||
dbi.Release()
|
||||
}
|
||||
|
||||
func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
|
||||
if truncate {
|
||||
var tf FileInfoTruncated
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
var tf protocol.FileInfo
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
func ldbCheckGlobals(db *leveldb.DB, folder []byte, globalSize *sizeTracker) {
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
l.Debugf("created snapshot %p", snap)
|
||||
defer func() {
|
||||
l.Debugf("close snapshot %p", snap)
|
||||
snap.Release()
|
||||
}()
|
||||
|
||||
start := globalKey(folder, nil)
|
||||
limit := globalKey(folder, []byte{0xff, 0xff, 0xff, 0xff})
|
||||
dbi := snap.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
batch := new(leveldb.Batch)
|
||||
l.Debugf("new batch %p", batch)
|
||||
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
gk := dbi.Key()
|
||||
var vl versionList
|
||||
err := vl.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Check the global version list for consistency. An issue in previous
|
||||
// versions of goleveldb could result in reordered writes so that
|
||||
// there are global entries pointing to no longer existing files. Here
|
||||
// we find those and clear them out.
|
||||
|
||||
name := globalKeyName(gk)
|
||||
var newVL versionList
|
||||
for i, version := range vl.versions {
|
||||
fk = deviceKeyInto(fk[:cap(fk)], folder, version.device, name)
|
||||
|
||||
l.Debugf("snap.Get %p %x", snap, fk)
|
||||
_, err := snap.Get(fk, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
newVL.versions = append(newVL.versions, version)
|
||||
|
||||
if i == 0 {
|
||||
fi, ok := ldbGet(snap, folder, version.device, name)
|
||||
if !ok {
|
||||
panic("nonexistent global master file")
|
||||
}
|
||||
globalSize.addFile(fi)
|
||||
}
|
||||
}
|
||||
|
||||
if len(newVL.versions) != len(vl.versions) {
|
||||
l.Infof("db repair: rewriting global version list for %x %x", gk[1:1+64], gk[1+64:])
|
||||
batch.Put(dbi.Key(), newVL.MustMarshalXDR())
|
||||
}
|
||||
}
|
||||
l.Debugf("db check completed for %q", folder)
|
||||
db.Write(batch, nil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type deletionHandler func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator) int64
|
||||
|
||||
type Instance struct {
|
||||
*leveldb.DB
|
||||
}
|
||||
|
||||
func Open(file string) (*Instance, error) {
|
||||
opts := &opt.Options{
|
||||
OpenFilesCacheCapacity: 100,
|
||||
WriteBuffer: 4 << 20,
|
||||
}
|
||||
|
||||
db, err := leveldb.OpenFile(file, opts)
|
||||
if leveldbIsCorrupted(err) {
|
||||
db, err = leveldb.RecoverFile(file, opts)
|
||||
}
|
||||
if leveldbIsCorrupted(err) {
|
||||
// The database is corrupted, and we've tried to recover it but it
|
||||
// didn't work. At this point there isn't much to do beyond dropping
|
||||
// the database and reindexing...
|
||||
l.Infoln("Database corruption detected, unable to recover. Reinitializing...")
|
||||
if err := os.RemoveAll(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err = leveldb.OpenFile(file, opts)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newDBInstance(db), nil
|
||||
}
|
||||
|
||||
func OpenMemory() *Instance {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
return newDBInstance(db)
|
||||
}
|
||||
|
||||
func newDBInstance(db *leveldb.DB) *Instance {
|
||||
return &Instance{
|
||||
DB: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) genericReplace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker, deleteFn deletionHandler) int64 {
|
||||
sort.Sort(fileList(fs)) // sort list on name, same as in the database
|
||||
|
||||
start := db.deviceKey(folder, device, nil) // before all folder/device files
|
||||
limit := db.deviceKey(folder, device, []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
|
||||
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
moreDb := dbi.Next()
|
||||
fsi := 0
|
||||
var maxLocalVer int64
|
||||
|
||||
isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
|
||||
for {
|
||||
var newName, oldName []byte
|
||||
moreFs := fsi < len(fs)
|
||||
|
||||
if !moreDb && !moreFs {
|
||||
break
|
||||
}
|
||||
|
||||
if moreFs {
|
||||
newName = []byte(fs[fsi].Name)
|
||||
}
|
||||
|
||||
if moreDb {
|
||||
oldName = db.deviceKeyName(dbi.Key())
|
||||
}
|
||||
|
||||
cmp := bytes.Compare(newName, oldName)
|
||||
|
||||
l.Debugf("generic replace; folder=%q device=%v moreFs=%v moreDb=%v cmp=%d newName=%q oldName=%q", folder, protocol.DeviceIDFromBytes(device), moreFs, moreDb, cmp, newName, oldName)
|
||||
|
||||
switch {
|
||||
case moreFs && (!moreDb || cmp == -1):
|
||||
l.Debugln("generic replace; missing - insert")
|
||||
// Database is missing this file. Insert it.
|
||||
if lv := t.insertFile(folder, device, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if isLocalDevice {
|
||||
localSize.addFile(fs[fsi])
|
||||
}
|
||||
if fs[fsi].IsInvalid() {
|
||||
t.removeFromGlobal(folder, device, newName, globalSize)
|
||||
} else {
|
||||
t.updateGlobal(folder, device, fs[fsi], globalSize)
|
||||
}
|
||||
fsi++
|
||||
|
||||
case moreFs && moreDb && cmp == 0:
|
||||
// File exists on both sides - compare versions. We might get an
|
||||
// update with the same version and different flags if a device has
|
||||
// marked a file as invalid, so handle that too.
|
||||
l.Debugln("generic replace; exists - compare")
|
||||
var ef FileInfoTruncated
|
||||
ef.UnmarshalXDR(dbi.Value())
|
||||
if !fs[fsi].Version.Equal(ef.Version) || fs[fsi].Flags != ef.Flags {
|
||||
l.Debugln("generic replace; differs - insert")
|
||||
if lv := t.insertFile(folder, device, fs[fsi]); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if isLocalDevice {
|
||||
localSize.removeFile(ef)
|
||||
localSize.addFile(fs[fsi])
|
||||
}
|
||||
if fs[fsi].IsInvalid() {
|
||||
t.removeFromGlobal(folder, device, newName, globalSize)
|
||||
} else {
|
||||
t.updateGlobal(folder, device, fs[fsi], globalSize)
|
||||
}
|
||||
} else {
|
||||
l.Debugln("generic replace; equal - ignore")
|
||||
}
|
||||
|
||||
fsi++
|
||||
moreDb = dbi.Next()
|
||||
|
||||
case moreDb && (!moreFs || cmp == 1):
|
||||
l.Debugln("generic replace; exists - remove")
|
||||
if lv := deleteFn(t, folder, device, oldName, dbi); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
moreDb = dbi.Next()
|
||||
}
|
||||
|
||||
// Write out and reuse the batch every few records, to avoid the batch
|
||||
// growing too large and thus allocating unnecessarily much memory.
|
||||
t.checkFlush()
|
||||
}
|
||||
|
||||
return maxLocalVer
|
||||
}
|
||||
|
||||
func (db *Instance) replace(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) int64 {
|
||||
// TODO: Return the remaining maxLocalVer?
|
||||
return db.genericReplace(folder, device, fs, localSize, globalSize, func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator) int64 {
|
||||
// Database has a file that we are missing. Remove it.
|
||||
l.Debugf("delete; folder=%q device=%v name=%q", folder, protocol.DeviceIDFromBytes(device), name)
|
||||
t.removeFromGlobal(folder, device, name, globalSize)
|
||||
t.Delete(dbi.Key())
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, localSize, globalSize *sizeTracker) int64 {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
var maxLocalVer int64
|
||||
var fk []byte
|
||||
isLocalDevice := bytes.Equal(device, protocol.LocalDeviceID[:])
|
||||
for _, f := range fs {
|
||||
name := []byte(f.Name)
|
||||
fk = db.deviceKeyInto(fk[:cap(fk)], folder, device, name)
|
||||
bs, err := t.Get(fk, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
if isLocalDevice {
|
||||
localSize.addFile(f)
|
||||
}
|
||||
|
||||
if lv := t.insertFile(folder, device, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if f.IsInvalid() {
|
||||
t.removeFromGlobal(folder, device, name, globalSize)
|
||||
} else {
|
||||
t.updateGlobal(folder, device, f, globalSize)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var ef FileInfoTruncated
|
||||
err = ef.UnmarshalXDR(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Flags might change without the version being bumped when we set the
|
||||
// invalid flag on an existing file.
|
||||
if !ef.Version.Equal(f.Version) || ef.Flags != f.Flags {
|
||||
if isLocalDevice {
|
||||
localSize.removeFile(ef)
|
||||
localSize.addFile(f)
|
||||
}
|
||||
|
||||
if lv := t.insertFile(folder, device, f); lv > maxLocalVer {
|
||||
maxLocalVer = lv
|
||||
}
|
||||
if f.IsInvalid() {
|
||||
t.removeFromGlobal(folder, device, name, globalSize)
|
||||
} else {
|
||||
t.updateGlobal(folder, device, f, globalSize)
|
||||
}
|
||||
}
|
||||
|
||||
// Write out and reuse the batch every few records, to avoid the batch
|
||||
// growing too large and thus allocating unnecessarily much memory.
|
||||
t.checkFlush()
|
||||
}
|
||||
|
||||
return maxLocalVer
|
||||
}
|
||||
|
||||
func (db *Instance) withHave(folder, device []byte, truncate bool, fn Iterator) {
|
||||
start := db.deviceKey(folder, device, nil) // before all folder/device files
|
||||
limit := db.deviceKey(folder, device, []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
|
||||
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
f, err := unmarshalTrunc(dbi.Value(), truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if cont := fn(f); !cont {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte, f FileInfoTruncated) bool) {
|
||||
start := db.deviceKey(folder, nil, nil) // before all folder/device files
|
||||
limit := db.deviceKey(folder, protocol.LocalDeviceID[:], []byte{0xff, 0xff, 0xff, 0xff}) // after all folder/device files
|
||||
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
device := db.deviceKeyDevice(dbi.Key())
|
||||
var f FileInfoTruncated
|
||||
err := f.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
switch f.Name {
|
||||
case "", ".", "..", "/": // A few obviously invalid filenames
|
||||
l.Infof("Dropping invalid filename %q from database", f.Name)
|
||||
t.removeFromGlobal(folder, device, nil, nil)
|
||||
t.Delete(dbi.Key())
|
||||
t.checkFlush()
|
||||
continue
|
||||
}
|
||||
|
||||
if cont := fn(device, f); !cont {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) getFile(folder, device, file []byte) (protocol.FileInfo, bool) {
|
||||
return getFile(db, db.deviceKey(folder, device, file))
|
||||
}
|
||||
|
||||
func (db *Instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, bool) {
|
||||
k := db.globalKey(folder, file)
|
||||
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
bs, err := t.Get(k, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return nil, false
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var vl versionList
|
||||
err = vl.UnmarshalXDR(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(vl.versions) == 0 {
|
||||
l.Debugln(k)
|
||||
panic("no versions?")
|
||||
}
|
||||
|
||||
k = db.deviceKey(folder, vl.versions[0].device, file)
|
||||
bs, err = t.Get(k, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fi, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fi, true
|
||||
}
|
||||
|
||||
func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.globalKey(folder, prefix)), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
var vl versionList
|
||||
err := vl.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(vl.versions) == 0 {
|
||||
l.Debugln(dbi.Key())
|
||||
panic("no versions?")
|
||||
}
|
||||
name := db.globalKeyName(dbi.Key())
|
||||
fk = db.deviceKeyInto(fk[:cap(fk)], folder, vl.versions[0].device, name)
|
||||
bs, err := t.Get(fk, nil)
|
||||
if err != nil {
|
||||
l.Debugf("folder: %q (%x)", folder, folder)
|
||||
l.Debugf("key: %q (%x)", dbi.Key(), dbi.Key())
|
||||
l.Debugf("vl: %v", vl)
|
||||
l.Debugf("vl.versions[0].device: %x", vl.versions[0].device)
|
||||
l.Debugf("name: %q (%x)", name, name)
|
||||
l.Debugf("fk: %q", fk)
|
||||
l.Debugf("fk: %x %x %x", fk[1:1+64], fk[1+64:1+64+32], fk[1+64+32:])
|
||||
panic(err)
|
||||
}
|
||||
|
||||
f, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if cont := fn(f); !cont {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) availability(folder, file []byte) []protocol.DeviceID {
|
||||
k := db.globalKey(folder, file)
|
||||
bs, err := db.Get(k, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var vl versionList
|
||||
err = vl.UnmarshalXDR(bs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var devices []protocol.DeviceID
|
||||
for _, v := range vl.versions {
|
||||
if !v.version.Equal(vl.versions[0].version) {
|
||||
break
|
||||
}
|
||||
n := protocol.DeviceIDFromBytes(v.device)
|
||||
devices = append(devices, n)
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
||||
|
||||
func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator) {
|
||||
start := db.globalKey(folder, nil)
|
||||
limit := db.globalKey(folder, []byte{0xff, 0xff, 0xff, 0xff})
|
||||
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
nextFile:
|
||||
for dbi.Next() {
|
||||
var vl versionList
|
||||
err := vl.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(vl.versions) == 0 {
|
||||
l.Debugln(dbi.Key())
|
||||
panic("no versions?")
|
||||
}
|
||||
|
||||
have := false // If we have the file, any version
|
||||
need := false // If we have a lower version of the file
|
||||
var haveVersion protocol.Vector
|
||||
for _, v := range vl.versions {
|
||||
if bytes.Compare(v.device, device) == 0 {
|
||||
have = true
|
||||
haveVersion = v.version
|
||||
// XXX: This marks Concurrent (i.e. conflicting) changes as
|
||||
// needs. Maybe we should do that, but it needs special
|
||||
// handling in the puller.
|
||||
need = !v.version.GreaterEqual(vl.versions[0].version)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if need || !have {
|
||||
name := db.globalKeyName(dbi.Key())
|
||||
needVersion := vl.versions[0].version
|
||||
|
||||
nextVersion:
|
||||
for i := range vl.versions {
|
||||
if !vl.versions[i].version.Equal(needVersion) {
|
||||
// We haven't found a valid copy of the file with the needed version.
|
||||
continue nextFile
|
||||
}
|
||||
fk = db.deviceKeyInto(fk[:cap(fk)], folder, vl.versions[i].device, name)
|
||||
bs, err := t.Get(fk, nil)
|
||||
if err != nil {
|
||||
var id protocol.DeviceID
|
||||
copy(id[:], device)
|
||||
l.Debugf("device: %v", id)
|
||||
l.Debugf("need: %v, have: %v", need, have)
|
||||
l.Debugf("key: %q (%x)", dbi.Key(), dbi.Key())
|
||||
l.Debugf("vl: %v", vl)
|
||||
l.Debugf("i: %v", i)
|
||||
l.Debugf("fk: %q (%x)", fk, fk)
|
||||
l.Debugf("name: %q (%x)", name, name)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
gf, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if gf.IsInvalid() {
|
||||
// The file is marked invalid for whatever reason, don't use it.
|
||||
continue nextVersion
|
||||
}
|
||||
|
||||
if gf.IsDeleted() && !have {
|
||||
// We don't need deleted files that we don't have
|
||||
continue nextFile
|
||||
}
|
||||
|
||||
l.Debugf("need folder=%q device=%v name=%q need=%v have=%v haveV=%d globalV=%d", folder, protocol.DeviceIDFromBytes(device), name, need, have, haveVersion, vl.versions[0].version)
|
||||
|
||||
if cont := fn(gf); !cont {
|
||||
return
|
||||
}
|
||||
|
||||
// This file is handled, no need to look further in the version list
|
||||
continue nextFile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) ListFolders() []string {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeGlobal}), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
folderExists := make(map[string]bool)
|
||||
for dbi.Next() {
|
||||
folder := string(db.globalKeyFolder(dbi.Key()))
|
||||
if !folderExists[folder] {
|
||||
folderExists[folder] = true
|
||||
}
|
||||
}
|
||||
|
||||
folders := make([]string, 0, len(folderExists))
|
||||
for k := range folderExists {
|
||||
folders = append(folders, k)
|
||||
}
|
||||
|
||||
sort.Strings(folders)
|
||||
return folders
|
||||
}
|
||||
|
||||
func (db *Instance) dropFolder(folder []byte) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
// Remove all items related to the given folder from the device->file bucket
|
||||
dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeDevice}), nil)
|
||||
for dbi.Next() {
|
||||
itemFolder := db.deviceKeyFolder(dbi.Key())
|
||||
if bytes.Compare(folder, itemFolder) == 0 {
|
||||
db.Delete(dbi.Key(), nil)
|
||||
}
|
||||
}
|
||||
dbi.Release()
|
||||
|
||||
// Remove all items related to the given folder from the global bucket
|
||||
dbi = t.NewIterator(util.BytesPrefix([]byte{KeyTypeGlobal}), nil)
|
||||
for dbi.Next() {
|
||||
itemFolder := db.globalKeyFolder(dbi.Key())
|
||||
if bytes.Compare(folder, itemFolder) == 0 {
|
||||
db.Delete(dbi.Key(), nil)
|
||||
}
|
||||
}
|
||||
dbi.Release()
|
||||
}
|
||||
|
||||
func (db *Instance) checkGlobals(folder []byte, globalSize *sizeTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
start := db.globalKey(folder, nil)
|
||||
limit := db.globalKey(folder, []byte{0xff, 0xff, 0xff, 0xff})
|
||||
dbi := t.NewIterator(&util.Range{Start: start, Limit: limit}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
gk := dbi.Key()
|
||||
var vl versionList
|
||||
err := vl.UnmarshalXDR(dbi.Value())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Check the global version list for consistency. An issue in previous
|
||||
// versions of goleveldb could result in reordered writes so that
|
||||
// there are global entries pointing to no longer existing files. Here
|
||||
// we find those and clear them out.
|
||||
|
||||
name := db.globalKeyName(gk)
|
||||
var newVL versionList
|
||||
for i, version := range vl.versions {
|
||||
fk = db.deviceKeyInto(fk[:cap(fk)], folder, version.device, name)
|
||||
|
||||
_, err := t.Get(fk, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
newVL.versions = append(newVL.versions, version)
|
||||
|
||||
if i == 0 {
|
||||
fi, ok := t.getFile(folder, version.device, name)
|
||||
if !ok {
|
||||
panic("nonexistent global master file")
|
||||
}
|
||||
globalSize.addFile(fi)
|
||||
}
|
||||
}
|
||||
|
||||
if len(newVL.versions) != len(vl.versions) {
|
||||
t.Put(dbi.Key(), newVL.MustMarshalXDR())
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
l.Debugf("db check completed for %q", folder)
|
||||
}
|
||||
|
||||
// deviceKey returns a byte slice encoding the following information:
|
||||
// keyTypeDevice (1 byte)
|
||||
// folder (64 bytes)
|
||||
// device (32 bytes)
|
||||
// name (variable size)
|
||||
func (db *Instance) deviceKey(folder, device, file []byte) []byte {
|
||||
return db.deviceKeyInto(nil, folder, device, file)
|
||||
}
|
||||
|
||||
func (db *Instance) deviceKeyInto(k []byte, folder, device, file []byte) []byte {
|
||||
reqLen := 1 + 64 + 32 + len(file)
|
||||
if len(k) < reqLen {
|
||||
k = make([]byte, reqLen)
|
||||
}
|
||||
k[0] = KeyTypeDevice
|
||||
if len(folder) > 64 {
|
||||
panic("folder name too long")
|
||||
}
|
||||
copy(k[1:], []byte(folder))
|
||||
copy(k[1+64:], device[:])
|
||||
copy(k[1+64+32:], []byte(file))
|
||||
return k[:reqLen]
|
||||
}
|
||||
|
||||
func (db *Instance) deviceKeyName(key []byte) []byte {
|
||||
return key[1+64+32:]
|
||||
}
|
||||
|
||||
func (db *Instance) deviceKeyFolder(key []byte) []byte {
|
||||
folder := key[1 : 1+64]
|
||||
izero := bytes.IndexByte(folder, 0)
|
||||
if izero < 0 {
|
||||
return folder
|
||||
}
|
||||
return folder[:izero]
|
||||
}
|
||||
|
||||
func (db *Instance) deviceKeyDevice(key []byte) []byte {
|
||||
return key[1+64 : 1+64+32]
|
||||
}
|
||||
|
||||
// globalKey returns a byte slice encoding the following information:
|
||||
// keyTypeGlobal (1 byte)
|
||||
// folder (64 bytes)
|
||||
// name (variable size)
|
||||
func (db *Instance) globalKey(folder, file []byte) []byte {
|
||||
k := make([]byte, 1+64+len(file))
|
||||
k[0] = KeyTypeGlobal
|
||||
if len(folder) > 64 {
|
||||
panic("folder name too long")
|
||||
}
|
||||
copy(k[1:], []byte(folder))
|
||||
copy(k[1+64:], []byte(file))
|
||||
return k
|
||||
}
|
||||
|
||||
func (db *Instance) globalKeyName(key []byte) []byte {
|
||||
return key[1+64:]
|
||||
}
|
||||
|
||||
func (db *Instance) globalKeyFolder(key []byte) []byte {
|
||||
folder := key[1 : 1+64]
|
||||
izero := bytes.IndexByte(folder, 0)
|
||||
if izero < 0 {
|
||||
return folder
|
||||
}
|
||||
return folder[:izero]
|
||||
}
|
||||
|
||||
func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
|
||||
if truncate {
|
||||
var tf FileInfoTruncated
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
var tf protocol.FileInfo
|
||||
err := tf.UnmarshalXDR(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
// A "better" version of leveldb's errors.IsCorrupted.
|
||||
func leveldbIsCorrupted(err error) bool {
|
||||
switch {
|
||||
case err == nil:
|
||||
return false
|
||||
|
||||
case errors.IsCorrupted(err):
|
||||
return true
|
||||
|
||||
case strings.Contains(err.Error(), "corrupted"):
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
+11
-7
@@ -16,17 +16,19 @@ func TestDeviceKey(t *testing.T) {
|
||||
dev := []byte("device67890123456789012345678901")
|
||||
name := []byte("name")
|
||||
|
||||
key := deviceKey(fld, dev, name)
|
||||
db := &Instance{}
|
||||
|
||||
fld2 := deviceKeyFolder(key)
|
||||
key := db.deviceKey(fld, dev, name)
|
||||
|
||||
fld2 := db.deviceKeyFolder(key)
|
||||
if bytes.Compare(fld2, fld) != 0 {
|
||||
t.Errorf("wrong folder %q != %q", fld2, fld)
|
||||
}
|
||||
dev2 := deviceKeyDevice(key)
|
||||
dev2 := db.deviceKeyDevice(key)
|
||||
if bytes.Compare(dev2, dev) != 0 {
|
||||
t.Errorf("wrong device %q != %q", dev2, dev)
|
||||
}
|
||||
name2 := deviceKeyName(key)
|
||||
name2 := db.deviceKeyName(key)
|
||||
if bytes.Compare(name2, name) != 0 {
|
||||
t.Errorf("wrong name %q != %q", name2, name)
|
||||
}
|
||||
@@ -36,13 +38,15 @@ func TestGlobalKey(t *testing.T) {
|
||||
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
|
||||
name := []byte("name")
|
||||
|
||||
key := globalKey(fld, name)
|
||||
db := &Instance{}
|
||||
|
||||
fld2 := globalKeyFolder(key)
|
||||
key := db.globalKey(fld, name)
|
||||
|
||||
fld2 := db.globalKeyFolder(key)
|
||||
if bytes.Compare(fld2, fld) != 0 {
|
||||
t.Errorf("wrong folder %q != %q", fld2, fld)
|
||||
}
|
||||
name2 := globalKeyName(key)
|
||||
name2 := db.globalKeyName(key)
|
||||
if bytes.Compare(name2, name) != 0 {
|
||||
t.Errorf("wrong name %q != %q", name2, name)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
// A readOnlyTransaction represents a database snapshot.
|
||||
type readOnlyTransaction struct {
|
||||
*leveldb.Snapshot
|
||||
db *Instance
|
||||
}
|
||||
|
||||
func (db *Instance) newReadOnlyTransaction() readOnlyTransaction {
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return readOnlyTransaction{
|
||||
Snapshot: snap,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (t readOnlyTransaction) close() {
|
||||
t.Release()
|
||||
}
|
||||
|
||||
func (t readOnlyTransaction) getFile(folder, device, file []byte) (protocol.FileInfo, bool) {
|
||||
return getFile(t, t.db.deviceKey(folder, device, file))
|
||||
}
|
||||
|
||||
// A readWriteTransaction is a readOnlyTransaction plus a batch for writes.
|
||||
// The batch will be committed on close() or by checkFlush() if it exceeds the
|
||||
// batch size.
|
||||
type readWriteTransaction struct {
|
||||
readOnlyTransaction
|
||||
*leveldb.Batch
|
||||
}
|
||||
|
||||
func (db *Instance) newReadWriteTransaction() readWriteTransaction {
|
||||
t := db.newReadOnlyTransaction()
|
||||
return readWriteTransaction{
|
||||
readOnlyTransaction: t,
|
||||
Batch: new(leveldb.Batch),
|
||||
}
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) close() {
|
||||
if err := t.db.Write(t.Batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.readOnlyTransaction.close()
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) checkFlush() {
|
||||
if t.Batch.Len() > batchFlushSize {
|
||||
if err := t.db.Write(t.Batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
t.Batch.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) insertFile(folder, device []byte, file protocol.FileInfo) int64 {
|
||||
l.Debugf("insert; folder=%q device=%v %v", folder, protocol.DeviceIDFromBytes(device), file)
|
||||
|
||||
if file.LocalVersion == 0 {
|
||||
file.LocalVersion = clock(0)
|
||||
}
|
||||
|
||||
name := []byte(file.Name)
|
||||
nk := t.db.deviceKey(folder, device, name)
|
||||
t.Put(nk, file.MustMarshalXDR())
|
||||
|
||||
return file.LocalVersion
|
||||
}
|
||||
|
||||
// updateGlobal adds this device+version to the version list for the given
|
||||
// file. If the device is already present in the list, the version is updated.
|
||||
// If the file does not have an entry in the global list, it is created.
|
||||
func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.FileInfo, globalSize *sizeTracker) bool {
|
||||
l.Debugf("update global; folder=%q device=%v file=%q version=%d", folder, protocol.DeviceIDFromBytes(device), file.Name, file.Version)
|
||||
name := []byte(file.Name)
|
||||
gk := t.db.globalKey(folder, name)
|
||||
svl, err := t.Get(gk, nil)
|
||||
if err != nil && err != leveldb.ErrNotFound {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var fl versionList
|
||||
var oldFile protocol.FileInfo
|
||||
var hasOldFile bool
|
||||
// Remove the device from the current version list
|
||||
if svl != nil {
|
||||
err = fl.UnmarshalXDR(svl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i := range fl.versions {
|
||||
if bytes.Compare(fl.versions[i].device, device) == 0 {
|
||||
if fl.versions[i].version.Equal(file.Version) {
|
||||
// No need to do anything
|
||||
return false
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
// Keep the current newest file around so we can subtract it from
|
||||
// the globalSize if we replace it.
|
||||
oldFile, hasOldFile = t.getFile(folder, fl.versions[0].device, name)
|
||||
}
|
||||
|
||||
fl.versions = append(fl.versions[:i], fl.versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nv := fileVersion{
|
||||
device: device,
|
||||
version: file.Version,
|
||||
}
|
||||
|
||||
insertedAt := -1
|
||||
// Find a position in the list to insert this file. The file at the front
|
||||
// of the list is the newer, the "global".
|
||||
for i := range fl.versions {
|
||||
switch fl.versions[i].version.Compare(file.Version) {
|
||||
case protocol.Equal, protocol.Lesser:
|
||||
// The version at this point in the list is equal to or lesser
|
||||
// ("older") than us. We insert ourselves in front of it.
|
||||
fl.versions = insertVersion(fl.versions, i, nv)
|
||||
insertedAt = i
|
||||
goto done
|
||||
|
||||
case protocol.ConcurrentLesser, protocol.ConcurrentGreater:
|
||||
// The version at this point is in conflict with us. We must pull
|
||||
// the actual file metadata to determine who wins. If we win, we
|
||||
// insert ourselves in front of the loser here. (The "Lesser" and
|
||||
// "Greater" in the condition above is just based on the device
|
||||
// IDs in the version vector, which is not the only thing we use
|
||||
// to determine the winner.)
|
||||
of, ok := t.getFile(folder, fl.versions[i].device, name)
|
||||
if !ok {
|
||||
panic("file referenced in version list does not exist")
|
||||
}
|
||||
if file.WinsConflict(of) {
|
||||
fl.versions = insertVersion(fl.versions, i, nv)
|
||||
insertedAt = i
|
||||
goto done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find a position for an insert above, so append to the end.
|
||||
fl.versions = append(fl.versions, nv)
|
||||
insertedAt = len(fl.versions) - 1
|
||||
|
||||
done:
|
||||
if insertedAt == 0 {
|
||||
// We just inserted a new newest version. Fixup the global size
|
||||
// calculation.
|
||||
if !file.Version.Equal(oldFile.Version) {
|
||||
globalSize.addFile(file)
|
||||
if hasOldFile {
|
||||
// We have the old file that was removed at the head of the list.
|
||||
globalSize.removeFile(oldFile)
|
||||
} else if len(fl.versions) > 1 {
|
||||
// The previous newest version is now at index 1, grab it from there.
|
||||
oldFile, ok := t.getFile(folder, fl.versions[1].device, name)
|
||||
if !ok {
|
||||
panic("file referenced in version list does not exist")
|
||||
}
|
||||
globalSize.removeFile(oldFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
l.Debugf("new global after update: %v", fl)
|
||||
t.Put(gk, fl.MustMarshalXDR())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// removeFromGlobal removes the device from the global version list for the
|
||||
// given file. If the version list is empty after this, the file entry is
|
||||
// removed entirely.
|
||||
func (t readWriteTransaction) removeFromGlobal(folder, device, file []byte, globalSize *sizeTracker) {
|
||||
l.Debugf("remove from global; folder=%q device=%v file=%q", folder, protocol.DeviceIDFromBytes(device), file)
|
||||
|
||||
gk := t.db.globalKey(folder, file)
|
||||
svl, err := t.Get(gk, nil)
|
||||
if err != nil {
|
||||
// We might be called to "remove" a global version that doesn't exist
|
||||
// if the first update for the file is already marked invalid.
|
||||
return
|
||||
}
|
||||
|
||||
var fl versionList
|
||||
err = fl.UnmarshalXDR(svl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
removed := false
|
||||
for i := range fl.versions {
|
||||
if bytes.Compare(fl.versions[i].device, device) == 0 {
|
||||
if i == 0 && globalSize != nil {
|
||||
f, ok := t.getFile(folder, device, file)
|
||||
if !ok {
|
||||
panic("removing nonexistent file")
|
||||
}
|
||||
globalSize.removeFile(f)
|
||||
removed = true
|
||||
}
|
||||
fl.versions = append(fl.versions[:i], fl.versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(fl.versions) == 0 {
|
||||
t.Delete(gk)
|
||||
} else {
|
||||
l.Debugf("new global after remove: %v", fl)
|
||||
t.Put(gk, fl.MustMarshalXDR())
|
||||
if removed {
|
||||
f, ok := t.getFile(folder, fl.versions[0].device, file)
|
||||
if !ok {
|
||||
panic("new global is nonexistent file")
|
||||
}
|
||||
globalSize.addFile(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func insertVersion(vl []fileVersion, i int, v fileVersion) []fileVersion {
|
||||
t := append(vl, fileVersion{})
|
||||
copy(t[i+1:], t[i:])
|
||||
t[i] = v
|
||||
return t
|
||||
}
|
||||
@@ -17,13 +17,13 @@ import (
|
||||
// NamespacedKV is a simple key-value store using a specific namespace within
|
||||
// a leveldb.
|
||||
type NamespacedKV struct {
|
||||
db *leveldb.DB
|
||||
db *Instance
|
||||
prefix []byte
|
||||
}
|
||||
|
||||
// NewNamespacedKV returns a new NamespacedKV that lives in the namespace
|
||||
// specified by the prefix.
|
||||
func NewNamespacedKV(db *leveldb.DB, prefix string) *NamespacedKV {
|
||||
func NewNamespacedKV(db *Instance, prefix string) *NamespacedKV {
|
||||
return &NamespacedKV{
|
||||
db: db,
|
||||
prefix: []byte(prefix),
|
||||
|
||||
@@ -9,16 +9,10 @@ package db
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
func TestNamespacedInt(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := OpenMemory()
|
||||
|
||||
n1 := NewNamespacedKV(ldb, "foo")
|
||||
n2 := NewNamespacedKV(ldb, "bar")
|
||||
@@ -53,10 +47,7 @@ func TestNamespacedInt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespacedTime(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := OpenMemory()
|
||||
|
||||
n1 := NewNamespacedKV(ldb, "foo")
|
||||
|
||||
@@ -73,10 +64,7 @@ func TestNamespacedTime(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespacedString(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := OpenMemory()
|
||||
|
||||
n1 := NewNamespacedKV(ldb, "foo")
|
||||
|
||||
@@ -92,10 +80,7 @@ func TestNamespacedString(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNamespacedReset(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := OpenMemory()
|
||||
|
||||
n1 := NewNamespacedKV(ldb, "foo")
|
||||
|
||||
|
||||
+20
-26
@@ -18,14 +18,13 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type FileSet struct {
|
||||
localVersion map[protocol.DeviceID]int64
|
||||
mutex sync.Mutex
|
||||
folder string
|
||||
db *leveldb.DB
|
||||
db *Instance
|
||||
blockmap *BlockMap
|
||||
localSize sizeTracker
|
||||
globalSize sizeTracker
|
||||
@@ -93,7 +92,7 @@ func (s *sizeTracker) Size() (files, deleted int, bytes int64) {
|
||||
return s.files, s.deleted, s.bytes
|
||||
}
|
||||
|
||||
func NewFileSet(folder string, db *leveldb.DB) *FileSet {
|
||||
func NewFileSet(folder string, db *Instance) *FileSet {
|
||||
var s = FileSet{
|
||||
localVersion: make(map[protocol.DeviceID]int64),
|
||||
folder: folder,
|
||||
@@ -102,10 +101,10 @@ func NewFileSet(folder string, db *leveldb.DB) *FileSet {
|
||||
mutex: sync.NewMutex(),
|
||||
}
|
||||
|
||||
ldbCheckGlobals(db, []byte(folder), &s.globalSize)
|
||||
s.db.checkGlobals([]byte(folder), &s.globalSize)
|
||||
|
||||
var deviceID protocol.DeviceID
|
||||
ldbWithAllFolderTruncated(db, []byte(folder), func(device []byte, f FileInfoTruncated) bool {
|
||||
s.db.withAllFolderTruncated([]byte(folder), func(device []byte, f FileInfoTruncated) bool {
|
||||
copy(deviceID[:], device)
|
||||
if f.LocalVersion > s.localVersion[deviceID] {
|
||||
s.localVersion[deviceID] = f.LocalVersion
|
||||
@@ -126,7 +125,7 @@ func (s *FileSet) Replace(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
normalizeFilenames(fs)
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
s.localVersion[device] = ldbReplace(s.db, []byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
|
||||
s.localVersion[device] = s.db.replace([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize)
|
||||
if len(fs) == 0 {
|
||||
// Reset the local version if all files were removed.
|
||||
s.localVersion[device] = 0
|
||||
@@ -146,7 +145,7 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
discards := make([]protocol.FileInfo, 0, len(fs))
|
||||
updates := make([]protocol.FileInfo, 0, len(fs))
|
||||
for _, newFile := range fs {
|
||||
existingFile, ok := ldbGet(s.db, []byte(s.folder), device[:], []byte(newFile.Name))
|
||||
existingFile, ok := s.db.getFile([]byte(s.folder), device[:], []byte(newFile.Name))
|
||||
if !ok || !existingFile.Version.Equal(newFile.Version) {
|
||||
discards = append(discards, existingFile)
|
||||
updates = append(updates, newFile)
|
||||
@@ -155,54 +154,54 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
s.blockmap.Discard(discards)
|
||||
s.blockmap.Update(updates)
|
||||
}
|
||||
if lv := ldbUpdate(s.db, []byte(s.folder), device[:], fs, &s.localSize, &s.globalSize); lv > s.localVersion[device] {
|
||||
if lv := s.db.updateFiles([]byte(s.folder), device[:], fs, &s.localSize, &s.globalSize); lv > s.localVersion[device] {
|
||||
s.localVersion[device] = lv
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FileSet) WithNeed(device protocol.DeviceID, fn Iterator) {
|
||||
l.Debugf("%s WithNeed(%v)", s.folder, device)
|
||||
ldbWithNeed(s.db, []byte(s.folder), device[:], false, nativeFileIterator(fn))
|
||||
s.db.withNeed([]byte(s.folder), device[:], false, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *FileSet) WithNeedTruncated(device protocol.DeviceID, fn Iterator) {
|
||||
l.Debugf("%s WithNeedTruncated(%v)", s.folder, device)
|
||||
ldbWithNeed(s.db, []byte(s.folder), device[:], true, nativeFileIterator(fn))
|
||||
s.db.withNeed([]byte(s.folder), device[:], true, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *FileSet) WithHave(device protocol.DeviceID, fn Iterator) {
|
||||
l.Debugf("%s WithHave(%v)", s.folder, device)
|
||||
ldbWithHave(s.db, []byte(s.folder), device[:], false, nativeFileIterator(fn))
|
||||
s.db.withHave([]byte(s.folder), device[:], false, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *FileSet) WithHaveTruncated(device protocol.DeviceID, fn Iterator) {
|
||||
l.Debugf("%s WithHaveTruncated(%v)", s.folder, device)
|
||||
ldbWithHave(s.db, []byte(s.folder), device[:], true, nativeFileIterator(fn))
|
||||
s.db.withHave([]byte(s.folder), device[:], true, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *FileSet) WithGlobal(fn Iterator) {
|
||||
l.Debugf("%s WithGlobal()", s.folder)
|
||||
ldbWithGlobal(s.db, []byte(s.folder), nil, false, nativeFileIterator(fn))
|
||||
s.db.withGlobal([]byte(s.folder), nil, false, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *FileSet) WithGlobalTruncated(fn Iterator) {
|
||||
l.Debugf("%s WithGlobalTruncated()", s.folder)
|
||||
ldbWithGlobal(s.db, []byte(s.folder), nil, true, nativeFileIterator(fn))
|
||||
s.db.withGlobal([]byte(s.folder), nil, true, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *FileSet) WithPrefixedGlobalTruncated(prefix string, fn Iterator) {
|
||||
l.Debugf("%s WithPrefixedGlobalTruncated()", s.folder, prefix)
|
||||
ldbWithGlobal(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(prefix)), true, nativeFileIterator(fn))
|
||||
s.db.withGlobal([]byte(s.folder), []byte(osutil.NormalizedFilename(prefix)), true, nativeFileIterator(fn))
|
||||
}
|
||||
|
||||
func (s *FileSet) Get(device protocol.DeviceID, file string) (protocol.FileInfo, bool) {
|
||||
f, ok := ldbGet(s.db, []byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file)))
|
||||
f, ok := s.db.getFile([]byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file)))
|
||||
f.Name = osutil.NativeFilename(f.Name)
|
||||
return f, ok
|
||||
}
|
||||
|
||||
func (s *FileSet) GetGlobal(file string) (protocol.FileInfo, bool) {
|
||||
fi, ok := ldbGetGlobal(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(file)), false)
|
||||
fi, ok := s.db.getGlobal([]byte(s.folder), []byte(osutil.NormalizedFilename(file)), false)
|
||||
if !ok {
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
@@ -212,7 +211,7 @@ func (s *FileSet) GetGlobal(file string) (protocol.FileInfo, bool) {
|
||||
}
|
||||
|
||||
func (s *FileSet) GetGlobalTruncated(file string) (FileInfoTruncated, bool) {
|
||||
fi, ok := ldbGetGlobal(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(file)), true)
|
||||
fi, ok := s.db.getGlobal([]byte(s.folder), []byte(osutil.NormalizedFilename(file)), true)
|
||||
if !ok {
|
||||
return FileInfoTruncated{}, false
|
||||
}
|
||||
@@ -222,7 +221,7 @@ func (s *FileSet) GetGlobalTruncated(file string) (FileInfoTruncated, bool) {
|
||||
}
|
||||
|
||||
func (s *FileSet) Availability(file string) []protocol.DeviceID {
|
||||
return ldbAvailability(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(file)))
|
||||
return s.db.availability([]byte(s.folder), []byte(osutil.NormalizedFilename(file)))
|
||||
}
|
||||
|
||||
func (s *FileSet) LocalVersion(device protocol.DeviceID) int64 {
|
||||
@@ -239,15 +238,10 @@ func (s *FileSet) GlobalSize() (files, deleted int, bytes int64) {
|
||||
return s.globalSize.Size()
|
||||
}
|
||||
|
||||
// ListFolders returns the folder IDs seen in the database.
|
||||
func ListFolders(db *leveldb.DB) []string {
|
||||
return ldbListFolders(db)
|
||||
}
|
||||
|
||||
// DropFolder clears out all information related to the given folder from the
|
||||
// database.
|
||||
func DropFolder(db *leveldb.DB, folder string) {
|
||||
ldbDropFolder(db, []byte(folder))
|
||||
func DropFolder(db *Instance, folder string) {
|
||||
db.dropFolder([]byte(folder))
|
||||
bm := &BlockMap{
|
||||
db: db,
|
||||
folder: folder,
|
||||
|
||||
+12
-45
@@ -15,8 +15,6 @@ import (
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
var remoteDevice0, remoteDevice1 protocol.DeviceID
|
||||
@@ -96,11 +94,7 @@ func (l fileList) String() string {
|
||||
}
|
||||
|
||||
func TestGlobalSet(t *testing.T) {
|
||||
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
m := db.NewFileSet("test", ldb)
|
||||
|
||||
@@ -303,10 +297,7 @@ func TestGlobalSet(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNeedWithInvalid(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
s := db.NewFileSet("test", ldb)
|
||||
|
||||
@@ -343,10 +334,7 @@ func TestNeedWithInvalid(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateToInvalid(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
s := db.NewFileSet("test", ldb)
|
||||
|
||||
@@ -378,10 +366,7 @@ func TestUpdateToInvalid(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInvalidAvailability(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
s := db.NewFileSet("test", ldb)
|
||||
|
||||
@@ -419,10 +404,7 @@ func TestInvalidAvailability(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGlobalReset(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
m := db.NewFileSet("test", ldb)
|
||||
|
||||
@@ -460,10 +442,7 @@ func TestGlobalReset(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestNeed(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
m := db.NewFileSet("test", ldb)
|
||||
|
||||
@@ -501,10 +480,7 @@ func TestNeed(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLocalVersion(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
m := db.NewFileSet("test", ldb)
|
||||
|
||||
@@ -534,10 +510,7 @@ func TestLocalVersion(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestListDropFolder(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
s0 := db.NewFileSet("test0", ldb)
|
||||
local1 := []protocol.FileInfo{
|
||||
@@ -558,7 +531,7 @@ func TestListDropFolder(t *testing.T) {
|
||||
// Check that we have both folders and their data is in the global list
|
||||
|
||||
expectedFolderList := []string{"test0", "test1"}
|
||||
if actualFolderList := db.ListFolders(ldb); !reflect.DeepEqual(actualFolderList, expectedFolderList) {
|
||||
if actualFolderList := ldb.ListFolders(); !reflect.DeepEqual(actualFolderList, expectedFolderList) {
|
||||
t.Fatalf("FolderList mismatch\nE: %v\nA: %v", expectedFolderList, actualFolderList)
|
||||
}
|
||||
if l := len(globalList(s0)); l != 3 {
|
||||
@@ -573,7 +546,7 @@ func TestListDropFolder(t *testing.T) {
|
||||
db.DropFolder(ldb, "test1")
|
||||
|
||||
expectedFolderList = []string{"test0"}
|
||||
if actualFolderList := db.ListFolders(ldb); !reflect.DeepEqual(actualFolderList, expectedFolderList) {
|
||||
if actualFolderList := ldb.ListFolders(); !reflect.DeepEqual(actualFolderList, expectedFolderList) {
|
||||
t.Fatalf("FolderList mismatch\nE: %v\nA: %v", expectedFolderList, actualFolderList)
|
||||
}
|
||||
if l := len(globalList(s0)); l != 3 {
|
||||
@@ -585,10 +558,7 @@ func TestListDropFolder(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGlobalNeedWithInvalid(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
s := db.NewFileSet("test1", ldb)
|
||||
|
||||
@@ -625,10 +595,7 @@ func TestGlobalNeedWithInvalid(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLongPath(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
s := db.NewFileSet("test", ldb)
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ package db
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
// This type encapsulates a repository of mtimes for platforms where file mtimes
|
||||
@@ -25,7 +23,7 @@ type VirtualMtimeRepo struct {
|
||||
ns *NamespacedKV
|
||||
}
|
||||
|
||||
func NewVirtualMtimeRepo(ldb *leveldb.DB, folder string) *VirtualMtimeRepo {
|
||||
func NewVirtualMtimeRepo(ldb *Instance, folder string) *VirtualMtimeRepo {
|
||||
prefix := string(KeyTypeVirtualMtime) + folder
|
||||
|
||||
return &VirtualMtimeRepo{
|
||||
@@ -47,7 +45,7 @@ func (r *VirtualMtimeRepo) UpdateMtime(path string, diskMtime, actualMtime time.
|
||||
func (r *VirtualMtimeRepo) GetMtime(path string, diskMtime time.Time) time.Time {
|
||||
data, exists := r.ns.Bytes(path)
|
||||
if !exists {
|
||||
// Absense of debug print is significant enough in itself here
|
||||
// Absence of debug print is significant enough in itself here
|
||||
return diskMtime
|
||||
}
|
||||
|
||||
|
||||
@@ -9,16 +9,10 @@ package db
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
func TestVirtualMtimeRepo(t *testing.T) {
|
||||
ldb, err := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ldb := OpenMemory()
|
||||
|
||||
// A few repos so we can ensure they don't pollute each other
|
||||
repo1 := NewVirtualMtimeRepo(ldb, "folder1")
|
||||
|
||||
@@ -109,6 +109,12 @@ func (m *CachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays
|
||||
when: time.Now(),
|
||||
found: len(td)+len(tr) > 0,
|
||||
})
|
||||
} else {
|
||||
// Lookup returned error, add a negative cache entry.
|
||||
m.caches[i].Set(deviceID, CacheEntry{
|
||||
when: time.Now(),
|
||||
found: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
m.mut.Unlock()
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@ connection addresses (if any) and relay addresses (if any).
|
||||
}
|
||||
|
||||
It's OK for either of the "direct" or "relays" fields to be either the empty
|
||||
list ([]), null, or missing entirely. An announcment with both fields missing
|
||||
list ([]), null, or missing entirely. An announcement with both fields missing
|
||||
or empty is however not useful...
|
||||
|
||||
Any empty or unspecified IP addresses (i.e. addresses like tcp://:22000,
|
||||
@@ -39,7 +39,7 @@ The server response is empty, with code 200 (OK) on success. If no certificate
|
||||
was presented, status 403 (Forbidden) is returned. If the posted data doesn't
|
||||
conform to the expected format, 400 (Bad Request) is returned.
|
||||
|
||||
In successfull responses, the server may return a "Reannounce-After" header
|
||||
In successful responses, the server may return a "Reannounce-After" header
|
||||
containing the number of seconds after which the client should perform a new
|
||||
announcement.
|
||||
|
||||
@@ -58,7 +58,7 @@ Queries are performed as HTTPS GET requests to the announce server URL. The
|
||||
requested device ID is passed as the query parameter "device", in canonical
|
||||
string form, i.e. https://announce.syncthing.net/?device=ABC12345-....
|
||||
|
||||
Successfull responses will have status code 200 (OK) and carry a JSON payload
|
||||
Successful responses will have status code 200 (OK) and carry a JSON payload
|
||||
of the same format as the announcement above. The response will not contain
|
||||
empty or unspecified addresses.
|
||||
|
||||
|
||||
+5
-33
@@ -10,7 +10,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
@@ -197,6 +196,7 @@ func (c *localClient) registerDevice(src net.Addr, device Device) bool {
|
||||
// Any empty or unspecified addresses should be set to the source address
|
||||
// of the announcement. We also skip any addresses we can't parse.
|
||||
|
||||
l.Debugln("discover: Registering addresses for", id)
|
||||
var validAddresses []string
|
||||
for _, addr := range device.Addresses {
|
||||
u, err := url.Parse(addr.URL)
|
||||
@@ -215,9 +215,12 @@ func (c *localClient) registerDevice(src net.Addr, device Device) bool {
|
||||
continue
|
||||
}
|
||||
u.Host = net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port))
|
||||
l.Debugf("discover: Reconstructed URL is %#v", u)
|
||||
validAddresses = append(validAddresses, u.String())
|
||||
l.Debugf("discover: Replaced address %v in %s to get %s", tcpAddr.IP, addr.URL, u.String())
|
||||
} else {
|
||||
validAddresses = append(validAddresses, addr.URL)
|
||||
l.Debugf("discover: Accepted address %s verbatim", addr.URL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,41 +234,10 @@ func (c *localClient) registerDevice(src net.Addr, device Device) bool {
|
||||
if isNewDevice {
|
||||
events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
|
||||
"device": id.String(),
|
||||
"addrs": device.Addresses,
|
||||
"addrs": validAddresses,
|
||||
"relays": device.Relays,
|
||||
})
|
||||
}
|
||||
|
||||
return isNewDevice
|
||||
}
|
||||
|
||||
func addrToAddr(addr *net.TCPAddr) string {
|
||||
if len(addr.IP) == 0 || addr.IP.IsUnspecified() {
|
||||
return fmt.Sprintf(":%c", addr.Port)
|
||||
} else if bs := addr.IP.To4(); bs != nil {
|
||||
return net.JoinHostPort(bs.String(), strconv.Itoa(addr.Port))
|
||||
} else if bs := addr.IP.To16(); bs != nil {
|
||||
return net.JoinHostPort(bs.String(), strconv.Itoa(addr.Port))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func resolveAddrs(addrs []string) []string {
|
||||
var raddrs []string
|
||||
for _, addrStr := range addrs {
|
||||
uri, err := url.Parse(addrStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addrRes, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addr := addrToAddr(addrRes)
|
||||
if len(addr) > 0 {
|
||||
uri.Host = addr
|
||||
raddrs = append(raddrs, uri.String())
|
||||
}
|
||||
}
|
||||
return raddrs
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ const (
|
||||
FolderScanProgress
|
||||
ExternalPortMappingChanged
|
||||
RelayStateChanged
|
||||
LoginAttempt
|
||||
|
||||
AllEvents = (1 << iota) - 1
|
||||
)
|
||||
@@ -93,6 +94,8 @@ func (t EventType) String() string {
|
||||
return "ExternalPortMappingChanged"
|
||||
case RelayStateChanged:
|
||||
return "RelayStateChanged"
|
||||
case LoginAttempt:
|
||||
return "LoginAttempt"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
@@ -281,7 +284,7 @@ func (s *BufferedSubscription) Since(id int, into []Event) []Event {
|
||||
}
|
||||
|
||||
// Error returns a string pointer suitable for JSON marshalling errors. It
|
||||
// retains the "null on sucess" semantics, but ensures the error result is a
|
||||
// retains the "null on success" semantics, but ensures the error result is a
|
||||
// string regardless of the underlying concrete error type.
|
||||
func Error(err error) *string {
|
||||
if err == nil {
|
||||
|
||||
+13
-7
@@ -63,7 +63,7 @@ type logger struct {
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
// The default logger logs to standard output with a time prefix.
|
||||
// DefaultLogger logs to standard output with a time prefix.
|
||||
var DefaultLogger = New()
|
||||
|
||||
func New() Logger {
|
||||
@@ -107,19 +107,25 @@ func (l *logger) callHandlers(level LogLevel, s string) {
|
||||
|
||||
// Debugln logs a line with a DEBUG prefix.
|
||||
func (l *logger) Debugln(vals ...interface{}) {
|
||||
l.debugln(3, vals)
|
||||
}
|
||||
func (l *logger) debugln(level int, vals ...interface{}) {
|
||||
l.mut.Lock()
|
||||
defer l.mut.Unlock()
|
||||
s := fmt.Sprintln(vals...)
|
||||
l.logger.Output(2, "DEBUG: "+s)
|
||||
l.logger.Output(level, "DEBUG: "+s)
|
||||
l.callHandlers(LevelDebug, s)
|
||||
}
|
||||
|
||||
// Debugf logs a formatted line with a DEBUG prefix.
|
||||
func (l *logger) Debugf(format string, vals ...interface{}) {
|
||||
l.debugf(3, format, vals...)
|
||||
}
|
||||
func (l *logger) debugf(level int, format string, vals ...interface{}) {
|
||||
l.mut.Lock()
|
||||
defer l.mut.Unlock()
|
||||
s := fmt.Sprintf(format, vals...)
|
||||
l.logger.Output(2, "DEBUG: "+s)
|
||||
l.logger.Output(level, "DEBUG: "+s)
|
||||
l.callHandlers(LevelDebug, s)
|
||||
}
|
||||
|
||||
@@ -293,7 +299,7 @@ func (l *facilityLogger) Debugln(vals ...interface{}) {
|
||||
if !l.ShouldDebug(l.facility) {
|
||||
return
|
||||
}
|
||||
l.logger.Debugln(vals...)
|
||||
l.logger.debugln(3, vals...)
|
||||
}
|
||||
|
||||
// Debugf logs a formatted line with a DEBUG prefix.
|
||||
@@ -301,7 +307,7 @@ func (l *facilityLogger) Debugf(format string, vals ...interface{}) {
|
||||
if !l.ShouldDebug(l.facility) {
|
||||
return
|
||||
}
|
||||
l.logger.Debugf(format, vals...)
|
||||
l.logger.debugf(3, format, vals...)
|
||||
}
|
||||
|
||||
// A Recorder keeps a size limited record of log events.
|
||||
@@ -313,8 +319,8 @@ type Recorder struct {
|
||||
|
||||
// A Line represents a single log entry.
|
||||
type Line struct {
|
||||
When time.Time
|
||||
Message string
|
||||
When time.Time `json:"when"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func NewRecorder(l Logger, level LogLevel, size, initial int) *Recorder {
|
||||
|
||||
@@ -78,7 +78,7 @@ func TestFacilityDebugging(t *testing.T) {
|
||||
f1.Debugln("Debug line from f1")
|
||||
|
||||
if msgs != 1 {
|
||||
t.Fatalf("Incorrent number of messages, %d != 1", msgs)
|
||||
t.Fatalf("Incorrect number of messages, %d != 1", msgs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+107
-61
@@ -33,7 +33,6 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/symlinks"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/versioner"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
@@ -64,7 +63,7 @@ type Model struct {
|
||||
*suture.Supervisor
|
||||
|
||||
cfg *config.Wrapper
|
||||
db *leveldb.DB
|
||||
db *db.Instance
|
||||
finder *db.BlockFinder
|
||||
progressEmitter *ProgressEmitter
|
||||
id protocol.DeviceID
|
||||
@@ -76,15 +75,16 @@ type Model struct {
|
||||
clientName string
|
||||
clientVersion string
|
||||
|
||||
folderCfgs map[string]config.FolderConfiguration // folder -> cfg
|
||||
folderFiles map[string]*db.FileSet // folder -> files
|
||||
folderDevices map[string][]protocol.DeviceID // folder -> deviceIDs
|
||||
deviceFolders map[protocol.DeviceID][]string // deviceID -> folders
|
||||
deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
|
||||
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
|
||||
folderRunners map[string]service // folder -> puller or scanner
|
||||
folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef
|
||||
fmut sync.RWMutex // protects the above
|
||||
folderCfgs map[string]config.FolderConfiguration // folder -> cfg
|
||||
folderFiles map[string]*db.FileSet // folder -> files
|
||||
folderDevices map[string][]protocol.DeviceID // folder -> deviceIDs
|
||||
deviceFolders map[protocol.DeviceID][]string // deviceID -> folders
|
||||
deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
|
||||
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
|
||||
folderRunners map[string]service // folder -> puller or scanner
|
||||
folderRunnerTokens map[string][]suture.ServiceToken // folder -> tokens for puller or scanner
|
||||
folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef
|
||||
fmut sync.RWMutex // protects the above
|
||||
|
||||
conn map[protocol.DeviceID]Connection
|
||||
deviceVer map[protocol.DeviceID]string
|
||||
@@ -99,35 +99,36 @@ var (
|
||||
// NewModel creates and starts a new model. The model starts in read-only mode,
|
||||
// where it sends index information to connected peers and responds to requests
|
||||
// for file data without altering the local folder in any way.
|
||||
func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName, clientVersion string, ldb *leveldb.DB, protectedFiles []string) *Model {
|
||||
func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName, clientVersion string, ldb *db.Instance, protectedFiles []string) *Model {
|
||||
m := &Model{
|
||||
Supervisor: suture.New("model", suture.Spec{
|
||||
Log: func(line string) {
|
||||
l.Debugln(line)
|
||||
},
|
||||
}),
|
||||
cfg: cfg,
|
||||
db: ldb,
|
||||
finder: db.NewBlockFinder(ldb),
|
||||
progressEmitter: NewProgressEmitter(cfg),
|
||||
id: id,
|
||||
shortID: id.Short(),
|
||||
cacheIgnoredFiles: cfg.Options().CacheIgnoredFiles,
|
||||
protectedFiles: protectedFiles,
|
||||
deviceName: deviceName,
|
||||
clientName: clientName,
|
||||
clientVersion: clientVersion,
|
||||
folderCfgs: make(map[string]config.FolderConfiguration),
|
||||
folderFiles: make(map[string]*db.FileSet),
|
||||
folderDevices: make(map[string][]protocol.DeviceID),
|
||||
deviceFolders: make(map[protocol.DeviceID][]string),
|
||||
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
|
||||
folderIgnores: make(map[string]*ignore.Matcher),
|
||||
folderRunners: make(map[string]service),
|
||||
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
||||
conn: make(map[protocol.DeviceID]Connection),
|
||||
deviceVer: make(map[protocol.DeviceID]string),
|
||||
devicePaused: make(map[protocol.DeviceID]bool),
|
||||
cfg: cfg,
|
||||
db: ldb,
|
||||
finder: db.NewBlockFinder(ldb),
|
||||
progressEmitter: NewProgressEmitter(cfg),
|
||||
id: id,
|
||||
shortID: id.Short(),
|
||||
cacheIgnoredFiles: cfg.Options().CacheIgnoredFiles,
|
||||
protectedFiles: protectedFiles,
|
||||
deviceName: deviceName,
|
||||
clientName: clientName,
|
||||
clientVersion: clientVersion,
|
||||
folderCfgs: make(map[string]config.FolderConfiguration),
|
||||
folderFiles: make(map[string]*db.FileSet),
|
||||
folderDevices: make(map[string][]protocol.DeviceID),
|
||||
deviceFolders: make(map[protocol.DeviceID][]string),
|
||||
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
|
||||
folderIgnores: make(map[string]*ignore.Matcher),
|
||||
folderRunners: make(map[string]service),
|
||||
folderRunnerTokens: make(map[string][]suture.ServiceToken),
|
||||
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
||||
conn: make(map[protocol.DeviceID]Connection),
|
||||
deviceVer: make(map[protocol.DeviceID]string),
|
||||
devicePaused: make(map[protocol.DeviceID]bool),
|
||||
|
||||
fmut: sync.NewRWMutex(),
|
||||
pmut: sync.NewRWMutex(),
|
||||
@@ -164,7 +165,6 @@ func (m *Model) StartFolderRW(folder string) {
|
||||
}
|
||||
p := newRWFolder(m, m.shortID, cfg)
|
||||
m.folderRunners[folder] = p
|
||||
m.fmut.Unlock()
|
||||
|
||||
if len(cfg.Versioning.Type) > 0 {
|
||||
factory, ok := versioner.Factories[cfg.Versioning.Type]
|
||||
@@ -177,14 +177,17 @@ func (m *Model) StartFolderRW(folder string) {
|
||||
// The versioner implements the suture.Service interface, so
|
||||
// expects to be run in the background in addition to being called
|
||||
// when files are going to be archived.
|
||||
m.Add(service)
|
||||
token := m.Add(service)
|
||||
m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
|
||||
}
|
||||
p.versioner = versioner
|
||||
}
|
||||
|
||||
m.warnAboutOverwritingProtectedFiles(folder)
|
||||
|
||||
m.Add(p)
|
||||
token := m.Add(p)
|
||||
m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
|
||||
m.fmut.Unlock()
|
||||
|
||||
l.Okln("Ready to synchronize", folder, "(read-write)")
|
||||
}
|
||||
@@ -233,13 +236,49 @@ func (m *Model) StartFolderRO(folder string) {
|
||||
}
|
||||
s := newROFolder(m, folder, time.Duration(cfg.RescanIntervalS)*time.Second)
|
||||
m.folderRunners[folder] = s
|
||||
|
||||
token := m.Add(s)
|
||||
m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
|
||||
m.fmut.Unlock()
|
||||
|
||||
m.Add(s)
|
||||
|
||||
l.Okln("Ready to synchronize", folder, "(read only; no external updates accepted)")
|
||||
}
|
||||
|
||||
func (m *Model) RemoveFolder(folder string) {
|
||||
m.fmut.Lock()
|
||||
m.pmut.Lock()
|
||||
|
||||
// Stop the services running for this folder
|
||||
for _, id := range m.folderRunnerTokens[folder] {
|
||||
m.Remove(id)
|
||||
}
|
||||
|
||||
// Close connections to affected devices
|
||||
for _, dev := range m.folderDevices[folder] {
|
||||
if conn, ok := m.conn[dev]; ok {
|
||||
closeRawConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up our config maps
|
||||
delete(m.folderCfgs, folder)
|
||||
delete(m.folderFiles, folder)
|
||||
delete(m.folderDevices, folder)
|
||||
delete(m.folderIgnores, folder)
|
||||
delete(m.folderRunners, folder)
|
||||
delete(m.folderRunnerTokens, folder)
|
||||
delete(m.folderStatRefs, folder)
|
||||
for dev, folders := range m.deviceFolders {
|
||||
m.deviceFolders[dev] = stringSliceWithout(folders, folder)
|
||||
}
|
||||
|
||||
// Remove it from the database
|
||||
db.DropFolder(m.db, folder)
|
||||
|
||||
m.pmut.Unlock()
|
||||
m.fmut.Unlock()
|
||||
}
|
||||
|
||||
type ConnectionInfo struct {
|
||||
protocol.Statistics
|
||||
Connected bool
|
||||
@@ -262,7 +301,7 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
// ConnectionStats returns a map with connection statistics for each connected device.
|
||||
// ConnectionStats returns a map with connection statistics for each device.
|
||||
func (m *Model) ConnectionStats() map[string]interface{} {
|
||||
type remoteAddrer interface {
|
||||
RemoteAddr() net.Addr
|
||||
@@ -354,16 +393,6 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
return completionPct
|
||||
}
|
||||
|
||||
func sizeOf(fs []protocol.FileInfo) (files, deleted int, bytes int64) {
|
||||
for _, f := range fs {
|
||||
fs, de, by := sizeOfFile(f)
|
||||
files += fs
|
||||
deleted += de
|
||||
bytes += by
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func sizeOfFile(f db.FileIntf) (files, deleted int, bytes int64) {
|
||||
if !f.IsDeleted() {
|
||||
files++
|
||||
@@ -602,7 +631,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
|
||||
events.Default.Log(events.DeviceConnected, event)
|
||||
|
||||
l.Infof(`Device %s client is "%s %s named %s"`, deviceID, cm.ClientName, cm.ClientVersion, cm.DeviceName)
|
||||
l.Infof(`Device %s client is "%s %s" named "%s"`, deviceID, cm.ClientName, cm.ClientVersion, cm.DeviceName)
|
||||
|
||||
var changed bool
|
||||
|
||||
@@ -1025,7 +1054,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
|
||||
minLocalVer, err = sendIndexTo(false, minLocalVer, conn, folder, fs, ignores)
|
||||
|
||||
// Wait a short amount of time before entering the next loop. If there
|
||||
// are continous changes happening to the local index, this gives us
|
||||
// are continuous changes happening to the local index, this gives us
|
||||
// time to batch them up a little.
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
@@ -1261,6 +1290,11 @@ nextSub:
|
||||
}
|
||||
subs = unifySubs
|
||||
|
||||
// The cancel channel is closed whenever we return (such as from an error),
|
||||
// to signal the potentially still running walker to stop.
|
||||
cancel := make(chan struct{})
|
||||
defer close(cancel)
|
||||
|
||||
w := &scanner.Walker{
|
||||
Folder: folderCfg.ID,
|
||||
Dir: folderCfg.Path(),
|
||||
@@ -1276,6 +1310,7 @@ nextSub:
|
||||
Hashers: m.numHashers(folder),
|
||||
ShortID: m.shortID,
|
||||
ProgressTickIntervalS: folderCfg.ScanProgressIntervalS,
|
||||
Cancel: cancel,
|
||||
}
|
||||
|
||||
runner.setState(FolderScanning)
|
||||
@@ -1685,17 +1720,17 @@ func (m *Model) BringToFront(folder, file string) {
|
||||
// CheckFolderHealth checks the folder for common errors and returns the
|
||||
// current folder error, or nil if the folder is healthy.
|
||||
func (m *Model) CheckFolderHealth(id string) error {
|
||||
folder, ok := m.cfg.Folders()[id]
|
||||
if !ok {
|
||||
return errors.New("folder does not exist")
|
||||
}
|
||||
|
||||
if minFree := m.cfg.Options().MinHomeDiskFreePct; minFree > 0 {
|
||||
if free, err := osutil.DiskFreePercentage(m.cfg.ConfigPath()); err == nil && free < minFree {
|
||||
return errors.New("home disk has insufficient free space")
|
||||
}
|
||||
}
|
||||
|
||||
folder, ok := m.cfg.Folders()[id]
|
||||
if !ok {
|
||||
return errors.New("folder does not exist")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(folder.Path())
|
||||
|
||||
v, ok := m.CurrentLocalVersion(id)
|
||||
@@ -1808,9 +1843,9 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
||||
for folderID, fromCfg := range fromFolders {
|
||||
toCfg, ok := toFolders[folderID]
|
||||
if !ok {
|
||||
// A folder was removed. Requires restart.
|
||||
l.Debugln(m, "requires restart, removing folder", folderID)
|
||||
return false
|
||||
// The folder was removed.
|
||||
m.RemoveFolder(folderID)
|
||||
continue
|
||||
}
|
||||
|
||||
// This folder exists on both sides. Compare the device lists, as we
|
||||
@@ -1859,7 +1894,7 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Removing a device requres restart
|
||||
// Removing a device requires restart
|
||||
toDevs := mapDeviceCfgs(from.Devices)
|
||||
for _, dev := range from.Devices {
|
||||
if _, ok := toDevs[dev.DeviceID]; !ok {
|
||||
@@ -1972,3 +2007,14 @@ func closeRawConn(conn io.Closer) error {
|
||||
}
|
||||
return conn.Close()
|
||||
}
|
||||
|
||||
func stringSliceWithout(ss []string, s string) []string {
|
||||
for i := range ss {
|
||||
if ss[i] == s {
|
||||
copy(ss[i:], ss[i+1:])
|
||||
ss = ss[:len(ss)-1]
|
||||
return ss
|
||||
}
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
+17
-30
@@ -22,8 +22,6 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
var device1, device2 protocol.DeviceID
|
||||
@@ -34,22 +32,11 @@ func init() {
|
||||
device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
|
||||
device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
|
||||
|
||||
defaultFolderConfig = config.FolderConfiguration{
|
||||
ID: "default",
|
||||
RawPath: "testdata",
|
||||
Devices: []config.FolderDeviceConfiguration{
|
||||
{
|
||||
DeviceID: device1,
|
||||
},
|
||||
},
|
||||
}
|
||||
defaultFolderConfig = config.NewFolderConfiguration("default", "testdata")
|
||||
defaultFolderConfig.Devices = []config.FolderDeviceConfiguration{{DeviceID: device1}}
|
||||
_defaultConfig := config.Configuration{
|
||||
Folders: []config.FolderConfiguration{defaultFolderConfig},
|
||||
Devices: []config.DeviceConfiguration{
|
||||
{
|
||||
DeviceID: device1,
|
||||
},
|
||||
},
|
||||
Devices: []config.DeviceConfiguration{config.NewDeviceConfiguration(device1, "device1")},
|
||||
Options: config.OptionsConfiguration{
|
||||
// Don't remove temporaries directly on startup
|
||||
KeepTemporariesH: 1,
|
||||
@@ -90,7 +77,7 @@ func init() {
|
||||
}
|
||||
|
||||
func TestRequest(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
|
||||
@@ -167,7 +154,7 @@ func BenchmarkIndex_100(b *testing.B) {
|
||||
}
|
||||
|
||||
func benchmarkIndex(b *testing.B, nfiles int) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolderRO("default")
|
||||
@@ -196,7 +183,7 @@ func BenchmarkIndexUpdate_10000_1(b *testing.B) {
|
||||
}
|
||||
|
||||
func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolderRO("default")
|
||||
@@ -261,7 +248,7 @@ func (FakeConnection) Statistics() protocol.Statistics {
|
||||
}
|
||||
|
||||
func BenchmarkRequest(b *testing.B) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
@@ -317,7 +304,7 @@ func TestDeviceRename(t *testing.T) {
|
||||
}
|
||||
cfg := config.Wrap("tmpconfig.xml", rawCfg)
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
|
||||
fc := FakeConnection{
|
||||
@@ -391,7 +378,7 @@ func TestClusterConfig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
|
||||
m := NewModel(config.Wrap("/tmp/test", cfg), protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(cfg.Folders[0])
|
||||
@@ -463,7 +450,7 @@ func TestIgnores(t *testing.T) {
|
||||
ioutil.WriteFile("testdata/.stfolder", nil, 0644)
|
||||
ioutil.WriteFile("testdata/.stignore", []byte(".*\nquux\n"), 0644)
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolderRO("default")
|
||||
@@ -538,7 +525,7 @@ func TestIgnores(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRefuseUnknownBits(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
@@ -576,7 +563,7 @@ func TestRefuseUnknownBits(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestROScanRecovery(t *testing.T) {
|
||||
ldb, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
ldb := db.OpenMemory()
|
||||
set := db.NewFileSet("default", ldb)
|
||||
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
|
||||
{Name: "dummyfile"},
|
||||
@@ -660,7 +647,7 @@ func TestROScanRecovery(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRWScanRecovery(t *testing.T) {
|
||||
ldb, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
ldb := db.OpenMemory()
|
||||
set := db.NewFileSet("default", ldb)
|
||||
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
|
||||
{Name: "dummyfile"},
|
||||
@@ -744,7 +731,7 @@ func TestRWScanRecovery(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGlobalDirectoryTree(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
@@ -994,7 +981,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGlobalDirectorySelfFixing(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
@@ -1168,7 +1155,7 @@ func BenchmarkTree_100_10(b *testing.B) {
|
||||
}
|
||||
|
||||
func benchmarkTree(b *testing.B, n1, n2 int) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
@@ -1186,7 +1173,7 @@ func benchmarkTree(b *testing.B, n1, n2 int) {
|
||||
}
|
||||
|
||||
func TestIgnoreDelete(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
|
||||
// This folder should ignore external deletes
|
||||
|
||||
+29
-17
@@ -49,7 +49,7 @@ const retainBits = os.ModeSetgid | os.ModeSetuid | os.ModeSticky
|
||||
|
||||
var (
|
||||
activity = newDeviceActivity()
|
||||
errNoDevice = errors.New("no available source device")
|
||||
errNoDevice = errors.New("peers who had this file went away, or the file has changed while syncing. will retry later")
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -896,9 +896,9 @@ func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
// handleFile queues the copies and pulls as necessary for a single new or
|
||||
// changed file.
|
||||
func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState) {
|
||||
curFile, ok := p.model.CurrentFolderFile(p.folder, file.Name)
|
||||
curFile, hasCurFile := p.model.CurrentFolderFile(p.folder, file.Name)
|
||||
|
||||
if ok && len(curFile.Blocks) == len(file.Blocks) && scanner.BlocksEqual(curFile.Blocks, file.Blocks) {
|
||||
if hasCurFile && len(curFile.Blocks) == len(file.Blocks) && scanner.BlocksEqual(curFile.Blocks, file.Blocks) {
|
||||
// We are supposed to copy the entire file, and then fetch nothing. We
|
||||
// are only updating metadata, so we don't actually *need* to make the
|
||||
// copy.
|
||||
@@ -938,6 +938,32 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
return
|
||||
}
|
||||
|
||||
// Figure out the absolute filenames we need once and for all
|
||||
tempName := filepath.Join(p.dir, defTempNamer.TempName(file.Name))
|
||||
realName := filepath.Join(p.dir, file.Name)
|
||||
|
||||
if hasCurFile && !curFile.IsDirectory() && !curFile.IsSymlink() {
|
||||
// Check that the file on disk is what we expect it to be according to
|
||||
// the database. If there's a mismatch here, there might be local
|
||||
// changes that we don't know about yet and we should scan before
|
||||
// touching the file. If we can't stat the file we'll just pull it.
|
||||
if info, err := osutil.Lstat(realName); err == nil {
|
||||
mtime := p.virtualMtimeRepo.GetMtime(file.Name, info.ModTime())
|
||||
if mtime.Unix() != curFile.Modified || info.Size() != curFile.Size() {
|
||||
l.Debugln("file modified but not rescanned; not pulling:", realName)
|
||||
// Scan() is synchronous (i.e. blocks until the scan is
|
||||
// completed and returns an error), but a scan can't happen
|
||||
// while we're in the puller routine. Request the scan in the
|
||||
// background and it'll be handled when the current pulling
|
||||
// sweep is complete. As we do retries, we'll queue the scan
|
||||
// for this file up to ten times, but the last nine of those
|
||||
// scans will be cheap...
|
||||
go p.Scan([]string{file.Name})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if free, err := osutil.DiskFreeBytes(p.dir); err == nil && free < file.Size() {
|
||||
l.Warnf(`Folder "%s": insufficient disk space in %s for %s: have %.2f MiB, need %.2f MiB`, p.folder, p.dir, file.Name, float64(free)/1024/1024, float64(file.Size())/1024/1024)
|
||||
p.newError(file.Name, errors.New("insufficient space"))
|
||||
@@ -953,10 +979,6 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
|
||||
scanner.PopulateOffsets(file.Blocks)
|
||||
|
||||
// Figure out the absolute filenames we need once and for all
|
||||
tempName := filepath.Join(p.dir, defTempNamer.TempName(file.Name))
|
||||
realName := filepath.Join(p.dir, file.Name)
|
||||
|
||||
reused := 0
|
||||
var blocks []protocol.BlockInfo
|
||||
|
||||
@@ -1429,16 +1451,6 @@ func (p *rwFolder) inConflict(current, replacement protocol.Vector) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func invalidateFolder(cfg *config.Configuration, folderID string, err error) {
|
||||
for i := range cfg.Folders {
|
||||
folder := &cfg.Folders[i]
|
||||
if folder.ID == folderID {
|
||||
folder.Invalid = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeDevice(devices []protocol.DeviceID, device protocol.DeviceID) []protocol.DeviceID {
|
||||
for i := range devices {
|
||||
if devices[i] == device {
|
||||
|
||||
@@ -12,12 +12,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/scanner"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -69,7 +67,7 @@ func TestHandleFile(t *testing.T) {
|
||||
requiredFile := existingFile
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
@@ -125,7 +123,7 @@ func TestHandleFileWithTemp(t *testing.T) {
|
||||
requiredFile := existingFile
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
@@ -187,7 +185,7 @@ func TestCopierFinder(t *testing.T) {
|
||||
requiredFile.Blocks = blocks[1:]
|
||||
requiredFile.Name = "file2"
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
// Update index
|
||||
@@ -264,7 +262,7 @@ func TestCopierCleanup(t *testing.T) {
|
||||
return true
|
||||
}
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
@@ -313,7 +311,7 @@ func TestCopierCleanup(t *testing.T) {
|
||||
// Make sure that the copier routine hashes the content when asked, and pulls
|
||||
// if it fails to find the block.
|
||||
func TestLastResortPulling(t *testing.T) {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
@@ -387,7 +385,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
}
|
||||
defer os.Remove("testdata/" + defTempNamer.TempName("filex"))
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
@@ -480,7 +478,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
}
|
||||
defer os.Remove("testdata/" + defTempNamer.TempName("filex"))
|
||||
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
@@ -516,7 +514,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
|
||||
p.handleFile(file, copyChan, finisherChan)
|
||||
|
||||
// Receove at finisher, we shoud error out as puller has nowhere to pull
|
||||
// Receive at finisher, we should error out as puller has nowhere to pull
|
||||
// from.
|
||||
select {
|
||||
case state := <-finisherBufferChan:
|
||||
|
||||
@@ -16,3 +16,7 @@ var (
|
||||
func init() {
|
||||
l.SetDebug("protocol", strings.Contains(os.Getenv("STTRACE"), "protocol") || os.Getenv("STTRACE") == "all")
|
||||
}
|
||||
|
||||
func shouldDebug() bool {
|
||||
return l.ShouldDebug("protocol")
|
||||
}
|
||||
|
||||
@@ -411,10 +411,12 @@ func (c *rawConnection) readMessage() (hdr header, msg encodable, err error) {
|
||||
l.Debugf("decompressed to %d bytes", len(msgBuf))
|
||||
}
|
||||
|
||||
if len(msgBuf) > 1024 {
|
||||
l.Debugf("message data:\n%s", hex.Dump(msgBuf[:1024]))
|
||||
} else {
|
||||
l.Debugf("message data:\n%s", hex.Dump(msgBuf))
|
||||
if shouldDebug() {
|
||||
if len(msgBuf) > 1024 {
|
||||
l.Debugf("message data:\n%s", hex.Dump(msgBuf[:1024]))
|
||||
} else {
|
||||
l.Debugf("message data:\n%s", hex.Dump(msgBuf))
|
||||
}
|
||||
}
|
||||
|
||||
// We check each returned error for the XDRError.IsEOF() method.
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
// We set the API key via the STGUIAPIKEY variable when we launch the binary,
|
||||
// to ensure that we have API access regardless of authentication settings.
|
||||
// APIKey is set via the STGUIAPIKEY variable when we launch the binary, to
|
||||
// ensure that we have API access regardless of authentication settings.
|
||||
const APIKey = "592A47BC-A7DF-4C2F-89E0-A80B3E5094EE"
|
||||
|
||||
type Process struct {
|
||||
|
||||
@@ -109,8 +109,8 @@ func TestRelay(uri *url.URL, certs []tls.Certificate, sleep time.Duration, times
|
||||
}
|
||||
go c.Serve()
|
||||
defer func() {
|
||||
close(invs)
|
||||
c.Stop()
|
||||
close(invs)
|
||||
}()
|
||||
|
||||
for i := 0; i < times; i++ {
|
||||
|
||||
+2
-1
@@ -170,12 +170,13 @@ func (s *Svc) RelayStatus(uri string) (time.Duration, bool) {
|
||||
}
|
||||
|
||||
s.mut.RLock()
|
||||
defer s.mut.RUnlock()
|
||||
|
||||
for _, client := range s.clients {
|
||||
if client.URI().String() == uri {
|
||||
return client.Latency(), client.StatusOK()
|
||||
}
|
||||
}
|
||||
s.mut.RUnlock()
|
||||
|
||||
return time.Hour, false
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
_random.data
|
||||
+28
-14
@@ -19,13 +19,13 @@ import (
|
||||
// workers are used in parallel. The outbox will become closed when the inbox
|
||||
// is closed and all items handled.
|
||||
|
||||
func newParallelHasher(dir string, blockSize, workers int, outbox, inbox chan protocol.FileInfo, counter *int64, done chan struct{}) {
|
||||
func newParallelHasher(dir string, blockSize, workers int, outbox, inbox chan protocol.FileInfo, counter *int64, done, cancel chan struct{}) {
|
||||
wg := sync.NewWaitGroup()
|
||||
wg.Add(workers)
|
||||
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
hashFiles(dir, blockSize, outbox, inbox, counter)
|
||||
hashFiles(dir, blockSize, outbox, inbox, counter, cancel)
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
@@ -59,19 +59,33 @@ func HashFile(path string, blockSize int, sizeHint int64, counter *int64) ([]pro
|
||||
return Blocks(fd, blockSize, sizeHint, counter)
|
||||
}
|
||||
|
||||
func hashFiles(dir string, blockSize int, outbox, inbox chan protocol.FileInfo, counter *int64) {
|
||||
for f := range inbox {
|
||||
if f.IsDirectory() || f.IsDeleted() {
|
||||
panic("Bug. Asked to hash a directory or a deleted file.")
|
||||
}
|
||||
func hashFiles(dir string, blockSize int, outbox, inbox chan protocol.FileInfo, counter *int64, cancel chan struct{}) {
|
||||
for {
|
||||
select {
|
||||
case f, ok := <-inbox:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
blocks, err := HashFile(filepath.Join(dir, f.Name), blockSize, f.CachedSize, counter)
|
||||
if err != nil {
|
||||
l.Debugln("hash error:", f.Name, err)
|
||||
continue
|
||||
}
|
||||
if f.IsDirectory() || f.IsDeleted() {
|
||||
panic("Bug. Asked to hash a directory or a deleted file.")
|
||||
}
|
||||
|
||||
f.Blocks = blocks
|
||||
outbox <- f
|
||||
blocks, err := HashFile(filepath.Join(dir, f.Name), blockSize, f.CachedSize, counter)
|
||||
if err != nil {
|
||||
l.Debugln("hash error:", f.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
f.Blocks = blocks
|
||||
select {
|
||||
case outbox <- f:
|
||||
case <-cancel:
|
||||
return
|
||||
}
|
||||
|
||||
case <-cancel:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+71
-8
@@ -20,15 +20,27 @@ var SHA256OfNothing = []uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x
|
||||
|
||||
// Blocks returns the blockwise hash of the reader.
|
||||
func Blocks(r io.Reader, blocksize int, sizehint int64, counter *int64) ([]protocol.BlockInfo, error) {
|
||||
var blocks []protocol.BlockInfo
|
||||
if sizehint > 0 {
|
||||
blocks = make([]protocol.BlockInfo, 0, int(sizehint/int64(blocksize)))
|
||||
}
|
||||
var offset int64
|
||||
hf := sha256.New()
|
||||
hashLength := hf.Size()
|
||||
|
||||
var blocks []protocol.BlockInfo
|
||||
var hashes, thisHash []byte
|
||||
|
||||
if sizehint > 0 {
|
||||
// Allocate contiguous blocks for the BlockInfo structures and their
|
||||
// hashes once and for all.
|
||||
numBlocks := int(sizehint / int64(blocksize))
|
||||
blocks = make([]protocol.BlockInfo, 0, numBlocks)
|
||||
hashes = make([]byte, 0, hashLength*numBlocks)
|
||||
}
|
||||
|
||||
// A 32k buffer is used for copying into the hash function.
|
||||
buf := make([]byte, 32<<10)
|
||||
|
||||
var offset int64
|
||||
for {
|
||||
lr := &io.LimitedReader{R: r, N: int64(blocksize)}
|
||||
n, err := io.Copy(hf, lr)
|
||||
lr := io.LimitReader(r, int64(blocksize))
|
||||
n, err := copyBuffer(hf, lr, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -41,11 +53,17 @@ func Blocks(r io.Reader, blocksize int, sizehint int64, counter *int64) ([]proto
|
||||
atomic.AddInt64(counter, int64(n))
|
||||
}
|
||||
|
||||
// Carve out a hash-sized chunk of "hashes" to store the hash for this
|
||||
// block.
|
||||
hashes = hf.Sum(hashes)
|
||||
thisHash, hashes = hashes[:hashLength], hashes[hashLength:]
|
||||
|
||||
b := protocol.BlockInfo{
|
||||
Size: int32(n),
|
||||
Offset: offset,
|
||||
Hash: hf.Sum(nil),
|
||||
Hash: thisHash,
|
||||
}
|
||||
|
||||
blocks = append(blocks, b)
|
||||
offset += int64(n)
|
||||
|
||||
@@ -158,3 +176,48 @@ func BlocksEqual(src, tgt []protocol.BlockInfo) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// This is a copy & paste of io.copyBuffer from the Go 1.5 standard library,
|
||||
// as we want this but also want to build with Go 1.3+.
|
||||
|
||||
// copyBuffer is the actual implementation of Copy and CopyBuffer.
|
||||
// if buf is nil, one is allocated.
|
||||
func copyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) {
|
||||
// If the reader has a WriteTo method, use it to do the copy.
|
||||
// Avoids an allocation and a copy.
|
||||
if wt, ok := src.(io.WriterTo); ok {
|
||||
return wt.WriteTo(dst)
|
||||
}
|
||||
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
|
||||
if rt, ok := dst.(io.ReaderFrom); ok {
|
||||
return rt.ReadFrom(src)
|
||||
}
|
||||
if buf == nil {
|
||||
buf = make([]byte, 32*1024)
|
||||
}
|
||||
for {
|
||||
nr, er := src.Read(buf)
|
||||
if nr > 0 {
|
||||
nw, ew := dst.Write(buf[0:nr])
|
||||
if nw > 0 {
|
||||
written += int64(nw)
|
||||
}
|
||||
if ew != nil {
|
||||
err = ew
|
||||
break
|
||||
}
|
||||
if nr != nw {
|
||||
err = io.ErrShortWrite
|
||||
break
|
||||
}
|
||||
}
|
||||
if er == io.EOF {
|
||||
break
|
||||
}
|
||||
if er != nil {
|
||||
err = er
|
||||
break
|
||||
}
|
||||
}
|
||||
return written, err
|
||||
}
|
||||
|
||||
+31
-6
@@ -72,6 +72,8 @@ type Walker struct {
|
||||
// Optional progress tick interval which defines how often FolderScanProgress
|
||||
// events are emitted. Negative number means disabled.
|
||||
ProgressTickIntervalS int
|
||||
// Signals cancel from the outside - when closed, we should stop walking.
|
||||
Cancel chan struct{}
|
||||
}
|
||||
|
||||
type TempNamer interface {
|
||||
@@ -121,7 +123,7 @@ func (w *Walker) Walk() (chan protocol.FileInfo, error) {
|
||||
// We're not required to emit scan progress events, just kick off hashers,
|
||||
// and feed inputs directly from the walker.
|
||||
if w.ProgressTickIntervalS < 0 {
|
||||
newParallelHasher(w.Dir, w.BlockSize, w.Hashers, finishedChan, toHashChan, nil, nil)
|
||||
newParallelHasher(w.Dir, w.BlockSize, w.Hashers, finishedChan, toHashChan, nil, nil, w.Cancel)
|
||||
return finishedChan, nil
|
||||
}
|
||||
|
||||
@@ -149,7 +151,7 @@ func (w *Walker) Walk() (chan protocol.FileInfo, error) {
|
||||
|
||||
realToHashChan := make(chan protocol.FileInfo)
|
||||
done := make(chan struct{})
|
||||
newParallelHasher(w.Dir, w.BlockSize, w.Hashers, finishedChan, realToHashChan, &progress, done)
|
||||
newParallelHasher(w.Dir, w.BlockSize, w.Hashers, finishedChan, realToHashChan, &progress, done, w.Cancel)
|
||||
|
||||
// A routine which actually emits the FolderScanProgress events
|
||||
// every w.ProgressTicker ticks, until the hasher routines terminate.
|
||||
@@ -168,13 +170,21 @@ func (w *Walker) Walk() (chan protocol.FileInfo, error) {
|
||||
"current": current,
|
||||
"total": total,
|
||||
})
|
||||
case <-w.Cancel:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
loop:
|
||||
for _, file := range filesToHash {
|
||||
l.Debugln("real to hash:", file.Name)
|
||||
realToHashChan <- file
|
||||
select {
|
||||
case realToHashChan <- file:
|
||||
case <-w.Cancel:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
close(realToHashChan)
|
||||
}()
|
||||
@@ -329,7 +339,11 @@ func (w *Walker) walkAndHashFiles(fchan, dchan chan protocol.FileInfo) filepath.
|
||||
|
||||
l.Debugln("symlink changedb:", p, f)
|
||||
|
||||
dchan <- f
|
||||
select {
|
||||
case dchan <- f:
|
||||
case <-w.Cancel:
|
||||
return errors.New("cancelled")
|
||||
}
|
||||
|
||||
return skip
|
||||
}
|
||||
@@ -363,7 +377,13 @@ func (w *Walker) walkAndHashFiles(fchan, dchan chan protocol.FileInfo) filepath.
|
||||
Modified: mtime.Unix(),
|
||||
}
|
||||
l.Debugln("dir:", p, f)
|
||||
dchan <- f
|
||||
|
||||
select {
|
||||
case dchan <- f:
|
||||
case <-w.Cancel:
|
||||
return errors.New("cancelled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -406,7 +426,12 @@ func (w *Walker) walkAndHashFiles(fchan, dchan chan protocol.FileInfo) filepath.
|
||||
CachedSize: info.Size(),
|
||||
}
|
||||
l.Debugln("to hash:", p, f)
|
||||
fchan <- f
|
||||
|
||||
select {
|
||||
case fchan <- f:
|
||||
case <-w.Cancel:
|
||||
return errors.New("cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -8,13 +8,16 @@ package scanner
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
rdebug "runtime/debug"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/ignore"
|
||||
@@ -372,3 +375,39 @@ func TestSymlinkTypeEqual(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var initOnce sync.Once
|
||||
|
||||
const (
|
||||
testdataSize = 17 << 20
|
||||
testdataName = "_random.data"
|
||||
)
|
||||
|
||||
func BenchmarkHashFile(b *testing.B) {
|
||||
initOnce.Do(initTestFile)
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := HashFile(testdataName, protocol.BlockSize, testdataSize, nil); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
func initTestFile() {
|
||||
fd, err := os.Create(testdataName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
lr := io.LimitReader(rand.Reader, testdataSize)
|
||||
if _, err := io.Copy(fd, lr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := fd.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func GenerateKeys() (privKey []byte, pubKey []byte, err error) {
|
||||
})
|
||||
|
||||
// Marshal the public key
|
||||
bs, err = x509.MarshalPKIXPublicKey(key.Public())
|
||||
bs, err = x509.MarshalPKIXPublicKey(&key.PublicKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
+1
-2
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type DeviceStatistics struct {
|
||||
@@ -22,7 +21,7 @@ type DeviceStatisticsReference struct {
|
||||
device string
|
||||
}
|
||||
|
||||
func NewDeviceStatisticsReference(ldb *leveldb.DB, device string) *DeviceStatisticsReference {
|
||||
func NewDeviceStatisticsReference(ldb *db.Instance, device string) *DeviceStatisticsReference {
|
||||
prefix := string(db.KeyTypeDeviceStatistic) + device
|
||||
return &DeviceStatisticsReference{
|
||||
ns: db.NewNamespacedKV(ldb, prefix),
|
||||
|
||||
+1
-2
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type FolderStatistics struct {
|
||||
@@ -28,7 +27,7 @@ type LastFile struct {
|
||||
Deleted bool `json:"deleted"`
|
||||
}
|
||||
|
||||
func NewFolderStatisticsReference(ldb *leveldb.DB, folder string) *FolderStatisticsReference {
|
||||
func NewFolderStatisticsReference(ldb *db.Instance, folder string) *FolderStatisticsReference {
|
||||
prefix := string(db.KeyTypeFolderStatistic) + folder
|
||||
return &FolderStatisticsReference{
|
||||
ns: db.NewNamespacedKV(ldb, prefix),
|
||||
|
||||
@@ -33,7 +33,3 @@ func init() {
|
||||
}
|
||||
l.Debugf("Enabling lock logging at %v threshold", threshold)
|
||||
}
|
||||
|
||||
func shouldDebug() bool {
|
||||
return l.ShouldDebug("sync")
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
package upgrade
|
||||
|
||||
// This is the public key used to verify signed upgrades. It must match the
|
||||
// private key used to sign binaries for the built in upgrade mechanism to
|
||||
// SigningKey is the public key used to verify signed upgrades. It must match
|
||||
// the private key used to sign binaries for the built in upgrade mechanism to
|
||||
// accept an upgrade. Keys and signatures can be created and verified with the
|
||||
// stsigtool utility. The build script creates signed binaries when given the
|
||||
// -sign option.
|
||||
|
||||
@@ -89,7 +89,7 @@ func SelectLatestRelease(version string, rels []Release) (Release, error) {
|
||||
|
||||
sort.Sort(SortByRelease(rels))
|
||||
// Check for a beta build
|
||||
beta := strings.Contains(version, "-beta")
|
||||
beta := strings.Contains(version, "-")
|
||||
|
||||
for _, rel := range rels {
|
||||
if rel.Prerelease && !beta {
|
||||
@@ -190,11 +190,7 @@ func readTarGz(dir string, r io.Reader) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
shortName := path.Base(hdr.Name)
|
||||
|
||||
l.Debugf("considering file %q", shortName)
|
||||
|
||||
err = archiveFileVisitor(dir, &tempName, &sig, shortName, tr)
|
||||
err = archiveFileVisitor(dir, &tempName, &sig, hdr.Name, tr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -227,16 +223,12 @@ func readZip(dir string, r io.Reader) (string, error) {
|
||||
|
||||
// Iterate through the files in the archive.
|
||||
for _, file := range archive.File {
|
||||
shortName := path.Base(file.Name)
|
||||
|
||||
l.Debugf("considering file %q", shortName)
|
||||
|
||||
inFile, err := file.Open()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = archiveFileVisitor(dir, &tempName, &sig, shortName, inFile)
|
||||
err = archiveFileVisitor(dir, &tempName, &sig, file.Name, inFile)
|
||||
inFile.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -256,18 +248,26 @@ func readZip(dir string, r io.Reader) (string, error) {
|
||||
|
||||
// archiveFileVisitor is called for each file in an archive. It may set
|
||||
// tempFile and signature.
|
||||
func archiveFileVisitor(dir string, tempFile *string, signature *[]byte, filename string, filedata io.Reader) error {
|
||||
func archiveFileVisitor(dir string, tempFile *string, signature *[]byte, archivePath string, filedata io.Reader) error {
|
||||
var err error
|
||||
filename := path.Base(archivePath)
|
||||
archiveDir := path.Dir(archivePath)
|
||||
archiveDirs := strings.Split(archiveDir, "/")
|
||||
if len(archiveDirs) > 1 {
|
||||
//don't consider files in subfolders
|
||||
return nil
|
||||
}
|
||||
l.Debugf("considering file %s", archivePath)
|
||||
switch filename {
|
||||
case "syncthing", "syncthing.exe":
|
||||
l.Debugln("reading binary")
|
||||
l.Debugf("found upgrade binary %s", archivePath)
|
||||
*tempFile, err = writeBinary(dir, filedata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case "syncthing.sig", "syncthing.exe.sig":
|
||||
l.Debugln("reading signature")
|
||||
l.Debugf("found signature %s", archivePath)
|
||||
*signature, err = ioutil.ReadAll(filedata)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -62,7 +62,7 @@ var upgrades = map[string]string{
|
||||
"v0.10.21": "v0.10.30",
|
||||
"v0.10.29": "v0.10.30",
|
||||
"v0.10.31": "v0.10.30",
|
||||
"v0.10.0-alpha": "v0.10.30",
|
||||
"v0.10.0-alpha": "v0.11.0-beta0",
|
||||
"v0.10.0-beta": "v0.11.0-beta0",
|
||||
"v0.11.0-beta0+40-g53cb66e-dirty": "v0.11.0-beta0",
|
||||
}
|
||||
|
||||
+5
-4
@@ -158,10 +158,11 @@ func discover(intf *net.Interface, deviceType string, timeout time.Duration, res
|
||||
ssdp := &net.UDPAddr{IP: []byte{239, 255, 255, 250}, Port: 1900}
|
||||
|
||||
tpl := `M-SEARCH * HTTP/1.1
|
||||
Host: 239.255.255.250:1900
|
||||
St: %s
|
||||
Man: "ssdp:discover"
|
||||
Mx: %d
|
||||
HOST: 239.255.255.250:1900
|
||||
ST: %s
|
||||
MAN: "ssdp:discover"
|
||||
MX: %d
|
||||
USER-AGENT: syncthing/1.0
|
||||
|
||||
`
|
||||
searchStr := fmt.Sprintf(tpl, deviceType, timeout/time.Second)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-BEP" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "November 14, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-bep \- Block Exchange Protocol v1
|
||||
.
|
||||
|
||||
+130
-25
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "November 14, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.
|
||||
@@ -81,33 +81,42 @@ The following shows the default configuration file:
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
<configuration version="10">
|
||||
<folder id="default" path="/Users/jb/Sync" ro="false" rescanIntervalS="60" ignorePerms="false" autoNormalize="false">
|
||||
<device id="5SYI2FS\-LW6YAXI\-JJDYETS\-NDBBPIO\-256MWBO\-XDPXWVG\-24QPUM4\-PDW4UQU"></device>
|
||||
<configuration version="12">
|
||||
<folder id="default" path="/Users/jb/Sync/" ro="false" rescanIntervalS="60" ignorePerms="false" autoNormalize="true">
|
||||
<device id="3LT2GA5\-CQI4XJM\-WTZ264P\-MLOGMHL\-MCRLDNT\-MZV4RD3\-KA745CL\-OGAERQZ"></device>
|
||||
<minDiskFreePct>1</minDiskFreePct>
|
||||
<versioning></versioning>
|
||||
<copiers>0</copiers>
|
||||
<pullers>0</pullers>
|
||||
<hashers>0</hashers>
|
||||
<order>random</order>
|
||||
<ignoreDelete>false</ignoreDelete>
|
||||
<scanProgressIntervalS>0</scanProgressIntervalS>
|
||||
<pullerSleepS>0</pullerSleepS>
|
||||
<pullerPauseS>0</pullerPauseS>
|
||||
<maxConflicts>0</maxConflicts>
|
||||
</folder>
|
||||
<device id="5SYI2FS\-LW6YAXI\-JJDYETS\-NDBBPIO\-256MWBO\-XDPXWVG\-24QPUM4\-PDW4UQU" name="syno" compression="metadata" introducer="false">
|
||||
<device id="3LT2GA5\-CQI4XJM\-WTZ264P\-MLOGMHL\-MCRLDNT\-MZV4RD3\-KA745CL\-OGAERQZ" name="syno" compression="metadata" introducer="false">
|
||||
<address>dynamic</address>
|
||||
</device>
|
||||
<gui enabled="true" tls="false">
|
||||
<address>127.0.0.1:8384</address>
|
||||
<apikey>l7jSbCqPD95JYZ0g8vi4ZLAMg3ulnN1b</apikey>
|
||||
<address>127.0.0.1:52620</address>
|
||||
<apikey>k1dnz1Dd0rzTBjjFFh7CXPnrF12C49B1</apikey>
|
||||
</gui>
|
||||
<options>
|
||||
<listenAddress>0.0.0.0:56847</listenAddress>
|
||||
<globalAnnounceServer>udp4://announce.syncthing.net:22026</globalAnnounceServer>
|
||||
<globalAnnounceServer>udp6://announce\-v6.syncthing.net:22026</globalAnnounceServer>
|
||||
<listenAddress>tcp://0.0.0.0:22000</listenAddress>
|
||||
<globalAnnounceServer>default</globalAnnounceServer>
|
||||
<globalAnnounceEnabled>true</globalAnnounceEnabled>
|
||||
<localAnnounceEnabled>true</localAnnounceEnabled>
|
||||
<localAnnouncePort>21025</localAnnouncePort>
|
||||
<localAnnounceMCAddr>[ff32::5222]:21026</localAnnounceMCAddr>
|
||||
<localAnnouncePort>21027</localAnnouncePort>
|
||||
<localAnnounceMCAddr>[ff12::8384]:21027</localAnnounceMCAddr>
|
||||
<relayServer>dynamic+https://relays.syncthing.net/endpoint</relayServer>
|
||||
<maxSendKbps>0</maxSendKbps>
|
||||
<maxRecvKbps>0</maxRecvKbps>
|
||||
<reconnectionIntervalS>60</reconnectionIntervalS>
|
||||
<relaysEnabled>true</relaysEnabled>
|
||||
<relayReconnectIntervalM>10</relayReconnectIntervalM>
|
||||
<relayWithoutGlobalAnn>false</relayWithoutGlobalAnn>
|
||||
<startBrowser>true</startBrowser>
|
||||
<upnpEnabled>true</upnpEnabled>
|
||||
<upnpLeaseMinutes>60</upnpLeaseMinutes>
|
||||
@@ -115,6 +124,9 @@ The following shows the default configuration file:
|
||||
<upnpTimeoutSeconds>10</upnpTimeoutSeconds>
|
||||
<urAccepted>0</urAccepted>
|
||||
<urUniqueID></urUniqueID>
|
||||
<urURL>https://data.syncthing.net/newdata</urURL>
|
||||
<urPostInsecurely>false</urPostInsecurely>
|
||||
<urInitialDelayS>1800</urInitialDelayS>
|
||||
<restartOnWakeup>true</restartOnWakeup>
|
||||
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
|
||||
<keepTemporariesH>24</keepTemporariesH>
|
||||
@@ -123,6 +135,8 @@ The following shows the default configuration file:
|
||||
<symlinksEnabled>true</symlinksEnabled>
|
||||
<limitBandwidthInLan>false</limitBandwidthInLan>
|
||||
<databaseBlockCacheMiB>0</databaseBlockCacheMiB>
|
||||
<minHomeDiskFreePct>1</minHomeDiskFreePct>
|
||||
<releasesURL>https://api.github.com/repos/syncthing/syncthing/releases?per_page=30</releasesURL>
|
||||
</options>
|
||||
</configuration>
|
||||
.ft P
|
||||
@@ -144,13 +158,19 @@ migration from previous formats.
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
<folder id="default" path="/Users/jb/Sync" ro="false" rescanIntervalS="60" ignorePerms="false" autoNormalize="false">
|
||||
<device id="5SYI2FS\-LW6YAXI\-JJDYETS\-NDBBPIO\-256MWBO\-XDPXWVG\-24QPUM4\-PDW4UQU"></device>
|
||||
<folder id="default" path="/Users/jb/Sync/" ro="false" rescanIntervalS="60" ignorePerms="false" autoNormalize="true">
|
||||
<device id="3LT2GA5\-CQI4XJM\-WTZ264P\-MLOGMHL\-MCRLDNT\-MZV4RD3\-KA745CL\-OGAERQZ"></device>
|
||||
<minDiskFreePct>1</minDiskFreePct>
|
||||
<versioning></versioning>
|
||||
<copiers>0</copiers>
|
||||
<pullers>0</pullers>
|
||||
<hashers>0</hashers>
|
||||
<order>random</order>
|
||||
<ignoreDelete>false</ignoreDelete>
|
||||
<scanProgressIntervalS>0</scanProgressIntervalS>
|
||||
<pullerSleepS>0</pullerSleepS>
|
||||
<pullerPauseS>0</pullerPauseS>
|
||||
<maxConflicts>0</maxConflicts>
|
||||
</folder>
|
||||
.ft P
|
||||
.fi
|
||||
@@ -194,6 +214,11 @@ customary that the local device ID is included in all repositories.
|
||||
Syncthing will currently add this automatically if it is not present in
|
||||
the configuration file.
|
||||
.TP
|
||||
.B minDiskFreePct
|
||||
The percentage of space that should be available on the disk this folder
|
||||
resides. The folder will be stopped when the percentage of free space goes
|
||||
below the threshold. Set to zero to disable.
|
||||
.TP
|
||||
.B versioning
|
||||
Specifies a versioning configuration.
|
||||
.UNINDENT
|
||||
@@ -231,6 +256,23 @@ Pull files ordered by file size; smallest and largest first respectively.
|
||||
Pull files ordered by modification time; oldest and newest first
|
||||
respectively.
|
||||
.UNINDENT
|
||||
.TP
|
||||
.B ignoreDelete
|
||||
When set to true, this device will pretend not to see instructions to
|
||||
delete files from other devices.
|
||||
.TP
|
||||
.B scanProgressIntervalS
|
||||
The interval with which scan progress information is sent to the GUI. Zero
|
||||
means the default value (two seconds).
|
||||
.TP
|
||||
.B pullerSleepS, pullerPauseS
|
||||
Tweaks for rate limiting the puller. Don\(aqt change these unless you know
|
||||
what you\(aqre doing.
|
||||
.TP
|
||||
.B maxConflicts
|
||||
The maximum number of conflict copies to keep around for any given file.
|
||||
The default, \-1, means an unlimited number. Setting this to zero disables
|
||||
conflict copies altogether.
|
||||
.UNINDENT
|
||||
.SH DEVICE ELEMENT
|
||||
.INDENT 0.0
|
||||
@@ -370,16 +412,19 @@ If set, this is the API key that enables usage of the REST interface.
|
||||
.nf
|
||||
.ft C
|
||||
<options>
|
||||
<listenAddress>0.0.0.0:56847</listenAddress>
|
||||
<globalAnnounceServer>udp4://announce.syncthing.net:22026</globalAnnounceServer>
|
||||
<globalAnnounceServer>udp6://announce\-v6.syncthing.net:22026</globalAnnounceServer>
|
||||
<listenAddress>tcp://0.0.0.0:22000</listenAddress>
|
||||
<globalAnnounceServer>default</globalAnnounceServer>
|
||||
<globalAnnounceEnabled>true</globalAnnounceEnabled>
|
||||
<localAnnounceEnabled>true</localAnnounceEnabled>
|
||||
<localAnnouncePort>21025</localAnnouncePort>
|
||||
<localAnnounceMCAddr>[ff32::5222]:21026</localAnnounceMCAddr>
|
||||
<localAnnouncePort>21027</localAnnouncePort>
|
||||
<localAnnounceMCAddr>[ff12::8384]:21027</localAnnounceMCAddr>
|
||||
<relayServer>dynamic+https://relays.syncthing.net/endpoint</relayServer>
|
||||
<maxSendKbps>0</maxSendKbps>
|
||||
<maxRecvKbps>0</maxRecvKbps>
|
||||
<reconnectionIntervalS>60</reconnectionIntervalS>
|
||||
<relaysEnabled>true</relaysEnabled>
|
||||
<relayReconnectIntervalM>10</relayReconnectIntervalM>
|
||||
<relayWithoutGlobalAnn>false</relayWithoutGlobalAnn>
|
||||
<startBrowser>true</startBrowser>
|
||||
<upnpEnabled>true</upnpEnabled>
|
||||
<upnpLeaseMinutes>60</upnpLeaseMinutes>
|
||||
@@ -387,6 +432,9 @@ If set, this is the API key that enables usage of the REST interface.
|
||||
<upnpTimeoutSeconds>10</upnpTimeoutSeconds>
|
||||
<urAccepted>0</urAccepted>
|
||||
<urUniqueID></urUniqueID>
|
||||
<urURL>https://data.syncthing.net/newdata</urURL>
|
||||
<urPostInsecurely>false</urPostInsecurely>
|
||||
<urInitialDelayS>1800</urInitialDelayS>
|
||||
<restartOnWakeup>true</restartOnWakeup>
|
||||
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
|
||||
<keepTemporariesH>24</keepTemporariesH>
|
||||
@@ -395,8 +443,8 @@ If set, this is the API key that enables usage of the REST interface.
|
||||
<symlinksEnabled>true</symlinksEnabled>
|
||||
<limitBandwidthInLan>false</limitBandwidthInLan>
|
||||
<databaseBlockCacheMiB>0</databaseBlockCacheMiB>
|
||||
<pingTimeoutS>60</pingTimeoutS>
|
||||
<pingIdleTimeS>120</pingIdleTimeS>
|
||||
<minHomeDiskFreePct>1</minHomeDiskFreePct>
|
||||
<releasesURL>https://api.github.com/repos/syncthing/syncthing/releases?per_page=30</releasesURL>
|
||||
</options>
|
||||
.ft P
|
||||
.fi
|
||||
@@ -408,12 +456,18 @@ The \fBoptions\fP element contains all other global configuration options.
|
||||
.TP
|
||||
.B listenAddress
|
||||
The listen address for incoming sync connections. See the \fBaddress\fP
|
||||
element under the \fI\%GUI Element\fP for allowed syntax.
|
||||
element under the \fI\%GUI Element\fP for allowed syntax, with the addition
|
||||
that the address must have a protocol scheme prefix. Currently \fBtcp://\fP
|
||||
is the only supported protocol scheme.
|
||||
.TP
|
||||
.B globalAnnounceServer
|
||||
A URI to a global announce (discovery) server. Allowed protocol prefixes
|
||||
are \fBudp4://\fP (UDP over IPv4), \fBudp6://\fP (UDP over IPv6) and
|
||||
\fBudp://\fP (UDP over any available protocol).
|
||||
A URI to a global announce (discovery) server, or the word \fBdefault\fP to
|
||||
include the default servers. Any number of globalAnnounceServer elements
|
||||
may be present. The syntax for non\-default entries is that of a HTTP or
|
||||
HTTPS URL. A number of options may be added as query options to the URL:
|
||||
\fBinsecure\fP to prevent certificate validation (required for HTTP URLs)
|
||||
and \fBid=<device ID>\fP to perform certificate pinning. The device ID to
|
||||
use is printed by the discovery server on startup.
|
||||
.TP
|
||||
.B globalAnnounceEnabled
|
||||
Whether to announce this device to the global announce (discovery) server,
|
||||
@@ -429,6 +483,12 @@ The port on which to listen and send IPv4 broadcast announcements to.
|
||||
.B localAnnounceMCAddr
|
||||
The group address and port to join and send IPv6 multicast announcements on.
|
||||
.TP
|
||||
.B relayServer
|
||||
Lists one or more relay servers, on the format \fBrelay://hostname:port\fP\&.
|
||||
Alternatively, a relay list can be loaded over https by using an URL like
|
||||
\fBdynamic+https://somehost/path\fP\&. The default loads the list of relays
|
||||
from the relay pool server, \fBrelays.syncthing.net\fP\&.
|
||||
.TP
|
||||
.B maxSendKbps
|
||||
Outgoing data rate limit, in kibibits per second.
|
||||
.TP
|
||||
@@ -439,6 +499,17 @@ Incoming data rate limits, in kibibits per second.
|
||||
The number of seconds to wait between each attempt to connect to currently
|
||||
unconnected devices.
|
||||
.TP
|
||||
.B relaysEnabled
|
||||
When true, relays will be connected to and potentially used for device to device connections.
|
||||
.TP
|
||||
.B relayReconnectIntervalM
|
||||
Sets the interval, in minutes, between relay reconnect attempts.
|
||||
.TP
|
||||
.B relayWithoutGlobalAnn
|
||||
When set to true, relay connections will be attempted even when global
|
||||
discovery is disabled. This is useful only in the case where devices are
|
||||
known to be connected to the same relays. The default is \fBfalse\fP\&.
|
||||
.TP
|
||||
.B startBrowser
|
||||
Whether to attempt to start a browser to show the GUI when Syncthing starts.
|
||||
.TP
|
||||
@@ -465,6 +536,17 @@ version of usage reporting has been accepted.
|
||||
The unique ID sent together with the usage report. Generated when usage
|
||||
reporting is enabled.
|
||||
.TP
|
||||
.B urURL
|
||||
The URL to post usage report data to, when enabled.
|
||||
.TP
|
||||
.B urPostInsecurely
|
||||
When true, the UR URL can be http instead of https, or have a self signed
|
||||
certificate. The default is \fBfalse\fP\&.
|
||||
.TP
|
||||
.B urInitialDelayS
|
||||
The time to wait from startup to the first usage report being sent. Allows
|
||||
the system to stabilize before reporting statistics.
|
||||
.TP
|
||||
.B restartOnWakeup
|
||||
Whether to perform a restart of Syncthing when it is detected that we are
|
||||
waking from sleep mode (i.e. a folded up laptop).
|
||||
@@ -508,7 +590,30 @@ slow response time (slow connection/cpu) and large index exchanges
|
||||
.TP
|
||||
.B pingIdleTimeS
|
||||
ping interval in seconds. Don\(aqt change it unless you feel it\(aqs necessary.
|
||||
.TP
|
||||
.B minHomeDiskFreePct
|
||||
The percentage of space that should be available on the partition holding
|
||||
the configuration and index.
|
||||
.TP
|
||||
.B releasesURL
|
||||
The URL from which release information is loaded, for automatic upgrades.
|
||||
.UNINDENT
|
||||
.SH SYNCING CONFIGURATION FILES
|
||||
.sp
|
||||
Syncing configuration files between devices (such that multiple devices are
|
||||
using the same configuration files) can cause issues. This is easy to do
|
||||
accidentally if you sync your home folder between devices. A common symptom
|
||||
of syncing configuration files is two devices ending up with the same Device ID.
|
||||
.sp
|
||||
If you want to use syncthing to backup your configuration files, it is recommended
|
||||
that the files you are backing up are in a folder\-master to prevent other
|
||||
devices from overwriting the per device configuration. The folder on the remote
|
||||
device(s) should not be used as configuration for the remote devices.
|
||||
.sp
|
||||
If you\(aqd like to sync your home folder in non\-master mode, you may add the
|
||||
folder that stores the configuration files to the ignore list\&.
|
||||
If you\(aqd also like to backup your configuration files, add another folder in
|
||||
master mode for just the configuration folder.
|
||||
.SH AUTHOR
|
||||
The Syncthing Authors
|
||||
.SH COPYRIGHT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "November 14, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-EVENT-API" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "November 14, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-FAQ" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "November 14, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "November 14, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.
|
||||
@@ -67,15 +67,7 @@ certificate was presented, status \fB403\fP (Forbidden) is returned. If the
|
||||
posted data doesn\(aqt conform to the expected format, \fB400\fP (Bad Request) is
|
||||
returned.
|
||||
.sp
|
||||
In successfull responses, the server may return a
|
||||
.nf
|
||||
\(ga\(ga
|
||||
.fi
|
||||
Reannounce\-After"
|
||||
.nf
|
||||
\(ga\(ga
|
||||
.fi
|
||||
header
|
||||
In successfull responses, the server may return a \fBReannounce\-After\fP header
|
||||
containing the number of seconds after which the client should perform a new
|
||||
announcement.
|
||||
.sp
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "November 14, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v3
|
||||
.
|
||||
@@ -165,6 +165,9 @@ struct Relay {
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
In the \fBAnnounce\fP structure field \fBMagic\fP is used to ensure
|
||||
a correct datagram was received and MUST be equal to \fB0x9D79BC40\fP\&.
|
||||
.sp
|
||||
The first Device structure contains information about the sending
|
||||
device. The following zero or more Extra devices contain information
|
||||
about other devices known to the sending device.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-NETWORKING" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "November 14, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.
|
||||
@@ -55,18 +55,20 @@ The external forwarded port and the internal destination port has to be the same
|
||||
Communication in Syncthing works both ways. Therefore if you set up port
|
||||
forwards for one device, other devices will be able to connect to it even when
|
||||
they are behind a NAT network or firewall.
|
||||
.sp
|
||||
In the absence of port forwarding, relaying may work well enough to get
|
||||
devices connected and synced, but will perform poorly in comparison to a
|
||||
direct connection.
|
||||
.SH LOCAL FIREWALL
|
||||
.sp
|
||||
If your PC has a local firewall, you will need to open the following ports for
|
||||
incoming traffic:
|
||||
incoming and outgoing traffic:
|
||||
.INDENT 0.0
|
||||
.IP \(bu 2
|
||||
Port \fB22000/TCP\fP (or the actual listening port if you have changed
|
||||
the \fISync Protocol Listen Address\fP setting.)
|
||||
.IP \(bu 2
|
||||
Port \fB21025/UDP\fP (for discovery broadcasts on IPv4)
|
||||
.IP \(bu 2
|
||||
Port \fB21026/UDP\fP (for discovery multicasts on IPv6)
|
||||
Port \fB21027/UDP\fP (for discovery broadcasts on IPv4 and multicasts on IPv6)
|
||||
.UNINDENT
|
||||
.SH REMOTE WEB GUI
|
||||
.sp
|
||||
|
||||
+198
-33
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-REST-API" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "November 14, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.
|
||||
@@ -83,35 +83,127 @@ the corresponding GET request. The configuration will be saved to disk and the
|
||||
\fBconfigInSync\fP flag set to false. Restart Syncthing to activate.
|
||||
.SS GET /rest/system/connections
|
||||
.sp
|
||||
Returns the list of current connections and some metadata associated
|
||||
with the connection/peer.
|
||||
\fBNOTE:\fP
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
Return format changed in 0.12.0.
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
Returns the list of configured devices and some metadata associated
|
||||
with them. The list also contains the local device itself as not connected.
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
{
|
||||
"connections": {
|
||||
"SMAHWLH\-AP74FAB\-QWLDYGV\-Q65ASPL\-GAAR2TB\-KEF5FLB\-DRLZCPN\-DJBFZAG": {
|
||||
"address": "172.21.20.78:22000",
|
||||
"at": "2015\-03\-16T21:51:38.672758819+01:00",
|
||||
"clientVersion": "v0.10.27",
|
||||
"inBytesTotal": 415980,
|
||||
"outBytesTotal": 396300
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"address": "",
|
||||
"at": "2015\-03\-16T21:51:38.672868814+01:00",
|
||||
"clientVersion": "",
|
||||
"inBytesTotal": 415980,
|
||||
"outBytesTotal": 396300
|
||||
"total" : {
|
||||
"paused" : false,
|
||||
"clientVersion" : "",
|
||||
"at" : "2015\-11\-07T17:29:47.691637262+01:00",
|
||||
"connected" : false,
|
||||
"inBytesTotal" : 1479,
|
||||
"type" : "direct\-accept",
|
||||
"outBytesTotal" : 1318,
|
||||
"address" : ""
|
||||
},
|
||||
"connections" : {
|
||||
"YZJBJFX\-RDBL7WY\-6ZGKJ2D\-4MJB4E7\-ZATSDUY\-LD6Y3L3\-MLFUYWE\-AEMXJAC" : {
|
||||
"connected" : true,
|
||||
"inBytesTotal" : 556,
|
||||
"paused" : false,
|
||||
"at" : "2015\-11\-07T17:29:47.691548971+01:00",
|
||||
"clientVersion" : "v0.12.1",
|
||||
"address" : "127.0.0.1:22002",
|
||||
"type" : "direct\-dial",
|
||||
"outBytesTotal" : 550
|
||||
},
|
||||
"DOVII4U\-SQEEESM\-VZ2CVTC\-CJM4YN5\-QNV7DCU\-5U3ASRL\-YVFG6TH\-W5DV5AA" : {
|
||||
"outBytesTotal" : 0,
|
||||
"type" : "direct\-accept",
|
||||
"address" : "",
|
||||
"at" : "0001\-01\-01T00:00:00Z",
|
||||
"clientVersion" : "",
|
||||
"paused" : false,
|
||||
"inBytesTotal" : 0,
|
||||
"connected" : false
|
||||
},
|
||||
"UYGDMA4\-TPHOFO5\-2VQYDCC\-7CWX7XW\-INZINQT\-LE4B42N\-4JUZTSM\-IWCSXA4" : {
|
||||
"address" : "",
|
||||
"type" : "direct\-accept",
|
||||
"outBytesTotal" : 0,
|
||||
"connected" : false,
|
||||
"inBytesTotal" : 0,
|
||||
"paused" : false,
|
||||
"at" : "0001\-01\-01T00:00:00Z",
|
||||
"clientVersion" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS GET /rest/system/debug
|
||||
.sp
|
||||
New in version 0.12.0.
|
||||
|
||||
.sp
|
||||
Returns the set of debug facilities and which of them are currently enabled.
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
{
|
||||
"enabled": [
|
||||
"beacon"
|
||||
],
|
||||
"facilities": {
|
||||
"beacon": "Multicast and broadcast discovery",
|
||||
"config": "Configuration loading and saving",
|
||||
"connections": "Connection handling",
|
||||
"db": "The database layer",
|
||||
"dialer": "Dialing connections",
|
||||
"discover": "Remote device discovery",
|
||||
"events": "Event generation and logging",
|
||||
"http": "REST API",
|
||||
"main": "Main package",
|
||||
"model": "The root hub",
|
||||
"protocol": "The BEP protocol",
|
||||
"relay": "Relay connection handling",
|
||||
"scanner": "File change detection and hashing",
|
||||
"stats": "Persistent device and folder statistics",
|
||||
"sync": "Mutexes",
|
||||
"upgrade": "Binary upgrades",
|
||||
"upnp": "UPnP discovery and port mapping",
|
||||
"versioner": "File versioning"
|
||||
}
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS POST /rest/system/debug
|
||||
.sp
|
||||
New in version 0.12.0.
|
||||
|
||||
.sp
|
||||
Enables or disables debugging for specified facilities. Give one or both of
|
||||
\fBenable\fP and \fBdisable\fP query parameters, with comma separated facility
|
||||
names. To disable debugging of the beacon and discovery packages, and enable it
|
||||
for config and db:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
$ curl \-H X\-API\-Key:abc123 \-X POST \(aqhttp://localhost:8385/rest/system/debug?disable=beacon,discovery&enable=config,db\(aq
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS GET /rest/system/discovery
|
||||
.sp
|
||||
Returns the contents of the local discovery cache.
|
||||
@@ -150,6 +242,13 @@ curl \-X POST \-\-header "X\-API\-Key: TcE28kVPdtJ8COws1JdM0b2nodj77WeQ" http://
|
||||
Post with empty to body to remove all recent errors.
|
||||
.SS GET /rest/system/error
|
||||
.sp
|
||||
\fBNOTE:\fP
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
Return format changed in 0.12.0.
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
Returns the list of recent errors.
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
@@ -159,8 +258,8 @@ Returns the list of recent errors.
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"time": "2014\-09\-18T12:59:26.549953186+02:00",
|
||||
"error": "This is an error string"
|
||||
"when": "2014\-09\-18T12:59:26.549953186+02:00",
|
||||
"message": "This is an error string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -172,6 +271,29 @@ Returns the list of recent errors.
|
||||
.sp
|
||||
Post with an error message in the body (plain text) to register a new
|
||||
error. The new error will be displayed on any active GUI clients.
|
||||
.SS GET /rest/system/log
|
||||
.sp
|
||||
New in version 0.12.0.
|
||||
|
||||
.sp
|
||||
Returns the list of recent log entries.
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"when": "2014\-09\-18T12:59:26.549953186+02:00",
|
||||
"message": "This is a log entry"
|
||||
}
|
||||
]
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS GET /rest/system/ping
|
||||
.sp
|
||||
Returns a \fB{"ping": "pong"}\fP object.
|
||||
@@ -480,6 +602,13 @@ needed by this device in order for it to become in sync.
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
\fBNOTE:\fP
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
This is an expensive call, increasing CPU and RAM usage on the device. Use sparingly.
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SS POST /rest/db/prio
|
||||
.sp
|
||||
Moves the file to the top of the download queue.
|
||||
@@ -654,19 +783,55 @@ Returns the data sent in the anonymous usage report.
|
||||
.nf
|
||||
.ft C
|
||||
{
|
||||
"folderMaxFiles": 42106,
|
||||
"folderMaxMiB": 12563,
|
||||
"longVersion": "syncthing v0.10.27+5\-g36c93b7 (go1.4 darwin\-amd64 default) jb@syno 2015\-03\-16 20:43:34 UTC",
|
||||
"memorySize": 16384,
|
||||
"memoryUsageMiB": 41,
|
||||
"numDevices": 10,
|
||||
"numFolders": 4,
|
||||
"platform": "darwin\-amd64",
|
||||
"sha256Perf": 122.38,
|
||||
"totFiles": 45180,
|
||||
"totMiB": 18151,
|
||||
"uniqueID": "6vulmdGw",
|
||||
"version": "v0.10.27+5\-g36c93b7"
|
||||
"folderMaxMiB" : 0,
|
||||
"platform" : "linux\-amd64",
|
||||
"totMiB" : 0,
|
||||
"longVersion" : "syncthing v0.12.2 \e"Beryllium Bedbug\e" (go1.4.3 linux\-amd64 default) unknown\-user@build2.syncthing.net 2015\-11\-09 13:23:26 UTC",
|
||||
"upgradeAllowedManual" : true,
|
||||
"totFiles" : 3,
|
||||
"folderUses" : {
|
||||
"ignorePerms" : 0,
|
||||
"autoNormalize" : 0,
|
||||
"readonly" : 0,
|
||||
"ignoreDelete" : 0
|
||||
},
|
||||
"memoryUsageMiB" : 13,
|
||||
"version" : "v0.12.2",
|
||||
"sha256Perf" : 27.28,
|
||||
"numFolders" : 2,
|
||||
"memorySize" : 1992,
|
||||
"announce" : {
|
||||
"defaultServersIP" : 0,
|
||||
"otherServers" : 0,
|
||||
"globalEnabled" : false,
|
||||
"defaultServersDNS" : 1,
|
||||
"localEnabled" : false
|
||||
},
|
||||
"usesRateLimit" : false,
|
||||
"numCPU" : 2,
|
||||
"uniqueID" : "",
|
||||
"urVersion" : 2,
|
||||
"rescanIntvs" : [
|
||||
60,
|
||||
60
|
||||
],
|
||||
"numDevices" : 2,
|
||||
"folderMaxFiles" : 3,
|
||||
"relays" : {
|
||||
"defaultServers" : 1,
|
||||
"enabled" : true,
|
||||
"otherServers" : 0
|
||||
},
|
||||
"deviceUses" : {
|
||||
"compressMetadata" : 1,
|
||||
"customCertName" : 0,
|
||||
"staticAddr" : 1,
|
||||
"compressAlways" : 0,
|
||||
"compressNever" : 1,
|
||||
"introducer" : 0,
|
||||
"dynamicAddr" : 1
|
||||
},
|
||||
"upgradeAllowedAuto" : false
|
||||
}
|
||||
.ft P
|
||||
.fi
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-SECURITY" "7" "October 20, 2015" "v0.11" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "November 14, 2015" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user