Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba4554f053 | ||
|
|
33bed5b1ec | ||
|
|
4f27bdfc27 | ||
|
|
9212303906 | ||
|
|
603da2dce2 | ||
|
|
d510e3cca3 | ||
|
|
f51514d0e7 | ||
|
|
e67be59c5f | ||
|
|
add12b43aa | ||
|
|
aec91d8f32 | ||
|
|
53b0f36be6 | ||
|
|
830bde2c83 | ||
|
|
be1744a481 | ||
|
|
01ade9c8ae | ||
|
|
b1acc37c16 | ||
|
|
64a591610b | ||
|
|
9a07b22d4a | ||
|
|
406bedf1e3 | ||
|
|
7f55fbbe84 | ||
|
|
75f9ea623c | ||
|
|
9745679c63 | ||
|
|
8519a24ba6 | ||
|
|
c0be9987d0 | ||
|
|
c1f1fd71fe | ||
|
|
3c657d1749 | ||
|
|
53f80fdf73 | ||
|
|
6325ae070c | ||
|
|
089c283ca6 | ||
|
|
8d7ea0424d | ||
|
|
f12d5771dc | ||
|
|
7b0c49a1b6 | ||
|
|
0690fe7585 | ||
|
|
3bc918ff78 | ||
|
|
3e50edf46f | ||
|
|
1b10607def | ||
|
|
9d7a811e72 | ||
|
|
4f7d43a6dd | ||
|
|
83675e7a1d | ||
|
|
34fee05a1d | ||
|
|
d10773c311 | ||
|
|
caa2356409 | ||
|
|
523ac45456 | ||
|
|
b50d57b7fd | ||
|
|
8e645ab782 | ||
|
|
dfc1101307 |
@@ -38,6 +38,7 @@ Ben Shepherd (benshep) <bjashepherd@gmail.com>
|
||||
Ben Sidhom (bsidhom) <bsidhom@gmail.com>
|
||||
Benedikt Heine (bebehei) <bebe@bebehei.de>
|
||||
Benedikt Morbach <benedikt.morbach@googlemail.com>
|
||||
Benno Fünfstück <benno.fuenfstueck@gmail.com>
|
||||
Benny Ng (tpng) <benny.tpng@gmail.com>
|
||||
Boris Rybalkin <ribalkin@gmail.com>
|
||||
Brandon Philips (philips) <brandon@ifup.org>
|
||||
@@ -131,6 +132,7 @@ Mike Boone <mike@boonedocks.net>
|
||||
MikeLund <MikeLund@users.noreply.github.com>
|
||||
Nate Morrison (nrm21) <natemorrison@gmail.com>
|
||||
Nicholas Rishel (PrototypeNM1) <rishel.nick@gmail.com> <PrototypeNM1@users.noreply.github.com>
|
||||
Nico Stapelbroek <3368018+nstapelbroek@users.noreply.github.com>
|
||||
Nicolas Braud-Santoni <nicolas@braud-santoni.eu>
|
||||
Niels Peter Roest (Niller303) <nielsproest@hotmail.com> <seje.niels@hotmail.com>
|
||||
Nils Jakobi (thunderstorm99) <jakobi.nils@gmail.com>
|
||||
@@ -140,6 +142,7 @@ Pascal Jungblut (pascalj) <github@pascalj.com> <mail@pascal-jungblut.com>
|
||||
Pawel Palenica (qepasa) <pawelpalenica11@gmail.com>
|
||||
Paweł Rozlach <vespian@users.noreply.github.com>
|
||||
perewa <cavalcante.ten@gmail.com>
|
||||
Peter Badida <KeyWeeUsr@users.noreply.github.com>
|
||||
Peter Dave Hello <hsu@peterdavehello.org>
|
||||
Peter Hoeg (peterhoeg) <peter@speartail.com>
|
||||
Peter Marquardt (wwwutz) <wwwutz@gmail.com> <wwwutz@googlemail.com>
|
||||
|
||||
@@ -111,6 +111,14 @@ var targets = map[string]target{
|
||||
{src: "etc/linux-systemd/system/syncthing-resume.service", dst: "deb/lib/systemd/system/syncthing-resume.service", perm: 0644},
|
||||
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
|
||||
{src: "etc/firewall-ufw/syncthing", dst: "deb/etc/ufw/applications.d/syncthing", perm: 0644},
|
||||
{src: "etc/linux-desktop/syncthing-start.desktop", dst: "deb/usr/share/applications/syncthing-start.desktop", perm: 0644},
|
||||
{src: "etc/linux-desktop/syncthing-ui.desktop", dst: "deb/usr/share/applications/syncthing-ui.desktop", perm: 0644},
|
||||
{src: "assets/logo-32.png", dst: "deb/usr/share/icons/hicolor/32x32/apps/syncthing.png", perm: 0644},
|
||||
{src: "assets/logo-64.png", dst: "deb/usr/share/icons/hicolor/64x64/apps/syncthing.png", perm: 0644},
|
||||
{src: "assets/logo-128.png", dst: "deb/usr/share/icons/hicolor/128x128/apps/syncthing.png", perm: 0644},
|
||||
{src: "assets/logo-256.png", dst: "deb/usr/share/icons/hicolor/256x256/apps/syncthing.png", perm: 0644},
|
||||
{src: "assets/logo-512.png", dst: "deb/usr/share/icons/hicolor/512x512/apps/syncthing.png", perm: 0644},
|
||||
{src: "assets/logo-only.svg", dst: "deb/usr/share/icons/hicolor/scalable/apps/syncthing.svg", perm: 0644},
|
||||
},
|
||||
},
|
||||
"stdiscosrv": {
|
||||
@@ -361,7 +369,7 @@ func setup() {
|
||||
"github.com/AlekSi/gocov-xml",
|
||||
"github.com/axw/gocov/gocov",
|
||||
"github.com/FiloSottile/gvt",
|
||||
"github.com/golang/lint/golint",
|
||||
"golang.org/x/lint/golint",
|
||||
"github.com/gordonklaus/ineffassign",
|
||||
"github.com/mdempsky/unconvert",
|
||||
"github.com/mitchellh/go-wordwrap",
|
||||
|
||||
@@ -162,10 +162,10 @@ func (s *levelDBStore) Serve() {
|
||||
// Start the statistics serve routine. It will exit with us when
|
||||
// statisticsTrigger is closed.
|
||||
statisticsTrigger := make(chan struct{})
|
||||
defer close(statisticsTrigger)
|
||||
statisticsDone := make(chan struct{})
|
||||
go s.statisticsServe(statisticsTrigger, statisticsDone)
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case fn := <-s.inbox:
|
||||
@@ -184,12 +184,18 @@ func (s *levelDBStore) Serve() {
|
||||
|
||||
case <-s.stop:
|
||||
// We're done.
|
||||
return
|
||||
close(statisticsTrigger)
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
// Also wait for statisticsServe to return
|
||||
<-statisticsDone
|
||||
}
|
||||
|
||||
func (s *levelDBStore) statisticsServe(trigger <-chan struct{}, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
|
||||
for range trigger {
|
||||
t0 := time.Now()
|
||||
nowNanos := t0.UnixNano()
|
||||
|
||||
@@ -121,7 +121,7 @@ func main() {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "stdiscosrv", 0)
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "stdiscosrv")
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to generate X509 key pair:", err)
|
||||
}
|
||||
|
||||
+11
-1
@@ -109,5 +109,15 @@ func init() {
|
||||
databaseKeys, databaseStatisticsSeconds,
|
||||
databaseOperations, databaseOperationSeconds)
|
||||
|
||||
prometheus.MustRegister(prometheus.NewProcessCollector(os.Getpid(), "syncthing_discovery"))
|
||||
processCollectorOpts := prometheus.ProcessCollectorOpts{
|
||||
Namespace: "syncthing_discovery",
|
||||
PidFn: func() (int, error) {
|
||||
return os.Getpid(), nil
|
||||
},
|
||||
}
|
||||
|
||||
prometheus.MustRegister(
|
||||
prometheus.NewProcessCollector(processCollectorOpts),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func dump(ldb *db.Instance) {
|
||||
func dump(ldb *db.Lowlevel) {
|
||||
it := ldb.NewIterator(nil, nil)
|
||||
for it.Next() {
|
||||
key := it.Key()
|
||||
|
||||
@@ -37,7 +37,7 @@ func (h *ElementHeap) Pop() interface{} {
|
||||
return x
|
||||
}
|
||||
|
||||
func dumpsize(ldb *db.Instance) {
|
||||
func dumpsize(ldb *db.Lowlevel) {
|
||||
h := &ElementHeap{}
|
||||
heap.Init(h)
|
||||
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type fileInfoKey struct {
|
||||
folder uint32
|
||||
device uint32
|
||||
name string
|
||||
}
|
||||
|
||||
type globalKey struct {
|
||||
folder uint32
|
||||
name string
|
||||
}
|
||||
|
||||
type sequenceKey struct {
|
||||
folder uint32
|
||||
sequence uint64
|
||||
}
|
||||
|
||||
func idxck(ldb *db.Lowlevel) (success bool) {
|
||||
folders := make(map[uint32]string)
|
||||
devices := make(map[uint32]string)
|
||||
deviceToIDs := make(map[string]uint32)
|
||||
fileInfos := make(map[fileInfoKey]protocol.FileInfo)
|
||||
globals := make(map[globalKey]db.VersionList)
|
||||
sequences := make(map[sequenceKey]string)
|
||||
needs := make(map[globalKey]struct{})
|
||||
var localDeviceKey uint32
|
||||
success = true
|
||||
|
||||
it := ldb.NewIterator(nil, nil)
|
||||
for it.Next() {
|
||||
key := it.Key()
|
||||
switch key[0] {
|
||||
case db.KeyTypeDevice:
|
||||
folder := binary.BigEndian.Uint32(key[1:])
|
||||
device := binary.BigEndian.Uint32(key[1+4:])
|
||||
name := nulString(key[1+4+4:])
|
||||
|
||||
var f protocol.FileInfo
|
||||
err := f.Unmarshal(it.Value())
|
||||
if err != nil {
|
||||
fmt.Println("Unable to unmarshal FileInfo:", err)
|
||||
success = false
|
||||
continue
|
||||
}
|
||||
|
||||
fileInfos[fileInfoKey{folder, device, name}] = f
|
||||
|
||||
case db.KeyTypeGlobal:
|
||||
folder := binary.BigEndian.Uint32(key[1:])
|
||||
name := nulString(key[1+4:])
|
||||
var flv db.VersionList
|
||||
if err := flv.Unmarshal(it.Value()); err != nil {
|
||||
fmt.Println("Unable to unmarshal VersionList:", err)
|
||||
success = false
|
||||
continue
|
||||
}
|
||||
globals[globalKey{folder, name}] = flv
|
||||
|
||||
case db.KeyTypeFolderIdx:
|
||||
key := binary.BigEndian.Uint32(it.Key()[1:])
|
||||
folders[key] = string(it.Value())
|
||||
|
||||
case db.KeyTypeDeviceIdx:
|
||||
key := binary.BigEndian.Uint32(it.Key()[1:])
|
||||
devices[key] = string(it.Value())
|
||||
deviceToIDs[string(it.Value())] = key
|
||||
if bytes.Equal(it.Value(), protocol.LocalDeviceID[:]) {
|
||||
localDeviceKey = key
|
||||
}
|
||||
|
||||
case db.KeyTypeSequence:
|
||||
folder := binary.BigEndian.Uint32(key[1:])
|
||||
seq := binary.BigEndian.Uint64(key[5:])
|
||||
val := it.Value()
|
||||
sequences[sequenceKey{folder, seq}] = string(val[9:])
|
||||
|
||||
case db.KeyTypeNeed:
|
||||
folder := binary.BigEndian.Uint32(key[1:])
|
||||
name := nulString(key[1+4:])
|
||||
needs[globalKey{folder, name}] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if localDeviceKey == 0 {
|
||||
fmt.Println("Missing key for local device in device index (bailing out)")
|
||||
success = false
|
||||
return
|
||||
}
|
||||
|
||||
for fk, fi := range fileInfos {
|
||||
if fk.name != fi.Name {
|
||||
fmt.Printf("Mismatching FileInfo name, %q (key) != %q (actual)\n", fk.name, fi.Name)
|
||||
success = false
|
||||
}
|
||||
|
||||
folder := folders[fk.folder]
|
||||
if folder == "" {
|
||||
fmt.Printf("Unknown folder ID %d for FileInfo %q\n", fk.folder, fk.name)
|
||||
success = false
|
||||
continue
|
||||
}
|
||||
if devices[fk.device] == "" {
|
||||
fmt.Printf("Unknown device ID %d for FileInfo %q, folder %q\n", fk.folder, fk.name, folder)
|
||||
success = false
|
||||
}
|
||||
|
||||
if fk.device == localDeviceKey {
|
||||
name, ok := sequences[sequenceKey{fk.folder, uint64(fi.Sequence)}]
|
||||
if !ok {
|
||||
fmt.Printf("Sequence entry missing for FileInfo %q, folder %q, seq %d\n", fi.Name, folder, fi.Sequence)
|
||||
success = false
|
||||
continue
|
||||
}
|
||||
if name != fi.Name {
|
||||
fmt.Printf("Sequence entry refers to wrong name, %q (seq) != %q (FileInfo), folder %q, seq %d\n", name, fi.Name, folder, fi.Sequence)
|
||||
success = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for gk, vl := range globals {
|
||||
folder := folders[gk.folder]
|
||||
if folder == "" {
|
||||
fmt.Printf("Unknown folder ID %d for VersionList %q\n", gk.folder, gk.name)
|
||||
success = false
|
||||
}
|
||||
for i, fv := range vl.Versions {
|
||||
dev, ok := deviceToIDs[string(fv.Device)]
|
||||
if !ok {
|
||||
fmt.Printf("VersionList %q, folder %q refers to unknown device %q\n", gk.name, folder, fv.Device)
|
||||
success = false
|
||||
}
|
||||
fi, ok := fileInfos[fileInfoKey{gk.folder, dev, gk.name}]
|
||||
if !ok {
|
||||
fmt.Printf("VersionList %q, folder %q, entry %d refers to unknown FileInfo\n", gk.name, folder, i)
|
||||
success = false
|
||||
}
|
||||
if !fi.Version.Equal(fv.Version) {
|
||||
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo version mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, fv.Version, fi.Version)
|
||||
success = false
|
||||
}
|
||||
if fi.IsInvalid() != fv.Invalid {
|
||||
fmt.Printf("VersionList %q, folder %q, entry %d, FileInfo invalid mismatch, %v (VersionList) != %v (FileInfo)\n", gk.name, folder, i, fv.Invalid, fi.IsInvalid())
|
||||
success = false
|
||||
}
|
||||
}
|
||||
|
||||
// If we need this file we should have a need entry for it. False
|
||||
// positives from needsLocally for deleted files, where we might
|
||||
// legitimately lack an entry if we never had it, and ignored files.
|
||||
if needsLocally(vl) {
|
||||
_, ok := needs[gk]
|
||||
if !ok {
|
||||
dev, _ := deviceToIDs[string(vl.Versions[0].Device)]
|
||||
fi, _ := fileInfos[fileInfoKey{gk.folder, dev, gk.name}]
|
||||
if !fi.IsDeleted() && !fi.IsIgnored() {
|
||||
fmt.Printf("Missing need entry for needed file %q, folder %q\n", gk.name, folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seenSeq := make(map[fileInfoKey]uint64)
|
||||
for sk, name := range sequences {
|
||||
folder := folders[sk.folder]
|
||||
if folder == "" {
|
||||
fmt.Printf("Unknown folder ID %d for sequence entry %d, %q\n", sk.folder, sk.sequence, name)
|
||||
success = false
|
||||
continue
|
||||
}
|
||||
|
||||
if prev, ok := seenSeq[fileInfoKey{folder: sk.folder, name: name}]; ok {
|
||||
fmt.Printf("Duplicate sequence entry for %q, folder %q, seq %d (prev %d)\n", name, folder, sk.sequence, prev)
|
||||
success = false
|
||||
}
|
||||
seenSeq[fileInfoKey{folder: sk.folder, name: name}] = sk.sequence
|
||||
|
||||
fi, ok := fileInfos[fileInfoKey{sk.folder, localDeviceKey, name}]
|
||||
if !ok {
|
||||
fmt.Printf("Missing FileInfo for sequence entry %d, folder %q, %q\n", sk.sequence, folder, name)
|
||||
success = false
|
||||
continue
|
||||
}
|
||||
if fi.Sequence != int64(sk.sequence) {
|
||||
fmt.Printf("Sequence mismatch for %q, folder %q, %d (key) != %d (FileInfo)\n", name, folder, sk.sequence, fi.Sequence)
|
||||
success = false
|
||||
}
|
||||
}
|
||||
|
||||
for nk := range needs {
|
||||
folder := folders[nk.folder]
|
||||
if folder == "" {
|
||||
fmt.Printf("Unknown folder ID %d for need entry %q\n", nk.folder, nk.name)
|
||||
success = false
|
||||
continue
|
||||
}
|
||||
|
||||
vl, ok := globals[nk]
|
||||
if !ok {
|
||||
fmt.Printf("Missing global for need entry %q, folder %q\n", nk.name, folder)
|
||||
success = false
|
||||
continue
|
||||
}
|
||||
|
||||
if !needsLocally(vl) {
|
||||
fmt.Printf("Need entry for file we don't need, %q, folder %q\n", nk.name, folder)
|
||||
success = false
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func needsLocally(vl db.VersionList) bool {
|
||||
var lv *protocol.Vector
|
||||
for _, fv := range vl.Versions {
|
||||
if bytes.Equal(fv.Device, protocol.LocalDeviceID[:]) {
|
||||
lv = &fv.Version
|
||||
break
|
||||
}
|
||||
}
|
||||
if lv == nil {
|
||||
return true // proviosinally, it looks like we need the file
|
||||
}
|
||||
return !lv.GreaterEqual(vl.Versions[0].Version)
|
||||
}
|
||||
+6
-4
@@ -21,7 +21,7 @@ func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
flag.StringVar(&mode, "mode", "dump", "Mode of operation: dump, dumpsize")
|
||||
flag.StringVar(&mode, "mode", "dump", "Mode of operation: dump, dumpsize, idxck")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
@@ -30,9 +30,7 @@ func main() {
|
||||
path = filepath.Join(defaultConfigDir(), "index-v0.14.0.db")
|
||||
}
|
||||
|
||||
fmt.Println("Path:", path)
|
||||
|
||||
ldb, err := db.Open(path)
|
||||
ldb, err := db.OpenRO(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -41,6 +39,10 @@ func main() {
|
||||
dump(ldb)
|
||||
} else if mode == "dumpsize" {
|
||||
dumpsize(ldb)
|
||||
} else if mode == "idxck" {
|
||||
if !idxck(ldb) {
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Unknown mode")
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
<script type="text/javascript" src="//code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
|
||||
<script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
||||
<script type="text/javascript" src="//maps.googleapis.com/maps/api/js"></script>
|
||||
<script type="text/javascript" src="//maps.googleapis.com/maps/api/js?key=AIzaSyDk5WJ8s7ueLKb99X5DbQ-vkWtPDAKqYs0"></script>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -636,7 +636,7 @@ func createTestCertificate() tls.Certificate {
|
||||
}
|
||||
|
||||
certFile, keyFile := filepath.Join(tmpDir, "cert.pem"), filepath.Join(tmpDir, "key.pem")
|
||||
cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 3072)
|
||||
cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv")
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to create test X509 key pair:", err)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,16 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(prometheus.NewProcessCollector(os.Getpid(), "syncthing_relaypoolsrv"))
|
||||
processCollectorOpts := prometheus.ProcessCollectorOpts{
|
||||
Namespace: "syncthing_relaypoolsrv",
|
||||
PidFn: func() (int, error) {
|
||||
return os.Getpid(), nil
|
||||
},
|
||||
}
|
||||
|
||||
prometheus.MustRegister(
|
||||
prometheus.NewProcessCollector(processCollectorOpts),
|
||||
)
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -166,7 +166,7 @@ func main() {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv", 3072)
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "strelaysrv")
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to generate X509 key pair:", err)
|
||||
}
|
||||
|
||||
+55
-32
@@ -115,7 +115,7 @@ type modelIntf interface {
|
||||
RemoteSequence(folder string) (int64, bool)
|
||||
State(folder string) (string, time.Time, error)
|
||||
UsageReportingStats(version int, preview bool) map[string]interface{}
|
||||
PullErrors(folder string) ([]model.FileError, error)
|
||||
FolderErrors(folder string) ([]model.FileError, error)
|
||||
WatchError(folder string) error
|
||||
}
|
||||
|
||||
@@ -185,28 +185,13 @@ func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener,
|
||||
name = tlsDefaultCommonName
|
||||
}
|
||||
|
||||
cert, err = tlsutil.NewCertificate(s.httpsCertFile, s.httpsKeyFile, name, httpsRSABits)
|
||||
cert, err = tlsutil.NewCertificate(s.httpsCertFile, s.httpsKeyFile, name)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS10, // No SSLv3
|
||||
CipherSuites: []uint16{
|
||||
// No RC4
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
|
||||
},
|
||||
}
|
||||
tlsCfg := tlsutil.SecureDefault()
|
||||
tlsCfg.Certificates = []tls.Certificate{cert}
|
||||
|
||||
if guiCfg.Network() == "unix" {
|
||||
// When listening on a UNIX socket we should unlink before bind,
|
||||
@@ -276,7 +261,8 @@ func (s *apiService) Serve() {
|
||||
getRestMux.HandleFunc("/rest/db/status", s.getDBStatus) // folder
|
||||
getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
|
||||
getRestMux.HandleFunc("/rest/folder/versions", s.getFolderVersions) // folder
|
||||
getRestMux.HandleFunc("/rest/folder/pullerrors", s.getPullErrors) // folder
|
||||
getRestMux.HandleFunc("/rest/folder/errors", s.getFolderErrors) // folder
|
||||
getRestMux.HandleFunc("/rest/folder/pullerrors", s.getFolderErrors) // folder (deprecated)
|
||||
getRestMux.HandleFunc("/rest/events", s.getIndexEvents) // [since] [limit] [timeout] [events]
|
||||
getRestMux.HandleFunc("/rest/events/disk", s.getDiskEvents) // [since] [limit] [timeout]
|
||||
getRestMux.HandleFunc("/rest/stats/device", s.getDeviceStats) // -
|
||||
@@ -710,12 +696,13 @@ func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
|
||||
func folderSummary(cfg configIntf, m modelIntf, folder string) (map[string]interface{}, error) {
|
||||
var res = make(map[string]interface{})
|
||||
|
||||
pullErrors, err := m.PullErrors(folder)
|
||||
errors, err := m.FolderErrors(folder)
|
||||
if err != nil && err != model.ErrFolderPaused {
|
||||
// Stats from the db can still be obtained if the folder is just paused
|
||||
return nil, err
|
||||
}
|
||||
res["pullErrors"] = len(pullErrors)
|
||||
res["errors"] = len(errors)
|
||||
res["pullErrors"] = len(errors) // deprecated
|
||||
|
||||
res["invalid"] = "" // Deprecated, retains external API for now
|
||||
|
||||
@@ -1060,7 +1047,7 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
for _, line := range s.systemLog.Since(time.Time{}) {
|
||||
fmt.Fprintf(&buflog, "%s: %s\n", line.When.Format(time.RFC3339), line.Message)
|
||||
}
|
||||
files = append(files, fileEntry{name: "log.txt", data: buflog.Bytes()})
|
||||
files = append(files, fileEntry{name: "log-inmemory.txt", data: buflog.Bytes()})
|
||||
|
||||
// Errors as a JSON
|
||||
if errs := s.guiErrors.Since(time.Time{}); len(errs) > 0 {
|
||||
@@ -1071,7 +1058,7 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Panic files as a JSON
|
||||
// Panic files
|
||||
if panicFiles, err := filepath.Glob(filepath.Join(baseDirs["config"], "panic*")); err == nil {
|
||||
for _, f := range panicFiles {
|
||||
if panicFile, err := ioutil.ReadFile(f); err != nil {
|
||||
@@ -1082,6 +1069,11 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Archived log (default on Windows)
|
||||
if logFile, err := ioutil.ReadFile(locations[locLogFile]); err == nil {
|
||||
files = append(files, fileEntry{name: "log-ondisk.txt", data: logFile})
|
||||
}
|
||||
|
||||
// Version and platform information as a JSON
|
||||
if versionPlatform, err := json.MarshalIndent(map[string]string{
|
||||
"now": time.Now().Format(time.RFC3339),
|
||||
@@ -1126,7 +1118,7 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Set zip file name and path
|
||||
zipFileName := fmt.Sprintf("support-bundle-%s.zip", time.Now().Format("2006-01-02T150405"))
|
||||
zipFileName := fmt.Sprintf("support-bundle-%s-%s.zip", s.id.Short().String(), time.Now().Format("2006-01-02T150405"))
|
||||
zipFilePath := filepath.Join(baseDirs["config"], zipFileName)
|
||||
|
||||
// Write buffer zip to local zip file (back up)
|
||||
@@ -1511,12 +1503,12 @@ func (s *apiService) postFolderVersionsRestore(w http.ResponseWriter, r *http.Re
|
||||
sendJSON(w, ferr)
|
||||
}
|
||||
|
||||
func (s *apiService) getPullErrors(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *apiService) getFolderErrors(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
folder := qs.Get("folder")
|
||||
page, perpage := getPagingParams(qs)
|
||||
|
||||
errors, err := s.model.PullErrors(folder)
|
||||
errors, err := s.model.FolderErrors(folder)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
@@ -1552,6 +1544,24 @@ func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, browseFiles(current, fsType))
|
||||
}
|
||||
|
||||
const (
|
||||
matchExact int = iota
|
||||
matchCaseIns
|
||||
noMatch
|
||||
)
|
||||
|
||||
func checkPrefixMatch(s, prefix string) int {
|
||||
if strings.HasPrefix(s, prefix) {
|
||||
return matchExact
|
||||
}
|
||||
|
||||
if strings.HasPrefix(strings.ToLower(s), strings.ToLower(prefix)) {
|
||||
return matchCaseIns
|
||||
}
|
||||
|
||||
return noMatch
|
||||
}
|
||||
|
||||
func browseFiles(current string, fsType fs.FilesystemType) []string {
|
||||
if current == "" {
|
||||
filesystem := fs.NewFilesystem(fsType, "")
|
||||
@@ -1577,16 +1587,29 @@ func browseFiles(current string, fsType fs.FilesystemType) []string {
|
||||
|
||||
fs := fs.NewFilesystem(fsType, searchDir)
|
||||
|
||||
subdirectories, _ := fs.Glob(searchFile + "*")
|
||||
subdirectories, _ := fs.DirNames(".")
|
||||
|
||||
exactMatches := make([]string, 0, len(subdirectories))
|
||||
caseInsMatches := make([]string, 0, len(subdirectories))
|
||||
|
||||
ret := make([]string, 0, len(subdirectories))
|
||||
for _, subdirectory := range subdirectories {
|
||||
info, err := fs.Stat(subdirectory)
|
||||
if err == nil && info.IsDir() {
|
||||
ret = append(ret, filepath.Join(searchDir, subdirectory)+pathSeparator)
|
||||
if err != nil || !info.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
switch checkPrefixMatch(subdirectory, searchFile) {
|
||||
case matchExact:
|
||||
exactMatches = append(exactMatches, filepath.Join(searchDir, subdirectory)+pathSeparator)
|
||||
case matchCaseIns:
|
||||
caseInsMatches = append(caseInsMatches, filepath.Join(searchDir, subdirectory)+pathSeparator)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
|
||||
// sort to return matches in deterministic order (don't depend on file system order)
|
||||
sort.Strings(exactMatches)
|
||||
sort.Strings(caseInsMatches)
|
||||
return append(exactMatches, caseInsMatches...)
|
||||
}
|
||||
|
||||
func (s *apiService) getCPUProf(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -988,10 +988,14 @@ func TestBrowse(t *testing.T) {
|
||||
if err := ioutil.WriteFile(filepath.Join(tmpDir, "file"), []byte("hello"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(tmpDir, "MiXEDCase"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We expect completion to return the full path to the completed
|
||||
// directory, with an ending slash.
|
||||
dirPath := filepath.Join(tmpDir, "dir") + pathSep
|
||||
mixedCaseDirPath := filepath.Join(tmpDir, "MiXEDCase") + pathSep
|
||||
|
||||
cases := []struct {
|
||||
current string
|
||||
@@ -1002,13 +1006,15 @@ func TestBrowse(t *testing.T) {
|
||||
// With slash it's completed to its contents.
|
||||
// Dirs are given pathSeps.
|
||||
// Files are not returned.
|
||||
{tmpDir + pathSep, []string{dirPath}},
|
||||
{tmpDir + pathSep, []string{mixedCaseDirPath, dirPath}},
|
||||
// Globbing is automatic based on prefix.
|
||||
{tmpDir + pathSep + "d", []string{dirPath}},
|
||||
{tmpDir + pathSep + "di", []string{dirPath}},
|
||||
{tmpDir + pathSep + "dir", []string{dirPath}},
|
||||
{tmpDir + pathSep + "f", nil},
|
||||
{tmpDir + pathSep + "q", nil},
|
||||
// Globbing is case-insensitve
|
||||
{tmpDir + pathSep + "mixed", []string{mixedCaseDirPath}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -1019,6 +1025,26 @@ func TestBrowse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixMatch(t *testing.T) {
|
||||
cases := []struct {
|
||||
s string
|
||||
prefix string
|
||||
expected int
|
||||
}{
|
||||
{"aaaA", "aaa", matchExact},
|
||||
{"AAAX", "BBB", noMatch},
|
||||
{"AAAX", "aAa", matchCaseIns},
|
||||
{"äÜX", "äü", matchCaseIns},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
ret := checkPrefixMatch(tc.s, tc.prefix)
|
||||
if ret != tc.expected {
|
||||
t.Errorf("checkPrefixMatch(%q, %q) => %v, expected %v", tc.s, tc.prefix, ret, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func equalStrings(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
|
||||
+18
-32
@@ -78,8 +78,6 @@ const (
|
||||
const (
|
||||
bepProtocolName = "bep/1.0"
|
||||
tlsDefaultCommonName = "syncthing"
|
||||
httpsRSABits = 2048
|
||||
bepRSABits = 0 // 384 bit ECDSA used instead
|
||||
defaultEventTimeout = time.Minute
|
||||
maxSystemErrors = 5
|
||||
initialSystemLog = 10
|
||||
@@ -471,7 +469,7 @@ func generate(generateDir string) {
|
||||
l.Warnln("Key exists; will not overwrite.")
|
||||
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
|
||||
} else {
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, tlsDefaultCommonName, bepRSABits)
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, tlsDefaultCommonName)
|
||||
if err != nil {
|
||||
l.Fatalln("Create certificate:", err)
|
||||
}
|
||||
@@ -639,7 +637,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
cert, err := tls.LoadX509KeyPair(locations[locCertFile], locations[locKeyFile])
|
||||
if err != nil {
|
||||
l.Infof("Generating ECDSA key and certificate for %s...", tlsDefaultCommonName)
|
||||
cert, err = tlsutil.NewCertificate(locations[locCertFile], locations[locKeyFile], tlsDefaultCommonName, bepRSABits)
|
||||
cert, err = tlsutil.NewCertificate(locations[locCertFile], locations[locKeyFile], tlsDefaultCommonName)
|
||||
if err != nil {
|
||||
l.Fatalln(err)
|
||||
}
|
||||
@@ -680,30 +678,6 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
}()
|
||||
}
|
||||
|
||||
// The TLS configuration is used for both the listening socket and outgoing
|
||||
// connections.
|
||||
|
||||
tlsCfg := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
NextProtos: []string{bepProtocolName},
|
||||
ClientAuth: tls.RequestClientCert,
|
||||
SessionTicketsDisabled: true,
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
0xCCA8, // TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, Go 1.8
|
||||
0xCCA9, // TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, Go 1.8
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||
},
|
||||
}
|
||||
|
||||
perf := cpuBench(3, 150*time.Millisecond, true)
|
||||
l.Infof("Hashing performance is %.02f MB/s", perf)
|
||||
|
||||
@@ -712,11 +686,13 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
if err != nil {
|
||||
l.Fatalln("Error opening database:", err)
|
||||
}
|
||||
if err := db.UpdateSchema(ldb); err != nil {
|
||||
l.Fatalln("Database schema:", err)
|
||||
}
|
||||
|
||||
if runtimeOptions.resetDeltaIdxs {
|
||||
l.Infoln("Reinitializing delta index IDs")
|
||||
ldb.DropLocalDeltaIndexIDs()
|
||||
ldb.DropRemoteDeltaIndexIDs()
|
||||
db.DropDeltaIndexIDs(ldb)
|
||||
}
|
||||
|
||||
protectedFiles := []string{
|
||||
@@ -737,7 +713,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
// Grab the previously running version string from the database.
|
||||
|
||||
miscDB := db.NewNamespacedKV(ldb, string(db.KeyTypeMiscData))
|
||||
miscDB := db.NewMiscDataNamespace(ldb)
|
||||
prevVersion, _ := miscDB.String("prevVersion")
|
||||
|
||||
// Strip away prerelease/beta stuff and just compare the release
|
||||
@@ -753,7 +729,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
// Drop delta indexes in case we've changed random stuff we
|
||||
// shouldn't have. We will resend our index on next connect.
|
||||
ldb.DropLocalDeltaIndexIDs()
|
||||
db.DropDeltaIndexIDs(ldb)
|
||||
|
||||
// Remember the new version.
|
||||
miscDB.PutString("prevVersion", Version)
|
||||
@@ -792,6 +768,16 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
cachedDiscovery := discover.NewCachingMux()
|
||||
mainService.Add(cachedDiscovery)
|
||||
|
||||
// The TLS configuration is used for both the listening socket and outgoing
|
||||
// connections.
|
||||
|
||||
tlsCfg := tlsutil.SecureDefault()
|
||||
tlsCfg.Certificates = []tls.Certificate{cert}
|
||||
tlsCfg.NextProtos = []string{bepProtocolName}
|
||||
tlsCfg.ClientAuth = tls.RequestClientCert
|
||||
tlsCfg.SessionTicketsDisabled = true
|
||||
tlsCfg.InsecureSkipVerify = true
|
||||
|
||||
// Start connection management
|
||||
|
||||
connectionsService := connections.NewService(cfg, myID, m, tlsCfg, cachedDiscovery, bepProtocolName, tlsDefaultCommonName)
|
||||
|
||||
@@ -139,7 +139,7 @@ func (m *mockedModel) UsageReportingStats(version int, preview bool) map[string]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) PullErrors(folder string) ([]model.FileError, error) {
|
||||
func (m *mockedModel) FolderErrors(folder string) ([]model.FileError, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
@@ -326,6 +327,8 @@ type usageReportingService struct {
|
||||
connectionsService *connections.Service
|
||||
forceRun chan struct{}
|
||||
stop chan struct{}
|
||||
stopped chan struct{}
|
||||
stopMut sync.RWMutex
|
||||
}
|
||||
|
||||
func newUsageReportingService(cfg *config.Wrapper, model *model.Model, connectionsService *connections.Service) *usageReportingService {
|
||||
@@ -335,7 +338,9 @@ func newUsageReportingService(cfg *config.Wrapper, model *model.Model, connectio
|
||||
connectionsService: connectionsService,
|
||||
forceRun: make(chan struct{}),
|
||||
stop: make(chan struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
}
|
||||
close(svc.stopped) // Not yet running, dont block on Stop()
|
||||
cfg.Subscribe(svc)
|
||||
return svc
|
||||
}
|
||||
@@ -359,8 +364,16 @@ func (s *usageReportingService) sendUsageReport() error {
|
||||
}
|
||||
|
||||
func (s *usageReportingService) Serve() {
|
||||
s.stopMut.Lock()
|
||||
s.stop = make(chan struct{})
|
||||
s.stopped = make(chan struct{})
|
||||
s.stopMut.Unlock()
|
||||
t := time.NewTimer(time.Duration(s.cfg.Options().URInitialDelayS) * time.Second)
|
||||
s.stopMut.RLock()
|
||||
defer func() {
|
||||
close(s.stopped)
|
||||
s.stopMut.RUnlock()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-s.stop:
|
||||
@@ -387,14 +400,21 @@ func (s *usageReportingService) VerifyConfiguration(from, to config.Configuratio
|
||||
|
||||
func (s *usageReportingService) CommitConfiguration(from, to config.Configuration) bool {
|
||||
if from.Options.URAccepted != to.Options.URAccepted || from.Options.URUniqueID != to.Options.URUniqueID || from.Options.URURL != to.Options.URURL {
|
||||
s.forceRun <- struct{}{}
|
||||
s.stopMut.RLock()
|
||||
select {
|
||||
case s.forceRun <- struct{}{}:
|
||||
case <-s.stop:
|
||||
}
|
||||
s.stopMut.RUnlock()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *usageReportingService) Stop() {
|
||||
s.stopMut.RLock()
|
||||
close(s.stop)
|
||||
close(s.forceRun)
|
||||
<-s.stopped
|
||||
s.stopMut.RUnlock()
|
||||
}
|
||||
|
||||
func (usageReportingService) String() string {
|
||||
|
||||
@@ -105,7 +105,7 @@ func (s *verboseService) formatEvent(ev events.Event) string {
|
||||
return fmt.Sprintf("Device %v sent an index update for %q with %d items", data["device"], data["folder"], data["items"])
|
||||
|
||||
case events.DeviceRejected:
|
||||
data := ev.Data.(map[string]interface{})
|
||||
data := ev.Data.(map[string]string)
|
||||
return fmt.Sprintf("Rejected connection from device %v at %v", data["device"], data["address"])
|
||||
|
||||
case events.FolderRejected:
|
||||
|
||||
@@ -17,7 +17,7 @@ found in the LICENSE file.
|
||||
<link href="static/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="static/bootstrap/js/bootstrap.min.js"></script>
|
||||
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?libraries=visualization"></script>
|
||||
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?libraries=visualization&key=AIzaSyDk5WJ8s7ueLKb99X5DbQ-vkWtPDAKqYs0"></script>
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 40px;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Desktop Entries
|
||||
|
||||
This directory contains files to integrate Syncthing in your desktop environment (DE).
|
||||
Specifically this works for DEs that implement the [XDG Desktop Menu Specification][1], which
|
||||
is virtually every DE.
|
||||
To add Syncthing to desktop menus for all users, copy the `.desktop` files to
|
||||
`/usr/local/share/applications` (root required). To add it for just your user, copy them to `~/.local/share/applications`.
|
||||
To start Syncthing automatically, you have two options: Either you go to the autostart settings of your DE and choose Syncthing or you copy the `syncthing-start.desktop` file to `~/.config/autostart`.
|
||||
For more information refer to the [ArchWiki page on Desktop entries][2]
|
||||
|
||||
[1]: https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html
|
||||
[2]: https://wiki.archlinux.org/index.php/Desktop_entries
|
||||
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Name=Start Syncthing
|
||||
GenericName=File synchronization
|
||||
Comment=Starts the main syncthing process in the background.
|
||||
Exec=/usr/bin/syncthing -no-browser
|
||||
Icon=syncthing
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;FileTransfer;P2P
|
||||
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Name=Syncthing Web UI
|
||||
GenericName=File synchronization UI
|
||||
Comment="Opens Syncthing's Web UI in the default browser (Syncthing must already be started)."
|
||||
Exec=/usr/bin/syncthing -browser-only
|
||||
Icon=syncthing
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Network;FileTransfer;P2P
|
||||
@@ -110,7 +110,7 @@
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Когато syncthing замени или изтрие файл той се премества в .stversions и преименува с добавяне на дата и час.",
|
||||
"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 synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Промените направени на на други устройства ще бъдат прилагани локално, но локалните промени няма да бъдат синхронизирани с останалите устройства.\n",
|
||||
"Filesystem Notifications": "Известия на системата",
|
||||
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
|
||||
"Filter by date": "Филтриране по дата",
|
||||
@@ -252,7 +252,7 @@
|
||||
"Select oldest version": "Избор на най-старата версия",
|
||||
"Select the devices to share this folder with.": "Изберете устройствата, с които да споделите папката.",
|
||||
"Select the folders to share with this device.": "Изберете папките за споделяне с това устройство.",
|
||||
"Send & Receive": "Изпращане & получаване",
|
||||
"Send & Receive": "Изпращане и получаване",
|
||||
"Send Only": "Само изпращане",
|
||||
"Settings": "Настройки",
|
||||
"Share": "Сподели",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Статистика",
|
||||
"Stopped": "Не се синхронизира",
|
||||
"Support": "Помощ",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Адрес за слушане на синхронизиращия протокол",
|
||||
"Syncing": "Синхронизиране",
|
||||
"Syncthing has been shut down.": "Syncthing е спрян.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estadístiques",
|
||||
"Stopped": "Parat",
|
||||
"Support": "Suport",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Direccions d'escolta del protocol de sincronització",
|
||||
"Syncing": "Sincronitzant",
|
||||
"Syncthing has been shut down.": "Syncthing s'ha apagat",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiky",
|
||||
"Stopped": "Pozastaveno",
|
||||
"Support": "Podpora",
|
||||
"Support Bundle": "Balík podpory",
|
||||
"Sync Protocol Listen Addresses": "Adresa naslouchání synchronizačního protokolu",
|
||||
"Syncing": "Synchronizuje se",
|
||||
"Syncthing has been shut down.": "Syncthing byl vypnut.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistikker",
|
||||
"Stopped": "Stoppet",
|
||||
"Support": "Støt",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Lytteadresser for synkroniseringsprotokol",
|
||||
"Syncing": "Synkroniserer",
|
||||
"Syncthing has been shut down.": "Syncthing er lukket ned.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiken",
|
||||
"Stopped": "Gestoppt",
|
||||
"Support": "Support",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Adresse(n) für das Synchronisierungsprotokoll",
|
||||
"Syncing": "Synchronisiere",
|
||||
"Syncthing has been shut down.": "Syncthing wurde heruntergefahren.",
|
||||
@@ -292,7 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Deiner Internetverbindung. Versuche erneut...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing scheint ein Problem mit der Verarbeitung Deiner Eingabe zu haben. Bitte lade die Seite neu oder führe einen Neustart durch, falls das Problem weiterhin besteht.",
|
||||
"Take me back": "Führe mich zurück",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Die GUI-Adresse wird durch Startoptionen überschrieben. Hier vorgenommene Änderungen werden nicht wirksam, solange die Überschreibung besteht.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Die Syncthing-Oberfläche erlaubt mit den jetzigen Einstellungen einen Zugriff ohne Passwort.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Die gesammelten Statistiken sind öffentlich unter der nachfolgenden URL verfügbar.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber noch nicht aktiviert. Syncthing muss neugestartet werden, um die neue Konfiguration zu übernehmen.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Στατιστικά",
|
||||
"Stopped": "Απενεργοποιημένο",
|
||||
"Support": "Υποστήριξη",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Διευθύνσεις για το πρωτόκολλο συγχρονισμού",
|
||||
"Syncing": "Συγχρονίζω",
|
||||
"Syncthing has been shut down.": "Το Syncthing έχει απενεργοποιηθεί.",
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions directory 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.": "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 synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronised from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Filesystem Notifications": "Filesystem Notifications",
|
||||
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
|
||||
"Filter by date": "Filter by date",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistics",
|
||||
"Stopped": "Stopped",
|
||||
"Support": "Support",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
|
||||
"Syncing": "Syncing",
|
||||
"Syncthing has been shut down.": "Syncthing has been shut down.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estadísticas",
|
||||
"Stopped": "Detenido",
|
||||
"Support": "Forum",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Direcciones de escucha del protocolo de sincronización",
|
||||
"Syncing": "Sincronizando",
|
||||
"Syncthing has been shut down.": "Syncthing se ha detenido.",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"Add Remote Device": "Añadir un dispositivo",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Añadir dispositivos desde el introductor a nuestra lista de dispositivos, para las carpetas compartidas mutuamente.",
|
||||
"Add new folder?": "¿Agregar una carpeta nueva?",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Además, el intervalo de reexploración completo se incrementará (60 veces, es decir, nuevo valor predeterminado de 1h). También puedes configurarlo manualmente para cada carpeta más adelante después de seleccionar No",
|
||||
"Address": "Dirección",
|
||||
"Addresses": "Direcciones",
|
||||
"Advanced": "Avanzado",
|
||||
@@ -23,7 +23,7 @@
|
||||
"Allowed Networks": "Redes permitidas",
|
||||
"Alphabetic": "Alfabético",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder.": "Un comando externo gestiona las versiones. Tiene que eliminar el fichero de la carpeta compartida.",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Un comando externo maneja las versiones. Tienes que eliminar el archivo de la carpeta compartida. Si la ruta a la aplicación contiene espacios, ésta debe estar entre comillas.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Un comando externo controla la versión. El fichero debe ser eliminado de la carpeta sincronizada.",
|
||||
"Anonymous Usage Reporting": "Informe anónimo de uso",
|
||||
"Anonymous usage report format has changed. Would you like to move to the new format?": "El formato del informe de uso anónimo a cambiado. ¿Desearía usar el nuevo formato?",
|
||||
@@ -35,7 +35,7 @@
|
||||
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Ahora la actualización automática permite elegir entre versiones estables o versiones candidatas.",
|
||||
"Automatic upgrades": "Actualizaciones automáticas",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Crear o compartir automáticamente carpetas que este dispositivo anuncia en la ruta por defecto.",
|
||||
"Available debug logging facilities:": "Available debug logging facilities:",
|
||||
"Available debug logging facilities:": "Funciones de registro de depuración disponibles:",
|
||||
"Be careful!": "¡Ten cuidado!",
|
||||
"Bugs": "Errores",
|
||||
"CPU Utilization": "Uso de CPU",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Connection Error": "Error de conexión",
|
||||
"Connection Type": "Tipo de conexión",
|
||||
"Connections": "Conexiones",
|
||||
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.",
|
||||
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Ahora está disponible en Syncthing la búsqueda continua de cambios. Se detectarán los cambios en disco y se hará un escaneado sólo en las rutas modificadas. Los beneficios son que los cambios se propagan más rápido y que se requieren menos escaneos completos.",
|
||||
"Copied from elsewhere": "Copiado de otro sitio",
|
||||
"Copied from original": "Copiado del original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes Colaboradores:",
|
||||
@@ -65,21 +65,21 @@
|
||||
"Device ID": "ID del Dispositivo",
|
||||
"Device Identification": "Identificación del Dispositivo",
|
||||
"Device Name": "Nombre del Dispositivo",
|
||||
"Device rate limits": "Device rate limits",
|
||||
"Device rate limits": "Límites de velocidad del dispositivo",
|
||||
"Device that last modified the item": "Dispositivo que modificó por última vez el ítem",
|
||||
"Devices": "Dispositivos",
|
||||
"Disabled": "Deshabilitado",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Disabled periodic scanning and disabled watching for changes",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Disabled periodic scanning and enabled watching for changes",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
|
||||
"Discard": "Discard",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Se desactivó el escaneo periódico y se desactivó el control de cambios",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Se desactivó el escaneo periódico y se activó el control de cambios",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Se desactivó el escaneo periódico y falló la configuración para detectar cambios, volviendo a intentarlo cada 1 m:",
|
||||
"Discard": "Descartar",
|
||||
"Disconnected": "Desconectado",
|
||||
"Discovered": "Descubierto",
|
||||
"Discovery": "Descubrimiento",
|
||||
"Discovery Failures": "Fallos de Descubrimiento",
|
||||
"Do not restore": "No restaurar",
|
||||
"Do not restore all": "No restaurar todos",
|
||||
"Do you want to enable watching for changes for all your folders?": "Do you want to enable watching for changes for all your folders?",
|
||||
"Do you want to enable watching for changes for all your folders?": "¿Deseas activar el control de cambios en todas tus carpetas?",
|
||||
"Documentation": "Documentación",
|
||||
"Download Rate": "Velocidad de descarga",
|
||||
"Downloaded": "Descargado",
|
||||
@@ -91,7 +91,7 @@
|
||||
"Editing {%path%}.": "Editando {{path}}.",
|
||||
"Enable NAT traversal": "Permitir NAT transversal",
|
||||
"Enable Relaying": "Habilitar Retransmisión",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled": "Activado",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Introduce un número no negativo (por ejemplo, \"2.35\") y selecciona una unidad. Los porcentajes son como parte del tamaño total del disco.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Introduce un puerto sin privilegios (1024 - 65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduzca las direcciones, separadas por comas (\"tcp://ip:port\", \"tcp://host:port\"), o \"dynamic\" para llevar a cabo el descubrimiento automático de la dirección.",
|
||||
@@ -99,8 +99,8 @@
|
||||
"Error": "Error",
|
||||
"External File Versioning": "Versionado externo de fichero",
|
||||
"Failed Items": "Elementos fallidos",
|
||||
"Failed to load ignore patterns": "Failed to load ignore patterns",
|
||||
"Failed to setup, retrying": "Failed to setup, retrying",
|
||||
"Failed to load ignore patterns": "No se cargaron los patrones de ignorar",
|
||||
"Failed to setup, retrying": "Fallo en la configuración, reintentando",
|
||||
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Se espera un fallo al conectar a los servidores IPv6 si no hay conectividad IPv6.",
|
||||
"File Pull Order": "Orden de obtención de los ficheros",
|
||||
"File Versioning": "Versionado de ficheros",
|
||||
@@ -110,9 +110,9 @@
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Los ficheros son movidos a una carpeta .stversions a versiones con control de fecha cuando son 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).",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Los archivos se sincronizan desde el clúster, pero los cambios realizados localmente no se enviarán a otros dispositivos.",
|
||||
"Filesystem Notifications": "Notificaciones del sistema de archivos",
|
||||
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
|
||||
"Filesystem Watcher Errors": "Errores del Vigilante del Sistema de Archivos",
|
||||
"Filter by date": "Filtrar por fecha",
|
||||
"Filter by name": "Filtrar por nombre",
|
||||
"Folder": "Carpeta",
|
||||
@@ -121,8 +121,8 @@
|
||||
"Folder Path": "Ruta de la carpeta",
|
||||
"Folder Type": "Tipo de carpeta",
|
||||
"Folders": "Carpetas",
|
||||
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.",
|
||||
"Full Rescan Interval (s)": "Full Rescan Interval (s)",
|
||||
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "En las siguientes carpetas se ha producido un error al empezar a buscar cambios. Se volverá a intentar cada minuto, por lo que los errores podrían solucionarse pronto. Si persisten, trata de arreglar el problema subyacente y pide ayuda si no puedes.",
|
||||
"Full Rescan Interval (s)": "Intervalo de rescaneo completo (s)",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "Password de la Interfaz Gráfica de Usuario (GUI)",
|
||||
"GUI Authentication User": "Autentificación de usuario de la Interfaz Gráfica de Usuario (GUI)",
|
||||
@@ -140,9 +140,9 @@
|
||||
"Ignore": "Ignorar",
|
||||
"Ignore Patterns": "Patrones a ignorar",
|
||||
"Ignore Permissions": "Permisos a ignorar",
|
||||
"Ignored Devices": "Ignored Devices",
|
||||
"Ignored Folders": "Ignored Folders",
|
||||
"Ignored at": "Ignored at",
|
||||
"Ignored Devices": "Dispositivos ignorados",
|
||||
"Ignored Folders": "Carpetas ignoradas",
|
||||
"Ignored at": "Ignorados en",
|
||||
"Incoming Rate Limit (KiB/s)": "Límite de descarga (KiB/s)",
|
||||
"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.",
|
||||
"Introduced By": "Introducido por",
|
||||
@@ -164,7 +164,7 @@
|
||||
"Local State (Total)": "Estado Local (Total)",
|
||||
"Log": "Registro",
|
||||
"Log tailing paused. Click here to continue.": "Seguimiento del registro pausado. Haga clic aquí para continuar.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Log tailing paused. Scroll to bottom continue.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Registro de cola en pausa. Mueve el cursor hasta la parte inferior para continuar.",
|
||||
"Logs": "Registros",
|
||||
"Major Upgrade": "Actualización importante",
|
||||
"Mass actions": "Acción masiva",
|
||||
@@ -203,11 +203,11 @@
|
||||
"Pause": "Pausar",
|
||||
"Pause All": "Pausar todo",
|
||||
"Paused": "Pausado",
|
||||
"Pending changes": "Pending changes",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:",
|
||||
"Permissions": "Permissions",
|
||||
"Pending changes": "Cambios pendientes",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Escaneando periódicamente a un intervalo dado y detección de cambios desactivada",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Escaneando periódicamente a un intervalo dado y detección de cambios activada",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Escaneando periódicamente a un intervalo dado y falló la configuración para detectar cambios, volviendo a intentarlo cada 1 m:",
|
||||
"Permissions": "Permisos",
|
||||
"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 set a GUI Authentication User and Password in the Settings dialog.": "Por favor, introduzca un Usuario y Contraseña para la Autenticación de la Interfaz de Usuario en el panel de Ajustes.",
|
||||
"Please wait": "Por favor, espere",
|
||||
@@ -218,7 +218,7 @@
|
||||
"Quick guide to supported patterns": "Guía rápida de patrones soportados",
|
||||
"RAM Utilization": "Uso de RAM",
|
||||
"Random": "Aleatorio",
|
||||
"Receive Only": "Receive Only",
|
||||
"Receive Only": "Solo Recibir",
|
||||
"Recent Changes": "Cambios recientes",
|
||||
"Reduced by ignore patterns": "Reducido por patrones de ignorar",
|
||||
"Release Notes": "Notas de la versión",
|
||||
@@ -231,7 +231,7 @@
|
||||
"Rescan": "Volver a analizar",
|
||||
"Rescan All": "Volver a analizar Todo",
|
||||
"Rescan Interval": "Intervalo de análisis",
|
||||
"Rescans": "Rescans",
|
||||
"Rescans": "Reescaneos",
|
||||
"Restart": "Reiniciar",
|
||||
"Restart Needed": "Reinicio necesario",
|
||||
"Restarting": "Reiniciando",
|
||||
@@ -240,8 +240,8 @@
|
||||
"Resume": "Continuar",
|
||||
"Resume All": "Continuar todo",
|
||||
"Reused": "Reutilizado",
|
||||
"Revert Local Changes": "Revert Local Changes",
|
||||
"Running": "Running",
|
||||
"Revert Local Changes": "Revertir Cambios Locales",
|
||||
"Running": "Ejecutando",
|
||||
"Save": "Guardar",
|
||||
"Scan Time Remaining": "Tiempo Restante de Escaneo",
|
||||
"Scanning": "Analizando",
|
||||
@@ -261,7 +261,7 @@
|
||||
"Share With Devices": "Compartir con dispositivos",
|
||||
"Share this folder?": "¿Deseas compartir esta carpeta?",
|
||||
"Shared With": "Compartir con",
|
||||
"Sharing": "Sharing",
|
||||
"Sharing": "Compartiendo",
|
||||
"Show ID": "Mostrar ID",
|
||||
"Show QR": "Mostrar QR",
|
||||
"Show diff with previous version": "Mostrar la diferencia con la versión anterior",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estadísticas",
|
||||
"Stopped": "Detenido",
|
||||
"Support": "Forum",
|
||||
"Support Bundle": "Paquete de Soporte",
|
||||
"Sync Protocol Listen Addresses": "Direcciones de escucha del protocolo de sincronización",
|
||||
"Syncing": "Sincronizando",
|
||||
"Syncthing has been shut down.": "Syncthing se ha detenido.",
|
||||
@@ -291,8 +292,8 @@
|
||||
"Syncthing is upgrading.": "Syncthing se está actualizando.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece no estar activo o hay un problema con tu conexión de internet. Reintentando...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing tiene problemas para procesar tu solicitud. Por favor, actualiza la página o reinicia Syncthing si el problema persiste.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"Take me back": "Llévame de vuelta",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "La dirección de la Interfaz Gráfica de Ususario (GUI) está sobreescrita por las opciones de inicio. Los cambios aquí no tendrán efecto mientras la sobreescritura esté activa.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "El panel de administración de Syncthing está configurado para permitir el acceso remoto sin contraseña.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Las estadísticas agragadas están disponibles públicamente en la URL de abajo.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido grabada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
|
||||
@@ -328,7 +329,7 @@
|
||||
"Unavailable": "No disponible",
|
||||
"Unavailable/Disabled by administrator or maintainer": "No disponible/Deshabilitado por el administrador o mantenedor",
|
||||
"Undecided (will prompt)": "No decidido (se preguntará)",
|
||||
"Unignore": "Unignore",
|
||||
"Unignore": "Dejar de ignorar",
|
||||
"Unknown": "Desconocido",
|
||||
"Unshared": "No compartido",
|
||||
"Unused": "No usado",
|
||||
@@ -349,18 +350,18 @@
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "'Peligro! Esta ruta es un subdirectorio de la carpeta ya existente \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Peligro! Esta ruta es un subdirectorio de una carpeta ya existente llamada \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Peligro, esta ruta es un subdirectorio de una carpeta ya existente llamada \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Warning: If you are using an external watcher like {{syncthingInotify}}, you should make sure it is deactivated.",
|
||||
"Watch for Changes": "Watch for Changes",
|
||||
"Watching for Changes": "Watching for Changes",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Advertencia: Si estás utilizando un observador externo como {{syncthingInotify}}, debes asegurarte de que está desactivado.",
|
||||
"Watch for Changes": "Vigila los cambios",
|
||||
"Watching for Changes": "Vigilando los cambios",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Cuando añada un nuevo dispositivo, tenga en cuenta que este debe añadirse también en el otro lado.",
|
||||
"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 can also select one of these nearby devices:": "También puede seleccionar uno de estos dispositivos cercanos:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Puedes cambiar tu elección en cualquier momento en el panel de Ajustes.",
|
||||
"You can read more about the two release channels at the link below.": "Puedes leer más sobre los dos método de publicación de versiones en el siguiente enlace.",
|
||||
"You have no ignored devices.": "You have no ignored devices.",
|
||||
"You have no ignored folders.": "You have no ignored folders.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
|
||||
"You have no ignored devices.": "No tienes dispositivos ignorados",
|
||||
"You have no ignored folders.": "No tienes carpetas ignoradas",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Tienes cambios sin guardar. ¿Quieres descartarlos realmente?",
|
||||
"You must keep at least one version.": "Debes mantener al menos una versión.",
|
||||
"days": "días",
|
||||
"directories": "directorios",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estatistikak",
|
||||
"Stopped": "Gelditua!",
|
||||
"Support": "Foroa",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Sinkronizatu protokoloaren entzun zuzenbideak",
|
||||
"Syncing": "Sinkronizazioa martxan",
|
||||
"Syncthing has been shut down.": "Syncthing gelditua izan da",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Tilastot",
|
||||
"Stopped": "Pysäytetty",
|
||||
"Support": "Tuki",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Synkronointiprotokollan kuunteluosoite",
|
||||
"Syncing": "Synkronoidaan",
|
||||
"Syncthing has been shut down.": "Syncthing on sammutettu.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiques",
|
||||
"Stopped": "Arrêté",
|
||||
"Support": "Forum",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Adresses d'écoute du protocole de synchronisation",
|
||||
"Syncing": "En cours de synchronisation",
|
||||
"Syncthing has been shut down.": "Syncthing a été arrêté.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiques",
|
||||
"Stopped": "Arrêté",
|
||||
"Support": "Forum",
|
||||
"Support Bundle": "Kit d'assistance",
|
||||
"Sync Protocol Listen Addresses": "Adresses d'écoute du protocole de synchronisation",
|
||||
"Syncing": "Synchronisation en cours",
|
||||
"Syncthing has been shut down.": "Syncthing a été arrêté.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiken",
|
||||
"Stopped": "Stoppe",
|
||||
"Support": "Help (Forum)",
|
||||
"Support Bundle": "Helpbundel",
|
||||
"Sync Protocol Listen Addresses": "Sync-protokolharkadressen",
|
||||
"Syncing": "Oan it Syncen",
|
||||
"Syncthing has been shut down.": "Syncthing is útsetten",
|
||||
@@ -292,7 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "It liket dêrop dat Syncthing op dit stuit net rint, of der is in swierrichheid mei jo ynternetferbining. Wurd no opnij besocht...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "It liket dêrop dat Syncthing swierrichheden ûnderfynt mei it ferwurkjen fan jo fersyk. Graach de stee ferfarskje of Syncthing werstarte as it probleem der bliuwt.",
|
||||
"Take me back": "Bring my werom",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "It ynterfaasje-adres waard oerskreaun troch opstart-opsjes. Feroarings wurde hjir net ynstelt wylst dizze oerskriuw-ynstelling aktyf is.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "De Syncthing haadbrûker-ynterfaasje is sa ynstelt dat tagong fan ôfstân sûnder wachtwurd tastean is.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "De fersammele statistiken binnen yn it publyk beskikber fia ûndersteande keppeling.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De konfiguraasje is bewarre mar noch net aktivearre. Syncthing moat werstarte om de nije konfiguraasje te aktivearren.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statisztika",
|
||||
"Stopped": "Leállítva",
|
||||
"Support": "Támogatás",
|
||||
"Support Bundle": "Támogatási csomag",
|
||||
"Sync Protocol Listen Addresses": "Szinkronizációs protokoll címe",
|
||||
"Syncing": "Szinkronizálás",
|
||||
"Syncthing has been shut down.": "Syncthing leállítva",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiche",
|
||||
"Stopped": "Fermato",
|
||||
"Support": "Supporto",
|
||||
"Support Bundle": "Pacchetto di supporto",
|
||||
"Sync Protocol Listen Addresses": "Indirizzi del Protocollo di Sincronizzazione",
|
||||
"Syncing": "Sincronizzazione in corso",
|
||||
"Syncthing has been shut down.": "Syncthing è stato arrestato.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "統計情報",
|
||||
"Stopped": "停止中",
|
||||
"Support": "サポート",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "同期プロトコルの待ち受けアドレス",
|
||||
"Syncing": "同期中",
|
||||
"Syncthing has been shut down.": "Syncthingをシャットダウンしました。",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "통계",
|
||||
"Stopped": "중지됨",
|
||||
"Support": "지원",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "동기화 프로토콜 수신 주소",
|
||||
"Syncing": "동기화 중",
|
||||
"Syncthing has been shut down.": "Syncthing이 종료되었습니다.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistika",
|
||||
"Stopped": "Sustabdyta",
|
||||
"Support": "Pagalba",
|
||||
"Support Bundle": "Palaikymo paketas",
|
||||
"Sync Protocol Listen Addresses": "Sutapatinimo taisyklių adresas",
|
||||
"Syncing": "Sutapatinama",
|
||||
"Syncthing has been shut down.": "Syncthing išjungtas",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistikk",
|
||||
"Stopped": "Stoppet",
|
||||
"Support": "Brukerstøtte",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Lytteadresse for synkroniseringsprotokoll",
|
||||
"Syncing": "Synkroniserer",
|
||||
"Syncthing has been shut down.": "Syncthing har blitt slått av.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistieken",
|
||||
"Stopped": "Gestopt",
|
||||
"Support": "Ondersteuning",
|
||||
"Support Bundle": "Ondersteuningspakket",
|
||||
"Sync Protocol Listen Addresses": "Luisteradressen synchronisatieprotocol",
|
||||
"Syncing": "Synchroniseren",
|
||||
"Syncthing has been shut down.": "Syncthing werd afgesloten.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statystyki",
|
||||
"Stopped": "Zatrzymany",
|
||||
"Support": "Wsparcie",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Adres nasłuchu protokołu synchronizacji",
|
||||
"Syncing": "Synchronizowanie",
|
||||
"Syncthing has been shut down.": "Syncthing został wyłączony",
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Caminho do diretório onde as versões são salvas (deixe em branco para que seja o diretório padrão .stversions dentro da pasta compartilhada). ",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "O caminho onde as versões serão salvas (deixe vazio para usar a pasta padrão .stversions dentro desta pasta).",
|
||||
"Pause": "Pausar",
|
||||
"Pause All": "Pausar Todas",
|
||||
"Pause All": "Pausar todas",
|
||||
"Paused": "Em pausa",
|
||||
"Pending changes": "Pending changes",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Verificação periódica habilitada no intervalo escolhido. Verificação automática de mudanças desabilitada",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estatísticas",
|
||||
"Stopped": "Parado",
|
||||
"Support": "Suporte",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Endereços de escuta do protocolo de sincronização",
|
||||
"Syncing": "Sincronizando",
|
||||
"Syncthing has been shut down.": "O Syncthing foi desligado.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estatísticas",
|
||||
"Stopped": "Parado",
|
||||
"Support": "Suporte",
|
||||
"Support Bundle": "Pacote de suporte",
|
||||
"Sync Protocol Listen Addresses": "Endereços de escuta do protocolo de sincronização",
|
||||
"Syncing": "A Sincronizar",
|
||||
"Syncthing has been shut down.": "O Syncthing foi desligado.",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"Add Remote Device": "Добавить удалённое устройство",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Add devices from the introducer to our device list, for mutually shared folders.",
|
||||
"Add new folder?": "Добавить новую папку?",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Также будет увеличен интервал полного сканирования (в 60 раз, т.е. новое значение по умолчанию 1 час). Вы можете вручную настроить интервал для каждой папки, выбрав \"Нет\".",
|
||||
"Address": "Адрес",
|
||||
"Addresses": "Адреса",
|
||||
"Advanced": "Дополнительно",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Connection Error": "Ошибка подключения",
|
||||
"Connection Type": "Тип соединения",
|
||||
"Connections": "Подключения",
|
||||
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.",
|
||||
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "В Syncthing теперь доступно непрерывное отслеживание изменений на диске, что позволяет выполнять сканирование только изменившихся путей. Благодаря этому полное сканирование выполняется реже, а изменения распространяются быстрее.",
|
||||
"Copied from elsewhere": "Скопировано из другого места",
|
||||
"Copied from original": "Скопировано с оригинала",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Авторские права © 2014–2016 принадлежат:",
|
||||
@@ -69,8 +69,8 @@
|
||||
"Device that last modified the item": "Устройство, последним изменившее объект",
|
||||
"Devices": "Устройства",
|
||||
"Disabled": "Отключено",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Disabled periodic scanning and disabled watching for changes",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Disabled periodic scanning and enabled watching for changes",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Периодическое сканирование и отслеживание изменений отключено",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Периодическое сканирование отключено, отслеживание изменений включено",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
|
||||
"Discard": "Discard",
|
||||
"Disconnected": "Нет соединения",
|
||||
@@ -78,7 +78,7 @@
|
||||
"Discovery": "Обнаружение",
|
||||
"Discovery Failures": "Ошибки обнаружения",
|
||||
"Do not restore": "Не восстанавливать",
|
||||
"Do not restore all": "Do not restore all",
|
||||
"Do not restore all": "Не восстанавливать все",
|
||||
"Do you want to enable watching for changes for all your folders?": "Хотите включить слежение за изменениями для всех своих папок?",
|
||||
"Documentation": "Документация",
|
||||
"Download Rate": "Скорость загрузки",
|
||||
@@ -92,14 +92,14 @@
|
||||
"Enable NAT traversal": "Включить NAT traversal",
|
||||
"Enable Relaying": "Включить релеи",
|
||||
"Enabled": "Включено",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Введите положительное значение (например, \"2.35\") и выберите единицу измерения, либо процент от общей ёмкости диска.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Введите непривилегированный порт (1024—65535).",
|
||||
"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": "Внешний контроль версий файлов",
|
||||
"Failed Items": "Сбои",
|
||||
"Failed to load ignore patterns": "Failed to load ignore patterns",
|
||||
"Failed to load ignore patterns": "Не удалось загрузить шаблоны игнорирования",
|
||||
"Failed to setup, retrying": "Не удалось настроить, пробуем ещё",
|
||||
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Если нет IPv6-соединений, при подключении к IPv6-серверам произойдёт ошибка.",
|
||||
"File Pull Order": "Порядок получения файлов",
|
||||
@@ -110,7 +110,7 @@
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Когда Syncthing изменяет или удаляет файлы, их версии с таймштампами помещаются в папку .stversions",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Файлы с временнОй меткой версии помещаются в папку .stversions при их замене или удалении 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.": "Файлы защищены от изменений сделанных на других устройствах, но изменения сделанные на этом устройстве будут отправлены всему кластеру.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Файлы синхронизируются из группы, но изменения, сделанные на этом устройстве, не будут отправлены на другие устройства группы.",
|
||||
"Filesystem Notifications": "Уведомления файловой системы",
|
||||
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
|
||||
"Filter by date": "Отфильтровать по дате",
|
||||
@@ -122,7 +122,7 @@
|
||||
"Folder Type": "Тип папки",
|
||||
"Folders": "Папки",
|
||||
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.",
|
||||
"Full Rescan Interval (s)": "Full Rescan Interval (s)",
|
||||
"Full Rescan Interval (s)": "Интервал полного сканирования (в секундах)",
|
||||
"GUI": "Интерфейс",
|
||||
"GUI Authentication Password": "Пароль для доступа к панели управления",
|
||||
"GUI Authentication User": "Имя пользователя для доступа к панели управления",
|
||||
@@ -140,12 +140,12 @@
|
||||
"Ignore": "Игнорировать",
|
||||
"Ignore Patterns": "Шаблоны игнорирования",
|
||||
"Ignore Permissions": "Игнорировать файловые права доступа",
|
||||
"Ignored Devices": "Ignored Devices",
|
||||
"Ignored Folders": "Ignored Folders",
|
||||
"Ignored at": "Ignored at",
|
||||
"Ignored Devices": "Игнорируемые устройства",
|
||||
"Ignored Folders": "Игнорируемые папки",
|
||||
"Ignored at": "Добавлено",
|
||||
"Incoming Rate Limit (KiB/s)": "Ограничение входящей скорости (КиБ/с)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Неправильные настройки могут повредить содержимое папок и сделать Syncthing неработоспособным.",
|
||||
"Introduced By": "Introduced By",
|
||||
"Introduced By": "Рекомендовано",
|
||||
"Introducer": "Рекомендатель",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Инвертировать текущее условие (например, исключить)",
|
||||
"Keep Versions": "Количество хранимых версий",
|
||||
@@ -163,8 +163,8 @@
|
||||
"Local State": "Локальное состояние",
|
||||
"Local State (Total)": "Локальное состояние (всего)",
|
||||
"Log": "Журнал",
|
||||
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Log tailing paused. Scroll to bottom continue.",
|
||||
"Log tailing paused. Click here to continue.": "Вывод журнала приостановлен. Чтобы продолжить, нажмите здесь.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Вывод журнала приостановлен. Чтобы продолжить, прокрутите журнал до конца.",
|
||||
"Logs": "Журналы",
|
||||
"Major Upgrade": "Обновление основной версии",
|
||||
"Mass actions": "Массовые действия",
|
||||
@@ -197,7 +197,7 @@
|
||||
"Override Changes": "Перезаписать изменения",
|
||||
"Path": "Путь",
|
||||
"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 new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
|
||||
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Путь, по которому создаются автоматически принятые папки (также предлагаемый по умолчанию путь при создании новых папок в интерфейсе). Символ тильды (~) разворачивается в {{tilde}}.",
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Путь, в котором нужно хранить версии (оставьте пустым для папки по умолчанию .stversions внутри общей папки).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Путь, где должны храниться версии (оставьте пустым, чтобы использовать папку по умолчанию .stversions внутри папки).",
|
||||
"Pause": "Пауза",
|
||||
@@ -218,7 +218,7 @@
|
||||
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
|
||||
"RAM Utilization": "Использование памяти",
|
||||
"Random": "Случайно",
|
||||
"Receive Only": "Receive Only",
|
||||
"Receive Only": "Только получить",
|
||||
"Recent Changes": "Последние изменения",
|
||||
"Reduced by ignore patterns": "Уменьшено шаблонами игнорирования",
|
||||
"Release Notes": "Примечания к выпуску",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Статистика",
|
||||
"Stopped": "Остановлено",
|
||||
"Support": "Поддержка",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Адрес протокола синхронизации",
|
||||
"Syncing": "Синхронизация",
|
||||
"Syncthing has been shut down.": "Syncthing был выключен.",
|
||||
@@ -292,7 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing столкнулся с проблемой при обработке Вашего запроса. Пожалуйста, обновите страницу или перезапустите Syncthing если проблема повторится.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Эти изменения не вступят в силу, пока адрес панели управления переопределён в настройках запуска.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Административный интерфейс Syncthing настроен для предоставления удаленного доступа без пароля.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Агрегированные статистические данные общедоступны по ссылке ниже.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурация была сохранена, но не активирована. Syncthing должен быть перезапущен для применения новой конфигурации.",
|
||||
@@ -328,7 +329,7 @@
|
||||
"Unavailable": "Недоступно",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
|
||||
"Undecided (will prompt)": "Undecided (will prompt)",
|
||||
"Unignore": "Unignore",
|
||||
"Unignore": "Не игнорировать",
|
||||
"Unknown": "Неизвестно",
|
||||
"Unshared": "Необщедоступно",
|
||||
"Unused": "Не используется",
|
||||
@@ -358,9 +359,9 @@
|
||||
"You can also select one of these nearby devices:": "Вы можете выбрать из этих устройств рядом:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Выбор можно изменить в любой момент в диалоге настроек.",
|
||||
"You can read more about the two release channels at the link below.": "О двух каналах выпусков можно почитать подробнее по нижеприведённой ссылке.",
|
||||
"You have no ignored devices.": "You have no ignored devices.",
|
||||
"You have no ignored folders.": "You have no ignored folders.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
|
||||
"You have no ignored devices.": "У вас нет игнорируемых устройств.",
|
||||
"You have no ignored folders.": "У вас нет игнорируемых папок.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Есть несохранённые изменения. Вы действительно хотите отменить их?",
|
||||
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
|
||||
"days": "дней",
|
||||
"directories": "папок",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Štatistika",
|
||||
"Stopped": "Zastavené",
|
||||
"Support": "Podpora",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Adresa načúvania synchronizačného protokolu",
|
||||
"Syncing": "Synchronizácia",
|
||||
"Syncthing has been shut down.": "Syncthing bol vypnutý.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistik",
|
||||
"Stopped": "Stoppad",
|
||||
"Support": "Support",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Synkroniseringsprotokollets lyssnaradresser",
|
||||
"Syncing": "Synkroniserar",
|
||||
"Syncthing has been shut down.": "Syncthing har stängts.",
|
||||
@@ -292,7 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd eller så är det problem med din Internetanslutning. Försöker igen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem med behandlingen av din begäran. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"Take me back": "Ta mig tillbaka",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Det grafiska gränssnittets adressen åsidosätts av startalternativ. Ändringar här träder inte i kraft så länge åsidosättandet är på plats.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratör gränssnittet är konfigurerat för att tillåta fjärrtillträde utan ett lösenord.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgänglig på webbadressen nedan.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"Add Remote Device": "Додати віддалений пристрій",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Додати пристрої від пристрою-рекомендувача до нашого списку пристроїв для спільно розділених директорій.",
|
||||
"Add new folder?": "Додати нову директорію?",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Крім того, буде збільшений інтервал повного сканування (у 60 разів, тобто нове значення за замовчанням - 1 година). Ви також можете налаштувати його вручну для кожної папки пізніше після вибору \"Ні\".",
|
||||
"Address": "Адреса",
|
||||
"Addresses": "Адреси",
|
||||
"Advanced": "Розширені",
|
||||
@@ -23,7 +23,7 @@
|
||||
"Allowed Networks": "Дозволені мережі",
|
||||
"Alphabetic": "За алфавітом",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder.": "Зовнішня команда керування версіями. Вона має видалити файл із спільної директорії.",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Зовнішня команда керування версіями. Вона має видалити файл із спільної директорії. Якщо шлях до програми містить пробіли, він буде взятий у лапки.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Зовнішня команда керування версіями. Вона має видалити файл із директорії, що синхронізується.",
|
||||
"Anonymous Usage Reporting": "Анонімна статистика використання",
|
||||
"Anonymous usage report format has changed. Would you like to move to the new format?": "Змінився формат анонімного звіту про користування. Бажаєте перейти на новий формат?",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Connection Error": "Помилка з’єднання",
|
||||
"Connection Type": "Тип з*єднання",
|
||||
"Connections": "З'єднання",
|
||||
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.",
|
||||
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Постійне стеження за змінами наразі доступне у Syncthing. Це дозволить виявити зміни на диску та сканувати тільки модифіковані шляхи. Переваги полягають у тому, що зміни поширюються швидше і зменшується кількість повних пересканувань.",
|
||||
"Copied from elsewhere": "Скопійовано з іншого місця",
|
||||
"Copied from original": "Скопійовано з оригіналу",
|
||||
"Copyright © 2014-2016 the following Contributors:": "© 2014-2016 Всі права застережено, вклад внесли:",
|
||||
@@ -65,21 +65,21 @@
|
||||
"Device ID": "ID пристрою",
|
||||
"Device Identification": "Ідентифікатор пристрою",
|
||||
"Device Name": "Назва пристрою",
|
||||
"Device rate limits": "Device rate limits",
|
||||
"Device rate limits": "Обмеження пристрою",
|
||||
"Device that last modified the item": "Пристрій, що останнім змінив елемент",
|
||||
"Devices": "Пристрої",
|
||||
"Disabled": "Вимкнено",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Disabled periodic scanning and disabled watching for changes",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Disabled periodic scanning and enabled watching for changes",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
|
||||
"Discard": "Discard",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Відключено періодичне сканування та відключено відстеження змін",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Відключено періодичне сканування та увімкнене стеження за змінами",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Відключено періодичне сканування та не вдається налаштувати перегляд змін, повторення кожну 1 хв:",
|
||||
"Discard": "Відхилити",
|
||||
"Disconnected": "З’єднання відсутнє",
|
||||
"Discovered": "Виявлено",
|
||||
"Discovery": "Сервери координації NAT",
|
||||
"Discovery Failures": "Помилки виявлення",
|
||||
"Do not restore": "Не відновлювати",
|
||||
"Do not restore all": "Не відновлювати все",
|
||||
"Do you want to enable watching for changes for all your folders?": "Do you want to enable watching for changes for all your folders?",
|
||||
"Do you want to enable watching for changes for all your folders?": "Бажаєте увімкнути стеження за змінами у всіх ваших папках?",
|
||||
"Documentation": "Документація",
|
||||
"Download Rate": "Швидкість завантаження",
|
||||
"Downloaded": "Завантажено",
|
||||
@@ -110,9 +110,9 @@
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Файли будуть поміщатися у директорію .stversions із відповідною позначкою часу, коли вони будуть замінятися або видалятися програмою.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Файли будуть поміщатися у директорію .stversions із відповідною позначкою часу, коли вони будуть замінятися або видалятися програмою.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Вміст папки захищено від змін, зроблених на інших пристроях, але зміни зроблені на цьому пристрої можна розіслати решті пристроїв кластеру.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Файли синхронізуються з кластера, але будь-які внесені локально зміни не надсилатимуться на інші пристрої.",
|
||||
"Filesystem Notifications": "Повідомлення файлової системи",
|
||||
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
|
||||
"Filesystem Watcher Errors": "Помилки спостерігача файлової системи",
|
||||
"Filter by date": "Фільтрувати по даті",
|
||||
"Filter by name": "Фільтрувати по імені",
|
||||
"Folder": "Директорія",
|
||||
@@ -121,7 +121,7 @@
|
||||
"Folder Path": "Шлях до директорії",
|
||||
"Folder Type": "Тип директорії",
|
||||
"Folders": "Директорії",
|
||||
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.",
|
||||
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "Сталася помилка при спробі відслідковувати зміни у вищенаведених папках. Їх доступність перевірятиметься щохвилини, доки помилка не зникне. Якщо помилки не зникають, спробуйте виправити права доступу або попросіть допомоги.",
|
||||
"Full Rescan Interval (s)": "Інтервал повного пересканування (секунди)",
|
||||
"GUI": "Графічний інтерфейс",
|
||||
"GUI Authentication Password": "Пароль для доступу до панелі управління",
|
||||
@@ -140,9 +140,9 @@
|
||||
"Ignore": "Ігнорувати",
|
||||
"Ignore Patterns": "Шаблони винятків",
|
||||
"Ignore Permissions": "Ігнорувати права доступу до файлів",
|
||||
"Ignored Devices": "Ignored Devices",
|
||||
"Ignored Folders": "Ignored Folders",
|
||||
"Ignored at": "Ignored at",
|
||||
"Ignored Devices": "Ігноровані пристрох",
|
||||
"Ignored Folders": "Ігноровані папки",
|
||||
"Ignored at": "Ігноруються в",
|
||||
"Incoming Rate Limit (KiB/s)": "Ліміт швидкості завантаження (КіБ/с)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Невірна конфігурація може пошкодити вміст вашої директорії та зробити Syncthing недієздатним.",
|
||||
"Introduced By": "Введено",
|
||||
@@ -164,7 +164,7 @@
|
||||
"Local State (Total)": "Локальний статус (загалом)",
|
||||
"Log": "Журнал",
|
||||
"Log tailing paused. Click here to continue.": "Перемотка журналу призупинена. Натиснути для продовження.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Log tailing paused. Scroll to bottom continue.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Висвітлення журналу призупинене. Прокрутіть нижче, щоби продовжити.",
|
||||
"Logs": "Журнали",
|
||||
"Major Upgrade": "Мажорне оновлення",
|
||||
"Mass actions": "Масові операції",
|
||||
@@ -203,7 +203,7 @@
|
||||
"Pause": "Пауза",
|
||||
"Pause All": "Призупинити все",
|
||||
"Paused": "Призупинено",
|
||||
"Pending changes": "Pending changes",
|
||||
"Pending changes": "Запит на зміни поставлено в чергу",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:",
|
||||
@@ -218,7 +218,7 @@
|
||||
"Quick guide to supported patterns": "Швидкий посібник по шаблонам, що підтримуються",
|
||||
"RAM Utilization": "Використання RAM",
|
||||
"Random": "Випадково",
|
||||
"Receive Only": "Receive Only",
|
||||
"Receive Only": "Тільки отримувати",
|
||||
"Recent Changes": "Останні зміни",
|
||||
"Reduced by ignore patterns": "Зменшено шаблонами ігнорування",
|
||||
"Release Notes": "Примітки до випуску",
|
||||
@@ -231,7 +231,7 @@
|
||||
"Rescan": "Пересканувати",
|
||||
"Rescan All": "Пересканувати усе",
|
||||
"Rescan Interval": "Інтервал для повторного сканування",
|
||||
"Rescans": "Rescans",
|
||||
"Rescans": "Пересканування",
|
||||
"Restart": "Перезапуск",
|
||||
"Restart Needed": "Необхідний перезапуск",
|
||||
"Restarting": "Відбувається перезапуск",
|
||||
@@ -240,7 +240,7 @@
|
||||
"Resume": "Продовжити",
|
||||
"Resume All": "Продовжити всі",
|
||||
"Reused": "Використано вдруге",
|
||||
"Revert Local Changes": "Revert Local Changes",
|
||||
"Revert Local Changes": "Інвертувати локальні зміни",
|
||||
"Running": "Running",
|
||||
"Save": "Зберегти",
|
||||
"Scan Time Remaining": "Час до кінця сканування",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Статистика",
|
||||
"Stopped": "Зупинено",
|
||||
"Support": "Підтримка",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Адреса і вхідний порт протоколу синхронізації",
|
||||
"Syncing": "Синхронізація",
|
||||
"Syncthing has been shut down.": "Syncthing вимкнено (закрито).",
|
||||
@@ -350,17 +351,17 @@
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Увага, цей шлях є підпапкою директорії \"{{otherFolder}}\", що й так синхронізується .",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Увага, цей шлях є підпапкою директорії \"{{otherFolderLabel}}\", що й так синхронізується ({{otherFolder}}).",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Warning: If you are using an external watcher like {{syncthingInotify}}, you should make sure it is deactivated.",
|
||||
"Watch for Changes": "Watch for Changes",
|
||||
"Watching for Changes": "Watching for Changes",
|
||||
"Watch for Changes": "Моніторити зміни",
|
||||
"Watching for Changes": "Моніторинг щмін",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Коли додаєте новий вузол, пам’ятайте, що цей вузол повинен бути доданий і на іншій стороні.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Коли додаєте нову директорію, пам’ятайте, що ID цієї директорії використовується для того, щоб зв’язувати директорії разом між пристроями. Назви повинні точно співпадати між усіма пристроями, регістр символів має значення.",
|
||||
"Yes": "Так",
|
||||
"You can also select one of these nearby devices:": "Ви також можете обрати один із сусідніх пристроїв:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Ви завжди можете змінити свій вибір у вікні Налаштувань.",
|
||||
"You can read more about the two release channels at the link below.": "Ви можете прочитати більше про два канали випусків за посиланням нижче.",
|
||||
"You have no ignored devices.": "You have no ignored devices.",
|
||||
"You have no ignored folders.": "You have no ignored folders.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
|
||||
"You have no ignored devices.": "Немає ігнорованих пристроїв",
|
||||
"You have no ignored folders.": "Немає ігнорованих папок",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Внесені зміни не збережено, чи дійсно відмовитись від змін?",
|
||||
"You must keep at least one version.": "Ви повинні зберігати щонайменше одну версію.",
|
||||
"days": "днів",
|
||||
"directories": "директорії",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "统计",
|
||||
"Stopped": "已停止",
|
||||
"Support": "支持",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "协议监听地址",
|
||||
"Syncing": "同步中",
|
||||
"Syncthing has been shut down.": "Syncthing 已关闭。",
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"Device ID": "裝置識別碼",
|
||||
"Device Identification": "裝置識別",
|
||||
"Device Name": "裝置名稱",
|
||||
"Device rate limits": "Device rate limits",
|
||||
"Device rate limits": "裝置速率限制",
|
||||
"Device that last modified the item": "前次修改裝置",
|
||||
"Devices": "裝置",
|
||||
"Disabled": "停用",
|
||||
@@ -112,7 +112,7 @@
|
||||
"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 synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Filesystem Notifications": "檔案系統通知",
|
||||
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
|
||||
"Filesystem Watcher Errors": "檔案系統監視器錯誤\n",
|
||||
"Filter by date": "以日期篩選",
|
||||
"Filter by name": "以名稱篩選",
|
||||
"Folder": "資料夾",
|
||||
@@ -218,7 +218,7 @@
|
||||
"Quick guide to supported patterns": "可支援樣式的快速指南",
|
||||
"RAM Utilization": "記憶體使用量",
|
||||
"Random": "隨機",
|
||||
"Receive Only": "Receive Only",
|
||||
"Receive Only": "僅接收\n",
|
||||
"Recent Changes": "最近變動",
|
||||
"Reduced by ignore patterns": "已由忽略樣式縮減",
|
||||
"Release Notes": "版本資訊",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "統計",
|
||||
"Stopped": "已停止",
|
||||
"Support": "支援",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "同步通訊協定監聽位址",
|
||||
"Syncing": "正在同步",
|
||||
"Syncthing has been shut down.": "Syncthing 已經關閉。",
|
||||
@@ -358,8 +359,8 @@
|
||||
"You can also select one of these nearby devices:": "您亦可從這些附近裝置中擇一:",
|
||||
"You can change your choice at any time in the Settings dialog.": "您可以在設定對話框中隨時更改您的選擇。",
|
||||
"You can read more about the two release channels at the link below.": "您可於下方連結閱讀更多關於發行頻道的說明。",
|
||||
"You have no ignored devices.": "You have no ignored devices.",
|
||||
"You have no ignored folders.": "You have no ignored folders.",
|
||||
"You have no ignored devices.": "您沒有已忽略的裝置。\n",
|
||||
"You have no ignored folders.": "您沒有已忽略的資料夾。\n",
|
||||
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
|
||||
"You must keep at least one version.": "您必須保留至少一個版本。",
|
||||
"days": "日",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<p translate>Copyright © 2014-2017 the following Contributors:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, Aaron Bieber, Adam Piggott, Adel Qalieh, Alessandro G., Andrew Dunham, Andrew Rabert, Andrey D, Antoine Lamielle, Aranjedeath, Arthur Axel fREW Schmidt, BAHADIR YILMAZ, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benny Ng, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Chris Tonkinson, Colin Kennedy, Dale Visser, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Graham Miln, Han Boetes, Harrison Jones, Heiko Zuerker, Iain Barnett, Ian Johnson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonathan Cross, Jose Manuel Delicado, Karol Różycki, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Kurt Fitzner, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mark Pulford, Mateusz Naściszewski, Matic Potočnik, Matt Burke, Matteo Ruina, Max Schulze, MaximAL, Maxime Thirouin, Michael Jephcote, Michael Tilli, Mike Boone, MikeLund, Nicholas Rishel, Nicolas Braud-Santoni, Niels Peter Roest, Nils Jakobi, NoLooseEnds, Oyebanji Jacob Mayowa, Pascal Jungblut, Pawel Palenica, Paweł Rozlach, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Richard Hartmann, Robert Carosi, Roman Zaynetdinov, Ross Smith II, Sacheendra Talluri, Scott Klupfel, Sly_tom_cat, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tomas Cerveny, Tommy Thorn, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, Vladimir Rusinov, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, chucic, derekriemer, janost, jaseg, klemens, marco-m, perewa, rubenbe, wangguoliang, xjtdy888, 佛跳墙
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, Aaron Bieber, Adam Piggott, Adel Qalieh, Alessandro G., Andrew Dunham, Andrew Rabert, Andrey D, Antoine Lamielle, Aranjedeath, Arthur Axel fREW Schmidt, BAHADIR YILMAZ, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benno Fünfstück, Benny Ng, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Chris Tonkinson, Colin Kennedy, Dale Visser, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Graham Miln, Han Boetes, Harrison Jones, Heiko Zuerker, Iain Barnett, Ian Johnson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonathan Cross, Jose Manuel Delicado, Karol Różycki, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Kurt Fitzner, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mark Pulford, Mateusz Naściszewski, Matic Potočnik, Matt Burke, Matteo Ruina, Max Schulze, MaximAL, Maxime Thirouin, Michael Jephcote, Michael Tilli, Mike Boone, MikeLund, Nicholas Rishel, Nico Stapelbroek, Nicolas Braud-Santoni, Niels Peter Roest, Nils Jakobi, NoLooseEnds, Oyebanji Jacob Mayowa, Pascal Jungblut, Pawel Palenica, Paweł Rozlach, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Richard Hartmann, Robert Carosi, Roman Zaynetdinov, Ross Smith II, Sacheendra Talluri, Scott Klupfel, Sly_tom_cat, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tomas Cerveny, Tommy Thorn, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, Vladimir Rusinov, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, chucic, derekriemer, janost, jaseg, klemens, marco-m, perewa, rubenbe, wangguoliang, xjtdy888, 佛跳墙
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
@@ -1276,11 +1276,17 @@ angular.module('syncthing.core')
|
||||
}
|
||||
|
||||
// Apply new settings locally
|
||||
$scope.thisDevice().name = $scope.tmpOptions.deviceName;
|
||||
$scope.thisDeviceIn($scope.tmpDevices).name = $scope.tmpOptions.deviceName;
|
||||
$scope.config.options = angular.copy($scope.tmpOptions);
|
||||
$scope.config.gui = angular.copy($scope.tmpGUI);
|
||||
$scope.config.remoteIgnoredDevices = angular.copy($scope.tmpRemoteIgnoredDevices);
|
||||
$scope.config.devices = angular.copy($scope.tmpDevices);
|
||||
// $scope.devices is updated by updateLocalConfig based on
|
||||
// the config changed event, but settingsModified will look
|
||||
// at it before that and conclude that the settings are
|
||||
// modified (even though we just saved) unless we update
|
||||
// here as well...
|
||||
$scope.devices = $scope.config.devices;
|
||||
|
||||
['listenAddresses', 'globalAnnounceServers'].forEach(function (key) {
|
||||
$scope.config.options[key] = $scope.config.options["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
|
||||
@@ -1365,6 +1371,20 @@ angular.module('syncthing.core')
|
||||
$('#editDevice').modal();
|
||||
};
|
||||
|
||||
$scope.selectAllFolders = function() {
|
||||
Object.entries($scope.folders).forEach(entry =>{
|
||||
let id = entry[1].id;
|
||||
$scope.currentDevice.selectedFolders[id] = true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deSelectAllFolders = function() {
|
||||
Object.entries($scope.folders).forEach(entry =>{
|
||||
let id = entry[1].id;
|
||||
$scope.currentDevice.selectedFolders[id] = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addDevice = function (deviceID, name) {
|
||||
return $http.get(urlbase + '/system/discovery')
|
||||
.success(function (registry) {
|
||||
@@ -1509,8 +1529,12 @@ angular.module('syncthing.core')
|
||||
};
|
||||
|
||||
$scope.thisDevice = function () {
|
||||
for (var i = 0; i < $scope.devices.length; i++) {
|
||||
var n = $scope.devices[i];
|
||||
return $scope.thisDeviceIn($scope.devices);
|
||||
}
|
||||
|
||||
$scope.thisDeviceIn = function (l) {
|
||||
for (var i = 0; i < l.length; i++) {
|
||||
var n = l[i];
|
||||
if (n.deviceID === $scope.myID) {
|
||||
return n;
|
||||
}
|
||||
@@ -1684,6 +1708,20 @@ angular.module('syncthing.core')
|
||||
$scope.editFolderModal();
|
||||
};
|
||||
|
||||
$scope.selectAllDevices = function() {
|
||||
var devices = $scope.otherDevices();
|
||||
for (var i = 0; i < devices.length; i++){
|
||||
$scope.currentFolder.selectedDevices[devices[i].deviceID] = true;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.deSelectAllDevices = function() {
|
||||
var devices = $scope.otherDevices();
|
||||
for (var i = 0; i < devices.length; i++){
|
||||
$scope.currentFolder.selectedDevices[devices[i].deviceID] = false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.addFolder = function () {
|
||||
$http.get(urlbase + '/svc/random/string?length=10').success(function (data) {
|
||||
$scope.editingExisting = false;
|
||||
@@ -1958,114 +1996,116 @@ angular.module('syncthing.core')
|
||||
});
|
||||
|
||||
$q.all([dataReceived, modalShown.promise]).then(function() {
|
||||
if (closed) {
|
||||
resetRestoreVersions();
|
||||
return;
|
||||
}
|
||||
$timeout(function(){
|
||||
if (closed) {
|
||||
resetRestoreVersions();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.restoreVersions.tree = $("#restoreTree").fancytree({
|
||||
extensions: ["table", "filter"],
|
||||
quicksearch: true,
|
||||
filter: {
|
||||
autoApply: true,
|
||||
counter: true,
|
||||
hideExpandedCounter: true,
|
||||
hideExpanders: true,
|
||||
highlight: true,
|
||||
leavesOnly: false,
|
||||
nodata: true,
|
||||
mode: "hide"
|
||||
},
|
||||
table: {
|
||||
indentation: 20,
|
||||
nodeColumnIdx: 0,
|
||||
},
|
||||
debugLevel: 2,
|
||||
source: buildTree($scope.restoreVersions.versions),
|
||||
renderColumns: function(event, data) {
|
||||
var node = data.node,
|
||||
$tdList = $(node.tr).find(">td"),
|
||||
template;
|
||||
if (node.folder) {
|
||||
template = '<div ng-include="\'syncthing/folder/restoreVersionsMassActions.html\'" class="pull-right"/>';
|
||||
} else {
|
||||
template = '<div ng-include="\'syncthing/folder/restoreVersionsVersionSelector.html\'" class="pull-right"/>';
|
||||
$scope.restoreVersions.tree = $("#restoreTree").fancytree({
|
||||
extensions: ["table", "filter"],
|
||||
quicksearch: true,
|
||||
filter: {
|
||||
autoApply: true,
|
||||
counter: true,
|
||||
hideExpandedCounter: true,
|
||||
hideExpanders: true,
|
||||
highlight: true,
|
||||
leavesOnly: false,
|
||||
nodata: true,
|
||||
mode: "hide"
|
||||
},
|
||||
table: {
|
||||
indentation: 20,
|
||||
nodeColumnIdx: 0,
|
||||
},
|
||||
debugLevel: 2,
|
||||
source: buildTree($scope.restoreVersions.versions),
|
||||
renderColumns: function(event, data) {
|
||||
var node = data.node,
|
||||
$tdList = $(node.tr).find(">td"),
|
||||
template;
|
||||
if (node.folder) {
|
||||
template = '<div ng-include="\'syncthing/folder/restoreVersionsMassActions.html\'" class="pull-right"/>';
|
||||
} else {
|
||||
template = '<div ng-include="\'syncthing/folder/restoreVersionsVersionSelector.html\'" class="pull-right"/>';
|
||||
}
|
||||
|
||||
var scope = $rootScope.$new(true);
|
||||
scope.key = node.key;
|
||||
scope.restoreVersions = $scope.restoreVersions;
|
||||
|
||||
$tdList.eq(1).html(
|
||||
$compile(template)(scope)
|
||||
);
|
||||
|
||||
// Force angular to redraw.
|
||||
$timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
}
|
||||
}).fancytree("getTree");
|
||||
|
||||
var scope = $rootScope.$new(true);
|
||||
scope.key = node.key;
|
||||
scope.restoreVersions = $scope.restoreVersions;
|
||||
var minDate = moment(),
|
||||
maxDate = moment(0, 'X'),
|
||||
date;
|
||||
|
||||
$tdList.eq(1).html(
|
||||
$compile(template)(scope)
|
||||
);
|
||||
// Find version window.
|
||||
$.each($scope.restoreVersions.versions, function(key) {
|
||||
$.each($scope.restoreVersions.versions[key], function(idx, version) {
|
||||
date = moment(version.versionTime);
|
||||
if (date.isBefore(minDate)) {
|
||||
minDate = date;
|
||||
}
|
||||
if (date.isAfter(maxDate)) {
|
||||
maxDate = date;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Force angular to redraw.
|
||||
$scope.restoreVersions.filters['start'] = minDate;
|
||||
$scope.restoreVersions.filters['end'] = maxDate;
|
||||
|
||||
var ranges = {
|
||||
'All time': [minDate, maxDate],
|
||||
'Today': [moment(), moment()],
|
||||
'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
|
||||
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
|
||||
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
|
||||
'This Month': [moment().startOf('month'), moment().endOf('month')],
|
||||
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
|
||||
};
|
||||
|
||||
// Filter out invalid ranges.
|
||||
$.each(ranges, function(key, range) {
|
||||
if (!range[0].isBetween(minDate, maxDate, null, '[]') && !range[1].isBetween(minDate, maxDate, null, '[]')) {
|
||||
delete ranges[key];
|
||||
}
|
||||
});
|
||||
|
||||
$("#restoreVersionDateRange").daterangepicker({
|
||||
timePicker: true,
|
||||
timePicker24Hour: true,
|
||||
timePickerSeconds: true,
|
||||
autoUpdateInput: true,
|
||||
opens: "left",
|
||||
drops: "up",
|
||||
startDate: minDate,
|
||||
endDate: maxDate,
|
||||
minDate: minDate,
|
||||
maxDate: maxDate,
|
||||
ranges: ranges,
|
||||
locale: {
|
||||
format: 'YYYY/MM/DD HH:mm:ss',
|
||||
}
|
||||
}).on('apply.daterangepicker', function(ev, picker) {
|
||||
$scope.restoreVersions.filters['start'] = picker.startDate;
|
||||
$scope.restoreVersions.filters['end'] = picker.endDate;
|
||||
// Events for this UI element are not managed by angular.
|
||||
// Force angular to wake up.
|
||||
$timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
}
|
||||
}).fancytree("getTree");
|
||||
|
||||
var minDate = moment(),
|
||||
maxDate = moment(0, 'X'),
|
||||
date;
|
||||
|
||||
// Find version window.
|
||||
$.each($scope.restoreVersions.versions, function(key) {
|
||||
$.each($scope.restoreVersions.versions[key], function(idx, version) {
|
||||
date = moment(version.versionTime);
|
||||
if (date.isBefore(minDate)) {
|
||||
minDate = date;
|
||||
}
|
||||
if (date.isAfter(maxDate)) {
|
||||
maxDate = date;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.restoreVersions.filters['start'] = minDate;
|
||||
$scope.restoreVersions.filters['end'] = maxDate;
|
||||
|
||||
var ranges = {
|
||||
'All time': [minDate, maxDate],
|
||||
'Today': [moment(), moment()],
|
||||
'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
|
||||
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
|
||||
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
|
||||
'This Month': [moment().startOf('month'), moment().endOf('month')],
|
||||
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
|
||||
};
|
||||
|
||||
// Filter out invalid ranges.
|
||||
$.each(ranges, function(key, range) {
|
||||
if (!range[0].isBetween(minDate, maxDate, null, '[]') && !range[1].isBetween(minDate, maxDate, null, '[]')) {
|
||||
delete ranges[key];
|
||||
}
|
||||
});
|
||||
|
||||
$("#restoreVersionDateRange").daterangepicker({
|
||||
timePicker: true,
|
||||
timePicker24Hour: true,
|
||||
timePickerSeconds: true,
|
||||
autoUpdateInput: true,
|
||||
opens: "left",
|
||||
drops: "up",
|
||||
startDate: minDate,
|
||||
endDate: maxDate,
|
||||
minDate: minDate,
|
||||
maxDate: maxDate,
|
||||
ranges: ranges,
|
||||
locale: {
|
||||
format: 'YYYY/MM/DD HH:mm:ss',
|
||||
}
|
||||
}).on('apply.daterangepicker', function(ev, picker) {
|
||||
$scope.restoreVersions.filters['start'] = picker.startDate;
|
||||
$scope.restoreVersions.filters['end'] = picker.endDate;
|
||||
// Events for this UI element are not managed by angular.
|
||||
// Force angular to wake up.
|
||||
$timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<p translate ng-if="currentDevice.deviceID != myID" class="help-block">Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="device-sharing">
|
||||
<div id="device-sharing" class="tab-pane">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
@@ -67,7 +67,11 @@
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label translate for="folders">Share Folders With Device</label>
|
||||
<p translate class="help-block">Select the folders to share with this device.</p>
|
||||
<p class="help-block">
|
||||
<span translate>Select the folders to share with this device.</span> 
|
||||
<small><a href="#" ng-click="selectAllFolders()" translate>Select All</a> 
|
||||
<a href="#" ng-click="deSelectAllFolders()" translate>Deselect All</a></small>
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4" ng-repeat="folder in folderList()">
|
||||
<div class="checkbox">
|
||||
@@ -84,7 +88,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="device-advanced">
|
||||
<div id="device-advanced" class="tab-pane">
|
||||
<div class="row form-group">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<form role="form" name="folderEditor">
|
||||
<ul class="nav nav-tabs" ng-init="loadFormIntoScope(folderEditor)">
|
||||
<li class="active"><a data-toggle="tab" href="#folder-general"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
|
||||
<li><a data-toggle="tab" href="#folder-sharing"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
|
||||
<li><a data-toggle="tab" href="#folder-versioning"><span class="fas fa-copy"></span> <span translate>File Versioning</span></a></li>
|
||||
<li><a data-toggle="tab" href="#folder-ignores"><span class="fas fa-filter"></span> <span translate>Ignore Patterns</span></a></li>
|
||||
<li><a data-toggle="tab" href="#folder-advanced"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
|
||||
@@ -23,6 +24,7 @@
|
||||
<span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Required identifier for the folder. Must be the same on all cluster devices.</span>
|
||||
<span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
|
||||
<span translate ng-if="folderEditor.folderID.$error.required && folderEditor.folderID.$dirty">The folder ID cannot be blank.</span>
|
||||
<span translate ng-show="!editingExisting">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.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty}">
|
||||
@@ -40,9 +42,15 @@
|
||||
<span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" translate-value-other-folder-label="{{folderPathErrors.otherLabel}}" ng-if="folderPathErrors.isParent && folderPathErrors.otherLabel.length != 0">Warning, this path is a parent directory of an existing folder "{%otherFolderLabel%}" ({%otherFolder%}).</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="folder-sharing" class="tab-pane">
|
||||
<div class="form-group">
|
||||
<label translate for="devices">Share With Devices</label>
|
||||
<p translate class="help-block">Select the devices to share this folder with.</p>
|
||||
<p class="help-block">
|
||||
<span translate>Select the devices to share this folder with.</span> 
|
||||
<small><a href="#" ng-click="selectAllDevices()" translate>Select All</a> 
|
||||
<a href="#" ng-click="deSelectAllDevices()" translate>Deselect All</a></small>
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4" ng-repeat="device in otherDevices()">
|
||||
<div class="checkbox">
|
||||
@@ -53,7 +61,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div translate ng-show="!editingExisting" class="help-block">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.</div>
|
||||
</div>
|
||||
<div id="folder-versioning" class="tab-pane">
|
||||
<div class="form-group">
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate for="urVersion">Anonymous Usage Reporting</label> (<a href="" translate data-toggle="modal" data-target="#urPreview">Preview</a>)
|
||||
<div ng-if="tmpOptions.upgrades != 'candidate'">
|
||||
<label translate for="urVersion">Anonymous Usage Reporting</label> (<a href="" translate data-toggle="modal" data-target="#urPreview">Preview</a>)
|
||||
<select class="form-control" id="urVersion" ng-model="tmpOptions._urAcceptedStr">
|
||||
<option ng-repeat="n in urVersions()" value="{{n}}">{{'Version' | translate}} {{n}}</option>
|
||||
<!-- 1 does not exist, as we did not support incremental formats back then. -->
|
||||
@@ -79,7 +79,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<p class="help-block" ng-if="tmpOptions.upgrades == 'candidate'">
|
||||
<span translate>Usage reporting is always enabled for candidate releases.</span> (<a href="" translate data-toggle="modal" data-target="#urPreview">Preview</a>)
|
||||
<span translate>Usage reporting is always enabled for candidate releases.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@ type DeviceConfiguration struct {
|
||||
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
|
||||
IgnoredFolders []ObservedFolder `xml:"ignoredFolder" json:"ignoredFolders"`
|
||||
PendingFolders []ObservedFolder `xml:"pendingFolder" json:"pendingFolders"`
|
||||
MaxRequestKiB int `xml:"maxRequestKiB" json:"maxRequestKiB"`
|
||||
}
|
||||
|
||||
func NewDeviceConfiguration(id protocol.DeviceID, name string) DeviceConfiguration {
|
||||
|
||||
@@ -269,6 +269,10 @@ func (f *FolderConfiguration) SharedWith(device protocol.DeviceID) bool {
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) CheckAvailableSpace(req int64) error {
|
||||
val := f.MinDiskFree.BaseValue()
|
||||
if val <= 0 {
|
||||
return nil
|
||||
}
|
||||
fs := f.Filesystem()
|
||||
usage, err := fs.Usage(".")
|
||||
if err != nil {
|
||||
|
||||
@@ -245,7 +245,9 @@ next:
|
||||
|
||||
deviceCfg, ok := s.cfg.Device(remoteID)
|
||||
if !ok {
|
||||
panic("bug: unknown device should already have been rejected")
|
||||
l.Infof("Device %s removed from config during connection attempt at %s", remoteID, c)
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify the name on the certificate. By default we set it to
|
||||
|
||||
@@ -45,7 +45,7 @@ func lazyInitBenchFileSet() {
|
||||
replace(benchS, protocol.LocalDeviceID, firstHalf)
|
||||
}
|
||||
|
||||
func tempDB() (*db.Instance, string) {
|
||||
func tempDB() (*db.Lowlevel, string) {
|
||||
dir, err := ioutil.TempDir("", "syncthing")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
+5
-5
@@ -22,14 +22,14 @@ var blockFinder *BlockFinder
|
||||
const maxBatchSize = 1000
|
||||
|
||||
type BlockMap struct {
|
||||
db *Instance
|
||||
db *Lowlevel
|
||||
folder uint32
|
||||
}
|
||||
|
||||
func NewBlockMap(db *Instance, folder uint32) *BlockMap {
|
||||
func NewBlockMap(db *Lowlevel, folder string) *BlockMap {
|
||||
return &BlockMap{
|
||||
db: db,
|
||||
folder: folder,
|
||||
folder: db.folderIdx.ID([]byte(folder)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,10 +139,10 @@ func (m *BlockMap) blockKeyInto(o, hash []byte, file string) []byte {
|
||||
}
|
||||
|
||||
type BlockFinder struct {
|
||||
db *Instance
|
||||
db *Lowlevel
|
||||
}
|
||||
|
||||
func NewBlockFinder(db *Instance) *BlockFinder {
|
||||
func NewBlockFinder(db *Lowlevel) *BlockFinder {
|
||||
if blockFinder != nil {
|
||||
return blockFinder
|
||||
}
|
||||
|
||||
@@ -48,14 +48,14 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
func setup() (*Instance, *BlockFinder) {
|
||||
func setup() (*Lowlevel, *BlockFinder) {
|
||||
// Setup
|
||||
|
||||
db := OpenMemory()
|
||||
return db, NewBlockFinder(db)
|
||||
}
|
||||
|
||||
func dbEmpty(db *Instance) bool {
|
||||
func dbEmpty(db *Lowlevel) bool {
|
||||
iter := db.NewIterator(util.BytesPrefix([]byte{KeyTypeBlock}), nil)
|
||||
defer iter.Release()
|
||||
return !iter.Next()
|
||||
@@ -68,7 +68,7 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
||||
t.Fatal("db not empty")
|
||||
}
|
||||
|
||||
m := NewBlockMap(db, db.folderIdx.ID([]byte("folder1")))
|
||||
m := NewBlockMap(db, "folder1")
|
||||
|
||||
f3.Type = protocol.FileInfoTypeDirectory
|
||||
|
||||
@@ -152,8 +152,8 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
||||
func TestBlockFinderLookup(t *testing.T) {
|
||||
db, f := setup()
|
||||
|
||||
m1 := NewBlockMap(db, db.folderIdx.ID([]byte("folder1")))
|
||||
m2 := NewBlockMap(db, db.folderIdx.ID([]byte("folder2")))
|
||||
m1 := NewBlockMap(db, "folder1")
|
||||
m2 := NewBlockMap(db, "folder2")
|
||||
|
||||
err := m1.Add([]protocol.FileInfo{f1})
|
||||
if err != nil {
|
||||
|
||||
@@ -7,107 +7,20 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func TestDropIndexIDs(t *testing.T) {
|
||||
db := OpenMemory()
|
||||
|
||||
d1 := []byte("device67890123456789012345678901")
|
||||
d2 := []byte("device12345678901234567890123456")
|
||||
|
||||
// Set some index IDs
|
||||
|
||||
db.setIndexID(protocol.LocalDeviceID[:], []byte("foo"), 1)
|
||||
db.setIndexID(protocol.LocalDeviceID[:], []byte("bar"), 2)
|
||||
db.setIndexID(d1, []byte("foo"), 3)
|
||||
db.setIndexID(d1, []byte("bar"), 4)
|
||||
db.setIndexID(d2, []byte("foo"), 5)
|
||||
db.setIndexID(d2, []byte("bar"), 6)
|
||||
|
||||
// Verify them
|
||||
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 1 {
|
||||
t.Fatal("fail local 1")
|
||||
}
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 2 {
|
||||
t.Fatal("fail local 2")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("foo")) != 3 {
|
||||
t.Fatal("fail remote 1")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("bar")) != 4 {
|
||||
t.Fatal("fail remote 2")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("foo")) != 5 {
|
||||
t.Fatal("fail remote 3")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("bar")) != 6 {
|
||||
t.Fatal("fail remote 4")
|
||||
}
|
||||
|
||||
// Drop the local ones, verify only they got dropped
|
||||
|
||||
db.DropLocalDeltaIndexIDs()
|
||||
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 0 {
|
||||
t.Fatal("fail local 1")
|
||||
}
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 0 {
|
||||
t.Fatal("fail local 2")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("foo")) != 3 {
|
||||
t.Fatal("fail remote 1")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("bar")) != 4 {
|
||||
t.Fatal("fail remote 2")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("foo")) != 5 {
|
||||
t.Fatal("fail remote 3")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("bar")) != 6 {
|
||||
t.Fatal("fail remote 4")
|
||||
}
|
||||
|
||||
// Set local ones again
|
||||
|
||||
db.setIndexID(protocol.LocalDeviceID[:], []byte("foo"), 1)
|
||||
db.setIndexID(protocol.LocalDeviceID[:], []byte("bar"), 2)
|
||||
|
||||
// Drop the remote ones, verify only they got dropped
|
||||
|
||||
db.DropRemoteDeltaIndexIDs()
|
||||
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 1 {
|
||||
t.Fatal("fail local 1")
|
||||
}
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 2 {
|
||||
t.Fatal("fail local 2")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("foo")) != 0 {
|
||||
t.Fatal("fail remote 1")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("bar")) != 0 {
|
||||
t.Fatal("fail remote 2")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("foo")) != 0 {
|
||||
t.Fatal("fail remote 3")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("bar")) != 0 {
|
||||
t.Fatal("fail remote 4")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoredFiles(t *testing.T) {
|
||||
ldb, err := openJSONS("testdata/v0.14.48-ignoredfiles.db.jsons")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, _ := newDBInstance(ldb, "<memory>")
|
||||
db := NewLowlevel(ldb, "<memory>")
|
||||
UpdateSchema(db)
|
||||
|
||||
fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
|
||||
|
||||
// The contents of the database are like this:
|
||||
@@ -228,11 +141,13 @@ func TestUpdate0to3(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, _ := newDBInstance(ldb, "<memory>")
|
||||
|
||||
db := newInstance(NewLowlevel(ldb, "<memory>"))
|
||||
updater := schemaUpdater{db}
|
||||
|
||||
folder := []byte(update0to3Folder)
|
||||
|
||||
db.updateSchema0to1()
|
||||
updater.updateSchema0to1()
|
||||
|
||||
if _, ok := db.getFile(db.keyer.GenerateDeviceFileKey(nil, folder, protocol.LocalDeviceID[:], []byte(slashPrefixed))); ok {
|
||||
t.Error("File prefixed by '/' was not removed during transition to schema 1")
|
||||
@@ -242,7 +157,7 @@ func TestUpdate0to3(t *testing.T) {
|
||||
t.Error("Invalid file wasn't added to global list")
|
||||
}
|
||||
|
||||
db.updateSchema1to2()
|
||||
updater.updateSchema1to2()
|
||||
|
||||
found := false
|
||||
db.withHaveSequence(folder, 0, func(fi FileIntf) bool {
|
||||
@@ -263,7 +178,7 @@ func TestUpdate0to3(t *testing.T) {
|
||||
t.Error("Local file wasn't added to sequence bucket", err)
|
||||
}
|
||||
|
||||
db.updateSchema2to3()
|
||||
updater.updateSchema2to3()
|
||||
|
||||
need := map[string]protocol.FileInfo{
|
||||
haveUpdate0to3[remoteDevice0][0].Name: haveUpdate0to3[remoteDevice0][0],
|
||||
@@ -288,22 +203,17 @@ func TestUpdate0to3(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDowngrade(t *testing.T) {
|
||||
loc := "testdata/downgrade.db"
|
||||
db, err := Open(loc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
db.Close()
|
||||
os.RemoveAll(loc)
|
||||
}()
|
||||
db := OpenMemory()
|
||||
UpdateSchema(db) // sets the min version etc
|
||||
|
||||
miscDB := NewNamespacedKV(db, string(KeyTypeMiscData))
|
||||
// Bump the database version to something newer than we actually support
|
||||
miscDB := NewMiscDataNamespace(db)
|
||||
miscDB.PutInt64("dbVersion", dbVersion+1)
|
||||
l.Infoln(dbVersion)
|
||||
|
||||
db.Close()
|
||||
db, err = Open(loc)
|
||||
// Pretend we just opened the DB and attempt to update it again
|
||||
err := UpdateSchema(db)
|
||||
|
||||
if err, ok := err.(databaseDowngradeError); !ok {
|
||||
t.Fatal("Expected error due to database downgrade, got", err)
|
||||
} else if err.minSyncthingVersion != dbMinSyncthingVersion {
|
||||
@@ -8,89 +8,29 @@ package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"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)
|
||||
|
||||
type Instance struct {
|
||||
committed int64 // this must be the first attribute in the struct to ensure 64 bit alignment on 32 bit plaforms
|
||||
*leveldb.DB
|
||||
location string
|
||||
folderIdx *smallIndex
|
||||
deviceIdx *smallIndex
|
||||
keyer keyer
|
||||
type instance struct {
|
||||
*Lowlevel
|
||||
keyer keyer
|
||||
}
|
||||
|
||||
func Open(file string) (*Instance, error) {
|
||||
opts := &opt.Options{
|
||||
OpenFilesCacheCapacity: 100,
|
||||
WriteBuffer: 4 << 20,
|
||||
func newInstance(ll *Lowlevel) *instance {
|
||||
return &instance{
|
||||
Lowlevel: ll,
|
||||
keyer: newDefaultKeyer(ll.folderIdx, ll.deviceIdx),
|
||||
}
|
||||
|
||||
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, errorSuggestion{err, "failed to delete corrupted database"}
|
||||
}
|
||||
db, err = leveldb.OpenFile(file, opts)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errorSuggestion{err, "is another instance of Syncthing running?"}
|
||||
}
|
||||
|
||||
return newDBInstance(db, file)
|
||||
}
|
||||
|
||||
func OpenMemory() *Instance {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
ldb, _ := newDBInstance(db, "<memory>")
|
||||
return ldb
|
||||
}
|
||||
|
||||
func newDBInstance(db *leveldb.DB, location string) (*Instance, error) {
|
||||
i := &Instance{
|
||||
DB: db,
|
||||
location: location,
|
||||
folderIdx: newSmallIndex(db, []byte{KeyTypeFolderIdx}),
|
||||
deviceIdx: newSmallIndex(db, []byte{KeyTypeDeviceIdx}),
|
||||
}
|
||||
i.keyer = newDefaultKeyer(i.folderIdx, i.deviceIdx)
|
||||
err := i.updateSchema()
|
||||
return i, err
|
||||
}
|
||||
|
||||
// Committed returns the number of items committed to the database since startup
|
||||
func (db *Instance) Committed() int64 {
|
||||
return atomic.LoadInt64(&db.committed)
|
||||
}
|
||||
|
||||
// Location returns the filesystem path where the database is stored
|
||||
func (db *Instance) Location() string {
|
||||
return db.location
|
||||
}
|
||||
|
||||
func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, meta *metadataTracker) {
|
||||
func (db *instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, meta *metadataTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -131,7 +71,7 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, m
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) addSequences(folder []byte, fs []protocol.FileInfo) {
|
||||
func (db *instance) addSequences(folder []byte, fs []protocol.FileInfo) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -146,7 +86,7 @@ func (db *Instance) addSequences(folder []byte, fs []protocol.FileInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) removeSequences(folder []byte, fs []protocol.FileInfo) {
|
||||
func (db *instance) removeSequences(folder []byte, fs []protocol.FileInfo) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -158,7 +98,7 @@ func (db *Instance) removeSequences(folder []byte, fs []protocol.FileInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) {
|
||||
func (db *instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) {
|
||||
if len(prefix) > 0 {
|
||||
unslashedPrefix := prefix
|
||||
if bytes.HasSuffix(prefix, []byte{'/'}) {
|
||||
@@ -199,7 +139,7 @@ func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn It
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator) {
|
||||
func (db *instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -214,10 +154,8 @@ func (db *Instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator)
|
||||
}
|
||||
|
||||
if shouldDebug() {
|
||||
key := dbi.Key()
|
||||
seq := int64(binary.BigEndian.Uint64(key[keyPrefixLen+keyFolderLen:]))
|
||||
if f.Sequence != seq {
|
||||
panic(fmt.Sprintf("sequence index corruption, file sequence %d != expected %d", f.Sequence, seq))
|
||||
if seq := db.keyer.SequenceFromSequenceKey(dbi.Key()); f.Sequence != seq {
|
||||
panic(fmt.Sprintf("sequence index corruption (folder %v, file %v): sequence %d != expected %d", string(folder), f.Name, f.Sequence, seq))
|
||||
}
|
||||
}
|
||||
if !fn(f) {
|
||||
@@ -226,11 +164,11 @@ func (db *Instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator)
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte, f FileInfoTruncated) bool) {
|
||||
func (db *instance) withAllFolderTruncated(folder []byte, fn func(device []byte, f FileInfoTruncated) bool) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.keyer.GenerateDeviceFileKey(nil, folder, nil, nil).WithoutName()), nil)
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.keyer.GenerateDeviceFileKey(nil, folder, nil, nil).WithoutNameAndDevice()), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var gk []byte
|
||||
@@ -271,14 +209,14 @@ func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) getFile(key []byte) (protocol.FileInfo, bool) {
|
||||
func (db *instance) getFile(key []byte) (protocol.FileInfo, bool) {
|
||||
if f, ok := db.getFileTrunc(key, false); ok {
|
||||
return f.(protocol.FileInfo), true
|
||||
}
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
|
||||
func (db *Instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) {
|
||||
func (db *instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) {
|
||||
bs, err := db.Get(key, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return nil, false
|
||||
@@ -296,7 +234,7 @@ func (db *Instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) {
|
||||
return f, true
|
||||
}
|
||||
|
||||
func (db *Instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, bool) {
|
||||
func (db *instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, bool) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -304,7 +242,7 @@ func (db *Instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, boo
|
||||
return f, ok
|
||||
}
|
||||
|
||||
func (db *Instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file []byte, truncate bool) ([]byte, []byte, FileIntf, bool) {
|
||||
func (db *instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file []byte, truncate bool) ([]byte, []byte, FileIntf, bool) {
|
||||
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, file)
|
||||
|
||||
bs, err := t.Get(gk, nil)
|
||||
@@ -325,7 +263,7 @@ func (db *Instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file []
|
||||
return gk, dk, nil, false
|
||||
}
|
||||
|
||||
func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator) {
|
||||
func (db *instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator) {
|
||||
if len(prefix) > 0 {
|
||||
unslashedPrefix := prefix
|
||||
if bytes.HasSuffix(prefix, []byte{'/'}) {
|
||||
@@ -370,7 +308,7 @@ func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) availability(folder, file []byte) []protocol.DeviceID {
|
||||
func (db *instance) availability(folder, file []byte) []protocol.DeviceID {
|
||||
k := db.keyer.GenerateGlobalVersionKey(nil, folder, file)
|
||||
bs, err := db.Get(k, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
@@ -401,7 +339,7 @@ func (db *Instance) availability(folder, file []byte) []protocol.DeviceID {
|
||||
return devices
|
||||
}
|
||||
|
||||
func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator) {
|
||||
func (db *instance) withNeed(folder, device []byte, truncate bool, fn Iterator) {
|
||||
if bytes.Equal(device, protocol.LocalDeviceID[:]) {
|
||||
db.withNeedLocal(folder, truncate, fn)
|
||||
return
|
||||
@@ -473,7 +411,7 @@ func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator)
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) {
|
||||
func (db *instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -495,37 +433,13 @@ func (db *Instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) {
|
||||
}
|
||||
}
|
||||
|
||||
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, ok := db.keyer.FolderFromGlobalVersionKey(dbi.Key())
|
||||
if ok && !folderExists[string(folder)] {
|
||||
folderExists[string(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) {
|
||||
func (db *instance) dropFolder(folder []byte) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
for _, key := range [][]byte{
|
||||
// Remove all items related to the given folder from the device->file bucket
|
||||
db.keyer.GenerateDeviceFileKey(nil, folder, nil, nil).WithoutName(),
|
||||
db.keyer.GenerateDeviceFileKey(nil, folder, nil, nil).WithoutNameAndDevice(),
|
||||
// Remove all sequences related to the folder
|
||||
db.keyer.GenerateSequenceKey(nil, []byte(folder), 0).WithoutSequence(),
|
||||
// Remove all items related to the given folder from the global bucket
|
||||
@@ -537,7 +451,7 @@ func (db *Instance) dropFolder(folder []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) dropDeviceFolder(device, folder []byte, meta *metadataTracker) {
|
||||
func (db *instance) dropDeviceFolder(device, folder []byte, meta *metadataTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -556,7 +470,7 @@ func (db *Instance) dropDeviceFolder(device, folder []byte, meta *metadataTracke
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) {
|
||||
func (db *instance) checkGlobals(folder []byte, meta *metadataTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -604,7 +518,7 @@ func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) {
|
||||
l.Debugf("db check completed for %q", folder)
|
||||
}
|
||||
|
||||
func (db *Instance) getIndexID(device, folder []byte) protocol.IndexID {
|
||||
func (db *instance) getIndexID(device, folder []byte) protocol.IndexID {
|
||||
key := db.keyer.GenerateIndexIDKey(nil, device, folder)
|
||||
cur, err := db.Get(key, nil)
|
||||
if err != nil {
|
||||
@@ -619,7 +533,7 @@ func (db *Instance) getIndexID(device, folder []byte) protocol.IndexID {
|
||||
return id
|
||||
}
|
||||
|
||||
func (db *Instance) setIndexID(device, folder []byte, id protocol.IndexID) {
|
||||
func (db *instance) setIndexID(device, folder []byte, id protocol.IndexID) {
|
||||
key := db.keyer.GenerateIndexIDKey(nil, device, folder)
|
||||
bs, _ := id.Marshal() // marshalling can't fail
|
||||
if err := db.Put(key, bs, nil); err != nil {
|
||||
@@ -627,44 +541,15 @@ func (db *Instance) setIndexID(device, folder []byte, id protocol.IndexID) {
|
||||
}
|
||||
}
|
||||
|
||||
// DropLocalDeltaIndexIDs removes all index IDs for the local device ID from
|
||||
// the database. This will cause a full index transmission on the next
|
||||
// connection.
|
||||
func (db *Instance) DropLocalDeltaIndexIDs() {
|
||||
db.dropDeltaIndexIDs(true)
|
||||
}
|
||||
|
||||
// DropRemoteDeltaIndexIDs removes all index IDs for the other devices than
|
||||
// the local one from the database. This will cause them to send us a full
|
||||
// index on the next connection.
|
||||
func (db *Instance) DropRemoteDeltaIndexIDs() {
|
||||
db.dropDeltaIndexIDs(false)
|
||||
}
|
||||
|
||||
func (db *Instance) dropDeltaIndexIDs(local bool) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeIndexID}), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
device, _ := db.keyer.DeviceFromIndexIDKey(dbi.Key())
|
||||
if bytes.Equal(device, protocol.LocalDeviceID[:]) == local {
|
||||
t.Delete(dbi.Key())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) dropMtimes(folder []byte) {
|
||||
func (db *instance) dropMtimes(folder []byte) {
|
||||
db.dropPrefix(db.keyer.GenerateMtimesKey(nil, folder))
|
||||
}
|
||||
|
||||
func (db *Instance) dropFolderMeta(folder []byte) {
|
||||
func (db *instance) dropFolderMeta(folder []byte) {
|
||||
db.dropPrefix(db.keyer.GenerateFolderMetaKey(nil, folder))
|
||||
}
|
||||
|
||||
func (db *Instance) dropPrefix(prefix []byte) {
|
||||
func (db *instance) dropPrefix(prefix []byte) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -701,22 +586,6 @@ func unmarshalVersionList(data []byte) (VersionList, bool) {
|
||||
return vl, true
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
type errorSuggestion struct {
|
||||
inner error
|
||||
suggestion string
|
||||
+2
-2
@@ -82,8 +82,8 @@ func newDefaultKeyer(folderIdx, deviceIdx *smallIndex) defaultKeyer {
|
||||
|
||||
type deviceFileKey []byte
|
||||
|
||||
func (k deviceFileKey) WithoutName() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen+keyDeviceLen]
|
||||
func (k deviceFileKey) WithoutNameAndDevice() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) GenerateDeviceFileKey(key, folder, device, name []byte) deviceFileKey {
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestDeviceKey(t *testing.T) {
|
||||
dev := []byte("device67890123456789012345678901")
|
||||
name := []byte("name")
|
||||
|
||||
db := OpenMemory()
|
||||
db := newInstance(OpenMemory())
|
||||
|
||||
key := db.keyer.GenerateDeviceFileKey(nil, fld, dev, name)
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestGlobalKey(t *testing.T) {
|
||||
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
|
||||
name := []byte("name")
|
||||
|
||||
db := OpenMemory()
|
||||
db := newInstance(OpenMemory())
|
||||
|
||||
key := db.keyer.GenerateGlobalVersionKey(nil, fld, name)
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestGlobalKey(t *testing.T) {
|
||||
func TestSequenceKey(t *testing.T) {
|
||||
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
|
||||
|
||||
db := OpenMemory()
|
||||
db := newInstance(OpenMemory())
|
||||
|
||||
const seq = 1234567890
|
||||
key := db.keyer.GenerateSequenceKey(nil, fld, seq)
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func (vl VersionList) String() string {
|
||||
var b bytes.Buffer
|
||||
var id protocol.DeviceID
|
||||
b.WriteString("{")
|
||||
for i, v := range vl.Versions {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
copy(id[:], v.Device)
|
||||
fmt.Fprintf(&b, "{%v, %v, %v}", v.Version, id, v.Invalid)
|
||||
}
|
||||
b.WriteString("}")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// update brings the VersionList up to date with file. It returns the updated
|
||||
// VersionList, a potentially removed old FileVersion and its index, as well as
|
||||
// the index where the new FileVersion was inserted.
|
||||
func (vl VersionList) update(folder, device []byte, file protocol.FileInfo, db *Instance) (_ VersionList, removedFV FileVersion, removedAt int, insertedAt int) {
|
||||
removedAt, insertedAt = -1, -1
|
||||
for i, v := range vl.Versions {
|
||||
if bytes.Equal(v.Device, device) {
|
||||
removedAt = i
|
||||
removedFV = v
|
||||
vl.Versions = append(vl.Versions[:i], vl.Versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
nv := FileVersion{
|
||||
Device: device,
|
||||
Version: file.Version,
|
||||
Invalid: file.IsInvalid(),
|
||||
}
|
||||
for i, v := range vl.Versions {
|
||||
switch v.Version.Compare(file.Version) {
|
||||
case protocol.Equal:
|
||||
if nv.Invalid {
|
||||
continue
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case 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.
|
||||
vl = vl.insertAt(i, nv)
|
||||
return vl, removedFV, removedAt, i
|
||||
|
||||
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.)
|
||||
//
|
||||
// A surprise missing file entry here is counted as a win for us.
|
||||
if of, ok := db.getFile(db.keyer.GenerateDeviceFileKey(nil, folder, v.Device, []byte(file.Name))); !ok || file.WinsConflict(of) {
|
||||
vl = vl.insertAt(i, nv)
|
||||
return vl, removedFV, removedAt, i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find a position for an insert above, so append to the end.
|
||||
vl.Versions = append(vl.Versions, nv)
|
||||
|
||||
return vl, removedFV, removedAt, len(vl.Versions) - 1
|
||||
}
|
||||
|
||||
func (vl VersionList) insertAt(i int, v FileVersion) VersionList {
|
||||
vl.Versions = append(vl.Versions, FileVersion{})
|
||||
copy(vl.Versions[i+1:], vl.Versions[i:])
|
||||
vl.Versions[i] = v
|
||||
return vl
|
||||
}
|
||||
|
||||
func (vl VersionList) Get(device []byte) (FileVersion, bool) {
|
||||
for _, v := range vl.Versions {
|
||||
if bytes.Equal(v.Device, device) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
|
||||
return FileVersion{}, false
|
||||
}
|
||||
|
||||
type fileList []protocol.FileInfo
|
||||
|
||||
func (fl fileList) Len() int {
|
||||
return len(fl)
|
||||
}
|
||||
|
||||
func (fl fileList) Swap(a, b int) {
|
||||
fl[a], fl[b] = fl[b], fl[a]
|
||||
}
|
||||
|
||||
func (fl fileList) Less(a, b int) bool {
|
||||
return fl[a].Name < fl[b].Name
|
||||
}
|
||||
|
||||
// Flush batches to disk when they contain this many records.
|
||||
const batchFlushSize = 64
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
dbMaxOpenFiles = 100
|
||||
dbWriteBuffer = 4 << 20
|
||||
)
|
||||
|
||||
// Lowlevel is the lowest level database interface. It has a very simple
|
||||
// purpose: hold the actual *leveldb.DB database, and the in-memory state
|
||||
// that belong to that database. In the same way that a single on disk
|
||||
// database can only be opened once, there should be only one Lowlevel for
|
||||
// any given *leveldb.DB.
|
||||
type Lowlevel struct {
|
||||
committed int64 // atomic, must come first
|
||||
*leveldb.DB
|
||||
location string
|
||||
folderIdx *smallIndex
|
||||
deviceIdx *smallIndex
|
||||
}
|
||||
|
||||
// Open attempts to open the database at the given location, and runs
|
||||
// recovery on it if opening fails. Worst case, if recovery is not possible,
|
||||
// the database is erased and created from scratch.
|
||||
func Open(location string) (*Lowlevel, error) {
|
||||
opts := &opt.Options{
|
||||
OpenFilesCacheCapacity: dbMaxOpenFiles,
|
||||
WriteBuffer: dbWriteBuffer,
|
||||
}
|
||||
return open(location, opts)
|
||||
}
|
||||
|
||||
// OpenRO attempts to open the database at the given location, read only.
|
||||
func OpenRO(location string) (*Lowlevel, error) {
|
||||
opts := &opt.Options{
|
||||
OpenFilesCacheCapacity: dbMaxOpenFiles,
|
||||
ReadOnly: true,
|
||||
}
|
||||
return open(location, opts)
|
||||
}
|
||||
|
||||
func open(location string, opts *opt.Options) (*Lowlevel, error) {
|
||||
db, err := leveldb.OpenFile(location, opts)
|
||||
if leveldbIsCorrupted(err) {
|
||||
db, err = leveldb.RecoverFile(location, 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(location); err != nil {
|
||||
return nil, errorSuggestion{err, "failed to delete corrupted database"}
|
||||
}
|
||||
db, err = leveldb.OpenFile(location, opts)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errorSuggestion{err, "is another instance of Syncthing running?"}
|
||||
}
|
||||
return NewLowlevel(db, location), nil
|
||||
}
|
||||
|
||||
// OpenMemory returns a new Lowlevel referencing an in-memory database.
|
||||
func OpenMemory() *Lowlevel {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
return NewLowlevel(db, "<memory>")
|
||||
}
|
||||
|
||||
// Location returns the filesystem path where the database is stored
|
||||
func (db *Lowlevel) Location() string {
|
||||
return db.location
|
||||
}
|
||||
|
||||
// ListFolders returns the list of folders currently in the database
|
||||
func (db *Lowlevel) ListFolders() []string {
|
||||
return db.folderIdx.Values()
|
||||
}
|
||||
|
||||
// Committed returns the number of items committed to the database since startup
|
||||
func (db *Lowlevel) Committed() int64 {
|
||||
return atomic.LoadInt64(&db.committed)
|
||||
}
|
||||
|
||||
func (db *Lowlevel) Put(key, val []byte, wo *opt.WriteOptions) error {
|
||||
atomic.AddInt64(&db.committed, 1)
|
||||
return db.DB.Put(key, val, wo)
|
||||
}
|
||||
|
||||
func (db *Lowlevel) Delete(key []byte, wo *opt.WriteOptions) error {
|
||||
atomic.AddInt64(&db.committed, 1)
|
||||
return db.DB.Delete(key, wo)
|
||||
}
|
||||
|
||||
// NewLowlevel wraps the given *leveldb.DB into a *lowlevel
|
||||
func NewLowlevel(db *leveldb.DB, location string) *Lowlevel {
|
||||
return &Lowlevel{
|
||||
DB: db,
|
||||
location: location,
|
||||
folderIdx: newSmallIndex(db, []byte{KeyTypeFolderIdx}),
|
||||
deviceIdx: newSmallIndex(db, []byte{KeyTypeDeviceIdx}),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
+22
-3
@@ -20,6 +20,7 @@ type metadataTracker struct {
|
||||
mut sync.RWMutex
|
||||
counts CountsSet
|
||||
indexes map[metaKey]int // device ID + local flags -> index in counts
|
||||
dirty bool
|
||||
}
|
||||
|
||||
type metaKey struct {
|
||||
@@ -55,18 +56,31 @@ func (m *metadataTracker) Marshal() ([]byte, error) {
|
||||
|
||||
// toDB saves the marshalled metadataTracker to the given db, under the key
|
||||
// corresponding to the given folder
|
||||
func (m *metadataTracker) toDB(db *Instance, folder []byte) error {
|
||||
func (m *metadataTracker) toDB(db *instance, folder []byte) error {
|
||||
key := db.keyer.GenerateFolderMetaKey(nil, folder)
|
||||
|
||||
m.mut.RLock()
|
||||
defer m.mut.RUnlock()
|
||||
|
||||
if !m.dirty {
|
||||
return nil
|
||||
}
|
||||
|
||||
bs, err := m.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Put(key, bs, nil)
|
||||
err = db.Put(key, bs, nil)
|
||||
if err == nil {
|
||||
m.dirty = false
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// fromDB initializes the metadataTracker from the marshalled data found in
|
||||
// the database under the key corresponding to the given folder
|
||||
func (m *metadataTracker) fromDB(db *Instance, folder []byte) error {
|
||||
func (m *metadataTracker) fromDB(db *instance, folder []byte) error {
|
||||
key := db.keyer.GenerateFolderMetaKey(nil, folder)
|
||||
bs, err := db.Get(key, nil)
|
||||
if err != nil {
|
||||
@@ -99,6 +113,7 @@ func (m *metadataTracker) addFile(dev protocol.DeviceID, f FileIntf) {
|
||||
}
|
||||
|
||||
m.mut.Lock()
|
||||
m.dirty = true
|
||||
|
||||
if flags := f.FileLocalFlags(); flags == 0 {
|
||||
// Account regular files in the zero-flags bucket.
|
||||
@@ -141,6 +156,7 @@ func (m *metadataTracker) removeFile(dev protocol.DeviceID, f FileIntf) {
|
||||
}
|
||||
|
||||
m.mut.Lock()
|
||||
m.dirty = true
|
||||
|
||||
if flags := f.FileLocalFlags(); flags == 0 {
|
||||
// Remove regular files from the zero-flags bucket
|
||||
@@ -194,6 +210,7 @@ func (m *metadataTracker) removeFileLocked(dev protocol.DeviceID, flags uint32,
|
||||
// resetAll resets all metadata for the given device
|
||||
func (m *metadataTracker) resetAll(dev protocol.DeviceID) {
|
||||
m.mut.Lock()
|
||||
m.dirty = true
|
||||
for i, c := range m.counts.Counts {
|
||||
if bytes.Equal(c.DeviceID, dev[:]) {
|
||||
m.counts.Counts[i] = Counts{
|
||||
@@ -209,6 +226,7 @@ func (m *metadataTracker) resetAll(dev protocol.DeviceID) {
|
||||
// sequence number
|
||||
func (m *metadataTracker) resetCounts(dev protocol.DeviceID) {
|
||||
m.mut.Lock()
|
||||
m.dirty = true
|
||||
|
||||
for i, c := range m.counts.Counts {
|
||||
if bytes.Equal(c.DeviceID, dev[:]) {
|
||||
@@ -285,6 +303,7 @@ func (m *metadataTracker) Created() time.Time {
|
||||
func (m *metadataTracker) SetCreated() {
|
||||
m.mut.Lock()
|
||||
m.counts.Created = time.Now().UnixNano()
|
||||
m.dirty = true
|
||||
m.mut.Unlock()
|
||||
}
|
||||
|
||||
|
||||
+22
-2
@@ -17,13 +17,13 @@ import (
|
||||
// NamespacedKV is a simple key-value store using a specific namespace within
|
||||
// a leveldb.
|
||||
type NamespacedKV struct {
|
||||
db *Instance
|
||||
db *Lowlevel
|
||||
prefix []byte
|
||||
}
|
||||
|
||||
// NewNamespacedKV returns a new NamespacedKV that lives in the namespace
|
||||
// specified by the prefix.
|
||||
func NewNamespacedKV(db *Instance, prefix string) *NamespacedKV {
|
||||
func NewNamespacedKV(db *Lowlevel, prefix string) *NamespacedKV {
|
||||
return &NamespacedKV{
|
||||
db: db,
|
||||
prefix: []byte(prefix),
|
||||
@@ -157,3 +157,23 @@ func (n NamespacedKV) Delete(key string) {
|
||||
keyBs := append(n.prefix, []byte(key)...)
|
||||
n.db.Delete(keyBs, nil)
|
||||
}
|
||||
|
||||
// Well known namespaces that can be instantiated without knowing the key
|
||||
// details.
|
||||
|
||||
// NewDeviceStatisticsNamespace creates a KV namespace for device statistics
|
||||
// for the given device.
|
||||
func NewDeviceStatisticsNamespace(db *Lowlevel, device string) *NamespacedKV {
|
||||
return NewNamespacedKV(db, string(KeyTypeDeviceStatistic)+device)
|
||||
}
|
||||
|
||||
// NewFolderStatisticsNamespace creates a KV namespace for folder statistics
|
||||
// for the given folder.
|
||||
func NewFolderStatisticsNamespace(db *Lowlevel, folder string) *NamespacedKV {
|
||||
return NewNamespacedKV(db, string(KeyTypeFolderStatistic)+folder)
|
||||
}
|
||||
|
||||
// NewMiscDateNamespace creates a KV namespace for miscellaneous metadata.
|
||||
func NewMiscDataNamespace(db *Lowlevel) *NamespacedKV {
|
||||
return NewNamespacedKV(db, string(KeyTypeMiscData))
|
||||
}
|
||||
|
||||
@@ -22,9 +22,10 @@ import (
|
||||
// 4: v0.14.49
|
||||
// 5: v0.14.49
|
||||
// 6: v0.14.50
|
||||
// 7: v0.14.53
|
||||
const (
|
||||
dbVersion = 6
|
||||
dbMinSyncthingVersion = "v0.14.50"
|
||||
dbVersion = 7
|
||||
dbMinSyncthingVersion = "v0.14.53"
|
||||
)
|
||||
|
||||
type databaseDowngradeError struct {
|
||||
@@ -38,8 +39,17 @@ func (e databaseDowngradeError) Error() string {
|
||||
return fmt.Sprintf("Syncthing %s required", e.minSyncthingVersion)
|
||||
}
|
||||
|
||||
func (db *Instance) updateSchema() error {
|
||||
miscDB := NewNamespacedKV(db, string(KeyTypeMiscData))
|
||||
func UpdateSchema(ll *Lowlevel) error {
|
||||
updater := &schemaUpdater{newInstance(ll)}
|
||||
return updater.updateSchema()
|
||||
}
|
||||
|
||||
type schemaUpdater struct {
|
||||
*instance
|
||||
}
|
||||
|
||||
func (db *schemaUpdater) updateSchema() error {
|
||||
miscDB := NewMiscDataNamespace(db.Lowlevel)
|
||||
prevVersion, _ := miscDB.Int64("dbVersion")
|
||||
|
||||
if prevVersion > dbVersion {
|
||||
@@ -70,6 +80,9 @@ func (db *Instance) updateSchema() error {
|
||||
if prevVersion < 6 {
|
||||
db.updateSchema5to6()
|
||||
}
|
||||
if prevVersion < 7 {
|
||||
db.updateSchema6to7()
|
||||
}
|
||||
|
||||
miscDB.PutInt64("dbVersion", dbVersion)
|
||||
miscDB.PutString("dbMinSyncthingVersion", dbMinSyncthingVersion)
|
||||
@@ -77,7 +90,7 @@ func (db *Instance) updateSchema() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Instance) updateSchema0to1() {
|
||||
func (db *schemaUpdater) updateSchema0to1() {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -159,7 +172,7 @@ func (db *Instance) updateSchema0to1() {
|
||||
|
||||
// updateSchema1to2 introduces a sequenceKey->deviceKey bucket for local items
|
||||
// to allow iteration in sequence order (simplifies sending indexes).
|
||||
func (db *Instance) updateSchema1to2() {
|
||||
func (db *schemaUpdater) updateSchema1to2() {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -178,7 +191,7 @@ func (db *Instance) updateSchema1to2() {
|
||||
}
|
||||
|
||||
// updateSchema2to3 introduces a needKey->nil bucket for locally needed files.
|
||||
func (db *Instance) updateSchema2to3() {
|
||||
func (db *schemaUpdater) updateSchema2to3() {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -209,7 +222,7 @@ func (db *Instance) updateSchema2to3() {
|
||||
// release candidates (dbVersion 3 and 4)
|
||||
// https://github.com/syncthing/syncthing/issues/5007
|
||||
// https://github.com/syncthing/syncthing/issues/5053
|
||||
func (db *Instance) updateSchemaTo5() {
|
||||
func (db *schemaUpdater) updateSchemaTo5() {
|
||||
t := db.newReadWriteTransaction()
|
||||
var nk []byte
|
||||
for _, folderStr := range db.ListFolders() {
|
||||
@@ -221,7 +234,7 @@ func (db *Instance) updateSchemaTo5() {
|
||||
db.updateSchema2to3()
|
||||
}
|
||||
|
||||
func (db *Instance) updateSchema5to6() {
|
||||
func (db *schemaUpdater) updateSchema5to6() {
|
||||
// For every local file with the Invalid bit set, clear the Invalid bit and
|
||||
// set LocalFlags = FlagLocalIgnored.
|
||||
|
||||
@@ -250,3 +263,39 @@ func (db *Instance) updateSchema5to6() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// updateSchema6to7 checks whether all currently locally needed files are really
|
||||
// needed and removes them if not.
|
||||
func (db *schemaUpdater) updateSchema6to7() {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
var gk []byte
|
||||
var nk []byte
|
||||
|
||||
for _, folderStr := range db.ListFolders() {
|
||||
folder := []byte(folderStr)
|
||||
db.withNeedLocal(folder, false, func(f FileIntf) bool {
|
||||
name := []byte(f.FileName())
|
||||
global := f.(protocol.FileInfo)
|
||||
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
|
||||
svl, err := t.Get(gk, nil)
|
||||
if err != nil {
|
||||
// If there is no global list, we hardly need it.
|
||||
t.Delete(t.db.keyer.GenerateNeedFileKey(nk, folder, name))
|
||||
return true
|
||||
}
|
||||
var fl VersionList
|
||||
err = fl.Unmarshal(svl)
|
||||
if err != nil {
|
||||
// This can't happen, but it's ignored everywhere else too,
|
||||
// so lets not act on it.
|
||||
return true
|
||||
}
|
||||
if localFV, haveLocalFV := fl.Get(protocol.LocalDeviceID[:]); !need(global, haveLocalFV, localFV.Version) {
|
||||
t.Delete(t.db.keyer.GenerateNeedFileKey(nk, folder, name))
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
+23
-9
@@ -21,12 +21,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/util"
|
||||
)
|
||||
|
||||
type FileSet struct {
|
||||
folder string
|
||||
fs fs.Filesystem
|
||||
db *Instance
|
||||
db *instance
|
||||
blockmap *BlockMap
|
||||
meta *metadataTracker
|
||||
|
||||
@@ -66,12 +67,14 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
func NewFileSet(folder string, fs fs.Filesystem, db *Instance) *FileSet {
|
||||
func NewFileSet(folder string, fs fs.Filesystem, ll *Lowlevel) *FileSet {
|
||||
db := newInstance(ll)
|
||||
|
||||
var s = FileSet{
|
||||
folder: folder,
|
||||
fs: fs,
|
||||
db: db,
|
||||
blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
|
||||
blockmap: NewBlockMap(ll, folder),
|
||||
meta: newMetadataTracker(),
|
||||
updateMutex: sync.NewMutex(),
|
||||
}
|
||||
@@ -310,7 +313,7 @@ func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
|
||||
|
||||
func (s *FileSet) MtimeFS() *fs.MtimeFS {
|
||||
prefix := s.db.keyer.GenerateMtimesKey(nil, []byte(s.folder))
|
||||
kv := NewNamespacedKV(s.db, string(prefix))
|
||||
kv := NewNamespacedKV(s.db.Lowlevel, string(prefix))
|
||||
return fs.NewMtimeFS(s.fs, kv)
|
||||
}
|
||||
|
||||
@@ -320,15 +323,26 @@ func (s *FileSet) ListDevices() []protocol.DeviceID {
|
||||
|
||||
// DropFolder clears out all information related to the given folder from the
|
||||
// database.
|
||||
func DropFolder(db *Instance, folder string) {
|
||||
func DropFolder(ll *Lowlevel, folder string) {
|
||||
db := newInstance(ll)
|
||||
db.dropFolder([]byte(folder))
|
||||
db.dropMtimes([]byte(folder))
|
||||
db.dropFolderMeta([]byte(folder))
|
||||
bm := &BlockMap{
|
||||
db: db,
|
||||
folder: db.folderIdx.ID([]byte(folder)),
|
||||
}
|
||||
bm := NewBlockMap(ll, folder)
|
||||
bm.Drop()
|
||||
|
||||
// Also clean out the folder ID mapping.
|
||||
db.folderIdx.Delete([]byte(folder))
|
||||
}
|
||||
|
||||
// DropDeltaIndexIDs removes all delta index IDs from the database.
|
||||
// This will cause a full index transmission on the next connection.
|
||||
func DropDeltaIndexIDs(db *Lowlevel) {
|
||||
dbi := db.NewIterator(util.BytesPrefix([]byte{KeyTypeIndexID}), nil)
|
||||
defer dbi.Release()
|
||||
for dbi.Next() {
|
||||
db.Delete(dbi.Key(), nil)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeFilenames(fs []protocol.FileInfo) {
|
||||
|
||||
+65
-2
@@ -1178,8 +1178,8 @@ func TestReceiveOnlyAccounting(t *testing.T) {
|
||||
if n := s.GlobalSize().Files; n != 3 {
|
||||
t.Fatal("expected 3 global files after local change, not", n)
|
||||
}
|
||||
if n := s.GlobalSize().Bytes; n != 120 {
|
||||
t.Fatal("expected 120 global bytes after local change, not", n)
|
||||
if n := s.GlobalSize().Bytes; n != 30 {
|
||||
t.Fatal("expected 30 global files after local change, not", n)
|
||||
}
|
||||
if n := s.ReceiveOnlyChangedSize().Files; n != 1 {
|
||||
t.Fatal("expected 1 receive only changed file after local change, not", n)
|
||||
@@ -1271,6 +1271,69 @@ func TestRemoteInvalidNotAccounted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedWithNewerInvalid(t *testing.T) {
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
s := db.NewFileSet("default", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
|
||||
|
||||
rem0ID := remoteDevice0.Short()
|
||||
rem1ID := remoteDevice1.Short()
|
||||
|
||||
// Initial state: file present on rem0 and rem1, but not locally.
|
||||
file := protocol.FileInfo{Name: "foo"}
|
||||
file.Version = file.Version.Update(rem0ID)
|
||||
s.Update(remoteDevice0, fileList{file})
|
||||
s.Update(remoteDevice1, fileList{file})
|
||||
|
||||
need := needList(s, protocol.LocalDeviceID)
|
||||
if len(need) != 1 {
|
||||
t.Fatal("Locally missing file should be needed")
|
||||
}
|
||||
if !need[0].IsEquivalent(file) {
|
||||
t.Fatalf("Got needed file %v, expected %v", need[0], file)
|
||||
}
|
||||
|
||||
// rem1 sends an invalid file with increased version
|
||||
inv := file
|
||||
inv.Version = inv.Version.Update(rem1ID)
|
||||
inv.RawInvalid = true
|
||||
s.Update(remoteDevice1, fileList{inv})
|
||||
|
||||
// We still have an old file, we need the newest valid file
|
||||
need = needList(s, protocol.LocalDeviceID)
|
||||
if len(need) != 1 {
|
||||
t.Fatal("Locally missing file should be needed regardless of invalid files")
|
||||
}
|
||||
if !need[0].IsEquivalent(file) {
|
||||
t.Fatalf("Got needed file %v, expected %v", need[0], file)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedAfterDeviceRemove(t *testing.T) {
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
file := "foo"
|
||||
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
|
||||
|
||||
fs := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}}}
|
||||
|
||||
s.Update(protocol.LocalDeviceID, fs)
|
||||
|
||||
fs[0].Version = fs[0].Version.Update(myID)
|
||||
|
||||
s.Update(remoteDevice0, fs)
|
||||
|
||||
if need := needList(s, protocol.LocalDeviceID); len(need) != 1 {
|
||||
t.Fatal("Expected one local need, got", need)
|
||||
}
|
||||
|
||||
s.Drop(remoteDevice0)
|
||||
|
||||
if need := needList(s, protocol.LocalDeviceID); len(need) != 0 {
|
||||
t.Fatal("Expected no local need, got", need)
|
||||
}
|
||||
}
|
||||
|
||||
func replace(fs *db.FileSet, device protocol.DeviceID, files []protocol.FileInfo) {
|
||||
fs.Drop(device)
|
||||
fs.Update(device, files)
|
||||
|
||||
+48
-2
@@ -8,6 +8,7 @@ package db
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
@@ -46,8 +47,11 @@ func (i *smallIndex) load() {
|
||||
for it.Next() {
|
||||
val := string(it.Value())
|
||||
id := binary.BigEndian.Uint32(it.Key()[len(i.prefix):])
|
||||
i.id2val[id] = val
|
||||
i.val2id[val] = id
|
||||
if val != "" {
|
||||
// Empty value means the entry has been deleted.
|
||||
i.id2val[id] = val
|
||||
i.val2id[val] = id
|
||||
}
|
||||
if id >= i.nextID {
|
||||
i.nextID = id + 1
|
||||
}
|
||||
@@ -96,3 +100,45 @@ func (i *smallIndex) Val(id uint32) ([]byte, bool) {
|
||||
|
||||
return []byte(val), true
|
||||
}
|
||||
|
||||
func (i *smallIndex) Delete(val []byte) {
|
||||
i.mut.Lock()
|
||||
defer i.mut.Unlock()
|
||||
|
||||
// Check the reverse mapping to get the ID for the value.
|
||||
if id, ok := i.val2id[string(val)]; ok {
|
||||
// Generate the corresponding database key.
|
||||
key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id
|
||||
copy(key, i.prefix)
|
||||
binary.BigEndian.PutUint32(key[len(i.prefix):], id)
|
||||
|
||||
// Put an empty value into the database. This indicates that the
|
||||
// entry does not exist any more and prevents the ID from being
|
||||
// reused in the future.
|
||||
i.db.Put(key, []byte{}, nil)
|
||||
|
||||
// Delete reverse mapping.
|
||||
delete(i.id2val, id)
|
||||
}
|
||||
|
||||
// Delete forward mapping.
|
||||
delete(i.val2id, string(val))
|
||||
}
|
||||
|
||||
// Values returns the set of values in the index
|
||||
func (i *smallIndex) Values() []string {
|
||||
// In principle this method should return [][]byte because all the other
|
||||
// methods deal in []byte keys. However, in practice, where it's used
|
||||
// wants a []string and it's easier to just create that here rather than
|
||||
// having to convert both here and there...
|
||||
|
||||
i.mut.Lock()
|
||||
vals := make([]string, 0, len(i.val2id))
|
||||
for val := range i.val2id {
|
||||
vals = append(vals, val)
|
||||
}
|
||||
i.mut.Unlock()
|
||||
|
||||
sort.Strings(vals)
|
||||
return vals
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSmallIndex(t *testing.T) {
|
||||
db := OpenMemory()
|
||||
idx := newSmallIndex(db.DB, []byte{12, 34})
|
||||
|
||||
// ID zero should be unallocated
|
||||
if val, ok := idx.Val(0); ok || val != nil {
|
||||
t.Fatal("Unexpected return for nonexistent ID 0")
|
||||
}
|
||||
|
||||
// A new key should get ID zero
|
||||
if id := idx.ID([]byte("hello")); id != 0 {
|
||||
t.Fatal("Expected 0, not", id)
|
||||
}
|
||||
// Looking up ID zero should work
|
||||
if val, ok := idx.Val(0); !ok || string(val) != "hello" {
|
||||
t.Fatalf(`Expected true, "hello", not %v, %q`, ok, val)
|
||||
}
|
||||
|
||||
// Delete the key
|
||||
idx.Delete([]byte("hello"))
|
||||
|
||||
// Next ID should be one
|
||||
if id := idx.ID([]byte("key2")); id != 1 {
|
||||
t.Fatal("Expected 1, not", id)
|
||||
}
|
||||
|
||||
// Now lets create a new index instance based on what's actually serialized to the database.
|
||||
idx = newSmallIndex(db.DB, []byte{12, 34})
|
||||
|
||||
// Status should be about the same as before.
|
||||
if val, ok := idx.Val(0); ok || val != nil {
|
||||
t.Fatal("Unexpected return for deleted ID 0")
|
||||
}
|
||||
if id := idx.ID([]byte("key2")); id != 1 {
|
||||
t.Fatal("Expected 1, not", id)
|
||||
}
|
||||
|
||||
// Setting "hello" again should get us ID 2, not 0 as it was originally.
|
||||
if id := idx.ID([]byte("hello")); id != 2 {
|
||||
t.Fatal("Expected 2, not", id)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
@@ -139,3 +141,113 @@ func (c Counts) Add(other Counts) Counts {
|
||||
LocalFlags: c.LocalFlags | other.LocalFlags,
|
||||
}
|
||||
}
|
||||
|
||||
func (vl VersionList) String() string {
|
||||
var b bytes.Buffer
|
||||
var id protocol.DeviceID
|
||||
b.WriteString("{")
|
||||
for i, v := range vl.Versions {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
copy(id[:], v.Device)
|
||||
fmt.Fprintf(&b, "{%v, %v}", v.Version, id)
|
||||
}
|
||||
b.WriteString("}")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// update brings the VersionList up to date with file. It returns the updated
|
||||
// VersionList, a potentially removed old FileVersion and its index, as well as
|
||||
// the index where the new FileVersion was inserted.
|
||||
func (vl VersionList) update(folder, device []byte, file protocol.FileInfo, db *instance) (_ VersionList, removedFV FileVersion, removedAt int, insertedAt int) {
|
||||
vl, removedFV, removedAt = vl.pop(device)
|
||||
|
||||
nv := FileVersion{
|
||||
Device: device,
|
||||
Version: file.Version,
|
||||
Invalid: file.IsInvalid(),
|
||||
}
|
||||
i := 0
|
||||
if nv.Invalid {
|
||||
i = sort.Search(len(vl.Versions), func(j int) bool {
|
||||
return vl.Versions[j].Invalid
|
||||
})
|
||||
}
|
||||
for ; i < len(vl.Versions); i++ {
|
||||
switch vl.Versions[i].Version.Compare(file.Version) {
|
||||
case protocol.Equal:
|
||||
fallthrough
|
||||
|
||||
case 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.
|
||||
vl = vl.insertAt(i, nv)
|
||||
return vl, removedFV, removedAt, i
|
||||
|
||||
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.)
|
||||
//
|
||||
// A surprise missing file entry here is counted as a win for us.
|
||||
if of, ok := db.getFile(db.keyer.GenerateDeviceFileKey(nil, folder, vl.Versions[i].Device, []byte(file.Name))); !ok || file.WinsConflict(of) {
|
||||
vl = vl.insertAt(i, nv)
|
||||
return vl, removedFV, removedAt, i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find a position for an insert above, so append to the end.
|
||||
vl.Versions = append(vl.Versions, nv)
|
||||
|
||||
return vl, removedFV, removedAt, len(vl.Versions) - 1
|
||||
}
|
||||
|
||||
func (vl VersionList) insertAt(i int, v FileVersion) VersionList {
|
||||
vl.Versions = append(vl.Versions, FileVersion{})
|
||||
copy(vl.Versions[i+1:], vl.Versions[i:])
|
||||
vl.Versions[i] = v
|
||||
return vl
|
||||
}
|
||||
|
||||
// pop returns the VersionList without the entry for the given device, as well
|
||||
// as the removed FileVersion and the position, where that FileVersion was.
|
||||
// If there is no FileVersion for the given device, the position is -1.
|
||||
func (vl VersionList) pop(device []byte) (VersionList, FileVersion, int) {
|
||||
removedAt := -1
|
||||
for i, v := range vl.Versions {
|
||||
if bytes.Equal(v.Device, device) {
|
||||
vl.Versions = append(vl.Versions[:i], vl.Versions[i+1:]...)
|
||||
return vl, v, i
|
||||
}
|
||||
}
|
||||
return vl, FileVersion{}, removedAt
|
||||
}
|
||||
|
||||
func (vl VersionList) Get(device []byte) (FileVersion, bool) {
|
||||
for _, v := range vl.Versions {
|
||||
if bytes.Equal(v.Device, device) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
|
||||
return FileVersion{}, false
|
||||
}
|
||||
|
||||
type fileList []protocol.FileInfo
|
||||
|
||||
func (fl fileList) Len() int {
|
||||
return len(fl)
|
||||
}
|
||||
|
||||
func (fl fileList) Swap(a, b int) {
|
||||
fl[a], fl[b] = fl[b], fl[a]
|
||||
}
|
||||
|
||||
func (fl fileList) Less(a, b int) bool {
|
||||
return fl[a].Name < fl[b].Name
|
||||
}
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
// Flush batches to disk when they contain this many records.
|
||||
const batchFlushSize = 64
|
||||
|
||||
// A readOnlyTransaction represents a database snapshot.
|
||||
type readOnlyTransaction struct {
|
||||
*leveldb.Snapshot
|
||||
db *Instance
|
||||
db *instance
|
||||
}
|
||||
|
||||
func (db *Instance) newReadOnlyTransaction() readOnlyTransaction {
|
||||
func (db *instance) newReadOnlyTransaction() readOnlyTransaction {
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -48,7 +48,7 @@ type readWriteTransaction struct {
|
||||
*leveldb.Batch
|
||||
}
|
||||
|
||||
func (db *Instance) newReadWriteTransaction() readWriteTransaction {
|
||||
func (db *instance) newReadWriteTransaction() readWriteTransaction {
|
||||
t := db.newReadOnlyTransaction()
|
||||
return readWriteTransaction{
|
||||
readOnlyTransaction: t,
|
||||
@@ -72,7 +72,6 @@ func (t readWriteTransaction) flush() {
|
||||
if err := t.db.Write(t.Batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
atomic.AddInt64(&t.db.committed, int64(t.Batch.Len()))
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) insertFile(fk, folder, device []byte, file protocol.FileInfo) {
|
||||
@@ -99,29 +98,18 @@ func (t readWriteTransaction) updateGlobal(gk, folder, device []byte, file proto
|
||||
|
||||
name := []byte(file.Name)
|
||||
|
||||
var newGlobal protocol.FileInfo
|
||||
var global protocol.FileInfo
|
||||
if insertedAt == 0 {
|
||||
// Inserted a new newest version
|
||||
newGlobal = file
|
||||
global = file
|
||||
} else if new, ok := t.getFile(folder, fl.Versions[0].Device, name); ok {
|
||||
// The previous second version is now the first
|
||||
newGlobal = new
|
||||
global = new
|
||||
} else {
|
||||
panic("This file must exist in the db")
|
||||
}
|
||||
|
||||
// Fixup the list of files we need.
|
||||
nk := t.db.keyer.GenerateNeedFileKey(nil, folder, name)
|
||||
hasNeeded, _ := t.db.Has(nk, nil)
|
||||
if localFV, haveLocalFV := fl.Get(protocol.LocalDeviceID[:]); need(newGlobal, haveLocalFV, localFV.Version) {
|
||||
if !hasNeeded {
|
||||
l.Debugf("local need insert; folder=%q, name=%q", folder, name)
|
||||
t.Put(nk, nil)
|
||||
}
|
||||
} else if hasNeeded {
|
||||
l.Debugf("local need delete; folder=%q, name=%q", folder, name)
|
||||
t.Delete(nk)
|
||||
}
|
||||
t.updateLocalNeed(folder, name, fl, global)
|
||||
|
||||
if removedAt != 0 && insertedAt != 0 {
|
||||
l.Debugf(`new global for "%v" after update: %v`, file.Name, fl)
|
||||
@@ -144,7 +132,7 @@ func (t readWriteTransaction) updateGlobal(gk, folder, device []byte, file proto
|
||||
}
|
||||
|
||||
// Add the new global to the global size counter
|
||||
meta.addFile(protocol.GlobalDeviceID, newGlobal)
|
||||
meta.addFile(protocol.GlobalDeviceID, global)
|
||||
|
||||
l.Debugf(`new global for "%v" after update: %v`, file.Name, fl)
|
||||
t.Put(gk, mustMarshal(&fl))
|
||||
@@ -152,6 +140,23 @@ func (t readWriteTransaction) updateGlobal(gk, folder, device []byte, file proto
|
||||
return true
|
||||
}
|
||||
|
||||
// updateLocalNeeds checks whether the given file is still needed on the local
|
||||
// device according to the version list and global FileInfo given and updates
|
||||
// the db accordingly.
|
||||
func (t readWriteTransaction) updateLocalNeed(folder, name []byte, fl VersionList, global protocol.FileInfo) {
|
||||
nk := t.db.keyer.GenerateNeedFileKey(nil, folder, name)
|
||||
hasNeeded, _ := t.db.Has(nk, nil)
|
||||
if localFV, haveLocalFV := fl.Get(protocol.LocalDeviceID[:]); need(global, haveLocalFV, localFV.Version) {
|
||||
if !hasNeeded {
|
||||
l.Debugf("local need insert; folder=%q, name=%q", folder, name)
|
||||
t.Put(nk, nil)
|
||||
}
|
||||
} else if hasNeeded {
|
||||
l.Debugf("local need delete; folder=%q, name=%q", folder, name)
|
||||
t.Delete(nk)
|
||||
}
|
||||
}
|
||||
|
||||
func need(global FileIntf, haveLocal bool, localVersion protocol.Vector) bool {
|
||||
// We never need an invalid file.
|
||||
if global.IsInvalid() {
|
||||
@@ -162,7 +167,7 @@ func need(global FileIntf, haveLocal bool, localVersion protocol.Vector) bool {
|
||||
return false
|
||||
}
|
||||
// We don't need the global file if we already have the same version.
|
||||
if haveLocal && localVersion.Equal(global.FileVersion()) {
|
||||
if haveLocal && localVersion.GreaterEqual(global.FileVersion()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -188,36 +193,37 @@ func (t readWriteTransaction) removeFromGlobal(gk, folder, device, file []byte,
|
||||
return
|
||||
}
|
||||
|
||||
removed := false
|
||||
for i := range fl.Versions {
|
||||
if bytes.Equal(fl.Versions[i].Device, device) {
|
||||
if i == 0 && meta != nil {
|
||||
f, ok := t.getFile(folder, device, file)
|
||||
if !ok {
|
||||
// didn't exist anyway, apparently
|
||||
continue
|
||||
}
|
||||
meta.removeFile(protocol.GlobalDeviceID, f)
|
||||
removed = true
|
||||
}
|
||||
fl.Versions = append(fl.Versions[:i], fl.Versions[i+1:]...)
|
||||
break
|
||||
fl, _, removedAt := fl.pop(device)
|
||||
if removedAt == -1 {
|
||||
// There is no version for the given device
|
||||
return
|
||||
}
|
||||
|
||||
if removedAt == 0 {
|
||||
// A failure to get the file here is surprising and our
|
||||
// global size data will be incorrect until a restart...
|
||||
if f, ok := t.getFile(folder, device, file); ok {
|
||||
meta.removeFile(protocol.GlobalDeviceID, f)
|
||||
}
|
||||
}
|
||||
|
||||
if len(fl.Versions) == 0 {
|
||||
t.Delete(t.db.keyer.GenerateNeedFileKey(nil, folder, file))
|
||||
t.Delete(gk)
|
||||
return
|
||||
}
|
||||
|
||||
if removedAt == 0 {
|
||||
global, ok := t.getFile(folder, fl.Versions[0].Device, file)
|
||||
if !ok {
|
||||
panic("This file must exist in the db")
|
||||
}
|
||||
t.updateLocalNeed(folder, file, fl, global)
|
||||
meta.addFile(protocol.GlobalDeviceID, global)
|
||||
}
|
||||
|
||||
l.Debugf("new global after remove: %v", fl)
|
||||
t.Put(gk, mustMarshal(&fl))
|
||||
if removed {
|
||||
if f, ok := t.getFile(folder, fl.Versions[0].Device, file); ok {
|
||||
// A failure to get the file here is surprising and our
|
||||
// global size data will be incorrect until a restart...
|
||||
meta.addFile(protocol.GlobalDeviceID, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) deleteKeyPrefix(prefix []byte) {
|
||||
@@ -110,9 +110,8 @@ func TestGlobalOverHTTPS(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate a server certificate, using fewer bits than usual to hurry the
|
||||
// process along a bit.
|
||||
cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing", 1024)
|
||||
// Generate a server certificate.
|
||||
cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -176,9 +175,8 @@ func TestGlobalAnnounce(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Generate a server certificate, using fewer bits than usual to hurry the
|
||||
// process along a bit.
|
||||
cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing", 1024)
|
||||
// Generate a server certificate.
|
||||
cert, err := tlsutil.NewCertificate(dir+"/cert.pem", dir+"/key.pem", "syncthing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ type FileInfo interface {
|
||||
// FileMode is similar to os.FileMode
|
||||
type FileMode uint32
|
||||
|
||||
func (fm FileMode) String() string {
|
||||
return os.FileMode(fm).String()
|
||||
}
|
||||
|
||||
// Usage represents filesystem space usage
|
||||
type Usage struct {
|
||||
Free int64
|
||||
|
||||
@@ -98,3 +98,11 @@ func TestCanonicalize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileModeString(t *testing.T) {
|
||||
var fm FileMode = 0777
|
||||
exp := "-rwxrwxrwx"
|
||||
if fm.String() != exp {
|
||||
t.Fatalf("Got %v, expected %v", fm.String(), exp)
|
||||
}
|
||||
}
|
||||
|
||||
+29
-30
@@ -28,7 +28,10 @@ const (
|
||||
NumLevels
|
||||
)
|
||||
|
||||
const DebugFlags = log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile
|
||||
const (
|
||||
DefaultFlags = log.Ltime
|
||||
DebugFlags = log.Ltime | log.Ldate | log.Lmicroseconds | log.Lshortfile
|
||||
)
|
||||
|
||||
// A MessageHandler is called with the log level and message text.
|
||||
type MessageHandler func(l LogLevel, msg string)
|
||||
@@ -57,8 +60,8 @@ type Logger interface {
|
||||
type logger struct {
|
||||
logger *log.Logger
|
||||
handlers [NumLevels][]MessageHandler
|
||||
facilities map[string]string // facility name => description
|
||||
debug map[string]bool // facility name => debugging enabled
|
||||
facilities map[string]string // facility name => description
|
||||
debug map[string]struct{} // only facility names with debugging enabled
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
@@ -66,16 +69,17 @@ type logger struct {
|
||||
var DefaultLogger = New()
|
||||
|
||||
func New() Logger {
|
||||
res := &logger{
|
||||
facilities: make(map[string]string),
|
||||
debug: make(map[string]struct{}),
|
||||
}
|
||||
if os.Getenv("LOGGER_DISCARD") != "" {
|
||||
// Hack to completely disable logging, for example when running benchmarks.
|
||||
return &logger{
|
||||
logger: log.New(ioutil.Discard, "", 0),
|
||||
}
|
||||
}
|
||||
|
||||
return &logger{
|
||||
logger: log.New(os.Stdout, "", log.Ltime),
|
||||
res.logger = log.New(ioutil.Discard, "", 0)
|
||||
return res
|
||||
}
|
||||
res.logger = log.New(os.Stdout, "", DefaultFlags)
|
||||
return res
|
||||
}
|
||||
|
||||
// AddHandler registers a new MessageHandler to receive messages with the
|
||||
@@ -207,7 +211,7 @@ func (l *logger) Fatalf(format string, vals ...interface{}) {
|
||||
// ShouldDebug returns true if the given facility has debugging enabled.
|
||||
func (l *logger) ShouldDebug(facility string) bool {
|
||||
l.mut.Lock()
|
||||
res := l.debug[facility]
|
||||
_, res := l.debug[facility]
|
||||
l.mut.Unlock()
|
||||
return res
|
||||
}
|
||||
@@ -215,20 +219,25 @@ func (l *logger) ShouldDebug(facility string) bool {
|
||||
// SetDebug enabled or disables debugging for the given facility name.
|
||||
func (l *logger) SetDebug(facility string, enabled bool) {
|
||||
l.mut.Lock()
|
||||
l.debug[facility] = enabled
|
||||
l.mut.Unlock()
|
||||
l.SetFlags(DebugFlags)
|
||||
defer l.mut.Unlock()
|
||||
if _, ok := l.debug[facility]; enabled && !ok {
|
||||
l.SetFlags(DebugFlags)
|
||||
l.debug[facility] = struct{}{}
|
||||
} else if !enabled && ok {
|
||||
delete(l.debug, facility)
|
||||
if len(l.debug) == 0 {
|
||||
l.SetFlags(DefaultFlags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FacilityDebugging returns the set of facilities that have debugging
|
||||
// enabled.
|
||||
func (l *logger) FacilityDebugging() []string {
|
||||
var enabled []string
|
||||
enabled := make([]string, 0, len(l.debug))
|
||||
l.mut.Lock()
|
||||
for facility, isEnabled := range l.debug {
|
||||
if isEnabled {
|
||||
enabled = append(enabled, facility)
|
||||
}
|
||||
for facility := range l.debug {
|
||||
enabled = append(enabled, facility)
|
||||
}
|
||||
l.mut.Unlock()
|
||||
return enabled
|
||||
@@ -249,17 +258,7 @@ func (l *logger) Facilities() map[string]string {
|
||||
// NewFacility returns a new logger bound to the named facility.
|
||||
func (l *logger) NewFacility(facility, description string) Logger {
|
||||
l.mut.Lock()
|
||||
if l.facilities == nil {
|
||||
l.facilities = make(map[string]string)
|
||||
}
|
||||
if description != "" {
|
||||
l.facilities[facility] = description
|
||||
}
|
||||
|
||||
if l.debug == nil {
|
||||
l.debug = make(map[string]bool)
|
||||
}
|
||||
l.debug[facility] = false
|
||||
l.facilities[facility] = description
|
||||
l.mut.Unlock()
|
||||
|
||||
return &facilityLogger{
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package model
|
||||
|
||||
import "sync"
|
||||
|
||||
type byteSemaphore struct {
|
||||
max int
|
||||
available int
|
||||
mut sync.Mutex
|
||||
cond *sync.Cond
|
||||
}
|
||||
|
||||
func newByteSemaphore(max int) *byteSemaphore {
|
||||
s := byteSemaphore{
|
||||
max: max,
|
||||
available: max,
|
||||
}
|
||||
s.cond = sync.NewCond(&s.mut)
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *byteSemaphore) take(bytes int) {
|
||||
if bytes > s.max {
|
||||
bytes = s.max
|
||||
}
|
||||
s.mut.Lock()
|
||||
for bytes > s.available {
|
||||
s.cond.Wait()
|
||||
}
|
||||
s.available -= bytes
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *byteSemaphore) give(bytes int) {
|
||||
if bytes > s.max {
|
||||
bytes = s.max
|
||||
}
|
||||
s.mut.Lock()
|
||||
if s.available+bytes > s.max {
|
||||
panic("bug: can never give more than max")
|
||||
}
|
||||
s.available += bytes
|
||||
s.cond.Broadcast()
|
||||
s.mut.Unlock()
|
||||
}
|
||||
+325
-5
@@ -11,14 +11,20 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/ignore"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/scanner"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/watchaggregator"
|
||||
)
|
||||
@@ -41,6 +47,8 @@ type folder struct {
|
||||
scanDelay chan time.Duration
|
||||
initialScanFinished chan struct{}
|
||||
stopped chan struct{}
|
||||
scanErrors []FileError
|
||||
scanErrorsMut sync.Mutex
|
||||
|
||||
pullScheduled chan struct{}
|
||||
|
||||
@@ -80,6 +88,7 @@ func newFolder(model *Model, cfg config.FolderConfiguration) folder {
|
||||
scanDelay: make(chan time.Duration),
|
||||
initialScanFinished: make(chan struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
scanErrorsMut: sync.NewMutex(),
|
||||
|
||||
pullScheduled: make(chan struct{}, 1), // This needs to be 1-buffered so that we queue a pull if we're busy when it comes.
|
||||
|
||||
@@ -266,14 +275,255 @@ func (f *folder) getHealthError() error {
|
||||
}
|
||||
|
||||
func (f *folder) scanSubdirs(subDirs []string) error {
|
||||
if err := f.model.internalScanFolderSubdirs(f.ctx, f.folderID, subDirs, f.localFlags); err != nil {
|
||||
// Potentially sets the error twice, once in the scanner just
|
||||
// by doing a check, and once here, if the error returned is
|
||||
// the same one as returned by CheckHealth, though
|
||||
// duplicate set is handled by setError.
|
||||
if err := f.CheckHealth(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.model.fmut.RLock()
|
||||
fset := f.model.folderFiles[f.ID]
|
||||
ignores := f.model.folderIgnores[f.ID]
|
||||
f.model.fmut.RUnlock()
|
||||
mtimefs := fset.MtimeFS()
|
||||
|
||||
for i := range subDirs {
|
||||
sub := osutil.NativeFilename(subDirs[i])
|
||||
|
||||
if sub == "" {
|
||||
// A blank subdirs means to scan the entire folder. We can trim
|
||||
// the subDirs list and go on our way.
|
||||
subDirs = nil
|
||||
break
|
||||
}
|
||||
|
||||
subDirs[i] = sub
|
||||
}
|
||||
|
||||
// Check if the ignore patterns changed as part of scanning this folder.
|
||||
// If they did we should schedule a pull of the folder so that we
|
||||
// request things we might have suddenly become unignored and so on.
|
||||
oldHash := ignores.Hash()
|
||||
defer func() {
|
||||
if ignores.Hash() != oldHash {
|
||||
l.Debugln("Folder", f.ID, "ignore patterns changed; triggering puller")
|
||||
f.IgnoresUpdated()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
|
||||
err = fmt.Errorf("loading ignores: %v", err)
|
||||
f.setError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Clean the list of subitems to ensure that we start at a known
|
||||
// directory, and don't scan subdirectories of things we've already
|
||||
// scanned.
|
||||
subDirs = unifySubs(subDirs, func(f string) bool {
|
||||
_, ok := fset.Get(protocol.LocalDeviceID, f)
|
||||
return ok
|
||||
})
|
||||
|
||||
f.setState(FolderScanning)
|
||||
|
||||
fchan := scanner.Walk(f.ctx, scanner.Config{
|
||||
Folder: f.ID,
|
||||
Subs: subDirs,
|
||||
Matcher: ignores,
|
||||
TempLifetime: time.Duration(f.model.cfg.Options().KeepTemporariesH) * time.Hour,
|
||||
CurrentFiler: cFiler{f.model, f.ID},
|
||||
Filesystem: mtimefs,
|
||||
IgnorePerms: f.IgnorePerms,
|
||||
AutoNormalize: f.AutoNormalize,
|
||||
Hashers: f.model.numHashers(f.ID),
|
||||
ShortID: f.model.shortID,
|
||||
ProgressTickIntervalS: f.ScanProgressIntervalS,
|
||||
UseLargeBlocks: f.UseLargeBlocks,
|
||||
LocalFlags: f.localFlags,
|
||||
})
|
||||
|
||||
batchFn := func(fs []protocol.FileInfo) error {
|
||||
if err := f.CheckHealth(); err != nil {
|
||||
l.Debugf("Stopping scan of folder %s due to: %s", f.Description(), err)
|
||||
return err
|
||||
}
|
||||
f.model.updateLocalsFromScanning(f.ID, fs)
|
||||
return nil
|
||||
}
|
||||
// Resolve items which are identical with the global state.
|
||||
if f.localFlags&protocol.FlagLocalReceiveOnly != 0 {
|
||||
oldBatchFn := batchFn // can't reference batchFn directly (recursion)
|
||||
batchFn = func(fs []protocol.FileInfo) error {
|
||||
for i := range fs {
|
||||
switch gf, ok := fset.GetGlobal(fs[i].Name); {
|
||||
case !ok:
|
||||
continue
|
||||
case gf.IsEquivalentOptional(fs[i], false, false, protocol.FlagLocalReceiveOnly):
|
||||
// What we have locally is equivalent to the global file.
|
||||
fs[i].Version = fs[i].Version.Merge(gf.Version)
|
||||
fallthrough
|
||||
case fs[i].IsDeleted() && gf.IsReceiveOnlyChanged():
|
||||
// Our item is deleted and the global item is our own
|
||||
// receive only file. We can't delete file infos, so
|
||||
// we just pretend it is a normal deleted file (nobody
|
||||
// cares about that).
|
||||
fs[i].LocalFlags &^= protocol.FlagLocalReceiveOnly
|
||||
}
|
||||
}
|
||||
return oldBatchFn(fs)
|
||||
}
|
||||
}
|
||||
batch := newFileInfoBatch(batchFn)
|
||||
|
||||
// Schedule a pull after scanning, but only if we actually detected any
|
||||
// changes.
|
||||
changes := 0
|
||||
defer func() {
|
||||
if changes > 0 {
|
||||
f.SchedulePull()
|
||||
}
|
||||
}()
|
||||
|
||||
f.clearScanErrors(subDirs)
|
||||
for res := range fchan {
|
||||
if res.Err != nil {
|
||||
f.newScanError(res.Path, res.Err)
|
||||
continue
|
||||
}
|
||||
if err := batch.flushIfFull(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
batch.append(res.File)
|
||||
changes++
|
||||
}
|
||||
|
||||
if err := batch.flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(subDirs) == 0 {
|
||||
// If we have no specific subdirectories to traverse, set it to one
|
||||
// empty prefix so we traverse the entire folder contents once.
|
||||
subDirs = []string{""}
|
||||
}
|
||||
|
||||
// Do a scan of the database for each prefix, to check for deleted and
|
||||
// ignored files.
|
||||
var toIgnore []db.FileInfoTruncated
|
||||
ignoredParent := ""
|
||||
pathSep := string(fs.PathSeparator)
|
||||
for _, sub := range subDirs {
|
||||
var iterError error
|
||||
|
||||
fset.WithPrefixedHaveTruncated(protocol.LocalDeviceID, sub, func(fi db.FileIntf) bool {
|
||||
file := fi.(db.FileInfoTruncated)
|
||||
|
||||
if err := batch.flushIfFull(); err != nil {
|
||||
iterError = err
|
||||
return false
|
||||
}
|
||||
|
||||
if ignoredParent != "" && !strings.HasPrefix(file.Name, ignoredParent+pathSep) {
|
||||
for _, file := range toIgnore {
|
||||
l.Debugln("marking file as ignored", file)
|
||||
nf := file.ConvertToIgnoredFileInfo(f.model.id.Short())
|
||||
batch.append(nf)
|
||||
changes++
|
||||
if err := batch.flushIfFull(); err != nil {
|
||||
iterError = err
|
||||
return false
|
||||
}
|
||||
}
|
||||
toIgnore = toIgnore[:0]
|
||||
ignoredParent = ""
|
||||
}
|
||||
|
||||
switch ignored := ignores.Match(file.Name).IsIgnored(); {
|
||||
case !file.IsIgnored() && ignored:
|
||||
// File was not ignored at last pass but has been ignored.
|
||||
if file.IsDirectory() {
|
||||
// Delay ignoring as a child might be unignored.
|
||||
toIgnore = append(toIgnore, file)
|
||||
if ignoredParent == "" {
|
||||
// If the parent wasn't ignored already, set
|
||||
// this path as the "highest" ignored parent
|
||||
ignoredParent = file.Name
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
l.Debugln("marking file as ignored", f)
|
||||
nf := file.ConvertToIgnoredFileInfo(f.model.id.Short())
|
||||
batch.append(nf)
|
||||
changes++
|
||||
|
||||
case file.IsIgnored() && !ignored:
|
||||
// Successfully scanned items are already un-ignored during
|
||||
// the scan, so check whether it is deleted.
|
||||
fallthrough
|
||||
case !file.IsIgnored() && !file.IsDeleted() && !file.IsUnsupported():
|
||||
// The file is not ignored, deleted or unsupported. Lets check if
|
||||
// it's still here. Simply stat:ing it wont do as there are
|
||||
// tons of corner cases (e.g. parent dir->symlink, missing
|
||||
// permissions)
|
||||
if !osutil.IsDeleted(mtimefs, file.Name) {
|
||||
if ignoredParent != "" {
|
||||
// Don't ignore parents of this not ignored item
|
||||
toIgnore = toIgnore[:0]
|
||||
ignoredParent = ""
|
||||
}
|
||||
return true
|
||||
}
|
||||
nf := protocol.FileInfo{
|
||||
Name: file.Name,
|
||||
Type: file.Type,
|
||||
Size: 0,
|
||||
ModifiedS: file.ModifiedS,
|
||||
ModifiedNs: file.ModifiedNs,
|
||||
ModifiedBy: f.model.id.Short(),
|
||||
Deleted: true,
|
||||
Version: file.Version.Update(f.model.shortID),
|
||||
LocalFlags: f.localFlags,
|
||||
}
|
||||
// We do not want to override the global version
|
||||
// with the deleted file. Keeping only our local
|
||||
// counter makes sure we are in conflict with any
|
||||
// other existing versions, which will be resolved
|
||||
// by the normal pulling mechanisms.
|
||||
if file.ShouldConflict() {
|
||||
nf.Version = nf.Version.DropOthers(f.model.shortID)
|
||||
}
|
||||
|
||||
batch.append(nf)
|
||||
changes++
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if iterError == nil && len(toIgnore) > 0 {
|
||||
for _, file := range toIgnore {
|
||||
l.Debugln("marking file as ignored", f)
|
||||
nf := file.ConvertToIgnoredFileInfo(f.model.id.Short())
|
||||
batch.append(nf)
|
||||
changes++
|
||||
if iterError = batch.flushIfFull(); iterError != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
toIgnore = toIgnore[:0]
|
||||
}
|
||||
|
||||
if iterError != nil {
|
||||
return iterError
|
||||
}
|
||||
}
|
||||
|
||||
if err := batch.flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.model.folderStatRef(f.ID).ScanCompleted()
|
||||
f.setState(FolderIdle)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -430,3 +680,73 @@ func (f *folder) basePause() time.Duration {
|
||||
func (f *folder) String() string {
|
||||
return fmt.Sprintf("%s/%s@%p", f.Type, f.folderID, f)
|
||||
}
|
||||
|
||||
func (f *folder) newScanError(path string, err error) {
|
||||
f.scanErrorsMut.Lock()
|
||||
f.scanErrors = append(f.scanErrors, FileError{
|
||||
Err: err.Error(),
|
||||
Path: path,
|
||||
})
|
||||
f.scanErrorsMut.Unlock()
|
||||
}
|
||||
|
||||
func (f *folder) clearScanErrors(subDirs []string) {
|
||||
f.scanErrorsMut.Lock()
|
||||
defer f.scanErrorsMut.Unlock()
|
||||
if len(subDirs) == 0 {
|
||||
f.scanErrors = nil
|
||||
return
|
||||
}
|
||||
filtered := f.scanErrors[:0]
|
||||
pathSep := string(fs.PathSeparator)
|
||||
outer:
|
||||
for _, fe := range f.scanErrors {
|
||||
for _, sub := range subDirs {
|
||||
if strings.HasPrefix(fe.Path, sub+pathSep) {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
filtered = append(filtered, fe)
|
||||
}
|
||||
f.scanErrors = filtered
|
||||
}
|
||||
|
||||
func (f *folder) Errors() []FileError {
|
||||
f.scanErrorsMut.Lock()
|
||||
defer f.scanErrorsMut.Unlock()
|
||||
return append([]FileError{}, f.scanErrors...)
|
||||
}
|
||||
|
||||
// The exists function is expected to return true for all known paths
|
||||
// (excluding "" and ".")
|
||||
func unifySubs(dirs []string, exists func(dir string) bool) []string {
|
||||
if len(dirs) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(dirs)
|
||||
if dirs[0] == "" || dirs[0] == "." || dirs[0] == string(fs.PathSeparator) {
|
||||
return nil
|
||||
}
|
||||
prev := "./" // Anything that can't be parent of a clean path
|
||||
for i := 0; i < len(dirs); {
|
||||
dir, err := fs.Canonicalize(dirs[i])
|
||||
if err != nil {
|
||||
l.Debugf("Skipping %v for scan: %s", dirs[i], err)
|
||||
dirs = append(dirs[:i], dirs[i+1:]...)
|
||||
continue
|
||||
}
|
||||
if dir == prev || strings.HasPrefix(dir, prev+string(fs.PathSeparator)) {
|
||||
dirs = append(dirs[:i], dirs[i+1:]...)
|
||||
continue
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
for parent != "." && parent != string(fs.PathSeparator) && !exists(parent) {
|
||||
dir = parent
|
||||
parent = filepath.Dir(dir)
|
||||
}
|
||||
dirs[i] = dir
|
||||
prev = dir
|
||||
i++
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
@@ -196,8 +196,8 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
|
||||
|
||||
size = m.GlobalSize("ro")
|
||||
const sizeOfDir = 128
|
||||
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
|
||||
t.Fatalf("Global: expected the new file to be reflected: %+v", size)
|
||||
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(oldData)) {
|
||||
t.Fatalf("Global: expected no change due to the new file: %+v", size)
|
||||
}
|
||||
size = m.LocalSize("ro")
|
||||
if size.Files != 1 || size.Bytes != sizeOfDir+int64(len(newData)) {
|
||||
@@ -230,6 +230,99 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecvOnlyUndoChanges(t *testing.T) {
|
||||
if err := os.RemoveAll("_recvonly"); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll("_recvonly"); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create some test data
|
||||
|
||||
if err := os.MkdirAll("_recvonly/.stfolder", 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
oldData := []byte("hello\n")
|
||||
knownFiles := setupKnownFiles(t, oldData)
|
||||
|
||||
// Get us a model up and running
|
||||
|
||||
m := setupROFolder()
|
||||
defer m.Stop()
|
||||
|
||||
m.fmut.Lock()
|
||||
fset := m.folderFiles["ro"]
|
||||
m.fmut.Unlock()
|
||||
folderFs := fset.MtimeFS()
|
||||
|
||||
// Send and index update for the known stuff
|
||||
|
||||
m.Index(device1, "ro", knownFiles)
|
||||
m.updateLocalsFromScanning("ro", knownFiles)
|
||||
|
||||
// Start the folder. This will cause a scan.
|
||||
|
||||
m.StartFolder("ro")
|
||||
m.ScanFolder("ro")
|
||||
|
||||
// Everything should be in sync.
|
||||
|
||||
size := m.GlobalSize("ro")
|
||||
if size.Files != 1 || size.Directories != 1 {
|
||||
t.Fatalf("Global: expected 1 file and 1 directory: %+v", size)
|
||||
}
|
||||
size = m.LocalSize("ro")
|
||||
if size.Files != 1 || size.Directories != 1 {
|
||||
t.Fatalf("Local: expected 1 file and 1 directory: %+v", size)
|
||||
}
|
||||
size = m.NeedSize("ro")
|
||||
if size.Files+size.Directories > 0 {
|
||||
t.Fatalf("Need: expected nothing: %+v", size)
|
||||
}
|
||||
size = m.ReceiveOnlyChangedSize("ro")
|
||||
if size.Files+size.Directories > 0 {
|
||||
t.Fatalf("ROChanged: expected nothing: %+v", size)
|
||||
}
|
||||
|
||||
// Create a file and modify another
|
||||
|
||||
file := "_recvonly/foo"
|
||||
if err := ioutil.WriteFile(file, []byte("hello\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile("_recvonly/knownDir/knownFile", []byte("bye\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m.ScanFolder("ro")
|
||||
|
||||
size = m.ReceiveOnlyChangedSize("ro")
|
||||
if size.Files != 2 {
|
||||
t.Fatalf("Receive only: expected 2 files: %+v", size)
|
||||
}
|
||||
|
||||
// Remove the file again and undo the modification
|
||||
|
||||
if err := os.Remove(file); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ioutil.WriteFile("_recvonly/knownDir/knownFile", oldData, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
folderFs.Chtimes("knownDir/knownFile", knownFiles[1].ModTime(), knownFiles[1].ModTime())
|
||||
|
||||
m.ScanFolder("ro")
|
||||
|
||||
size = m.ReceiveOnlyChangedSize("ro")
|
||||
if size.Files+size.Directories+size.Deleted != 0 {
|
||||
t.Fatalf("Receive only: expected all zero: %+v", size)
|
||||
}
|
||||
}
|
||||
|
||||
func setupKnownFiles(t *testing.T, data []byte) []protocol.FileInfo {
|
||||
if err := os.MkdirAll("_recvonly/knownDir", 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
+85
-134
@@ -15,7 +15,6 @@ import (
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
stdsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
@@ -102,17 +101,17 @@ type sendReceiveFolder struct {
|
||||
|
||||
queue *jobQueue
|
||||
|
||||
errors map[string]string // path -> error string
|
||||
errorsMut sync.Mutex
|
||||
pullErrors map[string]string // path -> error string
|
||||
pullErrorsMut sync.Mutex
|
||||
}
|
||||
|
||||
func newSendReceiveFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner, fs fs.Filesystem) service {
|
||||
f := &sendReceiveFolder{
|
||||
folder: newFolder(model, cfg),
|
||||
fs: fs,
|
||||
versioner: ver,
|
||||
queue: newJobQueue(),
|
||||
errorsMut: sync.NewMutex(),
|
||||
folder: newFolder(model, cfg),
|
||||
fs: fs,
|
||||
versioner: ver,
|
||||
queue: newJobQueue(),
|
||||
pullErrorsMut: sync.NewMutex(),
|
||||
}
|
||||
f.folder.puller = f
|
||||
|
||||
@@ -141,11 +140,6 @@ func (f *sendReceiveFolder) pull() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if err := f.CheckHealth(); err != nil {
|
||||
l.Debugln("Skipping pull of", f.Description(), "due to folder error:", err)
|
||||
return true
|
||||
}
|
||||
|
||||
f.model.fmut.RLock()
|
||||
curIgnores := f.model.folderIgnores[f.folderID]
|
||||
folderFiles := f.model.folderFiles[f.folderID]
|
||||
@@ -161,13 +155,18 @@ func (f *sendReceiveFolder) pull() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if err := f.CheckHealth(); err != nil {
|
||||
l.Debugln("Skipping pull of", f.Description(), "due to folder error:", err)
|
||||
return true
|
||||
}
|
||||
|
||||
curIgnoreHash := curIgnores.Hash()
|
||||
ignoresChanged := curIgnoreHash != f.prevIgnoreHash
|
||||
|
||||
l.Debugf("%v pulling (ignoresChanged=%v)", f, ignoresChanged)
|
||||
|
||||
f.setState(FolderSyncing)
|
||||
f.clearErrors()
|
||||
f.clearPullErrors()
|
||||
|
||||
scanChan := make(chan string)
|
||||
go f.pullScannerRoutine(scanChan)
|
||||
@@ -204,10 +203,10 @@ func (f *sendReceiveFolder) pull() bool {
|
||||
// we're not making it. Probably there are write
|
||||
// errors preventing us. Flag this with a warning and
|
||||
// wait a bit longer before retrying.
|
||||
if folderErrors := f.PullErrors(); len(folderErrors) > 0 {
|
||||
if errors := f.Errors(); len(errors) > 0 {
|
||||
events.Default.Log(events.FolderErrors, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
"errors": folderErrors,
|
||||
"errors": errors,
|
||||
})
|
||||
}
|
||||
break
|
||||
@@ -327,7 +326,7 @@ func (f *sendReceiveFolder) processNeeded(ignores *ignore.Matcher, folderFiles *
|
||||
changed++
|
||||
|
||||
case runtime.GOOS == "windows" && fs.WindowsInvalidFilename(file.Name):
|
||||
f.newError("pull", file.Name, fs.ErrInvalidFilename)
|
||||
f.newPullError("pull", file.Name, fs.ErrInvalidFilename)
|
||||
|
||||
case file.IsDeleted():
|
||||
if file.IsDirectory() {
|
||||
@@ -448,7 +447,7 @@ nextFile:
|
||||
continue
|
||||
}
|
||||
|
||||
if fi.IsDeleted() || fi.Type != protocol.FileInfoTypeFile {
|
||||
if fi.IsDeleted() || fi.IsInvalid() || fi.Type != protocol.FileInfoTypeFile {
|
||||
// The item has changed type or status in the index while we
|
||||
// were processing directories above.
|
||||
f.queue.Done(fileName)
|
||||
@@ -474,10 +473,14 @@ nextFile:
|
||||
// desired state with the delete bit set is in the deletion
|
||||
// map.
|
||||
desired := fileDeletions[candidate.Name]
|
||||
// Remove the pending deletion (as we perform it by renaming)
|
||||
delete(fileDeletions, candidate.Name)
|
||||
if err := f.renameFile(candidate, desired, fi, dbUpdateChan, scanChan); err != nil {
|
||||
// Failed to rename, try to handle files as separate
|
||||
// deletions and updates.
|
||||
break
|
||||
}
|
||||
|
||||
f.renameFile(candidate, desired, fi, dbUpdateChan, scanChan)
|
||||
// Remove the pending deletion (as we performed it by renaming)
|
||||
delete(fileDeletions, candidate.Name)
|
||||
|
||||
f.queue.Done(fileName)
|
||||
continue nextFile
|
||||
@@ -493,7 +496,7 @@ nextFile:
|
||||
continue nextFile
|
||||
}
|
||||
}
|
||||
f.newError("pull", fileName, errNotAvailable)
|
||||
f.newPullError("pull", fileName, errNotAvailable)
|
||||
}
|
||||
|
||||
return changed, fileDeletions, dirDeletions, nil
|
||||
@@ -509,7 +512,7 @@ func (f *sendReceiveFolder) processDeletions(ignores *ignore.Matcher, fileDeleti
|
||||
|
||||
l.Debugln(f, "Deleting file", file.Name)
|
||||
if update, err := f.deleteFile(file, scanChan); err != nil {
|
||||
f.newError("delete file", file.Name, err)
|
||||
f.newPullError("delete file", file.Name, err)
|
||||
} else {
|
||||
dbUpdateChan <- update
|
||||
}
|
||||
@@ -569,7 +572,7 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<
|
||||
case err == nil && (!info.IsDir() || info.IsSymlink()):
|
||||
err = osutil.InWritableDir(f.fs.Remove, f.fs, file.Name)
|
||||
if err != nil {
|
||||
f.newError("dir replace", file.Name, err)
|
||||
f.newPullError("dir replace", file.Name, err)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
@@ -599,13 +602,13 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<
|
||||
if err = osutil.InWritableDir(mkdir, f.fs, file.Name); err == nil {
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateHandleDir}
|
||||
} else {
|
||||
f.newError("dir mkdir", file.Name, err)
|
||||
f.newPullError("dir mkdir", file.Name, err)
|
||||
}
|
||||
return
|
||||
// Weird error when stat()'ing the dir. Probably won't work to do
|
||||
// anything else with it if we can't even stat() it.
|
||||
case err != nil:
|
||||
f.newError("dir stat", file.Name, err)
|
||||
f.newPullError("dir stat", file.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -617,7 +620,7 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<
|
||||
} else if err := f.fs.Chmod(file.Name, mode|(fs.FileMode(info.Mode())&retainBits)); err == nil {
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateHandleDir}
|
||||
} else {
|
||||
f.newError("dir chmod", file.Name, err)
|
||||
f.newPullError("dir chmod", file.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,7 +630,7 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
|
||||
parent := filepath.Dir(file)
|
||||
|
||||
if err := osutil.TraversesSymlink(f.fs, parent); err != nil {
|
||||
f.newError("traverses q", file, err)
|
||||
f.newPullError("traverses q", file, err)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -648,7 +651,7 @@ func (f *sendReceiveFolder) checkParent(file string, scanChan chan<- string) boo
|
||||
}
|
||||
l.Debugf("%v resurrecting parent directory of %v", f, file)
|
||||
if err := f.fs.MkdirAll(parent, 0755); err != nil {
|
||||
f.newError("resurrecting parent dir", file, err)
|
||||
f.newPullError("resurrecting parent dir", file, err)
|
||||
return false
|
||||
}
|
||||
scanChan <- parent
|
||||
@@ -687,7 +690,7 @@ func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, dbUpdateChan c
|
||||
// Index entry from a Syncthing predating the support for including
|
||||
// the link target in the index entry. We log this as an error.
|
||||
err = errors.New("incompatible symlink entry; rescan with newer Syncthing on source")
|
||||
f.newError("symlink", file.Name, err)
|
||||
f.newPullError("symlink", file.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -697,7 +700,7 @@ func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, dbUpdateChan c
|
||||
// path.
|
||||
err = osutil.InWritableDir(f.fs.Remove, f.fs, file.Name)
|
||||
if err != nil {
|
||||
f.newError("symlink remove", file.Name, err)
|
||||
f.newPullError("symlink remove", file.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -711,7 +714,7 @@ func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, dbUpdateChan c
|
||||
if err = osutil.InWritableDir(createLink, f.fs, file.Name); err == nil {
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateHandleSymlink}
|
||||
} else {
|
||||
f.newError("symlink create", file.Name, err)
|
||||
f.newPullError("symlink create", file.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -739,7 +742,7 @@ func (f *sendReceiveFolder) handleDeleteDir(file protocol.FileInfo, ignores *ign
|
||||
}()
|
||||
|
||||
if err = f.deleteDir(file.Name, ignores, scanChan); err != nil {
|
||||
f.newError("delete dir", file.Name, err)
|
||||
f.newPullError("delete dir", file.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -812,7 +815,7 @@ func (f *sendReceiveFolder) deleteFile(file protocol.FileInfo, scanChan chan<- s
|
||||
|
||||
// renameFile attempts to rename an existing file to a destination
|
||||
// and set the right attributes on it.
|
||||
func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) {
|
||||
func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
@@ -851,9 +854,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, db
|
||||
|
||||
// Check that source is compatible with what we have in the DB
|
||||
if err = f.checkToBeDeleted(cur, scanChan); err != nil {
|
||||
err = fmt.Errorf("from %s: %s", source.Name, err.Error())
|
||||
f.newError("rename check source", target.Name, err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
// Check that the target corresponds to what we have in the DB
|
||||
curTarget, ok := f.model.CurrentFolderFile(f.folderID, target.Name)
|
||||
@@ -874,7 +875,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, db
|
||||
err = errModified
|
||||
default:
|
||||
if fi, err := scanner.CreateFileInfo(stat, target.Name, f.fs); err == nil {
|
||||
if !fi.IsEquivalentOptional(curTarget, false, true, protocol.LocalAllFlags) {
|
||||
if !fi.IsEquivalentOptional(curTarget, f.IgnorePerms, true, protocol.LocalAllFlags) {
|
||||
// Target changed
|
||||
scanChan <- target.Name
|
||||
err = errModified
|
||||
@@ -882,9 +883,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, db
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("from %s: %s", source.Name, err.Error())
|
||||
f.newError("rename check target", target.Name, err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
tempName := fs.TempName(target.Name)
|
||||
@@ -900,36 +899,26 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, db
|
||||
} else {
|
||||
err = osutil.TryRename(f.fs, source.Name, tempName)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
blockStatsMut.Lock()
|
||||
blockStats["total"] += len(target.Blocks)
|
||||
blockStats["renamed"] += len(target.Blocks)
|
||||
blockStatsMut.Unlock()
|
||||
|
||||
// The file was renamed, so we have handled both the necessary delete
|
||||
// of the source and the creation of the target. Fix-up the metadata,
|
||||
// update the local index of the target file and rename from temp to real name.
|
||||
|
||||
dbUpdateChan <- dbUpdateJob{source, dbUpdateDeleteFile}
|
||||
|
||||
if err = f.performFinish(nil, target, curTarget, true, tempName, dbUpdateChan, scanChan); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// We failed the rename so we have a source file that we still need to
|
||||
// get rid of. Attempt to delete it instead so that we make *some*
|
||||
// progress. The target is unhandled.
|
||||
|
||||
err = osutil.InWritableDir(f.fs.Remove, f.fs, source.Name)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("from %s: %s", source.Name, err.Error())
|
||||
f.newError("rename delete", target.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
dbUpdateChan <- dbUpdateJob{source, dbUpdateDeleteFile}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
blockStatsMut.Lock()
|
||||
blockStats["total"] += len(target.Blocks)
|
||||
blockStats["renamed"] += len(target.Blocks)
|
||||
blockStatsMut.Unlock()
|
||||
|
||||
// The file was renamed, so we have handled both the necessary delete
|
||||
// of the source and the creation of the target temp file. Fix-up the metadata,
|
||||
// update the local index of the target file and rename from temp to real name.
|
||||
|
||||
if err = f.performFinish(nil, target, curTarget, true, tempName, dbUpdateChan, scanChan); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbUpdateChan <- dbUpdateJob{source, dbUpdateDeleteFile}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is the flow of data and events here, I think...
|
||||
@@ -978,6 +967,7 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c
|
||||
// are only updating metadata, so we don't actually *need* to make the
|
||||
// copy.
|
||||
f.shortcutFile(file, curFile, dbUpdateChan)
|
||||
return
|
||||
}
|
||||
|
||||
tempName := fs.TempName(file.Name)
|
||||
@@ -1027,7 +1017,7 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c
|
||||
}
|
||||
|
||||
if err := f.CheckAvailableSpace(blocksSize); err != nil {
|
||||
f.newError("pulling file", file.Name, err)
|
||||
f.newPullError("pulling file", file.Name, err)
|
||||
f.queue.Done(file.Name)
|
||||
return
|
||||
}
|
||||
@@ -1139,7 +1129,7 @@ func (f *sendReceiveFolder) shortcutFile(file, curFile protocol.FileInfo, dbUpda
|
||||
|
||||
if !f.IgnorePerms && !file.NoPermissions {
|
||||
if err = f.fs.Chmod(file.Name, fs.FileMode(file.Permissions&0777)); err != nil {
|
||||
f.newError("shortcut", file.Name, err)
|
||||
f.newPullError("shortcut", file.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1151,14 +1141,15 @@ func (f *sendReceiveFolder) shortcutFile(file, curFile protocol.FileInfo, dbUpda
|
||||
file.Version = file.Version.Merge(curFile.Version)
|
||||
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateShortcutFile}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// copierRoutine reads copierStates until the in channel closes and performs
|
||||
// the relevant copies when possible, or passes it to the puller routine.
|
||||
func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pullBlockState, out chan<- *sharedPullerState) {
|
||||
buf := make([]byte, protocol.MinBlockSize)
|
||||
buf := protocol.BufferPool.Get(protocol.MinBlockSize)
|
||||
defer func() {
|
||||
protocol.BufferPool.Put(buf)
|
||||
}()
|
||||
|
||||
for state := range in {
|
||||
dstFd, err := state.tempFile()
|
||||
@@ -1234,11 +1225,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
|
||||
continue
|
||||
}
|
||||
|
||||
if s := int(block.Size); s > cap(buf) {
|
||||
buf = make([]byte, s)
|
||||
} else {
|
||||
buf = buf[:s]
|
||||
}
|
||||
buf = protocol.BufferPool.Upgrade(buf, int(block.Size))
|
||||
|
||||
found, err := weakHashFinder.Iterate(block.WeakHash, buf, func(offset int64) bool {
|
||||
if verifyBuffer(buf, block) != nil {
|
||||
@@ -1554,7 +1541,7 @@ func (f *sendReceiveFolder) finisherRoutine(ignores *ignore.Matcher, in <-chan *
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
f.newError("finisher", state.file.Name, err)
|
||||
f.newPullError("finisher", state.file.Name, err)
|
||||
} else {
|
||||
blockStatsMut.Lock()
|
||||
blockStats["total"] += state.reused + state.copyTotal + state.pullTotal
|
||||
@@ -1779,34 +1766,36 @@ func (f *sendReceiveFolder) moveForConflict(name string, lastModBy string) error
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) newError(context, path string, err error) {
|
||||
f.errorsMut.Lock()
|
||||
defer f.errorsMut.Unlock()
|
||||
func (f *sendReceiveFolder) newPullError(context, path string, err error) {
|
||||
f.pullErrorsMut.Lock()
|
||||
defer f.pullErrorsMut.Unlock()
|
||||
|
||||
// We might get more than one error report for a file (i.e. error on
|
||||
// Write() followed by Close()); we keep the first error as that is
|
||||
// probably closer to the root cause.
|
||||
if _, ok := f.errors[path]; ok {
|
||||
if _, ok := f.pullErrors[path]; ok {
|
||||
return
|
||||
}
|
||||
l.Infof("Puller (folder %s, file %q): %s: %v", f.Description(), path, context, err)
|
||||
f.errors[path] = fmt.Sprintf("%s: %s", context, err.Error())
|
||||
f.pullErrors[path] = fmt.Sprintf("%s: %s", context, err.Error())
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) clearErrors() {
|
||||
f.errorsMut.Lock()
|
||||
f.errors = make(map[string]string)
|
||||
f.errorsMut.Unlock()
|
||||
func (f *sendReceiveFolder) clearPullErrors() {
|
||||
f.pullErrorsMut.Lock()
|
||||
f.pullErrors = make(map[string]string)
|
||||
f.pullErrorsMut.Unlock()
|
||||
}
|
||||
|
||||
func (f *sendReceiveFolder) PullErrors() []FileError {
|
||||
f.errorsMut.Lock()
|
||||
errors := make([]FileError, 0, len(f.errors))
|
||||
for path, err := range f.errors {
|
||||
func (f *sendReceiveFolder) Errors() []FileError {
|
||||
scanErrors := f.folder.Errors()
|
||||
f.pullErrorsMut.Lock()
|
||||
errors := make([]FileError, 0, len(f.pullErrors)+len(f.scanErrors))
|
||||
for path, err := range f.pullErrors {
|
||||
errors = append(errors, FileError{path, err})
|
||||
}
|
||||
f.pullErrorsMut.Unlock()
|
||||
errors = append(errors, scanErrors...)
|
||||
sort.Sort(fileErrorList(errors))
|
||||
f.errorsMut.Unlock()
|
||||
return errors
|
||||
}
|
||||
|
||||
@@ -1891,7 +1880,7 @@ func (f *sendReceiveFolder) checkToBeDeleted(cur protocol.FileInfo, scanChan cha
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.IsEquivalentOptional(cur, false, true, protocol.LocalAllFlags) {
|
||||
if !fi.IsEquivalentOptional(cur, f.IgnorePerms, true, protocol.LocalAllFlags) {
|
||||
// File changed
|
||||
scanChan <- cur.Name
|
||||
return errModified
|
||||
@@ -1944,41 +1933,3 @@ func componentCount(name string) int {
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
type byteSemaphore struct {
|
||||
max int
|
||||
available int
|
||||
mut stdsync.Mutex
|
||||
cond *stdsync.Cond
|
||||
}
|
||||
|
||||
func newByteSemaphore(max int) *byteSemaphore {
|
||||
s := byteSemaphore{
|
||||
max: max,
|
||||
available: max,
|
||||
}
|
||||
s.cond = stdsync.NewCond(&s.mut)
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *byteSemaphore) take(bytes int) {
|
||||
if bytes > s.max {
|
||||
panic("bug: more than max bytes will never be available")
|
||||
}
|
||||
s.mut.Lock()
|
||||
for bytes > s.available {
|
||||
s.cond.Wait()
|
||||
}
|
||||
s.available -= bytes
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *byteSemaphore) give(bytes int) {
|
||||
s.mut.Lock()
|
||||
if s.available+bytes > s.max {
|
||||
panic("bug: can never give more than max")
|
||||
}
|
||||
s.available += bytes
|
||||
s.cond.Broadcast()
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ func setUpSendReceiveFolder(model *Model) *sendReceiveFolder {
|
||||
},
|
||||
},
|
||||
|
||||
queue: newJobQueue(),
|
||||
errors: make(map[string]string),
|
||||
errorsMut: sync.NewMutex(),
|
||||
queue: newJobQueue(),
|
||||
pullErrors: make(map[string]string),
|
||||
pullErrorsMut: sync.NewMutex(),
|
||||
}
|
||||
f.fs = fs.NewMtimeFS(f.Filesystem(), db.NewNamespacedKV(model.db, "mtime"))
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/d4l3k/messagediff"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
)
|
||||
|
||||
type unifySubsCase struct {
|
||||
in []string // input to unifySubs
|
||||
exists []string // paths that exist in the database
|
||||
out []string // expected output
|
||||
}
|
||||
|
||||
func unifySubsCases() []unifySubsCase {
|
||||
cases := []unifySubsCase{
|
||||
{
|
||||
// 0. trailing slashes are cleaned, known paths are just passed on
|
||||
[]string{"foo/", "bar//"},
|
||||
[]string{"foo", "bar"},
|
||||
[]string{"bar", "foo"}, // the output is sorted
|
||||
},
|
||||
{
|
||||
// 1. "foo/bar" gets trimmed as it's covered by foo
|
||||
[]string{"foo", "bar/", "foo/bar/"},
|
||||
[]string{"foo", "bar"},
|
||||
[]string{"bar", "foo"},
|
||||
},
|
||||
{
|
||||
// 2. "" gets simplified to the empty list; ie scan all
|
||||
[]string{"foo", ""},
|
||||
[]string{"foo"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
// 3. "foo/bar" is unknown, but it's kept
|
||||
// because its parent is known
|
||||
[]string{"foo/bar"},
|
||||
[]string{"foo"},
|
||||
[]string{"foo/bar"},
|
||||
},
|
||||
{
|
||||
// 4. two independent known paths, both are kept
|
||||
// "usr/lib" is not a prefix of "usr/libexec"
|
||||
[]string{"usr/lib", "usr/libexec"},
|
||||
[]string{"usr", "usr/lib", "usr/libexec"},
|
||||
[]string{"usr/lib", "usr/libexec"},
|
||||
},
|
||||
{
|
||||
// 5. "usr/lib" is a prefix of "usr/lib/exec"
|
||||
[]string{"usr/lib", "usr/lib/exec"},
|
||||
[]string{"usr", "usr/lib", "usr/libexec"},
|
||||
[]string{"usr/lib"},
|
||||
},
|
||||
{
|
||||
// 6. .stignore and .stfolder are special and are passed on
|
||||
// verbatim even though they are unknown
|
||||
[]string{config.DefaultMarkerName, ".stignore"},
|
||||
[]string{},
|
||||
[]string{config.DefaultMarkerName, ".stignore"},
|
||||
},
|
||||
{
|
||||
// 7. but the presence of something else unknown forces an actual
|
||||
// scan
|
||||
[]string{config.DefaultMarkerName, ".stignore", "foo/bar"},
|
||||
[]string{},
|
||||
[]string{config.DefaultMarkerName, ".stignore", "foo"},
|
||||
},
|
||||
{
|
||||
// 8. explicit request to scan all
|
||||
nil,
|
||||
[]string{"foo"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
// 9. empty list of subs
|
||||
[]string{},
|
||||
[]string{"foo"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
// 10. absolute path
|
||||
[]string{"/foo"},
|
||||
[]string{"foo"},
|
||||
[]string{"foo"},
|
||||
},
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Fixup path separators
|
||||
for i := range cases {
|
||||
for j, p := range cases[i].in {
|
||||
cases[i].in[j] = filepath.FromSlash(p)
|
||||
}
|
||||
for j, p := range cases[i].exists {
|
||||
cases[i].exists[j] = filepath.FromSlash(p)
|
||||
}
|
||||
for j, p := range cases[i].out {
|
||||
cases[i].out[j] = filepath.FromSlash(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cases
|
||||
}
|
||||
|
||||
func unifyExists(f string, tc unifySubsCase) bool {
|
||||
for _, e := range tc.exists {
|
||||
if f == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestUnifySubs(t *testing.T) {
|
||||
cases := unifySubsCases()
|
||||
for i, tc := range cases {
|
||||
exists := func(f string) bool {
|
||||
return unifyExists(f, tc)
|
||||
}
|
||||
out := unifySubs(tc.in, exists)
|
||||
if diff, equal := messagediff.PrettyDiff(tc.out, out); !equal {
|
||||
t.Errorf("Case %d failed; got %v, expected %v, diff:\n%s", i, out, tc.out, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnifySubs(b *testing.B) {
|
||||
cases := unifySubsCases()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, tc := range cases {
|
||||
exists := func(f string) bool {
|
||||
return unifyExists(f, tc)
|
||||
}
|
||||
unifySubs(tc.in, exists)
|
||||
}
|
||||
}
|
||||
}
|
||||
+120
-316
@@ -8,7 +8,6 @@ package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -18,7 +17,6 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
stdsync "sync"
|
||||
"time"
|
||||
@@ -67,7 +65,7 @@ type service interface {
|
||||
Serve()
|
||||
Stop()
|
||||
CheckHealth() error
|
||||
PullErrors() []FileError
|
||||
Errors() []FileError
|
||||
WatchError() error
|
||||
|
||||
getState() (folderState, time.Time, error)
|
||||
@@ -84,7 +82,7 @@ type Model struct {
|
||||
*suture.Supervisor
|
||||
|
||||
cfg *config.Wrapper
|
||||
db *db.Instance
|
||||
db *db.Lowlevel
|
||||
finder *db.BlockFinder
|
||||
progressEmitter *ProgressEmitter
|
||||
id protocol.DeviceID
|
||||
@@ -107,6 +105,7 @@ type Model struct {
|
||||
|
||||
pmut sync.RWMutex // protects the below
|
||||
conn map[protocol.DeviceID]connections.Connection
|
||||
connRequestLimiters map[protocol.DeviceID]*byteSemaphore
|
||||
closed map[protocol.DeviceID]chan struct{}
|
||||
helloMessages map[protocol.DeviceID]protocol.HelloResult
|
||||
deviceDownloads map[protocol.DeviceID]*deviceDownloadState
|
||||
@@ -134,7 +133,7 @@ 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, clientName, clientVersion string, ldb *db.Instance, protectedFiles []string) *Model {
|
||||
func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string) *Model {
|
||||
m := &Model{
|
||||
Supervisor: suture.New("model", suture.Spec{
|
||||
Log: func(line string) {
|
||||
@@ -160,6 +159,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersi
|
||||
folderRunnerTokens: make(map[string][]suture.ServiceToken),
|
||||
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
||||
conn: make(map[protocol.DeviceID]connections.Connection),
|
||||
connRequestLimiters: make(map[protocol.DeviceID]*byteSemaphore),
|
||||
closed: make(map[protocol.DeviceID]chan struct{}),
|
||||
helloMessages: make(map[protocol.DeviceID]protocol.HelloResult),
|
||||
deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState),
|
||||
@@ -1283,6 +1283,7 @@ func (m *Model) Closed(conn protocol.Connection, err error) {
|
||||
m.progressEmitter.temporaryIndexUnsubscribe(conn)
|
||||
}
|
||||
delete(m.conn, device)
|
||||
delete(m.connRequestLimiters, device)
|
||||
delete(m.helloMessages, device)
|
||||
delete(m.deviceDownloads, device)
|
||||
delete(m.remotePausedFolders, device)
|
||||
@@ -1316,19 +1317,40 @@ func (m *Model) closeLocked(device protocol.DeviceID) {
|
||||
closeRawConn(conn)
|
||||
}
|
||||
|
||||
// Implements protocol.RequestResponse
|
||||
type requestResponse struct {
|
||||
data []byte
|
||||
closed chan struct{}
|
||||
once stdsync.Once
|
||||
}
|
||||
|
||||
func newRequestResponse(size int) *requestResponse {
|
||||
return &requestResponse{
|
||||
data: protocol.BufferPool.Get(size),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *requestResponse) Data() []byte {
|
||||
return r.data
|
||||
}
|
||||
|
||||
func (r *requestResponse) Close() {
|
||||
r.once.Do(func() {
|
||||
protocol.BufferPool.Put(r.data)
|
||||
close(r.closed)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *requestResponse) Wait() {
|
||||
<-r.closed
|
||||
}
|
||||
|
||||
// Request returns the specified data segment by reading it from local disk.
|
||||
// Implements the protocol.Model interface.
|
||||
func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset int64, hash []byte, weakHash uint32, fromTemporary bool, buf []byte) error {
|
||||
if offset < 0 {
|
||||
return protocol.ErrInvalid
|
||||
}
|
||||
|
||||
if cfg, ok := m.cfg.Folder(folder); !ok || !cfg.SharedWith(deviceID) {
|
||||
l.Warnf("Request from %s for file %s in unshared folder %q", deviceID, name, folder)
|
||||
return protocol.ErrNoSuchFile
|
||||
} else if cfg.Paused {
|
||||
l.Debugf("Request from %s for file %s in paused folder %q", deviceID, name, folder)
|
||||
return protocol.ErrInvalid
|
||||
func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (out protocol.RequestResponse, err error) {
|
||||
if size < 0 || offset < 0 {
|
||||
return nil, protocol.ErrInvalid
|
||||
}
|
||||
|
||||
m.fmut.RLock()
|
||||
@@ -1339,35 +1361,69 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
// The folder might be already unpaused in the config, but not yet
|
||||
// in the model.
|
||||
l.Debugf("Request from %s for file %s in unstarted folder %q", deviceID, name, folder)
|
||||
return protocol.ErrInvalid
|
||||
return nil, protocol.ErrInvalid
|
||||
}
|
||||
|
||||
if !folderCfg.SharedWith(deviceID) {
|
||||
l.Warnf("Request from %s for file %s in unshared folder %q", deviceID, name, folder)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
}
|
||||
if folderCfg.Paused {
|
||||
l.Debugf("Request from %s for file %s in paused folder %q", deviceID, name, folder)
|
||||
return nil, protocol.ErrInvalid
|
||||
}
|
||||
|
||||
// Make sure the path is valid and in canonical form
|
||||
var err error
|
||||
if name, err = fs.Canonicalize(name); err != nil {
|
||||
l.Debugf("Request from %s in folder %q for invalid filename %s", deviceID, folder, name)
|
||||
return protocol.ErrInvalid
|
||||
return nil, protocol.ErrInvalid
|
||||
}
|
||||
|
||||
if deviceID != protocol.LocalDeviceID {
|
||||
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d t=%v", m, deviceID, folder, name, offset, len(buf), fromTemporary)
|
||||
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d t=%v", m, deviceID, folder, name, offset, size, fromTemporary)
|
||||
}
|
||||
|
||||
if fs.IsInternal(name) {
|
||||
l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
if folderIgnores.Match(name).IsIgnored() {
|
||||
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
folderFs := folderCfg.Filesystem()
|
||||
|
||||
if fs.IsInternal(name) {
|
||||
l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
if folderIgnores.Match(name).IsIgnored() {
|
||||
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
if err := osutil.TraversesSymlink(folderFs, filepath.Dir(name)); err != nil {
|
||||
l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, len(buf))
|
||||
return protocol.ErrNoSuchFile
|
||||
l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
// Restrict parallel requests by connection/device
|
||||
|
||||
m.pmut.RLock()
|
||||
limiter := m.connRequestLimiters[deviceID]
|
||||
m.pmut.RUnlock()
|
||||
|
||||
if limiter != nil {
|
||||
limiter.take(int(size))
|
||||
}
|
||||
|
||||
// The requestResponse releases the bytes to the limiter when its Close method is called.
|
||||
res := newRequestResponse(int(size))
|
||||
defer func() {
|
||||
// Close it ourselves if it isn't returned due to an error
|
||||
if err != nil {
|
||||
res.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if limiter != nil {
|
||||
go func() {
|
||||
res.Wait()
|
||||
limiter.give(int(size))
|
||||
}()
|
||||
}
|
||||
|
||||
// Only check temp files if the flag is set, and if we are set to advertise
|
||||
@@ -1378,11 +1434,12 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
if info, err := folderFs.Lstat(tempFn); err != nil || !info.IsRegular() {
|
||||
// Reject reads for anything that doesn't exist or is something
|
||||
// other than a regular file.
|
||||
return protocol.ErrNoSuchFile
|
||||
l.Debugf("%v REQ(in) failed stating temp file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
}
|
||||
err := readOffsetIntoBuf(folderFs, tempFn, offset, buf)
|
||||
if err == nil && scanner.Validate(buf, hash, weakHash) {
|
||||
return nil
|
||||
err := readOffsetIntoBuf(folderFs, tempFn, offset, res.data)
|
||||
if err == nil && scanner.Validate(res.data, hash, weakHash) {
|
||||
return res, nil
|
||||
}
|
||||
// Fall through to reading from a non-temp file, just incase the temp
|
||||
// file has finished downloading.
|
||||
@@ -1391,21 +1448,25 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
if info, err := folderFs.Lstat(name); err != nil || !info.IsRegular() {
|
||||
// Reject reads for anything that doesn't exist or is something
|
||||
// other than a regular file.
|
||||
return protocol.ErrNoSuchFile
|
||||
l.Debugf("%v REQ(in) failed stating file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
if err = readOffsetIntoBuf(folderFs, name, offset, buf); fs.IsNotExist(err) {
|
||||
return protocol.ErrNoSuchFile
|
||||
if err := readOffsetIntoBuf(folderFs, name, offset, res.data); fs.IsNotExist(err) {
|
||||
l.Debugf("%v REQ(in) file doesn't exist: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
} else if err != nil {
|
||||
return protocol.ErrGeneric
|
||||
l.Debugf("%v REQ(in) failed reading file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrGeneric
|
||||
}
|
||||
|
||||
if !scanner.Validate(buf, hash, weakHash) {
|
||||
m.recheckFile(deviceID, folderFs, folder, name, int(offset)/len(buf), hash)
|
||||
return protocol.ErrNoSuchFile
|
||||
if !scanner.Validate(res.data, hash, weakHash) {
|
||||
m.recheckFile(deviceID, folderFs, folder, name, int(offset)/int(size), hash)
|
||||
l.Debugf("%v REQ(in) failed validating data (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
|
||||
return nil, protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
return nil
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (m *Model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem, folder, name string, blockIndex int, hash []byte) {
|
||||
@@ -1600,6 +1661,11 @@ func (m *Model) GetHello(id protocol.DeviceID) protocol.HelloIntf {
|
||||
// folder changes.
|
||||
func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloResult) {
|
||||
deviceID := conn.ID()
|
||||
device, ok := m.cfg.Device(deviceID)
|
||||
if !ok {
|
||||
l.Infoln("Trying to add connection to unknown device")
|
||||
return
|
||||
}
|
||||
|
||||
m.pmut.Lock()
|
||||
if oldConn, ok := m.conn[deviceID]; ok {
|
||||
@@ -1619,6 +1685,13 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
|
||||
m.conn[deviceID] = conn
|
||||
m.closed[deviceID] = make(chan struct{})
|
||||
m.deviceDownloads[deviceID] = newDeviceDownloadState()
|
||||
// 0: default, <0: no limiting
|
||||
switch {
|
||||
case device.MaxRequestKiB > 0:
|
||||
m.connRequestLimiters[deviceID] = newByteSemaphore(1024 * device.MaxRequestKiB)
|
||||
case device.MaxRequestKiB == 0:
|
||||
m.connRequestLimiters[deviceID] = newByteSemaphore(1024 * defaultPullerPendingKiB)
|
||||
}
|
||||
|
||||
m.helloMessages[deviceID] = hello
|
||||
|
||||
@@ -1646,8 +1719,7 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
|
||||
cm := m.generateClusterConfig(deviceID)
|
||||
conn.ClusterConfig(cm)
|
||||
|
||||
device, ok := m.cfg.Devices()[deviceID]
|
||||
if ok && (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
|
||||
if (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
|
||||
device.Name = hello.DeviceName
|
||||
m.cfg.SetDevice(device)
|
||||
m.cfg.Save()
|
||||
@@ -1981,240 +2053,6 @@ func (m *Model) ScanFolderSubdirs(folder string, subs []string) error {
|
||||
return runner.Scan(subs)
|
||||
}
|
||||
|
||||
func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, subDirs []string, localFlags uint32) error {
|
||||
m.fmut.RLock()
|
||||
if err := m.checkFolderRunningLocked(folder); err != nil {
|
||||
m.fmut.RUnlock()
|
||||
return err
|
||||
}
|
||||
fset := m.folderFiles[folder]
|
||||
folderCfg := m.folderCfgs[folder]
|
||||
ignores := m.folderIgnores[folder]
|
||||
runner := m.folderRunners[folder]
|
||||
m.fmut.RUnlock()
|
||||
mtimefs := fset.MtimeFS()
|
||||
|
||||
for i := range subDirs {
|
||||
sub := osutil.NativeFilename(subDirs[i])
|
||||
|
||||
if sub == "" {
|
||||
// A blank subdirs means to scan the entire folder. We can trim
|
||||
// the subDirs list and go on our way.
|
||||
subDirs = nil
|
||||
break
|
||||
}
|
||||
|
||||
subDirs[i] = sub
|
||||
}
|
||||
|
||||
// Check if the ignore patterns changed as part of scanning this folder.
|
||||
// If they did we should schedule a pull of the folder so that we
|
||||
// request things we might have suddenly become unignored and so on.
|
||||
oldHash := ignores.Hash()
|
||||
defer func() {
|
||||
if ignores.Hash() != oldHash {
|
||||
l.Debugln("Folder", folder, "ignore patterns changed; triggering puller")
|
||||
runner.IgnoresUpdated()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := runner.CheckHealth(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
|
||||
err = fmt.Errorf("loading ignores: %v", err)
|
||||
runner.setError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Clean the list of subitems to ensure that we start at a known
|
||||
// directory, and don't scan subdirectories of things we've already
|
||||
// scanned.
|
||||
subDirs = unifySubs(subDirs, func(f string) bool {
|
||||
_, ok := fset.Get(protocol.LocalDeviceID, f)
|
||||
return ok
|
||||
})
|
||||
|
||||
runner.setState(FolderScanning)
|
||||
|
||||
fchan := scanner.Walk(ctx, scanner.Config{
|
||||
Folder: folderCfg.ID,
|
||||
Subs: subDirs,
|
||||
Matcher: ignores,
|
||||
TempLifetime: time.Duration(m.cfg.Options().KeepTemporariesH) * time.Hour,
|
||||
CurrentFiler: cFiler{m, folder},
|
||||
Filesystem: mtimefs,
|
||||
IgnorePerms: folderCfg.IgnorePerms,
|
||||
AutoNormalize: folderCfg.AutoNormalize,
|
||||
Hashers: m.numHashers(folder),
|
||||
ShortID: m.shortID,
|
||||
ProgressTickIntervalS: folderCfg.ScanProgressIntervalS,
|
||||
UseLargeBlocks: folderCfg.UseLargeBlocks,
|
||||
LocalFlags: localFlags,
|
||||
})
|
||||
|
||||
if err := runner.CheckHealth(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
batch := newFileInfoBatch(func(fs []protocol.FileInfo) error {
|
||||
if err := runner.CheckHealth(); err != nil {
|
||||
l.Debugf("Stopping scan of folder %s due to: %s", folderCfg.Description(), err)
|
||||
return err
|
||||
}
|
||||
m.updateLocalsFromScanning(folder, fs)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Schedule a pull after scanning, but only if we actually detected any
|
||||
// changes.
|
||||
changes := 0
|
||||
defer func() {
|
||||
if changes > 0 {
|
||||
runner.SchedulePull()
|
||||
}
|
||||
}()
|
||||
|
||||
for f := range fchan {
|
||||
if err := batch.flushIfFull(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
batch.append(f)
|
||||
changes++
|
||||
}
|
||||
|
||||
if err := batch.flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(subDirs) == 0 {
|
||||
// If we have no specific subdirectories to traverse, set it to one
|
||||
// empty prefix so we traverse the entire folder contents once.
|
||||
subDirs = []string{""}
|
||||
}
|
||||
|
||||
// Do a scan of the database for each prefix, to check for deleted and
|
||||
// ignored files.
|
||||
var toIgnore []db.FileInfoTruncated
|
||||
ignoredParent := ""
|
||||
pathSep := string(fs.PathSeparator)
|
||||
for _, sub := range subDirs {
|
||||
var iterError error
|
||||
|
||||
fset.WithPrefixedHaveTruncated(protocol.LocalDeviceID, sub, func(fi db.FileIntf) bool {
|
||||
f := fi.(db.FileInfoTruncated)
|
||||
|
||||
if err := batch.flushIfFull(); err != nil {
|
||||
iterError = err
|
||||
return false
|
||||
}
|
||||
|
||||
if ignoredParent != "" && !strings.HasPrefix(f.Name, ignoredParent+pathSep) {
|
||||
for _, f := range toIgnore {
|
||||
l.Debugln("marking file as ignored", f)
|
||||
nf := f.ConvertToIgnoredFileInfo(m.id.Short())
|
||||
batch.append(nf)
|
||||
changes++
|
||||
if err := batch.flushIfFull(); err != nil {
|
||||
iterError = err
|
||||
return false
|
||||
}
|
||||
}
|
||||
toIgnore = toIgnore[:0]
|
||||
ignoredParent = ""
|
||||
}
|
||||
|
||||
switch ignored := ignores.Match(f.Name).IsIgnored(); {
|
||||
case !f.IsIgnored() && ignored:
|
||||
// File was not ignored at last pass but has been ignored.
|
||||
if f.IsDirectory() {
|
||||
// Delay ignoring as a child might be unignored.
|
||||
toIgnore = append(toIgnore, f)
|
||||
if ignoredParent == "" {
|
||||
// If the parent wasn't ignored already, set
|
||||
// this path as the "highest" ignored parent
|
||||
ignoredParent = f.Name
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
l.Debugln("marking file as ignored", f)
|
||||
nf := f.ConvertToIgnoredFileInfo(m.id.Short())
|
||||
batch.append(nf)
|
||||
changes++
|
||||
|
||||
case f.IsIgnored() && !ignored:
|
||||
// Successfully scanned items are already un-ignored during
|
||||
// the scan, so check whether it is deleted.
|
||||
fallthrough
|
||||
case !f.IsIgnored() && !f.IsDeleted() && !f.IsUnsupported():
|
||||
// The file is not ignored, deleted or unsupported. Lets check if
|
||||
// it's still here. Simply stat:ing it wont do as there are
|
||||
// tons of corner cases (e.g. parent dir->symlink, missing
|
||||
// permissions)
|
||||
if !osutil.IsDeleted(mtimefs, f.Name) {
|
||||
if ignoredParent != "" {
|
||||
// Don't ignore parents of this not ignored item
|
||||
toIgnore = toIgnore[:0]
|
||||
ignoredParent = ""
|
||||
}
|
||||
return true
|
||||
}
|
||||
nf := protocol.FileInfo{
|
||||
Name: f.Name,
|
||||
Type: f.Type,
|
||||
Size: 0,
|
||||
ModifiedS: f.ModifiedS,
|
||||
ModifiedNs: f.ModifiedNs,
|
||||
ModifiedBy: m.id.Short(),
|
||||
Deleted: true,
|
||||
Version: f.Version.Update(m.shortID),
|
||||
LocalFlags: localFlags,
|
||||
}
|
||||
// We do not want to override the global version
|
||||
// with the deleted file. Keeping only our local
|
||||
// counter makes sure we are in conflict with any
|
||||
// other existing versions, which will be resolved
|
||||
// by the normal pulling mechanisms.
|
||||
if f.ShouldConflict() {
|
||||
nf.Version = nf.Version.DropOthers(m.shortID)
|
||||
}
|
||||
|
||||
batch.append(nf)
|
||||
changes++
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if iterError == nil && len(toIgnore) > 0 {
|
||||
for _, f := range toIgnore {
|
||||
l.Debugln("marking file as ignored", f)
|
||||
nf := f.ConvertToIgnoredFileInfo(m.id.Short())
|
||||
batch.append(nf)
|
||||
changes++
|
||||
if iterError = batch.flushIfFull(); iterError != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
toIgnore = toIgnore[:0]
|
||||
}
|
||||
|
||||
if iterError != nil {
|
||||
return iterError
|
||||
}
|
||||
}
|
||||
|
||||
if err := batch.flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.folderStatRef(folder).ScanCompleted()
|
||||
runner.setState(FolderIdle)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) DelayScan(folder string, next time.Duration) {
|
||||
m.fmut.Lock()
|
||||
runner, ok := m.folderRunners[folder]
|
||||
@@ -2327,13 +2165,13 @@ func (m *Model) State(folder string) (string, time.Time, error) {
|
||||
return state.String(), changed, err
|
||||
}
|
||||
|
||||
func (m *Model) PullErrors(folder string) ([]FileError, error) {
|
||||
func (m *Model) FolderErrors(folder string) ([]FileError, error) {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if err := m.checkFolderRunningLocked(folder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m.folderRunners[folder].PullErrors(), nil
|
||||
return m.folderRunners[folder].Errors(), nil
|
||||
}
|
||||
|
||||
func (m *Model) WatchError(folder string) error {
|
||||
@@ -2872,40 +2710,6 @@ func readOffsetIntoBuf(fs fs.Filesystem, file string, offset int64, buf []byte)
|
||||
return err
|
||||
}
|
||||
|
||||
// The exists function is expected to return true for all known paths
|
||||
// (excluding "" and ".")
|
||||
func unifySubs(dirs []string, exists func(dir string) bool) []string {
|
||||
if len(dirs) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(dirs)
|
||||
if dirs[0] == "" || dirs[0] == "." || dirs[0] == string(fs.PathSeparator) {
|
||||
return nil
|
||||
}
|
||||
prev := "./" // Anything that can't be parent of a clean path
|
||||
for i := 0; i < len(dirs); {
|
||||
dir, err := fs.Canonicalize(dirs[i])
|
||||
if err != nil {
|
||||
l.Debugf("Skipping %v for scan: %s", dirs[i], err)
|
||||
dirs = append(dirs[:i], dirs[i+1:]...)
|
||||
continue
|
||||
}
|
||||
if dir == prev || strings.HasPrefix(dir, prev+string(fs.PathSeparator)) {
|
||||
dirs = append(dirs[:i], dirs[i+1:]...)
|
||||
continue
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
for parent != "." && parent != string(fs.PathSeparator) && !exists(parent) {
|
||||
dir = parent
|
||||
parent = filepath.Dir(dir)
|
||||
}
|
||||
dirs[i] = dir
|
||||
prev = dir
|
||||
i++
|
||||
}
|
||||
return dirs
|
||||
}
|
||||
|
||||
// makeForgetUpdate takes an index update and constructs a download progress update
|
||||
// causing to forget any progress for files which we've just been sent.
|
||||
func makeForgetUpdate(files []protocol.FileInfo) []protocol.FileDownloadProgressUpdate {
|
||||
|
||||
+51
-200
@@ -24,7 +24,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/d4l3k/messagediff"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
@@ -184,45 +183,42 @@ func TestRequest(t *testing.T) {
|
||||
defer m.Stop()
|
||||
m.ScanFolder("default")
|
||||
|
||||
bs := make([]byte, protocol.MinBlockSize)
|
||||
|
||||
// Existing, shared file
|
||||
bs = bs[:6]
|
||||
err := m.Request(device1, "default", "foo", 0, nil, 0, false, bs)
|
||||
res, err := m.Request(device1, "default", "foo", 6, 0, nil, 0, false)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
bs := res.Data()
|
||||
if !bytes.Equal(bs, []byte("foobar")) {
|
||||
t.Errorf("Incorrect data from request: %q", string(bs))
|
||||
}
|
||||
|
||||
// Existing, nonshared file
|
||||
err = m.Request(device2, "default", "foo", 0, nil, 0, false, bs)
|
||||
_, err = m.Request(device2, "default", "foo", 6, 0, nil, 0, false)
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Nonexistent file
|
||||
err = m.Request(device1, "default", "nonexistent", 0, nil, 0, false, bs)
|
||||
_, err = m.Request(device1, "default", "nonexistent", 6, 0, nil, 0, false)
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Shared folder, but disallowed file name
|
||||
err = m.Request(device1, "default", "../walk.go", 0, nil, 0, false, bs)
|
||||
_, err = m.Request(device1, "default", "../walk.go", 6, 0, nil, 0, false)
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Negative offset
|
||||
err = m.Request(device1, "default", "foo", -4, nil, 0, false, bs[:0])
|
||||
_, err = m.Request(device1, "default", "foo", -4, 0, nil, 0, false)
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
|
||||
// Larger block than available
|
||||
bs = bs[:42]
|
||||
err = m.Request(device1, "default", "foo", 0, nil, 0, false, bs)
|
||||
_, err = m.Request(device1, "default", "foo", 42, 0, nil, 0, false)
|
||||
if err == nil {
|
||||
t.Error("Unexpected nil error on insecure file read")
|
||||
}
|
||||
@@ -537,7 +533,7 @@ func BenchmarkRequestInSingleFile(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := m.Request(device1, "default", "request/for/a/file/in/a/couple/of/dirs/128k", 0, nil, 0, false, buf); err != nil {
|
||||
if _, err := m.Request(device1, "default", "request/for/a/file/in/a/couple/of/dirs/128k", 128<<10, 0, nil, 0, false); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -2352,140 +2348,6 @@ func benchmarkTree(b *testing.B, n1, n2 int) {
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
type unifySubsCase struct {
|
||||
in []string // input to unifySubs
|
||||
exists []string // paths that exist in the database
|
||||
out []string // expected output
|
||||
}
|
||||
|
||||
func unifySubsCases() []unifySubsCase {
|
||||
cases := []unifySubsCase{
|
||||
{
|
||||
// 0. trailing slashes are cleaned, known paths are just passed on
|
||||
[]string{"foo/", "bar//"},
|
||||
[]string{"foo", "bar"},
|
||||
[]string{"bar", "foo"}, // the output is sorted
|
||||
},
|
||||
{
|
||||
// 1. "foo/bar" gets trimmed as it's covered by foo
|
||||
[]string{"foo", "bar/", "foo/bar/"},
|
||||
[]string{"foo", "bar"},
|
||||
[]string{"bar", "foo"},
|
||||
},
|
||||
{
|
||||
// 2. "" gets simplified to the empty list; ie scan all
|
||||
[]string{"foo", ""},
|
||||
[]string{"foo"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
// 3. "foo/bar" is unknown, but it's kept
|
||||
// because its parent is known
|
||||
[]string{"foo/bar"},
|
||||
[]string{"foo"},
|
||||
[]string{"foo/bar"},
|
||||
},
|
||||
{
|
||||
// 4. two independent known paths, both are kept
|
||||
// "usr/lib" is not a prefix of "usr/libexec"
|
||||
[]string{"usr/lib", "usr/libexec"},
|
||||
[]string{"usr", "usr/lib", "usr/libexec"},
|
||||
[]string{"usr/lib", "usr/libexec"},
|
||||
},
|
||||
{
|
||||
// 5. "usr/lib" is a prefix of "usr/lib/exec"
|
||||
[]string{"usr/lib", "usr/lib/exec"},
|
||||
[]string{"usr", "usr/lib", "usr/libexec"},
|
||||
[]string{"usr/lib"},
|
||||
},
|
||||
{
|
||||
// 6. .stignore and .stfolder are special and are passed on
|
||||
// verbatim even though they are unknown
|
||||
[]string{config.DefaultMarkerName, ".stignore"},
|
||||
[]string{},
|
||||
[]string{config.DefaultMarkerName, ".stignore"},
|
||||
},
|
||||
{
|
||||
// 7. but the presence of something else unknown forces an actual
|
||||
// scan
|
||||
[]string{config.DefaultMarkerName, ".stignore", "foo/bar"},
|
||||
[]string{},
|
||||
[]string{config.DefaultMarkerName, ".stignore", "foo"},
|
||||
},
|
||||
{
|
||||
// 8. explicit request to scan all
|
||||
nil,
|
||||
[]string{"foo"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
// 9. empty list of subs
|
||||
[]string{},
|
||||
[]string{"foo"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
// 10. absolute path
|
||||
[]string{"/foo"},
|
||||
[]string{"foo"},
|
||||
[]string{"foo"},
|
||||
},
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Fixup path separators
|
||||
for i := range cases {
|
||||
for j, p := range cases[i].in {
|
||||
cases[i].in[j] = filepath.FromSlash(p)
|
||||
}
|
||||
for j, p := range cases[i].exists {
|
||||
cases[i].exists[j] = filepath.FromSlash(p)
|
||||
}
|
||||
for j, p := range cases[i].out {
|
||||
cases[i].out[j] = filepath.FromSlash(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cases
|
||||
}
|
||||
|
||||
func unifyExists(f string, tc unifySubsCase) bool {
|
||||
for _, e := range tc.exists {
|
||||
if f == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestUnifySubs(t *testing.T) {
|
||||
cases := unifySubsCases()
|
||||
for i, tc := range cases {
|
||||
exists := func(f string) bool {
|
||||
return unifyExists(f, tc)
|
||||
}
|
||||
out := unifySubs(tc.in, exists)
|
||||
if diff, equal := messagediff.PrettyDiff(tc.out, out); !equal {
|
||||
t.Errorf("Case %d failed; got %v, expected %v, diff:\n%s", i, out, tc.out, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnifySubs(b *testing.B) {
|
||||
cases := unifySubsCases()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, tc := range cases {
|
||||
exists := func(f string) bool {
|
||||
return unifyExists(f, tc)
|
||||
}
|
||||
unifySubs(tc.in, exists)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue3028(t *testing.T) {
|
||||
// Create two files that we'll delete, one with a name that is a prefix of the other.
|
||||
|
||||
@@ -2618,60 +2480,6 @@ func TestIssue4357(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanNoDatabaseWrite(t *testing.T) {
|
||||
// When scanning, nothing should be committed to database unless
|
||||
// something actually changed.
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultCfgWrapper, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
|
||||
// Reach in and update the ignore matcher to one that always does
|
||||
// reloads when asked to, instead of checking file mtimes. This is
|
||||
// because we will be changing the files on disk often enough that the
|
||||
// mtimes will be unreliable to determine change status.
|
||||
m.fmut.Lock()
|
||||
m.folderIgnores["default"] = ignore.New(defaultFs, ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
|
||||
m.fmut.Unlock()
|
||||
|
||||
m.SetIgnores("default", nil)
|
||||
defer os.Remove("testdata/.stignore")
|
||||
|
||||
// Scan the folder twice. The second scan should be a no-op database wise
|
||||
|
||||
m.ScanFolder("default")
|
||||
c0 := db.Committed()
|
||||
|
||||
m.ScanFolder("default")
|
||||
c1 := db.Committed()
|
||||
|
||||
if c1 != c0 {
|
||||
t.Errorf("scan should not commit data when nothing changed but %d != %d", c1, c0)
|
||||
}
|
||||
|
||||
// Ignore a file we know exists. It'll be updated in the database.
|
||||
|
||||
m.SetIgnores("default", []string{"foo"})
|
||||
|
||||
m.ScanFolder("default")
|
||||
c2 := db.Committed()
|
||||
|
||||
if c2 <= c1 {
|
||||
t.Errorf("scan should commit data when something got ignored but %d <= %d", c2, c1)
|
||||
}
|
||||
|
||||
// Scan again. Nothing should happen.
|
||||
|
||||
m.ScanFolder("default")
|
||||
c3 := db.Committed()
|
||||
|
||||
if c3 != c2 {
|
||||
t.Errorf("scan should not commit data when nothing changed (with ignores) but %d != %d", c3, c2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue2782(t *testing.T) {
|
||||
// CheckHealth should accept a symlinked folder, when using tilde-expanded path.
|
||||
|
||||
@@ -3856,6 +3664,7 @@ func TestFolderRestartZombies(t *testing.T) {
|
||||
// would leave more than one folder runner alive.
|
||||
|
||||
wrapper := createTmpWrapper(defaultCfg.Copy())
|
||||
defer os.Remove(wrapper.ConfigPath())
|
||||
folderCfg, _ := wrapper.Folder("default")
|
||||
folderCfg.FilesystemType = fs.FilesystemTypeFake
|
||||
wrapper.SetFolder(folderCfg)
|
||||
@@ -3948,3 +3757,45 @@ func (c *alwaysChanged) Seen(fs fs.Filesystem, name string) bool {
|
||||
func (c *alwaysChanged) Changed() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func TestRequestLimit(t *testing.T) {
|
||||
cfg := defaultCfg.Copy()
|
||||
cfg.Devices = append(cfg.Devices, config.NewDeviceConfiguration(device2, "device2"))
|
||||
cfg.Devices[1].MaxRequestKiB = 1
|
||||
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
|
||||
{DeviceID: device1},
|
||||
{DeviceID: device2},
|
||||
}
|
||||
m, _, wrapper := setupModelWithConnectionManual(cfg)
|
||||
defer m.Stop()
|
||||
defer os.Remove(wrapper.ConfigPath())
|
||||
|
||||
file := "tmpfile"
|
||||
befReq := time.Now()
|
||||
first, err := m.Request(device2, "default", file, 2000, 0, nil, 0, false)
|
||||
if err != nil {
|
||||
t.Fatalf("First request failed: %v", err)
|
||||
}
|
||||
reqDur := time.Since(befReq)
|
||||
returned := make(chan struct{})
|
||||
go func() {
|
||||
second, err := m.Request(device2, "default", file, 2000, 0, nil, 0, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Second request failed: %v", err)
|
||||
}
|
||||
close(returned)
|
||||
second.Close()
|
||||
}()
|
||||
time.Sleep(10 * reqDur)
|
||||
select {
|
||||
case <-returned:
|
||||
t.Fatalf("Second request returned before first was done")
|
||||
default:
|
||||
}
|
||||
first.Close()
|
||||
select {
|
||||
case <-returned:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("Second request did not return after first was done")
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
@@ -98,9 +98,8 @@ func TestSymlinkTraversalRead(t *testing.T) {
|
||||
<-done
|
||||
|
||||
// Request a file by traversing the symlink
|
||||
buf := make([]byte, 10)
|
||||
err := m.Request(device1, "default", "symlink/requests_test.go", 0, nil, 0, false, buf)
|
||||
if err == nil || !bytes.Equal(buf, make([]byte, 10)) {
|
||||
res, err := m.Request(device1, "default", "symlink/requests_test.go", 10, 0, nil, 0, false)
|
||||
if err == nil || res != nil {
|
||||
t.Error("Managed to traverse symlink")
|
||||
}
|
||||
}
|
||||
@@ -225,6 +224,7 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
cfg := defaultCfgWrapper.RawCopy()
|
||||
cfg.Devices = append(cfg.Devices, config.NewDeviceConfiguration(device2, "device2"))
|
||||
cfg.Folders[0] = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, tmpDir)
|
||||
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
|
||||
{DeviceID: device1},
|
||||
@@ -519,12 +519,11 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
|
||||
t.Fatalf("unexpected weak hash: %d != 103547413", f.Blocks[0].WeakHash)
|
||||
}
|
||||
|
||||
buf := make([]byte, len(payload))
|
||||
|
||||
err := m.Request(device2, "default", "foo", 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false, buf)
|
||||
res, err := m.Request(device2, "default", "foo", int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := res.Data()
|
||||
if !bytes.Equal(buf, payload) {
|
||||
t.Errorf("%s != %s", buf, payload)
|
||||
}
|
||||
@@ -536,7 +535,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = m.Request(device2, "default", "foo", 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false, buf)
|
||||
res, err = m.Request(device2, "default", "foo", int32(len(payload)), 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false)
|
||||
if err == nil {
|
||||
t.Fatalf("expected failure")
|
||||
}
|
||||
|
||||
@@ -171,12 +171,13 @@ func (m *fakeModel) Index(deviceID DeviceID, folder string, files []FileInfo) {
|
||||
func (m *fakeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo) {
|
||||
}
|
||||
|
||||
func (m *fakeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, weakHAsh uint32, fromTemporary bool, buf []byte) error {
|
||||
func (m *fakeModel) Request(deviceID DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error) {
|
||||
// We write the offset to the end of the buffer, so the receiver
|
||||
// can verify that it did in fact get some data back over the
|
||||
// connection.
|
||||
buf := make([]byte, size)
|
||||
binary.BigEndian.PutUint64(buf[len(buf)-8:], uint64(offset))
|
||||
return nil
|
||||
return &fakeRequestResponse{buf}, nil
|
||||
}
|
||||
|
||||
func (m *fakeModel) ClusterConfig(deviceID DeviceID, config ClusterConfig) {
|
||||
|
||||
+49
-39
@@ -4,32 +4,59 @@ package protocol
|
||||
|
||||
import "sync"
|
||||
|
||||
// Global pool to get buffers from. Requires Blocksizes to be initialised,
|
||||
// therefore it is initialized in the same init() as BlockSizes
|
||||
var BufferPool bufferPool
|
||||
|
||||
type bufferPool struct {
|
||||
minSize int
|
||||
pool sync.Pool
|
||||
pools []sync.Pool
|
||||
}
|
||||
|
||||
// get returns a new buffer of the requested size
|
||||
func (p *bufferPool) get(size int) []byte {
|
||||
intf := p.pool.Get()
|
||||
if intf == nil {
|
||||
// Pool is empty, must allocate.
|
||||
return p.new(size)
|
||||
}
|
||||
|
||||
bs := *intf.(*[]byte)
|
||||
if cap(bs) < size {
|
||||
// Buffer was too small, leave it for someone else and allocate.
|
||||
p.pool.Put(intf)
|
||||
return p.new(size)
|
||||
}
|
||||
|
||||
return bs[:size]
|
||||
func newBufferPool() bufferPool {
|
||||
return bufferPool{make([]sync.Pool, len(BlockSizes))}
|
||||
}
|
||||
|
||||
// upgrade grows the buffer to the requested size, while attempting to reuse
|
||||
func (p *bufferPool) Get(size int) []byte {
|
||||
// Too big, isn't pooled
|
||||
if size > MaxBlockSize {
|
||||
return make([]byte, size)
|
||||
}
|
||||
var i int
|
||||
for i = range BlockSizes {
|
||||
if size <= BlockSizes[i] {
|
||||
break
|
||||
}
|
||||
}
|
||||
var bs []byte
|
||||
// Try the fitting and all bigger pools
|
||||
for j := i; j < len(BlockSizes); j++ {
|
||||
if intf := p.pools[j].Get(); intf != nil {
|
||||
bs = *intf.(*[]byte)
|
||||
return bs[:size]
|
||||
}
|
||||
}
|
||||
// All pools are empty, must allocate.
|
||||
return make([]byte, BlockSizes[i])[:size]
|
||||
}
|
||||
|
||||
// Put makes the given byte slice availabe again in the global pool
|
||||
func (p *bufferPool) Put(bs []byte) {
|
||||
c := cap(bs)
|
||||
// Don't buffer huge byte slices
|
||||
if c > 2*MaxBlockSize {
|
||||
return
|
||||
}
|
||||
for i := range BlockSizes {
|
||||
if c >= BlockSizes[i] {
|
||||
p.pools[i].Put(&bs)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upgrade grows the buffer to the requested size, while attempting to reuse
|
||||
// it if possible.
|
||||
func (p *bufferPool) upgrade(bs []byte, size int) []byte {
|
||||
func (p *bufferPool) Upgrade(bs []byte, size int) []byte {
|
||||
if cap(bs) >= size {
|
||||
// Reslicing is enough, lets go!
|
||||
return bs[:size]
|
||||
@@ -37,23 +64,6 @@ func (p *bufferPool) upgrade(bs []byte, size int) []byte {
|
||||
|
||||
// It was too small. But it pack into the pool and try to get another
|
||||
// buffer.
|
||||
p.put(bs)
|
||||
return p.get(size)
|
||||
}
|
||||
|
||||
// put returns the buffer to the pool
|
||||
func (p *bufferPool) put(bs []byte) {
|
||||
p.pool.Put(&bs)
|
||||
}
|
||||
|
||||
// new creates a new buffer of the requested size, taking the minimum
|
||||
// allocation count into account. For internal use only.
|
||||
func (p *bufferPool) new(size int) []byte {
|
||||
allocSize := size
|
||||
if allocSize < p.minSize {
|
||||
// Avoid allocating tiny buffers that we won't be able to reuse for
|
||||
// anything useful.
|
||||
allocSize = p.minSize
|
||||
}
|
||||
return make([]byte, allocSize)[:size]
|
||||
p.Put(bs)
|
||||
return p.Get(size)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ type TestModel struct {
|
||||
folder string
|
||||
name string
|
||||
offset int64
|
||||
size int
|
||||
size int32
|
||||
hash []byte
|
||||
weakHash uint32
|
||||
fromTemporary bool
|
||||
@@ -29,16 +29,17 @@ func (t *TestModel) Index(deviceID DeviceID, folder string, files []FileInfo) {
|
||||
func (t *TestModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo) {
|
||||
}
|
||||
|
||||
func (t *TestModel) Request(deviceID DeviceID, folder, name string, offset int64, hash []byte, weakHash uint32, fromTemporary bool, buf []byte) error {
|
||||
func (t *TestModel) Request(deviceID DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error) {
|
||||
t.folder = folder
|
||||
t.name = name
|
||||
t.offset = offset
|
||||
t.size = len(buf)
|
||||
t.size = size
|
||||
t.hash = hash
|
||||
t.weakHash = weakHash
|
||||
t.fromTemporary = fromTemporary
|
||||
buf := make([]byte, len(t.data))
|
||||
copy(buf, t.data)
|
||||
return nil
|
||||
return &fakeRequestResponse{buf}, nil
|
||||
}
|
||||
|
||||
func (t *TestModel) Closed(conn Connection, err error) {
|
||||
@@ -60,3 +61,15 @@ func (t *TestModel) closedError() error {
|
||||
return nil // Timeout
|
||||
}
|
||||
}
|
||||
|
||||
type fakeRequestResponse struct {
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (r *fakeRequestResponse) Data() []byte {
|
||||
return r.data
|
||||
}
|
||||
|
||||
func (r *fakeRequestResponse) Close() {}
|
||||
|
||||
func (r *fakeRequestResponse) Wait() {}
|
||||
|
||||
Executable → Regular
@@ -26,7 +26,7 @@ func (m nativeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileI
|
||||
m.Model.IndexUpdate(deviceID, folder, files)
|
||||
}
|
||||
|
||||
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, weakHash uint32, fromTemporary bool, buf []byte) error {
|
||||
func (m nativeModel) Request(deviceID DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error) {
|
||||
name = norm.NFD.String(name)
|
||||
return m.Model.Request(deviceID, folder, name, offset, hash, weakHash, fromTemporary, buf)
|
||||
return m.Model.Request(deviceID, folder, name, size, offset, hash, weakHash, fromTemporary)
|
||||
}
|
||||
|
||||
@@ -25,14 +25,14 @@ func (m nativeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileI
|
||||
m.Model.IndexUpdate(deviceID, folder, files)
|
||||
}
|
||||
|
||||
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, weakHash uint32, fromTemporary bool, buf []byte) error {
|
||||
func (m nativeModel) Request(deviceID DeviceID, folder, name string, size int32, offset int64, hash []byte, weakHash uint32, fromTemporary bool) (RequestResponse, error) {
|
||||
if strings.Contains(name, `\`) {
|
||||
l.Warnf("Dropping request for %s, contains invalid path separator", name)
|
||||
return ErrNoSuchFile
|
||||
return nil, ErrNoSuchFile
|
||||
}
|
||||
|
||||
name = filepath.FromSlash(name)
|
||||
return m.Model.Request(deviceID, folder, name, offset, hash, weakHash, fromTemporary, buf)
|
||||
return m.Model.Request(deviceID, folder, name, size, offset, hash, weakHash, fromTemporary)
|
||||
}
|
||||
|
||||
func fixupFiles(files []FileInfo) []FileInfo {
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
package protocol
|
||||
|
||||
import "testing"
|
||||
import "reflect"
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFixupFiles(t *testing.T) {
|
||||
files := []FileInfo{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user