mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-03 11:29:10 -05:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
952ab7db1c | ||
|
|
abb3fb8a31 | ||
|
|
fc860df514 | ||
|
|
c934918347 | ||
|
|
c98a34a5d4 | ||
|
|
bdcffe703b | ||
|
|
a09079ed25 | ||
|
|
1b59960ff9 | ||
|
|
f04d054b5a | ||
|
|
132789785d | ||
|
|
002de7b6a0 | ||
|
|
9e725026d1 | ||
|
|
da39dfada3 | ||
|
|
ff2cde469e | ||
|
|
0fe4c01a28 | ||
|
|
351e855464 | ||
|
|
7203ccb73d | ||
|
|
e89c4c053a | ||
|
|
0391c57a37 | ||
|
|
2f9840ddae | ||
|
|
513d3bc374 | ||
|
|
c0a26c918a | ||
|
|
d1704d5304 | ||
|
|
d3d0161fb9 | ||
|
|
db8777c29e | ||
|
|
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 |
5
AUTHORS
5
AUTHORS
@@ -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>
|
||||
@@ -52,6 +53,7 @@ Chris Joel (cdata) <chris@scriptolo.gy>
|
||||
Chris Tonkinson <chris@masterbran.ch>
|
||||
chucic <chucic@seznam.cz>
|
||||
Colin Kennedy (moshen) <moshen.colin@gmail.com>
|
||||
Cromefire_ <tim.l@nghorst.net>
|
||||
Dale Visser <dale.visser@live.com>
|
||||
Daniel Bergmann (brgmnn) <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
|
||||
Daniel Harte (norgeous) <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@users.noreply.github.com>
|
||||
@@ -61,6 +63,7 @@ David Rimmer (dinosore) <dinosore@dbrsoftware.co.uk>
|
||||
Denis A. (dva) <denisva@gmail.com>
|
||||
Dennis Wilson (snnd) <dw@risu.io>
|
||||
derekriemer <derek.riemer@colorado.edu>
|
||||
desbma <desbma@users.noreply.github.com>
|
||||
Dmitry Saveliev (dsaveliev) <d.e.saveliev@gmail.com>
|
||||
Dominik Heidler (asdil12) <dominik@heidler.eu>
|
||||
Elias Jarlebring (jarlebring) <jarlebring@gmail.com>
|
||||
@@ -96,6 +99,7 @@ Johan Vromans (sciurius) <jvromans@squirrel.nl>
|
||||
John Rinehart (fuzzybear3965) <johnrichardrinehart@gmail.com>
|
||||
Jonathan Cross <jcross@gmail.com>
|
||||
Jose Manuel Delicado (jmdaweb) <jmdaweb@hotmail.com> <jmdaweb@users.noreply.github.com>
|
||||
Jörg Thalheim <Mic92@users.noreply.github.com>
|
||||
Karol Różycki (krozycki) <rozycki.karol@gmail.com>
|
||||
Keith Turner <kturner@apache.org>
|
||||
Kelong Cong (kc1212) <kc04bc@gmx.com> <kc1212@users.noreply.github.com>
|
||||
@@ -131,6 +135,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>
|
||||
|
||||
12
build.go
12
build.go
@@ -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",
|
||||
@@ -442,7 +450,7 @@ func build(target target, tags []string) {
|
||||
|
||||
rmr(target.BinaryName())
|
||||
|
||||
args := []string{"build", "-i", "-v"}
|
||||
args := []string{"build", "-v"}
|
||||
args = appendParameters(args, tags, target)
|
||||
|
||||
os.Setenv("GOOS", goos)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
242
cmd/stindex/idxck.go
Normal file
242
cmd/stindex/idxck.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ type modelIntf interface {
|
||||
Revert(folder string)
|
||||
NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated)
|
||||
RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error)
|
||||
LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated
|
||||
NeedSize(folder string) db.Counts
|
||||
ConnectionStats() map[string]interface{}
|
||||
DeviceStatistics() map[string]stats.DeviceStatistics
|
||||
@@ -115,7 +116,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 +186,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,
|
||||
@@ -273,10 +259,12 @@ func (s *apiService) Serve() {
|
||||
getRestMux.HandleFunc("/rest/db/ignores", s.getDBIgnores) // folder
|
||||
getRestMux.HandleFunc("/rest/db/need", s.getDBNeed) // folder [perpage] [page]
|
||||
getRestMux.HandleFunc("/rest/db/remoteneed", s.getDBRemoteNeed) // device folder [perpage] [page]
|
||||
getRestMux.HandleFunc("/rest/db/localchanged", s.getDBLocalChanged) // folder
|
||||
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,23 +698,24 @@ 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
|
||||
|
||||
global := m.GlobalSize(folder)
|
||||
res["globalFiles"], res["globalDirectories"], res["globalSymlinks"], res["globalDeleted"], res["globalBytes"] = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes
|
||||
res["globalFiles"], res["globalDirectories"], res["globalSymlinks"], res["globalDeleted"], res["globalBytes"], res["globalTotalItems"] = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes, global.TotalItems()
|
||||
|
||||
local := m.LocalSize(folder)
|
||||
res["localFiles"], res["localDirectories"], res["localSymlinks"], res["localDeleted"], res["localBytes"] = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes
|
||||
res["localFiles"], res["localDirectories"], res["localSymlinks"], res["localDeleted"], res["localBytes"], res["localTotalItems"] = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes, local.TotalItems()
|
||||
|
||||
need := m.NeedSize(folder)
|
||||
res["needFiles"], res["needDirectories"], res["needSymlinks"], res["needDeletes"], res["needBytes"] = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes
|
||||
res["needFiles"], res["needDirectories"], res["needSymlinks"], res["needDeletes"], res["needBytes"], res["needTotalItems"] = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes, need.TotalItems()
|
||||
|
||||
if cfg.Folders()[folder].Type == config.FolderTypeReceiveOnly {
|
||||
// Add statistics for things that have changed locally in a receive
|
||||
@@ -737,6 +726,7 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) (map[string]inter
|
||||
res["receiveOnlyChangedSymlinks"] = ro.Symlinks
|
||||
res["receiveOnlyChangedDeletes"] = ro.Deleted
|
||||
res["receiveOnlyChangedBytes"] = ro.Bytes
|
||||
res["receiveOnlyTotalItems"] = ro.TotalItems()
|
||||
}
|
||||
|
||||
res["inSyncFiles"], res["inSyncBytes"] = global.Files-need.Files, global.Bytes-need.Bytes
|
||||
@@ -804,9 +794,9 @@ func (s *apiService) getDBNeed(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Convert the struct to a more loose structure, and inject the size.
|
||||
sendJSON(w, map[string]interface{}{
|
||||
"progress": toNeedSlice(progress),
|
||||
"queued": toNeedSlice(queued),
|
||||
"rest": toNeedSlice(rest),
|
||||
"progress": toJsonFileInfoSlice(progress),
|
||||
"queued": toJsonFileInfoSlice(queued),
|
||||
"rest": toJsonFileInfoSlice(rest),
|
||||
"page": page,
|
||||
"perpage": perpage,
|
||||
})
|
||||
@@ -829,13 +819,29 @@ func (s *apiService) getDBRemoteNeed(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
} else {
|
||||
sendJSON(w, map[string]interface{}{
|
||||
"files": toNeedSlice(files),
|
||||
"files": toJsonFileInfoSlice(files),
|
||||
"page": page,
|
||||
"perpage": perpage,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiService) getDBLocalChanged(w http.ResponseWriter, r *http.Request) {
|
||||
qs := r.URL.Query()
|
||||
|
||||
folder := qs.Get("folder")
|
||||
|
||||
page, perpage := getPagingParams(qs)
|
||||
|
||||
files := s.model.LocalChangedFiles(folder, page, perpage)
|
||||
|
||||
sendJSON(w, map[string]interface{}{
|
||||
"files": toJsonFileInfoSlice(files),
|
||||
"page": page,
|
||||
"perpage": perpage,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiService) getSystemConnections(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, s.model.ConnectionStats())
|
||||
}
|
||||
@@ -1516,12 +1522,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)
|
||||
@@ -1557,6 +1563,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, "")
|
||||
@@ -1582,16 +1606,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) {
|
||||
@@ -1620,7 +1657,7 @@ func (s *apiService) getHeapProf(w http.ResponseWriter, r *http.Request) {
|
||||
pprof.WriteHeapProfile(w)
|
||||
}
|
||||
|
||||
func toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
|
||||
func toJsonFileInfoSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
|
||||
res := make([]jsonDBFileInfo, len(fs))
|
||||
for i, f := range fs {
|
||||
res[i] = jsonDBFileInfo(f)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,7 +54,7 @@ import (
|
||||
|
||||
var (
|
||||
Version = "unknown-dev"
|
||||
Codename = "Dysprosium Dragonfly"
|
||||
Codename = "Erbium Earthworm"
|
||||
BuildStamp = "0"
|
||||
BuildDate time.Time
|
||||
BuildHost = "unknown"
|
||||
@@ -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)
|
||||
|
||||
@@ -794,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)
|
||||
@@ -844,9 +828,11 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
pprof.StartCPUProfile(f)
|
||||
}
|
||||
|
||||
myDev, _ := cfg.Device(myID)
|
||||
l.Infof(`My name is "%v"`, myDev.Name)
|
||||
for _, device := range cfg.Devices() {
|
||||
if len(device.Name) > 0 {
|
||||
l.Infof("Device %s is %q at %v", device.DeviceID, device.Name, device.Addresses)
|
||||
if device.DeviceID != myID {
|
||||
l.Infof(`Device %s is "%v" at %v`, device.DeviceID, device.Name, device.Addresses)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -139,10 +139,14 @@ 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
|
||||
}
|
||||
|
||||
func (m *mockedModel) WatchError(folder string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
|
||||
return 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;
|
||||
|
||||
12
etc/linux-desktop/README.md
Normal file
12
etc/linux-desktop/README.md
Normal file
@@ -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
|
||||
9
etc/linux-desktop/syncthing-start.desktop
Normal file
9
etc/linux-desktop/syncthing-start.desktop
Normal file
@@ -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
|
||||
9
etc/linux-desktop/syncthing-ui.desktop
Normal file
9
etc/linux-desktop/syncthing-ui.desktop
Normal file
@@ -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
|
||||
@@ -10,5 +10,12 @@ Restart=on-failure
|
||||
SuccessExitStatus=3 4
|
||||
RestartForceExitStatus=3 4
|
||||
|
||||
# Hardening
|
||||
ProtectSystem=full
|
||||
PrivateTmp=true
|
||||
SystemCallArchitectures=native
|
||||
MemoryDenyWriteExecute=true
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -8,5 +8,12 @@ Restart=on-failure
|
||||
SuccessExitStatus=3 4
|
||||
RestartForceExitStatus=3 4
|
||||
|
||||
# Hardening
|
||||
ProtectSystem=full
|
||||
PrivateTmp=true
|
||||
SystemCallArchitectures=native
|
||||
MemoryDenyWriteExecute=true
|
||||
NoNewPrivileges=true
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Дебъгин Функционалост",
|
||||
"Default Folder Path": "Път до папка по подразбиране",
|
||||
"Deleted": "Изтрито",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Устройство",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство \"{{name}}\" ({{device}}) на {{address}} желае да се свърже. Добави ново устройство?",
|
||||
"Device ID": "Идентификатор на устройство",
|
||||
@@ -110,7 +111,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": "Филтриране по дата",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "По-късно",
|
||||
"Latest Change": "Последна промяна",
|
||||
"Learn more": "Научете повече",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Синхронизиращи устройства",
|
||||
"Loading data...": "Зарежадне на информация...",
|
||||
"Loading...": "Зареждане...",
|
||||
@@ -247,12 +249,13 @@
|
||||
"Scanning": "Сканиране",
|
||||
"See external versioner help for supported templated command line parameters.": "Прегледайте документацията на външното приложение за версии и поддържаните от него командни параметри. ",
|
||||
"See external versioning help for supported templated command line parameters.": "Прегледайте външната документацията за поддържаните командни параметри. ",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Изберте версия",
|
||||
"Select latest version": "Избор на най-новата версия",
|
||||
"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": "Сподели",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Utilitats de Depuració",
|
||||
"Default Folder Path": "Carpeta de la Ruta per Defecte",
|
||||
"Deleted": "Esborrat",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Dispositiu",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Dispositiu \"{{name}}\" ({{device}} a l'adreça {{address}}) vol connectar. Afegir nou dispositiu?",
|
||||
"Device ID": "ID del dispositiu",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Més tard",
|
||||
"Latest Change": "Últim Canvi",
|
||||
"Learn more": "Saber més",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Escoltants",
|
||||
"Loading data...": "Carregant dades...",
|
||||
"Loading...": "Carregant...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Rastrejant",
|
||||
"See external versioner help for supported templated command line parameters.": "Consulta l'ajuda externa sobre versions per a conéixer els paràmetres de la plantilla de la línia de comandaments.",
|
||||
"See external versioning help for supported templated command line parameters.": "Consulta l'ajuda externa sobre versions per a conéixer els paràmetres de la plantilla de la línia de comandaments.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Seleccionar una versió",
|
||||
"Select latest version": "Seleccionar l'última versió",
|
||||
"Select oldest version": "Seleccionar la versió més antiga",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Nástroje pro ladění",
|
||||
"Default Folder Path": "Výchozí cesta k adresáři",
|
||||
"Deleted": "Smazáno",
|
||||
"Deselect All": "Zrušit výběr",
|
||||
"Device": "Zařízení",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Zařízení \"{{name}}\" ({{device}} na {{address}}) se chce připojit. Přidat nové zařízení?",
|
||||
"Device ID": "ID zařízení",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Později",
|
||||
"Latest Change": "Poslední změna",
|
||||
"Learn more": "Zjistěte více",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Naslouchající",
|
||||
"Loading data...": "Nahrávání dat...",
|
||||
"Loading...": "Načítání...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Skenování",
|
||||
"See external versioner help for supported templated command line parameters.": "Pro upřesnění požadovaných parametrů příkazu navštivte nápovědu pro externí verzování.",
|
||||
"See external versioning help for supported templated command line parameters.": "Podporované šablonové parametry příkazové řádky jsou dostupné v nápovědě k externímu verzování.",
|
||||
"Select All": "Vybrat vše",
|
||||
"Select a version": "Vyberte verzi",
|
||||
"Select latest version": "Vybrat nejnovější verzi",
|
||||
"Select oldest version": "Vybrat nejstarší verzi",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Faciliteter til fejlretning",
|
||||
"Default Folder Path": "Standardmappesti",
|
||||
"Deleted": "Slettet",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Enhed",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enheden “{{name}}” ({{device}} på {{address}}) vil gerne forbinde. Tilføj denne enhed?",
|
||||
"Device ID": "Enheds-ID",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Senere",
|
||||
"Latest Change": "Seneste ændring",
|
||||
"Learn more": "Lær mere",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Lyttere",
|
||||
"Loading data...": "Indlæser data…",
|
||||
"Loading...": "Indlæser…",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Skanner",
|
||||
"See external versioner help for supported templated command line parameters.": "Se hjælp til ekstern versionering for understøttede kommandolinjeparametre.",
|
||||
"See external versioning help for supported templated command line parameters.": "Se hjælp til ekstern versionering for understøttede kommandolinjeparametre.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Vælg en version",
|
||||
"Select latest version": "Vælg seneste version",
|
||||
"Select oldest version": "Vælg ældste version",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"Allowed Networks": "Erlaubte Netzwerke",
|
||||
"Alphabetic": "Alphabetisch",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder.": "Ein externer Befehl führt die Versionierung durch. Er muss die Datei aus dem geteilten Ordner entfernen.",
|
||||
"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.": "Ein externer Befehl beeinflusst die Versionierung. Die Datei aus dem freigegebenen Ordner muss entfernen werden. Wenn der Pfad der Anwendung Leerzeichen enthält, sollte dieser in Anführungszeichen stehen.",
|
||||
"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.": "Ein externer Befehl behandelt die Versionierung. Die Datei aus dem freigegebenen Ordner muss entfernen werden. Wenn der Pfad der Anwendung Leerzeichen enthält, sollte dieser in Anführungszeichen stehen.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein externer Programmaufruf handhabt die Versionierung. Es muss die Datei aus dem zu synchronisierendem Ordner entfernen.",
|
||||
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
|
||||
"Anonymous usage report format has changed. Would you like to move to the new format?": "Das Format des anonymen Nutzungsberichts hat sich geändert. Möchten Sie auf das neue Format umsteigen?",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Connection Error": "Verbindungsfehler",
|
||||
"Connection Type": "Verbindungstyp",
|
||||
"Connections": "Verbindungen",
|
||||
"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.": "Kontinuierliches nach Änderungen suchen, ist jetzt in Syncthing verfügbar. Dadurch werden Änderungen auf der Festplatte erkannt und durch einen Scan nur die geänderten Pfade überprüft. Die Vorteile bestehen darin, dass Änderungen schneller festgestellt werden und weniger vollständige Scans erforderlich sind.",
|
||||
"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.": "Kontinuierliche Änderungssuche ist jetzt in Syncthing verfügbar. Dadurch werden Änderungen auf der Festplatte erkannt und durch einen Scan nur die geänderten Pfade überprüft. Die Vorteile bestehen darin, dass Änderungen schneller festgestellt werden und weniger vollständige Scans erforderlich sind.",
|
||||
"Copied from elsewhere": "Von anderer Quelle kopiert",
|
||||
"Copied from original": "Vom Original kopiert",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 der folgenden Unterstützer:",
|
||||
@@ -60,12 +60,13 @@
|
||||
"Debugging Facilities": "Debugging-Möglichkeiten",
|
||||
"Default Folder Path": "Standardmäßiger Ordnerpfad",
|
||||
"Deleted": "Gelöscht",
|
||||
"Deselect All": "Alle abwählen",
|
||||
"Device": "Gerät",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Gerät \"{{name}}\" ({{device}} {{address}}) möchte sich verbinden. Gerät hinzufügen?",
|
||||
"Device ID": "Gerätekennung",
|
||||
"Device Identification": "Geräteidentifikation",
|
||||
"Device Name": "Gerätename",
|
||||
"Device rate limits": "Teilnehmerbegrenzung",
|
||||
"Device rate limits": "Gerät Datenratelimit",
|
||||
"Device that last modified the item": "Gerät, das das Element zuletzt geändert hat",
|
||||
"Devices": "Geräte",
|
||||
"Disabled": "Deaktiviert",
|
||||
@@ -81,7 +82,7 @@
|
||||
"Do not restore all": "Nicht alle wiederherstellen",
|
||||
"Do you want to enable watching for changes for all your folders?": "Möchten Sie das nach Änderungen für alle Ihre Ordner gesucht wird aktivieren?",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Download",
|
||||
"Download Rate": "Downloadrate",
|
||||
"Downloaded": "Heruntergeladen",
|
||||
"Downloading": "Lädt herunter",
|
||||
"Edit": "Bearbeiten",
|
||||
@@ -100,7 +101,7 @@
|
||||
"External File Versioning": "Externe Dateiversionierung",
|
||||
"Failed Items": "Fehlgeschlagene Objekte",
|
||||
"Failed to load ignore patterns": "Fehler beim Laden der Ignoriermuster",
|
||||
"Failed to setup, retrying": "Fehler beim Installieren, erneut versuchen",
|
||||
"Failed to setup, retrying": "Fehler beim Installieren, versuche erneut",
|
||||
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Ein Verbindungsfehler zu IPv6-Servern ist zu erwarten, wenn es keine IPv6-Konnektivität gibt.",
|
||||
"File Pull Order": "Dateiübertragungsreihenfolge",
|
||||
"File Versioning": "Dateiversionierung",
|
||||
@@ -143,7 +144,7 @@
|
||||
"Ignored Devices": "Ignorierte Geräte",
|
||||
"Ignored Folders": "Ignorierte Ordner",
|
||||
"Ignored at": "Ignoriert bei/von",
|
||||
"Incoming Rate Limit (KiB/s)": "Limit Datenrate (eingehend) (KB/s)",
|
||||
"Incoming Rate Limit (KiB/s)": "Eingehendes Datenratelimit (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Eine falsche Konfiguration kann den Ordnerinhalt beschädigen und Syncthing in einen unausführbaren Zustand versetzen.",
|
||||
"Introduced By": "Verteilt von",
|
||||
"Introducer": "Verteilergerät",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Später",
|
||||
"Latest Change": "Letzte Änderung",
|
||||
"Learn more": "Mehr erfahren",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Zuhörer",
|
||||
"Loading data...": "Daten werden geladen...",
|
||||
"Loading...": "Wird geladen...",
|
||||
@@ -164,7 +166,7 @@
|
||||
"Local State (Total)": "Lokaler Status (Gesamt)",
|
||||
"Log": "Protokoll",
|
||||
"Log tailing paused. Click here to continue.": "Protokollaufzeichnungen sind pausiert. Klicken Sie hier um fortzufahren.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Protokoll-Tailing pausieren.\nScrolle nach unten zum fortfahren.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Protokoll-Tailing pausiert.\nScrolle nach unten zum fortfahren.",
|
||||
"Logs": "Protokolle",
|
||||
"Major Upgrade": "Hauptversionsaktualisierung",
|
||||
"Mass actions": "Massenaktionen",
|
||||
@@ -193,7 +195,7 @@
|
||||
"Options": "Optionen",
|
||||
"Out of Sync": "Nicht synchronisiert",
|
||||
"Out of Sync Items": "Nicht synchronisierte Objekte",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limit Datenrate (ausgehend) (KB/s)",
|
||||
"Outgoing Rate Limit (KiB/s)": "Ausgehendes Datenratelimit (KiB/s)",
|
||||
"Override Changes": "Änderungen überschreiben",
|
||||
"Path": "Pfad",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Pfad zum Ordner auf dem lokalen Gerät. Ordner wird erzeugt, wenn er nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Scannen",
|
||||
"See external versioner help for supported templated command line parameters.": "Siehe Hilfe des externen Versionierers für unterstützte Befehlszeilenparameter.",
|
||||
"See external versioning help for supported templated command line parameters.": "Siehe Hilfe zur externen Versionierung für unterstützte Befehlszeilenparameter.",
|
||||
"Select All": "Alle auswählen",
|
||||
"Select a version": "Wählen Sie eine Version",
|
||||
"Select latest version": "Letzte Version auswählen",
|
||||
"Select oldest version": "Älteste Version auswählen",
|
||||
@@ -283,7 +286,7 @@
|
||||
"Statistics": "Statistiken",
|
||||
"Stopped": "Gestoppt",
|
||||
"Support": "Support",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Support Bundle": "Supportpaket",
|
||||
"Sync Protocol Listen Addresses": "Adresse(n) für das Synchronisierungsprotokoll",
|
||||
"Syncing": "Synchronisiere",
|
||||
"Syncthing has been shut down.": "Syncthing wurde heruntergefahren.",
|
||||
@@ -315,7 +318,7 @@
|
||||
"The number of old versions to keep, per file.": "Anzahl der alten Versionen, die von jeder Datei behalten werden sollen.",
|
||||
"The number of versions must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Ganzzahl und darf nicht leer sein.",
|
||||
"The path cannot be blank.": "Der Pfad darf nicht leer sein.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Das Datenrate-Limit muss eine nicht negative Zahl sein (0 = kein Limit).",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Das Datenratelimit muss eine nicht negative Zahl sein (0 = kein Limit).",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Das Scanintervall muss eine nicht negative Anzahl (in Sekunden) sein.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Sie werden automatisch heruntergeladen und werden synchronisiert, wenn der Fehler behoben wurde.",
|
||||
"This Device": "Dieses Gerät",
|
||||
@@ -338,7 +341,7 @@
|
||||
"Upgrade": "Aktualisierung",
|
||||
"Upgrade To {%version%}": "Aktualisierung auf {{version}}",
|
||||
"Upgrading": "Wird aktualisiert",
|
||||
"Upload Rate": "Upload",
|
||||
"Upload Rate": "Uploadrate",
|
||||
"Uptime": "Betriebszeit",
|
||||
"Usage reporting is always enabled for candidate releases.": "Nutzungsbericht ist für Veröffentlichungskandidaten immer aktiviert.",
|
||||
"Use HTTPS for GUI": "HTTPS für Benutzeroberfläche verwenden",
|
||||
@@ -346,10 +349,10 @@
|
||||
"Versions": "Versionen",
|
||||
"Versions Path": "Versionierungspfad",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Alte Dateiversionen werden automatisch gelöscht, wenn sie älter als das angegebene Höchstalter sind oder die angegebene Höchstzahl an Dateien erreicht ist.",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warnung, dieser Pfad ist ein übergeordnetes Verzeichnis eines existierenden Ordners \"{{otherFolder}}\".",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warnung, dieser Pfad ist ein übergeordnetes Verzeichnis eines existierenden Ordners \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warnung, dieser Pfad ist ein übergeordneter Ordner eines existierenden Ordners \"{{otherFolder}}\".",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warnung, dieser Pfad ist ein übergeordneter Ordner eines existierenden Ordners \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warnung, dieser Pfad ist ein Unterordner des existierenden Ordners \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warnung, dieser Pfad ist ein Unterverzeichnis eines existierenden Ordners \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warnung, dieser Pfad ist ein Unterordner eines existierenden Ordners \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Achtung: Wenn Sie einen externen Beobachter wie {{syncthingInotify}} benutzen, sollten sie sicher sein das dieser deaktiviert ist.",
|
||||
"Watch for Changes": "Auf Änderungen achten",
|
||||
"Watching for Changes": "Auf Änderungen achten",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Εργαλεία αποσφαλμάτωσης",
|
||||
"Default Folder Path": "Προκαθορισμένη διαδρομή φακέλων",
|
||||
"Deleted": "Διαγραμμένα",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Συσκευή",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Η συσκευή \"{{name}}\" ({{device}} στη διεύθυνση {{address}}) επιθυμεί να συνδεθεί. Προσθήκη της νέας συσκευής;",
|
||||
"Device ID": "Ταυτότητα συσκευής",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Αργότερα",
|
||||
"Latest Change": "Τελευταία αλλαγή",
|
||||
"Learn more": "Μάθετε περισσότερα",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Ακροατές",
|
||||
"Loading data...": "Φόρτωση δεδομένων...",
|
||||
"Loading...": "Φόρτωση...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Έλεγχος για αλλαγές",
|
||||
"See external versioner help for supported templated command line parameters.": "Ανατρέξτε στην τεκμηρίωση της εξωτερικής τήρησης εκδόσεων για πληροφορίες σχετικά με τις υποστηριζόμενες παραμέτρους της γραμμής εντολών.",
|
||||
"See external versioning help for supported templated command line parameters.": "Ανατρέξτε στην τεκμηρίωση της εξωτερικής τήρησης εκδόσεων για πληροφορίες σχετικά με τις υποστηριζόμενες παραμέτρους της γραμμής εντολών.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Επιλογή έκδοσης",
|
||||
"Select latest version": "Επιλογή τελευταίας έκδοσης",
|
||||
"Select oldest version": "Επιλογή παλαιότερης έκδοσης",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
"Default Folder Path": "Default Folder Path",
|
||||
"Deleted": "Deleted",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Device",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "Device ID",
|
||||
@@ -110,7 +111,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",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Later",
|
||||
"Latest Change": "Latest Change",
|
||||
"Learn more": "Learn more",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Listeners",
|
||||
"Loading data...": "Loading data...",
|
||||
"Loading...": "Loading...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Scanning",
|
||||
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Select a version",
|
||||
"Select latest version": "Select latest version",
|
||||
"Select oldest version": "Select oldest version",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
"Default Folder Path": "Default Folder Path",
|
||||
"Deleted": "Deleted",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Device",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "Device ID",
|
||||
@@ -156,12 +157,14 @@
|
||||
"Later": "Later",
|
||||
"Latest Change": "Latest Change",
|
||||
"Learn more": "Learn more",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Listeners",
|
||||
"Loading data...": "Loading data...",
|
||||
"Loading...": "Loading...",
|
||||
"Local Discovery": "Local Discovery",
|
||||
"Local State": "Local State",
|
||||
"Local State (Total)": "Local State (Total)",
|
||||
"Locally Changed Items": "Locally Changed Items",
|
||||
"Log": "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.",
|
||||
@@ -247,6 +250,7 @@
|
||||
"Scanning": "Scanning",
|
||||
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Select a version",
|
||||
"Select latest version": "Select latest version",
|
||||
"Select oldest version": "Select oldest version",
|
||||
@@ -307,6 +311,7 @@
|
||||
"The folder path cannot be blank.": "The folder path cannot be blank.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
|
||||
"The following items could not be synchronized.": "The following items could not be synchronized.",
|
||||
"The following items were changed locally.": "The following items were changed locally.",
|
||||
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).",
|
||||
@@ -346,6 +351,7 @@
|
||||
"Versions": "Versions",
|
||||
"Versions Path": "Versions Path",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.",
|
||||
"Waiting to scan": "Waiting to scan",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a parent directory of an existing folder \"{{otherFolder}}\".",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warning, this path is a parent directory of an existing folder \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Ayudas a la depuración",
|
||||
"Default Folder Path": "Ruta de la carpeta por defecto",
|
||||
"Deleted": "Eliminado",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Dispositivo",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "El dispositivo \"{{name}}\" ({{device}} en la dirección {{address}}) quiere conectarse. Añadir nuevo dispositivo?",
|
||||
"Device ID": "ID del Dispositivo",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Más tarde",
|
||||
"Latest Change": "Último Cambio",
|
||||
"Learn more": "Saber más",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Oyentes",
|
||||
"Loading data...": "Cargando datos...",
|
||||
"Loading...": "Cargando...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Analizando",
|
||||
"See external versioner help for supported templated command line parameters.": "Consultar la ayuda externa del versionador para ver las plantillas de los parámetros de línea de comandos",
|
||||
"See external versioning help for supported templated command line parameters.": "Consultar la ayuda externa del versionado para ver las plantillas de los parámetros de línea de comandos",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Selecciona una versión",
|
||||
"Select latest version": "Selecciona la última versión",
|
||||
"Select oldest version": "Selecciona la versión más antigua",
|
||||
|
||||
@@ -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:",
|
||||
@@ -60,26 +60,27 @@
|
||||
"Debugging Facilities": "Servicios de depuración",
|
||||
"Default Folder Path": "Ruta de la carpeta por defecto",
|
||||
"Deleted": "Eliminado",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Dispositivo",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "El dispositivo \"{{name}}\" ({{device}} en la dirección {{address}}) quiere conectarse. Añadir nuevo dispositivo?",
|
||||
"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 +92,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 +100,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 +111,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 +122,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 +141,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",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Más tarde",
|
||||
"Latest Change": "Último Cambio",
|
||||
"Learn more": "Saber más",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Oyentes",
|
||||
"Loading data...": "Cargando datos...",
|
||||
"Loading...": "Cargando...",
|
||||
@@ -164,7 +166,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 +205,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 +220,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 +233,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,13 +242,14 @@
|
||||
"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",
|
||||
"See external versioner help for supported templated command line parameters.": "Vea la ayuda del gestor de versiones externo para los parámetros de linea de comandos que usan una plantilla.",
|
||||
"See external versioning help for supported templated command line parameters.": "Vea la ayuda del gestor de versiones externo para los parámetros de linea de comandos que usan una plantilla.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Seleccione una versión",
|
||||
"Select latest version": "Seleccione la última versión",
|
||||
"Select oldest version": "Seleccione la versión más antigua",
|
||||
@@ -261,7 +264,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,7 +286,7 @@
|
||||
"Statistics": "Estadísticas",
|
||||
"Stopped": "Detenido",
|
||||
"Support": "Forum",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"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.",
|
||||
@@ -292,8 +295,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.",
|
||||
@@ -329,7 +332,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",
|
||||
@@ -350,18 +353,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",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
"Default Folder Path": "Default Folder Path",
|
||||
"Deleted": "Kendua",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Tresna",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Tresna \"{{name}}\" ({{device}} {{address}} era) konektatu nahi du. Onhartzen duzu ?",
|
||||
"Device ID": "Tresnaren ID-a",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Berantago",
|
||||
"Latest Change": "Azken aldaketa",
|
||||
"Learn more": "Gehiago jakiteko",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Entzungailuak",
|
||||
"Loading data...": "Loading data...",
|
||||
"Loading...": "Loading...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Azterketa martxan",
|
||||
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Select a version",
|
||||
"Select latest version": "Select latest version",
|
||||
"Select oldest version": "Select oldest version",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Debug -luokat",
|
||||
"Default Folder Path": "Oletuspolku kansioille",
|
||||
"Deleted": "Poistettu",
|
||||
"Deselect All": "Poista valinnat",
|
||||
"Device": "Laite",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Laite \"{{name}}\" {{device}} osoitteessa ({{address}}) haluaa yhdistää. Lisää uusi laite?",
|
||||
"Device ID": "Laitteen ID",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Myöhemmin",
|
||||
"Latest Change": "Viimeisin muutos",
|
||||
"Learn more": "Lisätietoja",
|
||||
"Limit": "Rajoita",
|
||||
"Listeners": "Kuuntelijat",
|
||||
"Loading data...": "Lataa...",
|
||||
"Loading...": "Lataa...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Skannataan",
|
||||
"See external versioner help for supported templated command line parameters.": "Katso ulkopuolisen versiohallinnan tukisivu komentoriviparametreistä.",
|
||||
"See external versioning help for supported templated command line parameters.": "Katso ulkopuolisen versiohallinnan tukisivu komentoriviparametreistä.",
|
||||
"Select All": "Valitse kaikki",
|
||||
"Select a version": "Valitse versio",
|
||||
"Select latest version": "Valitse viimeisin versio",
|
||||
"Select oldest version": "Valitse vanhin versio",
|
||||
@@ -283,7 +286,7 @@
|
||||
"Statistics": "Tilastot",
|
||||
"Stopped": "Pysäytetty",
|
||||
"Support": "Tuki",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Support Bundle": "Tukipaketti. (Tiedostot vianselvitystä varten.)",
|
||||
"Sync Protocol Listen Addresses": "Synkronointiprotokollan kuunteluosoite",
|
||||
"Syncing": "Synkronoidaan",
|
||||
"Syncthing has been shut down.": "Syncthing on sammutettu.",
|
||||
@@ -293,7 +296,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing näyttää olevan alhaalla tai internetyhteydessä on ongelma. Yritetään uudelleen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ei pysty käsittelemään pyyntöäsi. Ole hyvä ja päivitä sivu tai käynnistä Syncthing uudelleen, jos ongelma jatkuu.",
|
||||
"Take me back": "Takaisin",
|
||||
"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.": "Käyttöliittymän osoite on asetettu käynnistysparametreillä. Muutokset täällä tulevat voimaan vasta, kun käynnistysparametrejä ei käytetä.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthingin hallintakäyttöliittymä on asetettu sallimaan ulkoiset yhteydet ilman salasanaa.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Koostetut tilastot ovat julkisesti saatavilla alla olevassa osoitteessa.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Asetukset on tallennettu, mutta niitä ei ole otettu käyttöön. Syncthingin täytyy käynnistyä uudelleen, jotta uudet asetukset saadaan käyttöön.",
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
{
|
||||
"A device with that ID is already added.": "L'appareil portant cette ID est déjà présent.",
|
||||
"A negative number of days doesn't make sense.": "Ce champ n'accepte qu'un entier positif ou nul.",
|
||||
"A new major version may not be compatible with previous versions.": "Une nouvelle version majeure peut présenter des incompatibilités avec les versions antérieures.",
|
||||
"API Key": "Clé API",
|
||||
"About": "À propos",
|
||||
"Action": "Action",
|
||||
"Actions": "Actions",
|
||||
"Add": "Ajouter",
|
||||
"Add Device": "Ajouter l'appareil",
|
||||
"Add Folder": "Ajouter un partage",
|
||||
"Add Remote Device": "Ajouter un appareil",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Lui permettre d'ajouter et enlever des membres à toutes mes listes de membres de partages dont il fait partie (ceci permet de créer toutes les liaisons point à point possibles en complétant mes listes par les siennes, meilleur débit de réception par cumul des débits d'envoi, indépendance vis à vis de l'introducteur, etc).",
|
||||
"Add new folder?": "Ajouter ce partage ?",
|
||||
"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.",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresses",
|
||||
"Advanced": "Avancé",
|
||||
"Advanced Configuration": "Configuration avancée",
|
||||
"Advanced settings": "Paramètres avancés",
|
||||
"All Data": "Toutes les données",
|
||||
"Allow Anonymous Usage Reporting?": "Autoriser l'envoi de statistiques d'utilisation anonymisées ?",
|
||||
"Allowed Networks": "Réseaux autorisés",
|
||||
"Alphabetic": "Alphabétique",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers dans le répertoire synchronisé.",
|
||||
"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.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers dans le répertoire synchronisé.",
|
||||
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
|
||||
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Lui permettre d'ajouter et enlever des membres à toutes mes listes de membres de partages dont il fait partie (ceci permet de créer toutes les liaisons point à point possibles en complétant mes listes par les siennes, meilleur débit de réception par cumul des débits d'envoi, indépendance vis à vis de l'introducteur, etc).",
|
||||
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
|
||||
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
|
||||
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
|
||||
"Auto Accept": "Auto Accept",
|
||||
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Le système de mise à jour automatique propose le choix entre versions stables et versions préliminaires.",
|
||||
"Automatic upgrades": "Mises à jour automatiques",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
|
||||
"Available debug logging facilities:": "Available debug logging facilities:",
|
||||
"Be careful!": "Faites attention !",
|
||||
"Bugs": "Bugs",
|
||||
"CPU Utilization": "Utilisation du CPU",
|
||||
"Changelog": "Historique des versions",
|
||||
"Clean out after": "Purger après :",
|
||||
"Click to see discovery failures": "Voir les échecs de découverte",
|
||||
"Close": "Fermer",
|
||||
"Command": "Commande",
|
||||
"Comment, when used at the start of a line": "Commentaire lorsque utilisé en début de ligne",
|
||||
"Compression": "Compression",
|
||||
"Configured": "Configuré",
|
||||
"Connection Error": "Erreur de connexion",
|
||||
"Connection Type": "Type de connexion",
|
||||
"Connections": "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.",
|
||||
"Copied from elsewhere": "Copié d'ailleurs",
|
||||
"Copied from original": "Copié depuis l'original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016, les contributeurs suivants:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017, les contributeurs suivants :",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Création de masques d'exclusion, remplacement du fichier existant : {{path}}.",
|
||||
"Danger!": "Attention !",
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
"Default Folder Path": "Default Folder Path",
|
||||
"Deleted": "Supprimé",
|
||||
"Device": "Appareil",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "L'appareil \"{{name}}\" ({{device}} à l'IP {{address}}) souhaite se connecter. L'acceptez-vous ?",
|
||||
"Device ID": "ID de l'appareil",
|
||||
"Device Identification": "Identifiant de l'appareil",
|
||||
"Device Name": "Nom de l'appareil",
|
||||
"Device rate limits": "Device rate limits",
|
||||
"Device that last modified the item": "Device that last modified the item",
|
||||
"Devices": "Appareil",
|
||||
"Disabled": "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",
|
||||
"Disconnected": "Déconnecté",
|
||||
"Discovered": "Découvert",
|
||||
"Discovery": "Découverte",
|
||||
"Discovery Failures": "Échecs de découverte",
|
||||
"Do not restore": "Do not restore",
|
||||
"Do not restore all": "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?",
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Débit de réception",
|
||||
"Downloaded": "Reçu",
|
||||
"Downloading": "Réception",
|
||||
"Edit": "Modifier",
|
||||
"Edit Device": "Modifier l'appareil",
|
||||
"Edit Folder": "Modifier le partage",
|
||||
"Editing": "Modifications",
|
||||
"Editing {%path%}.": "Modification de {{path}}.",
|
||||
"Enable NAT traversal": "Activer transfert d'adresses NAT",
|
||||
"Enable Relaying": "Activer le relayage",
|
||||
"Enabled": "Enabled",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Nombre positif (p.ex, \"2.35\") et unité. Pourcentage de l'espace disque total.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Enter a non-privileged port number (1024 - 65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://hôte:port\") séparées par une virgule, ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
|
||||
"Enter ignore patterns, one per line.": "Entrer les masques d'exclusion, un par ligne.",
|
||||
"Error": "Erreur",
|
||||
"External File Versioning": "Gestion externe des versions de fichiers",
|
||||
"Failed Items": "Éléments en échec",
|
||||
"Failed to load ignore patterns": "Failed to load ignore patterns",
|
||||
"Failed to setup, retrying": "Failed to setup, retrying",
|
||||
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "La connexion aux serveurs en IPv6 va échouer s'il n'y a pas de connectivité IPv6.",
|
||||
"File Pull Order": "Ordre de récupération de fichier",
|
||||
"File Versioning": "Méthode de préservation des fichiers",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Les bits de permission de fichier sont ignorés lors de la recherche de changements. Utilisé sur les systèmes de fichiers FAT.",
|
||||
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Les fichiers sont déplacés dans le sous-répertoire .stversions quand ils sont remplacés ou supprimés par Syncthing. Leurs chemins d'accès relatifs y sont recréés si besoin.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Les fichiers sont déplacés dans le sous-répertoire .stversions quand ils sont remplacés ou supprimés par Syncthing. Leurs chemins d'accès relatifs y sont recréés si besoin.",
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Quand ils sont remplacés ou supprimés par Syncthing, les fichiers sont déplacés et horodatés vers le sous-répertoire .stversions dans une arborescence relative identique à celle de l'original.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Quand ils sont remplacés ou supprimés par Syncthing, les fichiers sont déplacés et horodatés vers le sous-répertoire .stversions dans une arborescence relative identique à celle de l'original.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Les fichiers sont protégés des changements réalisés sur les autres appareils, mais les changements réalisés sur celui-ci seront transférés aux autres.",
|
||||
"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 Notifications",
|
||||
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
|
||||
"Filter by date": "Filter by date",
|
||||
"Filter by name": "Filter by name",
|
||||
"Folder": "Partage",
|
||||
"Folder ID": "ID du partage",
|
||||
"Folder Label": "Nom du partage",
|
||||
"Folder Path": "Chemin racine du partage",
|
||||
"Folder Type": "Type de partage",
|
||||
"Folders": "Partages",
|
||||
"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)",
|
||||
"GUI": "Interface graphique",
|
||||
"GUI Authentication Password": "Mot de passe d'authentification GUI",
|
||||
"GUI Authentication User": "Utilisateur autorisé GUI",
|
||||
"GUI Listen Address": "GUI Listen Address",
|
||||
"GUI Listen Addresses": "Adresses de l'interface (GUI)",
|
||||
"GUI Theme": "Thème graphique",
|
||||
"General": "General",
|
||||
"Generate": "Générer",
|
||||
"Global Changes": "Derniers changements",
|
||||
"Global Discovery": "Découverte globale",
|
||||
"Global Discovery Servers": "Serveurs de découverte globale",
|
||||
"Global State": "État global",
|
||||
"Help": "Aide",
|
||||
"Home page": "Page d'accueil",
|
||||
"Ignore": "Ignorer",
|
||||
"Ignore Patterns": "Règles d'exclusion",
|
||||
"Ignore Permissions": "Ignorer les permissions",
|
||||
"Ignored Devices": "Ignored Devices",
|
||||
"Ignored Folders": "Ignored Folders",
|
||||
"Ignored at": "Ignored at",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite du débit de réception (Kio/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Une configuration incorrecte peut créer des dommages dans vos répertoires et mettre Syncthing hors-service.",
|
||||
"Introduced By": "Introduit par",
|
||||
"Introducer": "Appareil introducteur",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inverser la condition donnée (i.e. ne pas exclure)",
|
||||
"Keep Versions": "Combien de versions conserver",
|
||||
"Largest First": "Les plus volumineux d'abord",
|
||||
"Last File Received": "Dernier changement",
|
||||
"Last Scan": "Dernière analyse",
|
||||
"Last seen": "Dernière apparition",
|
||||
"Later": "Plus tard",
|
||||
"Latest Change": "Dernier changement",
|
||||
"Learn more": "En savoir plus",
|
||||
"Listeners": "Systèmes à l'écoute",
|
||||
"Loading data...": "Loading data...",
|
||||
"Loading...": "Loading...",
|
||||
"Local Discovery": "Découverte locale",
|
||||
"Local State": "État local",
|
||||
"Local State (Total)": "État local (Total)",
|
||||
"Log": "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.",
|
||||
"Logs": "Logs",
|
||||
"Major Upgrade": "Mise à jour majeure",
|
||||
"Mass actions": "Mass actions",
|
||||
"Master": "Maître",
|
||||
"Maximum Age": "Ancienneté maximum",
|
||||
"Metadata Only": "Métadonnées uniquement",
|
||||
"Minimum Free Disk Space": "Espace disque libre minimum",
|
||||
"Mod. Device": "Mod. Device",
|
||||
"Mod. Time": "Mod. Time",
|
||||
"Move to top of queue": "Déplacer en haut de la file",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Joker multi niveaux (correspond aux répertoires et sous-répertoires)",
|
||||
"Never": "Jamais",
|
||||
"New Device": "Nouvel appareil",
|
||||
"New Folder": "Nouveau partage",
|
||||
"Newest First": "Les plus récents en premier",
|
||||
"No": "Non",
|
||||
"No File Versioning": "Pas de préservation",
|
||||
"No files will be deleted as a result of this operation.": "No files will be deleted as a result of this operation.",
|
||||
"No upgrades": "Pas de mises à jour",
|
||||
"Normal": "Normal",
|
||||
"Notice": "Notification",
|
||||
"OK": "OK",
|
||||
"Off": "Désactivé(e)",
|
||||
"Oldest First": "Les plus anciens en premier",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Nom convivial du partage, à votre guise. il peut être différent sur chaque appareil.",
|
||||
"Options": "Options",
|
||||
"Out of Sync": "Désynchronisé",
|
||||
"Out of Sync Items": "Éléments non synchronisés",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite du débit d'envoi (Kio/s)",
|
||||
"Override Changes": "Écraser les changements",
|
||||
"Path": "Chemin",
|
||||
"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": "Chemin vers le répertoire à partager dans l'appareil local. Il sera créé s'il n'existe pas. Vous pouvez entrer un chemin absolu (p.ex \"/home/moi/Sync/Exemple\") ou relatif à celui du programme (p.ex \"..\\Partages\\Exemple\" - utile pour installation portable). Le caractère tilde (~, ou ~+Espace sous Windows XP+Azerty) peut être utilisé comme raccourci vers",
|
||||
"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 versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire partagé).\nChemin relatif ou absolu (recommandé), mais dans un répertoire non synchronisé (par masque ou hors du chemin du partage).\nSur la même partition ou système de fichiers (recommandé).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire partagé).\nChemin relatif ou absolu (recommandé), mais dans un répertoire non synchronisé (par masque ou hors du chemin du partage).\nSur la même partition ou système de fichiers (recommandé).",
|
||||
"Pause": "Pause",
|
||||
"Pause All": "Tout suspendre",
|
||||
"Paused": "En pause",
|
||||
"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",
|
||||
"Please consult the release notes before performing a major upgrade.": "Veuillez consulter les notes de version avant de réaliser une mise à jour majeure.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Veuillez définir un nom d'utilisateur et un mot de passe dans les réglages.",
|
||||
"Please wait": "Merci de patienter",
|
||||
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix indicating that the file can be deleted if preventing directory removal",
|
||||
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefix indicating that the pattern should be matched without case sensitivity",
|
||||
"Preview": "Aperçu",
|
||||
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
|
||||
"Quick guide to supported patterns": "Guide rapide des masques compatibles ci-dessous",
|
||||
"RAM Utilization": "Utilisation de la RAM",
|
||||
"Random": "Aléatoire",
|
||||
"Receive Only": "Receive Only",
|
||||
"Recent Changes": "Recent Changes",
|
||||
"Reduced by ignore patterns": "(Limité par des masques d'exclusion)",
|
||||
"Release Notes": "Notes de version",
|
||||
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Les versions préliminaires contiennent les dernières fonctionnalités et derniers correctifs. Elles sont identiques aux traditionnelles mises à jour bimensuelles.",
|
||||
"Remote Devices": "Autres appareils",
|
||||
"Remove": "Enlever",
|
||||
"Remove Device": "Remove Device",
|
||||
"Remove Folder": "Remove Folder",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Identifiant du partage. Doit être le même sur tous les appareils concernés.",
|
||||
"Rescan": "Réanalyser",
|
||||
"Rescan All": "Tout réanalyser",
|
||||
"Rescan Interval": "Intervalle d'analyse",
|
||||
"Rescans": "Rescans",
|
||||
"Restart": "Redémarrer",
|
||||
"Restart Needed": "Redémarrage nécessaire",
|
||||
"Restarting": "Redémarrage en cours",
|
||||
"Restore": "Restore",
|
||||
"Restore Versions": "Restore Versions",
|
||||
"Resume": "Reprise",
|
||||
"Resume All": "Tout libérer",
|
||||
"Reused": "Réutilisé",
|
||||
"Revert Local Changes": "Revert Local Changes",
|
||||
"Running": "Running",
|
||||
"Save": "Enregistrer",
|
||||
"Scan Time Remaining": "Temps d'analyse restant",
|
||||
"Scanning": "Analyse en cours",
|
||||
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
|
||||
"Select a version": "Select a version",
|
||||
"Select latest version": "Select latest version",
|
||||
"Select oldest version": "Select oldest version",
|
||||
"Select the devices to share this folder with.": "Synchroniser avec :",
|
||||
"Select the folders to share with this device.": "Sélectionner les partages auxquels participe cet appareil.",
|
||||
"Send & Receive": "Envoi & réception",
|
||||
"Send Only": "Envoi (lecture seule)",
|
||||
"Settings": "Configuration",
|
||||
"Share": "Partager",
|
||||
"Share Folder": "Partager",
|
||||
"Share Folders With Device": "Partages avec cet appareil",
|
||||
"Share With Devices": "Synchroniser avec des appareils",
|
||||
"Share this folder?": "Acceptez-vous ce partage ?",
|
||||
"Shared With": "Synchronisé avec",
|
||||
"Sharing": "Sharing",
|
||||
"Show ID": "Afficher mon ID",
|
||||
"Show QR": "Afficher l'image QR",
|
||||
"Show diff with previous version": "Show diff with previous version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Affiché à la place de l'ID de l'appareil dans l'état du groupe. Sera diffusé aux autres appareils comme nom convivial optionnel par défaut.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de l'appareil dans l'état du groupe. Si laissé vide, il sera renseigné par le nom convivial proposé par l'appareil distant.",
|
||||
"Shutdown": "Arrêter",
|
||||
"Shutdown Complete": "Arrêté !",
|
||||
"Simple File Versioning": "Suivi simplifié des versions",
|
||||
"Single level wildcard (matches within a directory only)": "Joker à un seul niveau (correspond uniquement à l’intérieur du répertoire)",
|
||||
"Size": "Size",
|
||||
"Smallest First": "Les plus petits d'abord",
|
||||
"Some items could not be restored:": "Some items could not be restored:",
|
||||
"Source Code": "Code source",
|
||||
"Stable releases and release candidates": "Versions stables et préliminaires",
|
||||
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Les versions stables sont reportées d'environ deux semaines. Pendant ce temps elles sont testées en tant que versions préliminaires.",
|
||||
"Stable releases only": "Seulement les versions stables",
|
||||
"Staggered File Versioning": "Versions échelonnées",
|
||||
"Start Browser": "Lancer le navigateur web",
|
||||
"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é.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing intègre les logiciels suivants (ou des éléments provenant de ces logiciels) :",
|
||||
"Syncthing is restarting.": "Syncthing redémarre.",
|
||||
"Syncthing is upgrading.": "Syncthing se met à jour.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être arrêté, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plaît, rafraîchissez la page ou redémarrez Syncthing si le problème 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.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interface d'administration de Syncthing est configuré pour accepter l'accès distant sans mot de passe !",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Les statistiques agrégées sont publiquement disponibles à l'adresse ci-dessous.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
|
||||
"The device ID cannot be blank.": "L'ID de l'appareil ne peut être vide.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID d'appareil à saisir ici se trouve dans le menu \"Actions > Afficher mon ID\" sur l'appareil distant. Les tirets et espaces sont optionnels (et ignorés).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plates-formes utilisées, la taille des partages et les versions de l'application. Si le jeu de données rapportées devait être changé, il vous serait demandé de valider de nouveau son envoi via ce message. Vous pouvez revenir sur votre décision via Actions/Configuration, et agir sur la fréquence d'envoi via Actions/Avancé/Options (urInitialDelayS).",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID de l'appareil inséré ne semble pas valide. Il devrait ressembler à une chaîne de 52 ou 56 caractères comprenant des lettres, des chiffres et potentiellement des espaces et des traits d'union.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Le premier paramètre de ligne de commande est le chemin du répertoire partagé, et le second est le chemin relatif dans le répertoire.",
|
||||
"The folder ID cannot be blank.": "L'identifiant du partage ne peut être vide.",
|
||||
"The folder ID must be unique.": "L'ID du partage doit être unique.",
|
||||
"The folder path cannot be blank.": "Le chemin vers le répertoire ne peut pas être vide.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Les seuils de durée suivants définissent le nombre maximum de versions pour chaque fichier : pendant la première heure une version peut être conservée toutes les 30 secondes. Jusqu'à un jour, jusqu'à une version par heure - des versions de la première heure sont alors progressivement effacées pour n'en garder qu'une par heure. Jusqu'à 30 jours, jusqu'à une version par jour - des versions horaires du premier jour sont alors progressivement effacées pour n'en garder qu'une par jour. Au-delà, jusqu'à la limite d'âge, jusqu'à une version est conservée par semaine - des versions journalières du premier mois sont alors progressivement effacées pour n'en garder qu'une par semaine.",
|
||||
"The following items could not be synchronized.": "Les fichiers suivants n'ont pas pu être synchronisés.",
|
||||
"The maximum age must be a number and cannot be blank.": "L'âge maximum doit être un nombre et ne peut être vide.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Durée maximum de conservation d'une version (en jours, 0 pour conserver les versions indéfiniment)",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Le pourcentage d'espace disque libre doit être un nombre positif compris entre 0 et 100 (inclus).",
|
||||
"The number of days must be a number and cannot be blank.": "Le nombre de jours doit être numérique et ne peut pas être vide.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Le nombre de jours de conservation des fichiers dans la poubelle. 0 signifie toujours.",
|
||||
"The number of old versions to keep, per file.": "Le nombre maximum d'anciennes versions à garder indéfiniment, par fichier.",
|
||||
"The number of versions must be a number and cannot be blank.": "Le nombre de versions doit être numérique, et ne peut pas être vide.",
|
||||
"The path cannot be blank.": "Le chemin ne peut pas être vide.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "La limite de débit ne doit pas être négative (0 = pas de limite)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "L'intervalle d'analyse ne doit pas être un nombre négatif de secondes.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Ils seront automatiquement retentés et synchronisés quand l'erreur sera résolue.",
|
||||
"This Device": "Cet appareil",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Ceci peut aisément permettre à un intrus de lire et modifier n'importe quel fichier de votre ordinateur. ",
|
||||
"This is a major version upgrade.": "Il s'agit d'une mise à jour majeure.",
|
||||
"This setting controls the free space required on the home (i.e., index database) disk.": "Ce réglage contrôle l'espace disque requis dans le disque qui abrite votre répertoire utilisateur (pour la base de données d'indexation).",
|
||||
"Time": "Heure",
|
||||
"Time the item was last modified": "Time the item was last modified",
|
||||
"Trash Can File Versioning": "Style poubelle",
|
||||
"Type": "Type",
|
||||
"Unavailable": "Unavailable",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
|
||||
"Undecided (will prompt)": "Undecided (will prompt)",
|
||||
"Unignore": "Unignore",
|
||||
"Unknown": "Inconnu",
|
||||
"Unshared": "Non partagé",
|
||||
"Unused": "Non utilisé",
|
||||
"Up to Date": "À jour",
|
||||
"Updated": "Mis à jour",
|
||||
"Upgrade": "Mettre à jour",
|
||||
"Upgrade To {%version%}": "Mettre à jour vers {{version}}",
|
||||
"Upgrading": "Mise à jour de Syncthing",
|
||||
"Upload Rate": "Débit d'envoi",
|
||||
"Uptime": "Durée de fonctionnement",
|
||||
"Usage reporting is always enabled for candidate releases.": "Les statistiques d'utilisation sont toujours envoyées pour les versions préliminaires.",
|
||||
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
|
||||
"Version": "Version",
|
||||
"Versions": "Versions",
|
||||
"Versions Path": "Emplacement des versions",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les plus anciennes versions seront supprimées automatiquement quand elles dépassent la durée maximum de conservation ou si leur nombre (par fichier) est supérieur à la limite prédéfinie pour l'intervalle.",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Attention, ce chemin est un répertoire parent d'au moins un partage existant (par exemple \"{{otherFolder}}\"). Si vous continuez, vous devriez créer un nouveau sous-répertoire, sinon ceci peut causer des problèmes tels que duplications et/ou suppressions intempestives de fichiers.",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Attention, ce chemin est un répertoire parent d'au moins un partage existant (par exemple \"{{otherFolderLabel}}\" ({{otherFolder}})). Si vous continuez, vous devriez créer un nouveau sous-répertoire, sinon ceci peut causer des problèmes tels que duplications et/ou suppressions intempestives de fichiers.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "ATTENTION, ce chemin est un sous-répertoire du partage existant \"{{otherFolder}}\". Ceci peut causer des problèmes tels que duplications et/ou suppressions intempestives de fichiers.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "ATTENTION, ce chemin est un sous-répertoire du partage existant \"{{otherFolderLabel}}\" ({{otherFolder}}). Ceci peut causer des problèmes tels que duplications et/ou suppressions intempestives de fichiers.",
|
||||
"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",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Lorsque vous ajoutez un appareil, gardez à l'esprit que le votre doit aussi être ajouté de l'autre coté.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Lorsqu'un nouveau partage est ajouté, gardez à l'esprit que son ID est utilisée pour lier les répertoires à travers les appareils. L'ID est sensible à la casse et sera forcément la même sur tous les appareils participant à ce partage.",
|
||||
"Yes": "Oui",
|
||||
"You can also select one of these nearby devices:": "You can also select one of these nearby devices:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Vous pouvez changer votre choix dans la boîte de dialogue \"Configuration\".",
|
||||
"You can read more about the two release channels at the link below.": "Vous pouvez en savoir plus sur les deux canaux de distribution via le lien ci-dessous.",
|
||||
"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 must keep at least one version.": "Vous devez garder au minimum une version.",
|
||||
"days": "Jours",
|
||||
"directories": "répertoires",
|
||||
"files": "fichiers",
|
||||
"full documentation": "Documentation complète ici",
|
||||
"items": "éléments",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Outils de débogage",
|
||||
"Default Folder Path": "Chemin parent par défaut pour les nouveaux partages",
|
||||
"Deleted": "Supprimé",
|
||||
"Deselect All": "Tout déselectionner",
|
||||
"Device": "Appareil",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "\"{{name}}\" ({{device}}), appareil actuellement à {{address}}, demande à se connecter.\nAcceptez-vous de l'ajouter à votre liste d'appareils connus ?",
|
||||
"Device ID": "ID de l'appareil",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Plus tard",
|
||||
"Latest Change": "Dernier changement",
|
||||
"Learn more": "En savoir plus",
|
||||
"Limit": "Limite",
|
||||
"Listeners": "Systèmes en écoute",
|
||||
"Loading data...": "Chargement des données...",
|
||||
"Loading...": "Chargement...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Analyse",
|
||||
"See external versioner help for supported templated command line parameters.": "Voir l'aide sur la préservation externe des fichiers pour les paramètres supportés en lignes de commande dans les modèles.",
|
||||
"See external versioning help for supported templated command line parameters.": "Consulter l'aide à la gestion externe des versions pour voir les paramètres de ligne de commande supportés.",
|
||||
"Select All": "Tout sélectionner",
|
||||
"Select a version": "Choisissez une version",
|
||||
"Select latest version": "Restaurer la dernière version",
|
||||
"Select oldest version": "Restaurer la plus ancienne version",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Debug-foarsjennings",
|
||||
"Default Folder Path": "Standert Map-paad",
|
||||
"Deleted": "Fuortsmiten",
|
||||
"Deselect All": "Alles Deselektearje",
|
||||
"Device": "Apparaat",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Apparaat \"{{name}}\" {{device}} op ({{address}}) wol ferbining meitsje. Nij apparaat taheakje?",
|
||||
"Device ID": "Apparaat-ID",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Letter",
|
||||
"Latest Change": "Meast Resinte Feroarings",
|
||||
"Learn more": "Mear witte",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Harkers",
|
||||
"Loading data...": "Data oan it laden...",
|
||||
"Loading...": "Oan it laden...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Oan it skennen",
|
||||
"See external versioner help for supported templated command line parameters.": "Sjoch de eksterne help fan fersjebehearder foar stipe foarbylden fan kommando-rige-parameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "Sjoch de eksterne help fan fersjebehearder foar stipe foarbylden fan kommando-rige-parameters.",
|
||||
"Select All": "Alles Selektearje",
|
||||
"Select a version": "Kies in ferzje",
|
||||
"Select latest version": "Selektearje de nijste ferzje",
|
||||
"Select oldest version": "Selektearje de âldste ferzje",
|
||||
@@ -283,7 +286,7 @@
|
||||
"Statistics": "Statistiken",
|
||||
"Stopped": "Stoppe",
|
||||
"Support": "Help (Forum)",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Support Bundle": "Helpbundel",
|
||||
"Sync Protocol Listen Addresses": "Sync-protokolharkadressen",
|
||||
"Syncing": "Oan it Syncen",
|
||||
"Syncthing has been shut down.": "Syncthing is útsetten",
|
||||
@@ -293,7 +296,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.",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Hibakeresési képességek",
|
||||
"Default Folder Path": "Alapértelmezett mappa útvonala",
|
||||
"Deleted": "Törölve",
|
||||
"Deselect All": "Kijelölés megszüntetése",
|
||||
"Device": "Eszköz",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "\"{{name}}\" eszköz ({{device}} @ {{address}}) szeretne csatlakozni. Hozzáadható az új eszköz?",
|
||||
"Device ID": "Eszközazonosító",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Később",
|
||||
"Latest Change": "Utolsó módosítás",
|
||||
"Learn more": "Tudj meg többet",
|
||||
"Limit": "Sebességkorlát",
|
||||
"Listeners": "Kapcsolatok",
|
||||
"Loading data...": "Adatok betöltése...",
|
||||
"Loading...": "Betöltés...",
|
||||
@@ -193,7 +195,7 @@
|
||||
"Options": "Opciók",
|
||||
"Out of Sync": "Nincs szinkronban",
|
||||
"Out of Sync Items": "Nem szinkronizált elemek",
|
||||
"Outgoing Rate Limit (KiB/s)": "Kimenő sávszélesség (KiB/mp)",
|
||||
"Outgoing Rate Limit (KiB/s)": "Kimenő sebességkorlát (KiB/mp)",
|
||||
"Override Changes": "Változtatások felülbírálása",
|
||||
"Path": "Útvonal",
|
||||
"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": "A mappa elérési útvonala az eszközön. Amennyiben nem létezik, a program automatikusan létrehozza. A hullámvonal (~) a következő helyettesítésre használható: ",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Átnézés",
|
||||
"See external versioner help for supported templated command line parameters.": "A támogatott parancssori paraméter sablonokat a külső verziókezelő súgójában találod.",
|
||||
"See external versioning help for supported templated command line parameters.": "A támogatott parancssori paraméter sablonokat a külső verziókezelő súgójában találod.",
|
||||
"Select All": "Mindent kijelöl",
|
||||
"Select a version": "Válassz egy verziót",
|
||||
"Select latest version": "Legfrissebb verzió kijelölése",
|
||||
"Select oldest version": "Legrégebbi verzió kijelölése",
|
||||
@@ -293,7 +296,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Úgy tűnik, hogy a Syncthing nem működik, vagy valami probléma van a hálózati kapcsolattal. Újra próbálom...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Úgy tűnik, hogy a Syncthing problémába ütközött a kérés feldolgozása során. Ha a probléma továbbra is fennáll, akkor frissíteni kell az oldalt, vagy újra kell indítani a Syncthinget.",
|
||||
"Take me back": "Vissza",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "A grafikus felület címét egy indítási beállítás felülírta. Az itt történő módosítás hatástalan marad, amíg ez a felülírás érvényben van.",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "A grafikus felület címét az indítási beállítások felülírták. Az itt történő módosítások hatástalanok maradnak, amíg a felülírás érvényben van.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "A Syncthing adminisztrációs felületének távoli elérése be van kapcsolva jelszó nélkül.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Az összesített statisztikák elérhetők az alábbi címen.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A beállítások elmentésre kerültek, de nem lettek aktiválva. Újra kell indítani a Syncthing-et az aktiválásukhoz.",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"Add Device": "Aggiungi Dispositivo",
|
||||
"Add Folder": "Aggiungi Cartella",
|
||||
"Add Remote Device": "Aggiungi Dispositivo Remoto",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Aggiungi dispositivi dall'introduttore al nostro elenco, per le cartelle condivise reciprocamente.",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Aggiungi dispositivi dall'introduttore all'elenco dei dispositivi, per cartelle condivise reciprocamente.",
|
||||
"Add new folder?": "Aggiungere una nuova cartella?",
|
||||
"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.": "Inoltre, verrà incrementato l'intervallo di scansione completo (60 volte, vale a dire un nuovo default di 1h). Puoi anche configurarlo manualmente per ogni cartella dopo aver scelto No.",
|
||||
"Address": "Indirizzo",
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Servizi di Debug",
|
||||
"Default Folder Path": "Percorso Cartella di Default",
|
||||
"Deleted": "Cancellato",
|
||||
"Deselect All": "Deseleziona tutto",
|
||||
"Device": "Dispositivo",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Il dispositivo \"{{name}}\" ({{device}} - {{address}}) chiede di connettersi. Aggiungere il nuovo dispositivo?",
|
||||
"Device ID": "ID Dispositivo",
|
||||
@@ -140,7 +141,7 @@
|
||||
"Ignore": "Ignora",
|
||||
"Ignore Patterns": "Schemi Esclusione File",
|
||||
"Ignore Permissions": "Ignora Permessi",
|
||||
"Ignored Devices": "Dispositivi Ignorati",
|
||||
"Ignored Devices": "Dispositivi ignorati",
|
||||
"Ignored Folders": "Cartelle ignorate",
|
||||
"Ignored at": "Ignorato a",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite Velocità in Ingresso (KiB/s)",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Più Tardi",
|
||||
"Latest Change": "Ultima Modifica",
|
||||
"Learn more": "Impara di piu",
|
||||
"Limit": "Limite",
|
||||
"Listeners": "In Ascolto",
|
||||
"Loading data...": "Caricamento dati...",
|
||||
"Loading...": "Caricamento...",
|
||||
@@ -203,7 +205,7 @@
|
||||
"Pause": "Pausa",
|
||||
"Pause All": "Pausa Tutti",
|
||||
"Paused": "In Pausa",
|
||||
"Pending changes": "Modifiche in attessa",
|
||||
"Pending changes": "Modifiche in attesa",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Scansione periodica a intervalli determinati e monitoraggio cambiamenti disabilitata",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Scansione periodica a intervalli determinati e monitoraggio cambiamenti abilitata",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Scansione periodica a intervalli determinati e configurazione fallita del monitoraggio cambiamenti, nuovo tentativo ogni 1m:",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Scansione in corso",
|
||||
"See external versioner help for supported templated command line parameters.": "Consultare la guida al controllo di versione per i modelli dei parametri di riga di comando supportati.",
|
||||
"See external versioning help for supported templated command line parameters.": "Consultare la guida al controllo di versione per i modelli dei parametri di riga di comando supportati.",
|
||||
"Select All": "Seleziona Tutto",
|
||||
"Select a version": "Seleziona una versione",
|
||||
"Select latest version": "Seleziona l'ultima versione",
|
||||
"Select oldest version": "Seleziona la versione più vecchia",
|
||||
@@ -329,7 +332,7 @@
|
||||
"Unavailable": "Non disponibile",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Non disponibile/Disabilitato dall'amministratore o dal manutentore",
|
||||
"Undecided (will prompt)": "Non deciso (verrà richiesto)",
|
||||
"Unignore": "Non ingorare",
|
||||
"Unignore": "Non ignorare",
|
||||
"Unknown": "Sconosciuto",
|
||||
"Unshared": "Non Condiviso",
|
||||
"Unused": "Non Utilizzato",
|
||||
@@ -359,8 +362,8 @@
|
||||
"You can also select one of these nearby devices:": "È anche possibile selezionare uno di questi dispositivi nelle vicinanze:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Puoi sempre cambiare la tua scelta nel dialogo Impostazioni.",
|
||||
"You can read more about the two release channels at the link below.": "Puoi ottenere piu informazioni riguarda i due canali di rilascio nel collegamento sottostante.",
|
||||
"You have no ignored devices.": "Non ignorare i dispositivi.",
|
||||
"You have no ignored folders.": "Non ignorare le cartelle.",
|
||||
"You have no ignored devices.": "Non ci sono dispositivi ignorati.",
|
||||
"You have no ignored folders.": "Non ci sono cartelle ignorate.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Hai modifiche non salvate. Vuoi davvero scartarle?",
|
||||
"You must keep at least one version.": "È necessario mantenere almeno una versione.",
|
||||
"days": "giorni",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
|
||||
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
|
||||
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
|
||||
"Auto Accept": "Auto Accept",
|
||||
"Auto Accept": "自動承諾",
|
||||
"Automatic upgrade now offers the choice between stable releases and release candidates.": "自動アップグレードは、安定版とリリース候補版のいずれかを選べるようになりました。",
|
||||
"Automatic upgrades": "自動アップグレード",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
|
||||
@@ -49,7 +49,7 @@
|
||||
"Configured": "設定値",
|
||||
"Connection Error": "接続エラー",
|
||||
"Connection Type": "接続種別",
|
||||
"Connections": "Connections",
|
||||
"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.",
|
||||
"Copied from elsewhere": "別ファイルからコピー済",
|
||||
"Copied from original": "元ファイルからコピー済",
|
||||
@@ -60,19 +60,20 @@
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
"Default Folder Path": "Default Folder Path",
|
||||
"Deleted": "削除",
|
||||
"Deselect All": "すべて選択解除",
|
||||
"Device": "デバイス",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "デバイス「{{name}}」 ({{address}} の {{device}}) が接続を求めています。新しいデバイスとして追加しますか?",
|
||||
"Device ID": "デバイスID",
|
||||
"Device Identification": "デバイスID",
|
||||
"Device Name": "デバイス名",
|
||||
"Device rate limits": "Device rate limits",
|
||||
"Device rate limits": "デバイス速度制限",
|
||||
"Device that last modified the item": "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",
|
||||
"Discard": "破棄",
|
||||
"Disconnected": "切断中",
|
||||
"Discovered": "探索結果",
|
||||
"Discovery": "探索サーバー",
|
||||
@@ -91,7 +92,7 @@
|
||||
"Editing {%path%}.": "{{path}} を編集中",
|
||||
"Enable NAT traversal": "NATトラバーサルを有効にする",
|
||||
"Enable Relaying": "中継サーバー経由の通信を有効にする",
|
||||
"Enabled": "Enabled",
|
||||
"Enabled": "有効",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "0以上の数 (例: 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アドレス:ポート, tcp://ホスト名:ポート」のようにコンマで区切って入力してください。自動探索を行う場合は「dynamic」と入力してください。",
|
||||
@@ -129,7 +130,7 @@
|
||||
"GUI Listen Address": "GUI待ち受けアドレス",
|
||||
"GUI Listen Addresses": "GUI待ち受けアドレス",
|
||||
"GUI Theme": "GUIテーマ",
|
||||
"General": "General",
|
||||
"General": "一般",
|
||||
"Generate": "生成",
|
||||
"Global Changes": "全変更点",
|
||||
"Global Discovery": "グローバル探索",
|
||||
@@ -140,9 +141,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)": "下り帯域制限 (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "間違った設定を行うと、フォルダーの内容を壊したり、Syncthingが動作しなくなる可能性があります。",
|
||||
"Introduced By": "紹介元",
|
||||
@@ -156,16 +157,17 @@
|
||||
"Later": "後で設定",
|
||||
"Latest Change": "最終変更内容",
|
||||
"Learn more": "詳細を確認する",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "待ち受けポート",
|
||||
"Loading data...": "Loading data...",
|
||||
"Loading...": "Loading...",
|
||||
"Local Discovery": "LAN内で探索",
|
||||
"Local State": "ローカル状態",
|
||||
"Local State (Total)": "ローカル状態 (合計)",
|
||||
"Log": "Log",
|
||||
"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.",
|
||||
"Logs": "Logs",
|
||||
"Logs": "ログ",
|
||||
"Major Upgrade": "メジャーアップグレード",
|
||||
"Mass actions": "Mass actions",
|
||||
"Master": "マスター",
|
||||
@@ -203,7 +205,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:",
|
||||
@@ -219,7 +221,7 @@
|
||||
"RAM Utilization": "メモリ使用量",
|
||||
"Random": "ランダム",
|
||||
"Receive Only": "Receive Only",
|
||||
"Recent Changes": "Recent Changes",
|
||||
"Recent Changes": "最近の変更点",
|
||||
"Reduced by ignore patterns": "無視パターン該当分を除く",
|
||||
"Release Notes": "リリースノート",
|
||||
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "リリース候補版には最新の機能と修正が含まれます。これは従来の隔週リリースに近いものです。",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "スキャン中",
|
||||
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "使用可能なコマンドラインパラメータについてはヘルプの外部バージョン管理の項目を参照してください。",
|
||||
"Select All": "すべて選択",
|
||||
"Select a version": "バージョンを選択してください",
|
||||
"Select latest version": "Select latest version",
|
||||
"Select oldest version": "Select oldest version",
|
||||
@@ -261,7 +264,7 @@
|
||||
"Share With Devices": "共有するデバイス",
|
||||
"Share this folder?": "このフォルダーを共有しますか?",
|
||||
"Shared With": "共有中のデバイス",
|
||||
"Sharing": "Sharing",
|
||||
"Sharing": "共有",
|
||||
"Show ID": "IDを表示",
|
||||
"Show QR": "QRコードを表示",
|
||||
"Show diff with previous version": "前バージョンとの差分を表示",
|
||||
@@ -329,7 +332,7 @@
|
||||
"Unavailable": "Unavailable",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
|
||||
"Undecided (will prompt)": "未決定(再確認する)",
|
||||
"Unignore": "Unignore",
|
||||
"Unignore": "無視を解除",
|
||||
"Unknown": "不明",
|
||||
"Unshared": "非共有",
|
||||
"Unused": "未使用",
|
||||
@@ -352,16 +355,16 @@
|
||||
"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",
|
||||
"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はデバイス間でフォルダーの対応づけに使われることに注意してください。フォルダー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.": "2種類のリリースチャネルについての詳細は、以下のリンク先を参照してください。",
|
||||
"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": "個のディレクトリ",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "디버깅 기능",
|
||||
"Default Folder Path": "기본 폴더 경로",
|
||||
"Deleted": "삭제됨",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "기기",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "다른 기기 {{device}} ({{address}}) 에서 접속을 요청했습니다. 새 장치를 추가하시겠습니까?",
|
||||
"Device ID": "기기 ID",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "나중에",
|
||||
"Latest Change": "최신 변경",
|
||||
"Learn more": "더 알아보기",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "수신자",
|
||||
"Loading data...": "데이터 불러오는중...",
|
||||
"Loading...": "불러오는 중...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "탐색중",
|
||||
"See external versioner help for supported templated command line parameters.": "지원되는 템플릿 명령 행 매개 변수에 대해서는 외부 버전 도움말을 참조하십시오.",
|
||||
"See external versioning help for supported templated command line parameters.": "지원되는 템플릿 명령 행 매개 변수에 대해서는 외부 버전 도움말을 참조하십시오.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "버전 선택",
|
||||
"Select latest version": "가장 최신 버전 선택",
|
||||
"Select oldest version": "가장 오래된 버전 선택",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Derinimo priemonės",
|
||||
"Default Folder Path": "Numatytojo aplanko kelias",
|
||||
"Deleted": "Ištrinta",
|
||||
"Deselect All": "Nuimti žymėjimą nuo visų",
|
||||
"Device": "Įrenginys",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Įrenginys \"{{name}}\" ({{device}} {{address}}) nori prisijungti. Pridėti naują įrenginį?",
|
||||
"Device ID": "Įrenginio ID",
|
||||
@@ -143,7 +144,7 @@
|
||||
"Ignored Devices": "Nepaisomi įrenginiai",
|
||||
"Ignored Folders": "Nepaisomi aplankai",
|
||||
"Ignored at": "Nepaisoma ties",
|
||||
"Incoming Rate Limit (KiB/s)": "Įeinančio srauto maksimalus greitis (KiB/s)",
|
||||
"Incoming Rate Limit (KiB/s)": "Atsiunčiamo srauto maksimalus greitis (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Neteisinga konfigūracija gali pažeisti jūsų aplankų turinį ir padaryti Syncthing neoperuotina.",
|
||||
"Introduced By": "Supažindė",
|
||||
"Introducer": "Supažindintojas",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Vėliau",
|
||||
"Latest Change": "Paskutinis pakeitimas",
|
||||
"Learn more": "Sužinoti daugiau",
|
||||
"Limit": "Apribojimas",
|
||||
"Listeners": "Klausytojai",
|
||||
"Loading data...": "Įkeliami duomenys...",
|
||||
"Loading...": "Įkeliama...",
|
||||
@@ -193,7 +195,7 @@
|
||||
"Options": "Parametrai",
|
||||
"Out of Sync": "Išsisinchronizavę",
|
||||
"Out of Sync Items": "Nesutikrinta",
|
||||
"Outgoing Rate Limit (KiB/s)": "Išeinančio srauto maksimalus greitis (KiB/s)",
|
||||
"Outgoing Rate Limit (KiB/s)": "Išsiunčiamo srauto maksimalus greitis (KiB/s)",
|
||||
"Override Changes": "Perrašyti pakeitimus",
|
||||
"Path": "Kelias",
|
||||
"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": "Kelias iki aplanko šiame kompiuteryje. Bus sukurtas, jei neegzistuoja. Tildės simbolis (~) gali būti naudojamas kaip trumpinys",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Skenuojama",
|
||||
"See external versioner help for supported templated command line parameters.": "Palaikomiems šabloniniams komandų eilutės parametrams, žiūrėkite išorinį versijų valdymo programos žinyną.",
|
||||
"See external versioning help for supported templated command line parameters.": "Palaikomiems šabloniniams komandų eilutės parametrams, žiūrėkite išorinį versijų valdymo žinyną.",
|
||||
"Select All": "Žymėti visus",
|
||||
"Select a version": "Pasirinkti versiją",
|
||||
"Select latest version": "Pasirinkti paskiausią versiją",
|
||||
"Select oldest version": "Pasirinkti seniausią versiją",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Feilrettingsverktøy",
|
||||
"Default Folder Path": "Forvalgt mappeplassering",
|
||||
"Deleted": "Slettet",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Enhet",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) ønsker å koble til. Legge til ny enhet?",
|
||||
"Device ID": "Enhets-ID",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Senere",
|
||||
"Latest Change": "Sist endret",
|
||||
"Learn more": "Lær mer",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Lyttere",
|
||||
"Loading data...": "Laster inn data…",
|
||||
"Loading...": "Laster…",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Gjennomsøker",
|
||||
"See external versioner help for supported templated command line parameters.": "Se ekstern versjoneringshjelp for støttede mal-baserte kommandolinjeparameter.",
|
||||
"See external versioning help for supported templated command line parameters.": "Se ekstern versjoneringshjelp for støttede mal-baserte kommandolinjeparameter.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Velg en versjon",
|
||||
"Select latest version": "Velg siste versjon",
|
||||
"Select oldest version": "Velg eldste versjon",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Debugmogelijkheden",
|
||||
"Default Folder Path": "Standaardmaplocatie",
|
||||
"Deleted": "Verwijderd",
|
||||
"Deselect All": "Alles deselecteren",
|
||||
"Device": "Apparaat",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Apparaat \"{{name}}\" ({{device}} op {{address}}) wil verbinden. Nieuw apparaat toevoegen?",
|
||||
"Device ID": "Apparaat-ID",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Later",
|
||||
"Latest Change": "Laatste wijziging",
|
||||
"Learn more": "Lees meer",
|
||||
"Limit": "Begrenzing",
|
||||
"Listeners": "Luisteraars",
|
||||
"Loading data...": "Gegevens laden...",
|
||||
"Loading...": "Laden...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Scannen",
|
||||
"See external versioner help for supported templated command line parameters.": "Bekijk de documentatie van de externe versiebeheerder voor ondersteunde sjabloon-opdrachtregelparameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "Bekijk de documentatie van extern versiebeheer voor ondersteunde sjabloon-opdrachtregelparameters.",
|
||||
"Select All": "Alles selecteren",
|
||||
"Select a version": "Selecteer een versie",
|
||||
"Select latest version": "Laatste versie selecteren",
|
||||
"Select oldest version": "Oudste versie selecteren",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Odpluskwianie",
|
||||
"Default Folder Path": "Domyślna ścieżka folderu",
|
||||
"Deleted": "Usunięto",
|
||||
"Deselect All": "Odznacz wszystko",
|
||||
"Device": "Urządzenie",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Urządzenie \"{{name}}\" {{device}} ({{address}}) chce się połączyć. Dodać nowe urządzenie?",
|
||||
"Device ID": "ID urządzenia",
|
||||
@@ -142,7 +143,7 @@
|
||||
"Ignore Permissions": "Ignoruj uprawnienia",
|
||||
"Ignored Devices": "Ignorowane urządzenia",
|
||||
"Ignored Folders": "Ignorowane katalogi",
|
||||
"Ignored at": "Ignored at",
|
||||
"Ignored at": "Ignorowane od",
|
||||
"Incoming Rate Limit (KiB/s)": "Ograniczenie prędkości odbierania (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Niepoprawna konfiguracja może uszkodzić zawartośc Twojego folderu i uczynić Syncthing niedziałającym.",
|
||||
"Introduced By": "Wprowadzony przez",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Później",
|
||||
"Latest Change": "Ostatnia zmiana",
|
||||
"Learn more": "Zobacz więcej",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Nasłuchujący",
|
||||
"Loading data...": "Ładowanie danych...",
|
||||
"Loading...": "Ładowanie...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Skanowanie",
|
||||
"See external versioner help for supported templated command line parameters.": "Dostępne zmienne dla polecenia opisane są w dokumentacji w sekcji Zewnętrzne wersjonowanie plików.",
|
||||
"See external versioning help for supported templated command line parameters.": "Dostępne zmienne dla polecenia opisane są w dokumentacji w sekcji Zewnętrzne wersjonowanie plików.",
|
||||
"Select All": "Zaznacz wszystko",
|
||||
"Select a version": "Wybierz wersję",
|
||||
"Select latest version": "Wybierz najnowszą wersję",
|
||||
"Select oldest version": "Wybierz najstarszą wersję",
|
||||
@@ -283,7 +286,7 @@
|
||||
"Statistics": "Statystyki",
|
||||
"Stopped": "Zatrzymany",
|
||||
"Support": "Wsparcie",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Support Bundle": "Wsparcie",
|
||||
"Sync Protocol Listen Addresses": "Adres nasłuchu protokołu synchronizacji",
|
||||
"Syncing": "Synchronizowanie",
|
||||
"Syncthing has been shut down.": "Syncthing został wyłączony",
|
||||
@@ -292,8 +295,8 @@
|
||||
"Syncthing is upgrading.": "Aktualizowanie Syncthing",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing wydaje się być wyłączony lub jest problem z twoim połączeniem internetowym. Próbuje ponownie...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing nie może przetworzyć twojego zapytania. Proszę przeładuj stronę lub zrestartuj Syncthing, jeśli problem pozostanie.",
|
||||
"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": "Zabierz mnie z powrotem",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Adres GUI jest nadpisywany przez opcje uruchamiania. Zmiany tutaj nie będą obowiązywać, dopóki ta opcja uruchamiania jest używana.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Interfejs administracyjny Syncthing jest skonfigurowany w sposób pozwalający na zdalny dostęp bez hasła.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Zebrane statystyki są publicznie dostępne pod poniższym linkiem.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfiguracja została zapisana lecz nie jest aktywna. Syncthing musi zostać zrestartowany aby aktywować nową konfiguracje.",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Facilidades de depuração",
|
||||
"Default Folder Path": "Caminho padrão da pasta",
|
||||
"Deleted": "Apagado",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Dispositivo",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Dispositivo \"{{name}}\" ({{device}} em {{address}}) quer se conectar. Adicionar novo dispositivo?",
|
||||
"Device ID": "ID do dispositivo",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Depois",
|
||||
"Latest Change": "Última mudança",
|
||||
"Learn more": "Saiba mais",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Escutadores",
|
||||
"Loading data...": "Carregando dados...",
|
||||
"Loading...": "Carregando",
|
||||
@@ -201,7 +203,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",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Verificando",
|
||||
"See external versioner help for supported templated command line parameters.": "Consulte a ajuda sobre versionamento externo para modelos de parâmetros de linha de comando aceitos.",
|
||||
"See external versioning help for supported templated command line parameters.": "Consulte a ajuda sobre versionamento externo para modelos de parâmetros de linha de comando aceitos.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Selecione uma versão",
|
||||
"Select latest version": "Escolher a última versão",
|
||||
"Select oldest version": "Escolher a versão mais antiga",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Recursos de depuração",
|
||||
"Default Folder Path": "Caminho da pasta predefinida",
|
||||
"Deleted": "Eliminado",
|
||||
"Deselect All": "Desseleccionar tudo",
|
||||
"Device": "Dispositivo",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "O dispositivo \"{{name}}\" ({{device}} em {{address}}) quer conectar-se. Adiciono este novo dispositivo?",
|
||||
"Device ID": "ID do dispositivo",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Mais tarde",
|
||||
"Latest Change": "Última alteração",
|
||||
"Learn more": "Saiba mais",
|
||||
"Limit": "Limite",
|
||||
"Listeners": "Auscultadores",
|
||||
"Loading data...": "Carregando dados...",
|
||||
"Loading...": "Carregando...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Verificando",
|
||||
"See external versioner help for supported templated command line parameters.": "Veja a ajuda do gestor de versões externo para saber que parâmetros da linha de comandos são suportados.",
|
||||
"See external versioning help for supported templated command line parameters.": "Veja a ajuda externa sobre gestão de versões para ver os modelos suportados de parâmetros para a linha de comandos.",
|
||||
"Select All": "Seleccionar tudo",
|
||||
"Select a version": "Seleccione uma versão",
|
||||
"Select latest version": "Seleccionar a última versão",
|
||||
"Select oldest version": "Seleccionar a versão mais antiga",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Средства отладки",
|
||||
"Default Folder Path": "Путь для папок",
|
||||
"Deleted": "Удалено",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Устройство",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство «{{name}}» ({{device}} на {{address}}) хочет подключиться. Добавить новое устройство?",
|
||||
"Device ID": "ID устройства",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Позже",
|
||||
"Latest Change": "Последнее изменение",
|
||||
"Learn more": "Узнать больше",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Прослушиватель",
|
||||
"Loading data...": "Загрузка данных...",
|
||||
"Loading...": "Загрузка...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Сканирование",
|
||||
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Выберите версию",
|
||||
"Select latest version": "Выбрать последнюю версию",
|
||||
"Select oldest version": "Выбрать самую старую версию",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
"Default Folder Path": "Predvolená adresárová cesta",
|
||||
"Deleted": "Zmazané",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Zariadenie",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Zariadenie \"{{name}}\" ({{device}} na {{address}}) sa chce pripojiť. Pridať nové zariadenie?",
|
||||
"Device ID": "ID zariadenia",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Neskôr",
|
||||
"Latest Change": "Posledná zmena",
|
||||
"Learn more": "Zisti viac",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Načúvajúci",
|
||||
"Loading data...": "Načítavanie údajov...",
|
||||
"Loading...": "Načítavanie...",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "Skenovanie",
|
||||
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Zvoliť verziu",
|
||||
"Select latest version": "Zvoliť najnovšiu verziu",
|
||||
"Select oldest version": "Zvoliť najstaršiu verziu",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"Add Remote Device": "Lägg till fjärrenhet",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Lägg enheter från introduktören till vår enhetslista för ömsesidigt delade mappar.",
|
||||
"Add new folder?": "Lägg till ny mapp?",
|
||||
"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.": "Dessutom kommer det fullständiga omskanningsintervallet att höjas (60 gånger, d.v.s. ny standard på 1h). Du kan också konfigurera det manuellt för varje mapp senare efter att du valt Nej.",
|
||||
"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.": "Dessutom kommer det fullständiga återkommande genomsöksintervallet att höjas (60 gånger, d.v.s. ny standard på 1h). Du kan också konfigurera det manuellt för varje mapp senare efter att du valt Nej.",
|
||||
"Address": "Adress",
|
||||
"Addresses": "Adresser",
|
||||
"Advanced": "Avancerat",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Connection Error": "Anslutningsproblem",
|
||||
"Connection Type": "Anslutningstyp",
|
||||
"Connections": "Anslutningar",
|
||||
"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.": "Kontinuerligt utkik efter ändringar är nu tillgängligt för Syncthing. Detta kommer att upptäcka ändringar på disken och utfärda en skanning på endast de modifierade sökvägarna. Fördelarna är att förändringar sprids snabbare och att mindre fullständiga skanningar krävs.",
|
||||
"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.": "Kontinuerligt utkik efter ändringar är nu tillgängligt för Syncthing. Detta kommer att upptäcka ändringar på disken och utfärda en genomsökning på endast de ändrade sökvägarna. Fördelarna är att förändringar sprids snabbare och att mindre fullständiga genomsökning krävs.",
|
||||
"Copied from elsewhere": "Kopierat från annanstans",
|
||||
"Copied from original": "Kopierat från original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragare:",
|
||||
@@ -60,18 +60,19 @@
|
||||
"Debugging Facilities": "Felsökningsanläggningar",
|
||||
"Default Folder Path": "Standard mappsökväg",
|
||||
"Deleted": "Tog bort",
|
||||
"Deselect All": "Avmarkera alla",
|
||||
"Device": "Enhet",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
"Device ID": "Enhet-ID",
|
||||
"Device ID": "Enhets-ID",
|
||||
"Device Identification": "Enhetens identifikation",
|
||||
"Device Name": "Enhetsnamn",
|
||||
"Device rate limits": "Enhetshastighetsgränser",
|
||||
"Device that last modified the item": "Enhet som senast ändrade objektet",
|
||||
"Devices": "Enheter",
|
||||
"Disabled": "Inaktiverad",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Inaktiverad periodisk skanning och inaktiverad visning av ändringar",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Inaktiverad periodisk skanning och aktiverad visning av ändringar",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Inaktiverad periodisk skanning och misslyckad att ställa in visning av ändringar, försök igen varje 1m:",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Inaktiverad periodisk genomsökning och inaktiverad spaning efter ändringar",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Inaktiverad periodisk genomsökning och aktiverad spaning efter ändringar",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Inaktiverad periodisk genomsökning och misslyckad att ställa in spaning efter ändringar, försök igen varje 1m:",
|
||||
"Discard": "Kassera",
|
||||
"Disconnected": "Frånkopplad",
|
||||
"Discovered": "Upptäckt",
|
||||
@@ -122,7 +123,7 @@
|
||||
"Folder Type": "Mapptyp",
|
||||
"Folders": "Mappar",
|
||||
"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.": "För följande mappar uppstod ett fel när du började bevaka ändringar. Det kommer att omförsökas varje minut, så felen kan försvinna snart. Om de fortsätter, försök att åtgärda det underliggande problemet och fråga om hjälp om du inte kan.",
|
||||
"Full Rescan Interval (s)": "Fullständig omskanningsintervall(er)",
|
||||
"Full Rescan Interval (s)": "Fullständig(a) återkommande genomsökningsintervall(er)",
|
||||
"GUI": "Grafiskt gränssnitt",
|
||||
"GUI Authentication Password": "Gränssnittets autentiseringslösenord",
|
||||
"GUI Authentication User": "Gränssnittets autentiseringsanvändare",
|
||||
@@ -151,11 +152,12 @@
|
||||
"Keep Versions": "Behåll versioner",
|
||||
"Largest First": "Största först",
|
||||
"Last File Received": "Senaste fil mottagen",
|
||||
"Last Scan": "Senaste skanning",
|
||||
"Last Scan": "Senaste genomsökning",
|
||||
"Last seen": "Senast sedd",
|
||||
"Later": "Senare",
|
||||
"Latest Change": "Senaste ändring",
|
||||
"Learn more": "Ta reda på mer",
|
||||
"Limit": "Gräns",
|
||||
"Listeners": "Lyssnare",
|
||||
"Loading data...": "Laddar data...",
|
||||
"Loading...": "Laddar...",
|
||||
@@ -204,9 +206,9 @@
|
||||
"Pause All": "Pausa alla",
|
||||
"Paused": "Pausad",
|
||||
"Pending changes": "Väntar på ändringar",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodisk skanning i givet intervall och inaktiverad visning av ändringar",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodisk skanning i givet intervall och aktiverad visning av ändringar",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning i givet intervall och misslyckades med att ställa in visning av ändringar, försök igen varje 1m:",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodisk genomsökning i givet intervall och inaktiverad spaning efter ändringar",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodisk genomsökning i givet intervall och aktiverad spaning efter ändringar",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk genomsökning i givet intervall och misslyckades med att ställa in spaning efter ändringar, försök igen var 1m:",
|
||||
"Permissions": "Behörigheter",
|
||||
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiska gränssnittets användarautentisering och lösenord i inställningsdialogrutan.",
|
||||
@@ -228,10 +230,10 @@
|
||||
"Remove Device": "Ta bort enhet",
|
||||
"Remove Folder": "Ta bort mapp",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Krävs identifierare för mappen. Måste vara densamma på alla kluster enheter.",
|
||||
"Rescan": "Skanna om",
|
||||
"Rescan All": "Skanna om alla",
|
||||
"Rescan Interval": "Återskanningsintervall",
|
||||
"Rescans": "Omskanningar",
|
||||
"Rescan": "Genomsök igen",
|
||||
"Rescan All": "Genomsök alla igen",
|
||||
"Rescan Interval": "Återkommande genomsökningsintervall",
|
||||
"Rescans": "Återkommande genomsökningar",
|
||||
"Restart": "Starta om",
|
||||
"Restart Needed": "Omstart behövs",
|
||||
"Restarting": "Startar om",
|
||||
@@ -243,10 +245,11 @@
|
||||
"Revert Local Changes": "Återställ lokala ändringar",
|
||||
"Running": "Körs",
|
||||
"Save": "Spara",
|
||||
"Scan Time Remaining": "Återstående skanningstid",
|
||||
"Scanning": "Skannar",
|
||||
"Scan Time Remaining": "Återstående genomsökningstid",
|
||||
"Scanning": "Genomsöker",
|
||||
"See external versioner help for supported templated command line parameters.": "Se hjälp för extern version för stödda mallade kommandoradsparametrar.",
|
||||
"See external versioning help for supported templated command line parameters.": "Se hjälp för extern version för stödda mallade kommandoradsparametrar.",
|
||||
"Select All": "Markera alla",
|
||||
"Select a version": "Välj en version",
|
||||
"Select latest version": "Välj senaste versionen",
|
||||
"Select oldest version": "Välj äldsta versionen",
|
||||
@@ -293,7 +296,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 Всі права застережено, вклад внесли:",
|
||||
@@ -60,26 +60,27 @@
|
||||
"Debugging Facilities": "Засоби відладки",
|
||||
"Default Folder Path": "Шлях до директорії по замовчанню",
|
||||
"Deleted": "Видалене",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "Пристрій",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Пристрій \"{{name}}\" ({{device}} за адресою {{address}}) намагається під’єднатися. Додати новий пристрій?",
|
||||
"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 +111,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 +122,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 +141,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": "Введено",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "Пізніше",
|
||||
"Latest Change": "Найостанніша зміна",
|
||||
"Learn more": "Дізнатися більше",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "Приймачі (TCP & Relay)",
|
||||
"Loading data...": "Дані завантажуються...",
|
||||
"Loading...": "Завантаження...",
|
||||
@@ -164,7 +166,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 +205,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 +220,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 +233,7 @@
|
||||
"Rescan": "Пересканувати",
|
||||
"Rescan All": "Пересканувати усе",
|
||||
"Rescan Interval": "Інтервал для повторного сканування",
|
||||
"Rescans": "Rescans",
|
||||
"Rescans": "Пересканування",
|
||||
"Restart": "Перезапуск",
|
||||
"Restart Needed": "Необхідний перезапуск",
|
||||
"Restarting": "Відбувається перезапуск",
|
||||
@@ -240,13 +242,14 @@
|
||||
"Resume": "Продовжити",
|
||||
"Resume All": "Продовжити всі",
|
||||
"Reused": "Використано вдруге",
|
||||
"Revert Local Changes": "Revert Local Changes",
|
||||
"Revert Local Changes": "Інвертувати локальні зміни",
|
||||
"Running": "Running",
|
||||
"Save": "Зберегти",
|
||||
"Scan Time Remaining": "Час до кінця сканування",
|
||||
"Scanning": "Сканування",
|
||||
"See external versioner help for supported templated command line parameters.": "Переглянути допомогу по зовнішньому версіонуванню для підтримуваних шаблонних параметрів командного рядка.",
|
||||
"See external versioning help for supported templated command line parameters.": "Переглянути допомогу по зовнішньому версіонуванню для підтримуваних шаблонних параметрів командного рядка.",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "Обрати версію",
|
||||
"Select latest version": "Обрати найновішу версію",
|
||||
"Select oldest version": "Обрати найстарішу версію",
|
||||
@@ -351,17 +354,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": "директорії",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"Debugging Facilities": "调试功能",
|
||||
"Default Folder Path": "默认文件夹路径",
|
||||
"Deleted": "已删除",
|
||||
"Deselect All": "取消全选",
|
||||
"Device": "设备",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "设备 \"{{name}}\"(位于 {{address}} 的 {{device}})请求连接。是否添加新设备?",
|
||||
"Device ID": "设备 ID",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "稍后",
|
||||
"Latest Change": "最后更改",
|
||||
"Learn more": "了解更多",
|
||||
"Limit": "限制",
|
||||
"Listeners": "侦听程序",
|
||||
"Loading data...": "正在载入数据…",
|
||||
"Loading...": "正在载入…",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "扫描中",
|
||||
"See external versioner help for supported templated command line parameters.": "有关支持的命令行参数模板,请参阅外部的版本控制器帮助。",
|
||||
"See external versioning help for supported templated command line parameters.": "有关受支持的模板命令行参数,请参阅外部版本控制帮助。",
|
||||
"Select All": "全选",
|
||||
"Select a version": "选择版本",
|
||||
"Select latest version": "选择最新的版本",
|
||||
"Select oldest version": "选择最旧的版本",
|
||||
|
||||
@@ -60,12 +60,13 @@
|
||||
"Debugging Facilities": "除錯工具",
|
||||
"Default Folder Path": "預設資料夾路徑",
|
||||
"Deleted": "已刪除",
|
||||
"Deselect All": "Deselect All",
|
||||
"Device": "裝置",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "裝置 \"{{name}}\" ({{device}} 位於 {{address}}) 想要連線。要增加新裝置嗎?",
|
||||
"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 +113,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": "資料夾",
|
||||
@@ -156,6 +157,7 @@
|
||||
"Later": "稍後",
|
||||
"Latest Change": "最近變動",
|
||||
"Learn more": "瞭解更多",
|
||||
"Limit": "Limit",
|
||||
"Listeners": "監聽者",
|
||||
"Loading data...": "正在載入資料...",
|
||||
"Loading...": "正在載入...",
|
||||
@@ -218,7 +220,7 @@
|
||||
"Quick guide to supported patterns": "可支援樣式的快速指南",
|
||||
"RAM Utilization": "記憶體使用量",
|
||||
"Random": "隨機",
|
||||
"Receive Only": "Receive Only",
|
||||
"Receive Only": "僅接收\n",
|
||||
"Recent Changes": "最近變動",
|
||||
"Reduced by ignore patterns": "已由忽略樣式縮減",
|
||||
"Release Notes": "版本資訊",
|
||||
@@ -247,6 +249,7 @@
|
||||
"Scanning": "正在掃描",
|
||||
"See external versioner help for supported templated command line parameters.": "關於命令列模板參數請參閱外部版本管理說明。",
|
||||
"See external versioning help for supported templated command line parameters.": "查看關於命令列模板參數請參閱外部版本管理說明。",
|
||||
"Select All": "Select All",
|
||||
"Select a version": "選擇一個版本",
|
||||
"Select latest version": "選擇最新的版本",
|
||||
"Select oldest version": "選擇最舊的版本",
|
||||
@@ -359,8 +362,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": "日",
|
||||
|
||||
@@ -1 +1 @@
|
||||
var langPrettyprint = {"bg":"Bulgarian","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","eu":"Basque","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sk":"Slovak","sv":"Swedish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
|
||||
var langPrettyprint = {"bg":"Bulgarian","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","eu":"Basque","fi":"Finnish","fr":"French","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sk":"Slovak","sv":"Swedish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
var validLangs = ["bg","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","eu","fi","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","pl","pt-BR","pt-PT","ru","sk","sv","uk","zh-CN","zh-TW"]
|
||||
var validLangs = ["bg","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","eu","fi","fr","fy","hu","it","ja","ko-KR","lt","nb","nl","pl","pt-BR","pt-PT","ru","sk","sv","uk","zh-CN","zh-TW"]
|
||||
|
||||
@@ -313,6 +313,7 @@
|
||||
<span ng-switch-when="paused"><span class="hidden-xs" translate>Paused</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="unknown"><span class="hidden-xs" translate>Unknown</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="unshared"><span class="hidden-xs" translate>Unshared</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="scan-waiting"><span class="hidden-xs" translate>Waiting to scan</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="stopped"><span class="hidden-xs" translate>Stopped</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="scanning">
|
||||
<span class="hidden-xs" translate>Scanning</span>
|
||||
@@ -372,10 +373,10 @@
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="neededItems(folder.id) > 0">
|
||||
<tr ng-if="model[folder.id].needTotalItems > 0">
|
||||
<th><span class="fas fa-fw fa-cloud-download-alt"></span> <span translate>Out of Sync Items</span></th>
|
||||
<td class="text-right">
|
||||
<a href="" ng-click="showNeed(folder.id)">{{neededItems(folder.id) | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].needBytes | binary}}B</a>
|
||||
<a href="" ng-click="showNeed(folder.id)">{{model[folder.id].needTotalItems | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].needBytes | binary}}B</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="folderStatus(folder) === 'scanning' && scanRate(folder.id) > 0">
|
||||
@@ -391,6 +392,12 @@
|
||||
<a href="" ng-click="showFailed(folder.id)">{{model[folder.id].pullErrors | alwaysNumber | localeNumber}} <span translate>items</span></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="folder.type == 'receiveonly' && canRevert(folder.id)">
|
||||
<th><span class="fas fa-fw fa-exclamation-circle"></span> <span translate>Locally Changed Items</span></th>
|
||||
<td class="text-right">
|
||||
<a href="" ng-click="showLocalChanged(folder.id)">{{model[folder.id].receiveOnlyTotalItems | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].receiveOnlyBytes | binary}}B</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="folder.type != 'sendreceive'">
|
||||
<th><span class="fas fa-fw fa-folder"></span> <span translate>Folder Type</span></th>
|
||||
<td class="text-right">
|
||||
@@ -551,7 +558,13 @@
|
||||
<a href="#" class="toggler" ng-click="toggleUnits()">
|
||||
<span ng-if="!metricRates">{{connectionsTotal.inbps | binary}}B/s</span>
|
||||
<span ng-if="metricRates">{{connectionsTotal.inbps*8 | metric}}bps</span>
|
||||
({{connectionsTotal.inBytesTotal | binary}}B)</span>
|
||||
({{connectionsTotal.inBytesTotal | binary}}B)
|
||||
<small ng-if="config.options.maxRecvKbps > 0"><br/>
|
||||
<i class="text-muted"><span translate>Limit</span>:
|
||||
<span ng-if="!metricRates">{{config.options.maxRecvKbps*1024 | binary}}B/s</span>
|
||||
<span ng-if="metricRates">{{config.options.maxRecvKbps*1024*8 | metric}}bps</span>
|
||||
</i>
|
||||
</small>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -562,6 +575,12 @@
|
||||
<span ng-if="!metricRates">{{connectionsTotal.outbps | binary}}B/s</span>
|
||||
<span ng-if="metricRates">{{connectionsTotal.outbps*8 | metric}}bps</span>
|
||||
({{connectionsTotal.outBytesTotal | binary}}B)
|
||||
<small ng-if="config.options.maxSendKbps > 0"><br/>
|
||||
<i class="text-muted"><span translate>Limit</span>:
|
||||
<span ng-if="!metricRates">{{config.options.maxSendKbps*1024 | binary}}B/s</span>
|
||||
<span ng-if="metricRates">{{config.options.maxSendKbps*1024*8 | metric}}bps</span>
|
||||
</i>
|
||||
</small>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -569,9 +588,10 @@
|
||||
<th><span class="fas fa-fw fa-home"></span> <span translate>Local State (Total)</span></th>
|
||||
<td class="text-right">
|
||||
<span tooltip data-original-title="{{localStateTotal.files | alwaysNumber | localeNumber}} {{'files' | translate}}, {{ localStateTotal.directories | alwaysNumber | localeNumber}} {{'directories' | translate}}, ~{{ localStateTotal.bytes | binary}}B">
|
||||
<span class="far fa-copy"></span> {{localStateTotal.files | alwaysNumber | localeNumber}} 
|
||||
<span class="far fa-folder"></span> {{localStateTotal.directories| alwaysNumber | localeNumber}} 
|
||||
<span class="far fa-hdd"></span> ~{{localStateTotal.bytes | binary}}B
|
||||
<span class="far fa-copy"></span> {{localStateTotal.files | alwaysNumber | localeNumber}} 
|
||||
<span class="far fa-folder"></span> {{localStateTotal.directories| alwaysNumber | localeNumber}} 
|
||||
<span class="far fa-hdd"></span> ~{{localStateTotal.bytes | binary}}B
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -654,7 +674,13 @@
|
||||
<a href="#" class="toggler" ng-click="toggleUnits()">
|
||||
<span ng-if="!metricRates">{{connections[deviceCfg.deviceID].inbps | binary}}B/s</span>
|
||||
<span ng-if="metricRates">{{connections[deviceCfg.deviceID].inbps*8 | metric}}bps</span>
|
||||
({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)</span>
|
||||
({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)
|
||||
<small ng-if="deviceCfg.maxRecvKbps > 0"><br/>
|
||||
<i class="text-muted"><span translate>Limit</span>:
|
||||
<span ng-if="!metricRates">{{deviceCfg.maxRecvKbps*1024 | binary}}B/s</span>
|
||||
<span ng-if="metricRates">{{deviceCfg.maxRecvKbps*1024*8 | metric}}bps</span>
|
||||
</i>
|
||||
</small>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -664,7 +690,13 @@
|
||||
<a href="#" class="toggler" ng-click="toggleUnits()">
|
||||
<span ng-if="!metricRates">{{connections[deviceCfg.deviceID].outbps | binary}}B/s</span>
|
||||
<span ng-if="metricRates">{{connections[deviceCfg.deviceID].outbps*8 | metric}}bps</span>
|
||||
({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</span>
|
||||
({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)
|
||||
<small ng-if="deviceCfg.maxSendKbps > 0"><br/>
|
||||
<i class="text-muted"><span translate>Limit</span>:
|
||||
<span ng-if="!metricRates">{{deviceCfg.maxSendKbps*1024 | binary}}B/s</span>
|
||||
<span ng-if="metricRates">{{deviceCfg.maxSendKbps*1024*8 | metric}}bps</span>
|
||||
</i>
|
||||
</small>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -796,6 +828,7 @@
|
||||
<ng-include src="'syncthing/transfer/neededFilesModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/transfer/failedFilesModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/transfer/remoteNeededFilesModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/transfer/localChangedFilesModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/core/majorUpgradeModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/core/aboutModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/core/discoveryFailuresModalView.html'"></ng-include>
|
||||
|
||||
@@ -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 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, 佛跳墙
|
||||
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, Cromefire_, 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, Jörg Thalheim, 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, desbma, janost, jaseg, klemens, marco-m, perewa, rubenbe, wangguoliang, xjtdy888, 佛跳墙
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
@@ -46,6 +46,7 @@ angular.module('syncthing.core')
|
||||
$scope.neededCurrentPage = 1;
|
||||
$scope.neededPageSize = 10;
|
||||
$scope.failed = {};
|
||||
$scope.localChanged = {};
|
||||
$scope.scanProgress = {};
|
||||
$scope.themes = [];
|
||||
$scope.globalChangeEvents = {};
|
||||
@@ -672,6 +673,15 @@ angular.module('syncthing.core')
|
||||
});
|
||||
};
|
||||
|
||||
$scope.refreshLocalChanged = function (page, perpage) {
|
||||
var url = urlbase + '/db/localchanged?folder=';
|
||||
url += encodeURIComponent($scope.localChanged.folder);
|
||||
url += "&page=" + page + "&perpage=" + perpage;
|
||||
$http.get(url).success(function (data) {
|
||||
$scope.localChanged = data;
|
||||
}).error($scope.emitHTTPError);
|
||||
};
|
||||
|
||||
var refreshDeviceStats = debounce(function () {
|
||||
$http.get(urlbase + "/stats/device").success(function (data) {
|
||||
$scope.deviceStats = data;
|
||||
@@ -737,7 +747,7 @@ angular.module('syncthing.core')
|
||||
if (state === 'error') {
|
||||
return 'stopped'; // legacy, the state is called "stopped" in the GUI
|
||||
}
|
||||
if (state === 'idle' && $scope.neededItems(folderCfg.id) > 0) {
|
||||
if (state === 'idle' && $scope.model[folderCfg.id].needTotalItems > 0) {
|
||||
return 'outofsync';
|
||||
}
|
||||
if (state === 'scanning') {
|
||||
@@ -769,22 +779,13 @@ angular.module('syncthing.core')
|
||||
if (status === 'stopped' || status === 'outofsync' || status === 'error') {
|
||||
return 'danger';
|
||||
}
|
||||
if (status === 'unshared') {
|
||||
if (status === 'unshared' || status === 'scan-waiting') {
|
||||
return 'warning';
|
||||
}
|
||||
|
||||
return 'info';
|
||||
};
|
||||
|
||||
$scope.neededItems = function (folderID) {
|
||||
if (!$scope.model[folderID]) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return $scope.model[folderID].needFiles + $scope.model[folderID].needDirectories +
|
||||
$scope.model[folderID].needSymlinks + $scope.model[folderID].needDeletes;
|
||||
};
|
||||
|
||||
$scope.syncPercentage = function (folder) {
|
||||
if (typeof $scope.model[folder] === 'undefined') {
|
||||
return 100;
|
||||
@@ -1371,6 +1372,18 @@ angular.module('syncthing.core')
|
||||
$('#editDevice').modal();
|
||||
};
|
||||
|
||||
$scope.selectAllFolders = function () {
|
||||
angular.forEach($scope.folders, function (id) {
|
||||
$scope.currentDevice.selectedFolders[id] = true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deSelectAllFolders = function () {
|
||||
angular.forEach($scope.folders, function (id) {
|
||||
$scope.currentDevice.selectedFolders[id] = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addDevice = function (deviceID, name) {
|
||||
return $http.get(urlbase + '/system/discovery')
|
||||
.success(function (registry) {
|
||||
@@ -1694,6 +1707,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;
|
||||
@@ -1968,114 +1995,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2166,6 +2195,14 @@ angular.module('syncthing.core')
|
||||
$http.post(urlbase + "/db/override?folder=" + encodeURIComponent(folder));
|
||||
};
|
||||
|
||||
$scope.showLocalChanged = function (folder) {
|
||||
$scope.localChanged.folder = folder;
|
||||
$scope.localChanged = $scope.refreshLocalChanged(1, 10);
|
||||
$('#localChanged').modal().one('hidden.bs.modal', function () {
|
||||
$scope.localChanged = {};
|
||||
});
|
||||
};
|
||||
|
||||
$scope.revert = function (folder) {
|
||||
$http.post(urlbase + "/db/revert?folder=" + encodeURIComponent(folder));
|
||||
};
|
||||
@@ -2175,11 +2212,7 @@ angular.module('syncthing.core')
|
||||
if (!f) {
|
||||
return false;
|
||||
}
|
||||
return f.receiveOnlyChangedBytes > 0 ||
|
||||
f.receiveOnlyChangedDeletes > 0 ||
|
||||
f.receiveOnlyChangedDirectories > 0 ||
|
||||
f.receiveOnlyChangedFiles > 0 ||
|
||||
f.receiveOnlyChangedSymlinks > 0;
|
||||
return $scope.model[folder].receiveOnlyTotalItems > 0;
|
||||
};
|
||||
|
||||
$scope.advanced = function () {
|
||||
|
||||
@@ -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">
|
||||
@@ -108,17 +112,6 @@
|
||||
<div class="col-md-12">
|
||||
<label translate>Device rate limits</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div ng-class="{'has-error': deviceEditor.maxSendKbps.$invalid && deviceEditor.maxSendKbps.$dirty}">
|
||||
<div class="row">
|
||||
<span class="col-md-8" translate>Outgoing Rate Limit (KiB/s)</span>
|
||||
<div class="col-md-4">
|
||||
<input name="maxSendKbps" id="maxSendKbps" class="form-control" type="number" pattern="\d+" ng-model="currentDevice.maxSendKbps" min="0"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block" ng-if="!deviceEditor.maxSendKbps.$valid && deviceEditor.maxSendKbps.$dirty" translate>The rate limit must be a non-negative number (0: no limit)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6" ng-class="{'has-error': deviceEditor.maxRecvKbps.$invalid && deviceEditor.maxRecvKbps.$dirty}">
|
||||
<div class="row">
|
||||
<span class="col-md-8" translate>Incoming Rate Limit (KiB/s)</span>
|
||||
@@ -128,6 +121,15 @@
|
||||
</div>
|
||||
<p class="help-block" ng-if="!deviceEditor.maxRecvKbps.$valid && deviceEditor.maxRecvKbps.$dirty" translate>The rate limit must be a non-negative number (0: no limit)</p>
|
||||
</div>
|
||||
<div class="col-md-6" ng-class="{'has-error': deviceEditor.maxSendKbps.$invalid && deviceEditor.maxSendKbps.$dirty}">
|
||||
<div class="row">
|
||||
<span class="col-md-8" translate>Outgoing Rate Limit (KiB/s)</span>
|
||||
<div class="col-md-4">
|
||||
<input name="maxSendKbps" id="maxSendKbps" class="form-control" type="number" pattern="\d+" ng-model="currentDevice.maxSendKbps" min="0"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block" ng-if="!deviceEditor.maxSendKbps.$valid && deviceEditor.maxSendKbps.$dirty" translate>The rate limit must be a non-negative number (0: no limit)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<modal id="localChanged" status="info" icon="fas fa-exclamation-circle" heading="{{'Locally Changed Items' | translate}}" large="yes" closeable="yes">
|
||||
<div class="modal-body">
|
||||
<p translate>
|
||||
The following items were changed locally.
|
||||
</p>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th translate>Path</th>
|
||||
<th translate>Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr dir-paginate="file in localChanged.files | itemsPerPage: localChanged.perpage" current-page="localChanged.page" total-items="model[localChanged.folder].receiveOnlyTotalItems" pagination-id="localChanged">
|
||||
<td>{{file.name}}</td>
|
||||
<td><span ng-hide="file.type == 'DIRECTORY'">{{file.size | binary}}B</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
<dir-pagination-controls on-page-change="refreshLocalChanged(newPageNumber, localChanged.perpage)" pagination-id="localChanged"></dir-pagination-controls>
|
||||
<ul class="pagination pull-right">
|
||||
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: localChanged.page == option }">
|
||||
<a href="#" ng-click="refreshLocalChanged(localChanged.page, option)">{{option}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fas fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
|
||||
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededItems(neededFolder)" pagination-id="needed">
|
||||
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="model[neededFolder].needTotalItems" pagination-id="needed">
|
||||
|
||||
<!-- Icon -->
|
||||
<td class="small-data col-xs-2">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -51,6 +51,7 @@ type OptionsConfiguration struct {
|
||||
TrafficClass int `xml:"trafficClass" json:"trafficClass"`
|
||||
DefaultFolderPath string `xml:"defaultFolderPath" json:"defaultFolderPath" default:"~"`
|
||||
SetLowPriority bool `xml:"setLowPriority" json:"setLowPriority" default:"true"`
|
||||
MaxConcurrentScans int `xml:"maxConcurrentScans" json:"maxConcurrentScans"`
|
||||
|
||||
DeprecatedUPnPEnabled bool `xml:"upnpEnabled,omitempty" json:"-"`
|
||||
DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes,omitempty" json:"-"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,19 +21,44 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
KeyTypeDevice = 0
|
||||
KeyTypeGlobal = 1
|
||||
KeyTypeBlock = 2
|
||||
// KeyTypeDevice <int32 folder ID> <int32 device ID> <file name> = FileInfo
|
||||
KeyTypeDevice = 0
|
||||
|
||||
// KeyTypeGlobal <int32 folder ID> <file name> = VersionList
|
||||
KeyTypeGlobal = 1
|
||||
|
||||
// KeyTypeBlock <int32 folder ID> <32 bytes hash> <§file name> = int32 (block index)
|
||||
KeyTypeBlock = 2
|
||||
|
||||
// KeyTypeDeviceStatistic <device ID as string> <some string> = some value
|
||||
KeyTypeDeviceStatistic = 3
|
||||
|
||||
// KeyTypeFolderStatistic <folder ID as string> <some string> = some value
|
||||
KeyTypeFolderStatistic = 4
|
||||
KeyTypeVirtualMtime = 5
|
||||
KeyTypeFolderIdx = 6
|
||||
KeyTypeDeviceIdx = 7
|
||||
KeyTypeIndexID = 8
|
||||
KeyTypeFolderMeta = 9
|
||||
KeyTypeMiscData = 10
|
||||
KeyTypeSequence = 11
|
||||
KeyTypeNeed = 12
|
||||
|
||||
// KeyTypeVirtualMtime <int32 folder ID> <file name> = dbMtime
|
||||
KeyTypeVirtualMtime = 5
|
||||
|
||||
// KeyTypeFolderIdx <int32 id> = string value
|
||||
KeyTypeFolderIdx = 6
|
||||
|
||||
// KeyTypeDeviceIdx <int32 id> = string value
|
||||
KeyTypeDeviceIdx = 7
|
||||
|
||||
// KeyTypeIndexID <int32 device ID> <int32 folder ID> = protocol.IndexID
|
||||
KeyTypeIndexID = 8
|
||||
|
||||
// KeyTypeFolderMeta <int32 folder ID> = CountsSet
|
||||
KeyTypeFolderMeta = 9
|
||||
|
||||
// KeyTypeMiscData <some string> = some value
|
||||
KeyTypeMiscData = 10
|
||||
|
||||
// KeyTypeSequence <int32 folder ID> <int64 sequence number> = KeyTypeDevice key
|
||||
KeyTypeSequence = 11
|
||||
|
||||
// KeyTypeNeed <int32 folder ID> <file name> = <nothing>
|
||||
KeyTypeNeed = 12
|
||||
)
|
||||
|
||||
type keyer interface {
|
||||
|
||||
@@ -43,7 +43,19 @@ func Open(location string) (*Lowlevel, error) {
|
||||
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)
|
||||
|
||||
@@ -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 {
|
||||
@@ -79,6 +80,9 @@ func (db *schemaUpdater) updateSchema() error {
|
||||
if prevVersion < 6 {
|
||||
db.updateSchema5to6()
|
||||
}
|
||||
if prevVersion < 7 {
|
||||
db.updateSchema6to7()
|
||||
}
|
||||
|
||||
miscDB.PutInt64("dbVersion", dbVersion)
|
||||
miscDB.PutString("dbMinSyncthingVersion", dbMinSyncthingVersion)
|
||||
@@ -259,3 +263,39 @@ func (db *schemaUpdater) 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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ type FileIntf interface {
|
||||
IsIgnored() bool
|
||||
IsUnsupported() bool
|
||||
MustRescan() bool
|
||||
IsReceiveOnlyChanged() bool
|
||||
IsDirectory() bool
|
||||
IsSymlink() bool
|
||||
ShouldConflict() bool
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -1309,6 +1310,59 @@ func TestNeedWithNewerInvalid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 TestCaseSensitive(t *testing.T) {
|
||||
// Normal case sensitive lookup should work
|
||||
|
||||
ldb := db.OpenMemory()
|
||||
s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
|
||||
|
||||
local := []protocol.FileInfo{
|
||||
{Name: filepath.FromSlash("D1/f1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
|
||||
{Name: filepath.FromSlash("F1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
|
||||
{Name: filepath.FromSlash("d1/F1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
|
||||
{Name: filepath.FromSlash("d1/f1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
|
||||
{Name: filepath.FromSlash("f1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
|
||||
}
|
||||
|
||||
replace(s, protocol.LocalDeviceID, local)
|
||||
|
||||
gf := globalList(s)
|
||||
if l := len(gf); l != len(local) {
|
||||
t.Fatalf("Incorrect len %d != %d for global list", l, len(local))
|
||||
}
|
||||
for i := range local {
|
||||
if gf[i].Name != local[i].Name {
|
||||
t.Errorf("Incorrect filename;\n%q !=\n%q",
|
||||
gf[i].Name, local[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replace(fs *db.FileSet, device protocol.DeviceID, files []protocol.FileInfo) {
|
||||
fs.Drop(device)
|
||||
fs.Update(device, files)
|
||||
|
||||
@@ -142,6 +142,10 @@ func (c Counts) Add(other Counts) Counts {
|
||||
}
|
||||
}
|
||||
|
||||
func (c Counts) TotalItems() int32 {
|
||||
return c.Files + c.Directories + c.Symlinks + c.Deleted
|
||||
}
|
||||
|
||||
func (vl VersionList) String() string {
|
||||
var b bytes.Buffer
|
||||
var id protocol.DeviceID
|
||||
@@ -161,15 +165,7 @@ func (vl VersionList) String() string {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
vl, removedFV, removedAt = vl.pop(device)
|
||||
|
||||
nv := FileVersion{
|
||||
Device: device,
|
||||
@@ -222,6 +218,20 @@ func (vl VersionList) insertAt(i int, v FileVersion) VersionList {
|
||||
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) {
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
@@ -100,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)
|
||||
@@ -145,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))
|
||||
@@ -153,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() {
|
||||
@@ -189,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)
|
||||
}
|
||||
|
||||
@@ -204,7 +204,9 @@ type Logger struct {
|
||||
nextSubscriptionIDs []int
|
||||
nextGlobalID int
|
||||
timeout *time.Timer
|
||||
mutex sync.Mutex
|
||||
events chan Event
|
||||
funcs chan func()
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
@@ -225,6 +227,13 @@ type Subscription struct {
|
||||
|
||||
var Default = NewLogger()
|
||||
|
||||
func init() {
|
||||
// The default logger never stops. To ensure this we nil out the stop
|
||||
// channel so any attempt to stop it will panic.
|
||||
Default.stop = nil
|
||||
go Default.Serve()
|
||||
}
|
||||
|
||||
var (
|
||||
ErrTimeout = errors.New("timeout")
|
||||
ErrClosed = errors.New("closed")
|
||||
@@ -232,8 +241,10 @@ var (
|
||||
|
||||
func NewLogger() *Logger {
|
||||
l := &Logger{
|
||||
mutex: sync.NewMutex(),
|
||||
timeout: time.NewTimer(time.Second),
|
||||
events: make(chan Event, BufferSize),
|
||||
funcs: make(chan func()),
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
// Make sure the timer is in the stopped state and hasn't fired anything
|
||||
// into the channel.
|
||||
@@ -243,20 +254,52 @@ func NewLogger() *Logger {
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Logger) Log(t EventType, data interface{}) {
|
||||
l.mutex.Lock()
|
||||
l.nextGlobalID++
|
||||
dl.Debugln("log", l.nextGlobalID, t, data)
|
||||
func (l *Logger) Serve() {
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case e := <-l.events:
|
||||
// Incoming events get sent
|
||||
l.sendEvent(e)
|
||||
|
||||
e := Event{
|
||||
GlobalID: l.nextGlobalID,
|
||||
Time: time.Now(),
|
||||
Type: t,
|
||||
Data: data,
|
||||
case fn := <-l.funcs:
|
||||
// Subscriptions etc are handled here.
|
||||
fn()
|
||||
|
||||
case <-l.stop:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
// Closing the event channels corresponds to what happens when a
|
||||
// subscription is unsubscribed; this stops any BufferedSubscription,
|
||||
// makes Poll() return ErrClosed, etc.
|
||||
for _, s := range l.subs {
|
||||
close(s.events)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Stop() {
|
||||
close(l.stop)
|
||||
}
|
||||
|
||||
func (l *Logger) Log(t EventType, data interface{}) {
|
||||
l.events <- Event{
|
||||
Time: time.Now(),
|
||||
Type: t,
|
||||
Data: data,
|
||||
// SubscriptionID and GlobalID are set in sendEvent
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) sendEvent(e Event) {
|
||||
l.nextGlobalID++
|
||||
dl.Debugln("log", l.nextGlobalID, e.Type, e.Data)
|
||||
|
||||
e.GlobalID = l.nextGlobalID
|
||||
|
||||
for i, s := range l.subs {
|
||||
if s.mask&t != 0 {
|
||||
if s.mask&e.Type != 0 {
|
||||
e.SubscriptionID = l.nextSubscriptionIDs[i]
|
||||
l.nextSubscriptionIDs[i]++
|
||||
|
||||
@@ -278,59 +321,60 @@ func (l *Logger) Log(t EventType, data interface{}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
l.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (l *Logger) Subscribe(mask EventType) *Subscription {
|
||||
l.mutex.Lock()
|
||||
dl.Debugln("subscribe", mask)
|
||||
res := make(chan *Subscription)
|
||||
l.funcs <- func() {
|
||||
dl.Debugln("subscribe", mask)
|
||||
|
||||
s := &Subscription{
|
||||
mask: mask,
|
||||
events: make(chan Event, BufferSize),
|
||||
timeout: time.NewTimer(0),
|
||||
}
|
||||
s := &Subscription{
|
||||
mask: mask,
|
||||
events: make(chan Event, BufferSize),
|
||||
timeout: time.NewTimer(0),
|
||||
}
|
||||
|
||||
// We need to create the timeout timer in the stopped, non-fired state so
|
||||
// that Subscription.Poll() can safely reset it and select on the timeout
|
||||
// channel. This ensures the timer is stopped and the channel drained.
|
||||
if runningTests {
|
||||
// Make the behavior stable when running tests to avoid randomly
|
||||
// varying test coverage. This ensures, in practice if not in
|
||||
// theory, that the timer fires and we take the true branch of the
|
||||
// next if.
|
||||
runtime.Gosched()
|
||||
}
|
||||
if !s.timeout.Stop() {
|
||||
<-s.timeout.C
|
||||
}
|
||||
// We need to create the timeout timer in the stopped, non-fired state so
|
||||
// that Subscription.Poll() can safely reset it and select on the timeout
|
||||
// channel. This ensures the timer is stopped and the channel drained.
|
||||
if runningTests {
|
||||
// Make the behavior stable when running tests to avoid randomly
|
||||
// varying test coverage. This ensures, in practice if not in
|
||||
// theory, that the timer fires and we take the true branch of the
|
||||
// next if.
|
||||
runtime.Gosched()
|
||||
}
|
||||
if !s.timeout.Stop() {
|
||||
<-s.timeout.C
|
||||
}
|
||||
|
||||
l.subs = append(l.subs, s)
|
||||
l.nextSubscriptionIDs = append(l.nextSubscriptionIDs, 1)
|
||||
l.mutex.Unlock()
|
||||
return s
|
||||
l.subs = append(l.subs, s)
|
||||
l.nextSubscriptionIDs = append(l.nextSubscriptionIDs, 1)
|
||||
res <- s
|
||||
}
|
||||
return <-res
|
||||
}
|
||||
|
||||
func (l *Logger) Unsubscribe(s *Subscription) {
|
||||
l.mutex.Lock()
|
||||
dl.Debugln("unsubscribe")
|
||||
for i, ss := range l.subs {
|
||||
if s == ss {
|
||||
last := len(l.subs) - 1
|
||||
l.funcs <- func() {
|
||||
dl.Debugln("unsubscribe")
|
||||
for i, ss := range l.subs {
|
||||
if s == ss {
|
||||
last := len(l.subs) - 1
|
||||
|
||||
l.subs[i] = l.subs[last]
|
||||
l.subs[last] = nil
|
||||
l.subs = l.subs[:last]
|
||||
l.subs[i] = l.subs[last]
|
||||
l.subs[last] = nil
|
||||
l.subs = l.subs[:last]
|
||||
|
||||
l.nextSubscriptionIDs[i] = l.nextSubscriptionIDs[last]
|
||||
l.nextSubscriptionIDs[last] = 0
|
||||
l.nextSubscriptionIDs = l.nextSubscriptionIDs[:last]
|
||||
l.nextSubscriptionIDs[i] = l.nextSubscriptionIDs[last]
|
||||
l.nextSubscriptionIDs[last] = 0
|
||||
l.nextSubscriptionIDs = l.nextSubscriptionIDs[:last]
|
||||
|
||||
break
|
||||
break
|
||||
}
|
||||
}
|
||||
close(s.events)
|
||||
}
|
||||
close(s.events)
|
||||
l.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Poll returns an event from the subscription or an error if the poll times
|
||||
|
||||
@@ -9,6 +9,7 @@ package events
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -28,6 +29,9 @@ func TestNewLogger(t *testing.T) {
|
||||
|
||||
func TestSubscriber(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(0)
|
||||
defer l.Unsubscribe(s)
|
||||
if s == nil {
|
||||
@@ -37,6 +41,9 @@ func TestSubscriber(t *testing.T) {
|
||||
|
||||
func TestTimeout(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(0)
|
||||
defer l.Unsubscribe(s)
|
||||
_, err := s.Poll(timeout)
|
||||
@@ -47,6 +54,8 @@ func TestTimeout(t *testing.T) {
|
||||
|
||||
func TestEventBeforeSubscribe(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
l.Log(DeviceConnected, "foo")
|
||||
s := l.Subscribe(0)
|
||||
@@ -60,6 +69,8 @@ func TestEventBeforeSubscribe(t *testing.T) {
|
||||
|
||||
func TestEventAfterSubscribe(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(AllEvents)
|
||||
defer l.Unsubscribe(s)
|
||||
@@ -85,6 +96,8 @@ func TestEventAfterSubscribe(t *testing.T) {
|
||||
|
||||
func TestEventAfterSubscribeIgnoreMask(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(DeviceDisconnected)
|
||||
defer l.Unsubscribe(s)
|
||||
@@ -98,6 +111,8 @@ func TestEventAfterSubscribeIgnoreMask(t *testing.T) {
|
||||
|
||||
func TestBufferOverflow(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(AllEvents)
|
||||
defer l.Unsubscribe(s)
|
||||
@@ -121,6 +136,8 @@ func TestBufferOverflow(t *testing.T) {
|
||||
|
||||
func TestUnsubscribe(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(AllEvents)
|
||||
l.Log(DeviceConnected, "foo")
|
||||
@@ -141,6 +158,8 @@ func TestUnsubscribe(t *testing.T) {
|
||||
|
||||
func TestGlobalIDs(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(AllEvents)
|
||||
defer l.Unsubscribe(s)
|
||||
@@ -171,6 +190,8 @@ func TestGlobalIDs(t *testing.T) {
|
||||
|
||||
func TestSubscriptionIDs(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(DeviceConnected)
|
||||
defer l.Unsubscribe(s)
|
||||
@@ -211,6 +232,8 @@ func TestSubscriptionIDs(t *testing.T) {
|
||||
|
||||
func TestBufferedSub(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(AllEvents)
|
||||
defer l.Unsubscribe(s)
|
||||
@@ -240,6 +263,8 @@ func TestBufferedSub(t *testing.T) {
|
||||
|
||||
func BenchmarkBufferedSub(b *testing.B) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(AllEvents)
|
||||
defer l.Unsubscribe(s)
|
||||
@@ -294,6 +319,8 @@ func BenchmarkBufferedSub(b *testing.B) {
|
||||
|
||||
func TestSinceUsesSubscriptionId(t *testing.T) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(DeviceConnected)
|
||||
defer l.Unsubscribe(s)
|
||||
@@ -339,3 +366,93 @@ func TestUnmarshalEvent(t *testing.T) {
|
||||
t.Fatal("Failed to unmarshal event:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsubscribeContention(t *testing.T) {
|
||||
// Check that we can unsubscribe without blocking the whole system.
|
||||
|
||||
const (
|
||||
listeners = 50
|
||||
senders = 1000
|
||||
)
|
||||
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
// Start listeners. These will poll until the stop channel is closed,
|
||||
// then exit and unsubscribe.
|
||||
|
||||
stopListeners := make(chan struct{})
|
||||
var listenerWg sync.WaitGroup
|
||||
listenerWg.Add(listeners)
|
||||
for i := 0; i < listeners; i++ {
|
||||
go func() {
|
||||
defer listenerWg.Done()
|
||||
|
||||
s := l.Subscribe(AllEvents)
|
||||
defer l.Unsubscribe(s)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.C():
|
||||
|
||||
case <-stopListeners:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Start senders. These send pointless events until the stop channel is
|
||||
// closed.
|
||||
|
||||
stopSenders := make(chan struct{})
|
||||
defer close(stopSenders)
|
||||
var senderWg sync.WaitGroup
|
||||
senderWg.Add(senders)
|
||||
for i := 0; i < senders; i++ {
|
||||
go func() {
|
||||
defer senderWg.Done()
|
||||
|
||||
t := time.NewTicker(time.Millisecond)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
l.Log(StateChanged, nil)
|
||||
|
||||
case <-stopSenders:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Give everything time to start up.
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
// Stop the listeners and wait for them to exit. This should happen in a
|
||||
// reasonable time frame.
|
||||
|
||||
t0 := time.Now()
|
||||
close(stopListeners)
|
||||
listenerWg.Wait()
|
||||
if d := time.Since(t0); d > time.Minute {
|
||||
t.Error("It should not take", d, "to unsubscribe from an event stream")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLogEvent(b *testing.B) {
|
||||
l := NewLogger()
|
||||
defer l.Stop()
|
||||
go l.Serve()
|
||||
|
||||
s := l.Subscribe(AllEvents)
|
||||
defer l.Unsubscribe(s)
|
||||
NewBufferedSubscription(s, 1) // runs in the background
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
l.Log(StateChanged, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -195,12 +199,11 @@ func NewFilesystem(fsType FilesystemType, uri string) Filesystem {
|
||||
func IsInternal(file string) bool {
|
||||
// fs cannot import config, so we hard code .stfolder here (config.DefaultMarkerName)
|
||||
internals := []string{".stfolder", ".stignore", ".stversions"}
|
||||
pathSep := string(PathSeparator)
|
||||
for _, internal := range internals {
|
||||
if file == internal {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(file, internal+pathSep) {
|
||||
if IsParent(file, internal) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,3 +77,15 @@ func WindowsInvalidFilename(name string) bool {
|
||||
// The path must not contain any disallowed characters
|
||||
return strings.ContainsAny(name, windowsDisallowedCharacters)
|
||||
}
|
||||
|
||||
func IsParent(path, parent string) bool {
|
||||
if len(parent) == 0 {
|
||||
// The empty string is the parent of everything except the empty
|
||||
// string. (Avoids panic in the next step.)
|
||||
return len(path) > 0
|
||||
}
|
||||
if parent[len(parent)-1] != PathSeparator {
|
||||
parent += string(PathSeparator)
|
||||
}
|
||||
return strings.HasPrefix(path, parent)
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ func loadParseIncludeFile(filesystem fs.Filesystem, file string, cd ChangeDetect
|
||||
if filesystem.Type() == fs.FilesystemTypeBasic {
|
||||
uri := filesystem.URI()
|
||||
joined := filepath.Join(uri, file)
|
||||
if !strings.HasPrefix(joined, uri) {
|
||||
if !fs.IsParent(joined, uri) {
|
||||
filesystem = fs.NewFilesystem(filesystem.Type(), filepath.Dir(joined))
|
||||
file = filepath.Base(joined)
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
70
lib/model/bytesemaphore.go
Normal file
70
lib/model/bytesemaphore.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// 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) {
|
||||
s.mut.Lock()
|
||||
if bytes > s.max {
|
||||
bytes = s.max
|
||||
}
|
||||
for bytes > s.available {
|
||||
s.cond.Wait()
|
||||
if bytes > s.max {
|
||||
bytes = s.max
|
||||
}
|
||||
}
|
||||
s.available -= bytes
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *byteSemaphore) give(bytes int) {
|
||||
s.mut.Lock()
|
||||
if bytes > s.max {
|
||||
bytes = s.max
|
||||
}
|
||||
if s.available+bytes > s.max {
|
||||
s.available = s.max
|
||||
} else {
|
||||
s.available += bytes
|
||||
}
|
||||
s.cond.Broadcast()
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *byteSemaphore) setCapacity(cap int) {
|
||||
s.mut.Lock()
|
||||
diff := cap - s.max
|
||||
s.max = cap
|
||||
s.available += diff
|
||||
if s.available < 0 {
|
||||
s.available = 0
|
||||
} else if s.available > s.max {
|
||||
s.available = s.max
|
||||
}
|
||||
s.cond.Broadcast()
|
||||
s.mut.Unlock()
|
||||
}
|
||||
113
lib/model/bytesemaphore_test.go
Normal file
113
lib/model/bytesemaphore_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// 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 "testing"
|
||||
|
||||
func TestZeroByteSempahore(t *testing.T) {
|
||||
// A semaphore with zero capacity is just a no-op.
|
||||
|
||||
s := newByteSemaphore(0)
|
||||
|
||||
// None of these should block or panic
|
||||
s.take(123)
|
||||
s.take(456)
|
||||
s.give(1 << 30)
|
||||
}
|
||||
|
||||
func TestByteSempahoreCapChangeUp(t *testing.T) {
|
||||
// Waiting takes should unblock when the capacity increases
|
||||
|
||||
s := newByteSemaphore(100)
|
||||
|
||||
s.take(75)
|
||||
if s.available != 25 {
|
||||
t.Error("bad state after take")
|
||||
}
|
||||
|
||||
gotit := make(chan struct{})
|
||||
go func() {
|
||||
s.take(75)
|
||||
close(gotit)
|
||||
}()
|
||||
|
||||
s.setCapacity(155)
|
||||
<-gotit
|
||||
if s.available != 5 {
|
||||
t.Error("bad state after both takes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteSempahoreCapChangeDown1(t *testing.T) {
|
||||
// Things should make sense when capacity is adjusted down
|
||||
|
||||
s := newByteSemaphore(100)
|
||||
|
||||
s.take(75)
|
||||
if s.available != 25 {
|
||||
t.Error("bad state after take")
|
||||
}
|
||||
|
||||
s.setCapacity(90)
|
||||
if s.available != 15 {
|
||||
t.Error("bad state after adjust")
|
||||
}
|
||||
|
||||
s.give(75)
|
||||
if s.available != 90 {
|
||||
t.Error("bad state after give")
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteSempahoreCapChangeDown2(t *testing.T) {
|
||||
// Things should make sense when capacity is adjusted down, different case
|
||||
|
||||
s := newByteSemaphore(100)
|
||||
|
||||
s.take(75)
|
||||
if s.available != 25 {
|
||||
t.Error("bad state after take")
|
||||
}
|
||||
|
||||
s.setCapacity(10)
|
||||
if s.available != 0 {
|
||||
t.Error("bad state after adjust")
|
||||
}
|
||||
|
||||
s.give(75)
|
||||
if s.available != 10 {
|
||||
t.Error("bad state after give")
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteSempahoreGiveMore(t *testing.T) {
|
||||
// We shouldn't end up with more available than we have capacity...
|
||||
|
||||
s := newByteSemaphore(100)
|
||||
|
||||
s.take(150)
|
||||
if s.available != 0 {
|
||||
t.Errorf("bad state after large take")
|
||||
}
|
||||
|
||||
s.give(150)
|
||||
if s.available != 100 {
|
||||
t.Errorf("bad state after large take + give")
|
||||
}
|
||||
|
||||
s.take(150)
|
||||
s.setCapacity(125)
|
||||
// available was zero before, we're increasing capacity by 25
|
||||
if s.available != 25 {
|
||||
t.Errorf("bad state after setcap")
|
||||
}
|
||||
|
||||
s.give(150)
|
||||
if s.available != 125 {
|
||||
t.Errorf("bad state after large take + give with adjustment")
|
||||
}
|
||||
}
|
||||
@@ -11,18 +11,26 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"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"
|
||||
)
|
||||
|
||||
// scanLimiter limits the number of concurrent scans. A limit of zero means no limit.
|
||||
var scanLimiter = newByteSemaphore(0)
|
||||
|
||||
var errWatchNotStarted = errors.New("not started")
|
||||
|
||||
type folder struct {
|
||||
@@ -41,6 +49,8 @@ type folder struct {
|
||||
scanDelay chan time.Duration
|
||||
initialScanFinished chan struct{}
|
||||
stopped chan struct{}
|
||||
scanErrors []FileError
|
||||
scanErrorsMut sync.Mutex
|
||||
|
||||
pullScheduled chan struct{}
|
||||
|
||||
@@ -80,6 +90,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 +277,258 @@ 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()
|
||||
|
||||
f.setState(FolderScanWaiting)
|
||||
scanLimiter.take(1)
|
||||
defer scanLimiter.give(1)
|
||||
|
||||
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 := ""
|
||||
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 != "" && !fs.IsParent(file.Name, ignoredParent) {
|
||||
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 +685,72 @@ 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]
|
||||
outer:
|
||||
for _, fe := range f.scanErrors {
|
||||
for _, sub := range subDirs {
|
||||
if fe.Path == sub || fs.IsParent(fe.Path, sub) {
|
||||
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 || fs.IsParent(dir, prev) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -167,7 +166,7 @@ func (f *sendReceiveFolder) pull() bool {
|
||||
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)
|
||||
@@ -497,7 +496,7 @@ nextFile:
|
||||
continue nextFile
|
||||
}
|
||||
}
|
||||
f.newError("pull", fileName, errNotAvailable)
|
||||
f.newPullError("pull", fileName, errNotAvailable)
|
||||
}
|
||||
|
||||
return changed, fileDeletions, dirDeletions, nil
|
||||
@@ -513,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
|
||||
}
|
||||
@@ -573,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
|
||||
@@ -603,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
|
||||
}
|
||||
|
||||
@@ -621,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,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
|
||||
}
|
||||
|
||||
@@ -652,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
|
||||
@@ -691,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
|
||||
}
|
||||
|
||||
@@ -701,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
|
||||
}
|
||||
}
|
||||
@@ -715,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,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
|
||||
}
|
||||
|
||||
@@ -876,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
|
||||
@@ -1018,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
|
||||
}
|
||||
@@ -1130,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
|
||||
}
|
||||
}
|
||||
@@ -1142,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()
|
||||
@@ -1225,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 {
|
||||
@@ -1545,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
|
||||
@@ -1770,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
|
||||
}
|
||||
|
||||
@@ -1882,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
|
||||
@@ -1935,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"))
|
||||
|
||||
@@ -446,6 +446,9 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
m := NewModel(defaultCfgWrapper, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
// Set up our evet subscription early
|
||||
s := events.Default.Subscribe(events.ItemFinished)
|
||||
|
||||
f := setUpSendReceiveFolder(m)
|
||||
|
||||
// queue.Done should be called by the finisher routine
|
||||
@@ -470,15 +473,16 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
// Receive a block at puller, to indicate that at least a single copier
|
||||
// loop has been performed.
|
||||
toPull := <-pullChan
|
||||
// Wait until copier is trying to pass something down to the puller again
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// Close the file
|
||||
toPull.sharedPullerState.fail("test", os.ErrNotExist)
|
||||
// Unblock copier
|
||||
<-pullChan
|
||||
|
||||
s := events.Default.Subscribe(events.ItemFinished)
|
||||
timeout = time.Second
|
||||
// Close the file, causing errors on further access
|
||||
toPull.sharedPullerState.fail("test", os.ErrNotExist)
|
||||
|
||||
// Unblock copier
|
||||
go func() {
|
||||
for range pullChan {
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case state := <-finisherBufferChan:
|
||||
// At this point the file should still be registered with both the job
|
||||
@@ -490,12 +494,13 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
// Pass the file down the real finisher, and give it time to consume
|
||||
finisherChan <- state
|
||||
|
||||
if ev, err := s.Poll(timeout); err != nil {
|
||||
t0 := time.Now()
|
||||
if ev, err := s.Poll(time.Minute); err != nil {
|
||||
t.Fatal("Got error waiting for ItemFinished event:", err)
|
||||
} else if n := ev.Data.(map[string]interface{})["item"]; n != state.file.Name {
|
||||
t.Fatal("Got ItemFinished event for wrong file:", n)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
t.Log("event took", time.Since(t0))
|
||||
|
||||
state.mut.Lock()
|
||||
stateFd := state.fd
|
||||
@@ -510,11 +515,15 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
|
||||
|
||||
// Doing it again should have no effect
|
||||
finisherChan <- state
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
if _, err := s.Poll(time.Second); err != events.ErrTimeout {
|
||||
t.Fatal("Expected timeout, not another event", err)
|
||||
}
|
||||
|
||||
if f.model.progressEmitter.lenRegistry() != 0 || f.queue.lenProgress() != 0 || f.queue.lenQueued() != 0 {
|
||||
t.Fatal("Still registered", f.model.progressEmitter.lenRegistry(), f.queue.lenProgress(), f.queue.lenQueued())
|
||||
}
|
||||
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Didn't get anything to the finisher")
|
||||
}
|
||||
@@ -528,6 +537,9 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
m := NewModel(defaultCfgWrapper, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
|
||||
// Set up our evet subscription early
|
||||
s := events.Default.Subscribe(events.ItemFinished)
|
||||
|
||||
f := setUpSendReceiveFolder(m)
|
||||
|
||||
// queue.Done should be called by the finisher routine
|
||||
@@ -552,7 +564,6 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
|
||||
// Receive at finisher, we should error out as puller has nowhere to pull
|
||||
// from.
|
||||
s := events.Default.Subscribe(events.ItemFinished)
|
||||
timeout = time.Second
|
||||
select {
|
||||
case state := <-finisherBufferChan:
|
||||
@@ -565,12 +576,13 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
// Pass the file down the real finisher, and give it time to consume
|
||||
finisherChan <- state
|
||||
|
||||
if ev, err := s.Poll(timeout); err != nil {
|
||||
t0 := time.Now()
|
||||
if ev, err := s.Poll(time.Minute); err != nil {
|
||||
t.Fatal("Got error waiting for ItemFinished event:", err)
|
||||
} else if n := ev.Data.(map[string]interface{})["item"]; n != state.file.Name {
|
||||
t.Fatal("Got ItemFinished event for wrong file:", n)
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
t.Log("event took", time.Since(t0))
|
||||
|
||||
state.mut.Lock()
|
||||
stateFd := state.fd
|
||||
@@ -585,7 +597,10 @@ func TestDeregisterOnFailInPull(t *testing.T) {
|
||||
|
||||
// Doing it again should have no effect
|
||||
finisherChan <- state
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
if _, err := s.Poll(time.Second); err != events.ErrTimeout {
|
||||
t.Fatal("Expected timeout, not another event", err)
|
||||
}
|
||||
|
||||
if f.model.progressEmitter.lenRegistry() != 0 || f.queue.lenProgress() != 0 || f.queue.lenQueued() != 0 {
|
||||
t.Fatal("Still registered", f.model.progressEmitter.lenRegistry(), f.queue.lenProgress(), f.queue.lenQueued())
|
||||
@@ -686,3 +701,45 @@ func TestDiffEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteIgnorePerms checks, that a file gets deleted when the IgnorePerms
|
||||
// option is true and the permissions do not match between the file on disk and
|
||||
// in the db.
|
||||
func TestDeleteIgnorePerms(t *testing.T) {
|
||||
m := setUpModel(protocol.FileInfo{})
|
||||
f := setUpSendReceiveFolder(m)
|
||||
f.IgnorePerms = true
|
||||
|
||||
ffs := f.Filesystem()
|
||||
name := "deleteIgnorePerms"
|
||||
file, err := ffs.Create(name)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer ffs.Remove(name)
|
||||
defer file.Close()
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fi, err := scanner.CreateFileInfo(stat, name, ffs)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ffs.Chmod(name, 0600)
|
||||
scanChan := make(chan string)
|
||||
finished := make(chan struct{})
|
||||
go func() {
|
||||
err = f.checkToBeDeleted(fi, scanChan)
|
||||
close(finished)
|
||||
}()
|
||||
select {
|
||||
case <-scanChan:
|
||||
<-finished
|
||||
case <-finished:
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
150
lib/model/folder_test.go
Normal file
150
lib/model/folder_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ type folderState int
|
||||
const (
|
||||
FolderIdle folderState = iota
|
||||
FolderScanning
|
||||
FolderScanWaiting
|
||||
FolderSyncing
|
||||
FolderError
|
||||
)
|
||||
@@ -28,6 +29,8 @@ func (s folderState) String() string {
|
||||
return "idle"
|
||||
case FolderScanning:
|
||||
return "scanning"
|
||||
case FolderScanWaiting:
|
||||
return "scan-waiting"
|
||||
case FolderSyncing:
|
||||
return "syncing"
|
||||
case FolderError:
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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),
|
||||
@@ -170,6 +170,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersi
|
||||
if cfg.Options().ProgressUpdateIntervalS > -1 {
|
||||
go m.progressEmitter.Serve()
|
||||
}
|
||||
scanLimiter.setCapacity(cfg.Options().MaxConcurrentScans)
|
||||
cfg.Subscribe(m)
|
||||
|
||||
return m
|
||||
@@ -288,7 +289,7 @@ func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
|
||||
var filesAtRisk []string
|
||||
for _, protectedFilePath := range m.protectedFiles {
|
||||
// check if file is synced in this folder
|
||||
if !strings.HasPrefix(protectedFilePath, folderLocation) {
|
||||
if protectedFilePath != folderLocation && !fs.IsParent(protectedFilePath, folderLocation) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -796,12 +797,10 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
|
||||
skip--
|
||||
return true
|
||||
}
|
||||
if get > 0 {
|
||||
ft := f.(db.FileInfoTruncated)
|
||||
if _, ok := seen[ft.Name]; !ok {
|
||||
rest = append(rest, ft)
|
||||
get--
|
||||
}
|
||||
ft := f.(db.FileInfoTruncated)
|
||||
if _, ok := seen[ft.Name]; !ok {
|
||||
rest = append(rest, ft)
|
||||
get--
|
||||
}
|
||||
return get > 0
|
||||
})
|
||||
@@ -809,6 +808,47 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
|
||||
return progress, queued, rest
|
||||
}
|
||||
|
||||
// LocalChangedFiles returns a paginated list of currently needed files in
|
||||
// progress, queued, and to be queued on next puller iteration, as well as the
|
||||
// total number of files currently needed.
|
||||
func (m *Model) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
|
||||
rf, ok := m.folderFiles[folder]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
fcfg := m.folderCfgs[folder]
|
||||
if fcfg.Type != config.FolderTypeReceiveOnly {
|
||||
return nil
|
||||
}
|
||||
if rf.ReceiveOnlyChangedSize().TotalItems() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
files := make([]db.FileInfoTruncated, 0, perpage)
|
||||
|
||||
skip := (page - 1) * perpage
|
||||
get := perpage
|
||||
|
||||
rf.WithHaveTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
|
||||
if !f.IsReceiveOnlyChanged() {
|
||||
return true
|
||||
}
|
||||
if skip > 0 {
|
||||
skip--
|
||||
return true
|
||||
}
|
||||
ft := f.(db.FileInfoTruncated)
|
||||
files = append(files, ft)
|
||||
get--
|
||||
return get > 0
|
||||
})
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
// RemoteNeedFolderFiles returns paginated list of currently needed files in
|
||||
// progress, queued, and to be queued on next puller iteration, as well as the
|
||||
// total number of files currently needed.
|
||||
@@ -832,10 +872,8 @@ func (m *Model) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, p
|
||||
skip--
|
||||
return true
|
||||
}
|
||||
if get > 0 {
|
||||
files = append(files, f.(db.FileInfoTruncated))
|
||||
get--
|
||||
}
|
||||
files = append(files, f.(db.FileInfoTruncated))
|
||||
get--
|
||||
return get > 0
|
||||
})
|
||||
|
||||
@@ -1283,6 +1321,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 +1355,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 +1399,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 +1472,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 +1486,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 +1699,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 +1723,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 +1757,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,264 +2091,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
|
||||
}
|
||||
|
||||
batchFn := 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
|
||||
}
|
||||
// Resolve items which are identical with the global state.
|
||||
if 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 {
|
||||
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]
|
||||
@@ -2351,13 +2203,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 {
|
||||
@@ -2772,6 +2624,8 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
|
||||
}
|
||||
}
|
||||
|
||||
scanLimiter.setCapacity(to.Options.MaxConcurrentScans)
|
||||
|
||||
// Some options don't require restart as those components handle it fine
|
||||
// by themselves. Compare the options structs containing only the
|
||||
// attributes that require restart and act apprioriately.
|
||||
@@ -2896,40 +2750,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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3802,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)
|
||||
@@ -3894,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")
|
||||
}
|
||||
}
|
||||
|
||||
0
lib/model/progressemitter.go
Executable file → Normal file
0
lib/model/progressemitter.go
Executable file → Normal file
@@ -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) {
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
0
lib/protocol/errors.go
Executable file → Normal file
0
lib/protocol/errors.go
Executable file → Normal file
@@ -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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user