Compare commits

...

44 Commits

Author SHA1 Message Date
Jakob Borg
9260248543 gui: Fix scopes in notification directive (fixes #4941) (#4948)
The directive lives in its own isolated scope (where we put the visible() function). The stuff transcluded into the notification directive lives in the root scope and doesn't have access to the directive scope. Hence we cannot call dismiss() from inside the directive.

Similarly, config does not exist by itself in the directive scope, we need $parent to access the root scope.

Reference: https://docs.angularjs.org/guide/directive#isolating-the-scope-of-a-directive

How this worked before is a mystery. My guess is Angular bug with directive scope that was fixed in 1.3. One possibility is that transclude plus scope: true (which doesn't make sense as that is supposed to be an object) resulted in the root scope being used in the directive itself. This would then "work" as long as there is only one notification, as visible() and dismiss() would then be registered on the root scope, thus accessible from within the notification but also overridden by any notification rendered.
2018-05-14 08:53:06 +02:00
Simon Frei
c4a348db67 gui: Fallback to folder ID in recent changes (fixes #4947) (#4949) 2018-05-14 08:53:01 +02:00
Audrius Butkevicius
e125f8b05b gui: Enable proper asset caching (#4931) 2018-05-10 07:53:39 +02:00
Jakob Borg
fb198a0645 lib/db: Actually delete the correct sequence prefix 2018-05-09 12:06:29 +02:00
xjtdy888
506181599c lib/db: Remove all sequences related to the folder (fixes #4928) (#4929) 2018-05-09 08:57:42 +02:00
Jakob Borg
15d0bdcba9 authors: Add xjtdy888 2018-05-09 08:42:27 +02:00
Jakob Borg
b6b0d0664d gui, man: Update docs & translations 2018-05-09 07:45:15 +02:00
Audrius Butkevicius
baa3aa4bad gui: Don't save ignores if they haven't been loaded (fixes #4915) (#4930) 2018-05-08 23:39:17 +02:00
Audrius Butkevicius
a48a31e3f5 lib/ignores: Fix ignore loading, report errors to UI (fixes #4901) (#4932) 2018-05-08 23:37:13 +02:00
Jakob Borg
2343c82c33 lib/model: Don't include unshared folders in ClusterConfig (fixes #4926)
Also fixes a data race where ClusterConfig would access folderFiles
without a lock. Tweaked the ClusterConfig unit test to verify the
behavior.
2018-05-08 08:19:34 +01:00
Jakob Borg
616883304e Revert "build: Only the first dash in the Debian version should become a tilde"
This reverts commit 1cb09b7d2d.

I misunderstood and acted too quickly.
2018-05-05 15:33:54 +02:00
Jakob Borg
1cb09b7d2d build: Only the first dash in the Debian version should become a tilde 2018-05-05 15:29:51 +02:00
Jakob Borg
3b5e1fa0fc gui: Update Angular to 1.3.20, enable $applyAsync (fixes #4918) 2018-05-05 11:13:16 +01:00
Simon Frei
a94aceb22f lib/model: Don't create folder root when paused (fixes #4903) (#4904) 2018-05-05 10:30:39 +02:00
Nils Jakobi
f5d8243f15 dockerfile: Incorporate exposed ports, add volume (#4908)
Added EXPOSE to Dockerfile. this way these ports will show up in docker GUIs like cockpit.
Added VOLUME parameter, this renders creating the folder (/var/syncthing) obsolete.
2018-05-05 10:28:19 +02:00
Jakob Borg
c9c2e69f49 authors: Add thunderstorm99 2018-05-05 10:27:26 +02:00
Audrius Butkevicius
ef0dcea6a4 lib/model: Verify request content against weak (and possibly strong) hash (#4767) 2018-05-05 10:24:44 +02:00
Jakob Borg
678c80ffe4 authors: Add harrisonhjones (fixes #4909) 2018-05-02 08:40:33 +02:00
Jakob Borg
0f1d0380dc build: Syso stuff needs to happen on build (ref #4909) 2018-05-02 08:37:53 +02:00
Jakob Borg
58bc722a1f gui, man: Update docs & translations 2018-05-02 07:45:15 +02:00
Simon Frei
53dc346583 lib/model: Fix test function for introducer (#4898) 2018-05-01 22:56:20 +01:00
Jakob Borg
c2f498fc82 lib/model: Units are hard (fixes #4910) 2018-05-01 22:50:23 +01:00
Simon Frei
a548014755 lib/db, lib/model: Add sequence->deviceKey to db for sending indexes (#4906)
Instead of walking and unmarshalling the entire db and sorting the resulting
file infos by sequence, add store device keys by sequence number in the
database. Thus only the required file infos need be unmarshalled and are already
sorted by index.
2018-05-01 23:39:15 +02:00
Wulf Weich
2c18640386 gui: Localize number formatting (fixes #4896) (#4902) 2018-04-25 10:26:49 +02:00
Jakob Borg
78094fa0cb gui, man: Update docs & translations 2018-04-25 07:45:13 +02:00
Simon Frei
f6458d1b8f lib/config, lib/model: Include paused folders in cluster config (fixes #4897) 2018-04-22 17:01:52 +01:00
Jakob Borg
56cf2db68b Merge branch 'release'
* release:
  vendor: Update github.com/syncthing/notify (fixes #4885) (#4894)
2018-04-21 13:43:56 +02:00
Iain Barnett
4d3b5348ae lib/protocol: Add note about non-standard Luhn calculation (#4895)
Skip-check: authors
2018-04-20 18:52:03 +02:00
Simon Frei
3d02fcd473 vendor: Update github.com/syncthing/notify (fixes #4885) (#4894) 2018-04-20 17:01:03 +02:00
Jakob Borg
25bf406f0e authors: Patch pramodhkp 2018-04-20 08:03:11 +02:00
pramodhkp
071910b255 cmd/syncthing-cli: Fix errors show command (#4849) 2018-04-18 21:08:10 +01:00
Jakob Borg
bed6fb8baf authors: Add pramodhkp 2018-04-18 21:37:40 +02:00
Jakob Borg
d903bf79c5 gui, man: Update docs & translations 2018-04-18 07:45:16 +02:00
Jakob Borg
49cfe57edc Merge branch 'release'
* release:
  lib/osutil: Fix TraversesSymlink with symlinked fs root on windows (fixes #4875) (#4886)
2018-04-18 07:33:01 +02:00
Simon Frei
eca076cf7d lib/osutil: Fix TraversesSymlink with symlinked fs root on windows (fixes #4875) (#4886) 2018-04-17 22:53:06 +02:00
Jakob Borg
dbcf7a02a0 lib/model: Increase the default pull limit (fixes #4883)
Bumping the limit to 2 * the max block size (16 MiB) is a slight
increase compared to previously. Nonetheless I think it's good to allow
us to queue one request and have one on the way in, or conversely have
one large block on the way in and be able to ask for smaller blocks from
others at the same time.
2018-04-17 07:55:49 +01:00
Jakob Borg
7be53bbb88 Merge branch 'release'
* release:
  lib/fs: Fix watcher panic due to casing on windows (fixes #4877)  (#4878)
2018-04-16 20:14:54 +02:00
Jakob Borg
19c7cd99f5 all: Implement variable sized blocks (fixes #4807) 2018-04-16 19:08:50 +01:00
Simon Frei
01aef75c96 lib/fs: Fix watcher panic due to casing on windows (fixes #4877) (#4878) 2018-04-16 20:07:00 +02:00
Jakob Borg
8f6d587ecb authors: Patch wwwutz 2018-04-14 14:39:41 +02:00
Peter Marquardt
3ef19411f9 cmd/syncthing: Remove spurious <nil> in debug output
Remove spurious gui.go:985: DEBUG: <nil> on successful call time.Parse() in getSystemLog and getSystemLogTxt.
2018-04-14 10:46:33 +02:00
Jakob Borg
ea43e089d4 authors: Add wwwutz 2018-04-14 10:43:42 +02:00
Jakob Borg
5fa9237a62 script: Don't base64 encode the assets (#4874)
We did this to minimize source size and make it compile faster. Nowadays
byte slice literals compile as fast as anything, and the source isn't
checked in so it doesn't matter how big it is.

Not using base64 avoids a decode step at startup and makes the binary
about 400k smaller.
2018-04-11 22:02:14 +01:00
Jakob Borg
fa367e92b0 gui, man: Update docs & translations 2018-04-11 07:45:15 +02:00
103 changed files with 14973 additions and 8260 deletions

View File

@@ -55,6 +55,7 @@ Francois-Xavier Gsell (zukoo) <fxgsell@gmail.com>
Frank Isemann (fti7) <frank@isemann.name>
Gilli Sigurdsson (gillisig) <gilli@vx.is>
Graham Miln (grahammiln) <graham.miln@dssw.co.uk> <graham.miln@miln.eu>
Harrison Jones (harrisonhjones) <harrisonhjones@users.noreply.github.com>
Heiko Zuerker (Smiley73) <heiko@zuerker.org>
Jaakko Hannikainen (jgke) <jgke@jgke.fi>
Jacek Szafarkiewicz (hadogenes) <szafar@linux.pl>
@@ -94,12 +95,15 @@ Michael Tilli (pyfisch) <pyfisch@gmail.com>
Nate Morrison (nrm21) <natemorrison@gmail.com>
Nicholas Rishel (PrototypeNM1) <rishel.nick@gmail.com> <PrototypeNM1@users.noreply.github.com>
Niels Peter Roest (Niller303) <nielsproest@hotmail.com> <seje.niels@hotmail.com>
Nils Jakobi (thunderstorm99) <jakobi.nils@gmail.com>
Pascal Jungblut (pascalj) <github@pascalj.com> <mail@pascal-jungblut.com>
Pawel Palenica (qepasa) <pawelpalenica11@gmail.com>
Peter Hoeg (peterhoeg) <peter@speartail.com>
Peter Marquardt (wwwutz) <wwwutz@gmail.com> <wwwutz@googlemail.com>
Philippe Schommers (filoozoom) <philippe@schommers.be>
Phill Luby (pluby) <phill.luby@newredo.com>
Piotr Bejda (piobpl) <piotrb10@gmail.com>
Pramodh KP (pramodhkp) <pramodh.p@directi.com> <1507241+pramodhkp@users.noreply.github.com>
Robert Carosi (nov1n) <robert@carosi.nl>
Roman Zaynetdinov (zaynetro) <romanznet@gmail.com>
Ross Smith II (rasa) <ross@smithii.com>
@@ -126,4 +130,5 @@ Vil Brekin (Vilbrekin) <vilbrekin@gmail.com>
William A. Kennington III (wkennington) <william@wkennington.com>
Wulf Weich (wweich) <wweich@users.noreply.github.com> <wweich@gmx.de> <wulf@weich-kr.de>
Xavier O. (damajor) <damajor@gmail.com>
xjtdy888 (xjtdy888) <xjtdy888@163.com>
Yannic A. (eipiminus1) <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>

View File

@@ -10,13 +10,16 @@ RUN rm -f syncthing && go run build.go build syncthing
FROM alpine
EXPOSE 8384 22000 21027/udp
VOLUME ["/var/syncthing"]
RUN apk add --no-cache ca-certificates
COPY --from=builder /go/src/github.com/syncthing/syncthing/syncthing /bin/syncthing
RUN echo 'syncthing:x:1000:1000::/var/syncthing:/sbin/nologin' >> /etc/passwd \
&& echo 'syncthing:!::0:::::' >> /etc/shadow \
&& mkdir /var/syncthing \
&& chown syncthing /var/syncthing
USER syncthing

View File

@@ -429,8 +429,9 @@ func install(target target, tags []string) {
os.Setenv("GOOS", goos)
os.Setenv("GOARCH", goarch)
// On Windows generate a special file which the Go compiler will automatically use when generating Windows binaries
// to set things like the file icon, version, etc.
// On Windows generate a special file which the Go compiler will
// automatically use when generating Windows binaries to set things like
// the file icon, version, etc.
if goos == "windows" {
sysoPath, err := shouldBuildSyso(cwd)
if err != nil {
@@ -454,6 +455,22 @@ func build(target target, tags []string) {
os.Setenv("GOOS", goos)
os.Setenv("GOARCH", goarch)
// On Windows generate a special file which the Go compiler will
// automatically use when generating Windows binaries to set things like
// the file icon, version, etc.
if goos == "windows" {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
sysoPath, err := shouldBuildSyso(cwd)
if err != nil {
log.Printf("Warning: Windows binaries will not have file information encoded: %v", err)
}
defer shouldCleanupSyso(sysoPath)
}
runPrint("go", args...)
}

View File

@@ -44,9 +44,9 @@ func errorsShow(c *cli.Context) {
json.Unmarshal(responseToBArray(response), &data)
writer := newTableWriter()
for _, item := range data["errors"] {
time := item["time"].(string)[:19]
time := item["when"].(string)[:19]
time = strings.Replace(time, "T", " ", 1)
err := item["error"].(string)
err := item["message"].(string)
err = strings.TrimSpace(err)
fmt.Fprintln(writer, time+":\t"+err)
}

View File

@@ -68,8 +68,8 @@ func main() {
}
blockSize := int(fi.Size())
if *standardBlocks || blockSize < protocol.BlockSize {
blockSize = protocol.BlockSize
if *standardBlocks || blockSize < protocol.MinBlockSize {
blockSize = protocol.BlockSize(fi.Size())
}
bs, err := scanner.Blocks(context.TODO(), fd, blockSize, fi.Size(), nil, true)
if err != nil {

View File

@@ -251,6 +251,8 @@ func handleMetrics(w http.ResponseWriter, r *http.Request) {
}
func handleAssets(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
assets := auto.Assets()
path := r.URL.Path[1:]
if path == "" {
@@ -263,11 +265,28 @@ func handleAssets(w http.ResponseWriter, r *http.Request) {
return
}
etag := fmt.Sprintf("%d", auto.Generated)
modified := time.Unix(auto.Generated, 0).UTC()
w.Header().Set("Last-Modified", modified.Format(http.TimeFormat))
w.Header().Set("Etag", etag)
mtype := mimeTypeForFile(path)
if len(mtype) != 0 {
w.Header().Set("Content-Type", mtype)
}
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modified.Add(time.Second).After(t) {
w.WriteHeader(http.StatusNotModified)
return
}
if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, etag) {
w.WriteHeader(http.StatusNotModified)
return
}
}
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
} else {

View File

@@ -94,7 +94,7 @@ type modelIntf interface {
CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool)
CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool)
ResetFolder(folder string)
Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []model.Availability
Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []model.Availability
GetIgnores(folder string) ([]string, []string, error)
GetFolderVersions(folder string) (map[string][]versioner.FileVersion, error)
RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error)
@@ -828,7 +828,7 @@ func (s *apiService) getDBFile(w http.ResponseWriter, r *http.Request) {
return
}
av := s.model.Availability(folder, file, protocol.Vector{}, protocol.BlockInfo{})
av := s.model.Availability(folder, gf, protocol.BlockInfo{})
sendJSON(w, map[string]interface{}{
"global": jsonFileInfo(gf),
"local": jsonFileInfo(lf),
@@ -982,7 +982,9 @@ func (s *apiService) postSystemErrorClear(w http.ResponseWriter, r *http.Request
func (s *apiService) getSystemLog(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
since, err := time.Parse(time.RFC3339, q.Get("since"))
l.Debugln(err)
if err != nil {
l.Debugln(err)
}
sendJSON(w, map[string][]logger.Line{
"messages": s.systemLog.Since(since),
})
@@ -991,7 +993,9 @@ func (s *apiService) getSystemLog(w http.ResponseWriter, r *http.Request) {
func (s *apiService) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
since, err := time.Parse(time.RFC3339, q.Get("since"))
l.Debugln(err)
if err != nil {
l.Debugln(err)
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
for _, line := range s.systemLog.Since(since) {

View File

@@ -16,6 +16,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/syncthing/syncthing/lib/auto"
"github.com/syncthing/syncthing/lib/config"
@@ -71,6 +72,8 @@ func (s *staticsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
file := r.URL.Path
if file[0] == '/' {
@@ -122,6 +125,24 @@ func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
}
}
etag := fmt.Sprintf("%d", auto.Generated)
modified := time.Unix(auto.Generated, 0).UTC()
w.Header().Set("Last-Modified", modified.Format(http.TimeFormat))
w.Header().Set("Etag", etag)
if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modified.Add(time.Second).After(t) {
w.WriteHeader(http.StatusNotModified)
return
}
if match := r.Header.Get("If-None-Match"); match != "" {
if strings.Contains(match, etag) {
w.WriteHeader(http.StatusNotModified)
return
}
}
mtype := s.mimeTypeForFile(file)
if len(mtype) != 0 {
w.Header().Set("Content-Type", mtype)

View File

@@ -46,7 +46,6 @@ import (
"github.com/syncthing/syncthing/lib/sha256"
"github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/weakhash"
"github.com/thejerf/suture"
@@ -697,26 +696,8 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
},
}
if opts := cfg.Options(); opts.WeakHashSelectionMethod == config.WeakHashAuto {
perfWithWeakHash := cpuBench(3, 150*time.Millisecond, true)
l.Infof("Hashing performance with rolling hash is %.02f MB/s", perfWithWeakHash)
perfWithoutWeakHash := cpuBench(3, 150*time.Millisecond, false)
l.Infof("Hashing performance without rolling hash is %.02f MB/s", perfWithoutWeakHash)
if perfWithoutWeakHash*0.8 > perfWithWeakHash {
l.Infof("Rolling hash disabled, as it has an unacceptable performance impact.")
weakhash.Enabled = false
} else {
l.Infof("Rolling hash enabled, as it has an acceptable performance impact.")
weakhash.Enabled = true
}
} else if opts.WeakHashSelectionMethod == config.WeakHashNever {
l.Infof("Disabling rolling hash")
weakhash.Enabled = false
} else if opts.WeakHashSelectionMethod == config.WeakHashAlways {
l.Infof("Enabling rolling hash")
weakhash.Enabled = true
}
perf := cpuBench(3, 150*time.Millisecond, true)
l.Infof("Hashing performance is %.02f MB/s", perf)
dbFile := locations[locDatabase]
ldb, err := db.Open(dbFile)

View File

@@ -64,7 +64,7 @@ func (m *mockedModel) CurrentGlobalFile(folder string, file string) (protocol.Fi
func (m *mockedModel) ResetFolder(folder string) {
}
func (m *mockedModel) Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []model.Availability {
func (m *mockedModel) Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []model.Availability {
return nil
}

View File

@@ -26,7 +26,6 @@ import (
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/scanner"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/weakhash"
)
// Current version number of the usage report, for acceptance purposes. If
@@ -190,8 +189,6 @@ func reportData(cfg configIntf, m modelIntf, connectionsService connectionsIntf,
res["overwriteRemoteDeviceNames"] = opts.OverwriteRemoteDevNames
res["progressEmitterEnabled"] = opts.ProgressUpdateIntervalS > -1
res["customDefaultFolderPath"] = opts.DefaultFolderPath != "~"
res["weakHashSelection"] = opts.WeakHashSelectionMethod.String()
res["weakHashEnabled"] = weakhash.Enabled
res["customTrafficClass"] = opts.TrafficClass != 0
res["customTempIndexMinBlocks"] = opts.TempIndexMinBlocks != 10
res["temporariesDisabled"] = opts.KeepTemporariesH == 0
@@ -399,7 +396,7 @@ func (usageReportingService) String() string {
// cpuBench returns CPU performance as a measure of single threaded SHA-256 MiB/s
func cpuBench(iterations int, duration time.Duration, useWeakHash bool) float64 {
dataSize := 16 * protocol.BlockSize
dataSize := 16 * protocol.MinBlockSize
bs := make([]byte, dataSize)
rand.Reader.Read(bs)
@@ -420,7 +417,7 @@ func cpuBenchOnce(duration time.Duration, useWeakHash bool, bs []byte) float64 {
b := 0
for time.Since(t0) < duration {
r := bytes.NewReader(bs)
blocksResult, _ = scanner.Blocks(context.TODO(), r, protocol.BlockSize, int64(len(bs)), nil, useWeakHash)
blocksResult, _ = scanner.Blocks(context.TODO(), r, protocol.MinBlockSize, int64(len(bs)), nil, useWeakHash)
b += len(bs)
}
d := time.Since(t0)

View File

@@ -50,7 +50,7 @@
"Connection Error": "Connection Error",
"Connection Type": "Connection Type",
"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.",
"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 more quickly and that fewer full scans are required.",
"Copied from elsewhere": "Copied from elsewhere",
"Copied from original": "Copied from original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",

View File

@@ -12,7 +12,7 @@
"Add Remote Device": "Ajouter un appareil...",
"Add devices from the introducer to our device list, for mutually shared folders.": "ATTENTION !!! Lui permettre d'ajouter et enlever des membres à toutes mes listes de membres des partages dont il fait (ou fera !) partie (ceci permet de créer automatiquement 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.": "De plus, l'intervalle de réanalyse complète sera augmenté (60min, cad la nouvelle valeur par défaut de 1h). Vous pouvez également la configurer manuellement plus tard, pour chaque partage, après avoir choisi Non.",
"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.": "Dans ce cas, l'intervalle de réanalyse complète sera augmenté (60 fois, c.-à-d. une nouvelle valeur par défaut de 1h). Vous pouvez également la configurer manuellement plus tard, pour chaque partage, après avoir choisi Non.",
"Address": "Adresse",
"Addresses": "Adresses",
"Advanced": "Avancé",
@@ -50,7 +50,7 @@
"Connection Error": "Erreur de connexion",
"Connection Type": "Type de connexion",
"Connections": "Connexions",
"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.": "La surveillance permanente des changements est maintenant disponible. C'est disque qui signale les modifications à Syncthing qui lance alors une analyse uniquement sur les partages modifiés. Les avantages sont que les changements sont propagés plus rapidement et moins d'analyses complètes sont nécessaires.",
"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.": "La surveillance permanente des changements est maintenant disponible. C'est le disque qui signale les modifications à Syncthing qui lance alors une analyse uniquement sur les partages modifiés. Les avantages sont que les changements sont propagés plus rapidement et moins d'analyses complètes sont nécessaires.",
"Copied from elsewhere": "Copié d'ailleurs",
"Copied from original": "Copié depuis l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016, les contributeurs sont:",
@@ -220,7 +220,7 @@
"Rescan": "Réanalyser",
"Rescan All": "Tout réanalyser",
"Rescan Interval": "Intervalle d'analyse",
"Rescans": "Analyses/Surveillance",
"Rescans": "Réanalyses/Surveillance",
"Restart": "Redémarrer",
"Restart Needed": "Redémarrage nécessaire",
"Restarting": "Redémarrage en cours",
@@ -232,7 +232,7 @@
"Running": "En cours",
"Save": "Enregistrer",
"Scan Time Remaining": "Temps d'analyse restant",
"Scanning": "Analyse en cours",
"Scanning": "Détection des changements",
"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 a version": "Choisissez une version",
@@ -334,8 +334,8 @@
"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.": "Attention : si vous utilisez un système externe de surveillance tel que {{syncthingInotify}}, vous devez vous assurer qu'il est désactivé.",
"Watch for Changes": "Surveille les changements",
"Watching for Changes": "Surveiller les changements",
"Watch for Changes": "Écouter les changements",
"Watching for Changes": "Surveillance des changements",
"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",

View File

@@ -12,7 +12,7 @@
"Add Remote Device": "Apparaat op Ofstân Taheakje",
"Add devices from the introducer to our device list, for mutually shared folders.": "Heakje apparaten fan de yntrodusearders ta oan ús apparatenlyst, foar mei-inoar dielde mappen.",
"Add new folder?": "Nije map taheakje?",
"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.": "Boppedat wurd it ynterfal foar in folledige wer-sken omheech brocht (kear 60 minuten, dit is in nije standert fan 1 oere). Jo kinne dit ek letter foar elke map hânmjittich ynstelle nei it kiezen fan Nee.",
"Address": "Adres",
"Addresses": "Adressen",
"Advanced": "Avansearre",
@@ -50,7 +50,7 @@
"Connection Error": "Ferbiningsflater",
"Connection Type": "Ferbiningstype",
"Connections": "Ferbinings",
"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.": "It konstant byhâlden fan feroarings is no ek beskikber foar Syncthing. Dit hâld feroarings op de skiif yn de gaten en skent allinnich de paden dy't feroare binne. De foardielen binne dat feroarings earder trochjûn wurde en dat minder skens nedich binne. ",
"Copied from elsewhere": "Oernommen fan earne oars",
"Copied from original": "Oernommen fan orizjineel",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 de folgende bydragers:",
@@ -77,7 +77,7 @@
"Discovery Failures": "Untdekkingsflaters",
"Do not restore": "Net tebeksette",
"Do not restore all": "Hielendal net tebeksette",
"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?": "Wolle jo it konstant byhâlden fan feroarings foar al jo mappen oansette?",
"Documentation": "Dokumintaasje",
"Download Rate": "Downloadfluggens",
"Downloaded": "Downloaded",
@@ -117,7 +117,7 @@
"Folder Path": "Map-paad",
"Folder Type": "Maptype",
"Folders": "Mappen",
"Full Rescan Interval (s)": "Full Rescan Interval (s)",
"Full Rescan Interval (s)": "Folledich wersken ynterfal (s)",
"GUI": "GUI",
"GUI Authentication Password": "Wachtwurd foar ferifikaasje yn GUI",
"GUI Authentication User": "Brûkers-ID foar ferifikaasje yn GUI",
@@ -197,7 +197,7 @@
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning op opjûn ynterfal en feroarings wurde net yn'e gaten hâlden.",
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning op opjûn ynterfal en feroarings wurde yn'e gaten hâlden.",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning op opjûn ynterfal en it ynskeakeljen fan it yn'e gaten hâlden fan feroarings is mislearre, wurd eltse 1m opnij besocht:",
"Permissions": "Permissions",
"Permissions": "Rjochten",
"Please consult the release notes before performing a major upgrade.": "Foardat jo in wichtige fernijing ynstallearre, graach earst de fernijingsoantekenings lêze.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Graach foar GUI-ferifikaasje in brûkers-ID en wachtwurd ynstelle yn it ynstellingsdialooch.",
"Please wait": "In amerijke",
@@ -333,9 +333,9 @@
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warskôging, dit paad is in boppelizzende triemtafel fan in besteande map \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warskôging, dit paad is in ûnderlizzende triemtafel fan in besteande map \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warskôging, dit paad is in ûnderlizzende triemtafel fan in besteande map \"{{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.": "Warskôging: As jo in eksterne sjogger lykas {{syncthingInotify}} brûke, bin der dan wiis fan dat dizze út stiet.",
"Watch for Changes": "Sjoch foar Feroarings",
"Watching for Changes": "Sjocht foar Feroarings",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Hâld by it taheakjen fan in nij apparaat yn de holle dat it apparaat oan de oare kant ek taheakke wurde moat. ",
"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.": "Hâld by it taheakjen fan in nije map yn de holle dat de map-ID brûkt wurd om de mappen tusken apparaten mei-inoar te ferbinen. Se binne haadlettergefoelich en moatte oer alle apparaten eksakt oerienkomme.",
"Yes": "Ja",

View File

@@ -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": "고급",
@@ -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:": "Copyright © 2014-2016 the following Contributors:",
@@ -77,7 +77,7 @@
"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": "다운로드됨",
@@ -89,7 +89,7 @@
"Editing {%path%}.": "{{path}} 수정하기.",
"Enable NAT traversal": "NAT traversal 활성화",
"Enable Relaying": "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.": "음수가 아닌 수 (예, \"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.": "주소 자동 검색을 하기 위해서는 \"ip:port\" 형식의 주소들을 쉼표로 구분해서 입력하거나 \"dynamic\"을 입력하세요.",
@@ -117,7 +117,7 @@
"Folder Path": "폴더 경로",
"Folder Type": "폴더 유형",
"Folders": "폴더",
"Full Rescan Interval (s)": "Full Rescan Interval (s)",
"Full Rescan Interval (s)": "전체 탐색 간격 (s)",
"GUI": "GUI",
"GUI Authentication Password": "GUI 인증 비밀번호",
"GUI Authentication User": "GUI 인증 사용자",
@@ -163,8 +163,8 @@
"Maximum Age": "최대 보존 기간",
"Metadata Only": "메타데이터만",
"Minimum Free Disk Space": "최소 여유 디스크 용량",
"Mod. Device": "Mod. Device",
"Mod. Time": "Mod. Time",
"Mod. Device": "수정된 기기",
"Mod. Time": "수정된 시간",
"Move to top of queue": "대기열 상단으로 이동",
"Multi level wildcard (matches multiple directory levels)": "다중 레벨 와일드 카드 (여러 단계의 디렉토리와 일치하는 경우)",
"Never": "사용 안 함",
@@ -197,7 +197,7 @@
"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",
"Permissions": "권한",
"Please consult the release notes before performing a major upgrade.": "메이저 업데이트를 하기 전에 먼저 릴리즈 노트를 살펴보세요.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "설정에서 GUI 인증용 User와 암호를 입력해주세요.",
"Please wait": "기다려 주십시오",
@@ -220,24 +220,24 @@
"Rescan": "재탐색",
"Rescan All": "전체 재탐색",
"Rescan Interval": "재탐색 간격",
"Rescans": "Rescans",
"Rescans": "재탐색",
"Restart": "재시작",
"Restart Needed": "재시작 필요함",
"Restarting": "재시작 중",
"Restore": "복구",
"Restore Versions": "Restore Versions",
"Restore Versions": "버전 복구",
"Resume": "재개",
"Resume All": "모두 재개",
"Reused": "재개",
"Running": "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 a version": "버전 선택",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest 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": "송신 & 수신",
@@ -260,7 +260,7 @@
"Single level wildcard (matches within a directory only)": "단일 레벨 와일드카드 (하나의 디렉토리만 일치하는 경우)",
"Size": "크기",
"Smallest First": "작은 파일순",
"Some items could not be restored:": "Some items could not be restored:",
"Some items could not be restored:": "몇몇 항목들은 복구 할 수 없었습니다:",
"Source Code": "소스 코드",
"Stable releases and release candidates": "안정 버전과 출시 후보",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "안정 버전은 약 2주 정도 지연되어 출시됩니다. 그 기간 동안 출시 후보에 대한 테스트를 거칩니다.",
@@ -307,11 +307,11 @@
"This is a major version upgrade.": "이 업데이트는 메이저 버전입니다.",
"This setting controls the free space required on the home (i.e., index database) disk.": "이 설정은 홈 디스크에 필요한 여유 공간을 제어합니다. (즉, 인덱스 데이터베이스)",
"Time": "시간",
"Time the item was last modified": "Time the item was last modified",
"Time the item was last modified": "항목이 마지막으로 수정 된 시간",
"Trash Can File Versioning": "휴지통을 통한 파일 버전 관리",
"Type": "종류",
"Unavailable": "불가능",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Unavailable/Disabled by administrator or maintainer": "운영자 또는 관리자에 의해 불가능/비활성화 됨",
"Undecided (will prompt)": "Undecided (will prompt)",
"Unknown": "알 수 없음",
"Unshared": "공유되지 않음",
@@ -333,9 +333,9 @@
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "경고, 이 경로는 현재 존재하는 폴더 \"{{otherFolderLabel}}\" ({{otherFolder}}) 의 상위 폴더 입니다.",
"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",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "경고 : {{syncthingInotify}} 와 같은 외부 감시 도구를 사용하는 경우 비활성화되어 있는지 확인해야 합니다.",
"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는 장치간에 폴더를 묶을 때 사용됩니다. 대소문자를 구분하며 모든 장치에서 같은 ID를 사용해야 합니다.",
"Yes": "예",

View File

@@ -0,0 +1,353 @@
{
"A device with that ID is already added.": "Apparaats-ID reeds toegevoegd.",
"A negative number of days doesn't make sense.": "Een negatief aantal dagen is niet zinvol.",
"A new major version may not be compatible with previous versions.": "Een nieuwe belangrijke versie is misschien niet compatibel met eerdere versies.",
"API Key": "API-sleutel",
"About": "Over",
"Action": "Actie",
"Actions": "Acties",
"Add": "Toevoegen",
"Add Device": "Apparaat toevoegen",
"Add Folder": "Map toevoegen",
"Add Remote Device": "Extern apparaat toevoegen",
"Add devices from the introducer to our device list, for mutually shared folders.": "Voegd apparaten van het introductieapparaat toe aan de lijst met apparaten voor gemeenschappelijk gedeelde mappen.",
"Add new folder?": "Nieuwe map toevoegen?",
"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.": "Het interval van volledige scan zal daarbij ook vermeerderd worden (maal 60, dit is een nieuwe standaard van 1 uur). Ge kund het later voor elke map handmatig configureren nadat ge nee kiesd.",
"Address": "Adres",
"Addresses": "Adressen",
"Advanced": "Geavanceerd",
"Advanced Configuration": "Geavanceerde configuratie",
"Advanced settings": "Geavanceerde instellingen",
"All Data": "Alle gegevens",
"Allow Anonymous Usage Reporting?": "Versturen van anonieme gebruikersstatistieken toelaten?",
"Allowed Networks": "Toegelaten netwerken",
"Alphabetic": "Alfabetisch",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Nen externen opdracht regeld het versiebeheer. Dit moed het bestand verwijderen uit de gedeelde map.",
"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.": "Nen externen opdracht regeld het versiebeheer. Dit moed het bestand verwijderen uit de gedeelde map. Als het pad naar de toepassing spaties bevat, moet dit tussen aanhalingstekens geplaatst worden.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Nen externen opdracht regeld het versiebeheer. Dit moed het bestand verwijderen uit de gesynchroniseerde map.",
"Anonymous Usage Reporting": "Anonieme gebruikersstatistieken",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Het formaat voor anonieme gebruiksrapporten is gewijzigd. Wild ge naar het nieuwe formaat overschakelen?",
"Any devices configured on an introducer device will be added to this device as well.": "Apparaten die geconfigureerd worden op een introductieapparaat zullen ook aan dit apparaat worden toegevoegd.",
"Are you sure you want to remove device {%name%}?": "Zijd ge zeker dat ge apparaat {{name}} wild verwijderen?",
"Are you sure you want to remove folder {%label%}?": "Zijd ge zeker dat ge map {{label}} wild verwijderen?",
"Are you sure you want to restore {%count%} files?": "Zijd ge zeker dat ge {{count}} bestanden wild herstellen?",
"Auto Accept": "Automatisch aanvaarden",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatisch bijwerken bied nu de keuze tussen stabiele uitgaven en uitgavekandidaten.",
"Automatic upgrades": "Automatisch opwaarderen",
"Automatically create or share folders that this device advertises at the default path.": "Automatisch mappen die dit apparaat aankondigd aanmaken of delen in de standaardlocatie.",
"Available debug logging facilities:": "Beschikbare debuglogmogelijkheden:",
"Be careful!": "Zij voorzichtig!",
"Bugs": "Bugs",
"CPU Utilization": "CPU-gebruik",
"Changelog": "Logboek",
"Clean out after": "Kuist op na",
"Click to see discovery failures": "Klik voor ontdekkingsproblemen weer te geven",
"Close": "Sluiten",
"Command": "Opdracht",
"Comment, when used at the start of a line": "Reageerd indien gebruikt aan het begin van een regel",
"Compression": "Compressie",
"Configured": "Geconfigureerd",
"Connection Error": "Verbindingsfout",
"Connection Type": "Soort verbinding",
"Connections": "Verbindingen",
"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.": "Continu opvolgen voor wijzigingen is nu beschikbaar in Syncthing. Dit zal wijzigingen op een schijf detecteren en ne scan uitvoeren op alleen de gewijzigde paden. De voordelen zijn dat wijzigingen sneller doorgevoerd worden en dat minder volledige scans nodig zijn.",
"Copied from elsewhere": "Gekopieerd van ergens anders",
"Copied from original": "Gekopieerd van het origineel",
"Copyright © 2014-2016 the following Contributors:": "Auteursrecht © 2014-2016 voor de volgende bijdragers:",
"Copyright © 2014-2017 the following Contributors:": "Auteursrecht © 2014-2017 voor de volgende bijdragers:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Negeerpatronen worden aangemaakt, bestaand bestand word overschreven op {{path}}.",
"Danger!": "Let op!",
"Debugging Facilities": "Debugmogelijkheden",
"Default Folder Path": "Standaardmaplocatie",
"Deleted": "Verwijderd",
"Device": "Apparaat",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Apparaat \"{{name}}\" ({{device}} at {{address}}) wild verbinden. Wild ge dit toelaten?",
"Device ID": "Apparaats-ID",
"Device Identification": "Apparaatsidentificatie",
"Device Name": "Naam van apparaat",
"Device that last modified the item": "Apparaat dat het item laatst gewijzigd heefd",
"Devices": "Apparaten",
"Disabled": "Uitgeschakeld",
"Disabled periodic scanning and disabled watching for changes": "Periodiek scannen uitgeschakeld & niet op de hoogte van updates",
"Disabled periodic scanning and enabled watching for changes": "Periodiek scannen uitgeschakeld maar op de hoogte van updates!",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Period scannen uitgeschakeld & falende updatesynchronisatie(s)...",
"Disconnected": "Niet verbonden",
"Discovered": "Ontdekt",
"Discovery": "Ontdekken",
"Discovery Failures": "Ontdekkingsproblemen",
"Do not restore": "Niet herstellen",
"Do not restore all": "Niet alles herstellen",
"Do you want to enable watching for changes for all your folders?": "Wild ge het opvolgen van wijzigingen voor al uw mappen inschakelen?",
"Documentation": "Documentatie",
"Download Rate": "Downloadsnelheid",
"Downloaded": "Gedownload",
"Downloading": "Bezig met downloaden",
"Edit": "Bewerk",
"Edit Device": "Apparaat bewerken",
"Edit Folder": "Map bewerken",
"Editing": "Bezig met bewerken",
"Editing {%path%}.": "Bezig met bewerken van {{path}}.",
"Enable NAT traversal": "NAT traversal inschakelen",
"Enable Relaying": "Doorsturen inschakelen",
"Enabled": "Ingeschakeld",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Voerd een positief nummer in (bijv. 2,35) en selecteerd een eenheid. Percentages zijn een onderdeel van de totale schijfgrootte.",
"Enter a non-privileged port number (1024 - 65535).": "Voerd een niet-geprivilegieerd poortnummer in (1024 - 65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Voerd door kommas gescheiden (tcp://ip:port, tcp://host:port) adressen in of voerd dynamisch in voor automatische ontdekking van het adres uit te voeren.",
"Enter ignore patterns, one per line.": "Voerd negeerpatronen in, één per regel.",
"Error": "Fout",
"External File Versioning": "Extern versiebeheer voor bestanden",
"Failed Items": "Mislukte items",
"Failed to load ignore patterns": "Laden van negeerpatronen mislukt",
"Failed to setup, retrying": "Instellen mislukt, opnieuw proberen",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Als der geen IPv6-connectiviteit is worden problemen bij verbinden met IPv6-servers verwacht.",
"File Pull Order": "Volgorde ontvangen bestanden",
"File Versioning": "Versiebeheer",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Toegangsrechten voor bestanden worden genegeerd bij het zoeken naar wijzigingen. Gebruikt dit voor FAT-bestandssystemen.",
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Verwijderde of vervangen bestanden worden verplaatst naar de map .stversions.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Verwijderde of vervangen bestanden worden verplaatst naar de map .stversions.",
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Bestanden worden niet door Syncthing vervangen of verwijderd, maar verplaatst naar de map .stversions.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Bestanden worden niet door Syncthing vervangen of verwijderd, maar verplaatst naar de map .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.": "Bestanden zijn beschermd tegen aanpassingen die gemaakt zijn door andere apparaten, maar aanpassingen op dit apparaat worden doorgestuurd naar de rest van de cluster.",
"Filesystem Notifications": "Meldingen van bestandssysteem",
"Filter by date": "Filteren op datum",
"Filter by name": "Filteren op naam",
"Folder": "Map",
"Folder ID": "Map-ID",
"Folder Label": "Maplabel",
"Folder Path": "Maplocatie",
"Folder Type": "Soort map",
"Folders": "Mappen",
"Full Rescan Interval (s)": "Interval voor volledig opnieuw scannen (s)",
"GUI": "GUI",
"GUI Authentication Password": "GUI-wachtwoord",
"GUI Authentication User": "GUI-gebruikersnaam",
"GUI Listen Address": "GUI-luisteradres",
"GUI Listen Addresses": "GUI-adres",
"GUI Theme": "GUI-thema",
"General": "Algemeen",
"Generate": "Genereer",
"Global Changes": "Algemene veranderingen",
"Global Discovery": "Globaal zoeken",
"Global Discovery Servers": "Globale ontdekkingsservers",
"Global State": "Globale status",
"Help": "Help",
"Home page": "Startpagina",
"Ignore": "Negeer",
"Ignore Patterns": "Bestanden negeren",
"Ignore Permissions": "Bestandspermissies negeren",
"Incoming Rate Limit (KiB/s)": "Downloadsnelheid beperken (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Een verkeerde configuratie kan bestanden in mappen beschadigen en/of Syncthing onbruikbaar maken.",
"Introduced By": "Geïntroduceerd door",
"Introducer": "Introductieapparaat",
"Inversion of the given condition (i.e. do not exclude)": "Inversie van de gegeven voorwaarde (i.e. niet uitsluiten)",
"Keep Versions": "Versies behouden",
"Largest First": "Grootste eerst",
"Last File Received": "Laatst ontvangen bestand",
"Last Scan": "Laatste scan",
"Last seen": "Laatst gezien op",
"Later": "Later",
"Latest Change": "Meest recente wijziging",
"Learn more": "Leesd meer",
"Listeners": "Luisteraars",
"Loading data...": "Gegevens laden...",
"Loading...": "Laden...",
"Local Discovery": "Lokaal zoeken",
"Local State": "Lokale status",
"Local State (Total)": "Lokale status (totaal)",
"Log": "Logboek",
"Log tailing paused. Click here to continue.": "Loggen gepauzeerd. Klikt hier voor verder te gaan.",
"Logs": "Logboeken",
"Major Upgrade": "Groten update",
"Mass actions": "Groepsacties",
"Master": "Master",
"Maximum Age": "Maximum leeftijd",
"Metadata Only": "Alleen metadata",
"Minimum Free Disk Space": "Minimale vrije schijfruimte",
"Mod. Device": "Wijzigend apparaat",
"Mod. Time": "Tijdstip van wijziging",
"Move to top of queue": "Verplaatsen naar het begin van de wachtrij",
"Multi level wildcard (matches multiple directory levels)": "Wildcard op meerdere niveaus (toepasbaar op meerdere mapniveaus)",
"Never": "Nooit",
"New Device": "Nieuw apparaat",
"New Folder": "Nieuwe map",
"Newest First": "Nieuwste eerst",
"No": "Nee",
"No File Versioning": "Geen versiebeheer",
"No files will be deleted as a result of this operation.": "Deze handeling zal geen bestanden verwijderen.",
"No upgrades": "Geen upgrades",
"Normal": "Normaal",
"Notice": "Mededeling",
"OK": "Oké",
"Off": "Uit",
"Oldest First": "Oudste eerst",
"Optional descriptive label for the folder. Can be different on each device.": "Optioneel label met een beschrijving voor de map. Kan verschillend zijn op elk apparaat.",
"Options": "Opties",
"Out of Sync": "Niet gesynchroniseerd",
"Out of Sync Items": "Niet-gesynchroniseerde items",
"Outgoing Rate Limit (KiB/s)": "Uitgaande snelheidslimiet (KiB/s)",
"Override Changes": "Veranderingen overschrijven",
"Path": "Pad",
"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": "Locatie van de map op de lokale computer. Als deze niet bestaat zal de map aangemaakt worden. De tilde (~) kan gebruikt worden als snelkoppeling voor",
"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%}.": "Locatie waar nieuwe automatisch aanvaarde mappen aangemaakt zullen worden, evenals de standaard voorgestelde locatie bij toevoegen van nieuwe mappen in de gebruikersinterface. Een tilde (~) zet uit tot {{tilde}}.",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Pad waar de verschillende versies opgeslagen moeten worden (laat leeg voor de standaardmap '.stversion' in de gedeelde map).",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Betandspad waar dat de versies opgeslagen moeten worden (leeg laten voor de .stversions-standaardsubmap).",
"Pause": "Pauze",
"Pause All": "Alles pauzeren",
"Paused": "Gepauzeerd",
"Periodic scanning at given interval and disabled watching for changes": "Periodieke interval-scans maar nog niet op de hoogte van updates",
"Periodic scanning at given interval and enabled watching for changes": "Periodieke interval-scans en gelukkig synchroon lopende updates!",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodieke interval-scans & helaas falende synchronisatieupdating",
"Permissions": "Machtigingen",
"Please consult the release notes before performing a major upgrade.": "Leesd eerst de uitgaveopmerkingen vooraleer dat ge ne groten update uitvoert.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Steld ne gebruikersnaam en een paswoord in bij Instellingen.",
"Please wait": "Efkens geduld",
"Prefix indicating that the file can be deleted if preventing directory removal": "Voorvoegsel dat aangeefd dat het bestand kan worden verwijderd als het bestand het verwijderen van een map voorkomd",
"Prefix indicating that the pattern should be matched without case sensitivity": "Voorvoegsel dat aangeefd dat het patroon hoofdletterongevoelig moet worden vergeleken",
"Preview": "Voorbeeld",
"Preview Usage Report": "Voorbeeld van gebruiksstatistieken",
"Quick guide to supported patterns": "Snelgids voor ondersteunde patronen",
"RAM Utilization": "Geheugengebruik",
"Random": "Willekeurig",
"Recent Changes": "Recente wijzigingen",
"Reduced by ignore patterns": "Verminderd door negeerpatronen",
"Release Notes": "Uitgaveopmerkingen",
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Uitgavekandidaten bevatten de laatste nieuwe functionaliteit en oplossingen voor problemen. Ze gelijken op de traditionele tweewekelijkse Syncthing-uitgaven.",
"Remote Devices": "Externe apparaten",
"Remove": "Verwijderen",
"Remove Device": "Apparaat verwijderen",
"Remove Folder": "Map verwijderen",
"Required identifier for the folder. Must be the same on all cluster devices.": "Den identifier voor de map is verplicht. Deze moet dezelfde zijn op alle apparaten in de cluster.",
"Rescan": "Opnieuw scannen",
"Rescan All": "Scan alles opnieuw",
"Rescan Interval": "Scanfrequentie",
"Rescans": "Rescans",
"Restart": "Herstarten",
"Restart Needed": "Herstart vereist",
"Restarting": "Herstarten",
"Restore": "Herstellen",
"Restore Versions": "Versies herstellen",
"Resume": "Hervatten",
"Resume All": "Alles hervatten",
"Reused": "Hergebruikt",
"Running": "Draaiend",
"Save": "Opslaan",
"Scan Time Remaining": "Resterende scantijd",
"Scanning": "Scannen",
"See external versioner help for supported templated command line parameters.": "Bekijkt de documentatie van het extern versiebeheer voor ondersteunde opdrachtregelparameters.",
"See external versioning help for supported templated command line parameters.": "Bekijkt de documentatie van het extern versiebeheer voor ondersteunde opdrachtregelparameters.",
"Select a version": "Selecteerd een versie",
"Select latest version": "Meest recente versie selecteren",
"Select oldest version": "Oudste versie selecteren",
"Select the devices to share this folder with.": "Selecteerd de apparaten voor deze map mee te delen.",
"Select the folders to share with this device.": "Selecteerd de mappen voor met dit apparaat te delen.",
"Send & Receive": "Verzenden & ontvangen",
"Send Only": "Alleen verzenden",
"Settings": "Instellingen",
"Share": "Delen",
"Share Folder": "Map delen",
"Share Folders With Device": "Mappen delen met apparaat",
"Share With Devices": "Delen met apparaten",
"Share this folder?": "Deze map delen?",
"Shared With": "Gedeeld met",
"Show ID": "ID tonen",
"Show QR": "QR-code tonen",
"Show diff with previous version": "Verschil met vorige versie tonen",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Word i.p.v. den apparaats-ID getoond in de clusterstatus. Deze naam word aan andere apparaten voorgesteld als optionele standaardnaam.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Word in plaats van den apparaats-ID in de clusterstatus getoond. Zal bijgewerkt worden naar de naam die het apparaat uitzend.",
"Shutdown": "Afsluiten",
"Shutdown Complete": "Afsluiten voltooid",
"Simple File Versioning": "Eenvoudig versiebeheer",
"Single level wildcard (matches within a directory only)": "Wildcard op enkelvoudig niveau (alleen toepasbaar binnen een map)",
"Size": "Grootte",
"Smallest First": "Kleinste eerst",
"Some items could not be restored:": "Sommige items konden niet worden hersteld:",
"Source Code": "Broncode",
"Stable releases and release candidates": "Stabiele versies en uitgavekandidaten",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabiele versies zijn met ongeveer twee weken vertraagd. Tijdens deze periode worden ze als uitgavekandidaten getest.",
"Stable releases only": "Alleen stabiele versies",
"Staggered File Versioning": "Gelaagd versiebeheer",
"Start Browser": "Browser starten",
"Statistics": "Statistieken",
"Stopped": "Gestopt",
"Support": "Ondersteuning",
"Sync Protocol Listen Addresses": "Synchronisatieprotocolluisteradressen",
"Syncing": "Aan het synchroniseren",
"Syncthing has been shut down.": "Syncthing is afgesloten",
"Syncthing includes the following software or portions thereof:": "De volgende software of delen daarvan zijn onderdeel van syncthing:",
"Syncthing is restarting.": "Syncthing is aan het herstarten.",
"Syncthing is upgrading.": "Syncthing is aan het opwaarderen.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing lijkt afgesloten te zijn, of der is een verbindingsprobleem met het internet. Opnieuw proberen…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing lijkt een probleem te ondervinden met het verwerken van uw verzoek. Herlaad de pagina of herstart Syncthing als de problemen zich blijven voordoen.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthings beheerdersinterface is ingesteld voor externe toegang zonder paswoord toe te laten.",
"The aggregated statistics are publicly available at the URL below.": "De geaggregeerde statistieken zijn publiek beschikbaar op den onderstaanden URL.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De configuratie is opslagen maar nog niet actief. Syncthing moet opnieuw worden opgestart voor de nieuwe configuratie te activeren.",
"The device ID cannot be blank.": "Den apparaats-ID mag niet leeg zijn.",
"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).": "Den hier in te vullen apparaats-ID kan gevonden worden in het Acties > ID weergeven-venster op de andere apparaten. Spaties en gedachtestreepkes zijn optioneel (en worden genegeerd).",
"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.": "Het versleutelde gebruiksrapport word dagelijks gestuurd en word gebruikt voor de verschillende platformen, mappengrootte en versies op te volgen. Als de reeks gegevens wijzigd zal dit scherm opnieuw worden getoond.",
"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.": "Dezen apparaats-ID lijkt ongeldig. Den apparaats-ID bestaad uit 52 of 56 letters en cijfers, spaties en streepkes zijn optioneel.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den eerste parameter is het pad naar de map en den tweede parameter is het relatieve pad binnen die map.",
"The folder ID cannot be blank.": "De map-ID mag niet leeg zijn.",
"The folder ID must be unique.": "De map-ID moet uniek zijn.",
"The folder path cannot be blank.": "De maplocatie mag niet leeg zijn.",
"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.": "De volgende intervallen worden gebruikt: het eerste uur worden versies iedere 30 seconden bewaard, de eerste dag worden versies ieder uur bewaard, de eerste 30 dagen worden versies iedere dag bewaard, tot de maximale leeftijd worden versies iedere week bewaard.",
"The following items could not be synchronized.": "De volgende bestanden konden niet worden gesynchroniseerd.",
"The maximum age must be a number and cannot be blank.": "De maximumleeftijd moet uit cijfers bestaan en mag niet leeggelaten worden.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "De maximale tijdsduur voor een versie te bewaren (in dagen, gebruik 0 voor versies voor altijd te bewaren).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Het percentage minimale vrije schijfruimte dient een positief nummer tussen 0 en 100 (inclusief) te zijn.",
"The number of days must be a number and cannot be blank.": "Het aantal dagen moet een getal zijn en kan niet leeg gelaten worden.",
"The number of days to keep files in the trash can. Zero means forever.": "Het aantal dagen dat bestanden in de vuilbak blijven. Type 0 voor oneindig.",
"The number of old versions to keep, per file.": "Het aantal versies dat bewaard moet worden per bestand.",
"The number of versions must be a number and cannot be blank.": "Het aantal nummers moet een getal zijn en mag niet leeg blijven.",
"The path cannot be blank.": "Het bestandspad mag niet leeg zijn.",
"The rate limit must be a non-negative number (0: no limit)": "De snelheidslimiet moet een positief getal zijn (0: geen limiet)",
"The rescan interval must be a non-negative number of seconds.": "De scanfrequentie moet een positief getal in seconden zijn.",
"They are retried automatically and will be synced when the error is resolved.": "Het word automatisch opnieuw geprobeerd. Bestanden worden gesynchroniseerd als de fout is hersteld.",
"This Device": "Dit apparaat",
"This can easily give hackers access to read and change any files on your computer.": "Dit kan kwaadwilligen eenvoudig toegang geven tot het lezen en wijzigen van bestanden op uw computer.",
"This is a major version upgrade.": "Dit is ne groten update.",
"This setting controls the free space required on the home (i.e., index database) disk.": "Deze instelling beheerd de benodigde vrije ruimte op de homeschijf (bv. indexdatabank)",
"Time": "Tijd",
"Time the item was last modified": "Tijdstip waarop het item laatst gewijzigd is",
"Trash Can File Versioning": "Versiebeheer bestanden vuilbak",
"Type": "Type",
"Unavailable": "Niet beschikbaar",
"Unavailable/Disabled by administrator or maintainer": "Niet beschikbaar of uitgeschakeld door administrator of beheerder",
"Undecided (will prompt)": "Onbeslist (bevestiging vragen)",
"Unknown": "Onbekend",
"Unshared": "Niet gedeeld",
"Unused": "Ongebruikt",
"Up to Date": "Gesynchroniseerd",
"Updated": "Bijgewerkt",
"Upgrade": "Upgrade",
"Upgrade To {%version%}": "Upgrade naar {{version}}",
"Upgrading": "Bezig met upgrade",
"Upload Rate": "Uploadsnelheid",
"Uptime": "Uptime",
"Usage reporting is always enabled for candidate releases.": "Rapportage van gebruik is altijd ingeschakeld voor de uitgavekandidaten.",
"Use HTTPS for GUI": "HTTPS gebruiken voor de GUI",
"Version": "Versie",
"Versions": "Versies",
"Versions Path": "Bestandspadversies",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versies worden automatisch verwijderd als deze ouder zijn dan de maximale leeftijd of als ze het maximaal aantal toegelaten bestanden per interval overschrijden.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Let op, dit pad is een bovenliggende map van een bestaande map {{otherFolder}}.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Let op, dit pad is een bovenliggende map van een bestaande map {{otherFolderLabel}} ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Let op, dit bestandspad is een submap van een bestaande map {{otherFolder}}.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Let op, dit pad is een onderliggende map van een bestaande map {{otherFolderLabel}} ({{otherFolder}}).",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Let op: als ge nen externen opvolger gebruikt gelijk {{syncthingInotify}}, moed ge u dervan verzekeren dat die uitgeschakeld is.",
"Watch for Changes": "Wijzigingen opvolgen",
"Watching for Changes": "Wijzigingen worden opgevolgd",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Wanneer dat een nieuw apparaat word toegevoegd, houd er dan rekening mee dat dit apparaat ook aan den andere kant moet worden toegevoegd.",
"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.": "Houd er bij het toevoegen van nieuwe mappen rekening mee dat de map-ID gebruikt word voor mappen tussen apparaten te verbinden. Dezen ID is hoofdlettergevoelig en moed identiek zijn op andere apparaten.",
"Yes": "Ja",
"You can also select one of these nearby devices:": "Ge kund ook een van de apparaten die dichtbij zijn selecteren:",
"You can change your choice at any time in the Settings dialog.": "Ge kunt uw keuze op elk moment aanpassen in de Instellingen.",
"You can read more about the two release channels at the link below.": "Ge kunt meer te weten komen over de twee uitgavekanalen via onderstaande koppeling.",
"You must keep at least one version.": "Minstens 1 versie moed bewaard blijven.",
"days": "dagen",
"directories": "Mappen",
"files": "Bestanden",
"full documentation": "volledige documentatie",
"items": "objecten",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wild de map {{folder}} delen.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wild de map {{folderlabel}} ({{folder}}) delen."
}

View File

@@ -12,7 +12,7 @@
"Add Remote Device": "Dodaj urządzenie zdalne",
"Add devices from the introducer to our device list, for mutually shared folders.": "Dodaj urządzenia od wprowadzającego do tej maszyny alby obustronnie dzielić katalogi.",
"Add new folder?": "Dodać nowy 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.": "Dodatkowo, interwał całościowego skanowania będzie zwiększony (60x). Możesz również zmienić go później ręcznie dla każdego folderu wybierając \"Nie\"",
"Address": "Adres",
"Addresses": "Adresy",
"Advanced": "Zaawansowane",
@@ -23,19 +23,19 @@
"Allowed Networks": "Dozwolone sieci",
"Alphabetic": "Alfabetycznie",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Zewnętrzna komenda odpowiedzialna za wersjonowanie. Musi usunąć plik ze współdzielonego folderu.",
"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.": "Zewnętrzne polecenie obsługuje wersjonowanie. Musi ono usunąć plik z dzielonego foldeur. Jeśli ścieżka do aplikacji zawiera odstępy (spacje), powinna być zamknięta w cudzysłowie.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Zewnętrzna komenda odpowiedzialna za wersjonowanie. Musi usuwać ten plik z synchronizowanego folderu.",
"Anonymous Usage Reporting": "Anonimowe statystyki użycia",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Format anonimowego raportu zużycia uległ zmianie.\nCzy chcesz przejść na nowy format?",
"Any devices configured on an introducer device will be added to this device as well.": "Wszystkie urządzenia skonfigurowane na urządzeniu wprowadzającym zostaną dodane także do tego urządzenia.",
"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",
"Are you sure you want to remove device {%name%}?": "Czy na pewno chcesz usunąć urządzenie {{name}}?",
"Are you sure you want to remove folder {%label%}?": "Czy na pewno chcesz usunąć folder {{label}}?",
"Are you sure you want to restore {%count%} files?": "Czy na pewno chcesz przywrócić {{count}} plików?",
"Auto Accept": "Autoakceptacja",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatyczne aktualizacje pozwalają teraz wybrać pomiędzy wydaniami stabilnymi a wersjami kandydującymi.",
"Automatic upgrades": "Automatyczne aktualizacje",
"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:",
"Automatically create or share folders that this device advertises at the default path.": "Automatycznie utwórz lub udostępniaj katalogi udostępniane przez te urządzenie w domyślnej ścieżce",
"Available debug logging facilities:": "Dostępne narzędzia logowania debugowego",
"Be careful!": "Uważaj!",
"Bugs": "Błędy",
"CPU Utilization": "Użycie CPU",
@@ -49,35 +49,35 @@
"Configured": "Skonfigurowane",
"Connection Error": "Błąd połączenia",
"Connection Type": "Rodzaj połączenia",
"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.",
"Connections": "Połączenia",
"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.": "Ciągle obserwowanie zmian jest już dostępne w Syncthing. Będzie ono wykrywać zmiany na dysku i uruchamiać skanowanie tylko zmodyfikowanych plików. Zyski są takie, że zmiany są znacznie szybciej rozsyłane, i wymagana jest mniejsza ilość pełnych skanowań",
"Copied from elsewhere": "Skopiowane z innego miejsca ",
"Copied from original": "Skopiowane z oryginału",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016: ",
"Copyright © 2014-2017 the following Contributors:": "Prawa autorskie © 2014-2017 dla następujących autorów:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Ustawienie wzorów ignorowania, nadpisze istniejący plik w {{path}}.",
"Danger!": "Niebezpieczne!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Default Folder Path",
"Debugging Facilities": "Odpluskwianie",
"Default Folder Path": "Domyślna ścieżka folderu",
"Deleted": "Usunięto",
"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",
"Device Identification": "Identyfikator urządzenia",
"Device Name": "Nazwa urządzenia",
"Device that last modified the item": "Device that last modified the item",
"Device that last modified the item": "Urządzenie, które jako ostatnie zmodyfikowało element",
"Devices": "Urządzenia",
"Disabled": "Wyłączone",
"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:",
"Disabled periodic scanning and disabled watching for changes": "Wyłączono okresowe skanowanie i wyłączono obserwowanie zmian",
"Disabled periodic scanning and enabled watching for changes": "Wyłączono okresowe skanowanie i włączono obserwowanie zmian",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Wyłączono okresowe skanowanie i nie udało się skonfigurować obserwowania zmian, powtórzę co minutę:",
"Disconnected": "Rozłączony",
"Discovered": "Odkryte",
"Discovery": "Odnajdywanie",
"Discovery Failures": "Błędy odnajdowania",
"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?",
"Do not restore": "Nie przywracaj",
"Do not restore all": "Nie przywracaj wszystkich",
"Do you want to enable watching for changes for all your folders?": "Czy chcesz włączyć obserwowanie zmian we wszystkich swoich folderach?",
"Documentation": "Dokumentacja",
"Download Rate": "Prędkość pobierania",
"Downloaded": "Pobrane",
@@ -89,7 +89,7 @@
"Editing {%path%}.": "Edytowanie {{path}}.",
"Enable NAT traversal": "Włącz trawersowanie NAT",
"Enable Relaying": "Włącz przekazywanie",
"Enabled": "Enabled",
"Enabled": "Włączone",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Wpisz nieujemną liczbę (np. \"2.35\") oraz wybierz jednostkę. Wartość procentowa odnosi się do rozmiaru całego dysku.",
"Enter a non-privileged port number (1024 - 65535).": "Wpisz nieuprzywilejowany numer portu (1024-65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Wpisz oddzielone przecinkiem adresy (\"tcp://ip:port\", \"tcp://host:port\") lub \"dynamic\" by przeprowadzić automatyczne odnalezienie adresu.",
@@ -97,8 +97,8 @@
"Error": "Błąd",
"External File Versioning": "Zewnętrzne wersjonowanie pliku",
"Failed Items": "Niepowodzenia",
"Failed to load ignore patterns": "Failed to load ignore patterns",
"Failed to setup, retrying": "Failed to setup, retrying",
"Failed to load ignore patterns": "Nie udało się wczytać wzorców ignorowania",
"Failed to setup, retrying": "Nie udało się skonfigurować, ponawiam",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Błąd połączenia do serwerów IPv6 może wystąpić, jeśli brakuje połączenia po IPv6 w ogóle.",
"File Pull Order": "Kolejność pobierania plików",
"File Versioning": "Kontrola wersji",
@@ -109,22 +109,22 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Pliki przenoszone są do wersji oznaczonych datą w folderze .stversions kiedy są zastępowane bądź usuwane przez 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.": "Pliki są zabezpieczone przed zmianami na innym urządzeniu, jednak zmiany w tym urządzeniu będą wysłane do reszty.",
"Filesystem Notifications": "Powiadomienia systemowe",
"Filter by date": "Filter by date",
"Filter by name": "Filter by name",
"Filter by date": "Filtruj według daty",
"Filter by name": "Filtruj według nazwy",
"Folder": "Folder",
"Folder ID": "ID folderu",
"Folder Label": "Etykieta folderu",
"Folder Path": "Ścieżka folderu",
"Folder Type": "Rodzaj folderu",
"Folders": "Foldery",
"Full Rescan Interval (s)": "Full Rescan Interval (s)",
"Full Rescan Interval (s)": "Interwał pełnego skanowania (s)",
"GUI": "GUI",
"GUI Authentication Password": "Hasło",
"GUI Authentication User": "Użytkownik",
"GUI Listen Address": "Adres nasłuchu GUI",
"GUI Listen Addresses": "Adres nasłuchiwania",
"GUI Theme": "Motyw GUI",
"General": "General",
"General": "Ogólne",
"Generate": "Generuj",
"Global Changes": "Zmiany globalne",
"Global Discovery": "Globalne odnajdywanie",
@@ -149,22 +149,22 @@
"Latest Change": "Ostatnia zmiana",
"Learn more": "Zobacz więcej",
"Listeners": "Nasłuchujący",
"Loading data...": "Loading data...",
"Loading...": "Loading...",
"Loading data...": "Ładowanie danych...",
"Loading...": "Ładowanie...",
"Local Discovery": "Lokalne odnajdywanie",
"Local State": "Status lokalny",
"Local State (Total)": "Status lokalny (suma)",
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Logs": "Logs",
"Log tailing paused. Click here to continue.": "Śledzenie loga wstrzymane. Kliknij tutaj aby wznowić",
"Logs": "Logi",
"Major Upgrade": "Ważna aktualizacja",
"Mass actions": "Mass actions",
"Mass actions": "Działania masowe",
"Master": "Główny",
"Maximum Age": "Maksymalny wiek",
"Metadata Only": "Tylko metadane",
"Minimum Free Disk Space": "Minimum wolnego miejsca na dysku",
"Mod. Device": "Mod. Device",
"Mod. Time": "Mod. Time",
"Mod. Device": "Urządzenie modyfikacji",
"Mod. Time": "Czas modyfikacji",
"Move to top of queue": "Przenieś na początek kolejki",
"Multi level wildcard (matches multiple directory levels)": "Wieloznaczność na poziomie katalogów i plików (uwzględnia nazwy folderów i plików)",
"Never": "Nigdy",
@@ -173,7 +173,7 @@
"Newest First": "Najnowsze na początku",
"No": "Nie",
"No File Versioning": "Bez wersjonowania pliku",
"No files will be deleted as a result of this operation.": "No files will be deleted as a result of this operation.",
"No files will be deleted as a result of this operation.": "W wyniku tej operacji żadne pliki nie zostaną usunięte.",
"No upgrades": "Brak aktualizacji",
"Normal": "Zwykły",
"Notice": "Wskazówka",
@@ -188,16 +188,16 @@
"Override Changes": "Nadpisz zmiany",
"Path": "Ścieżka",
"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": "Ścieżka do lokalnego folderu. Zostanie utworzona jeżeli nie istnieje.\nZnak tyldy (~) może zostać użyty jako skrót do",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Ścieżka, w której zostaną utworzone nowe automatycznie akceptowane foldery, a także domyślna sugerowana ścieżka podczas dodawania nowych folderów za pośrednictwem interfejsu użytkownika. Znak tyldy (~) rozwija się do {{tilde}}.",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Ścieżka gdzie wersje będą przechowywane (pozostaw puste dla domyślnego folderu .stversions we współ. folderze).",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ścieżka gdzie będą przechowywane wersje (pozostaw puste dla domyślnego folderu .stversions)",
"Pause": "Zatrzymaj",
"Pause All": "Zatrzymaj wszystkie",
"Paused": "Zatrzymany",
"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",
"Periodic scanning at given interval and disabled watching for changes": "Okresowe skanowanie w podanym przedziale czasu i wyłączone obserwowanie zmian",
"Periodic scanning at given interval and enabled watching for changes": "Okresowe skanowanie w podanym przedziale czasu i włączone obserwowanie zmian",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Okresowe skanowanie w podanym przedziale czasu i nieudane konfigurowanie obserwowania zmian, ponowna próba co minutę:",
"Permissions": "Uprawnienia",
"Please consult the release notes before performing a major upgrade.": "Zaleca się przeanalizowanie \"release notes\" przed przeprowadzeniem znaczącej aktualizacji.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ustaw proszę użytkownika i hasło dostępowe do GUI w Ustawieniach",
"Please wait": "Proszę czekać",
@@ -214,30 +214,30 @@
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Wydania kandydujące zawierają najnowsze funkcje oraz poprawki błędów. Są one podobne do tradycyjnych co dwutygodniowych wydań Syncthing.",
"Remote Devices": "Urządzenia zdalne",
"Remove": "Usuń",
"Remove Device": "Remove Device",
"Remove Folder": "Remove Folder",
"Remove Device": "Usuń urządzenie",
"Remove Folder": "Usuń folder",
"Required identifier for the folder. Must be the same on all cluster devices.": "Wymagany identyfikator dla folderu. Musi być taki sam na wszystkich urządzeniach.",
"Rescan": "Skanuj ponownie",
"Rescan All": "Skanuj wszystko ponownie",
"Rescan Interval": "Interwał skanowania",
"Rescans": "Rescans",
"Rescans": "Skanowania",
"Restart": "Uruchom ponownie",
"Restart Needed": "Wymagane ponowne uruchomienie",
"Restarting": "Uruchamianie ponowne",
"Restore": "Restore",
"Restore Versions": "Restore Versions",
"Restore": "Przywróć",
"Restore Versions": "Przywróć wersje",
"Resume": "Wznów",
"Resume All": "Wznów wszystkie",
"Reused": "Ponownie użyte",
"Running": "Running",
"Running": "Działa",
"Save": "Zapisz",
"Scan Time Remaining": "Pozostały czas skanowania",
"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 a version": "Wybierz wersję",
"Select latest version": "Select latest version",
"Select oldest version": "Select oldest version",
"Select latest version": "Wybierz najnowszą wersję",
"Select oldest version": "Wybierz najstarszą wersję",
"Select the devices to share this folder with.": "Wybierz urządzenie, któremu udostępnić folder.",
"Select the folders to share with this device.": "Wybierz foldery do współdzielenia z tym urządzeniem.",
"Send & Receive": "Wyślij i odbierz",
@@ -258,9 +258,9 @@
"Shutdown Complete": "Wyłączanie ukończone",
"Simple File Versioning": "Proste wersjonowanie pliku",
"Single level wildcard (matches within a directory only)": "Wieloznaczność na poziomie plików (uwzględnia nazwy plików)",
"Size": "Size",
"Size": "Rozmiar",
"Smallest First": "Najmniejsze na początku",
"Some items could not be restored:": "Some items could not be restored:",
"Some items could not be restored:": "Niektórych elementów nie można przywrócić:",
"Source Code": "Kod źródłowy",
"Stable releases and release candidates": "Wydania stabilne i wydania kandydujące",
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Wydania stabilne są opóźnione ok. dwa tygodnie. W tym czasie są testowane jako wydania kandydujące.",
@@ -307,11 +307,11 @@
"This is a major version upgrade.": "To jest ważna aktualizacja",
"This setting controls the free space required on the home (i.e., index database) disk.": "Te ustawienia kontrolują ilość potrzebnej wolnej przestrzeni na dysku domowym (np. indeksowanie bazy danych).",
"Time": "Czas",
"Time the item was last modified": "Time the item was last modified",
"Time the item was last modified": "Czas ostatniej modyfikacji elementu",
"Trash Can File Versioning": "Kontrola werjsi plików w koszu",
"Type": "Typ",
"Unavailable": "Unavailable",
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
"Unavailable": "Niedostępne",
"Unavailable/Disabled by administrator or maintainer": "Niedostępne/Wyłączone przez administratora lub opiekuna",
"Undecided (will prompt)": "Jeszcze nie zdecydowałem (przypomnij później)",
"Unknown": "Nieznany",
"Unshared": "Nieudostępnione",
@@ -326,16 +326,16 @@
"Usage reporting is always enabled for candidate releases.": "Raportowanie użycia dla wydań kandydujących jest zawsze włączone.",
"Use HTTPS for GUI": "Używaj HTTPS",
"Version": "Wersja",
"Versions": "Versions",
"Versions": "Wersje",
"Versions Path": "Ścieżka wersji",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Wersje zostają automatycznie usunięte jeżeli są starsze niż maksymalny wiek lub przekraczają liczbę dopuszczalnych wersji.",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Uwaga, ta ścieżka jest nadrzędnym folderem istniejącego folderu \"{{otherFolder}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Uwaga, ten folder jest nadfolderem istniejącego folderu \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Uwaga, ta ścieżka to podkatalog istniejącego folderu \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Uwaga, ten folder jest podfolderem istniejącego folderu \"{{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.": "Uwaga: Jeśli korzystasz z zewnętrznego obserwatora takiego jak {{syncthingInotify}}, upewnij się, że jest on dezaktywowany.",
"Watch for Changes": "Obserwuj zmiany",
"Watching for Changes": "Obserwowanie zmian",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Gdy dodajesz nowe urządzenie, pamiętaj że urządzenie musi zostać dodane także po drugiej stronie.",
"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.": "Przy dodawaniu nowego folderu, pamiętaj, że ID użyte jest do łączenia folderów pomiędzy urządzeniami. Wielkość liter ciągu ma znaczenie musi zgadzać się na wszystkich urządzeniach.",
"Yes": "Tak",

View File

@@ -166,7 +166,7 @@
"Mod. Device": "Dispositivo mod.",
"Mod. Time": "Quando foi mod.",
"Move to top of queue": "Mover para o topo da fila",
"Multi level wildcard (matches multiple directory levels)": "Caractere polivalente multi-nível (faz corresponder a vários níveis de pastas)",
"Multi level wildcard (matches multiple directory levels)": "Símbolo polivalente multi-nível (faz corresponder a vários níveis de pastas)",
"Never": "Nunca",
"New Device": "Novo dispositivo",
"New Folder": "Nova pasta",
@@ -187,7 +187,7 @@
"Outgoing Rate Limit (KiB/s)": "Limite da velocidade de envio (KiB/s)",
"Override Changes": "Sobrepor alterações",
"Path": "Caminho",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Caminho para a pasta no computador local. Será criada, caso não exista. O caractere (~) pode ser utilizado como atalho para",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Caminho para a pasta no computador local. Será criada, caso não exista. O til (~) pode ser utilizado como atalho para",
"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%}.": "Caminho no qual as novas pastas aceites automaticamente serão criadas, assim como o caminho predefinido sugerido ao adicionar novas pastas através da interface do utilizador. O til (~) expande para {{tilde}}.",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Caminho da pasta onde as versões deverão ser guardadas (deixe vazio para ficar a pasta predefinida .stversions dentro da pasta partilhada).",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Caminho onde as versões são guardadas (deixe vazio para usar a pasta predefinida .stversions dentro da pasta a que se refere).",
@@ -220,7 +220,7 @@
"Rescan": "Verificar agora",
"Rescan All": "Verificar todas agora",
"Rescan Interval": "Intervalo entre verificações",
"Rescans": "Verificação de alterações",
"Rescans": "Verificações",
"Restart": "Reiniciar",
"Restart Needed": "É preciso reiniciar",
"Restarting": "Reiniciando",
@@ -232,7 +232,7 @@
"Running": "Em execução",
"Save": "Gravar",
"Scan Time Remaining": "Tempo restante da verificação",
"Scanning": "Verificando",
"Scanning": "Verificação de alterações",
"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 a version": "Seleccione uma versão",
@@ -257,7 +257,7 @@
"Shutdown": "Desligar",
"Shutdown Complete": "Encerramento completado",
"Simple File Versioning": "Simples",
"Single level wildcard (matches within a directory only)": "Caractere polivalente de um só nível (faz corresponder apenas dentro de uma pasta)",
"Single level wildcard (matches within a directory only)": "Símbolo polivalente de um só nível (faz corresponder apenas dentro de uma pasta)",
"Size": "Tamanho",
"Smallest First": "Primeiro os menores",
"Some items could not be restored:": "Não foi possível restaurar alguns dos itens:",

View File

@@ -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 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.": "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.": "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.",
"Address": "Adress",
"Addresses": "Adresser",
"Advanced": "Avancerat",
@@ -37,7 +37,7 @@
"Automatically create or share folders that this device advertises at the default path.": "Skapa eller dela automatiskt mappar som den här enheten annonserar på standardsökvägen.",
"Available debug logging facilities:": "Tillgängliga felsökningsfunktioner:",
"Be careful!": "Var aktsam!",
"Bugs": "Buggar",
"Bugs": "Felrapporter",
"CPU Utilization": "CPU användning",
"Changelog": "Ändringslogg",
"Clean out after": "Rensa efteråt",
@@ -77,7 +77,7 @@
"Discovery Failures": "Upptäcktsmisslyckanden",
"Do not restore": "Återställ inte",
"Do not restore all": "Återställ inte allt",
"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?": "Vill du aktivera håll utkik efter förändringar på alla dina mappar?",
"Documentation": "Dokumentation",
"Download Rate": "Hämtningshastighet",
"Downloaded": "Hämtat",
@@ -97,7 +97,7 @@
"Error": "Fel",
"External File Versioning": "Extern filversionshantering",
"Failed Items": "Misslyckade objekt",
"Failed to load ignore patterns": "Failed to load ignore patterns",
"Failed to load ignore patterns": "Misslyckades med att ladda ignorera mönster",
"Failed to setup, retrying": "Misslyckades med att ställa in, försök igen",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Misslyckande med att ansluta till IPv6-servrar förväntas om ingen IPv6-anslutning finns.",
"File Pull Order": "Filhämtningsprioritering",
@@ -117,7 +117,7 @@
"Folder Path": "Mappsökväg",
"Folder Type": "Mapptyp",
"Folders": "Mappar",
"Full Rescan Interval (s)": "Full Rescan Interval (s)",
"Full Rescan Interval (s)": "Fullständig omskanningsintervall(er)",
"GUI": "Grafiskt gränssnitt",
"GUI Authentication Password": "Gränssnittets autentiseringslösenord",
"GUI Authentication User": "Gränssnittets autentiseringsanvändare",
@@ -197,7 +197,7 @@
"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:",
"Permissions": "Permissions",
"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.",
"Please wait": "Var god vänta",
@@ -334,8 +334,8 @@
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en underkatalog till en befintlig mapp \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Varning, denna sökväg är en undermapp av en befintlig mapp \"{{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": "Håll utkik efter förändringar",
"Watching for Changes": "Håller utkik efter förändringar",
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att den här enheten måste läggas till på den andra enheten också.",
"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.": "När du lägger till ny mapp, tänk på att mappens ID knyter ihop mappar mellan olika enheter. De skiftlägeskänsliga och måste matcha precis mellan alla enheter.",
"Yes": "Ja",

View File

@@ -197,7 +197,7 @@
"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",
"Permissions": "Дозволи",
"Please consult the release notes before performing a major upgrade.": "Будь ласка перегляньте примітки до випуску перед мажорним оновленням. ",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Будь ласка, встановіть у налаштуваннях ім'я користувача та пароль до графічного інтерфейсу.",
"Please wait": "Будь ласка, зачекайте",

View File

@@ -38,7 +38,7 @@
"Available debug logging facilities:": "可用的除錯日誌工具:",
"Be careful!": "請小心!",
"Bugs": "程式錯誤",
"CPU Utilization": "CPU 使用",
"CPU Utilization": "CPU 使用",
"Changelog": "更新日誌",
"Clean out after": "於之後清空",
"Click to see discovery failures": "點擊以查閱失敗的探索",
@@ -48,9 +48,9 @@
"Compression": "壓縮",
"Configured": "已設定",
"Connection Error": "連線錯誤",
"Connection Type": "連類型",
"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:": "Copyright © 2014-2016 下列貢獻者:",
@@ -61,23 +61,23 @@
"Default Folder Path": "預設資料夾路徑",
"Deleted": "已刪除",
"Device": "裝置",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "裝置 \"{{name}}\" ({{device}} 位於 {{address}}) 想要連線。 要增加新裝置嗎?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "裝置 \"{{name}}\" ({{device}} 位於 {{address}}) 想要連線。要增加新裝置嗎?",
"Device ID": "裝置識別碼",
"Device Identification": "裝置識別",
"Device Name": "裝置名稱",
"Device that last modified the item": "前次修改裝置",
"Devices": "裝置",
"Disabled": "關閉",
"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 分鐘重試:",
"Disabled": "停用",
"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 分鐘重試:",
"Disconnected": "斷線",
"Discovered": "已發現",
"Discovery": "探索",
"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": "已下載",
@@ -90,24 +90,24 @@
"Enable NAT traversal": "啟用 NAT 穿透",
"Enable Relaying": "啟用中繼",
"Enabled": "啟用",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "請輸入一非負數(如:\"2.35\")並選擇一個單位。百分比表示佔用磁碟容量的大小。",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "請輸入一非負數(如:\"2\\.35\")並選擇一個單位。百分比表示佔用磁碟容量的大小。",
"Enter a non-privileged port number (1024 - 65535).": "輸入一個非特權通訊埠號 (1024 - 65535)。",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "輸入以半形逗號區隔的位址 (\"tcp://ip:port\", \"tcp://host:port\"),或輸入 \"dynamic\" 以進行位址的自動探索",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "輸入以半形逗號區隔的位址 (\"tcp://ip:port\", \"tcp://host:port\"),或輸入 \"dynamic\" 以進行位址的自動探索",
"Enter ignore patterns, one per line.": "輸入忽略樣式,每行一種。",
"Error": "錯誤",
"External File Versioning": "外部檔案版本控制",
"External File Versioning": "外部檔案版本控制",
"Failed Items": "失敗的項目",
"Failed to load ignore patterns": "Failed to load ignore patterns",
"Failed to load ignore patterns": "無法讀取忽略樣式",
"Failed to setup, retrying": "無法設定,正在重試",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "若未配置 IPv6則無法連接 IPv6 伺服器係屬正常。",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "若沒有 IPv6 連線能力,則無法連接 IPv6 伺服器係屬正常現象。",
"File Pull Order": "提取檔案的順序",
"File Versioning": "檔案版本控制",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "當改變時,檔案權限位元 File permission bits 會被忽略。用於 FAT 檔案系統上。",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "當改變時,檔案權限位元File permission bits會被忽略。用於 FAT 檔案系統上。",
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "當檔案被 Syncthing 取代或刪除時,它們將被移至 .stversions 資料夾。",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "當檔案被 Syncthing 取代或刪除時,它們將被移至 .stversions 資料夾。",
"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 protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "其他裝置做的改變不會影響此裝置的檔案,但在此裝置上的變將被發送到叢集的其他部分。",
"Filesystem Notifications": "檔案系統通知",
"Filter by date": "以日期篩選",
"Filter by name": "以名稱篩選",
@@ -117,7 +117,7 @@
"Folder Path": "資料夾路徑",
"Folder Type": "資料夾類型",
"Folders": "資料夾",
"Full Rescan Interval (s)": "Full Rescan Interval (s)",
"Full Rescan Interval (s)": "完全重新掃描間隔 ()",
"GUI": "GUI",
"GUI Authentication Password": "GUI 認證密碼",
"GUI Authentication User": "GUI 使用者認證名稱",
@@ -182,31 +182,31 @@
"Oldest First": "最舊的優先",
"Optional descriptive label for the folder. Can be different on each device.": "資料夾的說明標籤(選擇性)。在不同裝置上可不一致。",
"Options": "選項",
"Out of Sync": "同步",
"Out of Sync Items": "同步物件",
"Out of Sync": "同步",
"Out of Sync Items": "同步項目",
"Outgoing Rate Limit (KiB/s)": "連出速率限制 (KiB/s)",
"Override Changes": "置換改變",
"Override Changes": "覆蓋變動",
"Path": "路徑",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "資料夾在本機的路徑。若資料夾不存在則會建立。波浪符號 (~) 可用作下列資料夾的捷徑:",
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "新自動接受的資料夾所在路徑,也是從 UI 加入資料夾預設推薦路徑。波浪符字元 (~) 將被展開為 {{tilde}}。",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "儲存歷史版本的路徑(若為空,則預設使用資料夾中的 .stversions 資料夾。",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "儲存歷史版本的路徑 (若為空,則預設使用資料夾中的 .stversions 資料夾)。",
"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%}.": "新自動接受的資料夾將會建立在此路徑下,從 UI 加入資料夾時也將預設推薦路徑。波浪符字元 (~) 將被展開為 {{tilde}}。",
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "儲存歷史版本的路徑(若為空,則預設使用資料夾中的 .stversions 資料夾。",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "儲存歷史版本的路徑若為空,則預設使用資料夾中的 .stversions 資料夾。",
"Pause": "暫停",
"Pause All": "全部暫停",
"Paused": "暫停",
"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 failed setting up watching for changes, retrying every 1m:": "在一定的時間間隔,定期掃描,無法設定觀察變動,每 1 分鐘重試:",
"Permissions": "Permissions",
"Permissions": "權限",
"Please consult the release notes before performing a major upgrade.": "執行重大升級前請先參閱版本資訊。",
"Please set a GUI Authentication User and Password in the Settings dialog.": "請在設定對話方塊內設置 GUI 使用者認證名稱及密碼。",
"Please set a GUI Authentication User and Password in the Settings dialog.": "請在設定對話內設置 GUI 使用者認證名稱及密碼。",
"Please wait": "請稍後",
"Prefix indicating that the file can be deleted if preventing directory removal": "前綴表示當此檔案阻礙了資料夾刪除時,可一併刪除此檔",
"Prefix indicating that the pattern should be matched without case sensitivity": "前綴表示此樣式不區分大小寫",
"Preview": "預覽",
"Preview Usage Report": "預覽使用資訊報告",
"Quick guide to supported patterns": "可支援樣式的快速指南",
"RAM Utilization": "記憶體使用",
"RAM Utilization": "記憶體使用",
"Random": "隨機",
"Recent Changes": "最近變動",
"Reduced by ignore patterns": "已由忽略樣式縮減",
@@ -282,18 +282,18 @@
"The aggregated statistics are publicly available at the URL below.": "匯總統計資訊可於下方網址取得。",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "組態已經儲存但尚未啟用。Syncthing 必須重新啟動以便啟用新的組態。",
"The device ID cannot be blank.": "裝置識別碼不能為空白。",
"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).": "輸入裝置識別碼可在其它裝置的 \"作 > 顯示識別碼\" 對話框找到。空白及連接符號可省略。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "經過加密的使用資訊報告會每天傳送。報告是用來追蹤常用的平台、資料夾大小以及應用程式版本。若傳送的資料集有異動,您會再次看到這個對話框。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "輸入的裝置識別碼似乎無效。它應該為一串包含半形英文字母及數字,並可能會含有空白或連接符號的字串,且長度為 52 或 56 個字元。",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "其它裝置的裝置識別碼可在它們的 \"作 > 顯示識別碼\" 對話框找到。空白及連接符號可省略。",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "經過加密的使用資訊報告會每天傳送。報告是用來追蹤常用的平台、資料夾大小以及應用程式版本。若傳送的資料集有異動,您會再次看到這個對話框。",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "輸入的裝置識別碼似乎無效。它應該為一串長度為 52 或 56 個字元長的半形英文字母及數字,並可能會含有額外的空白或連接符號。",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "第一個命令列參數是資料夾路徑,第二個參數是在資料夾中的相對路徑。",
"The folder ID cannot be blank.": "資料夾識別碼不能為空白。",
"The folder ID must be unique.": "資料夾識別碼必須為獨一無二的。",
"The folder path cannot be blank.": "資料夾路徑不能空白。",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "使用下列的間隔:在第一個小時內每 30 秒保留一個版本,在第一天內每小時保留一個版本,在第 30 天內每一天保留一個版本,在達到最長保留時間前每一星期保留一個版本。",
"The following items could not be synchronized.": "以下項目不能被同步。",
"The following items could not be synchronized.": "無法同步以下項目。",
"The maximum age must be a number and cannot be blank.": "最長保留時間必須為一個數字且不得為空。",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "一個版本被保留的最長時間 (單位為天,若設定為 0 則表示永遠保留)。",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "最少閒置磁碟空間的百分比必須介於 0 到 100 (包含)的非負數。",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "最少可用磁碟空間的百分比必須介於 0 到 100(含)。",
"The number of days must be a number and cannot be blank.": "天數必須必須為一個數字且不得為空。",
"The number of days to keep files in the trash can. Zero means forever.": "檔案在垃圾筒中保留的天數。零表示永遠地保留。",
"The number of old versions to keep, per file.": "每個檔案要保留的舊版本數量。",
@@ -304,7 +304,7 @@
"They are retried automatically and will be synced when the error is resolved.": "解決間題後,將會自動重試和同步。",
"This Device": "本機",
"This can easily give hackers access to read and change any files on your computer.": "這能給駭客輕易的來讀取、變更電腦中的任何檔案。",
"This is a major version upgrade.": "這是一個主要版本更新。",
"This is a major version upgrade.": "這是一個重大版本更新。",
"This setting controls the free space required on the home (i.e., index database) disk.": "此設定控制家目錄(即:索引資料庫)的必須可用空間。",
"Time": "時間",
"Time the item was last modified": "前次修改時間",
@@ -312,7 +312,7 @@
"Type": "類型",
"Unavailable": "無法使用",
"Unavailable/Disabled by administrator or maintainer": "無法使用 / 被系統管理員或維護者停用",
"Undecided (will prompt)": "未定(將會提示)",
"Undecided (will prompt)": "未定(將會提示)",
"Unknown": "未知",
"Unshared": "未共享",
"Unused": "未使用",
@@ -323,7 +323,7 @@
"Upgrading": "正在升級",
"Upload Rate": "上載速率",
"Uptime": "上線時間",
"Usage reporting is always enabled for candidate releases.": "發行候選版永遠回報使用數據",
"Usage reporting is always enabled for candidate releases.": "發行候選版永遠啟用使用數據回報。",
"Use HTTPS for GUI": "為 GUI 使用 HTTPS",
"Version": "版本",
"Versions": "版本",
@@ -333,9 +333,9 @@
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "警告,此路徑是現存資料夾 \"{{otherFolderLabel}}\" ({{otherFolder}}) 的上級目錄。",
"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",
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "警告:如果您正在使用外部監視工具,如 {{syncthingInotify}},您應該確認已經將其關閉。",
"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.": "當新增一個資料夾時,請記住,資料夾識別碼是用來將裝置之間的資料夾綁定在一起的。它們有區分大小寫,且必須在所有裝置之間完全相同。",
"Yes": "是",

View File

@@ -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)","eo":"Esperanto","es":"Spanish","es-ES":"Spanish (Spain)","eu":"Basque","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","tr":"Turkish","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)","eo":"Esperanto","es":"Spanish","es-ES":"Spanish (Spain)","eu":"Basque","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","nl-BE":"Dutch (Belgium)","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sk":"Slovak","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}

View File

@@ -1 +1 @@
var validLangs = ["bg","ca@valencia","cs","da","de","el","en","en-GB","eo","es","es-ES","eu","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","pl","pt-BR","pt-PT","ru","sk","sv","tr","uk","zh-CN","zh-TW"]
var validLangs = ["bg","ca@valencia","cs","da","de","el","en","en-GB","eo","es","es-ES","eu","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","nl-BE","pl","pt-BR","pt-PT","ru","sk","sv","tr","uk","zh-CN","zh-TW"]

View File

@@ -321,9 +321,9 @@
<tr ng-if="!folder.paused">
<th><span class="fa fa-fw fa-globe"></span>&nbsp;<span translate>Global State</span></th>
<td class="text-right">
<span tooltip data-original-title="{{model[folder.id].globalFiles | alwaysNumber}} {{'files' | translate}}, {{model[folder.id].globalDirectories | alwaysNumber}} {{'directories' | translate}}, ~{{model[folder.id].globalBytes | binary}}B">
<span class="fa fa-files-o"></span>&nbsp;{{model[folder.id].globalFiles | alwaysNumber}}&ensp;
<span class="fa fa-folder-o"></span>&nbsp;{{model[folder.id].globalDirectories | alwaysNumber}}&ensp;
<span tooltip data-original-title="{{model[folder.id].globalFiles | alwaysNumber | localeNumber}} {{'files' | translate}}, {{model[folder.id].globalDirectories | alwaysNumber | localeNumber}} {{'directories' | translate}}, ~{{model[folder.id].globalBytes | binary}}B">
<span class="fa fa-files-o"></span>&nbsp;{{model[folder.id].globalFiles | alwaysNumber | localeNumber}}&ensp;
<span class="fa fa-folder-o"></span>&nbsp;{{model[folder.id].globalDirectories | alwaysNumber | localeNumber}}&ensp;
<span class="fa fa-hdd-o"></span>&nbsp;~{{model[folder.id].globalBytes | binary}}B
</span>
</td>
@@ -331,9 +331,9 @@
<tr ng-if="!folder.paused">
<th><span class="fa fa-fw fa-home"></span>&nbsp;<span translate>Local State</span></th>
<td class="text-right">
<span tooltip data-original-title="{{model[folder.id].localFiles | alwaysNumber}} {{'files' | translate}}, {{model[folder.id].localDirectories | alwaysNumber}} {{'directories' | translate}}, ~{{model[folder.id].localBytes | binary}}B">
<span class="fa fa-files-o"></span>&nbsp;{{model[folder.id].localFiles | alwaysNumber}}&ensp;
<span class="fa fa-folder-o"></span>&nbsp;{{model[folder.id].localDirectories | alwaysNumber}}&ensp;
<span tooltip data-original-title="{{model[folder.id].localFiles | alwaysNumber | localeNumber}} {{'files' | translate}}, {{model[folder.id].localDirectories | alwaysNumber | localeNumber}} {{'directories' | translate}}, ~{{model[folder.id].localBytes | binary}}B">
<span class="fa fa-files-o"></span>&nbsp;{{model[folder.id].localFiles | alwaysNumber | localeNumber}}&ensp;
<span class="fa fa-folder-o"></span>&nbsp;{{model[folder.id].localDirectories | alwaysNumber | localeNumber}}&ensp;
<span class="fa fa-hdd-o"></span>&nbsp;~{{model[folder.id].localBytes | binary}}B
<span ng-if="model[folder.id].ignorePatterns"><br/><i><small translate class="text-muted">Reduced by ignore patterns</small></i></span>
</span>
@@ -355,7 +355,7 @@
<th><span class="fa fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Failed Items</span></th>
<!-- Show the number of failed items as a link to bring up the list. -->
<td class="text-right">
<a href="" ng-click="showFailed(folder.id)">{{model[folder.id].pullErrors | alwaysNumber}}&nbsp;<span translate>items</span></a>
<a href="" ng-click="showFailed(folder.id)">{{model[folder.id].pullErrors | alwaysNumber | localeNumber}}&nbsp;<span translate>items</span></a>
</td>
</tr>
<tr ng-if="folder.type != 'readwrite'">
@@ -532,9 +532,9 @@
<tr>
<th><span class="fa fa-fw fa-home"></span>&nbsp;<span translate>Local State (Total)</span></th>
<td class="text-right">
<span tooltip data-original-title="{{localStateTotal.files | alwaysNumber}} {{'files' | translate}}, {{ localStateTotal.directories | alwaysNumber}} {{'directories' | translate}}, ~{{ localStateTotal.bytes | binary}}B">
<span class="fa fa-files-o"></span>&nbsp;{{localStateTotal.files | alwaysNumber}}&ensp;
<span class="fa fa-folder-o"></span>&nbsp;{{localStateTotal.directories| alwaysNumber}}&ensp;
<span tooltip data-original-title="{{localStateTotal.files | alwaysNumber | localeNumber}} {{'files' | translate}}, {{ localStateTotal.directories | alwaysNumber | localeNumber}} {{'directories' | translate}}, ~{{ localStateTotal.bytes | binary}}B">
<span class="fa fa-files-o"></span>&nbsp;{{localStateTotal.files | alwaysNumber | localeNumber}}&ensp;
<span class="fa fa-folder-o"></span>&nbsp;{{localStateTotal.directories| alwaysNumber | localeNumber}}&ensp;
<span class="fa fa-hdd-o"></span>&nbsp;~{{localStateTotal.bytes | binary}}B
</td>
</tr>
@@ -544,7 +544,7 @@
</tr>
<tr>
<th><span class="fa fa-fw fa-tachometer"></span>&nbsp;<span translate>CPU Utilization</span></th>
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
<td class="text-right">{{system.cpuPercent | alwaysNumber | localeNumber:2}}%</td>
</tr>
<tr>
<th><span class="fa fa-fw fa-sitemap"></span>&nbsp;<span translate>Listeners</span></th>
@@ -635,7 +635,7 @@
<tr ng-if="deviceStatus(deviceCfg) == 'syncing'">
<th><span class="fa fa-fw fa-exchange"></span>&nbsp;<span translate>Out of Sync Items</span></th>
<td class="text-right">
<a href="" ng-click="showRemoteNeed(deviceCfg)">{{completion[deviceCfg.deviceID]._needItems | alwaysNumber}} <span translate>items</span>, ~{{completion[deviceCfg.deviceID]._needBytes | binary}}B</a>
<a href="" ng-click="showRemoteNeed(deviceCfg)">{{completion[deviceCfg.deviceID]._needItems | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{completion[deviceCfg.deviceID]._needBytes | binary}}B</a>
</td>
</tr>
<tr>
@@ -784,6 +784,7 @@
<script type="text/javascript" src="syncthing/core/alwaysNumberFilter.js"></script>
<script type="text/javascript" src="syncthing/core/basenameFilter.js"></script>
<script type="text/javascript" src="syncthing/core/binaryFilter.js"></script>
<script type="text/javascript" src="syncthing/core/localeNumberFilter.js"></script>
<script type="text/javascript" src="syncthing/core/durationFilter.js"></script>
<script type="text/javascript" src="syncthing/core/eventService.js"></script>
<script type="text/javascript" src="syncthing/core/identiconDirective.js"></script>
@@ -791,7 +792,6 @@
<script type="text/javascript" src="syncthing/core/lastErrorComponentFilter.js"></script>
<script type="text/javascript" src="syncthing/core/localeService.js"></script>
<script type="text/javascript" src="syncthing/core/modalDirective.js"></script>
<script type="text/javascript" src="syncthing/core/naturalFilter.js"></script>
<script type="text/javascript" src="syncthing/core/metricFilter.js"></script>
<script type="text/javascript" src="syncthing/core/notificationDirective.js"></script>
<script type="text/javascript" src="syncthing/core/pathIsSubDirDirective.js"></script>

View File

@@ -21,6 +21,7 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
var deviceIDShort = metadata.deviceID.substr(0, 5);
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + deviceIDShort;
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIDShort;
$httpProvider.useApplyAsync(true);
// language and localisation
@@ -82,18 +83,6 @@ function folderList(m) {
return l;
}
function decimals(val, num) {
var digits, decs;
if (val === 0) {
return 0;
}
digits = Math.floor(Math.log(Math.abs(val)) / Math.log(10));
decs = Math.max(0, num - digits);
return decs;
}
function isEmptyObject(obj) {
var name;
for (name in obj) {

View File

@@ -12,7 +12,7 @@
<p translate>Copyright &copy; 2014-2017 the following Contributors:</p>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Alexander Graf, 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, Simon Frei, Stefan Tatschner, Aaron Bieber, Adam Piggott, Adel Qalieh, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Antoine Lamielle, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Graham Miln, Heiko Zuerker, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jose Manuel Delicado, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Kurt Fitzner, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mark Pulford, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nicholas Rishel, Niels Peter Roest, Pascal Jungblut, Pawel Palenica, Peter Hoeg, Phill Luby, Piotr Bejda, Robert Carosi, Roman Zaynetdinov, Ross Smith II, Sacheendra Talluri, Scott Klupfel, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tomas Cerveny, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Xavier O., Yannic A.
Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, 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., Alexandre Viau, Andrew Dunham, Andrey D, Antoine Lamielle, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Graham Miln, Harrison Jones, Heiko Zuerker, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jochen Voss, Johan Vromans, John Rinehart, Jose Manuel Delicado, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Kurt Fitzner, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mark Pulford, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nicholas Rishel, Niels Peter Roest, Nils Jakobi, Pascal Jungblut, Pawel Palenica, Peter Hoeg, Peter Marquardt, Phill Luby, Piotr Bejda, Pramodh KP, Robert Carosi, Roman Zaynetdinov, Ross Smith II, Sacheendra Talluri, Scott Klupfel, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tomas Cerveny, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Xavier O., Yannic A., xjtdy888
</div>
</div>
<hr/>

View File

@@ -6,16 +6,16 @@ angular.module('syncthing.core')
}
if (input > 1024 * 1024 * 1024) {
input /= 1024 * 1024 * 1024;
return input.toFixed(decimals(input, 2)) + ' Gi';
return input.toLocaleString(undefined, {maximumFractionDigits: 2}) + ' Gi';
}
if (input > 1024 * 1024) {
input /= 1024 * 1024;
return input.toFixed(decimals(input, 2)) + ' Mi';
return input.toLocaleString(undefined, {maximumFractionDigits: 2}) + ' Mi';
}
if (input > 1024) {
input /= 1024;
return input.toFixed(decimals(input, 2)) + ' Ki';
return input.toLocaleString(undefined, {maximumFractionDigits: 2}) + ' Ki';
}
return Math.round(input) + ' ';
return Math.round(input).toLocaleString(undefined, {maximumFractionDigits: 2}) + ' ';
};
});

View File

@@ -0,0 +1,9 @@
angular.module('syncthing.core')
.filter('localeNumber', function () {
return function (input, decimals) {
if (typeof(decimals) !== 'undefined') {
return input.toLocaleString(undefined, {maximumFractionDigits: decimals});
}
return input.toLocaleString();
};
});

View File

@@ -6,16 +6,16 @@ angular.module('syncthing.core')
}
if (input > 1000 * 1000 * 1000) {
input /= 1000 * 1000 * 1000;
return input.toFixed(decimals(input, 2)) + ' G';
return input.toLocaleString(undefined, {maximumFractionDigits: 2}) + ' G';
}
if (input > 1000 * 1000) {
input /= 1000 * 1000;
return input.toFixed(decimals(input, 2)) + ' M';
return input.toLocaleString(undefined, {maximumFractionDigits: 2}) + ' M';
}
if (input > 1000) {
input /= 1000;
return input.toFixed(decimals(input, 2)) + ' k';
return input.toLocaleString(undefined, {maximumFractionDigits: 2}) + ' k';
}
return Math.round(input) + ' ';
return Math.round(input).toLocaleString(undefined, {maximumFractionDigits: 2}) + ' ';
};
});

View File

@@ -1,6 +0,0 @@
angular.module('syncthing.core')
.filter('natural', function () {
return function (input, valid) {
return input.toFixed(decimals(input, valid));
};
});

View File

@@ -2,20 +2,13 @@ angular.module('syncthing.core')
.directive('notification', function () {
return {
restrict: 'E',
scope: true,
scope: {},
transclude: true,
template: '<div class="row" ng-if="visible()"><div class="col-md-12" ng-transclude></div></div>',
link: function (scope, elm, attrs) {
scope.visible = function () {
return scope.config.options.unackedNotificationIDs.indexOf(attrs.id) > -1;
}
scope.dismiss = function () {
var idx = scope.config.options.unackedNotificationIDs.indexOf(attrs.id);
if (idx > -1) {
scope.config.options.unackedNotificationIDs.splice(idx, 1);
scope.saveConfig();
}
}
return scope.$parent.config.options.unackedNotificationIDs.indexOf(attrs.id) > -1;
};
}
};
});

View File

@@ -6,7 +6,7 @@
<p translate>This is an example notification. ID of the notification should be appended to Options.UnackedNotificationIDs of the config.</p>
</div>
<div class="panel-footer">
<button type="button" class="btn btn-sm btn-default pull-right" ng-click="dismiss()">
<button type="button" class="btn btn-sm btn-default pull-right" ng-click="dismissNotification('exampleNotification')">
<span class="fa fa-check"></span>&nbsp;<span translate>OK</span>
</button>
<div class="clearfix"></div>
@@ -31,10 +31,10 @@
<p><a href="https://docs.syncthing.net/users/releases.html"><span class="fa fa-fw fa-book"></span>&nbsp;<span translate>Learn more</span></a></p>
</div>
<div class="panel-footer">
<button type="button" class="btn btn-sm btn-default pull-right" ng-click="editSettings()">
<button type="button" class="btn btn-sm btn-default pull-right" ng-click="editSettings(); dismissNotification('channelNotification')">
<span class="fa fa-cog"></span>&nbsp;<span translate>Settings</span>
</button>
<button type="button" class="btn btn-sm btn-default" ng-click="dismiss()">
<button type="button" class="btn btn-sm btn-default" ng-click="dismissNotification('channelNotification')">
<span class="fa fa-check"></span>&nbsp;<span translate>OK</span>
</button>
<div class="clearfix"></div>
@@ -58,10 +58,10 @@
</div>
<div class="panel-footer clearfix">
<div class="pull-right">
<button type="button" class="btn btn-primary btn-sm" ng-click="activateAllFsWatchers(); dismiss()">
<button type="button" class="btn btn-primary btn-sm" ng-click="activateAllFsWatchers(); dismissNotification('fsWatcherNotification')">
<span class="fa fa-check"></span>&nbsp;<span translate>Yes</span>
</button>
<button type="button" class="btn btn-default btn-sm" ng-click="dismiss()">
<button type="button" class="btn btn-default btn-sm" ng-click="dismissNotification('fsWatcherNotification')">
<span class="fa fa-times"></span>&nbsp;<span translate>No</span>
</button>
</div>

View File

@@ -1699,6 +1699,7 @@ angular.module('syncthing.core')
delete folderCfg.versioning;
}
var ignoresLoaded = !$('#folder-ignores textarea').is(':disabled');
var ignores = $('#folder-ignores textarea').val().split('\n');
// Split always returns a minimum 1-length array even for no patterns
if (ignores.length === 1 && ignores[0] === "") {
@@ -1711,7 +1712,7 @@ angular.module('syncthing.core')
$scope.folders[folderCfg.id] = folderCfg;
$scope.config.folders = folderList($scope.folders);
if ($scope.editingExisting && ignores !== folderCfg.ignores) {
if (ignoresLoaded && $scope.editingExisting && ignores !== folderCfg.ignores) {
saveIgnores(ignores);
};
@@ -2246,4 +2247,12 @@ angular.module('syncthing.core')
$scope.sizeOf = function (dict) {
return Object.keys(dict).length;
};
$scope.dismissNotification = function (id) {
var idx = $scope.config.options.unackedNotificationIDs.indexOf(id);
if (idx > -1) {
$scope.config.options.unackedNotificationIDs.splice(idx, 1);
$scope.saveConfig();
}
};
});

View File

@@ -19,7 +19,7 @@
<td ng-if="!changeEvent.data.modifiedBy"><span translate>Unknown</span></td>
<td>{{changeEvent.data.action}}</td>
<td>{{changeEvent.data.type}}</td>
<td class="no-overflow-ellipse">{{changeEvent.data.label}}</td>
<td class="no-overflow-ellipse">{{folderLabel(changeEvent.data.folder)}}</td>
<td class="no-overflow-ellipse">{{changeEvent.data.path}}</td>
<td class="no-overflow-ellipse">{{changeEvent.time | date:"yyyy-MM-dd HH:mm:ss"}}</td>
</tr>

View File

@@ -1,6 +1,6 @@
The files contained herein are:
- angular 1.2.9
- angular 1.3.20
- angular-translate 2.9.0.1
- angular-translate-loader-static-files 2.11.0
- angular-dirPagination 759009c

View File

@@ -1,10 +1,21 @@
/**
* @license AngularJS v1.2.27
* @license AngularJS v1.3.20
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Any commits to this file should be reviewed with security in mind. *
* Changes to this file can potentially create security vulnerabilities. *
* An approval from 2 Core members with history of modifying *
* this file is required. *
* *
* Does the change somehow allow for arbitrary javascript to be executed? *
* Or allows for someone to change the prototype of built-in objects? *
* Or gives undesired access to variables likes document or window? *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
var $sanitizeMinErr = angular.$$minErr('$sanitize');
/**
@@ -45,16 +56,16 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
* @kind function
*
* @description
* The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
* The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are
* then serialized back to properly escaped html string. This means that no unsafe input can make
* it into the returned string, however, since our parser is more strict than a typical browser
* parser, it's possible that some obscure input, which would be recognized as valid HTML by a
* browser, won't make it through the sanitizer.
* browser, won't make it through the sanitizer. The input may also contain SVG markup.
* The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
* `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
*
* @param {string} html Html input.
* @returns {string} Sanitized html.
* @param {string} html HTML input.
* @returns {string} Sanitized HTML.
*
* @example
<example module="sanitizeExample" deps="angular-sanitize.js">
@@ -198,6 +209,12 @@ var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a
"bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
"samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
// SVG Elements
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
var svgElements = makeMap("animate,animateColor,animateMotion,animateTransform,circle,defs," +
"desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient," +
"line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set," +
"stop,svg,switch,text,title,tspan,use");
// Special Elements (can contain anything)
var specialElements = makeMap("script,style");
@@ -206,16 +223,41 @@ var validElements = angular.extend({},
voidElements,
blockElements,
inlineElements,
optionalEndTagElements);
optionalEndTagElements,
svgElements);
//Attributes that have href and hence need to be sanitized
var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
var validAttrs = angular.extend({}, uriAttrs, makeMap(
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
'valign,value,vspace,width'));
var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap,xlink:href");
var htmlAttrs = makeMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
'scope,scrolling,shape,size,span,start,summary,target,title,type,' +
'valign,value,vspace,width');
// SVG attributes (without "id" and "name" attributes)
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
var svgAttrs = makeMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
'attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,' +
'color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,' +
'font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,' +
'gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,' +
'keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,' +
'markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,' +
'overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,' +
'repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,' +
'stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,' +
'stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,' +
'stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,' +
'underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,' +
'viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,' +
'xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,' +
'zoomAndPan');
var validAttrs = angular.extend({},
uriAttrs,
svgAttrs,
htmlAttrs);
function makeMap(str) {
var obj = {}, items = str.split(','), i;
@@ -236,7 +278,7 @@ function makeMap(str) {
* @param {string} html string
* @param {object} handler
*/
function htmlParser( html, handler ) {
function htmlParser(html, handler) {
if (typeof html !== 'string') {
if (html === null || typeof html === 'undefined') {
html = '';
@@ -245,52 +287,52 @@ function htmlParser( html, handler ) {
}
}
var index, chars, match, stack = [], last = html, text;
stack.last = function() { return stack[ stack.length - 1 ]; };
stack.last = function() { return stack[stack.length - 1]; };
while ( html ) {
while (html) {
text = '';
chars = true;
// Make sure we're not in a script or style element
if ( !stack.last() || !specialElements[ stack.last() ] ) {
if (!stack.last() || !specialElements[stack.last()]) {
// Comment
if ( html.indexOf("<!--") === 0 ) {
if (html.indexOf("<!--") === 0) {
// comments containing -- are not allowed unless they terminate the comment
index = html.indexOf("--", 4);
if ( index >= 0 && html.lastIndexOf("-->", index) === index) {
if (handler.comment) handler.comment( html.substring( 4, index ) );
html = html.substring( index + 3 );
if (index >= 0 && html.lastIndexOf("-->", index) === index) {
if (handler.comment) handler.comment(html.substring(4, index));
html = html.substring(index + 3);
chars = false;
}
// DOCTYPE
} else if ( DOCTYPE_REGEXP.test(html) ) {
match = html.match( DOCTYPE_REGEXP );
} else if (DOCTYPE_REGEXP.test(html)) {
match = html.match(DOCTYPE_REGEXP);
if ( match ) {
html = html.replace( match[0], '');
if (match) {
html = html.replace(match[0], '');
chars = false;
}
// end tag
} else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
match = html.match( END_TAG_REGEXP );
} else if (BEGING_END_TAGE_REGEXP.test(html)) {
match = html.match(END_TAG_REGEXP);
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( END_TAG_REGEXP, parseEndTag );
if (match) {
html = html.substring(match[0].length);
match[0].replace(END_TAG_REGEXP, parseEndTag);
chars = false;
}
// start tag
} else if ( BEGIN_TAG_REGEXP.test(html) ) {
match = html.match( START_TAG_REGEXP );
} else if (BEGIN_TAG_REGEXP.test(html)) {
match = html.match(START_TAG_REGEXP);
if ( match ) {
if (match) {
// We only have a valid start-tag if there is a '>'.
if ( match[4] ) {
html = html.substring( match[0].length );
match[0].replace( START_TAG_REGEXP, parseStartTag );
if (match[4]) {
html = html.substring(match[0].length);
match[0].replace(START_TAG_REGEXP, parseStartTag);
}
chars = false;
} else {
@@ -300,29 +342,30 @@ function htmlParser( html, handler ) {
}
}
if ( chars ) {
if (chars) {
index = html.indexOf("<");
text += index < 0 ? html : html.substring( 0, index );
html = index < 0 ? "" : html.substring( index );
text += index < 0 ? html : html.substring(0, index);
html = index < 0 ? "" : html.substring(index);
if (handler.chars) handler.chars( decodeEntities(text) );
if (handler.chars) handler.chars(decodeEntities(text));
}
} else {
html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
function(all, text){
// IE versions 9 and 10 do not understand the regex '[^]', so using a workaround with [\W\w].
html = html.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
function(all, text) {
text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
if (handler.chars) handler.chars( decodeEntities(text) );
if (handler.chars) handler.chars(decodeEntities(text));
return "";
});
parseEndTag( "", stack.last() );
parseEndTag("", stack.last());
}
if ( html == last ) {
if (html == last) {
throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
"of html: {0}", html);
}
@@ -332,22 +375,22 @@ function htmlParser( html, handler ) {
// Clean up any remaining tags
parseEndTag();
function parseStartTag( tag, tagName, rest, unary ) {
function parseStartTag(tag, tagName, rest, unary) {
tagName = angular.lowercase(tagName);
if ( blockElements[ tagName ] ) {
while ( stack.last() && inlineElements[ stack.last() ] ) {
parseEndTag( "", stack.last() );
if (blockElements[tagName]) {
while (stack.last() && inlineElements[stack.last()]) {
parseEndTag("", stack.last());
}
}
if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
parseEndTag( "", tagName );
if (optionalEndTagElements[tagName] && stack.last() == tagName) {
parseEndTag("", tagName);
}
unary = voidElements[ tagName ] || !!unary;
unary = voidElements[tagName] || !!unary;
if ( !unary )
stack.push( tagName );
if (!unary)
stack.push(tagName);
var attrs = {};
@@ -360,22 +403,22 @@ function htmlParser( html, handler ) {
attrs[name] = decodeEntities(value);
});
if (handler.start) handler.start( tagName, attrs, unary );
if (handler.start) handler.start(tagName, attrs, unary);
}
function parseEndTag( tag, tagName ) {
function parseEndTag(tag, tagName) {
var pos = 0, i;
tagName = angular.lowercase(tagName);
if ( tagName )
if (tagName)
// Find the closest opened tag of the same type
for ( pos = stack.length - 1; pos >= 0; pos-- )
if ( stack[ pos ] == tagName )
for (pos = stack.length - 1; pos >= 0; pos--)
if (stack[pos] == tagName)
break;
if ( pos >= 0 ) {
if (pos >= 0) {
// Close all the open elements, up the stack
for ( i = stack.length - 1; i >= pos; i-- )
if (handler.end) handler.end( stack[ i ] );
for (i = stack.length - 1; i >= pos; i--)
if (handler.end) handler.end(stack[i]);
// Remove the open elements from the stack
stack.length = pos;
@@ -384,7 +427,6 @@ function htmlParser( html, handler ) {
}
var hiddenPre=document.createElement("pre");
var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
/**
* decodes all entities into regular string
* @param value
@@ -393,22 +435,10 @@ var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
function decodeEntities(value) {
if (!value) { return ''; }
// Note: IE8 does not preserve spaces at the start/end of innerHTML
// so we must capture them and reattach them afterward
var parts = spaceRe.exec(value);
var spaceBefore = parts[1];
var spaceAfter = parts[3];
var content = parts[2];
if (content) {
hiddenPre.innerHTML=content.replace(/</g,"&lt;");
// innerText depends on styling as it doesn't display hidden elements.
// Therefore, it's better to use textContent not to cause unnecessary
// reflows. However, IE<9 don't support textContent so the innerText
// fallback is necessary.
content = 'textContent' in hiddenPre ?
hiddenPre.textContent : hiddenPre.innerText;
}
return spaceBefore + content + spaceAfter;
hiddenPre.innerHTML = value.replace(/</g,"&lt;");
// innerText depends on styling as it doesn't display hidden elements.
// Therefore, it's better to use textContent not to cause unnecessary reflows.
return hiddenPre.textContent;
}
/**
@@ -421,12 +451,12 @@ function decodeEntities(value) {
function encodeEntities(value) {
return value.
replace(/&/g, '&amp;').
replace(SURROGATE_PAIR_REGEXP, function (value) {
replace(SURROGATE_PAIR_REGEXP, function(value) {
var hi = value.charCodeAt(0);
var low = value.charCodeAt(1);
return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
}).
replace(NON_ALPHANUMERIC_REGEXP, function(value){
replace(NON_ALPHANUMERIC_REGEXP, function(value) {
return '&#' + value.charCodeAt(0) + ';';
}).
replace(/</g, '&lt;').
@@ -443,11 +473,11 @@ function encodeEntities(value) {
* comment: function(text) {}
* }
*/
function htmlSanitizeWriter(buf, uriValidator){
function htmlSanitizeWriter(buf, uriValidator) {
var ignore = false;
var out = angular.bind(buf, buf.push);
return {
start: function(tag, attrs, unary){
start: function(tag, attrs, unary) {
tag = angular.lowercase(tag);
if (!ignore && specialElements[tag]) {
ignore = tag;
@@ -455,7 +485,7 @@ function htmlSanitizeWriter(buf, uriValidator){
if (!ignore && validElements[tag] === true) {
out('<');
out(tag);
angular.forEach(attrs, function(value, key){
angular.forEach(attrs, function(value, key) {
var lkey=angular.lowercase(key);
var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
if (validAttrs[lkey] === true &&
@@ -470,7 +500,7 @@ function htmlSanitizeWriter(buf, uriValidator){
out(unary ? '/>' : '>');
}
},
end: function(tag){
end: function(tag) {
tag = angular.lowercase(tag);
if (!ignore && validElements[tag] === true) {
out('</');
@@ -481,7 +511,7 @@ function htmlSanitizeWriter(buf, uriValidator){
ignore = false;
}
},
chars: function(chars){
chars: function(chars) {
if (!ignore) {
out(encodeEntities(chars));
}
@@ -597,8 +627,8 @@ angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
*/
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP =
/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,
MAILTO_REGEXP = /^mailto:/;
/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/i,
MAILTO_REGEXP = /^mailto:/i;
return function(text, target) {
if (!text) return text;
@@ -610,8 +640,10 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
while ((match = raw.match(LINKY_URL_REGEXP))) {
// We can not end in these as they are sometimes found at the end of the sentence
url = match[0];
// if we did not match ftp/http/mailto then assume mailto
if (match[2] == match[3]) url = 'mailto:' + url;
// if we did not match ftp/http/www/mailto then assume mailto
if (!match[2] && !match[4]) {
url = (match[3] ? 'http://' : 'mailto:') + url;
}
i = match.index;
addText(raw.substr(0, i));
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
@@ -630,13 +662,13 @@ angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
function addLink(url, text) {
html.push('<a ');
if (angular.isDefined(target)) {
html.push('target="');
html.push(target);
html.push('" ');
html.push('target="',
target,
'" ');
}
html.push('href="');
html.push(url);
html.push('">');
html.push('href="',
url.replace(/"/g, '&quot;'),
'">');
addText(text);
html.push('</a>');
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -325,13 +325,18 @@ func (cfg *Configuration) clean() error {
existingDevices[device.DeviceID] = true
}
// Ensure that the device list is free from duplicates
// Ensure that the device list is
// - free from duplicates
// - sorted by ID
cfg.Devices = ensureNoDuplicateDevices(cfg.Devices)
sort.Sort(DeviceConfigurationList(cfg.Devices))
// Ensure that any loose devices are not present in the wrong places
// Ensure that there are no duplicate devices
// Ensure that the versioning configuration parameter map is not nil
// Ensure that the folder list is sorted by ID
sort.Sort(FolderConfigurationList(cfg.Folders))
// Ensure that in all folder configs
// - any loose devices are not present in the wrong places
// - there are no duplicate devices
// - the versioning configuration parameter map is not nil
for i := range cfg.Folders {
cfg.Folders[i].Devices = ensureExistingDevices(cfg.Folders[i].Devices, existingDevices)
cfg.Folders[i].Devices = ensureNoDuplicateFolderDevices(cfg.Folders[i].Devices)

View File

@@ -67,7 +67,6 @@ func TestDefaultValues(t *testing.T) {
OverwriteRemoteDevNames: false,
TempIndexMinBlocks: 10,
UnackedNotificationIDs: []string{},
WeakHashSelectionMethod: WeakHashAuto,
DefaultFolderPath: "~",
SetLowPriority: true,
}
@@ -209,9 +208,8 @@ func TestOverriddenValues(t *testing.T) {
"channelNotification", // added in 17->18 migration
"fsWatcherNotification", // added in 27->28 migration
},
WeakHashSelectionMethod: WeakHashNever,
DefaultFolderPath: "/media/syncthing",
SetLowPriority: false,
DefaultFolderPath: "/media/syncthing",
SetLowPriority: false,
}
os.Unsetenv("STNOUPGRADE")

View File

@@ -52,6 +52,7 @@ type FolderConfiguration struct {
Paused bool `xml:"paused" json:"paused"`
WeakHashThresholdPct int `xml:"weakHashThresholdPct" json:"weakHashThresholdPct"` // Use weak hash if more than X percent of the file has changed. Set to -1 to always use weak hash.
MarkerName string `xml:"markerName" json:"markerName"`
UseLargeBlocks bool `xml:"useLargeBlocks" json:"useLargeBlocks"`
cachedFilesystem fs.Filesystem
@@ -277,3 +278,17 @@ func (l FolderDeviceConfigurationList) Len() int {
func (f *FolderConfiguration) CheckFreeSpace() (err error) {
return checkFreeSpace(f.MinDiskFree, f.Filesystem())
}
type FolderConfigurationList []FolderConfiguration
func (l FolderConfigurationList) Len() int {
return len(l)
}
func (l FolderConfigurationList) Less(a, b int) bool {
return l[a].ID < l[b].ID
}
func (l FolderConfigurationList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}

View File

@@ -7,135 +7,50 @@
package config
import (
"encoding/json"
"encoding/xml"
"fmt"
"github.com/syncthing/syncthing/lib/util"
)
type WeakHashSelectionMethod int
const (
WeakHashAuto WeakHashSelectionMethod = iota
WeakHashAlways
WeakHashNever
)
func (m WeakHashSelectionMethod) MarshalString() (string, error) {
switch m {
case WeakHashAuto:
return "auto", nil
case WeakHashAlways:
return "always", nil
case WeakHashNever:
return "never", nil
default:
return "", fmt.Errorf("unrecognized hash selection method")
}
}
func (m WeakHashSelectionMethod) String() string {
s, err := m.MarshalString()
if err != nil {
panic(err)
}
return s
}
func (m *WeakHashSelectionMethod) UnmarshalString(value string) error {
switch value {
case "auto":
*m = WeakHashAuto
return nil
case "always":
*m = WeakHashAlways
return nil
case "never":
*m = WeakHashNever
return nil
}
return fmt.Errorf("unrecognized hash selection method")
}
func (m WeakHashSelectionMethod) MarshalJSON() ([]byte, error) {
val, err := m.MarshalString()
if err != nil {
return nil, err
}
return json.Marshal(val)
}
func (m *WeakHashSelectionMethod) UnmarshalJSON(data []byte) error {
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
return m.UnmarshalString(value)
}
func (m WeakHashSelectionMethod) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
val, err := m.MarshalString()
if err != nil {
return err
}
return e.EncodeElement(val, start)
}
func (m *WeakHashSelectionMethod) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var value string
if err := d.DecodeElement(&value, &start); err != nil {
return err
}
return m.UnmarshalString(value)
}
func (WeakHashSelectionMethod) ParseDefault(value string) (interface{}, error) {
var m WeakHashSelectionMethod
err := m.UnmarshalString(value)
return m, err
}
type OptionsConfiguration struct {
ListenAddresses []string `xml:"listenAddress" json:"listenAddresses" default:"default"`
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default" restart:"true"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true" restart:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true" restart:"true"`
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027" restart:"true"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027" restart:"true"`
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
NATEnabled bool `xml:"natEnabled" json:"natEnabled" default:"true"`
NATLeaseM int `xml:"natLeaseMinutes" json:"natLeaseMinutes" default:"60"`
NATRenewalM int `xml:"natRenewalMinutes" json:"natRenewalMinutes" default:"30"`
NATTimeoutS int `xml:"natTimeoutSeconds" json:"natTimeoutSeconds" default:"10"`
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
URSeen int `xml:"urSeen" json:"urSeen"` // Report which the user has been prompted for.
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
URPostInsecurely bool `xml:"urPostInsecurely" json:"urPostInsecurely" default:"false"` // For testing
URInitialDelayS int `xml:"urInitialDelayS" json:"urInitialDelayS" default:"1800"`
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true" restart:"true"`
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12" restart:"true"` // 0 for off
UpgradeToPreReleases bool `xml:"upgradeToPreReleases" json:"upgradeToPreReleases" restart:"true"` // when auto upgrades are enabled
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"false" restart:"true"`
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
MinHomeDiskFree Size `xml:"minHomeDiskFree" json:"minHomeDiskFree" default:"1 %"`
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://upgrades.syncthing.net/meta.json" restart:"true"`
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
OverwriteRemoteDevNames bool `xml:"overwriteRemoteDeviceNamesOnConnect" json:"overwriteRemoteDeviceNamesOnConnect" default:"false"`
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
UnackedNotificationIDs []string `xml:"unackedNotificationID" json:"unackedNotificationIDs"`
TrafficClass int `xml:"trafficClass" json:"trafficClass"`
WeakHashSelectionMethod WeakHashSelectionMethod `xml:"weakHashSelectionMethod" json:"weakHashSelectionMethod" restart:"true"`
DefaultFolderPath string `xml:"defaultFolderPath" json:"defaultFolderPath" default:"~"`
SetLowPriority bool `xml:"setLowPriority" json:"setLowPriority" default:"true"`
ListenAddresses []string `xml:"listenAddress" json:"listenAddresses" default:"default"`
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default" restart:"true"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true" restart:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true" restart:"true"`
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027" restart:"true"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff12::8384]:21027" restart:"true"`
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
NATEnabled bool `xml:"natEnabled" json:"natEnabled" default:"true"`
NATLeaseM int `xml:"natLeaseMinutes" json:"natLeaseMinutes" default:"60"`
NATRenewalM int `xml:"natRenewalMinutes" json:"natRenewalMinutes" default:"30"`
NATTimeoutS int `xml:"natTimeoutSeconds" json:"natTimeoutSeconds" default:"10"`
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
URSeen int `xml:"urSeen" json:"urSeen"` // Report which the user has been prompted for.
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
URPostInsecurely bool `xml:"urPostInsecurely" json:"urPostInsecurely" default:"false"` // For testing
URInitialDelayS int `xml:"urInitialDelayS" json:"urInitialDelayS" default:"1800"`
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true" restart:"true"`
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12" restart:"true"` // 0 for off
UpgradeToPreReleases bool `xml:"upgradeToPreReleases" json:"upgradeToPreReleases" restart:"true"` // when auto upgrades are enabled
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"false" restart:"true"`
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
MinHomeDiskFree Size `xml:"minHomeDiskFree" json:"minHomeDiskFree" default:"1 %"`
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://upgrades.syncthing.net/meta.json" restart:"true"`
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
OverwriteRemoteDevNames bool `xml:"overwriteRemoteDeviceNamesOnConnect" json:"overwriteRemoteDeviceNamesOnConnect" default:"false"`
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
UnackedNotificationIDs []string `xml:"unackedNotificationID" json:"unackedNotificationIDs"`
TrafficClass int `xml:"trafficClass" json:"trafficClass"`
DefaultFolderPath string `xml:"defaultFolderPath" json:"defaultFolderPath" default:"~"`
SetLowPriority bool `xml:"setLowPriority" json:"setLowPriority" default:"true"`
DeprecatedUPnPEnabled bool `xml:"upnpEnabled,omitempty" json:"-"`
DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes,omitempty" json:"-"`

View File

@@ -34,7 +34,6 @@
<releasesURL>https://localhost/releases</releasesURL>
<overwriteRemoteDeviceNamesOnConnect>true</overwriteRemoteDeviceNamesOnConnect>
<tempIndexMinBlocks>100</tempIndexMinBlocks>
<weakHashSelectionMethod>never</weakHashSelectionMethod>
<defaultFolderPath>/media/syncthing</defaultFolderPath>
<setLowPriority>false</setLowPriority>
</options>

View File

@@ -267,6 +267,13 @@ func (w *Wrapper) Folders() map[string]FolderConfiguration {
return w.folderMap
}
// FolderList returns a slice of folders.
func (w *Wrapper) FolderList() []FolderConfiguration {
w.mut.Lock()
defer w.mut.Unlock()
return append([]FolderConfiguration(nil), w.cfg.Folders...)
}
// SetFolder adds a new folder to the configuration, or overwrites an existing
// folder with the same ID.
func (w *Wrapper) SetFolder(fld FolderConfiguration) (Waiter, error) {

View File

@@ -182,19 +182,6 @@ func (f *BlockFinder) Iterate(folders []string, hash []byte, iterFn func(string,
return false
}
// Fix repairs incorrect blockmap entries, removing the old entry and
// replacing it with a new entry for the given block
func (f *BlockFinder) Fix(folder, file string, index int32, oldHash, newHash []byte) error {
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, uint32(index))
folderID := f.db.folderIdx.ID([]byte(folder))
batch := new(leveldb.Batch)
batch.Delete(blockKeyInto(nil, oldHash, folderID, file))
batch.Put(blockKeyInto(nil, newHash, folderID, file), buf)
return f.db.Write(batch, nil)
}
// m.blockKey returns a byte slice encoding the following information:
// keyTypeBlock (1 byte)
// folder (4 bytes)

View File

@@ -219,34 +219,3 @@ func TestBlockFinderLookup(t *testing.T) {
f1.Deleted = false
}
func TestBlockFinderFix(t *testing.T) {
db, f := setup()
iterFn := func(folder, file string, index int32) bool {
return true
}
m := NewBlockMap(db, db.folderIdx.ID([]byte("folder1")))
err := m.Add([]protocol.FileInfo{f1})
if err != nil {
t.Fatal(err)
}
if !f.Iterate(folders, f1.Blocks[0].Hash, iterFn) {
t.Fatal("Block not found")
}
err = f.Fix("folder1", f1.Name, 0, f1.Blocks[0].Hash, f2.Blocks[0].Hash)
if err != nil {
t.Fatal(err)
}
if f.Iterate(folders, f1.Blocks[0].Hash, iterFn) {
t.Fatal("Unexpected block")
}
if !f.Iterate(folders, f2.Blocks[0].Hash, iterFn) {
t.Fatal("Block not found")
}
}

View File

@@ -11,11 +11,9 @@ import (
"fmt"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
)
const dbVersion = 1
const dbVersion = 2
const (
KeyTypeDevice = iota
@@ -29,6 +27,7 @@ const (
KeyTypeIndexID
KeyTypeFolderMeta
KeyTypeMiscData
KeyTypeSequence
)
func (l VersionList) String() string {
@@ -60,28 +59,5 @@ func (l fileList) Less(a, b int) bool {
return l[a].Name < l[b].Name
}
type dbReader interface {
Get([]byte, *opt.ReadOptions) ([]byte, error)
}
// Flush batches to disk when they contain this many records.
const batchFlushSize = 64
func getFile(db dbReader, key []byte) (protocol.FileInfo, bool) {
bs, err := db.Get(key, nil)
if err == leveldb.ErrNotFound {
return protocol.FileInfo{}, false
}
if err != nil {
l.Debugln("surprise error:", err)
return protocol.FileInfo{}, false
}
var f protocol.FileInfo
err = f.Unmarshal(bs)
if err != nil {
l.Debugln("unmarshal error:", err)
return protocol.FileInfo{}, false
}
return f, true
}

View File

@@ -35,10 +35,13 @@ type Instance struct {
}
const (
keyPrefixLen = 1
keyFolderLen = 4 // indexed
keyDeviceLen = 4 // indexed
keyHashLen = 32
keyPrefixLen = 1
keyFolderLen = 4 // indexed
keyDeviceLen = 4 // indexed
keySequenceLen = 8
keyHashLen = 32
maxInt64 int64 = 1<<63 - 1
)
func Open(file string) (*Instance, error) {
@@ -88,13 +91,21 @@ func (db *Instance) UpdateSchema() {
miscDB := NewNamespacedKV(db, string(KeyTypeMiscData))
prevVersion, _ := miscDB.Int64("dbVersion")
if prevVersion >= dbVersion {
return
}
l.Infof("Updating database schema version from %v to %v...", prevVersion, dbVersion)
if prevVersion == 0 {
db.updateSchema0to1()
}
if prevVersion != dbVersion {
miscDB.PutInt64("dbVersion", dbVersion)
if prevVersion <= 1 {
db.updateSchema1to2()
}
l.Infof("Finished updating database schema version from %v to %v", prevVersion, dbVersion)
miscDB.PutInt64("dbVersion", dbVersion)
}
// Committed returns the number of items committed to the database since startup
@@ -112,9 +123,10 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, m
defer t.close()
var fk []byte
var gk []byte
for _, f := range fs {
name := []byte(f.Name)
fk = db.deviceKeyInto(fk[:cap(fk)], folder, device, name)
fk = db.deviceKeyInto(fk, folder, device, name)
// Get and unmarshal the file entry. If it doesn't exist or can't be
// unmarshalled we'll add it as a new entry.
@@ -135,8 +147,10 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, m
}
meta.addFile(devID, f)
t.insertFile(folder, device, f)
t.updateGlobal(folder, device, f, meta)
t.insertFile(fk, folder, device, f)
gk = db.globalKeyInto(gk, folder, name)
t.updateGlobal(gk, folder, device, f, meta)
// Write out and reuse the batch every few records, to avoid the batch
// growing too large and thus allocating unnecessarily much memory.
@@ -144,6 +158,33 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, m
}
}
func (db *Instance) addSequences(folder []byte, fs []protocol.FileInfo) {
t := db.newReadWriteTransaction()
defer t.close()
var sk []byte
var dk []byte
for _, f := range fs {
sk = db.sequenceKeyInto(sk, folder, f.Sequence)
dk = db.deviceKeyInto(dk, folder, protocol.LocalDeviceID[:], []byte(f.Name))
t.Put(sk, dk)
l.Debugf("adding sequence; folder=%q sequence=%v %v", folder, f.Sequence, f.Name)
t.checkFlush()
}
}
func (db *Instance) removeSequences(folder []byte, fs []protocol.FileInfo) {
t := db.newReadWriteTransaction()
defer t.close()
var sk []byte
for _, f := range fs {
t.Delete(db.sequenceKeyInto(sk, folder, f.Sequence))
l.Debugf("removing sequence; folder=%q sequence=%v %v", folder, f.Sequence, f.Name)
t.checkFlush()
}
}
func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) {
t := db.newReadOnlyTransaction()
defer t.close()
@@ -171,7 +212,26 @@ func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn It
l.Debugln("unmarshal error:", err)
continue
}
if cont := fn(f); !cont {
if !fn(f) {
return
}
}
}
func (db *Instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator) {
t := db.newReadOnlyTransaction()
defer t.close()
dbi := t.NewIterator(&util.Range{Start: db.sequenceKey(folder, startSeq), Limit: db.sequenceKey(folder, maxInt64)}, nil)
defer dbi.Release()
for dbi.Next() {
f, ok := db.getFile(dbi.Value())
if !ok {
l.Debugln("missing file for sequence number", db.sequenceKeySequence(dbi.Key()))
continue
}
if !fn(f) {
return
}
}
@@ -184,6 +244,8 @@ func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte,
dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, nil, nil)[:keyPrefixLen+keyFolderLen]), nil)
defer dbi.Release()
var gk []byte
for dbi.Next() {
device := db.deviceKeyDevice(dbi.Key())
var f FileInfoTruncated
@@ -200,20 +262,37 @@ func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte,
switch f.Name {
case "", ".", "..", "/": // A few obviously invalid filenames
l.Infof("Dropping invalid filename %q from database", f.Name)
t.removeFromGlobal(folder, device, nil, nil)
name := []byte(f.Name)
gk = db.globalKeyInto(gk, folder, name)
t.removeFromGlobal(gk, folder, device, name, nil)
t.Delete(dbi.Key())
t.checkFlush()
continue
}
if cont := fn(device, f); !cont {
if !fn(device, f) {
return
}
}
}
func (db *Instance) getFile(folder, device, file []byte) (protocol.FileInfo, bool) {
return getFile(db, db.deviceKey(folder, device, file))
func (db *Instance) getFile(key []byte) (protocol.FileInfo, bool) {
bs, err := db.Get(key, nil)
if err == leveldb.ErrNotFound {
return protocol.FileInfo{}, false
}
if err != nil {
l.Debugln("surprise error:", err)
return protocol.FileInfo{}, false
}
var f protocol.FileInfo
err = f.Unmarshal(bs)
if err != nil {
l.Debugln("unmarshal error:", err)
return protocol.FileInfo{}, false
}
return f, true
}
func (db *Instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, bool) {
@@ -286,7 +365,7 @@ func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator
return
}
fk = db.deviceKeyInto(fk[:cap(fk)], folder, vl.Versions[0].Device, name)
fk = db.deviceKeyInto(fk, folder, vl.Versions[0].Device, name)
bs, err := t.Get(fk, nil)
if err != nil {
l.Debugln("surprise error:", err)
@@ -299,7 +378,7 @@ func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator
continue
}
if cont := fn(f); !cont {
if !fn(f) {
return
}
}
@@ -392,7 +471,7 @@ func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator)
continue
}
fk = db.deviceKeyInto(fk[:cap(fk)], folder, vl.Versions[i].Device, name)
fk = db.deviceKeyInto(fk, folder, vl.Versions[i].Device, name)
bs, err := t.Get(fk, nil)
if err != nil {
l.Debugln("surprise error:", err)
@@ -412,7 +491,7 @@ func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator)
l.Debugf("need folder=%q device=%v name=%q need=%v have=%v invalid=%v haveV=%v globalV=%v globalDev=%v", folder, protocol.DeviceIDFromBytes(device), name, need, have, haveFileVersion.Invalid, haveFileVersion.Version, needVersion, needDevice)
if cont := fn(gf); !cont {
if !fn(gf) {
return
}
@@ -460,6 +539,14 @@ func (db *Instance) dropFolder(folder []byte) {
}
dbi.Release()
// Remove all sequences related to the folder
sequenceKey := db.sequenceKey([]byte(folder), 0)
dbi = t.NewIterator(util.BytesPrefix(sequenceKey[:keyPrefixLen+keyFolderLen]), nil)
for dbi.Next() {
db.Delete(dbi.Key(), nil)
}
dbi.Release()
// Remove all items related to the given folder from the global bucket
dbi = t.NewIterator(util.BytesPrefix([]byte{KeyTypeGlobal}), nil)
for dbi.Next() {
@@ -478,10 +565,13 @@ func (db *Instance) dropDeviceFolder(device, folder []byte, meta *metadataTracke
dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, device, nil)), nil)
defer dbi.Release()
var gk []byte
for dbi.Next() {
key := dbi.Key()
name := db.deviceKeyName(key)
t.removeFromGlobal(folder, device, name, meta)
gk = db.globalKeyInto(gk, folder, name)
t.removeFromGlobal(gk, folder, device, name, meta)
t.Delete(key)
t.checkFlush()
}
@@ -512,8 +602,7 @@ func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) {
name := db.globalKeyName(gk)
var newVL VersionList
for i, version := range vl.Versions {
fk = db.deviceKeyInto(fk[:cap(fk)], folder, version.Device, name)
fk = db.deviceKeyInto(fk, folder, version.Device, name)
_, err := t.Get(fk, nil)
if err == leveldb.ErrNotFound {
continue
@@ -525,7 +614,7 @@ func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) {
newVL.Versions = append(newVL.Versions, version)
if i == 0 {
if fi, ok := t.getFile(folder, version.Device, name); ok {
if fi, ok := db.getFile(fk); ok {
meta.addFile(globalDeviceID, fi)
}
}
@@ -550,18 +639,20 @@ func (db *Instance) updateSchema0to1() {
changedFolders := make(map[string]struct{})
ignAdded := 0
meta := newMetadataTracker() // dummy metadata tracker
var gk []byte
for dbi.Next() {
folder := db.deviceKeyFolder(dbi.Key())
device := db.deviceKeyDevice(dbi.Key())
name := string(db.deviceKeyName(dbi.Key()))
name := db.deviceKeyName(dbi.Key())
// Remove files with absolute path (see #4799)
if strings.HasPrefix(name, "/") {
if strings.HasPrefix(string(name), "/") {
if _, ok := changedFolders[string(folder)]; !ok {
changedFolders[string(folder)] = struct{}{}
}
t.removeFromGlobal(folder, device, nil, nil)
gk = db.globalKeyInto(gk, folder, name)
t.removeFromGlobal(gk, folder, device, nil, nil)
t.Delete(dbi.Key())
t.checkFlush()
continue
@@ -590,7 +681,8 @@ func (db *Instance) updateSchema0to1() {
// Add invalid files to global list
if f.Invalid {
if t.updateGlobal(folder, device, f, meta) {
gk = db.globalKeyInto(gk, folder, name)
if t.updateGlobal(gk, folder, device, f, meta) {
if _, ok := changedFolders[string(folder)]; !ok {
changedFolders[string(folder)] = struct{}{}
}
@@ -606,6 +698,25 @@ func (db *Instance) updateSchema0to1() {
l.Infof("Updated symlink type for %d index entries and added %d invalid files to global list", symlinkConv, ignAdded)
}
func (db *Instance) updateSchema1to2() {
t := db.newReadWriteTransaction()
defer t.close()
var sk []byte
var dk []byte
for _, folderStr := range db.ListFolders() {
folder := []byte(folderStr)
db.withHave(folder, protocol.LocalDeviceID[:], nil, true, func(f FileIntf) bool {
sk = db.sequenceKeyInto(sk, folder, f.SequenceNo())
dk = db.deviceKeyInto(dk, folder, protocol.LocalDeviceID[:], []byte(f.FileName()))
t.Put(sk, dk)
t.checkFlush()
return true
})
}
}
// deviceKey returns a byte slice encoding the following information:
// keyTypeDevice (1 byte)
// folder (4 bytes)
@@ -615,16 +726,14 @@ func (db *Instance) deviceKey(folder, device, file []byte) []byte {
return db.deviceKeyInto(nil, folder, device, file)
}
func (db *Instance) deviceKeyInto(k []byte, folder, device, file []byte) []byte {
func (db *Instance) deviceKeyInto(k, folder, device, file []byte) []byte {
reqLen := keyPrefixLen + keyFolderLen + keyDeviceLen + len(file)
if len(k) < reqLen {
k = make([]byte, reqLen)
}
k = resize(k, reqLen)
k[0] = KeyTypeDevice
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.folderIdx.ID(folder))
binary.BigEndian.PutUint32(k[keyPrefixLen+keyFolderLen:], db.deviceIdx.ID(device))
copy(k[keyPrefixLen+keyFolderLen+keyDeviceLen:], file)
return k[:reqLen]
return k
}
// deviceKeyName returns the device ID from the key
@@ -655,11 +764,16 @@ func (db *Instance) deviceKeyDevice(key []byte) []byte {
// folder (4 bytes)
// name (variable size)
func (db *Instance) globalKey(folder, file []byte) []byte {
k := make([]byte, keyPrefixLen+keyFolderLen+len(file))
k[0] = KeyTypeGlobal
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.folderIdx.ID(folder))
copy(k[keyPrefixLen+keyFolderLen:], file)
return k
return db.globalKeyInto(nil, folder, file)
}
func (db *Instance) globalKeyInto(gk, folder, file []byte) []byte {
reqLen := keyPrefixLen + keyFolderLen + len(file)
gk = resize(gk, reqLen)
gk[0] = KeyTypeGlobal
binary.BigEndian.PutUint32(gk[keyPrefixLen:], db.folderIdx.ID(folder))
copy(gk[keyPrefixLen+keyFolderLen:], file)
return gk[:reqLen]
}
// globalKeyName returns the filename from the key
@@ -672,6 +786,28 @@ func (db *Instance) globalKeyFolder(key []byte) ([]byte, bool) {
return db.folderIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
}
// sequenceKey returns a byte slice encoding the following information:
// KeyTypeSequence (1 byte)
// folder (4 bytes)
// sequence number (8 bytes)
func (db *Instance) sequenceKey(folder []byte, seq int64) []byte {
return db.sequenceKeyInto(nil, folder, seq)
}
func (db *Instance) sequenceKeyInto(k []byte, folder []byte, seq int64) []byte {
reqLen := keyPrefixLen + keyFolderLen + keySequenceLen
k = resize(k, reqLen)
k[0] = KeyTypeSequence
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.folderIdx.ID(folder))
binary.BigEndian.PutUint64(k[keyPrefixLen+keyFolderLen:], uint64(seq))
return k[:reqLen]
}
// sequenceKeySequence returns the sequence number from the key
func (db *Instance) sequenceKeySequence(key []byte) int64 {
return int64(binary.BigEndian.Uint64(key[keyPrefixLen+keyFolderLen:]))
}
func (db *Instance) getIndexID(device, folder []byte) protocol.IndexID {
key := db.indexIDKey(device, folder)
cur, err := db.Get(key, nil)
@@ -887,3 +1023,11 @@ func (i *smallIndex) Val(id uint32) ([]byte, bool) {
return []byte(val), true
}
// resize returns a byte array of length reqLen, reusing k if possible
func resize(k []byte, reqLen int) []byte {
if cap(k) < reqLen {
return make([]byte, reqLen)
}
return k[:reqLen]
}

View File

@@ -36,7 +36,7 @@ func (t readOnlyTransaction) close() {
}
func (t readOnlyTransaction) getFile(folder, device, file []byte) (protocol.FileInfo, bool) {
return getFile(t, t.db.deviceKey(folder, device, file))
return t.db.getFile(t.db.deviceKey(folder, device, file))
}
// A readWriteTransaction is a readOnlyTransaction plus a batch for writes.
@@ -74,21 +74,18 @@ func (t readWriteTransaction) flush() {
atomic.AddInt64(&t.db.committed, int64(t.Batch.Len()))
}
func (t readWriteTransaction) insertFile(folder, device []byte, file protocol.FileInfo) {
func (t readWriteTransaction) insertFile(fk, folder, device []byte, file protocol.FileInfo) {
l.Debugf("insert; folder=%q device=%v %v", folder, protocol.DeviceIDFromBytes(device), file)
name := []byte(file.Name)
nk := t.db.deviceKey(folder, device, name)
t.Put(nk, mustMarshal(&file))
t.Put(fk, mustMarshal(&file))
}
// updateGlobal adds this device+version to the version list for the given
// file. If the device is already present in the list, the version is updated.
// If the file does not have an entry in the global list, it is created.
func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.FileInfo, meta *metadataTracker) bool {
func (t readWriteTransaction) updateGlobal(gk, folder, device []byte, file protocol.FileInfo, meta *metadataTracker) bool {
l.Debugf("update global; folder=%q device=%v file=%q version=%v invalid=%v", folder, protocol.DeviceIDFromBytes(device), file.Name, file.Version, file.Invalid)
name := []byte(file.Name)
gk := t.db.globalKey(folder, name)
svl, _ := t.Get(gk, nil) // skip error, we check len(svl) != 0 later
var fl VersionList
@@ -150,8 +147,7 @@ insert:
// to determine the winner.)
//
// A surprise missing file entry here is counted as a win for us.
of, ok := t.getFile(folder, fl.Versions[i].Device, name)
if !ok || file.WinsConflict(of) {
if of, ok := t.getFile(folder, fl.Versions[i].Device, name); !ok || file.WinsConflict(of) {
fl.Versions = insertVersion(fl.Versions, i, nv)
insertedAt = i
break insert
@@ -193,10 +189,9 @@ insert:
// removeFromGlobal removes the device from the global version list for the
// given file. If the version list is empty after this, the file entry is
// removed entirely.
func (t readWriteTransaction) removeFromGlobal(folder, device, file []byte, meta *metadataTracker) {
func (t readWriteTransaction) removeFromGlobal(gk, folder, device, file []byte, meta *metadataTracker) {
l.Debugf("remove from global; folder=%q device=%v file=%q", folder, protocol.DeviceIDFromBytes(device), file)
gk := t.db.globalKey(folder, file)
svl, err := t.Get(gk, nil)
if err != nil {
// We might be called to "remove" a global version that doesn't exist

View File

@@ -43,6 +43,7 @@ type FileIntf interface {
IsSymlink() bool
HasPermissionBits() bool
SequenceNo() int64
BlockSize() int
}
// The Iterator is called with either a protocol.FileInfo or a
@@ -140,8 +141,11 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
// filter slice according to https://github.com/golang/go/wiki/SliceTricks#filtering-without-allocating
oldFs := fs
fs = fs[:0]
var dk []byte
folder := []byte(s.folder)
for _, nf := range oldFs {
ef, ok := s.db.getFile([]byte(s.folder), device[:], []byte(nf.Name))
dk = s.db.deviceKeyInto(dk, folder, device[:], []byte(osutil.NormalizedFilename(nf.Name)))
ef, ok := s.db.getFile(dk)
if ok && ef.Version.Equal(nf.Version) && ef.Invalid == nf.Invalid {
continue
}
@@ -156,6 +160,8 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
}
s.blockmap.Discard(discards)
s.blockmap.Update(updates)
s.db.removeSequences(folder, discards)
s.db.addSequences(folder, updates)
}
s.db.updateFiles([]byte(s.folder), device[:], fs, s.meta)
@@ -182,6 +188,11 @@ func (s *FileSet) WithHaveTruncated(device protocol.DeviceID, fn Iterator) {
s.db.withHave([]byte(s.folder), device[:], nil, true, nativeFileIterator(fn))
}
func (s *FileSet) WithHaveSequence(startSeq int64, fn Iterator) {
l.Debugf("%s WithHaveSequence(%v)", s.folder, startSeq)
s.db.withHaveSequence([]byte(s.folder), startSeq, nativeFileIterator(fn))
}
func (s *FileSet) WithPrefixedHaveTruncated(device protocol.DeviceID, prefix string, fn Iterator) {
l.Debugf("%s WithPrefixedHaveTruncated(%v)", s.folder, device)
s.db.withHave([]byte(s.folder), device[:], []byte(osutil.NormalizedFilename(prefix)), true, nativeFileIterator(fn))
@@ -202,7 +213,7 @@ func (s *FileSet) WithPrefixedGlobalTruncated(prefix string, fn Iterator) {
}
func (s *FileSet) Get(device protocol.DeviceID, file string) (protocol.FileInfo, bool) {
f, ok := s.db.getFile([]byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file)))
f, ok := s.db.getFile(s.db.deviceKey([]byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file))))
f.Name = osutil.NativeFilename(f.Name)
return f, ok
}

View File

@@ -865,6 +865,33 @@ func TestIssue4701(t *testing.T) {
}
}
func TestWithHaveSequence(t *testing.T) {
ldb := db.OpenMemory()
folder := "test)"
s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
// The files must not be in alphabetical order
localHave := fileList{
protocol.FileInfo{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Invalid: true},
protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(7)},
protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(5), Invalid: true},
}
replace(s, protocol.LocalDeviceID, localHave)
i := 2
s.WithHaveSequence(int64(i), func(fi db.FileIntf) bool {
if f := fi.(protocol.FileInfo); !f.IsEquivalent(localHave[i-1], false, false) {
t.Fatalf("Got %v\nExpected %v", f, localHave[i-1])
}
i++
return true
})
}
func replace(fs *db.FileSet, device protocol.DeviceID, files []protocol.FileInfo) {
fs.Drop(device)
fs.Update(device, files)

View File

@@ -17,8 +17,8 @@ import (
)
func (f FileInfoTruncated) String() string {
return fmt.Sprintf("File{Name:%q, Permissions:0%o, Modified:%v, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v}",
f.Name, f.Permissions, f.ModTime(), f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions)
return fmt.Sprintf("File{Name:%q, Permissions:0%o, Modified:%v, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v, BlockSize:%d}",
f.Name, f.Permissions, f.ModTime(), f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions, f.RawBlockSize)
}
func (f FileInfoTruncated) IsDeleted() bool {
@@ -56,6 +56,13 @@ func (f FileInfoTruncated) FileSize() int64 {
return f.Size
}
func (f FileInfoTruncated) BlockSize() int {
if f.RawBlockSize == 0 {
return protocol.MinBlockSize
}
return int(f.RawBlockSize)
}
func (f FileInfoTruncated) FileName() string {
return f.Name
}
@@ -70,12 +77,13 @@ func (f FileInfoTruncated) SequenceNo() int64 {
func (f FileInfoTruncated) ConvertToInvalidFileInfo(invalidatedBy protocol.ShortID) protocol.FileInfo {
return protocol.FileInfo{
Name: f.Name,
Type: f.Type,
ModifiedS: f.ModifiedS,
ModifiedNs: f.ModifiedNs,
ModifiedBy: invalidatedBy,
Invalid: true,
Version: f.Version,
Name: f.Name,
Type: f.Type,
ModifiedS: f.ModifiedS,
ModifiedNs: f.ModifiedNs,
ModifiedBy: invalidatedBy,
Invalid: true,
Version: f.Version,
RawBlockSize: f.RawBlockSize,
}
}

View File

@@ -70,7 +70,9 @@ type FileInfoTruncated struct {
NoPermissions bool `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
Version protocol.Vector `protobuf:"bytes,9,opt,name=version" json:"version"`
Sequence int64 `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
SymlinkTarget string `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
RawBlockSize int32 `protobuf:"varint,13,opt,name=block_size,json=blockSize,proto3" json:"block_size,omitempty"`
// repeated BlockInfo Blocks = 16
SymlinkTarget string `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
}
func (m *FileInfoTruncated) Reset() { *m = FileInfoTruncated{} }
@@ -277,6 +279,11 @@ func (m *FileInfoTruncated) MarshalTo(dAtA []byte) (int, error) {
i++
i = encodeVarintStructs(dAtA, i, uint64(m.ModifiedBy))
}
if m.RawBlockSize != 0 {
dAtA[i] = 0x68
i++
i = encodeVarintStructs(dAtA, i, uint64(m.RawBlockSize))
}
if len(m.SymlinkTarget) > 0 {
dAtA[i] = 0x8a
i++
@@ -472,6 +479,9 @@ func (m *FileInfoTruncated) ProtoSize() (n int) {
if m.ModifiedBy != 0 {
n += 1 + sovStructs(uint64(m.ModifiedBy))
}
if m.RawBlockSize != 0 {
n += 1 + sovStructs(uint64(m.RawBlockSize))
}
l = len(m.SymlinkTarget)
if l > 0 {
n += 2 + l + sovStructs(uint64(l))
@@ -1028,6 +1038,25 @@ func (m *FileInfoTruncated) Unmarshal(dAtA []byte) error {
break
}
}
case 13:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field RawBlockSize", wireType)
}
m.RawBlockSize = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowStructs
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.RawBlockSize |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 17:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field SymlinkTarget", wireType)
@@ -1481,43 +1510,45 @@ var (
func init() { proto.RegisterFile("structs.proto", fileDescriptorStructs) }
var fileDescriptorStructs = []byte{
// 598 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0xcd, 0x6a, 0xdb, 0x4c,
0x14, 0xb5, 0x62, 0xc9, 0xb1, 0x47, 0x71, 0xbe, 0x2f, 0x43, 0x08, 0xc2, 0x50, 0x5b, 0x18, 0x0a,
0xa2, 0x50, 0xa5, 0x4d, 0xe8, 0xa6, 0xdd, 0xb9, 0x21, 0x10, 0x28, 0x6d, 0x99, 0x84, 0xac, 0x0a,
0xc1, 0x92, 0xae, 0x9d, 0xa1, 0xf2, 0x8c, 0xa3, 0x19, 0x07, 0xd4, 0x27, 0xe9, 0x32, 0x0f, 0xd3,
0x45, 0x96, 0x5d, 0x77, 0x11, 0x5a, 0xf7, 0x39, 0x0a, 0x45, 0x77, 0x24, 0x45, 0xe9, 0xaa, 0xdd,
0xdd, 0x73, 0xff, 0xef, 0x39, 0x33, 0xa4, 0xaf, 0x74, 0xb6, 0x8a, 0xb5, 0x0a, 0x97, 0x99, 0xd4,
0x92, 0x6e, 0x24, 0xd1, 0xe0, 0xe9, 0x9c, 0xeb, 0xcb, 0x55, 0x14, 0xc6, 0x72, 0xb1, 0x3f, 0x97,
0x73, 0xb9, 0x8f, 0xa1, 0x68, 0x35, 0x43, 0x84, 0x00, 0x2d, 0x53, 0x32, 0x78, 0xd1, 0x48, 0x57,
0xb9, 0x88, 0xf5, 0x25, 0x17, 0xf3, 0x86, 0x95, 0xf2, 0xc8, 0x74, 0x88, 0x65, 0xba, 0x1f, 0xc1,
0xd2, 0x94, 0x8d, 0xaf, 0x88, 0x7b, 0xcc, 0x53, 0x38, 0x87, 0x4c, 0x71, 0x29, 0xe8, 0x33, 0xb2,
0x79, 0x6d, 0x4c, 0xcf, 0xf2, 0xad, 0xc0, 0x3d, 0xf8, 0x3f, 0xac, 0x8a, 0xc2, 0x73, 0x88, 0xb5,
0xcc, 0x26, 0xf6, 0xed, 0xdd, 0xa8, 0xc5, 0xaa, 0x34, 0xba, 0x47, 0x3a, 0x09, 0x5c, 0xf3, 0x18,
0xbc, 0x0d, 0xdf, 0x0a, 0xb6, 0x58, 0x89, 0xa8, 0x47, 0x36, 0xb9, 0xb8, 0x9e, 0xa6, 0x3c, 0xf1,
0xda, 0xbe, 0x15, 0x74, 0x59, 0x05, 0xc7, 0xc7, 0xc4, 0x2d, 0xc7, 0xbd, 0xe1, 0x4a, 0xd3, 0xe7,
0xa4, 0x5b, 0xf6, 0x52, 0x9e, 0xe5, 0xb7, 0x03, 0xf7, 0xe0, 0xbf, 0x30, 0x89, 0xc2, 0xc6, 0x56,
0xe5, 0xc8, 0x3a, 0xed, 0xa5, 0xfd, 0xf9, 0x66, 0xd4, 0x1a, 0xff, 0x6a, 0x93, 0x9d, 0x22, 0xeb,
0x44, 0xcc, 0xe4, 0x59, 0xb6, 0x12, 0xf1, 0x54, 0x43, 0x42, 0x29, 0xb1, 0xc5, 0x74, 0x01, 0xb8,
0x7e, 0x8f, 0xa1, 0x4d, 0x9f, 0x10, 0x5b, 0xe7, 0x4b, 0xb3, 0xe1, 0xf6, 0xc1, 0xde, 0xfd, 0x49,
0x75, 0x79, 0xbe, 0x04, 0x86, 0x39, 0x45, 0xbd, 0xe2, 0x9f, 0x00, 0x97, 0x6e, 0x33, 0xb4, 0xa9,
0x4f, 0xdc, 0x25, 0x64, 0x0b, 0xae, 0xcc, 0x96, 0xb6, 0x6f, 0x05, 0x7d, 0xd6, 0x74, 0xd1, 0x47,
0x84, 0x2c, 0x64, 0xc2, 0x67, 0x1c, 0x92, 0x0b, 0xe5, 0x39, 0x58, 0xdb, 0xab, 0x3c, 0xa7, 0x05,
0x19, 0x09, 0xa4, 0xa0, 0x21, 0xf1, 0x3a, 0x86, 0x8c, 0x12, 0x36, 0x69, 0xda, 0x7c, 0x40, 0x13,
0x7d, 0x4c, 0xb6, 0x85, 0xbc, 0x68, 0xce, 0xed, 0x62, 0x42, 0x5f, 0xc8, 0xf7, 0x8d, 0xc9, 0x0d,
0xc5, 0x7a, 0x7f, 0xa7, 0xd8, 0x80, 0x74, 0x15, 0x5c, 0xad, 0x40, 0xc4, 0xe0, 0x11, 0xdc, 0xb4,
0xc6, 0x74, 0x44, 0xdc, 0xfa, 0x0e, 0xa1, 0x3c, 0xd7, 0xb7, 0x02, 0x87, 0xd5, 0xa7, 0xbd, 0x55,
0xf4, 0x43, 0x23, 0x21, 0xca, 0xbd, 0x2d, 0xdf, 0x0a, 0xec, 0xc9, 0xab, 0x62, 0xc0, 0xb7, 0xbb,
0xd1, 0xe1, 0x3f, 0xbc, 0xc1, 0xf0, 0xf4, 0x52, 0x66, 0xfa, 0xe4, 0xe8, 0xbe, 0xfb, 0x24, 0x2f,
0x6e, 0x56, 0xf9, 0x22, 0xe5, 0xe2, 0xe3, 0x85, 0x9e, 0x66, 0x73, 0xd0, 0xde, 0x0e, 0xca, 0xd8,
0x2f, 0xbd, 0x67, 0xe8, 0x2c, 0xf5, 0xff, 0x62, 0x91, 0xce, 0x6b, 0xb9, 0x12, 0x5a, 0xd1, 0x5d,
0xe2, 0xcc, 0x78, 0x0a, 0x0a, 0x55, 0x77, 0x98, 0x01, 0x85, 0x6c, 0x09, 0xcf, 0x90, 0x03, 0x0e,
0x0a, 0xd5, 0x77, 0x58, 0xd3, 0x85, 0x54, 0x98, 0xce, 0x0a, 0x05, 0x77, 0x58, 0x8d, 0x9b, 0x9a,
0xd9, 0x18, 0xaa, 0x35, 0xdb, 0x25, 0x4e, 0x94, 0x6b, 0xa8, 0x74, 0x36, 0xe0, 0x01, 0xad, 0x9d,
0x3f, 0x68, 0x1d, 0x90, 0xae, 0xf9, 0x16, 0x27, 0x47, 0x78, 0xd1, 0x16, 0xab, 0xf1, 0xf8, 0x1d,
0xe9, 0x99, 0x2b, 0x4e, 0x41, 0xd3, 0x80, 0x74, 0x62, 0x04, 0xe5, 0x57, 0x20, 0xc5, 0x57, 0x30,
0xe1, 0x52, 0xc6, 0x32, 0x5e, 0xac, 0x17, 0x67, 0x50, 0x3c, 0x79, 0x3c, 0xac, 0xcd, 0x2a, 0x38,
0xd9, 0xbd, 0xfd, 0x31, 0x6c, 0xdd, 0xae, 0x87, 0xd6, 0xd7, 0xf5, 0xd0, 0xfa, 0xbe, 0x1e, 0xb6,
0x6e, 0x7e, 0x0e, 0xad, 0xa8, 0x83, 0xc4, 0x1f, 0xfe, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x6e, 0x86,
0x97, 0x21, 0x6a, 0x04, 0x00, 0x00,
// 630 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x53, 0xcf, 0x6a, 0xdb, 0x4c,
0x10, 0xb7, 0x62, 0xc9, 0xb1, 0x47, 0x76, 0xbe, 0x64, 0x09, 0x41, 0x18, 0x3e, 0x5b, 0x18, 0x0a,
0xa2, 0x50, 0xb9, 0x4d, 0xe8, 0xa5, 0xbd, 0xb9, 0x21, 0x10, 0x28, 0x6d, 0x59, 0x87, 0x9c, 0x0a,
0xc6, 0x92, 0xd6, 0xce, 0x12, 0x79, 0xd7, 0xd1, 0xae, 0x52, 0x94, 0x27, 0xe9, 0x31, 0x4f, 0xd0,
0xa7, 0xe8, 0x21, 0xc7, 0x9e, 0x7b, 0x08, 0xad, 0xfb, 0x22, 0x45, 0xbb, 0x92, 0xa2, 0xf4, 0xd4,
0xde, 0xe6, 0x37, 0xff, 0x67, 0x7e, 0x33, 0xd0, 0x13, 0x32, 0x49, 0x43, 0x29, 0xfc, 0x75, 0xc2,
0x25, 0x47, 0x5b, 0x51, 0xd0, 0x7f, 0xb6, 0xa4, 0xf2, 0x22, 0x0d, 0xfc, 0x90, 0xaf, 0xc6, 0x4b,
0xbe, 0xe4, 0x63, 0x65, 0x0a, 0xd2, 0x85, 0x42, 0x0a, 0x28, 0x49, 0x87, 0xf4, 0x5f, 0xd6, 0xdc,
0x45, 0xc6, 0x42, 0x79, 0x41, 0xd9, 0xb2, 0x26, 0xc5, 0x34, 0xd0, 0x19, 0x42, 0x1e, 0x8f, 0x03,
0xb2, 0xd6, 0x61, 0xa3, 0x2b, 0xb0, 0x4f, 0x68, 0x4c, 0xce, 0x49, 0x22, 0x28, 0x67, 0xe8, 0x39,
0x6c, 0x5f, 0x6b, 0xd1, 0x31, 0x5c, 0xc3, 0xb3, 0x0f, 0x77, 0xfd, 0x32, 0xc8, 0x3f, 0x27, 0xa1,
0xe4, 0xc9, 0xc4, 0xbc, 0xbb, 0x1f, 0x36, 0x70, 0xe9, 0x86, 0x0e, 0xa0, 0x15, 0x91, 0x6b, 0x1a,
0x12, 0x67, 0xcb, 0x35, 0xbc, 0x2e, 0x2e, 0x10, 0x72, 0x60, 0x9b, 0xb2, 0xeb, 0x79, 0x4c, 0x23,
0xa7, 0xe9, 0x1a, 0x5e, 0x1b, 0x97, 0x70, 0x74, 0x02, 0x76, 0x51, 0xee, 0x2d, 0x15, 0x12, 0xbd,
0x80, 0x76, 0x91, 0x4b, 0x38, 0x86, 0xdb, 0xf4, 0xec, 0xc3, 0xff, 0xfc, 0x28, 0xf0, 0x6b, 0x5d,
0x15, 0x25, 0x2b, 0xb7, 0x57, 0xe6, 0xe7, 0xdb, 0x61, 0x63, 0xf4, 0xc5, 0x84, 0xbd, 0xdc, 0xeb,
0x94, 0x2d, 0xf8, 0x59, 0x92, 0xb2, 0x70, 0x2e, 0x49, 0x84, 0x10, 0x98, 0x6c, 0xbe, 0x22, 0xaa,
0xfd, 0x0e, 0x56, 0x32, 0x7a, 0x0a, 0xa6, 0xcc, 0xd6, 0xba, 0xc3, 0x9d, 0xc3, 0x83, 0x87, 0x91,
0xaa, 0xf0, 0x6c, 0x4d, 0xb0, 0xf2, 0xc9, 0xe3, 0x05, 0xbd, 0x21, 0xaa, 0xe9, 0x26, 0x56, 0x32,
0x72, 0xc1, 0x5e, 0x93, 0x64, 0x45, 0x85, 0xee, 0xd2, 0x74, 0x0d, 0xaf, 0x87, 0xeb, 0x2a, 0xf4,
0x3f, 0xc0, 0x8a, 0x47, 0x74, 0x41, 0x49, 0x34, 0x13, 0x8e, 0xa5, 0x62, 0x3b, 0xa5, 0x66, 0x9a,
0x2f, 0x23, 0x22, 0x31, 0x91, 0x24, 0x72, 0x5a, 0x7a, 0x19, 0x05, 0xac, 0xaf, 0x69, 0xfb, 0xd1,
0x9a, 0xd0, 0x13, 0xd8, 0x61, 0x7c, 0x56, 0xaf, 0xdb, 0x56, 0x0e, 0x3d, 0xc6, 0x3f, 0xd4, 0x2a,
0xd7, 0x18, 0xeb, 0xfc, 0x1d, 0x63, 0x7d, 0x68, 0x0b, 0x72, 0x95, 0x12, 0x16, 0x12, 0x07, 0x54,
0xa7, 0x15, 0x46, 0x43, 0xb0, 0xab, 0x39, 0x98, 0x70, 0x6c, 0xd7, 0xf0, 0x2c, 0x5c, 0x8d, 0xf6,
0x4e, 0xa0, 0x8f, 0x35, 0x87, 0x20, 0x73, 0xba, 0xae, 0xe1, 0x99, 0x93, 0xd7, 0x79, 0x81, 0xef,
0xf7, 0xc3, 0xa3, 0x7f, 0xb8, 0x41, 0x7f, 0x7a, 0xc1, 0x13, 0x79, 0x7a, 0xfc, 0x90, 0x7d, 0x92,
0xa1, 0x31, 0x40, 0x10, 0xf3, 0xf0, 0x72, 0xa6, 0x28, 0xe8, 0xe5, 0xd5, 0x27, 0xbb, 0x9b, 0xfb,
0x61, 0x17, 0xcf, 0x3f, 0x4d, 0x72, 0xc3, 0x94, 0xde, 0x10, 0xdc, 0x09, 0x4a, 0x31, 0x5f, 0x92,
0xc8, 0x56, 0x31, 0x65, 0x97, 0x33, 0x39, 0x4f, 0x96, 0x44, 0x3a, 0x7b, 0x8a, 0xf7, 0x5e, 0xa1,
0x3d, 0x53, 0xca, 0xe2, 0x60, 0xbe, 0x1a, 0xd0, 0x7a, 0xc3, 0x53, 0x26, 0x05, 0xda, 0x07, 0x6b,
0x41, 0x63, 0x22, 0xd4, 0x99, 0x58, 0x58, 0x83, 0x9c, 0xe7, 0x88, 0x26, 0x6a, 0x69, 0x94, 0x08,
0x75, 0x2e, 0x16, 0xae, 0xab, 0xd4, 0xee, 0x74, 0x66, 0xa1, 0x2e, 0xc4, 0xc2, 0x15, 0xae, 0x93,
0x6c, 0x2a, 0x53, 0x45, 0xf2, 0x3e, 0x58, 0x41, 0x26, 0x49, 0x79, 0x18, 0x1a, 0x3c, 0xe2, 0xa1,
0xf5, 0x07, 0x0f, 0x7d, 0x68, 0xeb, 0x3f, 0x3a, 0x3d, 0x56, 0x13, 0x75, 0x71, 0x85, 0x47, 0xef,
0xa1, 0xa3, 0xa7, 0x98, 0x12, 0x89, 0x3c, 0x68, 0x85, 0x0a, 0x14, 0xbf, 0x03, 0xf9, 0xef, 0x68,
0x73, 0xc1, 0x7b, 0x61, 0xcf, 0xdb, 0x0b, 0x13, 0x92, 0xff, 0x88, 0x1a, 0xac, 0x89, 0x4b, 0x38,
0xd9, 0xbf, 0xfb, 0x39, 0x68, 0xdc, 0x6d, 0x06, 0xc6, 0xb7, 0xcd, 0xc0, 0xf8, 0xb1, 0x19, 0x34,
0x6e, 0x7f, 0x0d, 0x8c, 0xa0, 0xa5, 0x98, 0x3a, 0xfa, 0x1d, 0x00, 0x00, 0xff, 0xff, 0x28, 0xfa,
0x1e, 0x63, 0x9b, 0x04, 0x00, 0x00,
}

View File

@@ -35,6 +35,9 @@ message FileInfoTruncated {
bool no_permissions = 8;
protocol.Vector version = 9 [(gogoproto.nullable) = false];
int64 sequence = 10;
int32 block_size = 13 [(gogoproto.customname) = "RawBlockSize"];
// repeated BlockInfo Blocks = 16
string symlink_target = 17;
}

View File

@@ -140,9 +140,14 @@ func (m *Matcher) Load(file string) error {
defer fd.Close()
m.changeDetector.Reset()
m.changeDetector.Remember(m.fs, file, info.ModTime())
return m.parseLocked(fd, file)
err = m.parseLocked(fd, file)
// If we failed to parse, don't cache, as next time Load is called
// we'll pretend it's all good.
if err == nil {
m.changeDetector.Remember(m.fs, file, info.ModTime())
}
return err
}
func (m *Matcher) Parse(r io.Reader, file string) error {
@@ -445,6 +450,12 @@ func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd Chan
var includePatterns []Pattern
if includePatterns, err = loadParseIncludeFile(fs, includeFile, cd, linesSeen); err == nil {
patterns = append(patterns, includePatterns...)
} else {
// Wrap the error, as if the include does not exist, we get a
// IsNotExists(err) == true error, which we use to check
// existance of the .stignore file, and just end up assuming
// there is none, rather than a broken include.
err = fmt.Errorf("failed to load include file %s: %s", includeFile, err.Error())
}
case strings.HasSuffix(line, "/**"):
err = addPattern(line)

View File

@@ -981,3 +981,42 @@ func TestIssue4689(t *testing.T) {
t.Fatalf("wrong lines parsing changed comment:\n%v", lines)
}
}
func TestIssue4901(t *testing.T) {
dir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
stignore := `
#include unicorn-lazor-death
puppy
`
if err := ioutil.WriteFile(filepath.Join(dir, ".stignore"), []byte(stignore), 0777); err != nil {
t.Fatalf(err.Error())
}
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, dir), WithCache(true))
// Cache does not suddenly make the load succeed.
for i := 0; i < 2; i++ {
err := pats.Load(".stignore")
if err == nil {
t.Fatalf("expected an error")
}
if fs.IsNotExist(err) {
t.Fatalf("unexpected error type")
}
}
if err := ioutil.WriteFile(filepath.Join(dir, "unicorn-lazor-death"), []byte(" "), 0777); err != nil {
t.Fatalf(err.Error())
}
err = pats.Load(".stignore")
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}
}

View File

@@ -7,6 +7,7 @@
package model
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
@@ -34,7 +35,6 @@ import (
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/versioner"
"github.com/syncthing/syncthing/lib/weakhash"
"github.com/thejerf/suture"
)
@@ -626,7 +626,7 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) FolderComple
}
// This might might be more than it really is, because some blocks can be of a smaller size.
downloaded = int64(counts[ft.Name] * protocol.BlockSize)
downloaded = int64(counts[ft.Name] * int(ft.BlockSize()))
fileNeed = ft.FileSize() - downloaded
if fileNeed < 0 {
@@ -1304,7 +1304,7 @@ func (m *Model) closeLocked(device protocol.DeviceID) {
// 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, fromTemporary bool, buf []byte) error {
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
}
@@ -1362,8 +1362,8 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
// other than a regular file.
return protocol.ErrNoSuchFile
}
if err := readOffsetIntoBuf(folderFs, tempFn, offset, buf); err == nil {
err := readOffsetIntoBuf(folderFs, tempFn, offset, buf)
if err == nil && scanner.Validate(buf, hash, weakHash) {
return nil
}
// Fall through to reading from a non-temp file, just incase the temp
@@ -1382,9 +1382,55 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
return protocol.ErrGeneric
}
if !scanner.Validate(buf, hash, weakHash) {
m.recheckFile(deviceID, folderFs, folder, name, int(offset)/len(buf), hash)
return protocol.ErrNoSuchFile
}
return nil
}
func (m *Model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem, folder, name string, blockIndex int, hash []byte) {
cf, ok := m.CurrentFolderFile(folder, name)
if !ok {
l.Debugf("%v recheckFile: %s: %q / %q: no current file", m, deviceID, folder, name)
return
}
if cf.IsDeleted() || cf.IsInvalid() || cf.IsSymlink() || cf.IsDirectory() {
l.Debugf("%v recheckFile: %s: %q / %q: not a regular file", m, deviceID, folder, name)
return
}
if blockIndex > len(cf.Blocks) {
l.Debugf("%v recheckFile: %s: %q / %q i=%d: block index too far", m, deviceID, folder, name, blockIndex)
return
}
block := cf.Blocks[blockIndex]
// Seems to want a different version of the file, whatever.
if !bytes.Equal(block.Hash, hash) {
l.Debugf("%v recheckFile: %s: %q / %q i=%d: hash mismatch %x != %x", m, deviceID, folder, name, blockIndex, block.Hash, hash)
return
}
// The hashes provided part of the request match what we expect to find according
// to what we have in the database, yet the content we've read off the filesystem doesn't
// Something is fishy, invalidate the file and rescan it.
cf.Invalidate(m.shortID)
// Update the index and tell others
// The file will temporarily become invalid, which is ok as the content is messed up.
m.updateLocalsFromScanning(folder, []protocol.FileInfo{cf})
if err := m.ScanFolderSubdirs(folder, []string{name}); err != nil {
l.Debugf("%v recheckFile: %s: %q / %q rescan: %s", m, deviceID, folder, name, err)
} else {
l.Debugf("%v recheckFile: %s: %q / %q", m, deviceID, folder, name)
}
}
func (m *Model) CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
m.fmut.RLock()
fs, ok := m.folderFiles[folder]
@@ -1444,11 +1490,10 @@ func (m *Model) GetIgnores(folder string) ([]string, []string, error) {
}
ignores, ok := m.folderIgnores[folder]
if ok {
return ignores.Lines(), ignores.Patterns(), nil
if !ok {
ignores = ignore.New(fs.NewFilesystem(cfg.FilesystemType, cfg.Path))
}
ignores = ignore.New(fs.NewFilesystem(cfg.FilesystemType, cfg.Path))
if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
return nil, nil, err
}
@@ -1459,7 +1504,18 @@ func (m *Model) GetIgnores(folder string) ([]string, []string, error) {
func (m *Model) SetIgnores(folder string, content []string) error {
cfg, ok := m.cfg.Folders()[folder]
if !ok {
return fmt.Errorf("Folder %s does not exist", folder)
return fmt.Errorf("folder %s does not exist", cfg.Description())
}
err := cfg.CheckPath()
if err == config.ErrPathMissing {
if err = cfg.CreateRoot(); err != nil {
return fmt.Errorf("failed to create folder root: %v", err)
}
err = cfg.CheckPath()
}
if err != nil && err != config.ErrMarkerMissing {
return err
}
if err := ignore.WriteIgnores(cfg.Filesystem(), ".stignore", content); err != nil {
@@ -1639,14 +1695,15 @@ func (m *Model) receivedFile(folder string, file protocol.FileInfo) {
m.folderStatRef(folder).ReceivedFile(file.Name, file.IsDeleted())
}
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, startSequence int64, dbLocation string, dropSymlinks bool) {
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, prevSequence int64, dbLocation string, dropSymlinks bool) {
deviceID := conn.ID()
var err error
l.Debugf("Starting sendIndexes for %s to %s at %s (slv=%d)", folder, deviceID, conn, startSequence)
l.Debugf("Starting sendIndexes for %s to %s at %s (slv=%d)", folder, deviceID, conn, prevSequence)
defer l.Debugf("Exiting sendIndexes for %s to %s at %s: %v", folder, deviceID, conn, err)
minSequence, err := sendIndexTo(startSequence, conn, folder, fs, ignores, dbLocation, dropSymlinks)
// We need to send one index, regardless of whether there is something to send or not
prevSequence, err = sendIndexTo(prevSequence, conn, folder, fs, ignores, dbLocation, dropSymlinks)
// Subscribe to LocalIndexUpdated (we have new information to send) and
// DeviceDisconnected (it might be us who disconnected, so we should
@@ -1664,12 +1721,12 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
// currently in the database, wait for the local index to update. The
// local index may update for other folders than the one we are
// sending for.
if fs.Sequence(protocol.LocalDeviceID) <= minSequence {
if fs.Sequence(protocol.LocalDeviceID) <= prevSequence {
sub.Poll(time.Minute)
continue
}
minSequence, err = sendIndexTo(minSequence, conn, folder, fs, ignores, dbLocation, dropSymlinks)
prevSequence, err = sendIndexTo(prevSequence, conn, folder, fs, ignores, dbLocation, dropSymlinks)
// Wait a short amount of time before entering the next loop. If there
// are continuous changes happening to the local index, this gives us
@@ -1678,43 +1735,20 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
}
}
func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, dbLocation string, dropSymlinks bool) (int64, error) {
// sendIndexTo sends file infos with a sequence number higher than prevSequence and
// returns the highest sent sequence number.
func sendIndexTo(prevSequence int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, dbLocation string, dropSymlinks bool) (int64, error) {
deviceID := conn.ID()
batch := make([]protocol.FileInfo, 0, maxBatchSizeFiles)
batchSizeBytes := 0
initial := minSequence == 0
maxSequence := minSequence
initial := prevSequence == 0
var err error
var f protocol.FileInfo
debugMsg := func(t string) string {
return fmt.Sprintf("Sending indexes for %s to %s at %s: %d files (<%d bytes) (%s)", folder, deviceID, conn, len(batch), batchSizeBytes, t)
}
sorter := NewIndexSorter(dbLocation)
defer sorter.Close()
fs.WithHave(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
f := fi.(protocol.FileInfo)
if f.Sequence <= minSequence {
return true
}
if f.Sequence > maxSequence {
maxSequence = f.Sequence
}
if dropSymlinks && f.IsSymlink() {
// Do not send index entries with symlinks to clients that can't
// handle it. Fixes issue #3802. Once both sides are upgraded, a
// rescan (i.e., change) of the symlink is required for it to
// sync again, due to delta indexes.
return true
}
sorter.Append(f)
return true
})
sorter.Sorted(func(f protocol.FileInfo) bool {
fs.WithHaveSequence(prevSequence+1, func(fi db.FileIntf) bool {
if len(batch) == maxBatchSizeFiles || batchSizeBytes > maxBatchSizeBytes {
if initial {
if err = conn.Index(folder, batch); err != nil {
@@ -1733,24 +1767,43 @@ func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs
batchSizeBytes = 0
}
f = fi.(protocol.FileInfo)
if dropSymlinks && f.IsSymlink() {
// Do not send index entries with symlinks to clients that can't
// handle it. Fixes issue #3802. Once both sides are upgraded, a
// rescan (i.e., change) of the symlink is required for it to
// sync again, due to delta indexes.
return true
}
batch = append(batch, f)
batchSizeBytes += f.ProtoSize()
return true
})
if initial && err == nil {
if err != nil {
return prevSequence, err
}
if initial {
err = conn.Index(folder, batch)
if err == nil {
l.Debugln(debugMsg("small initial index"))
}
} else if len(batch) > 0 && err == nil {
} else if len(batch) > 0 {
err = conn.IndexUpdate(folder, batch)
if err == nil {
l.Debugln(debugMsg("last batch"))
}
}
return maxSequence, err
// True if there was nothing to be sent
if f.Sequence == 0 {
return prevSequence, err
}
return f.Sequence, err
}
func (m *Model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo) {
@@ -1839,7 +1892,7 @@ func (m *Model) diskChangeDetected(folderCfg config.FolderConfiguration, files [
}
}
func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
m.pmut.RLock()
nc, ok := m.conn[deviceID]
m.pmut.RUnlock()
@@ -1848,9 +1901,9 @@ func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, o
return nil, fmt.Errorf("requestGlobal: no such device: %s", deviceID)
}
l.Debugf("%v REQ(out): %s: %q / %q o=%d s=%d h=%x ft=%t", m, deviceID, folder, name, offset, size, hash, fromTemporary)
l.Debugf("%v REQ(out): %s: %q / %q o=%d s=%d h=%x wh=%x ft=%t", m, deviceID, folder, name, offset, size, hash, weakHash, fromTemporary)
return nc.Request(folder, name, offset, size, hash, fromTemporary)
return nc.Request(folder, name, offset, size, hash, weakHash, fromTemporary)
}
func (m *Model) ScanFolders() map[string]error {
@@ -1968,7 +2021,6 @@ func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, su
Folder: folderCfg.ID,
Subs: subDirs,
Matcher: ignores,
BlockSize: protocol.BlockSize,
TempLifetime: time.Duration(m.cfg.Options().KeepTemporariesH) * time.Hour,
CurrentFiler: cFiler{m, folder},
Filesystem: mtimefs,
@@ -1977,7 +2029,7 @@ func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, su
Hashers: m.numHashers(folder),
ShortID: m.shortID,
ProgressTickIntervalS: folderCfg.ScanProgressIntervalS,
UseWeakHashes: weakhash.Enabled,
UseLargeBlocks: folderCfg.UseLargeBlocks,
})
if err := runner.CheckHealth(); err != nil {
@@ -2154,17 +2206,23 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
var message protocol.ClusterConfig
m.fmut.RLock()
// The list of folders in the message is sorted, so we always get the
// same order.
folders := m.deviceFolders[device]
sort.Strings(folders)
defer m.fmut.RUnlock()
for _, folder := range folders {
folderCfg := m.cfg.Folders()[folder]
fs := m.folderFiles[folder]
for _, folderCfg := range m.cfg.FolderList() {
isShared := false
for _, sharedWith := range folderCfg.Devices {
if sharedWith.DeviceID == device {
isShared = true
break
}
}
if !isShared {
continue
}
protocolFolder := protocol.Folder{
ID: folder,
ID: folderCfg.ID,
Label: folderCfg.Label,
ReadOnly: folderCfg.Type == config.FolderTypeSendOnly,
IgnorePermissions: folderCfg.IgnorePerms,
@@ -2173,36 +2231,38 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
Paused: folderCfg.Paused,
}
// Devices are sorted, so we always get the same order.
for _, device := range m.folderDevices.sortedDevices(folder) {
deviceCfg := m.cfg.Devices()[device]
var fs *db.FileSet
if !folderCfg.Paused {
fs = m.folderFiles[folderCfg.ID]
}
var indexID protocol.IndexID
var maxSequence int64
if device == m.id {
indexID = fs.IndexID(protocol.LocalDeviceID)
maxSequence = fs.Sequence(protocol.LocalDeviceID)
} else {
indexID = fs.IndexID(device)
maxSequence = fs.Sequence(device)
}
for _, device := range folderCfg.Devices {
deviceCfg, _ := m.cfg.Device(device.DeviceID)
protocolDevice := protocol.Device{
ID: device,
ID: deviceCfg.DeviceID,
Name: deviceCfg.Name,
Addresses: deviceCfg.Addresses,
Compression: deviceCfg.Compression,
CertName: deviceCfg.CertName,
Introducer: deviceCfg.Introducer,
IndexID: indexID,
MaxSequence: maxSequence,
}
if fs != nil {
if deviceCfg.DeviceID == m.id {
protocolDevice.IndexID = fs.IndexID(protocol.LocalDeviceID)
protocolDevice.MaxSequence = fs.Sequence(protocol.LocalDeviceID)
} else {
protocolDevice.IndexID = fs.IndexID(deviceCfg.DeviceID)
protocolDevice.MaxSequence = fs.Sequence(deviceCfg.DeviceID)
}
}
protocolFolder.Devices = append(protocolFolder.Devices, protocolDevice)
}
message.Folders = append(message.Folders, protocolFolder)
}
m.fmut.RUnlock()
return message
}
@@ -2513,7 +2573,7 @@ func (m *Model) RestoreFolderVersions(folder string, versions map[string]time.Ti
return errors, nil
}
func (m *Model) Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []Availability {
func (m *Model) Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
// The slightly unusual locking sequence here is because we need to hold
// pmut for the duration (as the value returned from foldersFiles can
// get heavily modified on Close()), but also must acquire fmut before
@@ -2532,7 +2592,7 @@ func (m *Model) Availability(folder, file string, version protocol.Vector, block
var availabilities []Availability
next:
for _, device := range fs.Availability(file) {
for _, device := range fs.Availability(file.Name) {
for _, pausedFolder := range m.remotePausedFolders[device] {
if pausedFolder == folder {
continue next
@@ -2545,7 +2605,7 @@ next:
}
for device := range devices {
if m.deviceDownloads[device].Has(folder, file, version, int32(block.Offset/protocol.BlockSize)) {
if m.deviceDownloads[device].Has(folder, file.Name, file.Version, int32(block.Offset/int64(file.BlockSize()))) {
availabilities = append(availabilities, Availability{ID: device, FromTemporary: true})
}
}
@@ -2589,7 +2649,6 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
// A folder was added.
if cfg.Paused {
l.Infoln("Paused folder", cfg.Description())
cfg.CreateRoot()
} else {
l.Infoln("Adding folder", cfg.Description())
m.AddFolder(cfg)

View File

@@ -177,11 +177,11 @@ func TestRequest(t *testing.T) {
defer m.Stop()
m.ScanFolder("default")
bs := make([]byte, protocol.BlockSize)
bs := make([]byte, protocol.MinBlockSize)
// Existing, shared file
bs = bs[:6]
err := m.Request(device1, "default", "foo", 0, nil, false, bs)
err := m.Request(device1, "default", "foo", 0, nil, 0, false, bs)
if err != nil {
t.Error(err)
}
@@ -190,32 +190,32 @@ func TestRequest(t *testing.T) {
}
// Existing, nonshared file
err = m.Request(device2, "default", "foo", 0, nil, false, bs)
err = m.Request(device2, "default", "foo", 0, nil, 0, false, bs)
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
// Nonexistent file
err = m.Request(device1, "default", "nonexistent", 0, nil, false, bs)
err = m.Request(device1, "default", "nonexistent", 0, nil, 0, false, bs)
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, false, bs)
err = m.Request(device1, "default", "../walk.go", 0, nil, 0, false, bs)
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
// Negative offset
err = m.Request(device1, "default", "foo", -4, nil, false, bs[:0])
err = m.Request(device1, "default", "foo", -4, nil, 0, false, bs[:0])
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, false, bs)
err = m.Request(device1, "default", "foo", 0, nil, 0, false, bs)
if err == nil {
t.Error("Unexpected nil error on insecure file read")
}
@@ -357,7 +357,7 @@ func (f *fakeConnection) IndexUpdate(folder string, fs []protocol.FileInfo) erro
return nil
}
func (f *fakeConnection) Request(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
func (f *fakeConnection) Request(folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
f.mut.Lock()
defer f.mut.Unlock()
if f.requestFn != nil {
@@ -409,20 +409,22 @@ func (f *fakeConnection) addFile(name string, flags uint32, ftype protocol.FileI
f.mut.Lock()
defer f.mut.Unlock()
blocks, _ := scanner.Blocks(context.TODO(), bytes.NewReader(data), protocol.BlockSize, int64(len(data)), nil, true)
blockSize := protocol.BlockSize(int64(len(data)))
blocks, _ := scanner.Blocks(context.TODO(), bytes.NewReader(data), blockSize, int64(len(data)), nil, true)
var version protocol.Vector
version = version.Update(f.id.Short())
if ftype == protocol.FileInfoTypeFile || ftype == protocol.FileInfoTypeDirectory {
f.files = append(f.files, protocol.FileInfo{
Name: name,
Type: ftype,
Size: int64(len(data)),
ModifiedS: time.Now().Unix(),
Permissions: flags,
Version: version,
Sequence: time.Now().UnixNano(),
Blocks: blocks,
Name: name,
Type: ftype,
Size: int64(len(data)),
ModifiedS: time.Now().Unix(),
Permissions: flags,
Version: version,
Sequence: time.Now().UnixNano(),
RawBlockSize: int32(blockSize),
Blocks: blocks,
})
} else {
// Symlink
@@ -483,7 +485,7 @@ func BenchmarkRequestOut(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
data, err := m.requestGlobal(device1, "default", files[i%n].Name, 0, 32, nil, false)
data, err := m.requestGlobal(device1, "default", files[i%n].Name, 0, 32, nil, 0, false)
if err != nil {
b.Error(err)
}
@@ -511,7 +513,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, false, buf); err != nil {
if err := m.Request(device1, "default", "request/for/a/file/in/a/couple/of/dirs/128k", 0, nil, 0, false, buf); err != nil {
b.Error(err)
}
}
@@ -605,20 +607,29 @@ func TestClusterConfig(t *testing.T) {
cfg.Folders = []config.FolderConfiguration{
{
ID: "folder1",
Path: "testdata",
Path: "testdata1",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2},
},
},
{
ID: "folder2",
Path: "testdata",
ID: "folder2",
Path: "testdata2",
Paused: true, // should still be included
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2},
},
},
{
ID: "folder3",
Path: "testdata3",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
// should not be included, does not include device2
},
},
}
db := db.OpenMemory()
@@ -688,7 +699,7 @@ func TestIntroducer(t *testing.T) {
if introducedBy.Equals(introducedByAnyone) {
return true
}
return introducedBy.Equals(introducedBy)
return dev.IntroducedBy.Equals(introducedBy)
}
}
return false
@@ -1041,6 +1052,32 @@ func TestIntroducer(t *testing.T) {
}
}
func TestIssue4897(t *testing.T) {
_, m := newState(config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: device1,
Introducer: true,
},
},
Folders: []config.FolderConfiguration{
{
ID: "folder1",
Path: "testdata",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
},
Paused: true,
},
},
})
cm := m.generateClusterConfig(device1)
if l := len(cm.Folders); l != 1 {
t.Errorf("Cluster config contains %v folders, expected 1", l)
}
}
func TestAutoAcceptRejected(t *testing.T) {
// Nothing happens if AutoAcceptFolders not set
tcfg := defaultAutoAcceptCfg.Copy()
@@ -2803,7 +2840,7 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
files.Update(device1, []protocol.FileInfo{file})
files.Update(device2, []protocol.FileInfo{file})
avail := m.Availability("default", file.Name, file.Version, file.Blocks[0])
avail := m.Availability("default", file, file.Blocks[0])
if len(avail) != 0 {
t.Errorf("should not be available, no connections")
}
@@ -2813,7 +2850,7 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
// !!! This is not what I'd expect to happen, as we don't even know if the peer has the original index !!!
avail = m.Availability("default", file.Name, file.Version, file.Blocks[0])
avail = m.Availability("default", file, file.Blocks[0])
if len(avail) != 2 {
t.Errorf("should have two available")
}
@@ -2833,7 +2870,7 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
m.ClusterConfig(device1, cc)
m.ClusterConfig(device2, cc)
avail = m.Availability("default", file.Name, file.Version, file.Blocks[0])
avail = m.Availability("default", file, file.Blocks[0])
if len(avail) != 2 {
t.Errorf("should have two available")
}
@@ -2841,7 +2878,7 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
m.Closed(&fakeConnection{id: device1}, errDeviceUnknown)
m.Closed(&fakeConnection{id: device2}, errDeviceUnknown)
avail = m.Availability("default", file.Name, file.Version, file.Blocks[0])
avail = m.Availability("default", file, file.Blocks[0])
if len(avail) != 0 {
t.Errorf("should have no available")
}
@@ -2856,7 +2893,7 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
ccp.Folders[0].Paused = true
m.ClusterConfig(device1, ccp)
avail = m.Availability("default", file.Name, file.Version, file.Blocks[0])
avail = m.Availability("default", file, file.Blocks[0])
if len(avail) != 1 {
t.Errorf("should have one available")
}
@@ -3499,6 +3536,80 @@ func TestPausedFolders(t *testing.T) {
}
}
func TestIssue4094(t *testing.T) {
db := db.OpenMemory()
// Create a separate wrapper not to pollute other tests.
wrapper, path := createTmpWrapper(config.Configuration{})
defer os.Remove(path)
m := NewModel(wrapper, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
m.ServeBackground()
defer m.Stop()
// Force the model to wire itself and add the folders
folderPath := "testdata/nonexistent"
defer os.RemoveAll(folderPath)
cfg := defaultCfgWrapper.RawCopy()
fcfg := config.FolderConfiguration{
ID: "folder1",
Path: folderPath,
Paused: true,
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
},
}
cfg.Folders = []config.FolderConfiguration{fcfg}
p, err := wrapper.Replace(cfg)
if err != nil {
t.Fatal(err)
}
p.Wait()
if err := m.SetIgnores(fcfg.ID, []string{"foo"}); err != nil {
t.Fatalf("failed setting ignores: %v", err)
}
if _, err := fcfg.Filesystem().Lstat(".stignore"); err != nil {
t.Fatalf("failed stating .stignore: %v", err)
}
}
func TestIssue4903(t *testing.T) {
db := db.OpenMemory()
// Create a separate wrapper not to pollute other tests.
wrapper, path := createTmpWrapper(config.Configuration{})
defer os.Remove(path)
m := NewModel(wrapper, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
m.ServeBackground()
defer m.Stop()
// Force the model to wire itself and add the folders
folderPath := "testdata/nonexistent"
defer os.RemoveAll(folderPath)
cfg := defaultCfgWrapper.RawCopy()
fcfg := config.FolderConfiguration{
ID: "folder1",
Path: folderPath,
Paused: true,
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
},
}
cfg.Folders = []config.FolderConfiguration{fcfg}
p, err := wrapper.Replace(cfg)
if err != nil {
t.Fatal(err)
}
p.Wait()
if err := fcfg.CheckPath(); err != config.ErrPathMissing {
t.Fatalf("expected path missing error, got: %v", err)
}
if _, err := fcfg.Filesystem().Lstat("."); !fs.IsNotExist(err) {
t.Fatalf("Expected missing path error, got: %v", err)
}
}
func addFakeConn(m *Model, dev protocol.DeviceID) *fakeConnection {
fc := &fakeConnection{id: dev, model: m}
m.AddConnection(fc, protocol.HelloResult{})

View File

@@ -93,7 +93,7 @@ func TestSymlinkTraversalRead(t *testing.T) {
// Request a file by traversing the symlink
buf := make([]byte, 10)
err := m.Request(device1, "default", "symlink/requests_test.go", 0, nil, false, buf)
err := m.Request(device1, "default", "symlink/requests_test.go", 0, nil, 0, false, buf)
if err == nil || !bytes.Equal(buf, make([]byte, 10)) {
t.Error("Managed to traverse symlink")
}
@@ -464,6 +464,73 @@ func TestIssue4841(t *testing.T) {
}
}
func TestRescanIfHaveInvalidContent(t *testing.T) {
m, fc, tmpDir := setupModelWithConnection()
defer m.Stop()
defer os.RemoveAll(tmpDir)
payload := []byte("hello")
if err := ioutil.WriteFile(filepath.Join(tmpDir, "foo"), payload, 0777); err != nil {
t.Fatal(err)
}
received := make(chan protocol.FileInfo)
fc.mut.Lock()
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
if len(fs) != 1 {
t.Fatalf("Sent index with %d files, should be 1", len(fs))
}
if fs[0].Name != "foo" {
t.Fatalf(`Sent index with file %v, should be "foo"`, fs[0].Name)
}
received <- fs[0]
return
}
fc.mut.Unlock()
// Scan without ignore patterns with "foo" not existing locally
if err := m.ScanFolder("default"); err != nil {
t.Fatal("Failed scanning:", err)
}
f := <-received
if f.Blocks[0].WeakHash != 103547413 {
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)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(buf, payload) {
t.Errorf("%s != %s", buf, payload)
}
payload = []byte("bye")
buf = make([]byte, len(payload))
if err := ioutil.WriteFile(filepath.Join(tmpDir, "foo"), payload, 0777); err != nil {
t.Fatal(err)
}
err = m.Request(device2, "default", "foo", 0, f.Blocks[0].Hash, f.Blocks[0].WeakHash, false, buf)
if err == nil {
t.Fatalf("expected failure")
}
select {
case f := <-received:
if f.Blocks[0].WeakHash != 41943361 {
t.Fatalf("unexpected weak hash: %d != 41943361", f.Blocks[0].WeakHash)
}
case <-time.After(time.Second):
t.Fatalf("timed out")
}
}
func setupModelWithConnection() (*Model, *fakeConnection, string) {
tmpDir := createTmpDir()
cfg := defaultCfgWrapper.RawCopy()

View File

@@ -81,7 +81,7 @@ const (
const (
defaultCopiers = 2
defaultPullerPause = 60 * time.Second
defaultPullerPendingKiB = 8192 // must be larger than block size
defaultPullerPendingKiB = 2 * protocol.MaxBlockSize / 1024
maxPullerIterations = 3
)
@@ -126,7 +126,7 @@ func newSendReceiveFolder(model *Model, cfg config.FolderConfiguration, ver vers
if f.PullerMaxPendingKiB == 0 {
f.PullerMaxPendingKiB = defaultPullerPendingKiB
}
if blockSizeKiB := protocol.BlockSize / 1024; f.PullerMaxPendingKiB < blockSizeKiB {
if blockSizeKiB := protocol.MaxBlockSize / 1024; f.PullerMaxPendingKiB < blockSizeKiB {
f.PullerMaxPendingKiB = blockSizeKiB
}
@@ -1010,7 +1010,7 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c
// Check for an old temporary file which might have some blocks we could
// reuse.
tempBlocks, err := scanner.HashFile(f.ctx, f.fs, tempName, protocol.BlockSize, nil, false)
tempBlocks, err := scanner.HashFile(f.ctx, f.fs, tempName, file.BlockSize(), nil, false)
if err == nil {
// Check for any reusable blocks in the temp file
tempCopyBlocks, _ := blockDiff(tempBlocks, file.Blocks)
@@ -1160,7 +1160,7 @@ func (f *sendReceiveFolder) shortcutFile(file protocol.FileInfo) error {
// 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.BlockSize)
buf := make([]byte, protocol.MinBlockSize)
for state := range in {
dstFd, err := state.tempFile()
@@ -1186,36 +1186,32 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
var file fs.File
var weakHashFinder *weakhash.Finder
if weakhash.Enabled {
blocksPercentChanged := 0
if tot := len(state.file.Blocks); tot > 0 {
blocksPercentChanged = (tot - state.have) * 100 / tot
blocksPercentChanged := 0
if tot := len(state.file.Blocks); tot > 0 {
blocksPercentChanged = (tot - state.have) * 100 / tot
}
if blocksPercentChanged >= f.WeakHashThresholdPct {
hashesToFind := make([]uint32, 0, len(state.blocks))
for _, block := range state.blocks {
if block.WeakHash != 0 {
hashesToFind = append(hashesToFind, block.WeakHash)
}
}
if blocksPercentChanged >= f.WeakHashThresholdPct {
hashesToFind := make([]uint32, 0, len(state.blocks))
for _, block := range state.blocks {
if block.WeakHash != 0 {
hashesToFind = append(hashesToFind, block.WeakHash)
if len(hashesToFind) > 0 {
file, err = f.fs.Open(state.file.Name)
if err == nil {
weakHashFinder, err = weakhash.NewFinder(file, int(state.file.BlockSize()), hashesToFind)
if err != nil {
l.Debugln("weak hasher", err)
}
}
if len(hashesToFind) > 0 {
file, err = f.fs.Open(state.file.Name)
if err == nil {
weakHashFinder, err = weakhash.NewFinder(file, protocol.BlockSize, hashesToFind)
if err != nil {
l.Debugln("weak hasher", err)
}
}
} else {
l.Debugf("not weak hashing %s. file did not contain any weak hashes", state.file.Name)
}
} else {
l.Debugf("not weak hashing %s. not enough changed %.02f < %d", state.file.Name, blocksPercentChanged, f.WeakHashThresholdPct)
l.Debugf("not weak hashing %s. file did not contain any weak hashes", state.file.Name)
}
} else {
l.Debugf("not weak hashing %s. weak hashing disabled", state.file.Name)
l.Debugf("not weak hashing %s. not enough changed %.02f < %d", state.file.Name, blocksPercentChanged, f.WeakHashThresholdPct)
}
for _, block := range state.blocks {
@@ -1239,7 +1235,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
}
found, err := weakHashFinder.Iterate(block.WeakHash, buf, func(offset int64) bool {
if _, err := verifyBuffer(buf, block); err != nil {
if verifyBuffer(buf, block) != nil {
return true
}
@@ -1268,23 +1264,14 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
return false
}
_, err = fd.ReadAt(buf, protocol.BlockSize*int64(index))
_, err = fd.ReadAt(buf, int64(state.file.BlockSize())*int64(index))
fd.Close()
if err != nil {
return false
}
hash, err := verifyBuffer(buf, block)
if err != nil {
if hash != nil {
l.Debugf("Finder block mismatch in %s:%s:%d expected %q got %q", folder, path, index, block.Hash, hash)
err = f.model.finder.Fix(folder, path, index, block.Hash, hash)
if err != nil {
l.Warnln("finder fix:", err)
}
} else {
l.Debugln("Finder failed to verify buffer", err)
}
if err := verifyBuffer(buf, block); err != nil {
l.Debugln("Finder failed to verify buffer", err)
return false
}
@@ -1324,22 +1311,22 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
}
}
func verifyBuffer(buf []byte, block protocol.BlockInfo) ([]byte, error) {
func verifyBuffer(buf []byte, block protocol.BlockInfo) error {
if len(buf) != int(block.Size) {
return nil, fmt.Errorf("length mismatch %d != %d", len(buf), block.Size)
return fmt.Errorf("length mismatch %d != %d", len(buf), block.Size)
}
hf := sha256.New()
_, err := hf.Write(buf)
if err != nil {
return nil, err
return err
}
hash := hf.Sum(nil)
if !bytes.Equal(hash, block.Hash) {
return hash, fmt.Errorf("hash mismatch %x != %x", hash, block.Hash)
return fmt.Errorf("hash mismatch %x != %x", hash, block.Hash)
}
return hash, nil
return nil
}
func (f *sendReceiveFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPullerState) {
@@ -1391,7 +1378,7 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
}
var lastError error
candidates := f.model.Availability(f.folderID, state.file.Name, state.file.Version, state.block)
candidates := f.model.Availability(f.folderID, state.file, state.block)
for {
// Select the least busy device to pull the block from. If we found no
// feasible device at all, fail the block (and in the long run, the
@@ -1411,7 +1398,7 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
// Fetch the block, while marking the selected device as in use so that
// leastBusy can select another device when someone else asks.
activity.using(selected)
buf, lastError := f.model.requestGlobal(selected.ID, f.folderID, state.file.Name, state.block.Offset, int(state.block.Size), state.block.Hash, selected.FromTemporary)
buf, lastError := f.model.requestGlobal(selected.ID, f.folderID, state.file.Name, state.block.Offset, int(state.block.Size), state.block.Hash, state.block.WeakHash, selected.FromTemporary)
activity.done(selected)
if lastError != nil {
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "returned error:", lastError)
@@ -1420,7 +1407,7 @@ func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPu
// Verify that the received block matches the desired hash, if not
// try pulling it from another device.
_, lastError = verifyBuffer(buf, state.block)
lastError = verifyBuffer(buf, state.block)
if lastError != nil {
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "hash mismatch")
continue

View File

@@ -256,7 +256,7 @@ func TestCopierFinder(t *testing.T) {
}
// Verify that the fetched blocks have actually been written to the temp file
blks, err := scanner.HashFile(context.TODO(), fs.NewFilesystem(fs.FilesystemTypeBasic, "."), tempFile, protocol.BlockSize, nil, false)
blks, err := scanner.HashFile(context.TODO(), fs.NewFilesystem(fs.FilesystemTypeBasic, "."), tempFile, protocol.MinBlockSize, nil, false)
if err != nil {
t.Log(err)
}
@@ -273,8 +273,8 @@ func TestWeakHash(t *testing.T) {
tempFile := filepath.Join("testdata", fs.TempName("weakhash"))
var shift int64 = 10
var size int64 = 1 << 20
expectBlocks := int(size / protocol.BlockSize)
expectPulls := int(shift / protocol.BlockSize)
expectBlocks := int(size / protocol.MinBlockSize)
expectPulls := int(shift / protocol.MinBlockSize)
if shift > 0 {
expectPulls++
}
@@ -307,7 +307,7 @@ func TestWeakHash(t *testing.T) {
// File 1: abcdefgh
// File 2: xyabcdef
f.Seek(0, os.SEEK_SET)
existing, err := scanner.Blocks(context.TODO(), f, protocol.BlockSize, size, nil, true)
existing, err := scanner.Blocks(context.TODO(), f, protocol.MinBlockSize, size, nil, true)
if err != nil {
t.Error(err)
}
@@ -316,7 +316,7 @@ func TestWeakHash(t *testing.T) {
remainder := io.LimitReader(f, size-shift)
prefix := io.LimitReader(rand.Reader, shift)
nf := io.MultiReader(prefix, remainder)
desired, err := scanner.Blocks(context.TODO(), nf, protocol.BlockSize, size, nil, true)
desired, err := scanner.Blocks(context.TODO(), nf, protocol.MinBlockSize, size, nil, true)
if err != nil {
t.Error(err)
}
@@ -434,52 +434,6 @@ func TestCopierCleanup(t *testing.T) {
}
}
// Make sure that the copier routine hashes the content when asked, and pulls
// if it fails to find the block.
func TestLastResortPulling(t *testing.T) {
// Add a file to index (with the incorrect block representation, as content
// doesn't actually match the block list)
file := setUpFile("empty", []int{0})
m := setUpModel(file)
// Pretend that we are handling a new file of the same content but
// with a different name (causing to copy that particular block)
file.Name = "newfile"
iterFn := func(folder, file string, index int32) bool {
return true
}
f := setUpSendReceiveFolder(m)
copyChan := make(chan copyBlocksState)
pullChan := make(chan pullBlockState, 1)
finisherChan := make(chan *sharedPullerState, 1)
dbUpdateChan := make(chan dbUpdateJob, 1)
// Run a single copier routine
go f.copierRoutine(copyChan, pullChan, finisherChan)
f.handleFile(file, copyChan, finisherChan, dbUpdateChan)
// Copier should hash empty file, realise that the region it has read
// doesn't match the hash which was advertised by the block map, fix it
// and ask to pull the block.
<-pullChan
// Verify that it did fix the incorrect hash.
if m.finder.Iterate(folders, blocks[0].Hash, iterFn) {
t.Error("Found unexpected block")
}
if !m.finder.Iterate(folders, scanner.SHA256OfNothing, iterFn) {
t.Error("Expected block not found")
}
(<-finisherChan).fd.Close()
os.Remove(filepath.Join("testdata", fs.TempName("newfile")))
}
func TestDeregisterOnFailInCopy(t *testing.T) {
file := setUpFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
defer os.Remove("testdata/" + fs.TempName("filex"))

View File

@@ -217,7 +217,7 @@ func (s *sharedPullerState) copyDone(block protocol.BlockInfo) {
s.mut.Lock()
s.copyNeeded--
s.updated = time.Now()
s.available = append(s.available, int32(block.Offset/protocol.BlockSize))
s.available = append(s.available, int32(block.Offset/int64(s.file.BlockSize())))
s.availableUpdated = time.Now()
l.Debugln("sharedPullerState", s.folder, s.file.Name, "copyNeeded ->", s.copyNeeded)
s.mut.Unlock()
@@ -253,7 +253,7 @@ func (s *sharedPullerState) pullDone(block protocol.BlockInfo) {
s.mut.Lock()
s.pullNeeded--
s.updated = time.Now()
s.available = append(s.available, int32(block.Offset/protocol.BlockSize))
s.available = append(s.available, int32(block.Offset/int64(s.file.BlockSize())))
s.availableUpdated = time.Now()
l.Debugln("sharedPullerState", s.folder, s.file.Name, "pullNeeded done ->", s.pullNeeded)
s.mut.Unlock()
@@ -314,8 +314,8 @@ func (s *sharedPullerState) Progress() *pullerProgress {
CopiedFromElsewhere: s.copyTotal - s.copyNeeded - s.copyOrigin,
Pulled: s.pullTotal - s.pullNeeded,
Pulling: s.pullNeeded,
BytesTotal: blocksToSize(total),
BytesDone: blocksToSize(done),
BytesTotal: blocksToSize(s.file.BlockSize(), total),
BytesDone: blocksToSize(s.file.BlockSize(), done),
}
}
@@ -343,9 +343,9 @@ func (s *sharedPullerState) Available() []int32 {
return blocks
}
func blocksToSize(num int) int64 {
func blocksToSize(size int, num int) int64 {
if num < 2 {
return protocol.BlockSize / 2
return int64(size / 2)
}
return int64(num-1)*protocol.BlockSize + protocol.BlockSize/2
return int64(num-1)*int64(size) + int64(size/2)
}

View File

@@ -1,199 +0,0 @@
// Copyright (C) 2016 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 (
"encoding/binary"
"io/ioutil"
"os"
"sort"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
)
const (
maxBytesInMemory = 512 << 10
)
// The IndexSorter sorts FileInfos based on their Sequence. You use it
// by first Append()ing all entries to be sorted, then calling Sorted()
// which will iterate over all the items in correctly sorted order.
type IndexSorter interface {
Append(f protocol.FileInfo)
Sorted(fn func(f protocol.FileInfo) bool)
Close()
}
type internalIndexSorter interface {
IndexSorter
full() bool
copyTo(to IndexSorter)
}
// NewIndexSorter returns a new IndexSorter that will start out in memory
// for efficiency but switch to on disk storage once the amount of data
// becomes large.
func NewIndexSorter(location string) IndexSorter {
return &autoSwitchingIndexSorter{
internalIndexSorter: newInMemoryIndexSorter(),
location: location,
}
}
// An autoSwitchingSorter starts out as an inMemorySorter but becomes an
// onDiskSorter when the in memory sorter is full().
type autoSwitchingIndexSorter struct {
internalIndexSorter
location string
}
func (s *autoSwitchingIndexSorter) Append(f protocol.FileInfo) {
if s.internalIndexSorter.full() {
// We spill before adding a file instead of after, to handle the
// case where we're over max size but won't add any more files, in
// which case we *don't* need to spill. An example of this would be
// an index containing just a single large file.
l.Debugf("sorter %p spills to disk", s)
next := newOnDiskIndexSorter(s.location)
s.internalIndexSorter.copyTo(next)
s.internalIndexSorter = next
}
s.internalIndexSorter.Append(f)
}
// An inMemoryIndexSorter is simply a slice of FileInfos. The full() method
// returns true when the number of files exceeds maxFiles or the total
// number of blocks exceeds maxBlocks.
type inMemoryIndexSorter struct {
files []protocol.FileInfo
bytes int
maxBytes int
}
func newInMemoryIndexSorter() *inMemoryIndexSorter {
return &inMemoryIndexSorter{
maxBytes: maxBytesInMemory,
}
}
func (s *inMemoryIndexSorter) Append(f protocol.FileInfo) {
s.files = append(s.files, f)
s.bytes += f.ProtoSize()
}
func (s *inMemoryIndexSorter) Sorted(fn func(protocol.FileInfo) bool) {
sort.Sort(bySequence(s.files))
for _, f := range s.files {
if !fn(f) {
break
}
}
}
func (s *inMemoryIndexSorter) Close() {
}
func (s *inMemoryIndexSorter) full() bool {
return s.bytes >= s.maxBytes
}
func (s *inMemoryIndexSorter) copyTo(dst IndexSorter) {
for _, f := range s.files {
dst.Append(f)
}
}
// bySequence sorts FileInfos by Sequence
type bySequence []protocol.FileInfo
func (l bySequence) Len() int {
return len(l)
}
func (l bySequence) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func (l bySequence) Less(a, b int) bool {
return l[a].Sequence < l[b].Sequence
}
// An onDiskIndexSorter is backed by a LevelDB database in the temporary
// directory. It relies on the fact that iterating over the database is done
// in key order and uses the Sequence as key. When done with an
// onDiskIndexSorter you must call Close() to remove the temporary database.
type onDiskIndexSorter struct {
db *leveldb.DB
dir string
}
func newOnDiskIndexSorter(location string) *onDiskIndexSorter {
// Set options to minimize resource usage.
opts := &opt.Options{
OpenFilesCacheCapacity: 10,
WriteBuffer: 512 << 10,
}
// Use a temporary database directory.
tmp, err := ioutil.TempDir(location, "tmp-index-sorter.")
if err != nil {
panic("creating temporary directory: " + err.Error())
}
db, err := leveldb.OpenFile(tmp, opts)
if err != nil {
panic("creating temporary database: " + err.Error())
}
s := &onDiskIndexSorter{
db: db,
dir: tmp,
}
l.Debugf("onDiskIndexSorter %p created at %s", s, tmp)
return s
}
func (s *onDiskIndexSorter) Append(f protocol.FileInfo) {
key := make([]byte, 8)
binary.BigEndian.PutUint64(key[:], uint64(f.Sequence))
data, err := f.Marshal()
if err != nil {
panic("bug: marshalling FileInfo should never fail: " + err.Error())
}
err = s.db.Put(key, data, nil)
if err != nil {
panic("writing to temporary database: " + err.Error())
}
}
func (s *onDiskIndexSorter) Sorted(fn func(protocol.FileInfo) bool) {
it := s.db.NewIterator(nil, nil)
defer it.Release()
for it.Next() {
var f protocol.FileInfo
if err := f.Unmarshal(it.Value()); err != nil {
panic("unmarshal failed: " + err.Error())
}
if !fn(f) {
break
}
}
}
func (s *onDiskIndexSorter) Close() {
l.Debugf("onDiskIndexSorter %p closes", s)
s.db.Close()
os.RemoveAll(s.dir)
}
func (s *onDiskIndexSorter) full() bool {
return false
}
func (s *onDiskIndexSorter) copyTo(dst IndexSorter) {
// Just wrap Sorted() if we need to support this in the future.
panic("unsupported")
}

View File

@@ -1,157 +0,0 @@
// Copyright (C) 2016 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 (
"fmt"
"os"
"testing"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
)
func TestInMemoryIndexSorter(t *testing.T) {
// An inMemorySorter should be able to absorb a few files in unsorted
// order, and return them sorted.
s := newInMemoryIndexSorter()
addFiles(50, s)
verifySorted(t, s, 50)
verifyBreak(t, s, 50)
s.Close()
}
func TestOnDiskIndexSorter(t *testing.T) {
// An onDiskSorter should be able to absorb a few files in unsorted
// order, and return them sorted.
s := newOnDiskIndexSorter("testdata")
addFiles(50, s)
verifySorted(t, s, 50)
verifyBreak(t, s, 50)
// The temporary database should exist on disk. When Close()d, it should
// be removed.
info, err := os.Stat(s.dir)
if err != nil {
t.Fatal("temp database should exist on disk:", err)
}
if !info.IsDir() {
t.Fatal("temp database should be a directory")
}
s.Close()
_, err = os.Stat(s.dir)
if !os.IsNotExist(err) {
t.Fatal("temp database should have been removed")
}
}
func TestIndexSorter(t *testing.T) {
// An default IndexSorter should be able to absorb files, have them in
// memory, and at some point switch to an on disk database.
s := NewIndexSorter("testdata")
defer s.Close()
// We should start out as an in memory store.
nFiles := 1
addFiles(1, s)
verifySorted(t, s, nFiles)
as := s.(*autoSwitchingIndexSorter)
if _, ok := as.internalIndexSorter.(*inMemoryIndexSorter); !ok {
t.Fatalf("the sorter should be in memory after only one file")
}
// At some point, for sure with less than maxBytesInMemory files, we
// should switch over to an on disk sorter.
for i := 0; i < maxBytesInMemory; i++ {
addFiles(1, s)
nFiles++
if _, ok := as.internalIndexSorter.(*onDiskIndexSorter); ok {
break
}
}
if _, ok := as.internalIndexSorter.(*onDiskIndexSorter); !ok {
t.Fatalf("the sorter should be on disk after %d files", nFiles)
}
verifySorted(t, s, nFiles)
// For test coverage, as some methods are called on the onDiskSorter
// only after switching to it.
addFiles(1, s)
verifySorted(t, s, nFiles+1)
}
// addFiles adds files with random Sequence to the Sorter.
func addFiles(n int, s IndexSorter) {
for i := 0; i < n; i++ {
rnd := rand.Int63()
f := protocol.FileInfo{
Name: fmt.Sprintf("file-%d", rnd),
Size: rand.Int63(),
Permissions: uint32(rand.Intn(0777)),
ModifiedS: rand.Int63(),
ModifiedNs: int32(rand.Int63()),
Sequence: rnd,
Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: uint64(rand.Int63())}}},
Blocks: []protocol.BlockInfo{{
Size: int32(rand.Intn(128 << 10)),
Hash: []byte(rand.String(32)),
}},
}
s.Append(f)
}
}
// verifySorted checks that the files are returned sorted by Sequence.
func verifySorted(t *testing.T, s IndexSorter, expected int) {
prevSequence := int64(-1)
seen := 0
s.Sorted(func(f protocol.FileInfo) bool {
if f.Sequence <= prevSequence {
t.Fatalf("Unsorted Sequence, %d <= %d", f.Sequence, prevSequence)
}
prevSequence = f.Sequence
seen++
return true
})
if seen != expected {
t.Fatalf("expected %d files returned, got %d", expected, seen)
}
}
// verifyBreak checks that the Sorter stops iteration once we return false.
func verifyBreak(t *testing.T, s IndexSorter, expected int) {
prevSequence := int64(-1)
seen := 0
s.Sorted(func(f protocol.FileInfo) bool {
if f.Sequence <= prevSequence {
t.Fatalf("Unsorted Sequence, %d <= %d", f.Sequence, prevSequence)
}
if len(f.Blocks) != 1 {
t.Fatalf("incorrect number of blocks %d != 1", len(f.Blocks))
}
if len(f.Version.Counters) != 1 {
t.Fatalf("incorrect number of version counters %d != 1", len(f.Version.Counters))
}
prevSequence = f.Sequence
seen++
return seen < expected/2
})
if seen != expected/2 {
t.Fatalf("expected %d files iterated over, got %d", expected, seen)
}
}

View File

@@ -80,9 +80,9 @@ func benchmarkRequestsConnPair(b *testing.B, conn0, conn1 net.Conn) {
// Use c0 and c1 for each alternating request, so we get as much
// data flowing in both directions.
if i%2 == 0 {
buf, err = c0.Request("folder", "file", int64(i), 128<<10, nil, false)
buf, err = c0.Request("folder", "file", int64(i), 128<<10, nil, 0, false)
} else {
buf, err = c1.Request("folder", "file", int64(i), 128<<10, nil, false)
buf, err = c1.Request("folder", "file", int64(i), 128<<10, nil, 0, false)
}
if err != nil {
@@ -171,7 +171,7 @@ 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, fromTemporary bool, buf []byte) error {
func (m *fakeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, weakHAsh uint32, fromTemporary bool, buf []byte) 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.

View File

@@ -306,6 +306,7 @@ type FileInfo struct {
NoPermissions bool `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
Version Vector `protobuf:"bytes,9,opt,name=version" json:"version"`
Sequence int64 `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
RawBlockSize int32 `protobuf:"varint,13,opt,name=block_size,json=blockSize,proto3" json:"block_size,omitempty"`
Blocks []BlockInfo `protobuf:"bytes,16,rep,name=Blocks" json:"Blocks"`
SymlinkTarget string `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
}
@@ -352,6 +353,7 @@ type Request struct {
Size int32 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"`
Hash []byte `protobuf:"bytes,6,opt,name=hash,proto3" json:"hash,omitempty"`
FromTemporary bool `protobuf:"varint,7,opt,name=from_temporary,json=fromTemporary,proto3" json:"from_temporary,omitempty"`
WeakHash uint32 `protobuf:"varint,8,opt,name=weak_hash,json=weakHash,proto3" json:"weak_hash,omitempty"`
}
func (m *Request) Reset() { *m = Request{} }
@@ -876,6 +878,11 @@ func (m *FileInfo) MarshalTo(dAtA []byte) (int, error) {
i++
i = encodeVarintBep(dAtA, i, uint64(m.ModifiedBy))
}
if m.RawBlockSize != 0 {
dAtA[i] = 0x68
i++
i = encodeVarintBep(dAtA, i, uint64(m.RawBlockSize))
}
if len(m.Blocks) > 0 {
for _, msg := range m.Blocks {
dAtA[i] = 0x82
@@ -1056,6 +1063,11 @@ func (m *Request) MarshalTo(dAtA []byte) (int, error) {
}
i++
}
if m.WeakHash != 0 {
dAtA[i] = 0x40
i++
i = encodeVarintBep(dAtA, i, uint64(m.WeakHash))
}
return i, nil
}
@@ -1427,6 +1439,9 @@ func (m *FileInfo) ProtoSize() (n int) {
if m.ModifiedBy != 0 {
n += 1 + sovBep(uint64(m.ModifiedBy))
}
if m.RawBlockSize != 0 {
n += 1 + sovBep(uint64(m.RawBlockSize))
}
if len(m.Blocks) > 0 {
for _, e := range m.Blocks {
l = e.ProtoSize()
@@ -1510,6 +1525,9 @@ func (m *Request) ProtoSize() (n int) {
if m.FromTemporary {
n += 2
}
if m.WeakHash != 0 {
n += 1 + sovBep(uint64(m.WeakHash))
}
return n
}
@@ -2904,6 +2922,25 @@ func (m *FileInfo) Unmarshal(dAtA []byte) error {
break
}
}
case 13:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field RawBlockSize", wireType)
}
m.RawBlockSize = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowBep
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.RawBlockSize |= (int32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
case 16:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Blocks", wireType)
@@ -3487,6 +3524,25 @@ func (m *Request) Unmarshal(dAtA []byte) error {
}
}
m.FromTemporary = bool(v != 0)
case 8:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field WeakHash", wireType)
}
m.WeakHash = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowBep
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.WeakHash |= (uint32(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipBep(dAtA[iNdEx:])
@@ -4164,114 +4220,116 @@ var (
func init() { proto.RegisterFile("bep.proto", fileDescriptorBep) }
var fileDescriptorBep = []byte{
// 1729 bytes of a gzipped FileDescriptorProto
// 1762 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x56, 0x4f, 0x8f, 0xdb, 0xc6,
0x15, 0x17, 0x25, 0xea, 0xdf, 0x93, 0x76, 0xc3, 0x1d, 0xdb, 0x5b, 0x96, 0xd9, 0x68, 0x69, 0xc5,
0x8e, 0x37, 0x8b, 0x64, 0xe3, 0x26, 0x69, 0x8b, 0x16, 0x6d, 0x01, 0xfd, 0xe1, 0xae, 0x85, 0xca,
0x94, 0x3a, 0xd2, 0x3a, 0x75, 0x0e, 0x25, 0x28, 0x71, 0xa4, 0x25, 0x4c, 0x71, 0x54, 0x92, 0x5a,
0x5b, 0xfd, 0x08, 0xfa, 0x04, 0xbd, 0x08, 0x08, 0xd0, 0x53, 0xef, 0xfd, 0x10, 0x3e, 0x06, 0x3d,
0xf4, 0xd0, 0x83, 0xd1, 0x6c, 0x2f, 0x3d, 0xf6, 0x5e, 0xa0, 0x28, 0x38, 0x43, 0x52, 0xd4, 0xae,
0x37, 0xf0, 0xa1, 0x27, 0xce, 0xbc, 0xf7, 0x9b, 0x3f, 0xef, 0xf7, 0xde, 0xef, 0x0d, 0xa1, 0x3c,
0x22, 0xf3, 0x93, 0xb9, 0x47, 0x03, 0x8a, 0x4a, 0xec, 0x33, 0xa6, 0x8e, 0xf2, 0xe9, 0xd4, 0x0e,
0x2e, 0x16, 0xa3, 0x93, 0x31, 0x9d, 0x7d, 0x36, 0xa5, 0x53, 0xfa, 0x19, 0xf3, 0x8c, 0x16, 0x13,
0x36, 0x63, 0x13, 0x36, 0xe2, 0x0b, 0xeb, 0x73, 0xc8, 0x3f, 0x21, 0x8e, 0x43, 0xd1, 0x21, 0x54,
0x2c, 0x72, 0x69, 0x8f, 0x89, 0xe1, 0x9a, 0x33, 0x22, 0x0b, 0xaa, 0x70, 0x54, 0xc6, 0xc0, 0x4d,
0xba, 0x39, 0x23, 0x21, 0x60, 0xec, 0xd8, 0xc4, 0x0d, 0x38, 0x20, 0xcb, 0x01, 0xdc, 0xc4, 0x00,
0x0f, 0x61, 0x37, 0x02, 0x5c, 0x12, 0xcf, 0xb7, 0xa9, 0x2b, 0xe7, 0x18, 0x66, 0x87, 0x5b, 0x9f,
0x71, 0x63, 0xdd, 0x87, 0xc2, 0x13, 0x62, 0x5a, 0xc4, 0x43, 0x1f, 0x83, 0x18, 0x2c, 0xe7, 0xfc,
0xac, 0xdd, 0xcf, 0xef, 0x9d, 0xc4, 0x31, 0x9c, 0x3c, 0x25, 0xbe, 0x6f, 0x4e, 0xc9, 0x70, 0x39,
0x27, 0x98, 0x41, 0xd0, 0xaf, 0xa0, 0x32, 0xa6, 0xb3, 0xb9, 0x47, 0x7c, 0xb6, 0x71, 0x96, 0xad,
0x38, 0xb8, 0xb1, 0xa2, 0xb5, 0xc1, 0xe0, 0xf4, 0x82, 0x7a, 0x03, 0x76, 0x5a, 0xce, 0xc2, 0x0f,
0x88, 0xd7, 0xa2, 0xee, 0xc4, 0x9e, 0xa2, 0xc7, 0x50, 0x9c, 0x50, 0xc7, 0x22, 0x9e, 0x2f, 0x0b,
0x6a, 0xee, 0xa8, 0xf2, 0xb9, 0xb4, 0xd9, 0xec, 0x94, 0x39, 0x9a, 0xe2, 0xeb, 0x37, 0x87, 0x19,
0x1c, 0xc3, 0xea, 0x7f, 0xca, 0x42, 0x81, 0x7b, 0xd0, 0x3e, 0x64, 0x6d, 0x8b, 0x53, 0xd4, 0x2c,
0x5c, 0xbd, 0x39, 0xcc, 0x76, 0xda, 0x38, 0x6b, 0x5b, 0xe8, 0x2e, 0xe4, 0x1d, 0x73, 0x44, 0x9c,
0x88, 0x1c, 0x3e, 0x41, 0xef, 0x43, 0xd9, 0x23, 0xa6, 0x65, 0x50, 0xd7, 0x59, 0x32, 0x4a, 0x4a,
0xb8, 0x14, 0x1a, 0x7a, 0xae, 0xb3, 0x44, 0x9f, 0x02, 0xb2, 0xa7, 0x2e, 0xf5, 0x88, 0x31, 0x27,
0xde, 0xcc, 0x66, 0xb7, 0xf5, 0x65, 0x91, 0xa1, 0xf6, 0xb8, 0xa7, 0xbf, 0x71, 0xa0, 0x0f, 0x61,
0x27, 0x82, 0x5b, 0xc4, 0x21, 0x01, 0x91, 0xf3, 0x0c, 0x59, 0xe5, 0xc6, 0x36, 0xb3, 0xa1, 0xc7,
0x70, 0xd7, 0xb2, 0x7d, 0x73, 0xe4, 0x10, 0x23, 0x20, 0xb3, 0xb9, 0x61, 0xbb, 0x16, 0x79, 0x45,
0x7c, 0xb9, 0xc0, 0xb0, 0x28, 0xf2, 0x0d, 0xc9, 0x6c, 0xde, 0xe1, 0x1e, 0xb4, 0x0f, 0x85, 0xb9,
0xb9, 0xf0, 0x89, 0x25, 0x17, 0x19, 0x26, 0x9a, 0x85, 0x2c, 0xf1, 0x0a, 0xf0, 0x65, 0xe9, 0x3a,
0x4b, 0x6d, 0xe6, 0x88, 0x59, 0x8a, 0x60, 0xf5, 0x7f, 0x67, 0xa1, 0xc0, 0x3d, 0xe8, 0xa3, 0x84,
0xa5, 0x6a, 0x73, 0x3f, 0x44, 0xfd, 0xfd, 0xcd, 0x61, 0x89, 0xfb, 0x3a, 0xed, 0x14, 0x6b, 0x08,
0xc4, 0x54, 0x45, 0xb1, 0x31, 0x3a, 0x80, 0xb2, 0x69, 0x59, 0x61, 0xf6, 0x88, 0x2f, 0xe7, 0xd4,
0xdc, 0x51, 0x19, 0x6f, 0x0c, 0xe8, 0xa7, 0xdb, 0xd5, 0x20, 0x5e, 0xaf, 0x9f, 0xdb, 0xca, 0x20,
0x4c, 0xc5, 0x98, 0x78, 0x51, 0x05, 0xe7, 0xd9, 0x79, 0xa5, 0xd0, 0xc0, 0xea, 0xf7, 0x3e, 0x54,
0x67, 0xe6, 0x2b, 0xc3, 0x27, 0xbf, 0x5f, 0x10, 0x77, 0x4c, 0x18, 0x5d, 0x39, 0x5c, 0x99, 0x99,
0xaf, 0x06, 0x91, 0x09, 0xd5, 0x00, 0x6c, 0x37, 0xf0, 0xa8, 0xb5, 0x18, 0x13, 0x2f, 0xe2, 0x2a,
0x65, 0x41, 0x3f, 0x86, 0x12, 0x23, 0xdb, 0xb0, 0x2d, 0xb9, 0xa4, 0x0a, 0x47, 0x62, 0x53, 0x89,
0x02, 0x2f, 0x32, 0xaa, 0x59, 0xdc, 0xf1, 0x10, 0x17, 0x19, 0xb6, 0x63, 0xa1, 0x5f, 0x80, 0xe2,
0xbf, 0xb0, 0xc3, 0x44, 0xf1, 0x9d, 0x02, 0x9b, 0xba, 0x86, 0x47, 0x66, 0xf4, 0xd2, 0x74, 0x7c,
0xb9, 0xcc, 0x8e, 0x91, 0x43, 0x44, 0x27, 0x05, 0xc0, 0x91, 0xbf, 0xde, 0x83, 0x3c, 0xdb, 0x31,
0xcc, 0x22, 0x2f, 0xd6, 0x48, 0xbd, 0xd1, 0x0c, 0x9d, 0x40, 0x7e, 0x62, 0x3b, 0xc4, 0x97, 0xb3,
0x2c, 0x87, 0x28, 0x55, 0xe9, 0xb6, 0x43, 0x3a, 0xee, 0x84, 0x46, 0x59, 0xe4, 0xb0, 0xfa, 0x39,
0x54, 0xd8, 0x86, 0xe7, 0x73, 0xcb, 0x0c, 0xc8, 0xff, 0x6d, 0xdb, 0xff, 0xe4, 0xa0, 0x14, 0x7b,
0x92, 0xa4, 0x0b, 0xa9, 0xa4, 0x1f, 0x47, 0xfd, 0x80, 0xab, 0x7b, 0xff, 0xe6, 0x7e, 0xa9, 0x86,
0x80, 0x40, 0xf4, 0xed, 0x3f, 0x10, 0xa6, 0xa7, 0x1c, 0x66, 0x63, 0xa4, 0x42, 0xe5, 0xba, 0x88,
0x76, 0x70, 0xda, 0x84, 0x3e, 0x00, 0x98, 0x51, 0xcb, 0x9e, 0xd8, 0xc4, 0x32, 0x7c, 0x56, 0x00,
0x39, 0x5c, 0x8e, 0x2d, 0x03, 0x24, 0x87, 0xe5, 0x1e, 0x4a, 0xc8, 0x8a, 0xb4, 0x12, 0x4f, 0x43,
0x8f, 0xed, 0x5e, 0x9a, 0x8e, 0x1d, 0x2b, 0x24, 0x9e, 0x86, 0x5d, 0xcf, 0xa5, 0x5b, 0xe2, 0x2d,
0x31, 0xc0, 0x8e, 0x4b, 0xd3, 0xc2, 0x7d, 0x0c, 0xc5, 0xb8, 0x2b, 0x86, 0xf9, 0xdc, 0x52, 0xd2,
0x33, 0x32, 0x0e, 0x68, 0xd2, 0x6f, 0x22, 0x18, 0x52, 0xa0, 0x94, 0x94, 0x22, 0xb0, 0x9b, 0x26,
0xf3, 0xb0, 0x17, 0x27, 0x71, 0xb8, 0xbe, 0x5c, 0x51, 0x85, 0xa3, 0x3c, 0x4e, 0x42, 0xd3, 0xc3,
0xe3, 0x36, 0x80, 0xd1, 0x52, 0xae, 0xb2, 0x5a, 0x7c, 0x2f, 0xae, 0xc5, 0xc1, 0x05, 0xf5, 0x82,
0x4e, 0x7b, 0xb3, 0xa2, 0xb9, 0x44, 0x3f, 0x82, 0x42, 0xd3, 0xa1, 0xe3, 0x17, 0xb1, 0xd2, 0xef,
0x6c, 0xee, 0xc7, 0xec, 0xa9, 0x7c, 0x46, 0xc0, 0x30, 0x74, 0x7f, 0x39, 0x73, 0x6c, 0xf7, 0x85,
0x11, 0x98, 0xde, 0x94, 0x04, 0xf2, 0x1e, 0x6f, 0xf8, 0x91, 0x75, 0xc8, 0x8c, 0x3f, 0x17, 0xff,
0xf8, 0xcd, 0x61, 0xa6, 0xee, 0x42, 0x39, 0xd9, 0x27, 0x2c, 0x29, 0x3a, 0x99, 0xf8, 0x24, 0x60,
0xf9, 0xcf, 0xe1, 0x68, 0x96, 0x64, 0x35, 0xcb, 0x02, 0xe2, 0x59, 0x45, 0x20, 0x5e, 0x98, 0xfe,
0x05, 0xcb, 0x74, 0x15, 0xb3, 0x71, 0xa8, 0xe3, 0x97, 0xc4, 0x7c, 0x61, 0x30, 0x07, 0xcf, 0x73,
0x29, 0x34, 0x3c, 0x31, 0xfd, 0x8b, 0xe8, 0xbc, 0x5f, 0x42, 0x81, 0xf3, 0x8a, 0xbe, 0x80, 0xd2,
0x98, 0x2e, 0xdc, 0x60, 0xd3, 0xeb, 0xf7, 0xd2, 0xad, 0x82, 0x79, 0xa2, 0xc8, 0x12, 0x60, 0xfd,
0x14, 0x8a, 0x91, 0x0b, 0x3d, 0x4c, 0xfa, 0x98, 0xd8, 0xbc, 0x77, 0x8d, 0xc2, 0xed, 0xe6, 0x7f,
0x69, 0x3a, 0x0b, 0x7e, 0x79, 0x11, 0xf3, 0x49, 0xfd, 0x2f, 0x02, 0x14, 0x71, 0x98, 0x36, 0x3f,
0x48, 0x3d, 0x1b, 0xf9, 0xad, 0x67, 0x63, 0x23, 0xb0, 0xec, 0x96, 0xc0, 0x62, 0x8d, 0xe4, 0x52,
0x1a, 0xd9, 0x30, 0x27, 0xbe, 0x95, 0xb9, 0xfc, 0x5b, 0x98, 0x2b, 0xa4, 0x98, 0x7b, 0x08, 0xbb,
0x13, 0x8f, 0xce, 0xd8, 0xc3, 0x40, 0x3d, 0xd3, 0x5b, 0x46, 0xf5, 0xbc, 0x13, 0x5a, 0x87, 0xb1,
0xb1, 0x6e, 0x40, 0x09, 0x13, 0x7f, 0x4e, 0x5d, 0x9f, 0xdc, 0x7a, 0x6d, 0x04, 0xa2, 0x65, 0x06,
0x26, 0xbb, 0x74, 0x15, 0xb3, 0x31, 0x7a, 0x04, 0xe2, 0x98, 0x5a, 0xfc, 0xca, 0xbb, 0xe9, 0x1a,
0xd2, 0x3c, 0x8f, 0x7a, 0x2d, 0x6a, 0x11, 0xcc, 0x00, 0xf5, 0x39, 0x48, 0x6d, 0xfa, 0xd2, 0x75,
0xa8, 0x69, 0xf5, 0x3d, 0x3a, 0x0d, 0x1b, 0xf4, 0xad, 0x8d, 0xa6, 0x0d, 0xc5, 0x05, 0x6b, 0x45,
0x71, 0xab, 0x79, 0xb0, 0xdd, 0x1a, 0xae, 0x6f, 0xc4, 0xfb, 0x56, 0xac, 0xa7, 0x68, 0x69, 0xfd,
0x6f, 0x02, 0x28, 0xb7, 0xa3, 0x51, 0x07, 0x2a, 0x1c, 0x69, 0xa4, 0xfe, 0x49, 0x8e, 0xde, 0xe5,
0x20, 0xd6, 0x95, 0x60, 0x91, 0x8c, 0xdf, 0xfa, 0xa0, 0xa5, 0xf4, 0x9f, 0x7b, 0x37, 0xfd, 0x3f,
0x82, 0x9d, 0x51, 0x28, 0x98, 0xe4, 0xf9, 0x16, 0xd5, 0xdc, 0x51, 0xbe, 0x99, 0x95, 0x32, 0xb8,
0x3a, 0xe2, 0x4a, 0x62, 0xf6, 0x7a, 0x01, 0xc4, 0xbe, 0xed, 0x4e, 0xeb, 0x87, 0x90, 0x6f, 0x39,
0x94, 0x25, 0xac, 0xe0, 0x11, 0xd3, 0xa7, 0x6e, 0xcc, 0x23, 0x9f, 0x1d, 0xff, 0x35, 0x0b, 0x95,
0xd4, 0xaf, 0x15, 0x7a, 0x0c, 0xbb, 0xad, 0xee, 0xf9, 0x60, 0xa8, 0x61, 0xa3, 0xd5, 0xd3, 0x4f,
0x3b, 0x67, 0x52, 0x46, 0x39, 0x58, 0xad, 0x55, 0x79, 0xb6, 0x01, 0x6d, 0xff, 0x35, 0x1d, 0x42,
0xbe, 0xa3, 0xb7, 0xb5, 0xdf, 0x4a, 0x82, 0x72, 0x77, 0xb5, 0x56, 0xa5, 0x14, 0x90, 0x3f, 0x41,
0x9f, 0x40, 0x95, 0x01, 0x8c, 0xf3, 0x7e, 0xbb, 0x31, 0xd4, 0xa4, 0xac, 0xa2, 0xac, 0xd6, 0xea,
0xfe, 0x75, 0x5c, 0xc4, 0xf9, 0x87, 0x50, 0xc4, 0xda, 0x6f, 0xce, 0xb5, 0xc1, 0x50, 0xca, 0x29,
0xfb, 0xab, 0xb5, 0x8a, 0x52, 0xc0, 0x58, 0x35, 0x0f, 0xa1, 0x84, 0xb5, 0x41, 0xbf, 0xa7, 0x0f,
0x34, 0x49, 0x54, 0x7e, 0xb0, 0x5a, 0xab, 0x77, 0xb6, 0x50, 0x51, 0x95, 0xfe, 0x04, 0xf6, 0xda,
0xbd, 0xaf, 0xf4, 0x6e, 0xaf, 0xd1, 0x36, 0xfa, 0xb8, 0x77, 0x86, 0xb5, 0xc1, 0x40, 0xca, 0x2b,
0x87, 0xab, 0xb5, 0xfa, 0x7e, 0x0a, 0x7f, 0xa3, 0xe8, 0x3e, 0x00, 0xb1, 0xdf, 0xd1, 0xcf, 0xa4,
0x82, 0x72, 0x67, 0xb5, 0x56, 0xdf, 0x4b, 0x41, 0x43, 0x52, 0xc3, 0x88, 0x5b, 0xdd, 0xde, 0x40,
0x93, 0x8a, 0x37, 0x22, 0x66, 0x64, 0x1f, 0xff, 0x0e, 0xd0, 0xcd, 0x9f, 0x4f, 0xf4, 0x00, 0x44,
0xbd, 0xa7, 0x6b, 0x52, 0x86, 0xc7, 0x7f, 0x13, 0xa1, 0x53, 0x97, 0xa0, 0x3a, 0xe4, 0xba, 0x5f,
0x7f, 0x29, 0x09, 0xca, 0x0f, 0x57, 0x6b, 0xf5, 0xde, 0x4d, 0x50, 0xf7, 0xeb, 0x2f, 0x8f, 0x29,
0x54, 0xd2, 0x1b, 0xd7, 0xa1, 0xf4, 0x54, 0x1b, 0x36, 0xda, 0x8d, 0x61, 0x43, 0xca, 0xf0, 0x2b,
0xc5, 0xee, 0xa7, 0x24, 0x30, 0x99, 0x08, 0x0f, 0x20, 0xaf, 0x6b, 0xcf, 0x34, 0x2c, 0x09, 0xca,
0xde, 0x6a, 0xad, 0xee, 0xc4, 0x00, 0x9d, 0x5c, 0x12, 0x0f, 0xd5, 0xa0, 0xd0, 0xe8, 0x7e, 0xd5,
0x78, 0x3e, 0x90, 0xb2, 0x0a, 0x5a, 0xad, 0xd5, 0xdd, 0xd8, 0xdd, 0x70, 0x5e, 0x9a, 0x4b, 0xff,
0xf8, 0xbf, 0x02, 0x54, 0xd3, 0x0f, 0x2e, 0xaa, 0x81, 0x78, 0xda, 0xe9, 0x6a, 0xf1, 0x71, 0x69,
0x5f, 0x38, 0x46, 0x47, 0x50, 0x6e, 0x77, 0xb0, 0xd6, 0x1a, 0xf6, 0xf0, 0xf3, 0x38, 0x96, 0x34,
0xa8, 0x6d, 0x7b, 0xac, 0xc0, 0x97, 0xe8, 0x67, 0x50, 0x1d, 0x3c, 0x7f, 0xda, 0xed, 0xe8, 0xbf,
0x36, 0xd8, 0x8e, 0x59, 0xe5, 0xd1, 0x6a, 0xad, 0xde, 0xdf, 0x02, 0x93, 0xb9, 0x47, 0xc6, 0x66,
0x40, 0xac, 0x01, 0x7f, 0x44, 0x42, 0x67, 0x49, 0x40, 0x2d, 0xd8, 0x8b, 0x97, 0x6e, 0x0e, 0xcb,
0x29, 0x9f, 0xac, 0xd6, 0xea, 0x47, 0xdf, 0xbb, 0x3e, 0x39, 0xbd, 0x24, 0xa0, 0x07, 0x50, 0x8c,
0x36, 0x89, 0x2b, 0x29, 0xbd, 0x34, 0x5a, 0x70, 0xfc, 0x67, 0x01, 0xca, 0x49, 0xbb, 0x0a, 0x09,
0xd7, 0x7b, 0x86, 0x86, 0x71, 0x0f, 0xc7, 0x0c, 0x24, 0x4e, 0x9d, 0xb2, 0x21, 0xba, 0x0f, 0xc5,
0x33, 0x4d, 0xd7, 0x70, 0xa7, 0x15, 0x0b, 0x23, 0x81, 0x9c, 0x11, 0x97, 0x78, 0xf6, 0x18, 0x7d,
0x0c, 0x55, 0xbd, 0x67, 0x0c, 0xce, 0x5b, 0x4f, 0xe2, 0xd0, 0xd9, 0xf9, 0xa9, 0xad, 0x06, 0x8b,
0xf1, 0x05, 0xe3, 0xf3, 0x38, 0xd4, 0xd0, 0xb3, 0x46, 0xb7, 0xd3, 0xe6, 0xd0, 0x9c, 0x22, 0xaf,
0xd6, 0xea, 0xdd, 0x04, 0xda, 0xe1, 0x7f, 0x1e, 0x21, 0xf6, 0xd8, 0x82, 0xda, 0xf7, 0x37, 0x26,
0xa4, 0x42, 0xa1, 0xd1, 0xef, 0x6b, 0x7a, 0x3b, 0xbe, 0xfd, 0xc6, 0xd7, 0x98, 0xcf, 0x89, 0x6b,
0x85, 0x88, 0xd3, 0x1e, 0x3e, 0xd3, 0x86, 0xf1, 0xe5, 0x37, 0x88, 0x53, 0x1a, 0xbe, 0xe0, 0xcd,
0x83, 0xd7, 0xdf, 0xd5, 0x32, 0xdf, 0x7e, 0x57, 0xcb, 0xbc, 0xbe, 0xaa, 0x09, 0xdf, 0x5e, 0xd5,
0x84, 0x7f, 0x5c, 0xd5, 0x32, 0xff, 0xba, 0xaa, 0x09, 0xdf, 0xfc, 0xb3, 0x26, 0x8c, 0x0a, 0xac,
0x91, 0x7d, 0xf1, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xad, 0x8a, 0xef, 0x7f, 0x8f, 0x0e, 0x00,
0x00,
0x15, 0x5f, 0x4a, 0x94, 0x44, 0x3d, 0x69, 0x37, 0xdc, 0xb1, 0xbd, 0x65, 0x99, 0x8d, 0x44, 0x2b,
0x76, 0xbc, 0x59, 0x24, 0x6b, 0x37, 0x49, 0x5b, 0xb4, 0x68, 0x0b, 0xe8, 0x0f, 0x77, 0x2d, 0x54,
0xa6, 0xd4, 0x91, 0xd6, 0xa9, 0x73, 0x28, 0x41, 0x89, 0x23, 0x2d, 0x61, 0x8a, 0xa3, 0x92, 0xd4,
0xda, 0xca, 0x47, 0xd0, 0x27, 0xe8, 0x45, 0x40, 0x80, 0x9e, 0x0a, 0xf4, 0x83, 0xf8, 0x98, 0xf6,
0xd0, 0x43, 0x0f, 0x46, 0xb3, 0xbd, 0xf4, 0xd8, 0x4f, 0x50, 0x14, 0x9c, 0x21, 0x25, 0x6a, 0xd7,
0x0e, 0x7c, 0xc8, 0x89, 0x33, 0xef, 0xfd, 0xe6, 0x0d, 0xdf, 0x6f, 0x7e, 0xef, 0xcd, 0x40, 0x71,
0x48, 0x66, 0x27, 0x33, 0x9f, 0x86, 0x14, 0x49, 0xec, 0x33, 0xa2, 0xae, 0xfa, 0xe9, 0xc4, 0x09,
0x2f, 0xe6, 0xc3, 0x93, 0x11, 0x9d, 0x3e, 0x9c, 0xd0, 0x09, 0x7d, 0xc8, 0x3c, 0xc3, 0xf9, 0x98,
0xcd, 0xd8, 0x84, 0x8d, 0xf8, 0xc2, 0xda, 0x0c, 0x72, 0x8f, 0x89, 0xeb, 0x52, 0x54, 0x85, 0x92,
0x4d, 0x2e, 0x9d, 0x11, 0x31, 0x3d, 0x6b, 0x4a, 0x14, 0x41, 0x13, 0x8e, 0x8a, 0x18, 0xb8, 0xc9,
0xb0, 0xa6, 0x24, 0x02, 0x8c, 0x5c, 0x87, 0x78, 0x21, 0x07, 0x64, 0x38, 0x80, 0x9b, 0x18, 0xe0,
0x3e, 0xec, 0xc5, 0x80, 0x4b, 0xe2, 0x07, 0x0e, 0xf5, 0x94, 0x2c, 0xc3, 0xec, 0x72, 0xeb, 0x53,
0x6e, 0xac, 0x05, 0x90, 0x7f, 0x4c, 0x2c, 0x9b, 0xf8, 0xe8, 0x63, 0x10, 0xc3, 0xc5, 0x8c, 0xef,
0xb5, 0xf7, 0xd9, 0x9d, 0x93, 0x24, 0x87, 0x93, 0x27, 0x24, 0x08, 0xac, 0x09, 0x19, 0x2c, 0x66,
0x04, 0x33, 0x08, 0xfa, 0x0d, 0x94, 0x46, 0x74, 0x3a, 0xf3, 0x49, 0xc0, 0x02, 0x67, 0xd8, 0x8a,
0xc3, 0x1b, 0x2b, 0x9a, 0x1b, 0x0c, 0x4e, 0x2f, 0xa8, 0xd5, 0x61, 0xb7, 0xe9, 0xce, 0x83, 0x90,
0xf8, 0x4d, 0xea, 0x8d, 0x9d, 0x09, 0x7a, 0x04, 0x85, 0x31, 0x75, 0x6d, 0xe2, 0x07, 0x8a, 0xa0,
0x65, 0x8f, 0x4a, 0x9f, 0xc9, 0x9b, 0x60, 0xa7, 0xcc, 0xd1, 0x10, 0x5f, 0xbd, 0xae, 0xee, 0xe0,
0x04, 0x56, 0xfb, 0x73, 0x06, 0xf2, 0xdc, 0x83, 0x0e, 0x20, 0xe3, 0xd8, 0x9c, 0xa2, 0x46, 0xfe,
0xea, 0x75, 0x35, 0xd3, 0x6e, 0xe1, 0x8c, 0x63, 0xa3, 0xdb, 0x90, 0x73, 0xad, 0x21, 0x71, 0x63,
0x72, 0xf8, 0x04, 0xbd, 0x0f, 0x45, 0x9f, 0x58, 0xb6, 0x49, 0x3d, 0x77, 0xc1, 0x28, 0x91, 0xb0,
0x14, 0x19, 0xba, 0x9e, 0xbb, 0x40, 0x9f, 0x02, 0x72, 0x26, 0x1e, 0xf5, 0x89, 0x39, 0x23, 0xfe,
0xd4, 0x61, 0x7f, 0x1b, 0x28, 0x22, 0x43, 0xed, 0x73, 0x4f, 0x6f, 0xe3, 0x40, 0x1f, 0xc2, 0x6e,
0x0c, 0xb7, 0x89, 0x4b, 0x42, 0xa2, 0xe4, 0x18, 0xb2, 0xcc, 0x8d, 0x2d, 0x66, 0x43, 0x8f, 0xe0,
0xb6, 0xed, 0x04, 0xd6, 0xd0, 0x25, 0x66, 0x48, 0xa6, 0x33, 0xd3, 0xf1, 0x6c, 0xf2, 0x92, 0x04,
0x4a, 0x9e, 0x61, 0x51, 0xec, 0x1b, 0x90, 0xe9, 0xac, 0xcd, 0x3d, 0xe8, 0x00, 0xf2, 0x33, 0x6b,
0x1e, 0x10, 0x5b, 0x29, 0x30, 0x4c, 0x3c, 0x8b, 0x58, 0xe2, 0x0a, 0x08, 0x14, 0xf9, 0x3a, 0x4b,
0x2d, 0xe6, 0x48, 0x58, 0x8a, 0x61, 0xb5, 0xff, 0x66, 0x20, 0xcf, 0x3d, 0xe8, 0xa3, 0x35, 0x4b,
0xe5, 0xc6, 0x41, 0x84, 0xfa, 0xe7, 0xeb, 0xaa, 0xc4, 0x7d, 0xed, 0x56, 0x8a, 0x35, 0x04, 0x62,
0x4a, 0x51, 0x6c, 0x8c, 0x0e, 0xa1, 0x68, 0xd9, 0x76, 0x74, 0x7a, 0x24, 0x50, 0xb2, 0x5a, 0xf6,
0xa8, 0x88, 0x37, 0x06, 0xf4, 0xf3, 0x6d, 0x35, 0x88, 0xd7, 0xf5, 0xf3, 0x36, 0x19, 0x44, 0x47,
0x31, 0x22, 0x7e, 0xac, 0xe0, 0x1c, 0xdb, 0x4f, 0x8a, 0x0c, 0x4c, 0xbf, 0x77, 0xa1, 0x3c, 0xb5,
0x5e, 0x9a, 0x01, 0xf9, 0xe3, 0x9c, 0x78, 0x23, 0xc2, 0xe8, 0xca, 0xe2, 0xd2, 0xd4, 0x7a, 0xd9,
0x8f, 0x4d, 0xa8, 0x02, 0xe0, 0x78, 0xa1, 0x4f, 0xed, 0xf9, 0x88, 0xf8, 0x31, 0x57, 0x29, 0x0b,
0xfa, 0x29, 0x48, 0x8c, 0x6c, 0xd3, 0xb1, 0x15, 0x49, 0x13, 0x8e, 0xc4, 0x86, 0x1a, 0x27, 0x5e,
0x60, 0x54, 0xb3, 0xbc, 0x93, 0x21, 0x2e, 0x30, 0x6c, 0xdb, 0x46, 0xbf, 0x02, 0x35, 0x78, 0xee,
0x44, 0x07, 0xc5, 0x23, 0x85, 0x0e, 0xf5, 0x4c, 0x9f, 0x4c, 0xe9, 0xa5, 0xe5, 0x06, 0x4a, 0x91,
0x6d, 0xa3, 0x44, 0x88, 0x76, 0x0a, 0x80, 0x63, 0x7f, 0xad, 0x0b, 0x39, 0x16, 0x31, 0x3a, 0x45,
0x2e, 0xd6, 0xb8, 0x7a, 0xe3, 0x19, 0x3a, 0x81, 0xdc, 0xd8, 0x71, 0x49, 0xa0, 0x64, 0xd8, 0x19,
0xa2, 0x94, 0xd2, 0x1d, 0x97, 0xb4, 0xbd, 0x31, 0x8d, 0x4f, 0x91, 0xc3, 0x6a, 0xe7, 0x50, 0x62,
0x01, 0xcf, 0x67, 0xb6, 0x15, 0x92, 0x1f, 0x2c, 0xec, 0x5f, 0x45, 0x90, 0x12, 0xcf, 0xfa, 0xd0,
0x85, 0xd4, 0xa1, 0x1f, 0xc7, 0xfd, 0x80, 0x57, 0xf7, 0xc1, 0xcd, 0x78, 0xa9, 0x86, 0x80, 0x40,
0x0c, 0x9c, 0xaf, 0x09, 0xab, 0xa7, 0x2c, 0x66, 0x63, 0xa4, 0x41, 0xe9, 0x7a, 0x11, 0xed, 0xe2,
0xb4, 0x09, 0x7d, 0x00, 0x30, 0xa5, 0xb6, 0x33, 0x76, 0x88, 0x6d, 0x06, 0x4c, 0x00, 0x59, 0x5c,
0x4c, 0x2c, 0x7d, 0xa4, 0x44, 0x72, 0x8f, 0x4a, 0xc8, 0x8e, 0x6b, 0x25, 0x99, 0x46, 0x1e, 0xc7,
0xbb, 0xb4, 0x5c, 0x27, 0xa9, 0x90, 0x64, 0x1a, 0x75, 0x3d, 0x8f, 0x6e, 0x15, 0xaf, 0xc4, 0x00,
0xbb, 0x1e, 0x4d, 0x17, 0xee, 0x23, 0x28, 0x24, 0x5d, 0x31, 0x3a, 0xcf, 0xad, 0x4a, 0x7a, 0x4a,
0x46, 0x21, 0x5d, 0xf7, 0x9b, 0x18, 0x86, 0x54, 0x90, 0xd6, 0x52, 0x04, 0xf6, 0xa7, 0xeb, 0x79,
0xd4, 0x8b, 0xd7, 0x79, 0x78, 0x81, 0x52, 0xd2, 0x84, 0xa3, 0x1c, 0x5e, 0xa7, 0x66, 0x44, 0xdb,
0x6d, 0x00, 0xc3, 0x85, 0x52, 0x66, 0x5a, 0x7c, 0x2f, 0xd1, 0x62, 0xff, 0x82, 0xfa, 0x61, 0xbb,
0xb5, 0x59, 0xd1, 0x58, 0xa0, 0x87, 0x00, 0x43, 0x97, 0x8e, 0x9e, 0x9b, 0x8c, 0xd6, 0xdd, 0x28,
0x62, 0x43, 0xbe, 0x7a, 0x5d, 0x2d, 0x63, 0xeb, 0x45, 0x23, 0x72, 0xf4, 0x9d, 0xaf, 0x09, 0x2e,
0x0e, 0x93, 0x21, 0xfa, 0x09, 0xe4, 0x99, 0x3d, 0x69, 0x0d, 0xb7, 0x36, 0x09, 0x31, 0x7b, 0x4a,
0x00, 0x31, 0x30, 0xe2, 0x2a, 0x58, 0x4c, 0x5d, 0xc7, 0x7b, 0x6e, 0x86, 0x96, 0x3f, 0x21, 0xa1,
0xb2, 0xcf, 0x6f, 0x88, 0xd8, 0x3a, 0x60, 0xc6, 0x5f, 0x8a, 0x7f, 0xfa, 0xa6, 0xba, 0x53, 0xf3,
0xa0, 0xb8, 0x8e, 0x13, 0x69, 0x90, 0x8e, 0xc7, 0x01, 0x09, 0x99, 0x60, 0xb2, 0x38, 0x9e, 0xad,
0x65, 0x90, 0x61, 0x0c, 0x70, 0x19, 0x20, 0x10, 0x2f, 0xac, 0xe0, 0x82, 0x49, 0xa3, 0x8c, 0xd9,
0x38, 0x2a, 0xfc, 0x17, 0xc4, 0x7a, 0x6e, 0x32, 0x07, 0x17, 0x86, 0x14, 0x19, 0x1e, 0x5b, 0xc1,
0x45, 0xbc, 0xdf, 0xaf, 0x21, 0xcf, 0x0f, 0x02, 0x7d, 0x0e, 0xd2, 0x88, 0xce, 0xbd, 0x70, 0x73,
0x39, 0xec, 0xa7, 0x7b, 0x0b, 0xf3, 0xc4, 0x99, 0xad, 0x81, 0xb5, 0x53, 0x28, 0xc4, 0x2e, 0x74,
0x7f, 0xdd, 0xf8, 0xc4, 0xc6, 0x9d, 0x6b, 0x9c, 0x6f, 0xdf, 0x16, 0x97, 0x96, 0x3b, 0xe7, 0x3f,
0x2f, 0x62, 0x3e, 0xa9, 0xfd, 0x4d, 0x80, 0x02, 0x8e, 0xce, 0x39, 0x08, 0x53, 0xf7, 0x4c, 0x6e,
0xeb, 0x9e, 0xd9, 0x54, 0x64, 0x66, 0xab, 0x22, 0x93, 0xa2, 0xca, 0xa6, 0x8a, 0x6a, 0xc3, 0x9c,
0xf8, 0x46, 0xe6, 0x72, 0x6f, 0x60, 0x2e, 0x9f, 0x62, 0xee, 0x3e, 0xec, 0x8d, 0x7d, 0x3a, 0x65,
0x37, 0x09, 0xf5, 0x2d, 0x7f, 0x11, 0x17, 0xc0, 0x6e, 0x64, 0x1d, 0x24, 0xc6, 0x6d, 0x82, 0xa5,
0x6d, 0x82, 0x6b, 0x26, 0x48, 0x98, 0x04, 0x33, 0xea, 0x05, 0xe4, 0xad, 0x39, 0x21, 0x10, 0x6d,
0x2b, 0xb4, 0x58, 0x46, 0x65, 0xcc, 0xc6, 0xe8, 0x01, 0x88, 0x23, 0x6a, 0xf3, 0x7c, 0xf6, 0xd2,
0x02, 0xd3, 0x7d, 0x9f, 0xfa, 0x4d, 0x6a, 0x13, 0xcc, 0x00, 0xb5, 0x19, 0xc8, 0x2d, 0xfa, 0xc2,
0x73, 0xa9, 0x65, 0xf7, 0x7c, 0x3a, 0x89, 0xda, 0xfd, 0x5b, 0xdb, 0x56, 0x0b, 0x0a, 0x73, 0xd6,
0xd8, 0x92, 0xc6, 0x75, 0x6f, 0xbb, 0xd1, 0x5c, 0x0f, 0xc4, 0xbb, 0x60, 0x52, 0x9d, 0xf1, 0xd2,
0xda, 0x3f, 0x04, 0x50, 0xdf, 0x8e, 0x46, 0x6d, 0x28, 0x71, 0xa4, 0x99, 0x7a, 0xe1, 0x1c, 0xbd,
0xcb, 0x46, 0xac, 0xc7, 0xc1, 0x7c, 0x3d, 0x7e, 0xe3, 0xf5, 0x98, 0xea, 0x26, 0xd9, 0x77, 0xeb,
0x26, 0x0f, 0x60, 0x97, 0x97, 0x77, 0xf2, 0x18, 0x10, 0xb5, 0xec, 0x51, 0xae, 0x91, 0x91, 0x77,
0x70, 0x79, 0xc8, 0xcb, 0x8c, 0xd9, 0x6b, 0x79, 0x10, 0x7b, 0x8e, 0x37, 0xa9, 0x55, 0x21, 0xd7,
0x74, 0x29, 0x3b, 0xb0, 0xbc, 0x4f, 0xac, 0x80, 0x7a, 0x09, 0x8f, 0x7c, 0x76, 0xfc, 0xf7, 0x0c,
0x94, 0x52, 0x0f, 0x35, 0xf4, 0x08, 0xf6, 0x9a, 0x9d, 0xf3, 0xfe, 0x40, 0xc7, 0x66, 0xb3, 0x6b,
0x9c, 0xb6, 0xcf, 0xe4, 0x1d, 0xf5, 0x70, 0xb9, 0xd2, 0x94, 0xe9, 0x06, 0xb4, 0xfd, 0x06, 0xab,
0x42, 0xae, 0x6d, 0xb4, 0xf4, 0xdf, 0xcb, 0x82, 0x7a, 0x7b, 0xb9, 0xd2, 0xe4, 0x14, 0x90, 0x5f,
0x68, 0x9f, 0x40, 0x99, 0x01, 0xcc, 0xf3, 0x5e, 0xab, 0x3e, 0xd0, 0xe5, 0x8c, 0xaa, 0x2e, 0x57,
0xda, 0xc1, 0x75, 0x5c, 0xcc, 0xf9, 0x87, 0x50, 0xc0, 0xfa, 0xef, 0xce, 0xf5, 0xfe, 0x40, 0xce,
0xaa, 0x07, 0xcb, 0x95, 0x86, 0x52, 0xc0, 0xa4, 0xa4, 0xee, 0x83, 0x84, 0xf5, 0x7e, 0xaf, 0x6b,
0xf4, 0x75, 0x59, 0x54, 0x7f, 0xb4, 0x5c, 0x69, 0xb7, 0xb6, 0x50, 0xb1, 0x4a, 0x7f, 0x06, 0xfb,
0xad, 0xee, 0x97, 0x46, 0xa7, 0x5b, 0x6f, 0x99, 0x3d, 0xdc, 0x3d, 0xc3, 0x7a, 0xbf, 0x2f, 0xe7,
0xd4, 0xea, 0x72, 0xa5, 0xbd, 0x9f, 0xc2, 0xdf, 0x10, 0xdd, 0x07, 0x20, 0xf6, 0xda, 0xc6, 0x99,
0x9c, 0x57, 0x6f, 0x2d, 0x57, 0xda, 0x7b, 0x29, 0x68, 0x44, 0x6a, 0x94, 0x71, 0xb3, 0xd3, 0xed,
0xeb, 0x72, 0xe1, 0x46, 0xc6, 0x8c, 0xec, 0xe3, 0x3f, 0x00, 0xba, 0xf9, 0x94, 0x45, 0xf7, 0x40,
0x34, 0xba, 0x86, 0x2e, 0xef, 0xf0, 0xfc, 0x6f, 0x22, 0x0c, 0xea, 0x11, 0x54, 0x83, 0x6c, 0xe7,
0xab, 0x2f, 0x64, 0x41, 0xfd, 0xf1, 0x72, 0xa5, 0xdd, 0xb9, 0x09, 0xea, 0x7c, 0xf5, 0xc5, 0x31,
0x85, 0x52, 0x3a, 0x70, 0x0d, 0xa4, 0x27, 0xfa, 0xa0, 0xde, 0xaa, 0x0f, 0xea, 0xf2, 0x0e, 0xff,
0xa5, 0xc4, 0xfd, 0x84, 0x84, 0x16, 0x2b, 0xc2, 0x43, 0xc8, 0x19, 0xfa, 0x53, 0x1d, 0xcb, 0x82,
0xba, 0xbf, 0x5c, 0x69, 0xbb, 0x09, 0xc0, 0x20, 0x97, 0xc4, 0x47, 0x15, 0xc8, 0xd7, 0x3b, 0x5f,
0xd6, 0x9f, 0xf5, 0xe5, 0x8c, 0x8a, 0x96, 0x2b, 0x6d, 0x2f, 0x71, 0xd7, 0xdd, 0x17, 0xd6, 0x22,
0x38, 0xfe, 0x9f, 0x00, 0xe5, 0xf4, 0xf5, 0x8d, 0x2a, 0x20, 0x9e, 0xb6, 0x3b, 0x7a, 0xb2, 0x5d,
0xda, 0x17, 0x8d, 0xd1, 0x11, 0x14, 0x5b, 0x6d, 0xac, 0x37, 0x07, 0x5d, 0xfc, 0x2c, 0xc9, 0x25,
0x0d, 0x6a, 0x39, 0x3e, 0x13, 0xf8, 0x02, 0xfd, 0x02, 0xca, 0xfd, 0x67, 0x4f, 0x3a, 0x6d, 0xe3,
0xb7, 0x26, 0x8b, 0x98, 0x51, 0x1f, 0x2c, 0x57, 0xda, 0xdd, 0x2d, 0x30, 0x99, 0xf9, 0x64, 0x64,
0x85, 0xc4, 0xee, 0xf3, 0x1b, 0x26, 0x72, 0x4a, 0x02, 0x6a, 0xc2, 0x7e, 0xb2, 0x74, 0xb3, 0x59,
0x56, 0xfd, 0x64, 0xb9, 0xd2, 0x3e, 0xfa, 0xde, 0xf5, 0xeb, 0xdd, 0x25, 0x01, 0xdd, 0x83, 0x42,
0x1c, 0x24, 0x51, 0x52, 0x7a, 0x69, 0xbc, 0xe0, 0xf8, 0x2f, 0x02, 0x14, 0xd7, 0xed, 0x2a, 0x22,
0xdc, 0xe8, 0x9a, 0x3a, 0xc6, 0x5d, 0x9c, 0x30, 0xb0, 0x76, 0x1a, 0x94, 0x0d, 0xd1, 0x5d, 0x28,
0x9c, 0xe9, 0x86, 0x8e, 0xdb, 0xcd, 0xa4, 0x30, 0xd6, 0x90, 0x33, 0xe2, 0x11, 0xdf, 0x19, 0xa1,
0x8f, 0xa1, 0x6c, 0x74, 0xcd, 0xfe, 0x79, 0xf3, 0x71, 0x92, 0x3a, 0xdb, 0x3f, 0x15, 0xaa, 0x3f,
0x1f, 0x5d, 0x30, 0x3e, 0x8f, 0xa3, 0x1a, 0x7a, 0x5a, 0xef, 0xb4, 0x5b, 0x1c, 0x9a, 0x55, 0x95,
0xe5, 0x4a, 0xbb, 0xbd, 0x86, 0xb6, 0xf9, 0x3b, 0x26, 0xc2, 0x1e, 0xdb, 0x50, 0xf9, 0xfe, 0xc6,
0x84, 0x34, 0xc8, 0xd7, 0x7b, 0x3d, 0xdd, 0x68, 0x25, 0x7f, 0xbf, 0xf1, 0xd5, 0x67, 0x33, 0xe2,
0xd9, 0x11, 0xe2, 0xb4, 0x8b, 0xcf, 0xf4, 0x41, 0xf2, 0xf3, 0x1b, 0xc4, 0x29, 0x8d, 0xae, 0xf7,
0xc6, 0xe1, 0xab, 0xef, 0x2a, 0x3b, 0xdf, 0x7e, 0x57, 0xd9, 0x79, 0x75, 0x55, 0x11, 0xbe, 0xbd,
0xaa, 0x08, 0xff, 0xba, 0xaa, 0xec, 0xfc, 0xe7, 0xaa, 0x22, 0x7c, 0xf3, 0xef, 0x8a, 0x30, 0xcc,
0xb3, 0x46, 0xf6, 0xf9, 0xff, 0x03, 0x00, 0x00, 0xff, 0xff, 0xe5, 0x17, 0x2b, 0x62, 0xdd, 0x0e,
0x00, 0x00,
}

View File

@@ -105,6 +105,7 @@ message FileInfo {
bool no_permissions = 8;
Vector version = 9 [(gogoproto.nullable) = false];
int64 sequence = 10;
int32 block_size = 13 [(gogoproto.customname) = "RawBlockSize"];
repeated BlockInfo Blocks = 16 [(gogoproto.nullable) = false];
string symlink_target = 17;
@@ -145,6 +146,7 @@ message Request {
int32 size = 5;
bytes hash = 6;
bool from_temporary = 7;
uint32 weak_hash = 8;
}
// Response

View File

@@ -14,16 +14,11 @@ import (
"time"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/sha256"
)
const (
SyntheticDirectorySize = 128
)
var (
sha256OfEmptyBlock = sha256.Sum256(make([]byte, BlockSize))
HelloMessageMagic = uint32(0x2EA7D90B)
HelloMessageMagic = uint32(0x2EA7D90B)
)
func (m Hello) Magic() uint32 {
@@ -36,8 +31,8 @@ func (f FileInfo) String() string {
return fmt.Sprintf("Directory{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, Deleted:%v, Invalid:%v, NoPermissions:%v}",
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.Deleted, f.Invalid, f.NoPermissions)
case FileInfoTypeFile:
return fmt.Sprintf("File{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v, Blocks:%v}",
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions, f.Blocks)
return fmt.Sprintf("File{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v, BlockSize:%d, Blocks:%v}",
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions, f.RawBlockSize, f.Blocks)
case FileInfoTypeSymlink, FileInfoTypeDeprecatedSymlinkDirectory, FileInfoTypeDeprecatedSymlinkFile:
return fmt.Sprintf("Symlink{Name:%q, Type:%v, Sequence:%d, Version:%v, Deleted:%v, Invalid:%v, NoPermissions:%v, SymlinkTarget:%q}",
f.Name, f.Type, f.Sequence, f.Version, f.Deleted, f.Invalid, f.NoPermissions, f.SymlinkTarget)
@@ -81,6 +76,13 @@ func (f FileInfo) FileSize() int64 {
return f.Size
}
func (f FileInfo) BlockSize() int {
if f.RawBlockSize == 0 {
return MinBlockSize
}
return int(f.RawBlockSize)
}
func (f FileInfo) FileName() string {
return f.Name
}
@@ -204,7 +206,10 @@ func (b BlockInfo) String() string {
// IsEmpty returns true if the block is a full block of zeroes.
func (b BlockInfo) IsEmpty() bool {
return b.Size == BlockSize && bytes.Equal(b.Hash, sha256OfEmptyBlock[:])
if v, ok := sha256OfEmptyBlock[int(b.Size)]; ok {
return bytes.Equal(b.Hash, v[:])
}
return false
}
type IndexID uint64

View File

@@ -11,6 +11,7 @@ type TestModel struct {
offset int64
size int
hash []byte
weakHash uint32
fromTemporary bool
closedCh chan struct{}
closedErr error
@@ -28,12 +29,13 @@ 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, fromTemporary bool, buf []byte) error {
func (t *TestModel) Request(deviceID DeviceID, folder, name string, offset int64, hash []byte, weakHash uint32, fromTemporary bool, buf []byte) error {
t.folder = folder
t.name = name
t.offset = offset
t.size = len(buf)
t.hash = hash
t.weakHash = weakHash
t.fromTemporary = fromTemporary
copy(buf, t.data)
return nil

View File

@@ -17,6 +17,8 @@ var (
// generate returns a check digit for the string s, which should be composed
// of characters from the Alphabet a.
// Doesn't follow the actual Luhn algorithm
// see https://forum.syncthing.net/t/v0-9-0-new-node-id-format/478/6 for more.
func (a luhnAlphabet) generate(s string) (rune, error) {
factor := 1
sum := 0

View 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, fromTemporary bool, buf []byte) error {
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, weakHash uint32, fromTemporary bool, buf []byte) error {
name = norm.NFD.String(name)
return m.Model.Request(deviceID, folder, name, offset, hash, fromTemporary, buf)
return m.Model.Request(deviceID, folder, name, offset, hash, weakHash, fromTemporary, buf)
}

View File

@@ -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, fromTemporary bool, buf []byte) error {
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, weakHash uint32, fromTemporary bool, buf []byte) error {
if strings.Contains(name, `\`) {
l.Warnf("Dropping request for %s, contains invalid path separator", name)
return ErrNoSuchFile
}
name = filepath.FromSlash(name)
return m.Model.Request(deviceID, folder, name, offset, hash, fromTemporary, buf)
return m.Model.Request(deviceID, folder, name, offset, hash, weakHash, fromTemporary, buf)
}
func fixupFiles(files []FileInfo) []FileInfo {

View File

@@ -3,6 +3,7 @@
package protocol
import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
@@ -16,13 +17,51 @@ import (
)
const (
// BlockSize is the standard data block size (128 KiB)
BlockSize = 128 << 10
// Shifts
KiB = 10
MiB = 20
GiB = 30
)
const (
// MaxMessageLen is the largest message size allowed on the wire. (500 MB)
MaxMessageLen = 500 * 1000 * 1000
// MinBlockSize is the minimum block size allowed
MinBlockSize = 128 << KiB
// MaxBlockSize is the maximum block size allowed
MaxBlockSize = 16 << MiB
// DesiredPerFileBlocks is the number of blocks we aim for per file
DesiredPerFileBlocks = 2000
)
// BlockSizes is the list of valid block sizes, from min to max
var BlockSizes []int
// For each block size, the hash of a block of all zeroes
var sha256OfEmptyBlock = make(map[int][sha256.Size]byte)
func init() {
for blockSize := MinBlockSize; blockSize <= MaxBlockSize; blockSize *= 2 {
BlockSizes = append(BlockSizes, blockSize)
sha256OfEmptyBlock[blockSize] = sha256.Sum256(make([]byte, blockSize))
}
}
// BlockSize returns the block size to use for the given file size
func BlockSize(fileSize int64) int {
var blockSize int
for _, blockSize = range BlockSizes {
if fileSize < int64(DesiredPerFileBlocks*blockSize) {
break
}
}
return blockSize
}
const (
stateInitial = iota
stateReady
@@ -68,7 +107,7 @@ type Model interface {
// An index update was received from the peer device
IndexUpdate(deviceID DeviceID, folder string, files []FileInfo)
// A request was made by the peer device
Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, fromTemporary bool, buf []byte) error
Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, weakHash uint32, fromTemporary bool, buf []byte) error
// A cluster configuration message was received
ClusterConfig(deviceID DeviceID, config ClusterConfig)
// The peer device closed the connection
@@ -83,7 +122,7 @@ type Connection interface {
Name() string
Index(folder string, files []FileInfo) error
IndexUpdate(folder string, files []FileInfo) error
Request(folder string, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error)
Request(folder string, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error)
ClusterConfig(config ClusterConfig)
DownloadProgress(folder string, updates []FileDownloadProgressUpdate)
Statistics() Statistics
@@ -158,7 +197,7 @@ func NewConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, receiv
awaiting: make(map[int32]chan asyncResult),
outbox: make(chan asyncMessage),
closed: make(chan struct{}),
pool: bufferPool{minSize: BlockSize},
pool: bufferPool{minSize: MinBlockSize},
compression: compress,
}
@@ -215,7 +254,7 @@ func (c *rawConnection) IndexUpdate(folder string, idx []FileInfo) error {
}
// Request returns the bytes for the specified block after fetching them from the connected peer.
func (c *rawConnection) Request(folder string, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
func (c *rawConnection) Request(folder string, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
c.nextIDMut.Lock()
id := c.nextID
c.nextID++
@@ -236,6 +275,7 @@ func (c *rawConnection) Request(folder string, name string, offset int64, size i
Offset: offset,
Size: int32(size),
Hash: hash,
WeakHash: weakHash,
FromTemporary: fromTemporary,
}, nil)
if !ok {
@@ -533,7 +573,7 @@ func checkFilename(name string) error {
func (c *rawConnection) handleRequest(req Request) {
size := int(req.Size)
usePool := size <= BlockSize
usePool := size <= MaxBlockSize
var buf []byte
var done chan struct{}
@@ -545,7 +585,7 @@ func (c *rawConnection) handleRequest(req Request) {
buf = make([]byte, size)
}
err := c.receiver.Request(c.id, req.Folder, req.Name, req.Offset, req.Hash, req.FromTemporary, buf)
err := c.receiver.Request(c.id, req.Folder, req.Name, req.Offset, req.Hash, req.WeakHash, req.FromTemporary, buf)
if err != nil {
c.send(&Response{
ID: req.ID,

View File

@@ -11,7 +11,6 @@ import (
"strings"
"testing"
"testing/quick"
"time"
"encoding/hex"
@@ -73,7 +72,7 @@ func TestClose(t *testing.T) {
c0.Index("default", nil)
c0.Index("default", nil)
if _, err := c0.Request("default", "foo", 0, 0, nil, false); err == nil {
if _, err := c0.Request("default", "foo", 0, 0, nil, 0, false); err == nil {
t.Error("Request should return an error")
}
}
@@ -234,48 +233,6 @@ func testMarshal(t *testing.T, prefix string, m1, m2 message) bool {
return true
}
func TestMarshalledIndexMessageSize(t *testing.T) {
// We should be able to handle a 1 TiB file without
// blowing the default max message size.
if testing.Short() {
t.Skip("this test requires a lot of memory")
return
}
const (
maxMessageSize = MaxMessageLen
fileSize = 1 << 40
blockSize = BlockSize
)
f := FileInfo{
Name: "a normal length file name withoout any weird stuff.txt",
Type: FileInfoTypeFile,
Size: fileSize,
Permissions: 0666,
ModifiedS: time.Now().Unix(),
Version: Vector{Counters: []Counter{{ID: 1 << 60, Value: 1}, {ID: 2 << 60, Value: 1}}},
Blocks: make([]BlockInfo, fileSize/blockSize),
}
for i := 0; i < fileSize/blockSize; i++ {
f.Blocks[i].Offset = int64(i) * blockSize
f.Blocks[i].Size = blockSize
f.Blocks[i].Hash = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 20, 1, 2, 3, 4, 5, 6, 7, 8, 9, 30, 1, 2}
}
idx := Index{
Folder: "some folder ID",
Files: []FileInfo{f},
}
msgSize := idx.ProtoSize()
if msgSize > maxMessageSize {
t.Errorf("Message size %d bytes is larger than max %d", msgSize, maxMessageSize)
}
}
func TestLZ4Compression(t *testing.T) {
c := new(rawConnection)
@@ -400,3 +357,36 @@ func TestCheckConsistency(t *testing.T) {
}
}
}
func TestBlockSize(t *testing.T) {
cases := []struct {
fileSize int64
blockSize int
}{
{1 << KiB, 128 << KiB},
{1 << MiB, 128 << KiB},
{499 << MiB, 256 << KiB},
{500 << MiB, 512 << KiB},
{501 << MiB, 512 << KiB},
{1 << GiB, 1 << MiB},
{2 << GiB, 2 << MiB},
{3 << GiB, 2 << MiB},
{500 << GiB, 16 << MiB},
{50000 << GiB, 16 << MiB},
}
for _, tc := range cases {
size := BlockSize(tc.fileSize)
if size != tc.blockSize {
t.Errorf("BlockSize(%d), size=%d, expected %d", tc.fileSize, size, tc.blockSize)
}
}
}
var blockSize int
func BenchmarkBlockSize(b *testing.B) {
for i := 0; i < b.N; i++ {
blockSize = BlockSize(16 << 30)
}
}

View File

@@ -34,7 +34,7 @@ func (c wireFormatConnection) IndexUpdate(folder string, fs []FileInfo) error {
return c.Connection.IndexUpdate(folder, myFs)
}
func (c wireFormatConnection) Request(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
func (c wireFormatConnection) Request(folder, name string, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
name = norm.NFC.String(filepath.ToSlash(name))
return c.Connection.Request(folder, name, offset, size, hash, fromTemporary)
return c.Connection.Request(folder, name, offset, size, hash, weakHash, fromTemporary)
}

View File

@@ -62,28 +62,24 @@ func HashFile(ctx context.Context, fs fs.Filesystem, path string, blockSize int,
// workers are used in parallel. The outbox will become closed when the inbox
// is closed and all items handled.
type parallelHasher struct {
fs fs.Filesystem
blockSize int
workers int
outbox chan<- protocol.FileInfo
inbox <-chan protocol.FileInfo
counter Counter
done chan<- struct{}
useWeakHashes bool
wg sync.WaitGroup
fs fs.Filesystem
workers int
outbox chan<- protocol.FileInfo
inbox <-chan protocol.FileInfo
counter Counter
done chan<- struct{}
wg sync.WaitGroup
}
func newParallelHasher(ctx context.Context, fs fs.Filesystem, blockSize, workers int, outbox chan<- protocol.FileInfo, inbox <-chan protocol.FileInfo, counter Counter, done chan<- struct{}, useWeakHashes bool) {
func newParallelHasher(ctx context.Context, fs fs.Filesystem, workers int, outbox chan<- protocol.FileInfo, inbox <-chan protocol.FileInfo, counter Counter, done chan<- struct{}) {
ph := &parallelHasher{
fs: fs,
blockSize: blockSize,
workers: workers,
outbox: outbox,
inbox: inbox,
counter: counter,
done: done,
useWeakHashes: useWeakHashes,
wg: sync.NewWaitGroup(),
fs: fs,
workers: workers,
outbox: outbox,
inbox: inbox,
counter: counter,
done: done,
wg: sync.NewWaitGroup(),
}
for i := 0; i < workers; i++ {
@@ -108,7 +104,7 @@ func (ph *parallelHasher) hashFiles(ctx context.Context) {
panic("Bug. Asked to hash a directory or a deleted file.")
}
blocks, err := HashFile(ctx, ph.fs, f.Name, ph.blockSize, ph.counter, ph.useWeakHashes)
blocks, err := HashFile(ctx, ph.fs, f.Name, f.BlockSize(), ph.counter, true)
if err != nil {
l.Debugln("hash error:", f.Name, err)
continue

View File

@@ -7,6 +7,7 @@
package scanner
import (
"bytes"
"context"
"hash"
"io"
@@ -107,6 +108,29 @@ func Blocks(ctx context.Context, r io.Reader, blocksize int, sizehint int64, cou
return blocks, nil
}
func Validate(buf, hash []byte, weakHash uint32) bool {
rd := bytes.NewReader(buf)
if weakHash != 0 {
whf := adler32.New()
if _, err := io.Copy(whf, rd); err == nil && whf.Sum32() == weakHash {
return true
}
// Copy error or mismatch, go to next algo.
rd.Seek(0, io.SeekStart)
}
if len(hash) > 0 {
hf := sha256.New()
if _, err := io.Copy(hf, rd); err == nil {
// Sum allocates, so let's hope we don't hit this often.
return bytes.Equal(hf.Sum(nil), hash)
}
}
// Both algos failed or no hashes were specified. Assume it's all good.
return true
}
type noopHash struct{}
func (noopHash) Sum32() uint32 { return 0 }

View File

@@ -125,7 +125,7 @@ func TestAdler32Variants(t *testing.T) {
}
// protocol block sized data
data := make([]byte, protocol.BlockSize)
data := make([]byte, protocol.MinBlockSize)
for i := 0; i < 5; i++ {
rand.Read(data)
if !checkFn(data) {

View File

@@ -55,6 +55,48 @@ func (i infiniteFS) Open(name string) (fs.File, error) {
return &fakeFile{name, i.filesize, 0}, nil
}
type singleFileFS struct {
fs.Filesystem
name string
filesize int64
}
func (s singleFileFS) Lstat(name string) (fs.FileInfo, error) {
switch name {
case ".":
return fakeInfo{".", 0}, nil
case s.name:
return fakeInfo{s.name, s.filesize}, nil
default:
return nil, errors.New("no such file")
}
}
func (s singleFileFS) Stat(name string) (fs.FileInfo, error) {
switch name {
case ".":
return fakeInfo{".", 0}, nil
case s.name:
return fakeInfo{s.name, s.filesize}, nil
default:
return nil, errors.New("no such file")
}
}
func (s singleFileFS) DirNames(name string) ([]string, error) {
if name != "." {
return nil, errors.New("no such file")
}
return []string{s.name}, nil
}
func (s singleFileFS) Open(name string) (fs.File, error) {
if name != s.name {
return nil, errors.New("no such file")
}
return &fakeFile{s.name, s.filesize, 0}, nil
}
type fakeInfo struct {
name string
size int64

View File

@@ -42,8 +42,6 @@ type Config struct {
Folder string
// Limit walking to these paths within Dir, or no limit if Sub is empty
Subs []string
// BlockSize controls the size of the block used when hashing.
BlockSize int
// If Matcher is not nil, it is used to identify files to ignore which were specified by the user.
Matcher *ignore.Matcher
// Number of hours to keep temporary files for
@@ -66,8 +64,8 @@ type Config struct {
// Optional progress tick interval which defines how often FolderScanProgress
// events are emitted. Negative number means disabled.
ProgressTickIntervalS int
// Whether or not we should also compute weak hashes
UseWeakHashes bool
// Whether to use large blocks for large files or the old standard of 128KiB for everything.
UseLargeBlocks bool
}
type CurrentFiler interface {
@@ -98,7 +96,7 @@ type walker struct {
// Walk returns the list of files found in the local folder by scanning the
// file system. Files are blockwise hashed.
func (w *walker) walk(ctx context.Context) chan protocol.FileInfo {
l.Debugln("Walk", w.Subs, w.BlockSize, w.Matcher)
l.Debugln("Walk", w.Subs, w.Matcher)
toHashChan := make(chan protocol.FileInfo)
finishedChan := make(chan protocol.FileInfo)
@@ -120,7 +118,7 @@ func (w *walker) walk(ctx context.Context) chan protocol.FileInfo {
// We're not required to emit scan progress events, just kick off hashers,
// and feed inputs directly from the walker.
if w.ProgressTickIntervalS < 0 {
newParallelHasher(ctx, w.Filesystem, w.BlockSize, w.Hashers, finishedChan, toHashChan, nil, nil, w.UseWeakHashes)
newParallelHasher(ctx, w.Filesystem, w.Hashers, finishedChan, toHashChan, nil, nil)
return finishedChan
}
@@ -151,7 +149,7 @@ func (w *walker) walk(ctx context.Context) chan protocol.FileInfo {
done := make(chan struct{})
progress := newByteCounter()
newParallelHasher(ctx, w.Filesystem, w.BlockSize, w.Hashers, finishedChan, realToHashChan, progress, done, w.UseWeakHashes)
newParallelHasher(ctx, w.Filesystem, w.Hashers, finishedChan, realToHashChan, progress, done)
// A routine which actually emits the FolderScanProgress events
// every w.ProgressTicker ticks, until the hasher routines terminate.
@@ -161,7 +159,7 @@ func (w *walker) walk(ctx context.Context) chan protocol.FileInfo {
for {
select {
case <-done:
l.Debugln("Walk progress done", w.Folder, w.Subs, w.BlockSize, w.Matcher)
l.Debugln("Walk progress done", w.Folder, w.Subs, w.Matcher)
ticker.Stop()
return
case <-ticker.C:
@@ -285,32 +283,52 @@ func (w *walker) walkRegular(ctx context.Context, relPath string, info fs.FileIn
curMode |= 0111
}
cf, ok := w.CurrentFiler.CurrentFile(relPath)
blockSize := protocol.MinBlockSize
curFile, hasCurFile := w.CurrentFiler.CurrentFile(relPath)
if w.UseLargeBlocks {
blockSize = protocol.BlockSize(info.Size())
if hasCurFile {
// Check if we should retain current block size.
curBlockSize := curFile.BlockSize()
if blockSize > curBlockSize && blockSize/curBlockSize <= 2 {
// New block size is larger, but not more than twice larger.
// Retain.
blockSize = curBlockSize
} else if curBlockSize > blockSize && curBlockSize/blockSize <= 2 {
// Old block size is larger, but not more than twice larger.
// Retain.
blockSize = curBlockSize
}
}
}
f := protocol.FileInfo{
Name: relPath,
Type: protocol.FileInfoTypeFile,
Version: cf.Version.Update(w.ShortID),
Version: curFile.Version.Update(w.ShortID),
Permissions: curMode & uint32(maskModePerm),
NoPermissions: w.IgnorePerms,
ModifiedS: info.ModTime().Unix(),
ModifiedNs: int32(info.ModTime().Nanosecond()),
ModifiedBy: w.ShortID,
Size: info.Size(),
RawBlockSize: int32(blockSize),
}
if ok {
if cf.IsEquivalent(f, w.IgnorePerms, true) {
if hasCurFile {
if curFile.IsEquivalent(f, w.IgnorePerms, true) {
return nil
}
if cf.Invalid {
if curFile.Invalid {
// We do not want to override the global version with the file we
// currently have. 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.
f.Version = f.Version.DropOthers(w.ShortID)
}
l.Debugln("rescan:", cf, info.ModTime().Unix(), info.Mode()&fs.ModePerm)
l.Debugln("rescan:", curFile, info.ModTime().Unix(), info.Mode()&fs.ModePerm)
}
l.Debugln("to hash:", relPath, f)

View File

@@ -65,7 +65,6 @@ func TestWalkSub(t *testing.T) {
fchan := Walk(context.TODO(), Config{
Filesystem: fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata"),
Subs: []string{"dir2"},
BlockSize: 128 * 1024,
Matcher: ignores,
Hashers: 2,
})
@@ -98,7 +97,6 @@ func TestWalk(t *testing.T) {
fchan := Walk(context.TODO(), Config{
Filesystem: fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata"),
BlockSize: 128 * 1024,
Matcher: ignores,
Hashers: 2,
})
@@ -221,11 +219,11 @@ func TestNormalization(t *testing.T) {
// make sure it all gets done. In production, things will be correct
// eventually...
_, err := walkDir(fs, "testdata/normalization")
_, err := walkDir(fs, "testdata/normalization", nil)
if err != nil {
t.Fatal(err)
}
tmp, err := walkDir(fs, "testdata/normalization")
tmp, err := walkDir(fs, "testdata/normalization", nil)
if err != nil {
t.Fatal(err)
}
@@ -272,7 +270,7 @@ func TestWalkSymlinkUnix(t *testing.T) {
for _, path := range []string{".", "link"} {
// Scan it
files, _ := walkDir(fs.NewFilesystem(fs.FilesystemTypeBasic, "_symlinks"), path)
files, _ := walkDir(fs.NewFilesystem(fs.FilesystemTypeBasic, "_symlinks"), path, nil)
// Verify that we got one symlink and with the correct attributes
if len(files) != 1 {
@@ -303,7 +301,7 @@ func TestWalkSymlinkWindows(t *testing.T) {
for _, path := range []string{".", "link"} {
// Scan it
files, _ := walkDir(fs.NewFilesystem(fs.FilesystemTypeBasic, "_symlinks"), path)
files, _ := walkDir(fs.NewFilesystem(fs.FilesystemTypeBasic, "_symlinks"), path, nil)
// Verify that we got zero symlinks
if len(files) != 0 {
@@ -332,7 +330,7 @@ func TestWalkRootSymlink(t *testing.T) {
}
// Scan it
files, err := walkDir(fs.NewFilesystem(fs.FilesystemTypeBasic, link), ".")
files, err := walkDir(fs.NewFilesystem(fs.FilesystemTypeBasic, link), ".", nil)
if err != nil {
t.Fatal("Expected no error when root folder path is provided via a symlink: " + err.Error())
}
@@ -342,13 +340,83 @@ func TestWalkRootSymlink(t *testing.T) {
}
}
func walkDir(fs fs.Filesystem, dir string) ([]protocol.FileInfo, error) {
func TestBlocksizeHysteresis(t *testing.T) {
// Verify that we select the right block size in the presence of old
// file information.
sf := fs.NewWalkFilesystem(&singleFileFS{
name: "testfile.dat",
filesize: 500 << 20, // 500 MiB
})
current := make(fakeCurrentFiler)
runTest := func(expectedBlockSize int) {
files, err := walkDir(sf, ".", current)
if err != nil {
t.Fatal(err)
}
if len(files) != 1 {
t.Fatalf("expected one file, not %d", len(files))
}
if s := files[0].BlockSize(); s != expectedBlockSize {
t.Fatalf("incorrect block size %d != expected %d", s, expectedBlockSize)
}
}
// Scan with no previous knowledge. We should get a 512 KiB block size.
runTest(512 << 10)
// Scan on the assumption that previous size was 256 KiB. Retain 256 KiB
// block size.
current["testfile.dat"] = protocol.FileInfo{
Name: "testfile.dat",
Size: 500 << 20,
RawBlockSize: 256 << 10,
}
runTest(256 << 10)
// Scan on the assumption that previous size was 1 MiB. Retain 1 MiB
// block size.
current["testfile.dat"] = protocol.FileInfo{
Name: "testfile.dat",
Size: 500 << 20,
RawBlockSize: 1 << 20,
}
runTest(1 << 20)
// Scan on the assumption that previous size was 128 KiB. Move to 512
// KiB because the difference is large.
current["testfile.dat"] = protocol.FileInfo{
Name: "testfile.dat",
Size: 500 << 20,
RawBlockSize: 128 << 10,
}
runTest(512 << 10)
// Scan on the assumption that previous size was 2 MiB. Move to 512
// KiB because the difference is large.
current["testfile.dat"] = protocol.FileInfo{
Name: "testfile.dat",
Size: 500 << 20,
RawBlockSize: 2 << 20,
}
runTest(512 << 10)
}
func walkDir(fs fs.Filesystem, dir string, cfiler CurrentFiler) ([]protocol.FileInfo, error) {
fchan := Walk(context.TODO(), Config{
Filesystem: fs,
Subs: []string{dir},
BlockSize: 128 * 1024,
AutoNormalize: true,
Hashers: 2,
Filesystem: fs,
Subs: []string{dir},
AutoNormalize: true,
Hashers: 2,
UseLargeBlocks: true,
CurrentFiler: cfiler,
})
var tmp []protocol.FileInfo
@@ -410,7 +478,7 @@ func BenchmarkHashFile(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := HashFile(context.TODO(), fs.NewFilesystem(fs.FilesystemTypeBasic, ""), testdataName, protocol.BlockSize, nil, true); err != nil {
if _, err := HashFile(context.TODO(), fs.NewFilesystem(fs.FilesystemTypeBasic, ""), testdataName, protocol.MinBlockSize, nil, true); err != nil {
b.Fatal(err)
}
}
@@ -451,7 +519,6 @@ func TestStopWalk(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
fchan := Walk(ctx, Config{
Filesystem: fs,
BlockSize: 128 * 1024,
Hashers: numHashers,
ProgressTickIntervalS: -1, // Don't attempt to build the full list of files before starting to scan...
})
@@ -513,7 +580,7 @@ func TestIssue4799(t *testing.T) {
}
fd.Close()
files, err := walkDir(fs, "/foo")
files, err := walkDir(fs, "/foo", nil)
if err != nil {
t.Fatal(err)
}
@@ -540,7 +607,6 @@ func TestIssue4841(t *testing.T) {
fchan := Walk(context.TODO(), Config{
Filesystem: fs,
Subs: nil,
BlockSize: 128 * 1024,
AutoNormalize: true,
Hashers: 2,
CurrentFiler: fakeCurrentFiler{

View File

@@ -11,6 +11,10 @@ import (
"testing"
"github.com/chmduquesne/rollinghash/adler32"
"github.com/chmduquesne/rollinghash/bozo32"
"github.com/chmduquesne/rollinghash/buzhash32"
"github.com/chmduquesne/rollinghash/buzhash64"
"github.com/chmduquesne/rollinghash/rabinkarp64"
)
const testFile = "../model/testdata/~syncthing~file.tmp"
@@ -59,3 +63,115 @@ func BenchmarkWeakHashAdler32Roll(b *testing.B) {
b.SetBytes(size)
}
func BenchmarkWeakHashRabinKarp64(b *testing.B) {
data := make([]byte, size)
hf := rabinkarp64.New()
for i := 0; i < b.N; i++ {
hf.Write(data)
}
_ = hf.Sum64()
b.SetBytes(size)
}
func BenchmarkWeakHashRabinKarp64Roll(b *testing.B) {
data := make([]byte, size)
hf := rabinkarp64.New()
hf.Write(data)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for i := 0; i <= size; i++ {
hf.Roll('a')
}
}
b.SetBytes(size)
}
func BenchmarkWeakHashBozo32(b *testing.B) {
data := make([]byte, size)
hf := bozo32.New()
for i := 0; i < b.N; i++ {
hf.Write(data)
}
_ = hf.Sum32()
b.SetBytes(size)
}
func BenchmarkWeakHashBozo32Roll(b *testing.B) {
data := make([]byte, size)
hf := bozo32.New()
hf.Write(data)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for i := 0; i <= size; i++ {
hf.Roll('a')
}
}
b.SetBytes(size)
}
func BenchmarkWeakHashBuzhash32(b *testing.B) {
data := make([]byte, size)
hf := buzhash32.New()
for i := 0; i < b.N; i++ {
hf.Write(data)
}
_ = hf.Sum32()
b.SetBytes(size)
}
func BenchmarkWeakHashBuzhash32Roll(b *testing.B) {
data := make([]byte, size)
hf := buzhash32.New()
hf.Write(data)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for i := 0; i <= size; i++ {
hf.Roll('a')
}
}
b.SetBytes(size)
}
func BenchmarkWeakHashBuzhash64(b *testing.B) {
data := make([]byte, size)
hf := buzhash64.New()
for i := 0; i < b.N; i++ {
hf.Write(data)
}
_ = hf.Sum64()
b.SetBytes(size)
}
func BenchmarkWeakHashBuzhash64Roll(b *testing.B) {
data := make([]byte, size)
hf := buzhash64.New()
hf.Write(data)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for i := 0; i <= size; i++ {
hf.Roll('a')
}
}
b.SetBytes(size)
}

View File

@@ -20,10 +20,6 @@ const (
maxWeakhashFinderHits = 10
)
var (
Enabled = true
)
// Find finds all the blocks of the given size within io.Reader that matches
// the hashes provided, and returns a hash -> slice of offsets within reader
// map, that produces the same weak hash.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "STDISCOSRV" "1" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "STDISCOSRV" "1" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
stdiscosrv \- Syncthing Discovery Server
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "STRELAYSRV" "1" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "STRELAYSRV" "1" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
strelaysrv \- Syncthing Relay Server
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-BEP" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-BEP" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-bep \- Block Exchange Protocol v1
.
@@ -41,8 +41,10 @@ forms the \fIglobal model\fP\&. Each device strives to get its folders in sync
with the global model by requesting missing or outdated blocks from the
other devices in the cluster.
.sp
File data is described and transferred in units of \fIblocks\fP, each being
128 KiB (131072 bytes) in size.
File data is described and transferred in units of \fIblocks\fP, each being from
128 KiB (131072 bytes) to 16 MiB in size, in steps of powers of two. The
block size may vary between files but is constant in any given file, except
for the last block which may be smaller.
.sp
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”,
“SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this
@@ -421,7 +423,8 @@ message FileInfo {
bool invalid = 7;
bool no_permissions = 8;
Vector version = 9;
int64 sequence = 10;
int64 sequence = 10;
int32 block_size = 13;
repeated BlockInfo Blocks = 16;
string symlink_target = 17;
@@ -513,10 +516,16 @@ The \fBsequence\fP field is the value of a device local monotonic clock at the
time of last local database update to a file. The clock ticks on every local
database update, thus forming a sequence number over database updates.
.sp
The \fBblock_size\fP field is the size, in bytes, of each individual block in
the block list (except, possibly, the last block). If this field is missing
or zero, the block size is assumed to be 128 KiB (131072 bytes). Valid
values of this field are the powers of two from 128 KiB through 16 MiB. See
also \fI\%Selection of Block Size\fP\&.
.sp
The \fBblocks\fP list contains the size and hash for each block in the file.
Each block represents a 128 KiB slice of the file, except for the last block
which may represent a smaller amount of data. The block list is empty for
directories and symlinks.
Each block represents a \fBblock_size\fP\-sized slice of the file, except for
the last block which may represent a smaller amount of data. The block list
is empty for directories and symlinks.
.sp
The \fBsymlink_target\fP field contains the symlink target, for entries of
symlink type. It is empty for all other entry types.
@@ -839,6 +848,83 @@ a connection close. This size was chosen to accommodate Index messages
containing a large block list. Its intended that the limit may be further
reduced in a future protocol update supporting variable block sizes (and
thus shorter block lists for large files).
.SH SELECTION OF BLOCK SIZE
.sp
The desired block size for any given file is the smallest block size that
results in fewer than 2000 blocks, or the maximum block size for larger
files. This rule results in the following table of block sizes per file
size:
.TS
center;
|l|l|.
_
T{
File Size
T} T{
Block Size
T}
_
T{
0 \- 250 MiB
T} T{
128 KiB
T}
_
T{
250 MiB \- 500 MiB
T} T{
256 KiB
T}
_
T{
500 MiB \- 1 GiB
T} T{
512 KiB
T}
_
T{
1 GiB \- 2 GiB
T} T{
1 MiB
T}
_
T{
2 GiB \- 4 GiB
T} T{
2 MiB
T}
_
T{
4 GiB \- 8 GiB
T} T{
4 MiB
T}
_
T{
8 GiB \- 16 GiB
T} T{
8 MiB
T}
_
T{
16 GiB \- up
T} T{
16 MiB
T}
_
.TE
.sp
An implementation MAY deviate from the block size rule when there is good
reason to do so. For example, if a file has been indexed at a certain block
size and grows beyond 2000 blocks it may be retained at the current block
size for practical reasons. When there is no overriding reason to the
contrary, such as when indexing a new file for the first time, the block
size rule above SHOULD be followed.
.sp
An implementation MUST therefore accept files with a block size differing
from the above rule. This does not mean that arbitrary block sizes are
allowed. The block size used MUST be exactly one of the power\-of\-two block
sizes listed in the table above.
.SH EXAMPLE EXCHANGE
.TS
center;

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-CONFIG" "5" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-CONFIG" "5" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-config \- Syncthing Configuration
.
@@ -269,10 +269,12 @@ plugins are used to trigger rescans.
.TP
.B fsWatcherEnabled
If enabled this detects changes to files in the folder and scans them.
.UNINDENT
.INDENT 0.0
.TP
.B fsWatcherDelayS
The duration during which changes detected are accumulated, before a scan is
scheduled (only takes effect if fsWatcherEnabled is true).
scheduled (only takes effect if \fBfsWatcherEnabled\fP is true).
.TP
.B ignorePerms
True if the folder should ignore permissions.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-DEVICE-IDS" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-DEVICE-IDS" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-device-ids \- Understanding Device IDs
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-EVENT-API" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-EVENT-API" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-event-api \- Event API
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-FAQ" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-FAQ" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-faq \- Frequently Asked Questions
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-GLOBALDISCO" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-GLOBALDISCO" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-globaldisco \- Global Discovery Protocol v3
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-LOCALDISCO" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-LOCALDISCO" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-localdisco \- Local Discovery Protocol v4
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-NETWORKING" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-NETWORKING" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-networking \- Firewall Setup
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-RELAY" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-RELAY" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-relay \- Relay Protocol v1
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-REST-API" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-REST-API" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-rest-api \- REST API
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-SECURITY" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-SECURITY" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-security \- Security Principles
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-STIGNORE" "5" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-STIGNORE" "5" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-stignore \- Prevent files from being synchronized to other nodes
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-VERSIONING" "7" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-VERSIONING" "7" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-versioning \- Keep automatic backups of deleted files by other nodes
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING" "1" "Apr 02, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING" "1" "May 05, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing \- Syncthing
.

View File

@@ -11,26 +11,25 @@ package main
import (
"bytes"
"compress/gzip"
"encoding/base64"
"flag"
"fmt"
"go/format"
"io"
"os"
"path/filepath"
"strings"
"text/template"
"time"
)
var tpl = template.Must(template.New("assets").Parse(`package auto
import (
"encoding/base64"
)
const Generated int64 = {{.Generated}}
func Assets() map[string][]byte {
var assets = make(map[string][]byte, {{.Assets | len}})
{{range $asset := .Assets}}
assets["{{$asset.Name}}"], _ = base64.StdEncoding.DecodeString("{{$asset.Data}}"){{end}}
assets["{{$asset.Name}}"] = {{$asset.Data}}{{end}}
return assets
}
@@ -70,7 +69,7 @@ func walkerFor(basePath string) filepath.WalkFunc {
name, _ = filepath.Rel(basePath, name)
assets = append(assets, asset{
Name: filepath.ToSlash(name),
Data: base64.StdEncoding.EncodeToString(buf.Bytes()),
Data: fmt.Sprintf("%#v", buf.Bytes()), // "[]byte{0x00, 0x01, ...}"
})
}
@@ -79,7 +78,8 @@ func walkerFor(basePath string) filepath.WalkFunc {
}
type templateVars struct {
Assets []asset
Assets []asset
Generated int64
}
func main() {
@@ -88,7 +88,8 @@ func main() {
filepath.Walk(flag.Arg(0), walkerFor(flag.Arg(0)))
var buf bytes.Buffer
tpl.Execute(&buf, templateVars{
Assets: assets,
Assets: assets,
Generated: time.Now().Unix(),
})
bs, err := format.Source(buf.Bytes())
if err != nil {

View File

@@ -19,6 +19,7 @@
<paused>false</paused>
<weakHashThresholdPct>25</weakHashThresholdPct>
<markerName>.stfolder</markerName>
<useLargeBlocks>true</useLargeBlocks>
</folder>
<folder id="¯\_(ツ)_/¯ Räksmörgås 动作 Адрес" label="" path="s12-1/" type="readwrite" rescanIntervalS="10" fsWatcherEnabled="false" fsWatcherDelayS="10" ignorePerms="false" autoNormalize="true">
<filesystemType>basic</filesystemType>
@@ -38,6 +39,7 @@
<paused>false</paused>
<weakHashThresholdPct>25</weakHashThresholdPct>
<markerName>.stfolder</markerName>
<useLargeBlocks>true</useLargeBlocks>
</folder>
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK" name="s4" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
<address>tcp://127.0.0.1:22004</address>

Some files were not shown because too many files have changed in this diff Show More