Compare commits

...

50 Commits

Author SHA1 Message Date
André Colomb
e4956358fb lib/model: Remove superfluous check for IndexID in remote ClusterConfig (#5717)
The check in ClusterConfig() when iterating through announced devices
in a folder explicitly skips entries without a non-zero IndexID.
Therefore, the check for IndexID == 0 just below will never be true
and the intended cleanup of local index data will not happen.

Plainly remove that check to make the intended case distinction work.
2019-05-12 21:17:55 +02:00
dependabot[bot]
ad44d77a21 build(deps): bump github.com/lib/pq from 1.0.0 to 1.1.1 (#5714)
Bumps [github.com/lib/pq](https://github.com/lib/pq) from 1.0.0 to 1.1.1.
- [Release notes](https://github.com/lib/pq/releases)
- [Commits](https://github.com/lib/pq/compare/v1.0.0...v1.1.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-11 22:15:30 +02:00
dependabot[bot]
6bb5d140fa build(deps): bump github.com/oschwald/geoip2-golang from 1.1.0 to 1.3.0 (#5713)
Bumps [github.com/oschwald/geoip2-golang](https://github.com/oschwald/geoip2-golang) from 1.1.0 to 1.3.0.
- [Release notes](https://github.com/oschwald/geoip2-golang/releases)
- [Commits](https://github.com/oschwald/geoip2-golang/compare/v1.1.0...v1.3.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-11 21:02:49 +01:00
dependabot[bot]
d082b2ba4c build(deps): bump github.com/mattn/go-isatty from 0.0.4 to 0.0.7 (#5712)
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.4 to 0.0.7.
- [Release notes](https://github.com/mattn/go-isatty/releases)
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.4...v0.0.7)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-11 21:00:00 +01:00
dependabot[bot]
638789899c build(deps): bump github.com/gogo/protobuf from 1.2.0 to 1.2.1 (#5715)
Bumps [github.com/gogo/protobuf](https://github.com/gogo/protobuf) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/gogo/protobuf/releases)
- [Commits](https://github.com/gogo/protobuf/compare/v1.2.0...v1.2.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-11 20:59:44 +01:00
Simon Frei
dfbbb286fc lib/fs: Consider win83 for root path as well when watching (ref #5706) (#5709) 2019-05-11 10:06:04 +02:00
Simon Frei
fbd445fe0a lib/api: Clean up after test, enables test caching (#5710) 2019-05-11 08:41:32 +02:00
Simon Frei
2b246eeb52 lib/model: Move test utilities to separate files (#5694) 2019-05-10 13:33:45 +02:00
Simon Frei
2558b021e5 lib/fs: Remove \\?\ for drive letters when watching (fixes #5578) (#5701) 2019-05-10 09:09:58 +02:00
Jakob Borg
31be810eb6 lib/model: Don't fail operation when fsync() fails (fixes #5704) (#5705) 2019-05-09 21:20:29 +01:00
Jakob Borg
62a6d619e7 Merge branch 'release'
* release:
  lib/fs: Revert removal of \\?\ for drive letters (fixes #5695)
2019-05-08 17:38:08 +02:00
Jakob Borg
a04fcfe749 lib/fs: Revert removal of \\?\ for drive letters (fixes #5695)
This reverts commit ca823bd591 from #5633.

This un-fixes bug #5578.
2019-05-08 17:31:52 +02:00
Simon Frei
283f39ae5f lib/protocol: Revert unreleased changes related to closing connections (#5688)
This reverts commits:
    ec7c88ca55
    19b51c9b92
    5da41f75fa
    04b927104f
2019-05-08 08:08:26 +02:00
Jakob Borg
59e1349499 gui, man, authors: Update docs, translations, and contributors 2019-05-08 07:45:28 +02:00
Simon Frei
b45d77b6be lib/model: Fix regression deleting directories on pull (ref #5690) (#5691) 2019-05-06 20:55:26 +02:00
Simon Frei
79e67b7f79 lib/model: Remove individual pull errors on retry (fixes #5659) (#5690) 2019-05-06 17:21:56 +02:00
Jakob Borg
36a4a9fd34 Merge branch 'release'
* release:
  etc: Revert systemd After=multi-user.target addition (fixes #5676)
2019-05-06 12:24:39 +02:00
Nitroretro
92ed31fe21 gui: Fix typo in log viewer modal (#5687) 2019-05-03 07:28:17 +02:00
Simon Frei
5fa8467756 lib/model: Consistent use of locks (#5684) 2019-05-02 18:55:39 +01:00
Simon Frei
5954b105cd lib/model: Let fakeConnection call Model.Closed on close (#5682) 2019-05-02 14:24:55 +02:00
Simon Frei
defc5dca65 lib/model: Use correct lock (#5683) 2019-05-02 14:09:42 +02:00
Simon Frei
9f358ecae0 lib/protocol: Refactor to pass only relevant argument to writeMessage (#5681) 2019-05-02 14:09:19 +02:00
Simon Frei
fe4daf242b cmd, lib/db: Actually close goleveldb (fixes #5505) (#5671) 2019-05-02 11:15:00 +02:00
Simon Frei
ec7c88ca55 lib/protocol: Fix yet another deadlock (fixes #5678) (#5679)
* lib/protocol: Fix yet another deadlock (fixes #5678)

* more consistency

* read deadlock

* naming

* more naming
2019-05-02 09:21:07 +01:00
Jakob Borg
26e6d94c00 gui, man, authors: Update docs, translations, and contributors 2019-05-01 07:45:25 +02:00
Jakob Borg
e2470c8bc8 etc: Revert systemd After=multi-user.target addition (fixes #5676)
This reverts commit 84fe285659.
2019-04-29 21:45:01 +02:00
Simon Frei
19b51c9b92 lib/protocol: Don't close asyncMessage.done on success (fixes #5674) (#5675) 2019-04-29 17:52:57 +02:00
Audrius Butkevicius
0ca1f26ff8 lib/versioner: Restore for all versioners, cross-device support (#5514)
* lib/versioner: Restore for all versioners, cross-device support

Fixes #4631
Fixes #4586
Fixes #1634
Fixes #5338
Fixes #5419
2019-04-28 23:30:16 +01:00
Jakob Borg
2984d40641 lib/ignore: Additional test case (#5672) 2019-04-28 21:20:11 +01:00
Simon Frei
5da41f75fa lib/model, lib/protocol: Wait for reader/writer loops on close (fixes #4170) (#5657)
* lib/protocol: Wait for reader/writer loops on close (fixes #4170)

* waitgroup

* lib/model: Don't hold lock while closing connection

* fix comments

* review (lock once, func argument) and naming
2019-04-28 11:58:51 +01:00
Jakob Borg
32dec4a00d gui, man, authors: Update docs, translations, and contributors 2019-04-24 07:45:24 +02:00
Simon Frei
04b927104f lib/protocol: Don't send any messages before cluster config (#5646)
* lib/model: Send cluster config before releasing pmut

* reshuffle

* add model.connReady to track cluster-config status

* Corrected comments/strings

* do it in protocol
2019-04-23 20:47:11 +01:00
Simon Frei
110806842c lib/model: Pass the old not new fileinfo to deleteItemOnDisk (fixes #5654) (#5655) 2019-04-23 20:46:28 +01:00
Jonas Thelemann
d9b3415dec gui: Semicolon insertion (#5666) 2019-04-23 18:37:51 +01:00
Jonas Thelemann
d3d43d90f6 gui: Remove superfluous trailing argument (#5668) 2019-04-23 18:37:20 +01:00
Jonas Thelemann
e7d11adf3c gui: Remove unused variables (#5665) 2019-04-23 18:36:12 +01:00
Jonas Thelemann
afeb606b5b gui: Use of AngularJS markup in URL-valued attribute (#5667)
Using AngularJS markup (that is, AngularJS expressions enclosed in double curly braces) in HTML attributes that reference URLs is not recommended: the browser may attempt to fetch the URL before the AngularJS compiler evaluates the markup, resulting in a request for an invalid URL.
2019-04-23 18:33:40 +01:00
Jonas Thelemann
c6a179fa4d cmd/strelaypoolsrv: Missing explicit dependency injection (#5669)
https://lgtm.com/rules/1505800326162/
2019-04-23 12:17:27 +01:00
Simon Frei
e302ccf4b4 lib/fs: Improve IsParent (#5658) 2019-04-22 11:12:32 +02:00
Simon Frei
926e9228ed lib/scanner, lib/model: File -> item when logging error (#5664) 2019-04-21 16:19:59 +01:00
Simon Frei
86e72d9973 lib/model: Use RLock and legacy polish (#5663) 2019-04-21 13:21:36 +01:00
otbutz
952c8becf5 gui: Adjust table column width to content size (fixes #4531) (#5565) 2019-04-18 18:35:44 +02:00
Jakob Borg
32ee8d783d gui, man, authors: Update docs, translations, and contributors 2019-04-17 07:45:24 +02:00
Simon Frei
3bea59b0d9 lib/model: Refactor progressEmitter to de-/activate by config (fixes #4613) (#5623) 2019-04-13 14:20:51 +02:00
Simon Frei
8a4b65b937 gui: Fix setting page size on failed and locally changed modals (fixes #5421) (#5650) 2019-04-13 14:05:39 +02:00
Simon Frei
fca895a632 lib/model: Fix block index calculation for recheckFile (fixes #5649) (#5648) 2019-04-12 15:21:07 +02:00
Simon Frei
79360e2205 lib/fs: Add test that symlinks are skipped on walk (#5644) 2019-04-10 22:36:37 +02:00
Simon Frei
9b2a73f9ab lib/model: Pause pull for at least as long as failed pull took (fixes #5641) (#5643) 2019-04-10 16:22:23 +02:00
Simon Frei
c305265c62 lib/model: Request errors conforming to BEP specs (#5642) 2019-04-10 11:47:24 +02:00
Jakob Borg
1954239ffa gui, man, authors: Update docs, translations, and contributors 2019-04-10 07:45:25 +02:00
99 changed files with 2020 additions and 1113 deletions

View File

@@ -29,7 +29,7 @@ Antoine Lamielle (0x010C) <antoine.lamielle@0x010c.fr> <gh@0x010c.fr>
Antony Male (canton7) <antony.male@gmail.com>
Aranjedeath <Aranjedeath@users.noreply.github.com>
Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com>
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com>
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com> <github@audrius.rocks>
BAHADIR YILMAZ <bahadiryilmaz32@gmail.com>
Bart De Vries (mogwa1) <devriesb@gmail.com>
Ben Curthoys (bencurthoys) <ben@bencurthoys.com>
@@ -146,6 +146,7 @@ Nico Stapelbroek <3368018+nstapelbroek@users.noreply.github.com>
Nicolas Braud-Santoni <nicolas@braud-santoni.eu>
Niels Peter Roest (Niller303) <nielsproest@hotmail.com> <seje.niels@hotmail.com>
Nils Jakobi (thunderstorm99) <jakobi.nils@gmail.com>
Nitroretro <43112364+Nitroretro@users.noreply.github.com>
NoLooseEnds <jon.koslung@gmail.com>
otbutz <tbutz@optitool.de>
Oyebanji Jacob Mayowa <oyebanji05@gmail.com>

View File

@@ -193,9 +193,9 @@
<script>
angular.module('syncthing', [
])
.config(function($httpProvider) {
.config(['$httpProvider', function($httpProvider) {
$httpProvider.defaults.timeout = 5000;
})
}])
.filter('bytes', function() {
return function(bytes, precision) {
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) return '-';

View File

@@ -928,6 +928,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
code := exit.waitForExit()
mainService.Stop()
ldb.Close()
l.Infoln("Exiting")

8
go.mod
View File

@@ -11,18 +11,18 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d
github.com/gogo/protobuf v1.2.0
github.com/gogo/protobuf v1.2.1
github.com/golang/groupcache v0.0.0-20171101203131-84a468cf14b4
github.com/golang/snappy v0.0.0-20170215233205-553a64147049 // indirect
github.com/jackpal/gateway v0.0.0-20161225004348-5795ac81146e
github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657
github.com/kr/pretty v0.1.0 // indirect
github.com/lib/pq v1.0.0
github.com/mattn/go-isatty v0.0.4
github.com/lib/pq v1.1.1
github.com/mattn/go-isatty v0.0.7
github.com/minio/sha256-simd v0.0.0-20190117184323-cc1980cb0338
github.com/onsi/ginkgo v0.0.0-20171221013426-6c46eb8334b3 // indirect
github.com/onsi/gomega v0.0.0-20171227184521-ba3724c94e4d // indirect
github.com/oschwald/geoip2-golang v1.1.0
github.com/oschwald/geoip2-golang v1.3.0
github.com/oschwald/maxminddb-golang v0.0.0-20170901134056-26fe5ace1c70 // indirect
github.com/petermattis/goid v0.0.0-20170816195418-3db12ebb2a59 // indirect
github.com/pkg/errors v0.8.1

13
go.sum
View File

@@ -22,6 +22,8 @@ github.com/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d h1:IngNQgbqr5ZOU0exk39
github.com/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/groupcache v0.0.0-20171101203131-84a468cf14b4 h1:6o8aP0LGMKzo3NzwhhX6EJsiJ3ejmj+9yA/3p8Fjjlw=
github.com/golang/groupcache v0.0.0-20171101203131-84a468cf14b4/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
@@ -32,6 +34,8 @@ github.com/jackpal/gateway v0.0.0-20161225004348-5795ac81146e h1:lS8IitpqG4RkZbE
github.com/jackpal/gateway v0.0.0-20161225004348-5795ac81146e/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657 h1:vE7J1m7cCpiRVEIr1B5ccDxRpbPsWT5JU3if2Di5nE4=
github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -39,8 +43,12 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/minio/sha256-simd v0.0.0-20190117184323-cc1980cb0338 h1:USW1+zAUkUSvk097CAX/i8KR3r6f+DHNhk6Xe025Oyw=
@@ -51,6 +59,8 @@ github.com/onsi/gomega v0.0.0-20171227184521-ba3724c94e4d h1:r351oUAFgdsydkt/g+X
github.com/onsi/gomega v0.0.0-20171227184521-ba3724c94e4d/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/oschwald/geoip2-golang v1.1.0 h1:ACVPz5YqH4/jZkQdsp/PZc9shQVZmreCzAVNss5y3bo=
github.com/oschwald/geoip2-golang v1.1.0/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
github.com/oschwald/geoip2-golang v1.3.0 h1:D+Hsdos1NARPbzZ2aInUHZL+dApIzo8E0ErJVsWcku8=
github.com/oschwald/geoip2-golang v1.3.0/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
github.com/oschwald/maxminddb-golang v0.0.0-20170901134056-26fe5ace1c70 h1:XGLYUmodtNzThosQ8GkMvj9TiIB/uWsP8NfxKSa3aDc=
github.com/oschwald/maxminddb-golang v0.0.0-20170901134056-26fe5ace1c70/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY=
github.com/petermattis/goid v0.0.0-20170816195418-3db12ebb2a59 h1:2pHcLyJYXivxVvpoCc29uo3GDU1qFfJ1ggXKGYMrM0E=
@@ -96,10 +106,13 @@ golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsM
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.0.0-20171227012246-e19ae1496984 h1:ulYJn/BqO4fMRe1xAQzWjokgjsQLPpb21GltxXHI3fQ=
golang.org/x/text v0.0.0-20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20170927054726-6dc17368e09b h1:3X+R0qq1+64izd8es+EttB6qcY+JDlVmAhpRXl7gpzU=
golang.org/x/time v0.0.0-20170927054726-6dc17368e09b/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 h1:JBwmEvLfCqgPcIq8MjVMQxsF3LVL4XG/HH0qiG0+IFY=
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=

View File

@@ -124,6 +124,18 @@ table.table-condensed td.no-overflow-ellipse {
white-space: normal;
}
table.table-auto {
table-layout: auto;
}
table.table-auto th {
width: 1px;
}
table.table-auto td {
max-width: 0px;
}
.folder-advanced {
padding: 1rem;
margin-bottom: 15px;

View File

@@ -295,6 +295,7 @@
"Syncing": "Синхронизиране",
"Syncthing has been shut down.": "Syncthing е спрян.",
"Syncthing includes the following software or portions thereof:": "Syncthing уползотворява частично или изцяло следните софтуерни продукти:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthing се рестартира",
"Syncthing is upgrading.": "Syncthing се обновява.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Изглежда, че Syncthing не е включен, или има проблем с връзката с Интернет. Повторен опит...",

View File

@@ -34,7 +34,7 @@
"Auto Accept": "Auto Acceptar",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "L'actualització automàtica ara ofereix l'elecció entre les versions estables i les versions candidates.",
"Automatic upgrades": "Actualitzacions automàtiques",
"Automatic upgrades are always enabled for candidate releases.": "Automatic upgrades are always enabled for candidate releases.",
"Automatic upgrades are always enabled for candidate releases.": "Les actualitzacions automàtiques sempre estàn activades per a les versions candidates.",
"Automatically create or share folders that this device advertises at the default path.": "Crear o compartir automàticament les carpetes que aquest dispositiu anuncia en la ruta per defecte.",
"Available debug logging facilities:": "Hi han disponibles les següents utilitats per a depurar el registre:",
"Be careful!": "Tin precaució!",
@@ -56,13 +56,13 @@
"Copied from original": "Copiat de l'original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 els següents Col·laboradors:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 els següents Col·laboradors:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 els següents Col·laboradors:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creant patrons a ignorar, sobreescriguent un fitxer que ja existeix a {{path}}.",
"Danger!": "Perill!",
"Debugging Facilities": "Utilitats de Depuració",
"Default Folder Path": "Carpeta de la Ruta per Defecte",
"Deleted": "Esborrat",
"Deselect All": "Deselect All",
"Deselect All": "Anul·lar tota la selecció",
"Device": "Dispositiu",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Dispositiu \"{{name}}\" ({{device}} a l'adreça {{address}}) vol connectar. Afegir nou dispositiu?",
"Device ID": "ID del dispositiu",
@@ -75,7 +75,7 @@
"Disabled periodic scanning and disabled watching for changes": "Desactivat l'escaneig periòdic i el rastreig continu de canvis",
"Disabled periodic scanning and enabled watching for changes": "Desactivat l'escaneig periòdic i activat el rastreig continu de canvis",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Desactivat l'escaneig periòdic i errada al rastreig continu de canvis, es reintentarà cada 1 minut:",
"Discard": "Discard",
"Discard": "Descartar",
"Disconnected": "Desconnectat",
"Discovered": "Descobert",
"Discovery": "Descobriment",
@@ -98,7 +98,7 @@
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Introdueix un nombre no negatiu (per exemple, \"2.35\") i selecciona una unitat. Els percentatges són com a part del tamany total del disc.",
"Enter a non-privileged port number (1024 - 65535).": "Introdueix un nombre de port sense privilegis (1024-65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introdueix adreces separades per coma (\"tcp://ip:port\", \"tcp://host:port\") o \"dynamic\" per a realitzar el descobriment automàtic de l'adreça.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduïr adreces separades per coma (\"tcp://ip:port\", \"tcp://host:port\") o dinàmiques per al descobriment automàtic de l'adreça.",
"Enter ignore patterns, one per line.": "Introduïr patrons a ignorar, un per línia.",
"Error": "Error",
"External File Versioning": "Versionat extern de fitxers",
@@ -144,9 +144,9 @@
"Ignore": "Ignorar",
"Ignore Patterns": "Patrons a ignorar",
"Ignore Permissions": "Permisos a ignorar",
"Ignored Devices": "Ignored Devices",
"Ignored Folders": "Ignored Folders",
"Ignored at": "Ignored at",
"Ignored Devices": "Dispositius Ignorats",
"Ignored Folders": "Carpetes Ignorades",
"Ignored at": "Ignorat en",
"Incoming Rate Limit (KiB/s)": "Límit de descàrrega (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "La configuración incorrecta pot danyar el contingut de la teua carpeta i deixar Syncthing inoperatiu.",
"Introduced By": "Introduït Per",
@@ -160,14 +160,14 @@
"Later": "Més tard",
"Latest Change": "Últim Canvi",
"Learn more": "Saber més",
"Limit": "Limit",
"Limit": "Límit",
"Listeners": "Escoltants",
"Loading data...": "Carregant dades...",
"Loading...": "Carregant...",
"Local Discovery": "Descobriment local",
"Local State": "Estat local",
"Local State (Total)": "Estat Local (Total)",
"Locally Changed Items": "Locally Changed Items",
"Locally Changed Items": "Dispositius Canviats Localment",
"Log": "Registre",
"Log tailing paused. Click here to continue.": "Pausada l'adició de dades al registre. Polsa ací per continuar.",
"Log tailing paused. Scroll to bottom continue.": "Pausat el seguiment del registre. Es continua fins al final.",
@@ -209,7 +209,7 @@
"Pause": "Pausa",
"Pause All": "Pausa Tot",
"Paused": "Pausat",
"Pending changes": "Pending changes",
"Pending changes": "Canvis pendents",
"Periodic scanning at given interval and disabled watching for changes": "Escaneig periòdic a l'interval determinat i desactivat el rastreig continu de canvis",
"Periodic scanning at given interval and enabled watching for changes": "Escaneig periòdic a l'interval determinat i activat el rastreig continu de canvis",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Escaneig periòdic a l'interval determinat i errada al activar el rastreig continu de canvis, reintentant cada 1 minut:",
@@ -253,7 +253,7 @@
"Scanning": "Rastrejant",
"See external versioner help for supported templated command line parameters.": "Consulta l'ajuda externa sobre versions per a conéixer els paràmetres de la plantilla de la línia de comandaments.",
"See external versioning help for supported templated command line parameters.": "Consulta l'ajuda externa sobre versions per a conéixer els paràmetres de la plantilla de la línia de comandaments.",
"Select All": "Select All",
"Select All": "Sel·leccionar Tot",
"Select a version": "Seleccionar una versió",
"Select latest version": "Seleccionar l'última versió",
"Select oldest version": "Seleccionar la versió més antiga",
@@ -290,17 +290,18 @@
"Statistics": "Estadístiques",
"Stopped": "Parat",
"Support": "Suport",
"Support Bundle": "Support Bundle",
"Support Bundle": "Lot de Suport",
"Sync Protocol Listen Addresses": "Direccions d'escolta del protocol de sincronització",
"Syncing": "Sincronitzant",
"Syncthing has been shut down.": "Syncthing s'ha apagat",
"Syncthing includes the following software or portions thereof:": "Syncthing inclou el següent software o parts d'ell:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing és Software Gratuït i Open Source llicenciat com MPL v2.0.",
"Syncthing is restarting.": "Syncthing està reiniciant.",
"Syncthing is upgrading.": "Syncthing està actualitzant-se.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing pareix apagat o hi ha un problema amb la connexió a Internet. Tornant a intentar...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing pareix que té un problema processant la seua sol·licitud. Per favor, refresque la pàgina o reinicie Syncthing si el problema persistix.",
"Take me back": "Take me back",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
"Take me back": "Porta'm enrere",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "L'adreça del GUI és sobreescrita per les opcions d'inici. Els canvis ací no surtiràn efecte mentre la sobreescritura estiga en marxa.",
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfície d'administració de Syncthing està configurat per a permetre l'accés remot sense una contrasenya.",
"The aggregated statistics are publicly available at the URL below.": "Les estadístiques agregades estàn disponibles en la URL que figura a continuació.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració ha sigut gravada però no activada. Syncthing deu reiniciar per tal d'activar la nova configuració.",
@@ -314,7 +315,7 @@
"The folder path cannot be blank.": "La ruta de la carpeta no pot estar buida.",
"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.": "S'utilitzen els següents intervals: per a la primera hora es guarda una versió cada 30 segons, per al primer dia es guarda una versió cada hora, per als primers 30 dies es guarda una versió diaria, fins l'edat màxima es guarda una versió cada setmana.",
"The following items could not be synchronized.": "Els següents objectes no s'han pogut sincronitzar.",
"The following items were changed locally.": "The following items were changed locally.",
"The following items were changed locally.": "Els següents ítems es canviaren localment.",
"The maximum age must be a number and cannot be blank.": "L'edat màxima deu ser un nombre i no pot estar buida.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El temps màxim per a guardar una versió (en dies, ficar 0 per a guardar les versions per a sempre).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "El porcentatge d'espai mínim lliure en el disc deu ser un nombre no negatiu entre 0 i 100 (ambdós inclosos).",
@@ -337,7 +338,7 @@
"Unavailable": "No disponible",
"Unavailable/Disabled by administrator or maintainer": "No disponible/Desactivar per l'administrador o mantenedor",
"Undecided (will prompt)": "No decidit (es preguntarà)",
"Unignore": "Unignore",
"Unignore": "Designorar",
"Unknown": "Desconegut",
"Unshared": "No compartit",
"Unused": "No utilitzat",
@@ -350,14 +351,14 @@
"Uptime": "Temps de funcionament",
"Usage reporting is always enabled for candidate releases.": "Els informes d'ús sempre estan activats per a les versions candidates.",
"Use HTTPS for GUI": "Utilitzar HTTPS per a l'Interfície Gràfica d'Usuari (GUI)",
"Use notifications from the filesystem to detect changed items.": "Use notifications from the filesystem to detect changed items.",
"Variable Size Blocks": "Variable Size Blocks",
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Variable size blocks (also \"large blocks\") are more efficient for large files.",
"Use notifications from the filesystem to detect changed items.": "Usar notificacions del sistema de fitxers per a detectar els ítems canviats.",
"Variable Size Blocks": "Blocs de Tamany Variable",
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Els blocs de tamany variable (també coneguts com \"blocs grans\") són més eficients per als fitxers grans.",
"Version": "Versió",
"Versions": "Versions",
"Versions Path": "Ruta de les versions",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions s'esborren automàticament si són més antigues que l'edat màxima o excedixen el nombre de fitxer permesos en un interval.",
"Waiting to scan": "Waiting to scan",
"Waiting to scan": "Esperant per a escanetjar",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Perill! Esta ruta és un directori pare d'una carpeta ja existent \"{{otherFolder}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Perill! Esta ruta és un directori pare d'una carpeta existent \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Perill! Esta ruta és un subdirectori d'una carpeta que ja existeix nomenada \"{{otherFolder}}\".",
@@ -365,16 +366,16 @@
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "AVÍS: Si estàs utilitzant un observador extern com {{syncthingInotify}}, deus assegurar-te de que està desactivat.",
"Watch for Changes": "Vigilar els Canvis",
"Watching for Changes": "Vigilant els Canvis",
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
"Watching for changes discovers most changes without periodic scanning.": "Vigil·lar els canvis detecta més canvis sense escanetjar periòdicament.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quant s'afig un nou dispositiu, hi ha que tindre en compte que aquest dispositiu deu ser afegit també en l'altre costat.",
"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.": "Quant s'afig una nova carpeta, hi ha que tindre en compte que l'ID de la carpeta s'utilitza per a juntar les carpetes entre dispositius. Són sensibles a les majúscules i deuen coincidir exactament entre tots els dispositius.",
"Yes": "Sí",
"You can also select one of these nearby devices:": "Pots seleccionar també un d'aquestos dispositius propers:",
"You can change your choice at any time in the Settings dialog.": "Pots canviar la teua elecció en qualsevol moment en el dialog Ajustos",
"You can read more about the two release channels at the link below.": "Pots llegir més sobre els dos canals de versions en l'enllaç de baix.",
"You have no ignored devices.": "You have no ignored devices.",
"You have no ignored folders.": "You have no ignored folders.",
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
"You have no ignored devices.": "No tens dispositius ignorats.",
"You have no ignored folders.": "No tens carpetes ignorades.",
"You have unsaved changes. Do you really want to discard them?": "Tens canvis sense guardar. Realment vols descartar-los?",
"You must keep at least one version.": "Es deu mantindre al menys una versió.",
"days": "dies",
"directories": "directoris",

View File

@@ -56,7 +56,7 @@
"Copied from original": "Zkopírováno z originálu",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následující přispěvatelé:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 následující přispěvatelé:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 následující přispěvatelé:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Vytváření ignorovaných vzorů, přepisování existujícího souboru v {{path}}.",
"Danger!": "Pozor!",
"Debugging Facilities": "Nástroje pro ladění",
@@ -295,6 +295,7 @@
"Syncing": "Synchronizuje se",
"Syncthing has been shut down.": "Syncthing byl vypnut.",
"Syncthing includes the following software or portions thereof:": "Syncthing obsahuje následující software nebo jejich část:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthing se restartuje.",
"Syncthing is upgrading.": "Syncthing se aktualizuje.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing se zdá být nefunkční, nebo je problém s připojením k Internetu. Opakuji...",

View File

@@ -295,6 +295,7 @@
"Syncing": "Synkroniserer",
"Syncthing has been shut down.": "Syncthing er lukket ned.",
"Syncthing includes the following software or portions thereof:": "Syncthing indeholder følgende software eller dele heraf:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthing genstarter.",
"Syncthing is upgrading.": "Syncthing opgraderer.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ud til at være stoppet eller oplever problemer med din internetforbindelse. Prøver igen…",

View File

@@ -34,7 +34,7 @@
"Auto Accept": "Automatische Annahme",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Die automatische Aktualisierung bietet jetzt die Wahl zwischen stabilen Veröffentlichungen und Veröffentlichungskandidaten.",
"Automatic upgrades": "Automatische Aktualisierungen aktivieren",
"Automatic upgrades are always enabled for candidate releases.": "Automatic upgrades are always enabled for candidate releases.",
"Automatic upgrades are always enabled for candidate releases.": "Automatische Upgrades sind für Veröffentlichungskandidaten immer aktiviert.",
"Automatically create or share folders that this device advertises at the default path.": "Automatisch Ordner erstellen oder freigeben, die dieses Gerät im Standardpfad ankündigt.",
"Available debug logging facilities:": "Verfügbare Debugging-Möglichkeiten:",
"Be careful!": "Vorsicht!",
@@ -56,7 +56,7 @@
"Copied from original": "Vom Original kopiert",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 der folgenden Unterstützer:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 der folgenden Unterstützer:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 folgende Mitwirkende:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Erstelle Ignoriermuster, welche die existierende Datei {{path}} überschreiben.",
"Danger!": "Achtung!",
"Debugging Facilities": "Debugging-Möglichkeiten",
@@ -295,6 +295,7 @@
"Syncing": "Synchronisiere",
"Syncthing has been shut down.": "Syncthing wurde heruntergefahren.",
"Syncthing includes the following software or portions thereof:": "Syncthing enthält die folgende Software oder Teile von:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing ist freie und quelloffene Software, lizenziert als MPL v2.0.",
"Syncthing is restarting.": "Syncthing wird neu gestartet",
"Syncthing is upgrading.": "Syncthing wird aktualisiert",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Deiner Internetverbindung. Versuche erneut...",
@@ -350,9 +351,9 @@
"Uptime": "Betriebszeit",
"Usage reporting is always enabled for candidate releases.": "Nutzungsbericht ist für Veröffentlichungskandidaten immer aktiviert.",
"Use HTTPS for GUI": "HTTPS für Benutzeroberfläche verwenden",
"Use notifications from the filesystem to detect changed items.": "Use notifications from the filesystem to detect changed items.",
"Variable Size Blocks": "Variable Size Blocks",
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Variable size blocks (also \"large blocks\") are more efficient for large files.",
"Use notifications from the filesystem to detect changed items.": "Benachrichtigungen des Dateisystems nutzen, um Änderungen zu erkennen.",
"Variable Size Blocks": "Blöcke mit variabler Größe",
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Blöcke mit variabler Größe (auch „große Blöcke“) sind für große Dateien effizienter.",
"Version": "Version",
"Versions": "Versionen",
"Versions Path": "Versionierungspfad",
@@ -365,7 +366,7 @@
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Achtung: Wenn Sie einen externen Beobachter wie {{syncthingInotify}} benutzen, sollten sie sicher sein das dieser deaktiviert ist.",
"Watch for Changes": "Auf Änderungen achten",
"Watching for Changes": "Auf Änderungen achten",
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
"Watching for changes discovers most changes without periodic scanning.": "Das Überwachen von Änderungen entdeckt die meisten Änderungen ohne regelmäßiges Scannen.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Gerätes, dass dieses Gerät auch auf den anderen Geräten hinzugefügt werden muss.",
"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.": "Beachte bitte beim Hinzufügen eines neuen Ordners, dass die Ordnerkennung dazu verwendet wird, Ordner zwischen Geräten zu verbinden. Die Kennung muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
"Yes": "Ja",

View File

@@ -295,6 +295,7 @@
"Syncing": "Συγχρονίζω",
"Syncthing has been shut down.": "Το Syncthing έχει απενεργοποιηθεί.",
"Syncthing includes the following software or portions thereof:": "Το Syncthing περιλαμβάνει τα παρακάτω λογισμικά ή μέρη αυτών:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Το Syncthing επανεκκινείται.",
"Syncthing is upgrading.": "Το Syncthing αναβαθμίζεται.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Το Syncthing φαίνεται πως είναι απενεργοποιημένο ή υπάρχει πρόβλημα στη σύνδεσή σου στο διαδίκτυο. Προσπαθώ πάλι…",

View File

@@ -295,6 +295,7 @@
"Syncing": "Syncing",
"Syncthing has been shut down.": "Syncthing has been shut down.",
"Syncthing includes the following software or portions thereof:": "Syncthing includes the following software or portions thereof:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthing is restarting.",
"Syncthing is upgrading.": "Syncthing is upgrading.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",

View File

@@ -171,6 +171,7 @@
"Log": "Log",
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
"Log tailing paused. Scroll to bottom continue.": "Log tailing paused. Scroll to bottom continue.",
"Log tailing paused. Scroll to the bottom to continue.": "Log tailing paused. Scroll to the bottom to continue.",
"Logs": "Logs",
"Major Upgrade": "Major Upgrade",
"Mass actions": "Mass actions",

View File

@@ -295,6 +295,7 @@
"Syncing": "Sinkronigas",
"Syncthing has been shut down.": "Syncthing estis malŝaltita.",
"Syncthing includes the following software or portions thereof:": "Syncthing inkluzivas la jenajn programarojn aŭ porciojn ĝiajn:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing estas libera kaj malferma fonta programaro licencita kiel MPL v2.0.",
"Syncthing is restarting.": "Syncthing estas restartanta.",
"Syncthing is upgrading.": "Syncthing estas ĝisdatigita.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ŝajnas nefunkcii, aŭ estas problemo kun via retkonekto. Reprovado...",

View File

@@ -34,7 +34,7 @@
"Auto Accept": "Auto aceptar",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Ahora la actualización automática permite elegir entre versiones estables o versiones candidatas.",
"Automatic upgrades": "Actualizaciones automáticas",
"Automatic upgrades are always enabled for candidate releases.": "Automatic upgrades are always enabled for candidate releases.",
"Automatic upgrades are always enabled for candidate releases.": "Las actualizaciones automáticas siempre están activadas para las versiones candidatas.",
"Automatically create or share folders that this device advertises at the default path.": "Crear o compartir automáticamente las carpetas que este dispositivo anuncia en la ruta por defecto.",
"Available debug logging facilities:": "Ayudas disponibles para la depuración del registro:",
"Be careful!": "¡Ten cuidado!",
@@ -56,13 +56,13 @@
"Copied from original": "Copiado del original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes Colaboradores:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 Los siguientes colaboradores:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 los siguientes Colaboradores:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Crear patrones a ignorar, sobreescribiendo un fichero existente en {{path}}.",
"Danger!": "¡Peligro!",
"Debugging Facilities": "Ayudas a la depuración",
"Default Folder Path": "Ruta de la carpeta por defecto",
"Deleted": "Eliminado",
"Deselect All": "Deselect All",
"Deselect All": "Deseleccionar Todo",
"Device": "Dispositivo",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "El dispositivo \"{{name}}\" ({{device}} en la dirección {{address}}) quiere conectarse. Añadir nuevo dispositivo?",
"Device ID": "ID del Dispositivo",
@@ -75,7 +75,7 @@
"Disabled periodic scanning and disabled watching for changes": "Desactivados el escaneo periódico y la vigilancia de cambios",
"Disabled periodic scanning and enabled watching for changes": "Desactivado el escaneo periódico y activada la vigilancia de cambios",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Desactivado el escaneo periódico y falló la activación de la vigilancia de cambios, reintentando cada 1 minuto:",
"Discard": "Discard",
"Discard": "Descartar",
"Disconnected": "Desconectado",
"Discovered": "Descubierto",
"Discovery": "Descubrimiento",
@@ -98,7 +98,7 @@
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Introduce un número no negativo (por ejemplo, \"2.35\") y selecciona una unidad. Los porcentajes son como parte del tamaño total del disco.",
"Enter a non-privileged port number (1024 - 65535).": "Introduce un puerto sin privilegios (1024 - 65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduzca las direcciones, separadas por comas (\"tcp://ip:port\", \"tcp://host:port\"), o \"dynamic\" para llevar a cabo el descubrimiento automático de la dirección.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introducir direcciones separadas por coma (\"tcp://ip:port\", \"tcp://host:port\") o dinámicas para realizar el descubrimiento automático de la dirección.",
"Enter ignore patterns, one per line.": "Introducir patrones a ignorar, uno por línea.",
"Error": "Error",
"External File Versioning": "Versionado externo de fichero",
@@ -144,9 +144,9 @@
"Ignore": "Ignorar",
"Ignore Patterns": "Patrones a ignorar",
"Ignore Permissions": "Permisos a ignorar",
"Ignored Devices": "Ignored Devices",
"Ignored Folders": "Ignored Folders",
"Ignored at": "Ignored at",
"Ignored Devices": "Dispositivos Ignorados",
"Ignored Folders": "Carpetas Ignoradas",
"Ignored at": "Ignorado En",
"Incoming Rate Limit (KiB/s)": "Límite de descarga (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Una configuración incorrecta puede dañar los contenidos de la carpeta y hacer que Syncthing no funcione.",
"Introduced By": "Introducido por",
@@ -160,14 +160,14 @@
"Later": "Más tarde",
"Latest Change": "Último Cambio",
"Learn more": "Saber más",
"Limit": "Limit",
"Limit": "Límite",
"Listeners": "Oyentes",
"Loading data...": "Cargando datos...",
"Loading...": "Cargando...",
"Local Discovery": "Descubrimiento local",
"Local State": "Estado local",
"Local State (Total)": "Estado Local (Total)",
"Locally Changed Items": "Locally Changed Items",
"Locally Changed Items": "Ítems Cambiados Localmente",
"Log": "Registro",
"Log tailing paused. Click here to continue.": "Pausada la continuación del registro. Pulsar aquí para continuar.",
"Log tailing paused. Scroll to bottom continue.": "Pausada la continuación del registro. Continúa hasta el final.",
@@ -209,7 +209,7 @@
"Pause": "Pausar",
"Pause All": "Pausar todo",
"Paused": "Pausado",
"Pending changes": "Pending changes",
"Pending changes": "Cambios pendientes",
"Periodic scanning at given interval and disabled watching for changes": "Escaneo periódico en un intervalo determinado y desactivada la vigilancia de cambios",
"Periodic scanning at given interval and enabled watching for changes": "Escaneo periódico en un intervalo determinado y activada la vigilancia de cambios",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Escaneo periódico en un intervalo determinado y falló la configuración de la vigilancia de cambios, se reintentará cada 1 minuto:",
@@ -253,7 +253,7 @@
"Scanning": "Analizando",
"See external versioner help for supported templated command line parameters.": "Consultar la ayuda externa del versionador para ver las plantillas de los parámetros de línea de comandos",
"See external versioning help for supported templated command line parameters.": "Consultar la ayuda externa del versionado para ver las plantillas de los parámetros de línea de comandos",
"Select All": "Select All",
"Select All": "Seleccionar Todo",
"Select a version": "Selecciona una versión",
"Select latest version": "Selecciona la última versión",
"Select oldest version": "Selecciona la versión más antigua",
@@ -290,17 +290,18 @@
"Statistics": "Estadísticas",
"Stopped": "Detenido",
"Support": "Forum",
"Support Bundle": "Support Bundle",
"Support Bundle": "Lote de Soporte",
"Sync Protocol Listen Addresses": "Direcciones de escucha del protocolo de sincronización",
"Syncing": "Sincronizando",
"Syncthing has been shut down.": "Syncthing se ha detenido.",
"Syncthing includes the following software or portions thereof:": "Syncthing incluye el siguiente software o partes de él:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing es Software Gratuito y Open Source Software licenciado como MPL v2.0.",
"Syncthing is restarting.": "Syncthing se está reiniciando.",
"Syncthing is upgrading.": "Syncthing se está actualizando.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece no estar activo o hay un problema con tu conexión de internet. Reintentando...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing tiene problemas para procesar tu solicitud. Por favor, actualiza la página o reinicia Syncthing si el problema persiste.",
"Take me back": "Take me back",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
"Take me back": "Llévame atrás",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "La dirección del GUI es sobreescrita por las opciones de arranque. Los cambios de aquí no tendrán efecto mientras la sobreescritura esté activa.",
"The Syncthing admin interface is configured to allow remote access without a password.": "El panel de administración de Syncthing está configurado para permitir el acceso remoto sin contraseña.",
"The aggregated statistics are publicly available at the URL below.": "Las estadísticas agragadas están disponibles públicamente en la URL de abajo.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido grabada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
@@ -314,7 +315,7 @@
"The folder path cannot be blank.": "La ruta de la carpeta no puede estar en blanco.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Se utilizan los siguientes intervalos: para la primera hora se mantiene una versión cada 30 segundos, para el primer día se mantiene una versión cada hora, para los primeros 30 días se mantiene una versión diaria hasta la edad máxima de una semana.",
"The following items could not be synchronized.": "Los siguientes elementos no pueden ser sincronizados.",
"The following items were changed locally.": "The following items were changed locally.",
"The following items were changed locally.": "Los siguientes ítems fueron cambiados localmente.",
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar vacía.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El tiempo máximo para mantener una versión en días (introducir 0 para mantener las versiones indefinidamente).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "El porcentaje de espacio libre mínimo debe ser un número no negativo entre 0 y 100 (ambos inclusive).",
@@ -337,7 +338,7 @@
"Unavailable": "No disponible",
"Unavailable/Disabled by administrator or maintainer": "No disponible/Desactivado por el administrador o el mantenedor",
"Undecided (will prompt)": "Aún no decidido (se preguntará al usuario)",
"Unignore": "Unignore",
"Unignore": "Designorar",
"Unknown": "Desconocido",
"Unshared": "No compartido",
"Unused": "No usado",
@@ -350,14 +351,14 @@
"Uptime": "Tiempo de funcionamiento",
"Usage reporting is always enabled for candidate releases.": "El informe de uso está siempre habilitado en las versiones candidatas.",
"Use HTTPS for GUI": "Usar HTTPS para la Interfaz Gráfica de Usuario (GUI)",
"Use notifications from the filesystem to detect changed items.": "Use notifications from the filesystem to detect changed items.",
"Variable Size Blocks": "Variable Size Blocks",
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Variable size blocks (also \"large blocks\") are more efficient for large files.",
"Use notifications from the filesystem to detect changed items.": "Usar notificaciones del sistema de ficheros para detectar los ítems cambiados.",
"Variable Size Blocks": "Bloques de Tamaño Variable",
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Los bloques de tamaño variable (también conocidos como \"bloques grandes\") son más eficientes para ficheros grandes.",
"Version": "Versión",
"Versions": "Versiones",
"Versions Path": "Ruta de las versiones",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Las versiones se borran automáticamente si son más antiguas que la edad máxima o exceden el número de ficheros permitidos en un intervalo.",
"Waiting to scan": "Waiting to scan",
"Waiting to scan": "Esperando para escanear",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "¡Peligro! Esta ruta es un directorio principal de la carpeta ya existente \"{{otherFolder}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "'Peligro! Esta ruta es un subdirectorio de la carpeta ya existente \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Peligro! Esta ruta es un subdirectorio de una carpeta ya existente llamada \"{{otherFolder}}\".",
@@ -365,16 +366,16 @@
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Advertencia: Si estás usando un vigilante externo como {{syncthingInotify}}, deberías asegurarte de que está desactivado.",
"Watch for Changes": "Vigilar los cambios",
"Watching for Changes": "Vigilando los cambios",
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
"Watching for changes discovers most changes without periodic scanning.": "Vigilar los cambios descubre la mayoría de cambios sin escaneo periódico.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Cuando añada un nuevo dispositivo, tenga en cuenta que este debe añadirse también en el otro lado.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Cuando añada una nueva carpeta, tenga en cuenta que su ID se usa para unir carpetas entre dispositivos. Son sensibles a las mayúsculas y deben coincidir exactamente entre todos los dispositivos.",
"Yes": "Si",
"You can also select one of these nearby devices:": "Puedes seleccionar también uno de estos dispositivos cercanos:",
"You can change your choice at any time in the Settings dialog.": "Puedes cambiar tu elección en cualquier momento en el panel de Ajustes.",
"You can read more about the two release channels at the link below.": "Puedes leer más sobre los dos método de publicación de versiones en el siguiente enlace.",
"You have no ignored devices.": "You have no ignored devices.",
"You have no ignored folders.": "You have no ignored folders.",
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
"You have no ignored devices.": "No tienes dispositivos ignorados.",
"You have no ignored folders.": "No tienes carpetas ignoradas.",
"You have unsaved changes. Do you really want to discard them?": "Tienes cambios sin guardar. ¿Quieres descartarlos?",
"You must keep at least one version.": "Debes mantener al menos una versión.",
"days": "días",
"directories": "directorios",

View File

@@ -295,6 +295,7 @@
"Syncing": "Sincronizando",
"Syncthing has been shut down.": "Syncthing se ha detenido.",
"Syncthing includes the following software or portions thereof:": "Syncthing incluye el siguiente software o partes de él:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthing se está reiniciando.",
"Syncthing is upgrading.": "Syncthing se está actualizando.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece no estar activo o hay un problema con tu conexión de internet. Reintentando...",

View File

@@ -295,6 +295,7 @@
"Syncing": "Synkronoidaan",
"Syncthing has been shut down.": "Syncthing on sammutettu.",
"Syncthing includes the following software or portions thereof:": "Syncthing sisältää seuraavat ohjelmistot tai sen osat:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthing käynnistyy uudelleen.",
"Syncthing is upgrading.": "Syncthing päivittyy.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing näyttää olevan alhaalla tai internetyhteydessä on ongelma. Yritetään uudelleen...",

View File

@@ -295,6 +295,7 @@
"Syncing": "Synchronisation en cours",
"Syncthing has been shut down.": "Syncthing a été arrêté.",
"Syncthing includes the following software or portions thereof:": "Syncthing intègre les logiciels suivants (ou des éléments provenant de ces logiciels) :",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing est un logiciel Libre et Open Source sous licence MPL v2.0.",
"Syncthing is restarting.": "Syncthing redémarre.",
"Syncthing is upgrading.": "Syncthing se met à jour.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être arrêté, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",

View File

@@ -34,7 +34,7 @@
"Auto Accept": "Auto-akseptaasje",
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Automatyske fernijing biedt no de kar tusken stabyle ferzjes en ferzje kandidaten",
"Automatic upgrades": "Automatyske fernijings",
"Automatic upgrades are always enabled for candidate releases.": "Automatic upgrades are always enabled for candidate releases.",
"Automatic upgrades are always enabled for candidate releases.": "Automatyske opwurdearrings stean altyd oan foar kandidaat-ferzjes.",
"Automatically create or share folders that this device advertises at the default path.": "Meitsje of diel automatysk mappen dy't dit apparaat advertearret op it standert paad.",
"Available debug logging facilities:": "Beskikbere debug-lochfoarsjennings:",
"Be careful!": "Tink derom!",
@@ -56,7 +56,7 @@
"Copied from original": "Oernommen fan orizjineel",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 de folgende bydragers:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 de folgende Bydragers:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 de folgende Bydragers:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Meitsje negear-patroanen dy in besteande triem oerskriuwe yn {{path}}.",
"Danger!": "Gefaar!",
"Debugging Facilities": "Debug-foarsjennings",
@@ -98,7 +98,7 @@
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Fier in net-negatyf nûmer yn (bygelyks \"2.35\") en selektearje in ienheid. Percentages stean foar it part fan de totale skiifromte.",
"Enter a non-privileged port number (1024 - 65535).": "Fier in net-befoarrjochte poart-nûmer yn (1024 - 65535).",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Fier troch komma's skieden (\"tcp://ip:port\", \"tcp://host:port\") adressen yn of \"dynamic\" om automatyske ûntdekking fan it adres út te fieren.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Fier troch komma's skieden (\"tcp://ip:port\", \"tcp://host:port\") adressen yn of \"dynamic\" om automatyske ûntdekking fan it adres út te fieren.",
"Enter ignore patterns, one per line.": "Fier negearpatroanen yn, ien per rigel.",
"Error": "Flater",
"External File Versioning": "Ekstern ferzjebehear foar triemen",
@@ -160,14 +160,14 @@
"Later": "Letter",
"Latest Change": "Meast Resinte Feroarings",
"Learn more": "Mear witte",
"Limit": "Limit",
"Limit": "Limyt",
"Listeners": "Harkers",
"Loading data...": "Data oan it laden...",
"Loading...": "Oan it laden...",
"Local Discovery": "Lokale ûntdekking",
"Local State": "Lokale tastân",
"Local State (Total)": "Lokale tastân (Folledich)",
"Locally Changed Items": "Locally Changed Items",
"Locally Changed Items": "Lokaal Feroare Items",
"Log": "Loch",
"Log tailing paused. Click here to continue.": "Loch-sturt skofte. Klik hjir om fjirder te gean.",
"Log tailing paused. Scroll to bottom continue.": "Loch-sturt skofte. Rolje helendal nei ûnder om fjirder te gean.",
@@ -295,6 +295,7 @@
"Syncing": "Oan it Syncen",
"Syncthing has been shut down.": "Syncthing is útsetten",
"Syncthing includes the following software or portions thereof:": "Syncthing befettet de folgende sêftguod of parten dêrfan:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Fergees en Iepenboarne Programmatuer mei in MPL V2.0 lisinsje.",
"Syncthing is restarting.": "Syncthing oan it werstarten.",
"Syncthing is upgrading.": "Syncthing is oan it fernijen.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "It liket dêrop dat Syncthing op dit stuit net rint, of der is in swierrichheid mei jo ynternetferbining. Wurd no opnij besocht...",
@@ -314,7 +315,7 @@
"The folder path cannot be blank.": "It map-paad mei net leech wêze.",
"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 folgende yntervals wurd brûkt: foar it earste oere wurd eltse 30 sekonden in ferzje bewarre, foar de earste dei wurd eltse oere in ferzje bewarre, foar de earste 30 dagen wurd eltse dei in ferzje bewarre, oant ta de maksimale âldens wurd eltse wike in ferzje bewarre.",
"The following items could not be synchronized.": "De folgende items koene net syngronisearre wurde.",
"The following items were changed locally.": "The following items were changed locally.",
"The following items were changed locally.": "De neikommende items binne lokaal feroare.",
"The maximum age must be a number and cannot be blank.": "De maksimale âldens moat in nûmer en net leech wêze.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "De maksimale tiid dat in ferzje bewarre wurde moat (yn dagen, stel yn op 0 om ferzjes foar immer te bewarjen).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "It maksimale persintaazje frije skiifromte moat in posityf nûmer wêze tusken 0 en 100 (ynklusyf).",
@@ -350,14 +351,14 @@
"Uptime": "Rintiid",
"Usage reporting is always enabled for candidate releases.": "Brûkersrapportaazje stiet altyd oan foar ferzje kandidaten.",
"Use HTTPS for GUI": "Brûk HTTPS foar GUI",
"Use notifications from the filesystem to detect changed items.": "Use notifications from the filesystem to detect changed items.",
"Variable Size Blocks": "Variable Size Blocks",
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Variable size blocks (also \"large blocks\") are more efficient for large files.",
"Use notifications from the filesystem to detect changed items.": "Brûk notifikaasjes fan it triemsysteem om feroare items te detektearjen.",
"Variable Size Blocks": "Fariabele Blokgrutte",
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Fariabele blokgrutte (ek wol \"grutte blokken\") binne effisjinter foar gruttere triemmen.",
"Version": "Ferzje",
"Versions": "Ferzjes",
"Versions Path": "Ferzjes-paad",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Ferzjes wurde automatysk fuortsmiten wannear't se âlder binne dan de maksimale âldens of wannear it tal fan triemen yn in ynterval grutter is dan tastean.",
"Waiting to scan": "Waiting to scan",
"Waiting to scan": "Wachtet om te skennen",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warskôging, dit paad is in boppelizzende triemtafel fan in besteande map \"{{otherFolder}}\".",
"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}}\".",
@@ -365,7 +366,7 @@
"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",
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
"Watching for changes discovers most changes without periodic scanning.": "Sjen foar feroarings ûntdekt de measte feroarings sûnder periodyk skennen.",
"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

@@ -295,6 +295,7 @@
"Syncing": "Szinkronizálás",
"Syncthing has been shut down.": "Syncthing leállítva",
"Syncthing includes the following software or portions thereof:": "A Syncthing a következő programokat, vagy komponenseket tartalmazza.",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "A Syncthing szabad és nyílt forráskódú szoftver MPL v2.0 licenccel.",
"Syncthing is restarting.": "Syncthing újraindul",
"Syncthing is upgrading.": "Syncthing frissül",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Úgy tűnik, hogy a Syncthing nem működik, vagy valami probléma van a hálózati kapcsolattal. Újra próbálom...",

View File

@@ -56,7 +56,7 @@
"Copied from original": "Copiato dall'originale",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 i seguenti Collaboratori:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 i seguenti Collaboratori:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 i seguenti Collaboratori:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Creazione di schemi di esclusione, sovrascrivendo un file esistente in {{path}}.",
"Danger!": "Pericolo!",
"Debugging Facilities": "Servizi di Debug",
@@ -295,6 +295,7 @@
"Syncing": "Sincronizzazione in corso",
"Syncthing has been shut down.": "Syncthing è stato arrestato.",
"Syncthing includes the following software or portions thereof:": "Syncthing utilizza i seguenti software o porzioni di questi:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing è un software Libero e Open Source concesso in licenza MPL v2.0.",
"Syncthing is restarting.": "Riavvio di Syncthing in corso.",
"Syncthing is upgrading.": "Aggiornamento di Syncthing in corso.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing sembra inattivo, oppure c'è un problema con la tua connessione a Internet. Nuovo tentativo…",

View File

@@ -295,6 +295,7 @@
"Syncing": "同期中",
"Syncthing has been shut down.": "Syncthingをシャットダウンしました。",
"Syncthing includes the following software or portions thereof:": "Syncthingは以下のソフトウェアまたはその一部を内包しています:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthingを再起動しています。",
"Syncthing is upgrading.": "Syncthingをアップグレード中です。",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthingが落ちているか、インターネット接続に問題があります。リトライ中です…",

View File

@@ -295,6 +295,7 @@
"Syncing": "동기화 중",
"Syncthing has been shut down.": "Syncthing이 종료되었습니다.",
"Syncthing includes the following software or portions thereof:": "Syncthing은 다음과 같은 소프트웨어나 그 일부를 포함합니다:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthing이 재시작 중입니다.",
"Syncthing is upgrading.": "Syncthing이 업데이트 중입니다.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing이 중지되었거나 인터넷 연결에 문제가 있는 것 같습니다. 재시도 중입니다...",

View File

@@ -295,6 +295,7 @@
"Syncing": "Sutapatinama",
"Syncthing has been shut down.": "Syncthing išjungtas",
"Syncthing includes the following software or portions thereof:": "Syncthing naudoja šias programas ar jų dalis:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing yra laisva ir atvirojo kodo programinė įranga, licencijuota pagal MPL v2.0.",
"Syncthing is restarting.": "Syncthing perleidžiamas",
"Syncthing is upgrading.": "Syncthing atsinaujina.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing išjungta arba problemos su Interneto ryšių. Bandoma iš naujo...",

View File

@@ -295,6 +295,7 @@
"Syncing": "Synkroniserer",
"Syncthing has been shut down.": "Syncthing har blitt slått av.",
"Syncthing includes the following software or portions thereof:": "Syncthing inkluderer helt eller delvis følgende programvare:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthing starter på ny.",
"Syncthing is upgrading.": "Syncthing oppgraderer.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å være nede, eller så er det et problem med nettforbindelsen din. Prøver på ny …",

View File

@@ -295,6 +295,7 @@
"Syncing": "Synchroniseren",
"Syncthing has been shut down.": "Syncthing werd afgesloten.",
"Syncthing includes the following software or portions thereof:": "Syncthing bevat de volgende software of delen daarvan:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is gratis en opensource software onder licentie van MPL v2.0.",
"Syncthing is restarting.": "Syncthing is aan het herstarten.",
"Syncthing is upgrading.": "Syncthing is aan het bijwerken.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing lijkt gestopt te zijn, of er is een probleem met uw internetverbinding. Opnieuw proberen...",
@@ -372,9 +373,9 @@
"You can also select one of these nearby devices:": "U kunt ook een van deze apparaten in de buurt selecteren:",
"You can change your choice at any time in the Settings dialog.": "U kunt uw keuze op elk moment aanpassen in het instellingen-venster.",
"You can read more about the two release channels at the link below.": "U kunt meer te weten komen over de twee release-kanalen via onderstaande link.",
"You have no ignored devices.": "U heeft geen genegeerde apparaten.",
"You have no ignored folders.": "U heeft geen genegeerde mappen.",
"You have unsaved changes. Do you really want to discard them?": "U heeft niet-opgeslagen wijzigingen. Wilt u ze echt verwerpen?",
"You have no ignored devices.": "U hebt geen genegeerde apparaten.",
"You have no ignored folders.": "U hebt geen genegeerde mappen.",
"You have unsaved changes. Do you really want to discard them?": "U hebt niet-opgeslagen wijzigingen. Wilt u ze echt verwerpen?",
"You must keep at least one version.": "U moet minstens één versie bewaren.",
"days": "dagen",
"directories": "mappen",

View File

@@ -295,6 +295,7 @@
"Syncing": "Synchronizowanie",
"Syncthing has been shut down.": "Syncthing został wyłączony",
"Syncthing includes the following software or portions thereof:": "Syncthing zawiera następujące oprogramowanie lub ich częśći:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing to wolne oprogramowanie na licencji MPL 2.0",
"Syncthing is restarting.": "Restart Syncthing",
"Syncthing is upgrading.": "Aktualizowanie Syncthing",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing wydaje się być wyłączony lub jest problem z twoim połączeniem internetowym. Próbuje ponownie...",
@@ -351,7 +352,7 @@
"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",
"Use notifications from the filesystem to detect changed items.": "Use notifications from the filesystem to detect changed items.",
"Variable Size Blocks": "Variable Size Blocks",
"Variable Size Blocks": "Bloki różnych wielkości",
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Variable size blocks (also \"large blocks\") are more efficient for large files.",
"Version": "Wersja",
"Versions": "Wersje",

View File

@@ -295,6 +295,7 @@
"Syncing": "Sincronizando",
"Syncthing has been shut down.": "O Syncthing foi desligado.",
"Syncthing includes the following software or portions thereof:": "O Syncthing inclui os seguintes programas ou partes deles:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "O Syncthing está sendo reiniciado.",
"Syncthing is upgrading.": "O Syncthing está sendo atualizado.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Parece que o Syncthing está desligado ou há um problema com a sua conexão de internet. Tentando novamente...",

View File

@@ -295,6 +295,7 @@
"Syncing": "A Sincronizar",
"Syncthing has been shut down.": "O Syncthing foi desligado.",
"Syncthing includes the following software or portions thereof:": "O Syncthing inclui as seguintes aplicações ou partes delas:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing é Software Livre e de Código Aberto licenciado como MPL v2.0.",
"Syncthing is restarting.": "O Syncthing está a reiniciar.",
"Syncthing is upgrading.": "O Syncthing está a actualizar-se.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "O Syncthing parece estar em baixo, ou então existe um problema com a sua ligação à Internet. Tentando novamente...",
@@ -365,7 +366,7 @@
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Aviso: Se estiver a usar um verificador externo, tal como o {{syncthingInotify}}, deve certificar-se que está desactivado.",
"Watch for Changes": "Vigiar alterações",
"Watching for Changes": "Vigilância de alterações",
"Watching for changes discovers most changes without periodic scanning.": "A vigilância de alterações encontra a maior parte das alterações sem a verificação periódica.",
"Watching for changes discovers most changes without periodic scanning.": "A vigilância de alterações detecta a maior parte das alterações sem a necessidade de fazer uma verificação periódica.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando adicionar um novo dispositivo, lembre-se que este dispositivo tem que ser adicionado do outro lado também.",
"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.": "Quando adicionar uma nova pasta, lembre-se que o ID da pasta é utilizado para ligar as pastas entre dispositivos. É sensível às diferenças entre maiúsculas e minúsculas e tem que ter uma correspondência perfeita entre todos os dispositivos.",
"Yes": "Sim",

View File

@@ -295,6 +295,7 @@
"Syncing": "Синхронизация",
"Syncthing has been shut down.": "Syncthing был выключен.",
"Syncthing includes the following software or portions thereof:": "Syncthing включает в себя следующее ПО или его части:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing — свободное программное обеспечение с открытым кодом под лицензией MPL v2.0.",
"Syncthing is restarting.": "Перезапуск Syncthing.",
"Syncthing is upgrading.": "Обновление Syncthing.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",

View File

@@ -56,13 +56,13 @@
"Copied from original": "Skopírované z originálu",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následujúci prispivatelia:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 následujúci prispivatelia:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 the following Contributors:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 nasledujúci prispievatelia:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Vytváranie vzorov ignorovania, prepísanie existujúceho súboru v {{path}}.",
"Danger!": "Pozor!",
"Debugging Facilities": "Debugging Facilities",
"Default Folder Path": "Predvolená adresárová cesta",
"Deleted": "Zmazané",
"Deselect All": "Deselect All",
"Deselect All": "Odznačiť všetko",
"Device": "Zariadenie",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Zariadenie \"{{name}}\" ({{device}} na {{address}}) sa chce pripojiť. Pridať nové zariadenie?",
"Device ID": "ID zariadenia",
@@ -75,14 +75,14 @@
"Disabled periodic scanning and disabled watching for changes": "Disabled periodic scanning and disabled watching for changes",
"Disabled periodic scanning and enabled watching for changes": "Disabled periodic scanning and enabled watching for changes",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
"Discard": "Discard",
"Discard": "Zahodiť",
"Disconnected": "Odpojené",
"Discovered": "Zistené",
"Discovery": "Zisťovanie",
"Discovery Failures": "Zlyhania zisťovania",
"Do not restore": "Neobnovovať",
"Do not restore all": "Neobnovovať všetko",
"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?": "Chcete zapnúť sledovanie zmien vo všetkých priečinkoch?",
"Documentation": "Dokumentácia",
"Download Rate": "Rýchlosť sťahovania",
"Downloaded": "Stiahnuté",
@@ -144,8 +144,8 @@
"Ignore": "Ignorovať",
"Ignore Patterns": "Ignorované vzory",
"Ignore Permissions": "Ignorované práva",
"Ignored Devices": "Ignored Devices",
"Ignored Folders": "Ignored Folders",
"Ignored Devices": "Ignorované zariadenia",
"Ignored Folders": "Ingorované priečinky",
"Ignored at": "Ignored at",
"Incoming Rate Limit (KiB/s)": "Limit pre sťahovanie (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Nesprávna konfigurácia môže poškodiť váš adresár a spôsobiť nefunkčnosť aplikácie Súbory.",
@@ -167,7 +167,7 @@
"Local Discovery": "Lokálne vyhľadávanie",
"Local State": "Lokálny status",
"Local State (Total)": "Lokálny status (celkový)",
"Locally Changed Items": "Locally Changed Items",
"Locally Changed Items": "Lokálne zmenené položky",
"Log": "Záznam",
"Log tailing paused. Click here to continue.": "Posúvanie záznamu prerušené. Klikni pre pokračovanie.",
"Log tailing paused. Scroll to bottom continue.": "Log tailing paused. Scroll to bottom continue.",
@@ -209,11 +209,11 @@
"Pause": "Pozastaviť",
"Pause All": "Pozastaviť všetky",
"Paused": "Pozastavené",
"Pending changes": "Pending changes",
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
"Pending changes": "Čakajúce zmeny",
"Periodic scanning at given interval and disabled watching for changes": "Periodické skenovanie v zvolenom rozsahu a vypnuté sledovanie zmien.",
"Periodic scanning at given interval and enabled watching for changes": "Periodické skenovanie v zvolenom rozsahu a zapnuté sledovanie zmien.",
"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": "Prístupové oprávnenia",
"Please consult the release notes before performing a major upgrade.": "Pred spustením hlavnej aktualizácie si prosím prečítajte poznámky k vydaniu.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Zadajte prosím prihlasovanie meno a heslo v dialógovom okne nastavení.",
"Please wait": "Prosím čakajte",
@@ -224,7 +224,7 @@
"Quick guide to supported patterns": "Rýchly sprievodca podporovanými vzormi",
"RAM Utilization": "Využitie RAM",
"Random": "Náhodne",
"Receive Only": "Receive Only",
"Receive Only": "Iba prijímanie",
"Recent Changes": "Nedávne zmeny",
"Reduced by ignore patterns": "Znížené o ignorované vzory",
"Release Notes": "Poznámky k vydaniu",
@@ -237,7 +237,7 @@
"Rescan": "Opakovať skenovanie",
"Rescan All": "Opakovať skenovanie všetkých",
"Rescan Interval": "Interval opakovania skenovania",
"Rescans": "Rescans",
"Rescans": "Opätovné skeny",
"Restart": "Reštart",
"Restart Needed": "Potrebný reštart",
"Restarting": "Reštartovanie",
@@ -246,14 +246,14 @@
"Resume": "Pokračovať",
"Resume All": "Pokračuj so všetkými",
"Reused": "Opakovane použité",
"Revert Local Changes": "Revert Local Changes",
"Running": "Running",
"Revert Local Changes": "Vrátiť lokálne zmeny",
"Running": "Beží",
"Save": "Uložiť",
"Scan Time Remaining": "Zostávajúci čas skenovania",
"Scanning": "Skenovanie",
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
"Select All": "Select All",
"Select All": "Vybrať všetko",
"Select a version": "Zvoliť verziu",
"Select latest version": "Zvoliť najnovšiu verziu",
"Select oldest version": "Zvoliť najstaršiu verziu",
@@ -268,7 +268,7 @@
"Share With Devices": "Zdieľať so zariadeniami",
"Share this folder?": "Zdieľať tento adresár?",
"Shared With": "Zdieľané s",
"Sharing": "Sharing",
"Sharing": "Zdieľať",
"Show ID": "Zobraziť ID",
"Show QR": "Zobraziť QR",
"Show diff with previous version": "Ukázať rozdiely s predchádzajúcou verziou",
@@ -295,11 +295,12 @@
"Syncing": "Synchronizácia",
"Syncthing has been shut down.": "Syncthing bol vypnutý.",
"Syncthing includes the following software or portions thereof:": "Syncthing obsahuje nasledujúci software nebo jeho časti:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing je otvorený softvér s licenciou MPL v2.0.",
"Syncthing is restarting.": "Syncthing sa reštartuje.",
"Syncthing is upgrading.": "Syncthing sa aktualizuje.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing se zdá byť nefunkčný, alebo je problém s internetovým pripojením. Opakujem...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
"Take me back": "Take me back",
"Take me back": "Späť",
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
"The aggregated statistics are publicly available at the URL below.": "Súhrnné štatistiky sú verejne dostupné na uvedenej URL.",
@@ -314,7 +315,7 @@
"The folder path cannot be blank.": "Cesta k adresáru nemôže byť prázdna.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.",
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The following items were changed locally.": "The following items were changed locally.",
"The following items were changed locally.": "Tieto položky boli zmenené lokálne",
"The maximum age must be a number and cannot be blank.": "Maximálny vek musí byť číslo a nemôže byť prázdne.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Minimálne voľné miesto na disku v percentách musí byť kladné číslo medzi 0 a 100 (vrátane).",
@@ -351,20 +352,20 @@
"Usage reporting is always enabled for candidate releases.": "Hlásenia o používaní sú pri kandidátoch na vydanie vždy povolené.",
"Use HTTPS for GUI": "Použiť HTTPS pre grafické rozhranie",
"Use notifications from the filesystem to detect changed items.": "Use notifications from the filesystem to detect changed items.",
"Variable Size Blocks": "Variable Size Blocks",
"Variable Size Blocks": "Bloky premenlivej veľkosti",
"Variable size blocks (also \"large blocks\") are more efficient for large files.": "Variable size blocks (also \"large blocks\") are more efficient for large files.",
"Version": "Verzia",
"Versions": "Verzie",
"Versions Path": "Cesta k verziám",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Verzie sú automaticky zmazané ak sú staršie ako je maximálny časový limit alebo presiahnu počet súborov povolených v danom intervale.",
"Waiting to scan": "Waiting to scan",
"Waiting to scan": "Čaká na sken",
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a parent directory of an existing folder \"{{otherFolder}}\".",
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warning, this path is a parent directory of an existing folder \"{{otherFolderLabel}}\" ({{otherFolder}}).",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Warning, this path is a subdirectory of an existing folder \"{{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": "Sleduj zmeny",
"Watching for Changes": "Sledujú sa zmeny",
"Watching for changes discovers most changes without periodic scanning.": "Watching for changes discovers most changes without periodic scanning.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "When adding a new device, keep in mind that this device must be added on the other side too.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "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.",
@@ -372,9 +373,9 @@
"You can also select one of these nearby devices:": "Môžete tiež vybrať jedno z týchto blízkych zariadení:",
"You can change your choice at any time in the Settings dialog.": "Voľbu môžete kedykoľvek zmeniť v dialógu Nastavenia.",
"You can read more about the two release channels at the link below.": "O dvoch vydávacích kanáloch si môžete viacej prečítať v odkaze nižšie.",
"You have no ignored devices.": "You have no ignored devices.",
"You have no ignored folders.": "You have no ignored folders.",
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
"You have no ignored devices.": "Nemáte žiadne ignorované zariadenia.",
"You have no ignored folders.": "Nemáte žiadne ignorované priečinky.",
"You have unsaved changes. Do you really want to discard them?": "Niektoré zmeny ste neuložili. Chcete ich skutočne zahodiť?",
"You must keep at least one version.": "Musíte ponechať aspoň jednu verziu",
"days": "dní",
"directories": "adresáre",

View File

@@ -1,5 +1,5 @@
{
"A device with that ID is already added.": "En enhet med det ID är redan tillagt.",
"A device with that ID is already added.": "En enhet med det ID:t är redan tillagt.",
"A negative number of days doesn't make sense.": "Ett negativt antal dagar är inte rimligt.",
"A new major version may not be compatible with previous versions.": "En ny huvudversion kan eventuellt vara inkompatibel med tidigare versioner.",
"API Key": "API-nyckel",
@@ -55,7 +55,7 @@
"Copied from elsewhere": "Kopierat från annanstans",
"Copied from original": "Kopierat från original",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 följande bidragare:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 följande bidragande:",
"Copyright © 2014-2017 the following Contributors:": "Copyright © 2014-2017 följande bidragsgivare:",
"Copyright © 2014-2019 the following Contributors:": "Copyright © 2014-2019 följande bidragsgivare:",
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Skapa ignorera mönster, skriver över en existerande fil på {{path}}.",
"Danger!": "Fara!",
@@ -295,6 +295,7 @@
"Syncing": "Synkroniserar",
"Syncthing has been shut down.": "Syncthing har stängts.",
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller följande mjukvarupaket eller delar av dem:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing har fri och öppen källkod licensierad som MPL v2.0.",
"Syncthing is restarting.": "Syncthing startar om.",
"Syncthing is upgrading.": "Syncthing uppgraderas.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd eller så är det problem med din Internetanslutning. Försöker igen...",

View File

@@ -295,6 +295,7 @@
"Syncing": "Синхронізація",
"Syncthing has been shut down.": "Syncthing вимкнено (закрито).",
"Syncthing includes the following software or portions thereof:": "Syncthing містить наступне програмне забезпечення (або його частини):",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthing перезавантажується.",
"Syncthing is upgrading.": "Syncthing оновлюється.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Схоже на те, що Syncthing закритий, або виникла проблема із Інтернет-з’єднанням. Проводиться повторна спроба з’єднання…",

View File

@@ -295,6 +295,7 @@
"Syncing": "同步中",
"Syncthing has been shut down.": "Syncthing 已关闭。",
"Syncthing includes the following software or portions thereof:": "Syncthing 使用了下列软件或其中的一部分:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing 是个以 MPL v2.0 授权的免费开源软件。",
"Syncthing is restarting.": "Syncthing 正在重启。",
"Syncthing is upgrading.": "Syncthing 正在升级。",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎关闭了,或者您的网络连接存在故障。重试中…",

View File

@@ -295,6 +295,7 @@
"Syncing": "正在同步",
"Syncthing has been shut down.": "Syncthing 已經關閉。",
"Syncthing includes the following software or portions thereof:": "Syncthing 包括以下軟體或其中的一部分:",
"Syncthing is Free and Open Source Software licensed as MPL v2.0.": "Syncthing is Free and Open Source Software licensed as MPL v2.0.",
"Syncthing is restarting.": "Syncthing 正在重新啟動。",
"Syncthing is upgrading.": "Syncthing 正在進行升級。",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎離線了,或者您的網際網路連線出現問題。正在重試...",

View File

@@ -337,7 +337,7 @@
</button>
<div id="folder-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<table class="table table-condensed table-striped">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr ng-show="folder.label != undefined && folder.label.length > 0">
<th><span class="fas fa-fw fa-info-circle"></span>&nbsp;<span translate>Folder ID</span></th>
@@ -551,7 +551,7 @@
</button>
<div id="device-this" class="panel-collapse collapse in">
<div class="panel-body">
<table class="table table-condensed table-striped">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr>
<th><span class="fas fa-fw fa-cloud-download-alt"></span>&nbsp;<span translate>Download Rate</span></th>
@@ -667,7 +667,7 @@
</button>
<div id="device-{{$index}}" class="panel-collapse collapse">
<div class="panel-body">
<table class="table table-condensed table-striped">
<table class="table table-condensed table-striped table-auto">
<tbody>
<tr ng-if="connections[deviceCfg.deviceID].connected">
<th><span class="fas fa-fw fa-cloud-download-alt"></span>&nbsp;<span translate>Download Rate</span></th>

View File

@@ -179,7 +179,7 @@ function buildTree(children) {
key: keySoFar.join('/'),
folder: true,
children: []
}
};
parent.children.push(child);
parent = child;
}
@@ -209,7 +209,7 @@ function unitPrefixed(input, binary) {
var i = '';
if (binary) {
factor = 1024;
i = 'i'
i = 'i';
}
if (input > factor * factor * factor * factor * 1000) {
// Don't show any decimals for more than 4 digits

View File

@@ -14,7 +14,7 @@
<p translate>Copyright &copy; 2014-2019 the following Contributors:</p>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, Aaron Bieber, Adam Piggott, Adel Qalieh, Alessandro G., Andrew Dunham, Andrew Rabert, Andrey D, Antoine Lamielle, Aranjedeath, Arthur Axel fREW Schmidt, BAHADIR YILMAZ, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benno Fünfstück, Benny Ng, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Chris Tonkinson, Colin Kennedy, Cromefire_, Dale Visser, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Erik Meitner, Evgeny Kuznetsov, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Graham Miln, Han Boetes, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan Cross, Jose Manuel Delicado, Jörg Thalheim, Karol Różycki, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Kurt Fitzner, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mark Pulford, Mateusz Naściszewski, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max Schulze, MaximAL, Maxime Thirouin, Michael Jephcote, Michael Tilli, Mike Boone, MikeLund, Nicholas Rishel, Nico Stapelbroek, Nicolas Braud-Santoni, Niels Peter Roest, Nils Jakobi, NoLooseEnds, Oyebanji Jacob Mayowa, Pascal Jungblut, Pawel Palenica, Paweł Rozlach, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Richard Hartmann, Robert Carosi, Roman Zaynetdinov, Ross Smith II, Sacheendra Talluri, Scott Klupfel, Sly_tom_cat, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tomas Cerveny, Tommy Thorn, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, Vladimir Rusinov, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, chucic, derekriemer, desbma, georgespatton, janost, jaseg, klemens, marco-m, otbutz, perewa, rubenbe, wangguoliang, xjtdy888, 佛跳墙
Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, Aaron Bieber, Adam Piggott, Adel Qalieh, Alessandro G., Andrew Dunham, Andrew Rabert, Andrey D, Antoine Lamielle, Aranjedeath, Arthur Axel fREW Schmidt, BAHADIR YILMAZ, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benno Fünfstück, Benny Ng, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Chris Tonkinson, Colin Kennedy, Cromefire_, Dale Visser, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Erik Meitner, Evgeny Kuznetsov, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Graham Miln, Han Boetes, Harrison Jones, Heiko Zuerker, Hugo Locurcio, Iain Barnett, Ian Johnson, Iskander Sharipov, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonas Thelemann, Jonathan Cross, Jose Manuel Delicado, Jörg Thalheim, Karol Różycki, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Kurt Fitzner, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mark Pulford, Mateusz Naściszewski, Matic Potočnik, Matt Burke, Matt Robenolt, Matteo Ruina, Maurizio Tomasi, Max Schulze, MaximAL, Maxime Thirouin, Michael Jephcote, Michael Tilli, Mike Boone, MikeLund, Nicholas Rishel, Nico Stapelbroek, Nicolas Braud-Santoni, Niels Peter Roest, Nils Jakobi, Nitroretro, NoLooseEnds, Oyebanji Jacob Mayowa, Pascal Jungblut, Pawel Palenica, Paweł Rozlach, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Richard Hartmann, Robert Carosi, Roman Zaynetdinov, Ross Smith II, Sacheendra Talluri, Scott Klupfel, Sly_tom_cat, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tomas Cerveny, Tommy Thorn, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, Vladimir Rusinov, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, chucic, derekriemer, desbma, georgespatton, janost, jaseg, klemens, marco-m, otbutz, perewa, rubenbe, wangguoliang, xjtdy888, 佛跳墙
</div>
</div>
<hr />

View File

@@ -23,7 +23,6 @@ angular.module('syncthing.core')
svg.appendChild(rect);
};
var rect;
var row;
var col;
var middleCol;

View File

@@ -8,7 +8,6 @@ angular.module('syncthing.core')
var uid = new Date();
var storage = window.localStorage;
storage.setItem(uid, uid);
var success = storage.getItem(uid) == uid;
storage.removeItem(uid);
return storage;
} catch (exception) {

View File

@@ -9,7 +9,7 @@
<div id="log-viewer-log" class="tab-pane in active">
<label translate ng-if="logging.logEntries.length == 0">Loading...</label>
<textarea id="logViewerText" class="form-control" rows="20" ng-if="logging.logEntries.length != 0" readonly style="font-family: Consolas; font-size: 11px; overflow: auto;">{{ logging.content() }}</textarea>
<p translate class="help-block" ng-style="{'visibility': logging.paused ? 'visible' : 'hidden'}">Log tailing paused. Scroll to bottom continue.</p>
<p translate class="help-block" ng-style="{'visibility': logging.paused ? 'visible' : 'hidden'}">Log tailing paused. Scroll to the bottom to continue.</p>
</div>
<div id="log-viewer-facilities" class="tab-pane">

View File

@@ -6,7 +6,7 @@
<span translate>Please consult the release notes before performing a major upgrade.</span>
</p>
<p>
<a href="https://github.com/syncthing/syncthing/releases/tag/{{upgradeInfo.latest}}" target="_blank" translate>Release Notes</a>
<a ng-href="https://github.com/syncthing/syncthing/releases/tag/{{upgradeInfo.latest}}" target="_blank" translate>Release Notes</a>
</p>
</div>
<div class="modal-footer">

View File

@@ -329,7 +329,7 @@ angular.module('syncthing.core')
});
$scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
$scope.model[arg.data.folder].pullErrors = arg.data.errors.length;
$scope.model[arg.data.folder].errors = arg.data.errors.length;
});
$scope.$on(Events.FOLDER_SCAN_PROGRESS, function (event, arg) {
@@ -357,7 +357,7 @@ angular.module('syncthing.core')
recalcLocalStateTotal();
console.log("refreshFolder", folder, data);
}).error($scope.emitHTTPError);
}, 1000, true);
}, 1000);
}
debouncedFuncs[key]();
}
@@ -479,7 +479,7 @@ angular.module('syncthing.core')
$scope.completion[device]._needItems = 0;
} else {
$scope.completion[device]._total = Math.floor(100 * (1 - needed / total));
$scope.completion[device]._needBytes = needed
$scope.completion[device]._needBytes = needed;
$scope.completion[device]._needItems = items + deletes;
}
@@ -653,7 +653,7 @@ angular.module('syncthing.core')
};
$scope.refreshFailed = function (page, perpage) {
var url = urlbase + '/folder/pullerrors?folder=' + encodeURIComponent($scope.failed.folder);
var url = urlbase + '/folder/errors?folder=' + encodeURIComponent($scope.failed.folder);
url += "&page=" + page + "&perpage=" + perpage;
$http.get(url).success(function (data) {
$scope.failed = data;
@@ -676,7 +676,7 @@ angular.module('syncthing.core')
$scope.refreshLocalChanged = function (page, perpage) {
var url = urlbase + '/db/localchanged?folder=';
url += encodeURIComponent($scope.localChanged.folder);
url += encodeURIComponent($scope.localChangedFolder);
url += "&page=" + page + "&perpage=" + perpage;
$http.get(url).success(function (data) {
$scope.localChanged = data;
@@ -1172,7 +1172,7 @@ angular.module('syncthing.core')
}
});
}
}
};
$scope.editSettings = function () {
// Make a working copy
@@ -1253,7 +1253,7 @@ angular.module('syncthing.core')
var ignoredFoldersEquals = angular.equals($scope.config.devices, $scope.tmpDevices);
console.log("settings equals - options: " + optionsEqual + " gui: " + guiEquals + " ignDev: " + ignoredDevicesEquals + " ignFol: " + ignoredFoldersEquals);
return !optionsEqual || !guiEquals || !ignoredDevicesEquals || !ignoredFoldersEquals;
}
};
$scope.saveSettings = function () {
// Make sure something changed
@@ -1534,7 +1534,7 @@ angular.module('syncthing.core')
$scope.thisDevice = function () {
return $scope.thisDeviceIn($scope.devices);
}
};
$scope.thisDeviceIn = function (l) {
for (var i = 0; i < l.length; i++) {
@@ -1573,7 +1573,7 @@ angular.module('syncthing.core')
}
});
return errs;
}
};
$scope.friendlyDevices = function (str) {
for (var i = 0; i < $scope.devices.length; i++) {
@@ -1888,7 +1888,7 @@ angular.module('syncthing.core')
}
var label = $scope.folders[folderID].label;
return label && label.length > 0 ? label : folderID;
}
};
$scope.deleteFolder = function (id) {
$('#editFolder').modal('hide');
@@ -2193,7 +2193,7 @@ angular.module('syncthing.core')
if (!$scope.model[folder]) {
return false;
}
return $scope.model[folder].pullErrors !== 0;
return $scope.model[folder].errors !== 0;
};
$scope.override = function (folder) {
@@ -2201,7 +2201,7 @@ angular.module('syncthing.core')
};
$scope.showLocalChanged = function (folder) {
$scope.localChanged.folder = folder;
$scope.localChangedFolder = folder;
$scope.localChanged = $scope.refreshLocalChanged(1, 10);
$('#localChanged').modal().one('hidden.bs.modal', function () {
$scope.localChanged = {};

View File

@@ -12,7 +12,7 @@
</table>
<dir-pagination-controls on-page-change="refreshFailed(newPageNumber, failed.perpage)" pagination-id="failed"></dir-pagination-controls>
<ul class="pagination pull-right">
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: failed.page == option }">
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: failed.perpage == option }">
<a href="#" ng-click="refreshFailed(failed.page, option)">{{option}}</a>
</li>
</ul>

View File

@@ -10,14 +10,14 @@
<th translate>Size</th>
</tr>
</thead>
<tr dir-paginate="file in localChanged.files | itemsPerPage: localChanged.perpage" current-page="localChanged.page" total-items="model[localChanged.folder].receiveOnlyTotalItems" pagination-id="localChanged">
<tr dir-paginate="file in localChanged.files | itemsPerPage: localChanged.perpage" current-page="localChanged.page" total-items="model[localChangedFolder].receiveOnlyTotalItems" pagination-id="localChanged">
<td>{{file.name}}</td>
<td><span ng-hide="file.type == 'DIRECTORY'">{{file.size | binary}}B</span></td>
</tr>
</table>
<dir-pagination-controls on-page-change="refreshLocalChanged(newPageNumber, localChanged.perpage)" pagination-id="localChanged"></dir-pagination-controls>
<ul class="pagination pull-right">
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: localChanged.page == option }">
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: localChanged.perpage == option }">
<a href="#" ng-click="refreshLocalChanged(localChanged.page, option)">{{option}}</a>
</li>
</ul>

View File

@@ -35,9 +35,14 @@ import (
"github.com/thejerf/suture"
)
var (
confDir = filepath.Join("testdata", "config")
token = filepath.Join(confDir, "csrftokens.txt")
)
func TestMain(m *testing.M) {
orig := locations.GetBaseDir(locations.ConfigBaseDir)
locations.SetBaseDir(locations.ConfigBaseDir, "testdata/config")
locations.SetBaseDir(locations.ConfigBaseDir, confDir)
exitCode := m.Run()
@@ -47,6 +52,15 @@ func TestMain(m *testing.M) {
}
func TestCSRFToken(t *testing.T) {
defer os.Remove(token)
max := 250
int := 5
if testing.Short() {
max = 20
int = 2
}
t1 := newCsrfToken()
t2 := newCsrfToken()
@@ -55,8 +69,8 @@ func TestCSRFToken(t *testing.T) {
t.Fatal("t3 should be valid")
}
for i := 0; i < 250; i++ {
if i%5 == 0 {
for i := 0; i < max; i++ {
if i%int == 0 {
// t1 and t2 should remain valid by virtue of us checking them now
// and then.
if !validCsrfToken(t1) {
@@ -89,6 +103,7 @@ func TestStopAfterBrokenConfig(t *testing.T) {
w := config.Wrap("/dev/null", cfg)
srv := New(protocol.LocalDeviceID, w, "", "syncthing", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, false).(*service)
defer os.Remove(token)
srv.started = make(chan string)
sup := suture.New("test", suture.Spec{
@@ -499,6 +514,7 @@ func startHTTP(cfg *mockedConfig) (string, error) {
urService := ur.New(cfg, m, connections, false)
summaryService := model.NewFolderSummaryService(cfg, m, protocol.LocalDeviceID)
svc := New(protocol.LocalDeviceID, cfg, assetDir, "syncthing", m, eventSub, diskEventSub, discoverer, connections, urService, summaryService, errorLog, systemLog, cpu, nil, false).(*service)
defer os.Remove(token)
svc.started = addrChan
// Actually start the API service
@@ -960,6 +976,7 @@ func TestEventMasks(t *testing.T) {
defSub := new(mockedEventSub)
diskSub := new(mockedEventSub)
svc := New(protocol.LocalDeviceID, cfg, "", "syncthing", nil, defSub, diskSub, nil, nil, nil, nil, nil, nil, nil, nil, false).(*service)
defer os.Remove(token)
if mask := svc.getEventMask(""); mask != DefaultEventMask {
t.Errorf("incorrect default mask %x != %x", int64(mask), int64(DefaultEventMask))

View File

@@ -505,7 +505,7 @@ func (db *instance) getIndexID(device, folder []byte) protocol.IndexID {
func (db *instance) setIndexID(device, folder []byte, id protocol.IndexID) {
bs, _ := id.Marshal() // marshalling can't fail
if err := db.Put(db.keyer.GenerateIndexIDKey(nil, device, folder), bs, nil); err != nil {
if err := db.Put(db.keyer.GenerateIndexIDKey(nil, device, folder), bs, nil); err != nil && err != leveldb.ErrClosed {
panic("storing index ID: " + err.Error())
}
}

View File

@@ -9,12 +9,15 @@ package db
import (
"os"
"strings"
"sync"
"sync/atomic"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/syndtr/goleveldb/leveldb/storage"
"github.com/syndtr/goleveldb/leveldb/util"
)
const (
@@ -34,6 +37,8 @@ type Lowlevel struct {
location string
folderIdx *smallIndex
deviceIdx *smallIndex
closed bool
closeMut *sync.RWMutex
}
// Open attempts to open the database at the given location, and runs
@@ -103,6 +108,36 @@ func (db *Lowlevel) Delete(key []byte, wo *opt.WriteOptions) error {
return db.DB.Delete(key, wo)
}
func (db *Lowlevel) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
db.closeMut.RLock()
defer db.closeMut.RUnlock()
if db.closed {
return &closedIter{}
}
return db.DB.NewIterator(slice, ro)
}
func (db *Lowlevel) GetSnapshot() snapshot {
snap, err := db.DB.GetSnapshot()
if err != nil {
if err == leveldb.ErrClosed {
return &closedSnap{}
}
panic(err)
}
return snap
}
func (db *Lowlevel) Close() {
db.closeMut.Lock()
defer db.closeMut.Unlock()
if db.closed {
return
}
db.closed = true
db.DB.Close()
}
// NewLowlevel wraps the given *leveldb.DB into a *lowlevel
func NewLowlevel(db *leveldb.DB, location string) *Lowlevel {
return &Lowlevel{
@@ -110,6 +145,7 @@ func NewLowlevel(db *leveldb.DB, location string) *Lowlevel {
location: location,
folderIdx: newSmallIndex(db, []byte{KeyTypeFolderIdx}),
deviceIdx: newSmallIndex(db, []byte{KeyTypeDeviceIdx}),
closeMut: &sync.RWMutex{},
}
}
@@ -150,7 +186,37 @@ func (b *batch) checkFlush() {
}
func (b *batch) flush() {
if err := b.db.Write(b.Batch, nil); err != nil {
if err := b.db.Write(b.Batch, nil); err != nil && err != leveldb.ErrClosed {
panic(err)
}
}
type closedIter struct{}
func (it *closedIter) Release() {}
func (it *closedIter) Key() []byte { return nil }
func (it *closedIter) Value() []byte { return nil }
func (it *closedIter) Next() bool { return false }
func (it *closedIter) Prev() bool { return false }
func (it *closedIter) First() bool { return false }
func (it *closedIter) Last() bool { return false }
func (it *closedIter) Seek(key []byte) bool { return false }
func (it *closedIter) Valid() bool { return false }
func (it *closedIter) Error() error { return leveldb.ErrClosed }
func (it *closedIter) SetReleaser(releaser util.Releaser) {}
type snapshot interface {
Get([]byte, *opt.ReadOptions) ([]byte, error)
Has([]byte, *opt.ReadOptions) (bool, error)
NewIterator(*util.Range, *opt.ReadOptions) iterator.Iterator
Release()
}
type closedSnap struct{}
func (s *closedSnap) Get([]byte, *opt.ReadOptions) ([]byte, error) { return nil, leveldb.ErrClosed }
func (s *closedSnap) Has([]byte, *opt.ReadOptions) (bool, error) { return false, leveldb.ErrClosed }
func (s *closedSnap) NewIterator(*util.Range, *opt.ReadOptions) iterator.Iterator {
return &closedIter{}
}
func (s *closedSnap) Release() {}

View File

@@ -14,17 +14,13 @@ import (
// A readOnlyTransaction represents a database snapshot.
type readOnlyTransaction struct {
*leveldb.Snapshot
snapshot
keyer keyer
}
func (db *instance) newReadOnlyTransaction() readOnlyTransaction {
snap, err := db.GetSnapshot()
if err != nil {
panic(err)
}
return readOnlyTransaction{
Snapshot: snap,
snapshot: db.GetSnapshot(),
keyer: db.keyer,
}
}
@@ -129,7 +125,10 @@ func (t readWriteTransaction) updateGlobal(gk, keyBuf, folder, device []byte, fi
if new, ok := t.getFileByKey(keyBuf); ok {
global = new
} else {
panic("This file must exist in the db")
// This file must exist in the db, so this must be caused
// by the db being closed - bail out.
l.Debugln("File should exist:", name)
return keyBuf, false
}
}
@@ -246,7 +245,10 @@ func (t readWriteTransaction) removeFromGlobal(gk, keyBuf, folder, device []byte
keyBuf = t.keyer.GenerateDeviceFileKey(keyBuf, folder, fl.Versions[0].Device, file)
global, ok := t.getFileByKey(keyBuf)
if !ok {
panic("This file must exist in the db")
// This file must exist in the db, so this must be caused
// by the db being closed - bail out.
l.Debugln("File should exist:", file)
return keyBuf
}
keyBuf = t.updateLocalNeed(keyBuf, folder, file, fl, global)
meta.addFile(protocol.GlobalDeviceID, global)

View File

@@ -566,3 +566,9 @@ func TestRel(t *testing.T) {
}
}
}
func TestBasicWalkSkipSymlink(t *testing.T) {
_, dir := setup(t)
defer os.RemoveAll(dir)
testWalkSkipSymlink(t, FilesystemTypeBasic, dir)
}

View File

@@ -76,3 +76,19 @@ func rel(path, prefix string) string {
}
var evalSymlinks = filepath.EvalSymlinks
// watchPaths adjust the folder root for use with the notify backend and the
// corresponding absolute path to be passed to notify to watch name.
func (f *BasicFilesystem) watchPaths(name string) (string, string, error) {
root, err := evalSymlinks(f.root)
if err != nil {
return "", "", err
}
absName, err := rooted(name, root)
if err != nil {
return "", "", err
}
return filepath.Join(absName, "..."), root, nil
}

View File

@@ -11,8 +11,6 @@ package fs
import (
"context"
"errors"
"path/filepath"
"runtime"
"github.com/syncthing/notify"
)
@@ -23,22 +21,11 @@ import (
var backendBuffer = 500
func (f *BasicFilesystem) Watch(name string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, error) {
evalRoot, err := evalSymlinks(f.root)
watchPath, root, err := f.watchPaths(name)
if err != nil {
return nil, err
}
absName, err := rooted(name, evalRoot)
if err != nil {
return nil, err
}
// Remove `\\?\` prefix if the path is just a drive letter as a dirty
// fix for https://github.com/syncthing/syncthing/issues/5578
if runtime.GOOS == "windows" && len(absName) <= 7 && len(absName) > 4 && absName[:4] == `\\?\` {
absName = absName[4:]
}
outChan := make(chan Event)
backendChan := make(chan notify.EventInfo, backendBuffer)
@@ -49,11 +36,11 @@ func (f *BasicFilesystem) Watch(name string, ignore Matcher, ctx context.Context
if ignore.SkipIgnoredDirs() {
absShouldIgnore := func(absPath string) bool {
return ignore.ShouldIgnore(f.unrootedChecked(absPath, evalRoot))
return ignore.ShouldIgnore(f.unrootedChecked(absPath, root))
}
err = notify.WatchWithFilter(filepath.Join(absName, "..."), backendChan, absShouldIgnore, eventMask)
err = notify.WatchWithFilter(watchPath, backendChan, absShouldIgnore, eventMask)
} else {
err = notify.Watch(filepath.Join(absName, "..."), backendChan, eventMask)
err = notify.Watch(watchPath, backendChan, eventMask)
}
if err != nil {
notify.Stop(backendChan)
@@ -63,7 +50,7 @@ func (f *BasicFilesystem) Watch(name string, ignore Matcher, ctx context.Context
return nil, err
}
go f.watchLoop(name, evalRoot, backendChan, outChan, ignore, ctx)
go f.watchLoop(name, root, backendChan, outChan, ignore, ctx)
return outChan, nil
}

View File

@@ -149,6 +149,53 @@ func TestWatchRename(t *testing.T) {
testScenario(t, name, testCase, expectedEvents, allowedEvents, fakeMatcher{})
}
// TestWatchWinRoot checks that a watch at a drive letter does not panic due to
// out of root event on every event.
// https://github.com/syncthing/syncthing/issues/5695
func TestWatchWinRoot(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows specific test")
}
outChan := make(chan Event)
backendChan := make(chan notify.EventInfo, backendBuffer)
ctx, cancel := context.WithCancel(context.Background())
// testFs is Filesystem, but we need BasicFilesystem here
root := `D:\`
fs := newBasicFilesystem(root)
watch, root, err := fs.watchPaths(".")
if err != nil {
t.Fatal(err)
}
go func() {
defer func() {
if r := recover(); r != nil {
t.Error(r)
}
cancel()
}()
fs.watchLoop(".", root, backendChan, outChan, fakeMatcher{}, ctx)
}()
// filepath.Dir as watch has a /... suffix
name := "foo"
backendChan <- fakeEventInfo(filepath.Join(filepath.Dir(watch), name))
select {
case <-time.After(10 * time.Second):
cancel()
t.Errorf("Timed out before receiving event")
case ev := <-outChan:
if ev.Name != name {
t.Errorf("Unexpected event %v, expected %v", ev.Name, name)
}
case <-ctx.Done():
}
}
// TestWatchOutside checks that no changes from outside the folder make it in
func TestWatchOutside(t *testing.T) {
outChan := make(chan Event)
@@ -391,7 +438,7 @@ func testScenario(t *testing.T, name string, testCase func(), expectedEvents, al
testCase()
select {
case <-time.After(time.Minute):
case <-time.After(10 * time.Second):
t.Errorf("Timed out before receiving all expected events")
case <-ctx.Done():

View File

@@ -222,3 +222,28 @@ func evalSymlinks(in string) (string, error) {
}
return longFilenameSupport(out), nil
}
// watchPaths adjust the folder root for use with the notify backend and the
// corresponding absolute path to be passed to notify to watch name.
func (f *BasicFilesystem) watchPaths(name string) (string, string, error) {
root, err := evalSymlinks(f.root)
if err != nil {
return "", "", err
}
// Remove `\\?\` prefix if the path is just a drive letter as a dirty
// fix for https://github.com/syncthing/syncthing/issues/5578
if filepath.Clean(name) == "." && len(root) <= 7 && len(root) > 4 && root[:4] == `\\?\` {
root = root[4:]
}
absName, err := rooted(name, root)
if err != nil {
return "", "", err
}
root = f.resolveWin83(root)
absName = f.resolveWin83(absName)
return filepath.Join(absName, "..."), root, nil
}

View File

@@ -44,6 +44,10 @@ func TestWindowsPaths(t *testing.T) {
func TestResolveWindows83(t *testing.T) {
fs, dir := setup(t)
if isMaybeWin83(dir) {
dir = fs.resolveWin83(dir)
fs = newBasicFilesystem(dir)
}
defer os.RemoveAll(dir)
shortAbs, _ := fs.rooted("LFDATA~1")
@@ -71,6 +75,10 @@ func TestResolveWindows83(t *testing.T) {
func TestIsWindows83(t *testing.T) {
fs, dir := setup(t)
if isMaybeWin83(dir) {
dir = fs.resolveWin83(dir)
fs = newBasicFilesystem(dir)
}
defer os.RemoveAll(dir)
tempTop, _ := fs.rooted(TempName("baz"))

View File

@@ -8,6 +8,7 @@ package fs
import (
"path/filepath"
"runtime"
"testing"
)
@@ -106,3 +107,66 @@ func TestFileModeString(t *testing.T) {
t.Fatalf("Got %v, expected %v", fm.String(), exp)
}
}
func TestIsParent(t *testing.T) {
test := func(path, parent string, expected bool) {
t.Helper()
path = filepath.FromSlash(path)
parent = filepath.FromSlash(parent)
if res := IsParent(path, parent); res != expected {
t.Errorf(`Unexpected result: IsParent("%v", "%v"): %v should be %v`, path, parent, res, expected)
}
}
testBoth := func(path, parent string, expected bool) {
t.Helper()
test(path, parent, expected)
if runtime.GOOS == "windows" {
test("C:/"+path, "C:/"+parent, expected)
} else {
test("/"+path, "/"+parent, expected)
}
}
// rel - abs
for _, parent := range []string{"/", "/foo", "/foo/bar"} {
for _, path := range []string{"", ".", "foo", "foo/bar", "bas", "bas/baz"} {
if runtime.GOOS == "windows" {
parent = "C:/" + parent
}
test(parent, path, false)
test(path, parent, false)
}
}
// equal
for i, path := range []string{"/", "/foo", "/foo/bar", "", ".", "foo", "foo/bar"} {
if i < 3 && runtime.GOOS == "windows" {
path = "C:" + path
}
test(path, path, false)
}
test("", ".", false)
test(".", "", false)
for _, parent := range []string{"", "."} {
for _, path := range []string{"foo", "foo/bar"} {
test(path, parent, true)
test(parent, path, false)
}
}
for _, parent := range []string{"foo", "foo/bar"} {
for _, path := range []string{"bar", "bar/foo"} {
testBoth(path, parent, false)
testBoth(parent, path, false)
}
}
for _, parent := range []string{"foo", "foo/bar"} {
for _, path := range []string{"foo/bar/baz", "foo/bar/baz/bas"} {
testBoth(path, parent, true)
testBoth(parent, path, false)
if runtime.GOOS == "windows" {
test("C:/"+path, "D:/"+parent, false)
}
}
}
}

View File

@@ -78,14 +78,80 @@ func WindowsInvalidFilename(name string) bool {
return strings.ContainsAny(name, windowsDisallowedCharacters)
}
// IsParent compares paths purely lexicographically, meaning it returns false
// if path and parent aren't both absolute or relative.
func IsParent(path, parent string) bool {
if len(parent) == 0 {
if parent == path {
// Twice the same root on windows would not be caught at the end.
return false
}
if filepath.IsAbs(path) != filepath.IsAbs(parent) {
return false
}
if parent == "" || parent == "." {
// The empty string is the parent of everything except the empty
// string. (Avoids panic in the next step.)
return len(path) > 0
// string and ".". (Avoids panic in the last step.)
return path != "" && path != "."
}
if parent == "/" {
// The root is the parent of everything except itself, which would
// not be caught below.
return path != "/"
}
if parent[len(parent)-1] != PathSeparator {
parent += string(PathSeparator)
}
return strings.HasPrefix(path, parent)
}
func CommonPrefix(first, second string) string {
if filepath.IsAbs(first) != filepath.IsAbs(second) {
// Whatever
return ""
}
firstParts := strings.Split(filepath.Clean(first), string(PathSeparator))
secondParts := strings.Split(filepath.Clean(second), string(PathSeparator))
isAbs := filepath.IsAbs(first) && filepath.IsAbs(second)
count := len(firstParts)
if len(secondParts) < len(firstParts) {
count = len(secondParts)
}
common := make([]string, 0, count)
for i := 0; i < count; i++ {
if firstParts[i] != secondParts[i] {
break
}
common = append(common, firstParts[i])
}
if isAbs {
if runtime.GOOS == "windows" && isVolumeNameOnly(common) {
// Because strings.Split strips out path separators, if we're at the volume name, we end up without a separator
// Wedge an empty element to be joined with.
common = append(common, "")
} else if len(common) == 1 {
// If isAbs on non Windows, first element in both first and second is "", hence joining that returns nothing.
return string(PathSeparator)
}
}
// This should only be true on Windows when drive letters are different or when paths are relative.
// In case of UNC paths we should end up with more than a single element hence joining is fine
if len(common) == 0 {
return ""
}
// This has to be strings.Join, because filepath.Join([]string{"", "", "?", "C:", "Audrius"}...) returns garbage
result := strings.Join(common, string(PathSeparator))
return filepath.Clean(result)
}
func isVolumeNameOnly(parts []string) bool {
isNormalVolumeName := len(parts) == 1 && strings.HasSuffix(parts[0], ":")
isUNCVolumeName := len(parts) == 4 && strings.HasSuffix(parts[3], ":")
return isNormalVolumeName || isUNCVolumeName
}

46
lib/fs/util_test.go Normal file
View File

@@ -0,0 +1,46 @@
// Copyright (C) 2019 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 fs
import (
"runtime"
"testing"
)
func TestCommonPrefix(t *testing.T) {
test := func(first, second, expect string) {
t.Helper()
res := CommonPrefix(first, second)
if res != expect {
t.Errorf("Expected %s got %s", expect, res)
}
}
if runtime.GOOS == "windows" {
test(`c:\Audrius\Downloads`, `c:\Audrius\Docs`, `c:\Audrius`)
test(`c:\Audrius\Downloads`, `C:\Audrius\Docs`, ``) // Case differences :(
test(`C:\Audrius-a\Downloads`, `C:\Audrius-b\Docs`, `C:\`)
test(`\\?\C:\Audrius-a\Downloads`, `\\?\C:\Audrius-b\Docs`, `\\?\C:\`)
test(`\\?\C:\Audrius\Downloads`, `\\?\C:\Audrius\Docs`, `\\?\C:\Audrius`)
test(`Audrius-a\Downloads`, `Audrius-b\Docs`, ``)
test(`Audrius\Downloads`, `Audrius\Docs`, `Audrius`)
test(`c:\Audrius\Downloads`, `Audrius\Docs`, ``)
test(`c:\`, `c:\`, `c:\`)
test(`\\?\c:\`, `\\?\c:\`, `\\?\c:\`)
} else {
test(`/Audrius/Downloads`, `/Audrius/Docs`, `/Audrius`)
test(`/Audrius\Downloads`, `/Audrius\Docs`, `/`)
test(`/Audrius-a/Downloads`, `/Audrius-b/Docs`, `/`)
test(`Audrius\Downloads`, `Audrius\Docs`, ``) // Windows separators
test(`Audrius/Downloads`, `Audrius/Docs`, `Audrius`)
test(`Audrius-a\Downloads`, `Audrius-b\Docs`, ``)
test(`/Audrius/Downloads`, `Audrius/Docs`, ``)
test(`/`, `/`, `/`)
}
test(`Audrius`, `Audrius`, `Audrius`)
test(`.`, `.`, `.`)
}

41
lib/fs/walkfs_test.go Normal file
View File

@@ -0,0 +1,41 @@
// Copyright (C) 2019 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 fs
import (
"runtime"
"testing"
)
func testWalkSkipSymlink(t *testing.T, fsType FilesystemType, uri string) {
if runtime.GOOS == "windows" {
t.Skip("Symlinks on windows")
}
fs := NewFilesystem(fsType, uri)
if err := fs.MkdirAll("target/foo", 0755); err != nil {
t.Fatal(err)
}
if err := fs.Mkdir("towalk", 0755); err != nil {
t.Fatal(err)
}
if err := fs.CreateSymlink("target", "towalk/symlink"); err != nil {
t.Fatal(err)
}
if err := fs.Walk("towalk", func(path string, info FileInfo, err error) error {
if err != nil {
t.Fatal(err)
}
if info.Name() != "symlink" && info.Name() != "towalk" {
t.Fatal("Walk unexpected file", info.Name())
}
return nil
}); err != nil {
t.Fatal(err)
}
}

View File

@@ -1054,3 +1054,28 @@ func TestIssue5009(t *testing.T) {
t.Error("skipIgnoredDirs should not be true with includes")
}
}
func TestSpecialChars(t *testing.T) {
pats := New(fs.NewFilesystem(fs.FilesystemTypeBasic, "."), WithCache(true))
stignore := `(?i)/#recycle
(?i)/#nosync
(?i)/$Recycle.bin
(?i)/$RECYCLE.BIN
(?i)/System Volume Information`
if err := pats.Parse(bytes.NewBufferString(stignore), ".stignore"); err != nil {
t.Fatal(err)
}
cases := []string{
"#nosync",
"$RECYCLE.BIN",
filepath.FromSlash("$RECYCLE.BIN/S-1-5-18/desktop.ini"),
}
for _, c := range cases {
if !pats.Match(c).IsIgnored() {
t.Errorf("%q should be ignored", c)
}
}
}

246
lib/model/fakeconns_test.go Normal file
View File

@@ -0,0 +1,246 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package model
import (
"bytes"
"context"
"net"
"sync"
"time"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/scanner"
)
type downloadProgressMessage struct {
folder string
updates []protocol.FileDownloadProgressUpdate
}
type fakeConnection struct {
id protocol.DeviceID
downloadProgressMessages []downloadProgressMessage
closed bool
files []protocol.FileInfo
fileData map[string][]byte
folder string
model *model
indexFn func(string, []protocol.FileInfo)
requestFn func(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error)
closeFn func(error)
mut sync.Mutex
}
func (f *fakeConnection) Close(err error) {
f.mut.Lock()
defer f.mut.Unlock()
if f.closeFn != nil {
f.closeFn(err)
return
}
f.closed = true
f.model.Closed(f, err)
}
func (f *fakeConnection) Start() {
}
func (f *fakeConnection) ID() protocol.DeviceID {
return f.id
}
func (f *fakeConnection) Name() string {
return ""
}
func (f *fakeConnection) String() string {
return ""
}
func (f *fakeConnection) Option(string) string {
return ""
}
func (f *fakeConnection) Index(folder string, fs []protocol.FileInfo) error {
f.mut.Lock()
defer f.mut.Unlock()
if f.indexFn != nil {
f.indexFn(folder, fs)
}
return nil
}
func (f *fakeConnection) IndexUpdate(folder string, fs []protocol.FileInfo) error {
f.mut.Lock()
defer f.mut.Unlock()
if f.indexFn != nil {
f.indexFn(folder, fs)
}
return nil
}
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 {
return f.requestFn(folder, name, offset, size, hash, fromTemporary)
}
return f.fileData[name], nil
}
func (f *fakeConnection) ClusterConfig(protocol.ClusterConfig) {}
func (f *fakeConnection) Ping() bool {
f.mut.Lock()
defer f.mut.Unlock()
return f.closed
}
func (f *fakeConnection) Closed() bool {
f.mut.Lock()
defer f.mut.Unlock()
return f.closed
}
func (f *fakeConnection) Statistics() protocol.Statistics {
return protocol.Statistics{}
}
func (f *fakeConnection) RemoteAddr() net.Addr {
return &fakeAddr{}
}
func (f *fakeConnection) Type() string {
return "fake"
}
func (f *fakeConnection) Crypto() string {
return "fake"
}
func (f *fakeConnection) Transport() string {
return "fake"
}
func (f *fakeConnection) Priority() int {
return 9000
}
func (f *fakeConnection) DownloadProgress(folder string, updates []protocol.FileDownloadProgressUpdate) {
f.downloadProgressMessages = append(f.downloadProgressMessages, downloadProgressMessage{
folder: folder,
updates: updates,
})
}
func (f *fakeConnection) addFileLocked(name string, flags uint32, ftype protocol.FileInfoType, data []byte, version protocol.Vector) {
blockSize := protocol.BlockSize(int64(len(data)))
blocks, _ := scanner.Blocks(context.TODO(), bytes.NewReader(data), blockSize, int64(len(data)), nil, true)
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(),
RawBlockSize: int32(blockSize),
Blocks: blocks,
})
} else {
// Symlink
f.files = append(f.files, protocol.FileInfo{
Name: name,
Type: ftype,
Version: version,
Sequence: time.Now().UnixNano(),
SymlinkTarget: string(data),
NoPermissions: true,
})
}
if f.fileData == nil {
f.fileData = make(map[string][]byte)
}
f.fileData[name] = data
}
func (f *fakeConnection) addFile(name string, flags uint32, ftype protocol.FileInfoType, data []byte) {
f.mut.Lock()
defer f.mut.Unlock()
var version protocol.Vector
version = version.Update(f.id.Short())
f.addFileLocked(name, flags, ftype, data, version)
}
func (f *fakeConnection) updateFile(name string, flags uint32, ftype protocol.FileInfoType, data []byte) {
f.mut.Lock()
defer f.mut.Unlock()
for i, fi := range f.files {
if fi.Name == name {
f.files = append(f.files[:i], f.files[i+1:]...)
f.addFileLocked(name, flags, ftype, data, fi.Version.Update(f.id.Short()))
return
}
}
}
func (f *fakeConnection) deleteFile(name string) {
f.mut.Lock()
defer f.mut.Unlock()
for i, fi := range f.files {
if fi.Name == name {
fi.Deleted = true
fi.ModifiedS = time.Now().Unix()
fi.Version = fi.Version.Update(f.id.Short())
fi.Sequence = time.Now().UnixNano()
fi.Blocks = nil
f.files = append(append(f.files[:i], f.files[i+1:]...), fi)
return
}
}
}
func (f *fakeConnection) sendIndexUpdate() {
f.model.IndexUpdate(f.id, f.folder, f.files)
}
func addFakeConn(m *model, dev protocol.DeviceID) *fakeConnection {
fc := &fakeConnection{id: dev, model: m}
m.AddConnection(fc, protocol.HelloResult{})
m.ClusterConfig(dev, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: "default",
Devices: []protocol.Device{
{ID: myID},
{ID: device1},
},
},
},
})
return fc
}
type fakeAddr struct{}
func (fakeAddr) Network() string {
return "network"
}
func (fakeAddr) String() string {
return "address"
}

View File

@@ -132,6 +132,23 @@ func (f *folder) Serve() {
initialCompleted := f.initialScanFinished
pull := func() {
startTime := time.Now()
if f.puller.pull() {
// We're good. Don't schedule another pull and reset
// the pause interval.
pause = f.basePause()
return
}
// Pulling failed, try again later.
delay := pause + time.Since(startTime)
l.Infof("Folder %v isn't making sync progress - retrying in %v.", f.Description(), delay)
pullFailTimer.Reset(delay)
if pause < 60*f.basePause() {
pause *= 2
}
}
for {
select {
case <-f.ctx.Done():
@@ -143,27 +160,10 @@ func (f *folder) Serve() {
case <-pullFailTimer.C:
default:
}
if !f.puller.pull() {
// Pulling failed, try again later.
pullFailTimer.Reset(pause)
}
pull()
case <-pullFailTimer.C:
if f.puller.pull() {
// We're good. Don't schedule another fail pull and reset
// the pause interval.
pause = f.basePause()
continue
}
// Pulling failed, try again later.
l.Infof("Folder %v isn't making sync progress - retrying in %v.", f.Description(), pause)
pullFailTimer.Reset(pause)
// Back off from retrying to pull with an upper limit.
if pause < 60*f.basePause() {
pause *= 2
}
pull()
case <-initialCompleted:
// Initial scan has completed, we should do a pull

View File

@@ -32,6 +32,7 @@ func TestRecvOnlyRevertDeletes(t *testing.T) {
ffs := f.Filesystem()
defer os.Remove(m.cfg.ConfigPath())
defer os.Remove(ffs.URI())
defer m.db.Close()
defer m.Stop()
// Create some test data
@@ -115,6 +116,7 @@ func TestRecvOnlyRevertNeeds(t *testing.T) {
ffs := f.Filesystem()
defer os.Remove(m.cfg.ConfigPath())
defer os.Remove(ffs.URI())
defer m.db.Close()
defer m.Stop()
// Create some test data
@@ -208,6 +210,7 @@ func TestRecvOnlyUndoChanges(t *testing.T) {
ffs := f.Filesystem()
defer os.Remove(m.cfg.ConfigPath())
defer os.Remove(ffs.URI())
defer m.db.Close()
defer m.Stop()
// Create some test data

View File

@@ -310,6 +310,7 @@ func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyC
}
if f.IgnoreDelete && intf.IsDeleted() {
f.resetPullError(intf.FileName())
l.Debugln(f, "ignore file deletion (config)", intf.FileName())
return true
}
@@ -318,6 +319,7 @@ func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyC
switch {
case f.ignores.ShouldIgnore(file.Name):
f.resetPullError(file.Name)
file.SetIgnored(f.shortID)
l.Debugln(f, "Handling ignored file", file)
dbUpdateChan <- dbUpdateJob{file, dbUpdateInvalidate}
@@ -352,6 +354,7 @@ func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyC
// We are supposed to copy the entire file, and then fetch nothing. We
// are only updating metadata, so we don't actually *need* to make the
// copy.
f.resetPullError(file.Name)
f.shortcutFile(file, curFile, dbUpdateChan)
} else {
// Queue files for processing after directories and symlinks.
@@ -359,6 +362,7 @@ func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyC
}
case runtime.GOOS == "windows" && file.IsSymlink():
f.resetPullError(file.Name)
file.SetUnsupported(f.shortID)
l.Debugln(f, "Invalidating symlink (unsupported)", file.Name)
dbUpdateChan <- dbUpdateJob{file, dbUpdateInvalidate}
@@ -367,6 +371,7 @@ func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyC
case file.IsDirectory() && !file.IsSymlink():
changed++
l.Debugln(f, "Handling directory", file.Name)
f.resetPullError(file.Name)
if f.checkParent(file.Name, scanChan) {
f.handleDir(file, dbUpdateChan, scanChan)
}
@@ -374,6 +379,7 @@ func (f *sendReceiveFolder) processNeeded(dbUpdateChan chan<- dbUpdateJob, copyC
case file.IsSymlink():
changed++
l.Debugln(f, "Handling symlink", file.Name)
f.resetPullError(file.Name)
if f.checkParent(file.Name, scanChan) {
f.handleSymlink(file, dbUpdateChan, scanChan)
}
@@ -424,6 +430,8 @@ nextFile:
break
}
f.resetPullError(fileName)
fi, ok := f.fset.GetGlobal(fileName)
if !ok {
// File is no longer in the index. Mark it as done and drop it.
@@ -497,6 +505,7 @@ func (f *sendReceiveFolder) processDeletions(fileDeletions map[string]protocol.F
}
l.Debugln(f, "Deleting file", file.Name)
f.resetPullError(file.Name)
if update, err := f.deleteFile(file, scanChan); err != nil {
f.newPullError(file.Name, errors.Wrap(err, "delete file"))
} else {
@@ -504,6 +513,7 @@ func (f *sendReceiveFolder) processDeletions(fileDeletions map[string]protocol.F
}
}
// Process in reverse order to delete depth first
for i := range dirDeletions {
select {
case <-f.ctx.Done():
@@ -512,6 +522,7 @@ func (f *sendReceiveFolder) processDeletions(fileDeletions map[string]protocol.F
}
dir := dirDeletions[len(dirDeletions)-i-1]
f.resetPullError(dir.Name)
l.Debugln(f, "Deleting dir", dir.Name)
f.deleteDir(dir, dbUpdateChan, scanChan)
}
@@ -581,7 +592,7 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<
return f.moveForConflict(name, file.ModifiedBy.String(), scanChan)
}, f.fs, curFile.Name)
} else {
err = f.deleteItemOnDisk(file, scanChan)
err = f.deleteItemOnDisk(curFile, scanChan)
}
if err != nil {
f.newPullError(file.Name, err)
@@ -737,7 +748,7 @@ func (f *sendReceiveFolder) handleSymlink(file protocol.FileInfo, dbUpdateChan c
return f.moveForConflict(name, file.ModifiedBy.String(), scanChan)
}, f.fs, curFile.Name)
} else {
err = f.deleteItemOnDisk(file, scanChan)
err = f.deleteItemOnDisk(curFile, scanChan)
}
if err != nil {
f.newPullError(file.Name, errors.Wrap(err, "symlink remove"))
@@ -941,13 +952,13 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, db
if f.versioner != nil {
err = f.CheckAvailableSpace(source.Size)
if err == nil {
err = osutil.Copy(f.fs, source.Name, tempName)
err = osutil.Copy(f.fs, f.fs, source.Name, tempName)
if err == nil {
err = osutil.InWritableDir(f.versioner.Archive, f.fs, source.Name)
}
}
} else {
err = osutil.TryRename(f.fs, source.Name, tempName)
err = osutil.RenameOrCopy(f.fs, f.fs, source.Name, tempName)
}
if err != nil {
return err
@@ -1201,9 +1212,7 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
continue
}
if f.model.progressEmitter != nil {
f.model.progressEmitter.Register(state.sharedPullerState)
}
f.model.progressEmitter.Register(state.sharedPullerState)
folderFilesystems := make(map[string]fs.Filesystem)
var folders []string
@@ -1512,7 +1521,7 @@ func (f *sendReceiveFolder) performFinish(file, curFile protocol.FileInfo, hasCu
// Replace the original content with the new one. If it didn't work,
// leave the temp file in place for reuse.
if err := osutil.TryRename(f.fs, tempName, file.Name); err != nil {
if err := osutil.RenameOrCopy(f.fs, f.fs, tempName, file.Name); err != nil {
return err
}
@@ -1550,9 +1559,7 @@ func (f *sendReceiveFolder) finisherRoutine(in <-chan *sharedPullerState, dbUpda
blockStatsMut.Unlock()
}
if f.model.progressEmitter != nil {
f.model.progressEmitter.Deregister(state)
}
f.model.progressEmitter.Deregister(state)
events.Default.Log(events.ItemFinished, map[string]interface{}{
"folder": f.folderID,
@@ -1751,7 +1758,7 @@ func (f *sendReceiveFolder) newPullError(path string, err error) {
return
}
l.Infof("Puller (folder %s, file %q): %v", f.Description(), path, err)
l.Infof("Puller (folder %s, item %q): %v", f.Description(), path, err)
// Establish context to differentiate from errors while scanning.
// Use "syncing" as opposed to "pulling" as the latter might be used
@@ -1759,6 +1766,14 @@ func (f *sendReceiveFolder) newPullError(path string, err error) {
f.pullErrors[path] = fmt.Sprintln("syncing:", err)
}
// resetPullError removes the error at path in case there was an error on a
// previous pull iteration.
func (f *sendReceiveFolder) resetPullError(path string) {
f.pullErrorsMut.Lock()
delete(f.pullErrors, path)
f.pullErrorsMut.Unlock()
}
func (f *sendReceiveFolder) clearPullErrors() {
f.pullErrorsMut.Lock()
f.pullErrors = make(map[string]string)

View File

@@ -218,9 +218,7 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
fmut: sync.NewRWMutex(),
pmut: sync.NewRWMutex(),
}
if cfg.Options().ProgressUpdateIntervalS > -1 {
m.Add(m.progressEmitter)
}
m.Add(m.progressEmitter)
scanLimiter.setCapacity(cfg.Options().MaxConcurrentScans)
cfg.Subscribe(m)
@@ -240,29 +238,28 @@ func (m *model) StartDeadlockDetector(timeout time.Duration) {
// StartFolder constructs the folder service and starts it.
func (m *model) StartFolder(folder string) {
m.fmut.Lock()
m.pmut.Lock()
folderType := m.startFolderLocked(folder)
defer m.fmut.Unlock()
folderCfg := m.folderCfgs[folder]
m.pmut.Unlock()
m.fmut.Unlock()
m.startFolderLocked(folderCfg)
l.Infof("Ready to synchronize %s (%s)", folderCfg.Description(), folderType)
l.Infof("Ready to synchronize %s (%s)", folderCfg.Description(), folderCfg.Type)
}
func (m *model) startFolderLocked(folder string) config.FolderType {
if err := m.checkFolderRunningLocked(folder); err == errFolderMissing {
panic("cannot start nonexistent folder " + folder)
// Need to hold lock on m.fmut when calling this.
func (m *model) startFolderLocked(cfg config.FolderConfiguration) {
if err := m.checkFolderRunningLocked(cfg.ID); err == errFolderMissing {
panic("cannot start nonexistent folder " + cfg.Description())
} else if err == nil {
panic("cannot start already running folder " + folder)
panic("cannot start already running folder " + cfg.Description())
}
cfg := m.folderCfgs[folder]
folderFactory, ok := folderFactories[cfg.Type]
if !ok {
panic(fmt.Sprintf("unknown folder type 0x%x", cfg.Type))
}
folder := cfg.ID
fset := m.folderFiles[folder]
// Find any devices for which we hold the index in the db, but the folder
@@ -276,9 +273,9 @@ func (m *model) startFolderLocked(folder string) config.FolderType {
}
// Close connections to affected devices
for _, id := range cfg.DeviceIDs() {
m.closeLocked(id, fmt.Errorf("started folder %v", cfg.Description()))
}
m.fmut.Unlock()
m.closeConns(cfg.DeviceIDs(), fmt.Errorf("started folder %v", cfg.Description()))
m.fmut.Lock()
v, ok := fset.Sequence(protocol.LocalDeviceID), true
indexHasFiles := ok && v > 0
@@ -320,8 +317,6 @@ func (m *model) startFolderLocked(folder string) config.FolderType {
token := m.Add(p)
m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
return cfg.Type
}
func (m *model) warnAboutOverwritingProtectedFiles(folder string) {
@@ -368,8 +363,8 @@ func (m *model) AddFolder(cfg config.FolderConfiguration) {
}
m.fmut.Lock()
defer m.fmut.Unlock()
m.addFolderLocked(cfg)
m.fmut.Unlock()
}
func (m *model) addFolderLocked(cfg config.FolderConfiguration) {
@@ -386,36 +381,34 @@ func (m *model) addFolderLocked(cfg config.FolderConfiguration) {
func (m *model) RemoveFolder(cfg config.FolderConfiguration) {
m.fmut.Lock()
m.pmut.Lock()
defer m.fmut.Unlock()
// Delete syncthing specific files
cfg.Filesystem().RemoveAll(config.DefaultMarkerName)
m.tearDownFolderLocked(cfg, fmt.Errorf("removing folder %v", cfg.Description()))
// Remove it from the database
db.DropFolder(m.db, cfg.ID)
m.pmut.Unlock()
m.fmut.Unlock()
}
// Need to hold lock on m.fmut when calling this.
func (m *model) tearDownFolderLocked(cfg config.FolderConfiguration, err error) {
// Close connections to affected devices
// Must happen before stopping the folder service to abort ongoing
// transmissions and thus allow timely service termination.
for _, dev := range cfg.Devices {
m.closeLocked(dev.DeviceID, err)
}
// Stop the services running for this folder and wait for them to finish
// stopping to prevent races on restart.
tokens := m.folderRunnerTokens[cfg.ID]
m.pmut.Unlock()
m.fmut.Unlock()
// Close connections to affected devices
// Must happen before stopping the folder service to abort ongoing
// transmissions and thus allow timely service termination.
m.closeConns(cfg.DeviceIDs(), err)
for _, id := range tokens {
m.RemoveAndWait(id, 0)
}
m.fmut.Lock()
m.pmut.Lock()
// Clean up our config maps
delete(m.folderCfgs, cfg.ID)
@@ -443,11 +436,6 @@ func (m *model) RestartFolder(from, to config.FolderConfiguration) {
restartMut.Lock()
defer restartMut.Unlock()
m.fmut.Lock()
m.pmut.Lock()
defer m.fmut.Unlock()
defer m.pmut.Unlock()
var infoMsg string
var errMsg string
switch {
@@ -462,10 +450,13 @@ func (m *model) RestartFolder(from, to config.FolderConfiguration) {
errMsg = "restarting"
}
m.fmut.Lock()
defer m.fmut.Unlock()
m.tearDownFolderLocked(from, fmt.Errorf("%v folder %v", errMsg, to.Description()))
if !to.Paused {
m.addFolderLocked(to)
m.startFolderLocked(to.ID)
m.startFolderLocked(to)
}
l.Infof("%v folder %v (%v)", infoMsg, to.Description(), to.Type)
}
@@ -486,12 +477,12 @@ func (m *model) UsageReportingStats(version int, preview bool) map[string]interf
stats["blockStats"] = copyBlockStats
// Transport stats
m.pmut.Lock()
m.pmut.RLock()
transportStats := make(map[string]int)
for _, conn := range m.conn {
transportStats[conn.Transport()]++
}
m.pmut.Unlock()
m.pmut.RUnlock()
stats["transportStats"] = transportStats
// Ignore stats
@@ -594,6 +585,8 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
func (m *model) ConnectionStats() map[string]interface{} {
m.fmut.RLock()
m.pmut.RLock()
defer m.pmut.RUnlock()
defer m.fmut.RUnlock()
res := make(map[string]interface{})
devs := m.cfg.Devices()
@@ -623,9 +616,6 @@ func (m *model) ConnectionStats() map[string]interface{} {
res["connections"] = conns
m.pmut.RUnlock()
m.fmut.RUnlock()
in, out := protocol.TotalInOut()
res["total"] = ConnectionInfo{
Statistics: protocol.Statistics{
@@ -796,11 +786,12 @@ func (m *model) ReceiveOnlyChangedSize(folder string) db.Counts {
// NeedSize returns the number and total size of currently needed files.
func (m *model) NeedSize(folder string) db.Counts {
m.fmut.RLock()
defer m.fmut.RUnlock()
rf, ok := m.folderFiles[folder]
cfg := m.folderCfgs[folder]
m.fmut.RUnlock()
var result db.Counts
if rf, ok := m.folderFiles[folder]; ok {
cfg := m.folderCfgs[folder]
if ok {
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
if cfg.IgnoreDelete && f.IsDeleted() {
return true
@@ -820,10 +811,12 @@ func (m *model) NeedSize(folder string) db.Counts {
// total number of files currently needed.
func (m *model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated) {
m.fmut.RLock()
defer m.fmut.RUnlock()
rf, rfOk := m.folderFiles[folder]
runner, runnerOk := m.folderRunners[folder]
cfg := m.folderCfgs[folder]
m.fmut.RUnlock()
rf, ok := m.folderFiles[folder]
if !ok {
if !rfOk {
return nil, nil, nil
}
@@ -833,8 +826,7 @@ func (m *model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
skip := (page - 1) * perpage
get := perpage
runner, ok := m.folderRunners[folder]
if ok {
if runnerOk {
allProgressNames, allQueuedNames := runner.Jobs()
var progressNames, queuedNames []string
@@ -861,7 +853,6 @@ func (m *model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
}
rest = make([]db.FileInfoTruncated, 0, perpage)
cfg := m.folderCfgs[folder]
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
if cfg.IgnoreDelete && f.IsDeleted() {
return true
@@ -887,13 +878,13 @@ func (m *model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
// total number of files currently needed.
func (m *model) LocalChangedFiles(folder string, page, perpage int) []db.FileInfoTruncated {
m.fmut.RLock()
defer m.fmut.RUnlock()
rf, ok := m.folderFiles[folder]
fcfg := m.folderCfgs[folder]
m.fmut.RUnlock()
if !ok {
return nil
}
fcfg := m.folderCfgs[folder]
if fcfg.Type != config.FolderTypeReceiveOnly {
return nil
}
@@ -929,14 +920,13 @@ func (m *model) LocalChangedFiles(folder string, page, perpage int) []db.FileInf
func (m *model) RemoteNeedFolderFiles(device protocol.DeviceID, folder string, page, perpage int) ([]db.FileInfoTruncated, error) {
m.fmut.RLock()
m.pmut.RLock()
if err := m.checkDeviceFolderConnectedLocked(device, folder); err != nil {
m.pmut.RUnlock()
m.fmut.RUnlock()
return nil, err
}
err := m.checkDeviceFolderConnectedLocked(device, folder)
rf := m.folderFiles[folder]
m.pmut.RUnlock()
m.fmut.RUnlock()
if err != nil {
return nil, err
}
files := make([]db.FileInfoTruncated, 0, perpage)
skip := (page - 1) * perpage
@@ -1056,6 +1046,7 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
}
m.fmut.Lock()
defer m.fmut.Unlock()
var paused []string
for _, folder := range cm.Folders {
cfg, ok := m.cfg.Folder(folder.ID)
@@ -1127,7 +1118,7 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
l.Infof("Device %v folder %s has mismatching index ID for us (%v != %v)", deviceID, folder.Description(), dev.IndexID, myIndexID)
startSequence = 0
}
} else if dev.ID == deviceID && dev.IndexID != 0 {
} else if dev.ID == deviceID {
// This is the other side's description of themselves. We
// check to see that it matches the IndexID we have on file,
// otherwise we drop our old index data and expect to get a
@@ -1191,7 +1182,6 @@ func (m *model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
changed = true
}
}
m.fmut.Unlock()
if changed {
if err := m.cfg.Save(); err != nil {
@@ -1393,10 +1383,12 @@ func (m *model) Closed(conn protocol.Connection, err error) {
device := conn.ID()
m.pmut.Lock()
defer m.pmut.Unlock()
conn, ok := m.conn[device]
if ok {
m.progressEmitter.temporaryIndexUnsubscribe(conn)
if !ok {
return
}
m.progressEmitter.temporaryIndexUnsubscribe(conn)
delete(m.conn, device)
delete(m.connRequestLimiters, device)
delete(m.helloMessages, device)
@@ -1404,7 +1396,6 @@ func (m *model) Closed(conn protocol.Connection, err error) {
delete(m.remotePausedFolders, device)
closed := m.closed[device]
delete(m.closed, device)
m.pmut.Unlock()
l.Infof("Connection to %s at %s closed: %v", device, conn.Name(), err)
events.Default.Log(events.DeviceDisconnected, map[string]string{
@@ -1414,22 +1405,23 @@ func (m *model) Closed(conn protocol.Connection, err error) {
close(closed)
}
// close will close the underlying connection for a given device
func (m *model) close(device protocol.DeviceID, err error) {
// closeConns will close the underlying connection for given devices
func (m *model) closeConns(devs []protocol.DeviceID, err error) {
conns := make([]connections.Connection, 0, len(devs))
m.pmut.Lock()
m.closeLocked(device, err)
for _, dev := range devs {
if conn, ok := m.conn[dev]; ok {
conns = append(conns, conn)
}
}
m.pmut.Unlock()
for _, conn := range conns {
conn.Close(err)
}
}
// closeLocked will close the underlying connection for a given device
func (m *model) closeLocked(device protocol.DeviceID, err error) {
conn, ok := m.conn[device]
if !ok {
// There is no connection to close
return
}
conn.Close(err)
func (m *model) closeConn(dev protocol.DeviceID, err error) {
m.closeConns([]protocol.DeviceID{dev}, err)
}
// Implements protocol.RequestResponse
@@ -1476,22 +1468,22 @@ func (m *model) Request(deviceID protocol.DeviceID, folder, name string, size in
// The folder might be already unpaused in the config, but not yet
// in the model.
l.Debugf("Request from %s for file %s in unstarted folder %q", deviceID, name, folder)
return nil, protocol.ErrInvalid
return nil, protocol.ErrGeneric
}
if !folderCfg.SharedWith(deviceID) {
l.Warnf("Request from %s for file %s in unshared folder %q", deviceID, name, folder)
return nil, protocol.ErrNoSuchFile
return nil, protocol.ErrGeneric
}
if folderCfg.Paused {
l.Debugf("Request from %s for file %s in paused folder %q", deviceID, name, folder)
return nil, protocol.ErrInvalid
return nil, protocol.ErrGeneric
}
// Make sure the path is valid and in canonical form
if name, err = fs.Canonicalize(name); err != nil {
l.Debugf("Request from %s in folder %q for invalid filename %s", deviceID, folder, name)
return nil, protocol.ErrInvalid
return nil, protocol.ErrGeneric
}
if deviceID != protocol.LocalDeviceID {
@@ -1500,12 +1492,12 @@ func (m *model) Request(deviceID protocol.DeviceID, folder, name string, size in
if fs.IsInternal(name) {
l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
return nil, protocol.ErrNoSuchFile
return nil, protocol.ErrInvalid
}
if folderIgnores.Match(name).IsIgnored() {
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
return nil, protocol.ErrNoSuchFile
return nil, protocol.ErrInvalid
}
folderFs := folderCfg.Filesystem()
@@ -1576,7 +1568,7 @@ func (m *model) Request(deviceID protocol.DeviceID, folder, name string, size in
}
if !scanner.Validate(res.data, hash, weakHash) {
m.recheckFile(deviceID, folderFs, folder, name, int(offset)/int(size), hash)
m.recheckFile(deviceID, folderFs, folder, name, size, offset, hash)
l.Debugf("%v REQ(in) failed validating data (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
return nil, protocol.ErrNoSuchFile
}
@@ -1584,7 +1576,7 @@ func (m *model) Request(deviceID protocol.DeviceID, folder, name string, size in
return res, nil
}
func (m *model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem, folder, name string, blockIndex int, hash []byte) {
func (m *model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem, folder, name string, size int32, offset int64, hash []byte) {
cf, ok := m.CurrentFolderFile(folder, name)
if !ok {
l.Debugf("%v recheckFile: %s: %q / %q: no current file", m, deviceID, folder, name)
@@ -1596,6 +1588,7 @@ func (m *model) recheckFile(deviceID protocol.DeviceID, folderFs fs.Filesystem,
return
}
blockIndex := int(offset) / cf.BlockSize()
if blockIndex >= len(cf.Blocks) {
l.Debugf("%v recheckFile: %s: %q / %q i=%d: block index too far", m, deviceID, folder, name, blockIndex)
return
@@ -1660,12 +1653,13 @@ func (m *model) Connection(deviceID protocol.DeviceID) (connections.Connection,
func (m *model) GetIgnores(folder string) ([]string, []string, error) {
m.fmut.RLock()
defer m.fmut.RUnlock()
cfg, cfgOk := m.folderCfgs[folder]
ignores, ignoresOk := m.folderIgnores[folder]
m.fmut.RUnlock()
cfg, ok := m.folderCfgs[folder]
if !ok {
cfg, ok = m.cfg.Folders()[folder]
if !ok {
if !cfgOk {
cfg, cfgOk = m.cfg.Folders()[folder]
if !cfgOk {
return nil, nil, fmt.Errorf("Folder %s does not exist", folder)
}
}
@@ -1675,8 +1669,7 @@ func (m *model) GetIgnores(folder string) ([]string, []string, error) {
return nil, nil, err
}
ignores, ok := m.folderIgnores[folder]
if !ok {
if !ignoresOk {
ignores = ignore.New(fs.NewFilesystem(cfg.FilesystemType, cfg.Path))
}
@@ -2039,13 +2032,14 @@ func (m *model) ScanFolder(folder string) error {
func (m *model) ScanFolderSubdirs(folder string, subs []string) error {
m.fmut.RLock()
if err := m.checkFolderRunningLocked(folder); err != nil {
m.fmut.RUnlock()
return err
}
err := m.checkFolderRunningLocked(folder)
runner := m.folderRunners[folder]
m.fmut.RUnlock()
if err != nil {
return err
}
return runner.Scan(subs)
}
@@ -2230,10 +2224,10 @@ func (m *model) CurrentSequence(folder string) (int64, bool) {
// the remote or global folder has changed.
func (m *model) RemoteSequence(folder string) (int64, bool) {
m.fmut.RLock()
defer m.fmut.RUnlock()
fs, ok := m.folderFiles[folder]
cfg := m.folderCfgs[folder]
m.fmut.RUnlock()
if !ok {
// The folder might not exist, since this can be called with a user
// specified folder name from the REST interface.
@@ -2318,58 +2312,12 @@ func (m *model) GetFolderVersions(folder string) (map[string][]versioner.FileVer
return nil, errFolderMissing
}
files := make(map[string][]versioner.FileVersion)
filesystem := fcfg.Filesystem()
err := filesystem.Walk(".stversions", func(path string, f fs.FileInfo, err error) error {
// Skip root (which is ok to be a symlink)
if path == ".stversions" {
return nil
}
// Skip walking if we cannot walk...
if err != nil {
return err
}
// Ignore symlinks
if f.IsSymlink() {
return fs.SkipDir
}
// No records for directories
if f.IsDir() {
return nil
}
// Strip .stversions prefix.
path = strings.TrimPrefix(path, ".stversions"+string(fs.PathSeparator))
name, tag := versioner.UntagFilename(path)
// Something invalid
if name == "" || tag == "" {
return nil
}
name = osutil.NormalizedFilename(name)
versionTime, err := time.ParseInLocation(versioner.TimeFormat, tag, locationLocal)
if err != nil {
return nil
}
files[name] = append(files[name], versioner.FileVersion{
VersionTime: versionTime.Truncate(time.Second),
ModTime: f.ModTime().Truncate(time.Second),
Size: f.Size(),
})
return nil
})
if err != nil {
return nil, err
ver := fcfg.Versioner()
if ver == nil {
return nil, errors.New("no versioner configured")
}
return files, nil
return ver.GetVersions()
}
func (m *model) RestoreFolderVersions(folder string, versions map[string]time.Time) (map[string]string, error) {
@@ -2378,69 +2326,22 @@ func (m *model) RestoreFolderVersions(folder string, versions map[string]time.Ti
return nil, errFolderMissing
}
filesystem := fcfg.Filesystem()
ver := fcfg.Versioner()
restore := make(map[string]string)
errors := make(map[string]string)
restoreErrors := make(map[string]string)
// Validation
for file, version := range versions {
file = osutil.NativeFilename(file)
tag := version.In(locationLocal).Truncate(time.Second).Format(versioner.TimeFormat)
versionedTaggedFilename := filepath.Join(".stversions", versioner.TagFilename(file, tag))
// Check that the thing we've been asked to restore is actually a file
// and that it exists.
if info, err := filesystem.Lstat(versionedTaggedFilename); err != nil {
errors[file] = err.Error()
continue
} else if !info.IsRegular() {
errors[file] = "not a file"
continue
}
// Check that the target location of where we are supposed to restore
// either does not exist, or is actually a file.
if info, err := filesystem.Lstat(file); err == nil && !info.IsRegular() {
errors[file] = "cannot replace a non-file"
continue
} else if err != nil && !fs.IsNotExist(err) {
errors[file] = err.Error()
continue
}
restore[file] = versionedTaggedFilename
}
// Execution
var err error
for target, source := range restore {
err = nil
if _, serr := filesystem.Lstat(target); serr == nil {
if ver != nil {
err = osutil.InWritableDir(ver.Archive, filesystem, target)
} else {
err = osutil.InWritableDir(filesystem.Remove, filesystem, target)
}
}
filesystem.MkdirAll(filepath.Dir(target), 0755)
if err == nil {
err = osutil.Copy(filesystem, source, target)
}
if err != nil {
errors[target] = err.Error()
continue
if err := ver.Restore(file, version); err != nil {
restoreErrors[file] = err.Error()
}
}
// Trigger scan
if !fcfg.FSWatcherEnabled {
m.ScanFolder(folder)
go func() { _ = m.ScanFolder(folder) }()
}
return errors, nil
return restoreErrors, nil
}
func (m *model) Availability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability {
@@ -2485,10 +2386,10 @@ next:
// BringToFront bumps the given files priority in the job queue.
func (m *model) BringToFront(folder, file string) {
m.pmut.RLock()
defer m.pmut.RUnlock()
m.fmut.RLock()
runner, ok := m.folderRunners[folder]
m.fmut.RUnlock()
if ok {
runner.BringToFront(file)
}
@@ -2573,12 +2474,12 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
// Ignored folder was removed, reconnect to retrigger the prompt.
if len(fromCfg.IgnoredFolders) > len(toCfg.IgnoredFolders) {
m.close(deviceID, errIgnoredFolderRemoved)
m.closeConn(deviceID, errIgnoredFolderRemoved)
}
if toCfg.Paused {
l.Infoln("Pausing", deviceID)
m.close(deviceID, errDevicePaused)
m.closeConn(deviceID, errDevicePaused)
events.Default.Log(events.DevicePaused, map[string]string{"device": deviceID.String()})
} else {
events.Default.Log(events.DeviceResumed, map[string]string{"device": deviceID.String()})

View File

@@ -8,13 +8,11 @@ package model
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
"os"
"path/filepath"
"runtime"
@@ -33,55 +31,9 @@ import (
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
srand "github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/scanner"
"github.com/syncthing/syncthing/lib/versioner"
)
var myID, device1, device2 protocol.DeviceID
var defaultCfgWrapper config.Wrapper
var defaultFolderConfig config.FolderConfiguration
var defaultFs fs.Filesystem
var defaultCfg config.Configuration
var defaultAutoAcceptCfg config.Configuration
func init() {
myID, _ = protocol.DeviceIDFromString("ZNWFSWE-RWRV2BD-45BLMCV-LTDE2UR-4LJDW6J-R5BPWEB-TXD27XJ-IZF5RA4")
device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
defaultFs = fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata")
defaultFolderConfig = testFolderConfig("testdata")
defaultCfgWrapper = createTmpWrapper(config.New(myID))
defaultCfgWrapper.SetDevice(config.NewDeviceConfiguration(device1, "device1"))
defaultCfgWrapper.SetFolder(defaultFolderConfig)
opts := defaultCfgWrapper.Options()
opts.KeepTemporariesH = 1
defaultCfgWrapper.SetOptions(opts)
defaultCfg = defaultCfgWrapper.RawCopy()
defaultAutoAcceptCfg = config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: myID, // self
},
{
DeviceID: device1,
AutoAcceptFolders: true,
},
{
DeviceID: device2,
AutoAcceptFolders: true,
},
},
Options: config.OptionsConfiguration{
DefaultFolderPath: ".",
},
}
}
var testDataExpected = map[string]protocol.FileInfo{
"foo": {
Name: "foo",
@@ -171,31 +123,18 @@ func newState(cfg config.Configuration) (config.Wrapper, *model) {
m := setupModel(wcfg)
for _, dev := range cfg.Devices {
m.AddConnection(&fakeConnection{id: dev.DeviceID}, protocol.HelloResult{})
m.AddConnection(&fakeConnection{id: dev.DeviceID, model: m}, protocol.HelloResult{})
}
return wcfg, m
}
func setupModel(w config.Wrapper) *model {
db := db.OpenMemory()
m := newModel(w, myID, "syncthing", "dev", db, nil)
m.ServeBackground()
for id, cfg := range w.Folders() {
if !cfg.Paused {
m.AddFolder(cfg)
m.StartFolder(id)
}
}
m.ScanFolders()
return m
}
func TestRequest(t *testing.T) {
m := setupModel(defaultCfgWrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
// Existing, shared file
res, err := m.Request(device1, "default", "foo", 6, 0, nil, 0, false)
@@ -264,7 +203,10 @@ func BenchmarkIndex_100(b *testing.B) {
func benchmarkIndex(b *testing.B, nfiles int) {
m := setupModel(defaultCfgWrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
files := genFiles(nfiles)
m.Index(device1, "default", files)
@@ -290,7 +232,10 @@ func BenchmarkIndexUpdate_10000_1(b *testing.B) {
func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
m := setupModel(defaultCfgWrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
files := genFiles(nfiles)
ufiles := genFiles(nufiles)
@@ -304,206 +249,17 @@ func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
b.ReportAllocs()
}
type downloadProgressMessage struct {
folder string
updates []protocol.FileDownloadProgressUpdate
}
type fakeConnection struct {
id protocol.DeviceID
downloadProgressMessages []downloadProgressMessage
closed bool
files []protocol.FileInfo
fileData map[string][]byte
folder string
model *model
indexFn func(string, []protocol.FileInfo)
requestFn func(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error)
mut sync.Mutex
}
func (f *fakeConnection) Close(_ error) {
f.mut.Lock()
defer f.mut.Unlock()
f.closed = true
}
func (f *fakeConnection) Start() {
}
func (f *fakeConnection) ID() protocol.DeviceID {
return f.id
}
func (f *fakeConnection) Name() string {
return ""
}
func (f *fakeConnection) String() string {
return ""
}
func (f *fakeConnection) Option(string) string {
return ""
}
func (f *fakeConnection) Index(folder string, fs []protocol.FileInfo) error {
f.mut.Lock()
defer f.mut.Unlock()
if f.indexFn != nil {
f.indexFn(folder, fs)
}
return nil
}
func (f *fakeConnection) IndexUpdate(folder string, fs []protocol.FileInfo) error {
f.mut.Lock()
defer f.mut.Unlock()
if f.indexFn != nil {
f.indexFn(folder, fs)
}
return nil
}
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 {
return f.requestFn(folder, name, offset, size, hash, fromTemporary)
}
return f.fileData[name], nil
}
func (f *fakeConnection) ClusterConfig(protocol.ClusterConfig) {}
func (f *fakeConnection) Ping() bool {
f.mut.Lock()
defer f.mut.Unlock()
return f.closed
}
func (f *fakeConnection) Closed() bool {
f.mut.Lock()
defer f.mut.Unlock()
return f.closed
}
func (f *fakeConnection) Statistics() protocol.Statistics {
return protocol.Statistics{}
}
func (f *fakeConnection) RemoteAddr() net.Addr {
return &fakeAddr{}
}
func (f *fakeConnection) Type() string {
return "fake"
}
func (f *fakeConnection) Crypto() string {
return "fake"
}
func (f *fakeConnection) Transport() string {
return "fake"
}
func (f *fakeConnection) Priority() int {
return 9000
}
func (f *fakeConnection) DownloadProgress(folder string, updates []protocol.FileDownloadProgressUpdate) {
f.downloadProgressMessages = append(f.downloadProgressMessages, downloadProgressMessage{
folder: folder,
updates: updates,
})
}
func (f *fakeConnection) addFileLocked(name string, flags uint32, ftype protocol.FileInfoType, data []byte, version protocol.Vector) {
blockSize := protocol.BlockSize(int64(len(data)))
blocks, _ := scanner.Blocks(context.TODO(), bytes.NewReader(data), blockSize, int64(len(data)), nil, true)
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(),
RawBlockSize: int32(blockSize),
Blocks: blocks,
})
} else {
// Symlink
f.files = append(f.files, protocol.FileInfo{
Name: name,
Type: ftype,
Version: version,
Sequence: time.Now().UnixNano(),
SymlinkTarget: string(data),
NoPermissions: true,
})
}
if f.fileData == nil {
f.fileData = make(map[string][]byte)
}
f.fileData[name] = data
}
func (f *fakeConnection) addFile(name string, flags uint32, ftype protocol.FileInfoType, data []byte) {
f.mut.Lock()
defer f.mut.Unlock()
var version protocol.Vector
version = version.Update(f.id.Short())
f.addFileLocked(name, flags, ftype, data, version)
}
func (f *fakeConnection) updateFile(name string, flags uint32, ftype protocol.FileInfoType, data []byte) {
f.mut.Lock()
defer f.mut.Unlock()
for i, fi := range f.files {
if fi.Name == name {
f.files = append(f.files[:i], f.files[i+1:]...)
f.addFileLocked(name, flags, ftype, data, fi.Version.Update(f.id.Short()))
return
}
}
}
func (f *fakeConnection) deleteFile(name string) {
f.mut.Lock()
defer f.mut.Unlock()
for i, fi := range f.files {
if fi.Name == name {
fi.Deleted = true
fi.ModifiedS = time.Now().Unix()
fi.Version = fi.Version.Update(f.id.Short())
fi.Sequence = time.Now().UnixNano()
fi.Blocks = nil
f.files = append(append(f.files[:i], f.files[i+1:]...), fi)
return
}
}
}
func (f *fakeConnection) sendIndexUpdate() {
f.model.IndexUpdate(f.id, f.folder, f.files)
}
func BenchmarkRequestOut(b *testing.B) {
m := setupModel(defaultCfgWrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
const n = 1000
files := genFiles(n)
fc := &fakeConnection{id: device1}
fc := &fakeConnection{id: device1, model: m}
for _, f := range files {
fc.addFile(f.Name, 0644, protocol.FileInfoTypeFile, []byte("some data to return"))
}
@@ -569,12 +325,15 @@ func TestDeviceRename(t *testing.T) {
t.Errorf("Device already has a name")
}
conn := &fakeConnection{id: device1}
conn := &fakeConnection{id: device1, model: m}
m.AddConnection(conn, hello)
m.ServeBackground()
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
if cfg.Devices()[device1].Name != "" {
t.Errorf("Device already has a name")
@@ -666,7 +425,10 @@ func TestClusterConfig(t *testing.T) {
m.AddFolder(cfg.Folders[0])
m.AddFolder(cfg.Folders[1])
m.ServeBackground()
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
cm := m.generateClusterConfig(device2)
@@ -1121,6 +883,15 @@ func TestIssue5063(t *testing.T) {
wcfg, m := newState(defaultAutoAcceptCfg)
defer os.Remove(wcfg.ConfigPath())
m.pmut.Lock()
for _, c := range m.conn {
conn := c.(*fakeConnection)
conn.mut.Lock()
conn.closeFn = func(_ error) {}
conn.mut.Unlock()
}
m.pmut.Unlock()
wg := sync.WaitGroup{}
addAndVerify := func(id string) {
@@ -1150,7 +921,10 @@ func TestIssue5063(t *testing.T) {
os.RemoveAll(id)
}
}()
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
finished := make(chan struct{})
go func() {
@@ -1328,6 +1102,7 @@ func TestAutoAcceptMultipleFolders(t *testing.T) {
id2 := srand.String(8)
defer os.RemoveAll(id2)
defer m.Stop()
defer m.db.Close()
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
@@ -1365,6 +1140,7 @@ func TestAutoAcceptExistingFolder(t *testing.T) {
wcfg, m := newState(tcfg)
defer os.Remove(wcfg.ConfigPath())
defer m.Stop()
defer m.db.Close()
if fcfg, ok := wcfg.Folder(id); !ok || fcfg.SharedWith(device1) {
t.Error("missing folder, or shared", id)
}
@@ -1399,6 +1175,7 @@ func TestAutoAcceptNewAndExistingFolder(t *testing.T) {
wcfg, m := newState(tcfg)
defer os.Remove(wcfg.ConfigPath())
defer m.Stop()
defer m.db.Close()
if fcfg, ok := wcfg.Folder(id1); !ok || fcfg.SharedWith(device1) {
t.Error("missing folder, or shared", id1)
}
@@ -1441,6 +1218,7 @@ func TestAutoAcceptAlreadyShared(t *testing.T) {
wcfg, m := newState(tcfg)
defer os.Remove(wcfg.ConfigPath())
defer m.Stop()
defer m.db.Close()
if fcfg, ok := wcfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
t.Error("missing folder, or not shared", id)
}
@@ -1470,6 +1248,7 @@ func TestAutoAcceptNameConflict(t *testing.T) {
wcfg, m := newState(defaultAutoAcceptCfg)
defer os.Remove(wcfg.ConfigPath())
defer m.Stop()
defer m.db.Close()
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
@@ -1492,6 +1271,7 @@ func TestAutoAcceptPrefersLabel(t *testing.T) {
defer os.RemoveAll(id)
defer os.RemoveAll(label)
defer m.Stop()
defer m.db.Close()
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
@@ -1518,6 +1298,7 @@ func TestAutoAcceptFallsBackToID(t *testing.T) {
defer os.RemoveAll(label)
defer os.RemoveAll(id)
defer m.Stop()
defer m.db.Close()
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
@@ -1551,6 +1332,7 @@ func TestAutoAcceptPausedWhenFolderConfigChanged(t *testing.T) {
wcfg, m := newState(tcfg)
defer os.Remove(wcfg.ConfigPath())
defer m.Stop()
defer m.db.Close()
if fcfg, ok := wcfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
t.Error("missing folder, or not shared", id)
}
@@ -1609,6 +1391,7 @@ func TestAutoAcceptPausedWhenFolderConfigNotChanged(t *testing.T) {
wcfg, m := newState(tcfg)
defer os.Remove(wcfg.ConfigPath())
defer m.Stop()
defer m.db.Close()
if fcfg, ok := wcfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
t.Error("missing folder, or not shared", id)
}
@@ -1713,7 +1496,10 @@ func TestIgnores(t *testing.T) {
ioutil.WriteFile("testdata/.stignore", []byte(".*\nquux\n"), 0644)
m := setupModel(defaultCfgWrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
// Reach in and update the ignore matcher to one that always does
// reloads when asked to, instead of checking file mtimes. This is
@@ -1817,7 +1603,10 @@ func TestROScanRecovery(t *testing.T) {
m.AddFolder(fcfg)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
waitForState(t, m, "default", "folder path missing")
@@ -1871,7 +1660,10 @@ func TestRWScanRecovery(t *testing.T) {
m.AddFolder(fcfg)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
waitForState(t, m, "default", "folder path missing")
@@ -1898,7 +1690,10 @@ func TestGlobalDirectoryTree(t *testing.T) {
m := newModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
m.ServeBackground()
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
b := func(isfile bool, path ...string) protocol.FileInfo {
typ := protocol.FileInfoTypeDirectory
@@ -2390,7 +2185,10 @@ func TestIssue4357(t *testing.T) {
defer os.Remove(wrapper.ConfigPath())
m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
m.ServeBackground()
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
// Force the model to wire itself and add the folders
p, err := wrapper.Replace(cfg)
@@ -2539,11 +2337,14 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
defer os.Remove(wcfg.ConfigPath())
m := setupModel(wcfg)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
conn1 := &fakeConnection{id: device1}
conn1 := &fakeConnection{id: device1, model: m}
m.AddConnection(conn1, protocol.HelloResult{})
conn2 := &fakeConnection{id: device2}
conn2 := &fakeConnection{id: device2, model: m}
m.AddConnection(conn2, protocol.HelloResult{})
m.ClusterConfig(device1, protocol.ClusterConfig{
@@ -2612,20 +2413,6 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
t.Error("device still in config")
}
if _, ok := m.conn[device2]; !ok {
t.Error("conn missing early")
}
if _, ok := m.helloMessages[device2]; !ok {
t.Error("hello missing early")
}
if _, ok := m.deviceDownloads[device2]; !ok {
t.Error("downloads missing early")
}
m.Closed(conn2, fmt.Errorf("foo"))
if _, ok := m.conn[device2]; ok {
t.Error("conn not missing")
}
@@ -2647,7 +2434,10 @@ func TestIssue3496(t *testing.T) {
// checks on the completion calculation stuff.
m := setupModel(defaultCfgWrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
m.ScanFolder("default")
@@ -2716,7 +2506,10 @@ func TestIssue3496(t *testing.T) {
func TestIssue3804(t *testing.T) {
m := setupModel(defaultCfgWrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
// Subdirs ending in slash should be accepted
@@ -2727,7 +2520,10 @@ func TestIssue3804(t *testing.T) {
func TestIssue3829(t *testing.T) {
m := setupModel(defaultCfgWrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
// Empty subdirs should be accepted
@@ -2747,7 +2543,10 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
defer os.Remove(wcfg.ConfigPath())
m := setupModel(wcfg)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
file := testDataExpected["foo"]
files := m.folderFiles["default"]
@@ -2789,8 +2588,8 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
t.Errorf("should have two available")
}
m.Closed(&fakeConnection{id: device1}, errDeviceUnknown)
m.Closed(&fakeConnection{id: device2}, errDeviceUnknown)
m.Closed(&fakeConnection{id: device1, model: m}, errDeviceUnknown)
m.Closed(&fakeConnection{id: device2, model: m}, errDeviceUnknown)
avail = m.Availability("default", file, file.Blocks[0])
if len(avail) != 0 {
@@ -2982,7 +2781,10 @@ func TestCustomMarkerName(t *testing.T) {
m.AddFolder(fcfg)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
waitForState(t, m, "default", "folder path missing")
@@ -3005,7 +2807,10 @@ func TestRemoveDirWithContent(t *testing.T) {
fd.Close()
m := setupModel(defaultCfgWrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
dir, ok := m.CurrentFolderFile("default", "dirwith")
if !ok {
@@ -3151,8 +2956,8 @@ func TestVersionRestore(t *testing.T) {
".stversions/dir/file~20171210-040406.txt",
".stversions/very/very/deep/one~20171210-040406.txt", // lives deep down, no directory exists.
".stversions/dir/existing~20171210-040406.txt", // exists, should expect to be archived.
".stversions/dir/file.txt~20171210-040405", // incorrect tag format, ignored.
".stversions/dir/cat", // incorrect tag format, ignored.
".stversions/dir/file.txt~20171210-040405", // old tag format, supported
".stversions/dir/cat", // untagged which was used by trashcan, supported
// "file.txt" will be restored
"existing",
@@ -3182,9 +2987,10 @@ func TestVersionRestore(t *testing.T) {
"file.txt": 1,
"existing": 1,
"something": 1,
"dir/file.txt": 3,
"dir/file.txt": 4,
"dir/existing.txt": 1,
"very/very/deep/one.txt": 1,
"dir/cat": 1,
}
for name, vers := range versions {
@@ -3229,7 +3035,7 @@ func TestVersionRestore(t *testing.T) {
ferr, err := m.RestoreFolderVersions("default", restore)
must(t, err)
if err, ok := ferr["something"]; len(ferr) > 1 || !ok || err != "cannot replace a non-file" {
if err, ok := ferr["something"]; len(ferr) > 1 || !ok || err != "cannot restore on top of a directory" {
t.Fatalf("incorrect error or count: %d %s", len(ferr), ferr)
}
@@ -3314,7 +3120,10 @@ func TestPausedFolders(t *testing.T) {
defer os.Remove(wrapper.ConfigPath())
m := setupModel(wrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
if err := m.ScanFolder("default"); err != nil {
t.Error(err)
@@ -3344,7 +3153,10 @@ func TestIssue4094(t *testing.T) {
defer os.Remove(wrapper.ConfigPath())
m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
m.ServeBackground()
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
// Force the model to wire itself and add the folders
folderPath := "nonexistent"
@@ -3381,7 +3193,10 @@ func TestIssue4903(t *testing.T) {
defer os.Remove(wrapper.ConfigPath())
m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
m.ServeBackground()
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
// Force the model to wire itself and add the folders
folderPath := "nonexistent"
@@ -3413,7 +3228,10 @@ func TestIssue5002(t *testing.T) {
// recheckFile should not panic when given an index equal to the number of blocks
m := setupModel(defaultCfgWrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
if err := m.ScanFolder("default"); err != nil {
t.Error(err)
@@ -3423,11 +3241,11 @@ func TestIssue5002(t *testing.T) {
if !ok {
t.Fatal("test file should exist")
}
nBlocks := len(file.Blocks)
blockSize := int32(file.BlockSize())
m.recheckFile(protocol.LocalDeviceID, defaultFolderConfig.Filesystem(), "default", "foo", nBlocks-1, []byte{1, 2, 3, 4})
m.recheckFile(protocol.LocalDeviceID, defaultFolderConfig.Filesystem(), "default", "foo", nBlocks, []byte{1, 2, 3, 4}) // panic
m.recheckFile(protocol.LocalDeviceID, defaultFolderConfig.Filesystem(), "default", "foo", nBlocks+1, []byte{1, 2, 3, 4})
m.recheckFile(protocol.LocalDeviceID, defaultFolderConfig.Filesystem(), "default", "foo", blockSize, file.Size-int64(blockSize), []byte{1, 2, 3, 4})
m.recheckFile(protocol.LocalDeviceID, defaultFolderConfig.Filesystem(), "default", "foo", blockSize, file.Size, []byte{1, 2, 3, 4}) // panic
m.recheckFile(protocol.LocalDeviceID, defaultFolderConfig.Filesystem(), "default", "foo", blockSize, file.Size+int64(blockSize), []byte{1, 2, 3, 4})
}
func TestParentOfUnignored(t *testing.T) {
@@ -3447,25 +3265,6 @@ func TestParentOfUnignored(t *testing.T) {
}
}
func addFakeConn(m *model, dev protocol.DeviceID) *fakeConnection {
fc := &fakeConnection{id: dev, model: m}
m.AddConnection(fc, protocol.HelloResult{})
m.ClusterConfig(dev, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: "default",
Devices: []protocol.Device{
{ID: myID},
{ID: device1},
},
},
},
})
return fc
}
// TestFolderRestartZombies reproduces issue 5233, where multiple concurrent folder
// restarts would leave more than one folder runner alive.
func TestFolderRestartZombies(t *testing.T) {
@@ -3476,7 +3275,10 @@ func TestFolderRestartZombies(t *testing.T) {
wrapper.SetFolder(folderCfg)
m := setupModel(wrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
// Make sure the folder is up and running, because we want to count it.
m.ScanFolder("default")
@@ -3516,16 +3318,6 @@ func TestFolderRestartZombies(t *testing.T) {
}
}
type fakeAddr struct{}
func (fakeAddr) Network() string {
return "network"
}
func (fakeAddr) String() string {
return "address"
}
type alwaysChangedKey struct {
fs fs.Filesystem
name string
@@ -3566,7 +3358,10 @@ func TestRequestLimit(t *testing.T) {
dev.MaxRequestKiB = 1
wrapper.SetDevice(dev)
m, _ := setupModelWithConnectionFromWrapper(wrapper)
defer m.Stop()
defer func() {
m.Stop()
m.db.Close()
}()
file := "tmpfile"
befReq := time.Now()

View File

@@ -17,11 +17,13 @@ import (
)
type ProgressEmitter struct {
registry map[string]*sharedPullerState
registry map[string]map[string]*sharedPullerState // folder: name: puller
interval time.Duration
minBlocks int
sentDownloadStates map[protocol.DeviceID]*sentDownloadState // States representing what we've sent to the other peer via DownloadProgress messages.
connections map[string][]protocol.Connection
connections map[protocol.DeviceID]protocol.Connection
foldersByConns map[protocol.DeviceID][]string
disabled bool
mut sync.Mutex
timer *time.Timer
@@ -34,10 +36,11 @@ type ProgressEmitter struct {
func NewProgressEmitter(cfg config.Wrapper) *ProgressEmitter {
t := &ProgressEmitter{
stop: make(chan struct{}),
registry: make(map[string]*sharedPullerState),
registry: make(map[string]map[string]*sharedPullerState),
timer: time.NewTimer(time.Millisecond),
sentDownloadStates: make(map[protocol.DeviceID]*sentDownloadState),
connections: make(map[string][]protocol.Connection),
connections: make(map[protocol.DeviceID]protocol.Connection),
foldersByConns: make(map[protocol.DeviceID][]string),
mut: sync.NewMutex(),
}
@@ -62,20 +65,21 @@ func (t *ProgressEmitter) Serve() {
l.Debugln("progress emitter: timer - looking after", len(t.registry))
newLastUpdated := lastUpdate
newCount = len(t.registry)
for _, puller := range t.registry {
updated := puller.Updated()
if updated.After(newLastUpdated) {
newLastUpdated = updated
newCount = t.lenRegistryLocked()
for _, pullers := range t.registry {
for _, puller := range pullers {
if updated := puller.Updated(); updated.After(newLastUpdated) {
newLastUpdated = updated
}
}
}
if !newLastUpdated.Equal(lastUpdate) || newCount != lastCount {
lastUpdate = newLastUpdated
lastCount = newCount
t.sendDownloadProgressEvent()
t.sendDownloadProgressEventLocked()
if len(t.connections) > 0 {
t.sendDownloadProgressMessages()
t.sendDownloadProgressMessagesLocked()
}
} else {
l.Debugln("progress emitter: nothing new")
@@ -89,30 +93,29 @@ func (t *ProgressEmitter) Serve() {
}
}
func (t *ProgressEmitter) sendDownloadProgressEvent() {
// registry lock already held
func (t *ProgressEmitter) sendDownloadProgressEventLocked() {
output := make(map[string]map[string]*pullerProgress)
for _, puller := range t.registry {
if output[puller.folder] == nil {
output[puller.folder] = make(map[string]*pullerProgress)
for folder, pullers := range t.registry {
if len(pullers) == 0 {
continue
}
output[folder] = make(map[string]*pullerProgress)
for name, puller := range pullers {
output[folder][name] = puller.Progress()
}
output[puller.folder][puller.file.Name] = puller.Progress()
}
events.Default.Log(events.DownloadProgress, output)
l.Debugf("progress emitter: emitting %#v", output)
}
func (t *ProgressEmitter) sendDownloadProgressMessages() {
// registry lock already held
sharedFolders := make(map[protocol.DeviceID][]string)
deviceConns := make(map[protocol.DeviceID]protocol.Connection)
subscribers := t.connections
for folder, conns := range subscribers {
for _, conn := range conns {
id := conn.ID()
deviceConns[id] = conn
sharedFolders[id] = append(sharedFolders[id], folder)
func (t *ProgressEmitter) sendDownloadProgressMessagesLocked() {
for id, conn := range t.connections {
for _, folder := range t.foldersByConns[id] {
pullers, ok := t.registry[folder]
if !ok {
// There's never been any puller registered for this folder yet
continue
}
state, ok := t.sentDownloadStates[id]
if !ok {
@@ -122,8 +125,8 @@ func (t *ProgressEmitter) sendDownloadProgressMessages() {
t.sentDownloadStates[id] = state
}
var activePullers []*sharedPullerState
for _, puller := range t.registry {
activePullers := make([]*sharedPullerState, 0, len(pullers))
for _, puller := range pullers {
if puller.folder != folder || puller.file.IsSymlink() || puller.file.IsDirectory() || len(puller.file.Blocks) <= t.minBlocks {
continue
}
@@ -143,7 +146,7 @@ func (t *ProgressEmitter) sendDownloadProgressMessages() {
// Clean up sentDownloadStates for devices which we are no longer connected to.
for id := range t.sentDownloadStates {
_, ok := deviceConns[id]
_, ok := t.connections[id]
if !ok {
// Null out outstanding entries for device
delete(t.sentDownloadStates, id)
@@ -152,13 +155,12 @@ func (t *ProgressEmitter) sendDownloadProgressMessages() {
// If a folder was unshared from some device, tell it that all temp files
// are now gone.
for id, sharedDeviceFolders := range sharedFolders {
state := t.sentDownloadStates[id]
nextFolder:
for id, state := range t.sentDownloadStates {
// For each of the folders that the state is aware of,
// try to match it with a shared folder we've discovered above,
nextFolder:
for _, folder := range state.folders() {
for _, existingFolder := range sharedDeviceFolders {
for _, existingFolder := range t.foldersByConns[id] {
if existingFolder == folder {
continue nextFolder
}
@@ -189,12 +191,23 @@ func (t *ProgressEmitter) CommitConfiguration(from, to config.Configuration) boo
t.mut.Lock()
defer t.mut.Unlock()
t.interval = time.Duration(to.Options.ProgressUpdateIntervalS) * time.Second
if t.interval < time.Second {
t.interval = time.Second
switch {
case t.disabled && to.Options.ProgressUpdateIntervalS >= 0:
t.disabled = false
l.Debugln("progress emitter: enabled")
fallthrough
case !t.disabled && from.Options.ProgressUpdateIntervalS != to.Options.ProgressUpdateIntervalS:
t.interval = time.Duration(to.Options.ProgressUpdateIntervalS) * time.Second
if t.interval < time.Second {
t.interval = time.Second
}
l.Debugln("progress emitter: updated interval", t.interval)
case !t.disabled && to.Options.ProgressUpdateIntervalS < 0:
t.clearLocked()
t.disabled = true
l.Debugln("progress emitter: disabled")
}
t.minBlocks = to.Options.TempIndexMinBlocks
l.Debugln("progress emitter: updated interval", t.interval)
return true
}
@@ -209,13 +222,18 @@ func (t *ProgressEmitter) Stop() {
func (t *ProgressEmitter) Register(s *sharedPullerState) {
t.mut.Lock()
defer t.mut.Unlock()
if t.disabled {
l.Debugln("progress emitter: disabled, skip registering")
return
}
l.Debugln("progress emitter: registering", s.folder, s.file.Name)
if len(t.registry) == 0 {
if t.emptyLocked() {
t.timer.Reset(t.interval)
}
// Separate the folder ID (arbitrary string) and the file name by "//"
// because it never appears in a valid file name.
t.registry[s.folder+"//"+s.file.Name] = s
if _, ok := t.registry[s.folder]; !ok {
t.registry[s.folder] = make(map[string]*sharedPullerState)
}
t.registry[s.folder][s.file.Name] = s
}
// Deregister a puller which will stop broadcasting pullers state.
@@ -223,9 +241,13 @@ func (t *ProgressEmitter) Deregister(s *sharedPullerState) {
t.mut.Lock()
defer t.mut.Unlock()
l.Debugln("progress emitter: deregistering", s.folder, s.file.Name)
if t.disabled {
l.Debugln("progress emitter: disabled, skip deregistering")
return
}
delete(t.registry, s.folder+"//"+s.file.Name)
l.Debugln("progress emitter: deregistering", s.folder, s.file.Name)
delete(t.registry[s.folder], s.file.Name)
}
// BytesCompleted returns the number of bytes completed in the given folder.
@@ -233,10 +255,8 @@ func (t *ProgressEmitter) BytesCompleted(folder string) (bytes int64) {
t.mut.Lock()
defer t.mut.Unlock()
for _, s := range t.registry {
if s.folder == folder {
bytes += s.Progress().BytesDone
}
for _, s := range t.registry[folder] {
bytes += s.Progress().BytesDone
}
l.Debugf("progress emitter: bytes completed for %s: %d", folder, bytes)
return
@@ -249,40 +269,53 @@ func (t *ProgressEmitter) String() string {
func (t *ProgressEmitter) lenRegistry() int {
t.mut.Lock()
defer t.mut.Unlock()
return len(t.registry)
return t.lenRegistryLocked()
}
func (t *ProgressEmitter) lenRegistryLocked() (out int) {
for _, pullers := range t.registry {
out += len(pullers)
}
return out
}
func (t *ProgressEmitter) emptyLocked() bool {
for _, pullers := range t.registry {
if len(pullers) != 0 {
return false
}
}
return true
}
func (t *ProgressEmitter) temporaryIndexSubscribe(conn protocol.Connection, folders []string) {
t.mut.Lock()
for _, folder := range folders {
t.connections[folder] = append(t.connections[folder], conn)
}
t.mut.Unlock()
defer t.mut.Unlock()
t.connections[conn.ID()] = conn
t.foldersByConns[conn.ID()] = folders
}
func (t *ProgressEmitter) temporaryIndexUnsubscribe(conn protocol.Connection) {
t.mut.Lock()
left := make(map[string][]protocol.Connection, len(t.connections))
for folder, conns := range t.connections {
connsLeft := connsWithout(conns, conn)
if len(connsLeft) > 0 {
left[folder] = connsLeft
}
}
t.connections = left
t.mut.Unlock()
defer t.mut.Unlock()
delete(t.connections, conn.ID())
delete(t.foldersByConns, conn.ID())
}
func connsWithout(conns []protocol.Connection, conn protocol.Connection) []protocol.Connection {
if len(conns) == 0 {
return nil
}
newConns := make([]protocol.Connection, 0, len(conns)-1)
for _, existingConn := range conns {
if existingConn != conn {
newConns = append(newConns, existingConn)
func (t *ProgressEmitter) clearLocked() {
for id, state := range t.sentDownloadStates {
conn, ok := t.connections[id]
if !ok {
continue
}
for _, folder := range state.folders() {
if updates := state.cleanup(folder); len(updates) > 0 {
conn.DownloadProgress(folder, updates)
}
}
}
return newConns
t.registry = make(map[string]map[string]*sharedPullerState)
t.sentDownloadStates = make(map[protocol.DeviceID]*sentDownloadState)
t.connections = make(map[protocol.DeviceID]protocol.Connection)
t.foldersByConns = make(map[protocol.DeviceID][]string)
}

View File

@@ -114,6 +114,9 @@ func TestSendDownloadProgressMessages(t *testing.T) {
p := NewProgressEmitter(c)
p.temporaryIndexSubscribe(fc, []string{"folder", "folder2"})
p.registry["folder"] = make(map[string]*sharedPullerState)
p.registry["folder2"] = make(map[string]*sharedPullerState)
p.registry["folderXXX"] = make(map[string]*sharedPullerState)
expect := func(updateIdx int, state *sharedPullerState, updateType protocol.FileDownloadProgressUpdateType, version protocol.Vector, blocks []int32, remove bool) {
messageIdx := -1
@@ -202,39 +205,39 @@ func TestSendDownloadProgressMessages(t *testing.T) {
mut: sync.NewRWMutex(),
availableUpdated: time.Now(),
}
p.registry["1"] = state1
p.registry["folder"]["1"] = state1
// Has no blocks, hence no message is sent
p.sendDownloadProgressMessages()
sendMsgs(p)
expectEmpty()
// Returns update for puller with new extra blocks
state1.available = []int32{1}
p.sendDownloadProgressMessages()
sendMsgs(p)
expect(0, state1, protocol.UpdateTypeAppend, v1, []int32{1}, true)
expectEmpty()
// Does nothing if nothing changes
p.sendDownloadProgressMessages()
sendMsgs(p)
expectEmpty()
// Does nothing if timestamp updated, but no new blocks (should never happen)
state1.availableUpdated = tick()
p.sendDownloadProgressMessages()
sendMsgs(p)
expectEmpty()
// Does not return an update if date blocks change but date does not (should never happen)
state1.available = []int32{1, 2}
p.sendDownloadProgressMessages()
sendMsgs(p)
expectEmpty()
// If the date and blocks changes, returns only the diff
state1.availableUpdated = tick()
p.sendDownloadProgressMessages()
sendMsgs(p)
expect(0, state1, protocol.UpdateTypeAppend, v1, []int32{2}, true)
expectEmpty()
@@ -242,7 +245,7 @@ func TestSendDownloadProgressMessages(t *testing.T) {
// Returns forget and update if puller version has changed
state1.file.Version = v2
p.sendDownloadProgressMessages()
sendMsgs(p)
expect(0, state1, protocol.UpdateTypeForget, v1, nil, false)
expect(1, state1, protocol.UpdateTypeAppend, v2, []int32{1, 2}, true)
@@ -254,7 +257,7 @@ func TestSendDownloadProgressMessages(t *testing.T) {
state1.availableUpdated = tick()
state1.created = tick()
p.sendDownloadProgressMessages()
sendMsgs(p)
expect(0, state1, protocol.UpdateTypeForget, v2, nil, false)
expect(1, state1, protocol.UpdateTypeAppend, v2, []int32{1}, true)
@@ -265,7 +268,7 @@ func TestSendDownloadProgressMessages(t *testing.T) {
state1.available = nil
state1.availableUpdated = tick()
p.sendDownloadProgressMessages()
sendMsgs(p)
expect(0, state1, protocol.UpdateTypeForget, v2, nil, false)
expect(1, state1, protocol.UpdateTypeAppend, v1, nil, true)
@@ -308,11 +311,11 @@ func TestSendDownloadProgressMessages(t *testing.T) {
available: []int32{1, 2, 3},
availableUpdated: time.Now(),
}
p.registry["2"] = state2
p.registry["3"] = state3
p.registry["4"] = state4
p.registry["folder2"]["2"] = state2
p.registry["folder"]["3"] = state3
p.registry["folder2"]["4"] = state4
p.sendDownloadProgressMessages()
sendMsgs(p)
expect(-1, state1, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, false)
expect(-1, state3, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, true)
@@ -326,10 +329,10 @@ func TestSendDownloadProgressMessages(t *testing.T) {
state2.available = []int32{1, 2, 3, 4, 5}
state2.availableUpdated = tick()
delete(p.registry, "3")
delete(p.registry, "4")
delete(p.registry["folder"], "3")
delete(p.registry["folder2"], "4")
p.sendDownloadProgressMessages()
sendMsgs(p)
expect(-1, state1, protocol.UpdateTypeAppend, v1, []int32{4, 5}, false)
expect(-1, state3, protocol.UpdateTypeForget, v1, nil, true)
@@ -338,8 +341,8 @@ func TestSendDownloadProgressMessages(t *testing.T) {
expectEmpty()
// Deletions are sent only once (actual bug I found writing the tests)
p.sendDownloadProgressMessages()
p.sendDownloadProgressMessages()
sendMsgs(p)
sendMsgs(p)
expectEmpty()
// Not sent for "inactive" (symlinks, dirs, or wrong folder) pullers
@@ -392,31 +395,31 @@ func TestSendDownloadProgressMessages(t *testing.T) {
available: []int32{1, 2, 3},
availableUpdated: time.Now(),
}
p.registry["5"] = state5
p.registry["6"] = state6
p.registry["7"] = state7
p.registry["8"] = state8
p.registry["folder"]["5"] = state5
p.registry["folder"]["6"] = state6
p.registry["folderXXX"]["7"] = state7
p.registry["folder"]["8"] = state8
p.sendDownloadProgressMessages()
sendMsgs(p)
expectEmpty()
// Device is no longer subscribed to a particular folder
delete(p.registry, "1") // Clean up first
delete(p.registry, "2") // Clean up first
delete(p.registry["folder"], "1") // Clean up first
delete(p.registry["folder2"], "2") // Clean up first
p.sendDownloadProgressMessages()
sendMsgs(p)
expect(-1, state1, protocol.UpdateTypeForget, v1, nil, true)
expect(-1, state2, protocol.UpdateTypeForget, v1, nil, true)
expectEmpty()
p.registry["1"] = state1
p.registry["2"] = state2
p.registry["3"] = state3
p.registry["4"] = state4
p.registry["folder"]["1"] = state1
p.registry["folder2"]["2"] = state2
p.registry["folder"]["3"] = state3
p.registry["folder2"]["4"] = state4
p.sendDownloadProgressMessages()
sendMsgs(p)
expect(-1, state1, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3, 4, 5}, false)
expect(-1, state3, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, true)
@@ -427,7 +430,7 @@ func TestSendDownloadProgressMessages(t *testing.T) {
p.temporaryIndexUnsubscribe(fc)
p.temporaryIndexSubscribe(fc, []string{"folder"})
p.sendDownloadProgressMessages()
sendMsgs(p)
// See progressemitter.go for explanation why this is commented out.
// Search for state.cleanup
@@ -439,9 +442,15 @@ func TestSendDownloadProgressMessages(t *testing.T) {
// Cleanup when device no longer exists
p.temporaryIndexUnsubscribe(fc)
p.sendDownloadProgressMessages()
sendMsgs(p)
_, ok := p.sentDownloadStates[fc.ID()]
if ok {
t.Error("Should not be there")
}
}
func sendMsgs(p *ProgressEmitter) {
p.mut.Lock()
defer p.mut.Unlock()
p.sendDownloadProgressMessagesLocked()
}

View File

@@ -32,6 +32,7 @@ func TestRequestSimple(t *testing.T) {
tfs := fcfg.Filesystem()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(tfs.URI())
os.Remove(w.ConfigPath())
}()
@@ -78,6 +79,7 @@ func TestSymlinkTraversalRead(t *testing.T) {
m, fc, fcfg, w := setupModelWithConnection()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(fcfg.Filesystem().URI())
os.Remove(w.ConfigPath())
}()
@@ -125,6 +127,7 @@ func TestSymlinkTraversalWrite(t *testing.T) {
m, fc, fcfg, w := setupModelWithConnection()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(fcfg.Filesystem().URI())
os.Remove(w.ConfigPath())
}()
@@ -188,6 +191,7 @@ func TestRequestCreateTmpSymlink(t *testing.T) {
m, fc, fcfg, w := setupModelWithConnection()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(fcfg.Filesystem().URI())
os.Remove(w.ConfigPath())
}()
@@ -238,8 +242,8 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
fcfg.Versioning = config.VersioningConfiguration{Type: "trashcan"}
w.SetFolder(fcfg)
m, fc := setupModelWithConnectionFromWrapper(w)
defer m.db.Close()
defer m.Stop()
// Create a temporary directory that we will use as target to see if
@@ -312,6 +316,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
m, fc := setupModelWithConnectionFromWrapper(w)
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(fss.URI())
os.Remove(w.ConfigPath())
}()
@@ -430,6 +435,7 @@ func TestIssue4841(t *testing.T) {
m, fc, fcfg, w := setupModelWithConnection()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(fcfg.Filesystem().URI())
os.Remove(w.ConfigPath())
}()
@@ -473,6 +479,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
tmpDir := fcfg.Filesystem().URI()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(tmpDir)
os.Remove(w.ConfigPath())
}()
@@ -538,6 +545,7 @@ func TestParentDeletion(t *testing.T) {
testFs := fcfg.Filesystem()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(testFs.URI())
os.Remove(w.ConfigPath())
}()
@@ -620,6 +628,7 @@ func TestRequestSymlinkWindows(t *testing.T) {
m, fc, fcfg, w := setupModelWithConnection()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(fcfg.Filesystem().URI())
os.Remove(w.ConfigPath())
}()
@@ -678,50 +687,6 @@ func TestRequestSymlinkWindows(t *testing.T) {
}
}
func tmpDefaultWrapper() (config.Wrapper, config.FolderConfiguration) {
w := createTmpWrapper(defaultCfgWrapper.RawCopy())
fcfg := testFolderConfigTmp()
w.SetFolder(fcfg)
return w, fcfg
}
func testFolderConfigTmp() config.FolderConfiguration {
tmpDir := createTmpDir()
return testFolderConfig(tmpDir)
}
func testFolderConfig(path string) config.FolderConfiguration {
cfg := config.NewFolderConfiguration(myID, "default", "default", fs.FilesystemTypeBasic, path)
cfg.FSWatcherEnabled = false
cfg.Devices = append(cfg.Devices, config.FolderDeviceConfiguration{DeviceID: device1})
return cfg
}
func setupModelWithConnection() (*model, *fakeConnection, config.FolderConfiguration, config.Wrapper) {
w, fcfg := tmpDefaultWrapper()
m, fc := setupModelWithConnectionFromWrapper(w)
return m, fc, fcfg, w
}
func setupModelWithConnectionFromWrapper(w config.Wrapper) (*model, *fakeConnection) {
m := setupModel(w)
fc := addFakeConn(m, device1)
fc.folder = "default"
m.ScanFolder("default")
return m, fc
}
func createTmpDir() string {
tmpDir, err := ioutil.TempDir("", "syncthing_testFolder-")
if err != nil {
panic("Failed to create temporary testing dir")
}
return tmpDir
}
func equalContents(path string, contents []byte) error {
if bs, err := ioutil.ReadFile(path); err != nil {
return err
@@ -737,6 +702,7 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
tmpDir := tfs.URI()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(tmpDir)
os.Remove(w.ConfigPath())
}()
@@ -869,6 +835,7 @@ func TestRequestRemoteRenameConflict(t *testing.T) {
tmpDir := tfs.URI()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(tmpDir)
os.Remove(w.ConfigPath())
}()
@@ -963,6 +930,7 @@ func TestRequestDeleteChanged(t *testing.T) {
tfs := fcfg.Filesystem()
defer func() {
m.Stop()
m.db.Close()
os.RemoveAll(tfs.URI())
os.Remove(w.ConfigPath())
}()

View File

@@ -280,13 +280,15 @@ func (s *sharedPullerState) finalClose() (bool, error) {
}
if s.fd != nil {
// This is our error if we weren't errored before. Otherwise we
// keep the earlier error.
if fsyncErr := s.fd.Sync(); fsyncErr != nil && s.err == nil {
s.err = fsyncErr
if err := s.fd.Sync(); err != nil {
// Sync() is nice if it works but not worth failing the
// operation over if it fails.
l.Debugf("fsync %q failed: %v", s.tempName, err)
}
if closeErr := s.fd.Close(); closeErr != nil && s.err == nil {
s.err = closeErr
if err := s.fd.Close(); err != nil && s.err == nil {
// This is our error as we weren't errored before.
s.err = err
}
s.fd = nil
}

123
lib/model/testutils_test.go Normal file
View File

@@ -0,0 +1,123 @@
// 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 (
"io/ioutil"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/protocol"
)
var (
myID, device1, device2 protocol.DeviceID
defaultCfgWrapper config.Wrapper
defaultFolderConfig config.FolderConfiguration
defaultFs fs.Filesystem
defaultCfg config.Configuration
defaultAutoAcceptCfg config.Configuration
)
func init() {
myID, _ = protocol.DeviceIDFromString("ZNWFSWE-RWRV2BD-45BLMCV-LTDE2UR-4LJDW6J-R5BPWEB-TXD27XJ-IZF5RA4")
device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
defaultFs = fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata")
defaultFolderConfig = testFolderConfig("testdata")
defaultCfgWrapper = createTmpWrapper(config.New(myID))
_, _ = defaultCfgWrapper.SetDevice(config.NewDeviceConfiguration(device1, "device1"))
_, _ = defaultCfgWrapper.SetFolder(defaultFolderConfig)
opts := defaultCfgWrapper.Options()
opts.KeepTemporariesH = 1
_, _ = defaultCfgWrapper.SetOptions(opts)
defaultCfg = defaultCfgWrapper.RawCopy()
defaultAutoAcceptCfg = config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: myID, // self
},
{
DeviceID: device1,
AutoAcceptFolders: true,
},
{
DeviceID: device2,
AutoAcceptFolders: true,
},
},
Options: config.OptionsConfiguration{
DefaultFolderPath: ".",
},
}
}
func tmpDefaultWrapper() (config.Wrapper, config.FolderConfiguration) {
w := createTmpWrapper(defaultCfgWrapper.RawCopy())
fcfg := testFolderConfigTmp()
_, _ = w.SetFolder(fcfg)
return w, fcfg
}
func testFolderConfigTmp() config.FolderConfiguration {
tmpDir := createTmpDir()
return testFolderConfig(tmpDir)
}
func testFolderConfig(path string) config.FolderConfiguration {
cfg := config.NewFolderConfiguration(myID, "default", "default", fs.FilesystemTypeBasic, path)
cfg.FSWatcherEnabled = false
cfg.Devices = append(cfg.Devices, config.FolderDeviceConfiguration{DeviceID: device1})
return cfg
}
func setupModelWithConnection() (*model, *fakeConnection, config.FolderConfiguration, config.Wrapper) {
w, fcfg := tmpDefaultWrapper()
m, fc := setupModelWithConnectionFromWrapper(w)
return m, fc, fcfg, w
}
func setupModelWithConnectionFromWrapper(w config.Wrapper) (*model, *fakeConnection) {
m := setupModel(w)
fc := addFakeConn(m, device1)
fc.folder = "default"
_ = m.ScanFolder("default")
return m, fc
}
func setupModel(w config.Wrapper) *model {
db := db.OpenMemory()
m := newModel(w, myID, "syncthing", "dev", db, nil)
m.ServeBackground()
for id, cfg := range w.Folders() {
if !cfg.Paused {
m.AddFolder(cfg)
m.StartFolder(id)
}
}
m.ScanFolders()
return m
}
func createTmpDir() string {
tmpDir, err := ioutil.TempDir("", "syncthing_testFolder-")
if err != nil {
panic("Failed to create temporary testing dir")
}
return tmpDir
}

View File

@@ -22,37 +22,62 @@ import (
// often enough that there is any contention on this lock.
var renameLock = sync.NewMutex()
// TryRename renames a file, leaving source file intact in case of failure.
// RenameOrCopy renames a file, leaving source file intact in case of failure.
// Tries hard to succeed on various systems by temporarily tweaking directory
// permissions and removing the destination file when necessary.
func TryRename(filesystem fs.Filesystem, from, to string) error {
func RenameOrCopy(src, dst fs.Filesystem, from, to string) error {
renameLock.Lock()
defer renameLock.Unlock()
return withPreparedTarget(filesystem, from, to, func() error {
return filesystem.Rename(from, to)
})
}
return withPreparedTarget(dst, from, to, func() error {
// Optimisation 1
if src.Type() == dst.Type() && src.URI() == dst.URI() {
return src.Rename(from, to)
}
// Rename moves a temporary file to its final place.
// Will make sure to delete the from file if the operation fails, so use only
// for situations like committing a temp file to its final location.
// Tries hard to succeed on various systems by temporarily tweaking directory
// permissions and removing the destination file when necessary.
func Rename(filesystem fs.Filesystem, from, to string) error {
// Don't leave a dangling temp file in case of rename error
if !(runtime.GOOS == "windows" && strings.EqualFold(from, to)) {
defer filesystem.Remove(from)
}
return TryRename(filesystem, from, to)
// "Optimisation" 2
// Try to find a common prefix between the two filesystems, use that as the base for the new one
// and try a rename.
if src.Type() == dst.Type() {
commonPrefix := fs.CommonPrefix(src.URI(), dst.URI())
if len(commonPrefix) > 0 {
commonFs := fs.NewFilesystem(src.Type(), commonPrefix)
err := commonFs.Rename(
filepath.Join(strings.TrimPrefix(src.URI(), commonPrefix), from),
filepath.Join(strings.TrimPrefix(dst.URI(), commonPrefix), to),
)
if err == nil {
return nil
}
}
}
// Everything is sad, do a copy and delete.
if _, err := dst.Stat(to); !fs.IsNotExist(err) {
err := dst.Remove(to)
if err != nil {
return err
}
}
err := copyFileContents(src, dst, from, to)
if err != nil {
_ = dst.Remove(to)
return err
}
return withPreparedTarget(src, from, from, func() error {
return src.Remove(from)
})
})
}
// Copy copies the file content from source to destination.
// Tries hard to succeed on various systems by temporarily tweaking directory
// permissions and removing the destination file when necessary.
func Copy(filesystem fs.Filesystem, from, to string) (err error) {
return withPreparedTarget(filesystem, from, to, func() error {
return copyFileContents(filesystem, from, to)
func Copy(src, dst fs.Filesystem, from, to string) (err error) {
return withPreparedTarget(dst, from, to, func() error {
return copyFileContents(src, dst, from, to)
})
}
@@ -115,13 +140,13 @@ func withPreparedTarget(filesystem fs.Filesystem, from, to string, f func() erro
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all its contents will be replaced by the contents
// of the source file.
func copyFileContents(filesystem fs.Filesystem, src, dst string) (err error) {
in, err := filesystem.Open(src)
func copyFileContents(srcFs, dstFs fs.Filesystem, src, dst string) (err error) {
in, err := srcFs.Open(src)
if err != nil {
return
}
defer in.Close()
out, err := filesystem.Create(dst)
out, err := dstFs.Create(dst)
if err != nil {
return
}

View File

@@ -7,6 +7,7 @@
package osutil_test
import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
@@ -192,7 +193,7 @@ func TestInWritableDirWindowsRename(t *testing.T) {
}
rename := func(path string) error {
return osutil.Rename(fs, path, path+"new")
return osutil.RenameOrCopy(fs, fs, path, path+"new")
}
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
@@ -268,3 +269,79 @@ func TestIsDeleted(t *testing.T) {
testFs.Chmod("inacc", 0777)
os.RemoveAll("testdata")
}
func TestRenameOrCopy(t *testing.T) {
mustTempDir := func() string {
t.Helper()
tmpDir, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
return tmpDir
}
sameFs := fs.NewFilesystem(fs.FilesystemTypeBasic, mustTempDir())
tests := []struct {
src fs.Filesystem
dst fs.Filesystem
file string
}{
{
src: sameFs,
dst: sameFs,
file: "file",
},
{
src: fs.NewFilesystem(fs.FilesystemTypeBasic, mustTempDir()),
dst: fs.NewFilesystem(fs.FilesystemTypeBasic, mustTempDir()),
file: "file",
},
{
src: fs.NewFilesystem(fs.FilesystemTypeFake, `fake://fake/?files=1&seed=42`),
dst: fs.NewFilesystem(fs.FilesystemTypeBasic, mustTempDir()),
file: osutil.NativeFilename(`05/7a/4d52f284145b9fe8`),
},
}
for _, test := range tests {
content := test.src.URI()
if _, err := test.src.Lstat(test.file); err != nil {
if !fs.IsNotExist(err) {
t.Fatal(err)
}
if fd, err := test.src.Create(test.file); err != nil {
t.Fatal(err)
} else {
if _, err := fd.Write([]byte(test.src.URI())); err != nil {
t.Fatal(err)
}
_ = fd.Close()
}
} else {
fd, err := test.src.Open(test.file)
if err != nil {
t.Fatal(err)
}
buf, err := ioutil.ReadAll(fd)
if err != nil {
t.Fatal(err)
}
_ = fd.Close()
content = string(buf)
}
err := osutil.RenameOrCopy(test.src, test.dst, test.file, "new")
if err != nil {
t.Fatal(err)
}
if fd, err := test.dst.Open("new"); err != nil {
t.Fatal(err)
} else {
if buf, err := ioutil.ReadAll(fd); err != nil {
t.Fatal(err)
} else if string(buf) != content {
t.Fatalf("expected %s got %s", content, string(buf))
}
}
}
}

View File

@@ -643,7 +643,7 @@ func (c *rawConnection) writerLoop() {
for {
select {
case hm := <-c.outbox:
err := c.writeMessage(hm)
err := c.writeMessage(hm.msg)
if hm.done != nil {
close(hm.done)
}
@@ -658,17 +658,17 @@ func (c *rawConnection) writerLoop() {
}
}
func (c *rawConnection) writeMessage(hm asyncMessage) error {
if c.shouldCompressMessage(hm.msg) {
return c.writeCompressedMessage(hm)
func (c *rawConnection) writeMessage(msg message) error {
if c.shouldCompressMessage(msg) {
return c.writeCompressedMessage(msg)
}
return c.writeUncompressedMessage(hm)
return c.writeUncompressedMessage(msg)
}
func (c *rawConnection) writeCompressedMessage(hm asyncMessage) error {
size := hm.msg.ProtoSize()
func (c *rawConnection) writeCompressedMessage(msg message) error {
size := msg.ProtoSize()
buf := BufferPool.Get(size)
if _, err := hm.msg.MarshalTo(buf); err != nil {
if _, err := msg.MarshalTo(buf); err != nil {
return fmt.Errorf("marshalling message: %v", err)
}
@@ -678,7 +678,7 @@ func (c *rawConnection) writeCompressedMessage(hm asyncMessage) error {
}
hdr := Header{
Type: c.typeOf(hm.msg),
Type: c.typeOf(msg),
Compression: MessageCompressionLZ4,
}
hdrSize := hdr.ProtoSize()
@@ -711,11 +711,11 @@ func (c *rawConnection) writeCompressedMessage(hm asyncMessage) error {
return nil
}
func (c *rawConnection) writeUncompressedMessage(hm asyncMessage) error {
size := hm.msg.ProtoSize()
func (c *rawConnection) writeUncompressedMessage(msg message) error {
size := msg.ProtoSize()
hdr := Header{
Type: c.typeOf(hm.msg),
Type: c.typeOf(msg),
}
hdrSize := hdr.ProtoSize()
if hdrSize > 1<<16-1 {
@@ -734,7 +734,7 @@ func (c *rawConnection) writeUncompressedMessage(hm asyncMessage) error {
// Message length
binary.BigEndian.PutUint32(buf[2+hdrSize:], uint32(size))
// Message
if _, err := hm.msg.MarshalTo(buf[2+hdrSize+4:]); err != nil {
if _, err := msg.MarshalTo(buf[2+hdrSize+4:]); err != nil {
return fmt.Errorf("marshalling message: %v", err)
}

View File

@@ -530,7 +530,7 @@ func (w *walker) handleError(ctx context.Context, context, path string, err erro
if fs.IsNotExist(err) {
return
}
l.Infof("Scanner (folder %s, file %q): %s: %v", w.Folder, path, context, err)
l.Infof("Scanner (folder %s, item %q): %s: %v", w.Folder, path, context, err)
select {
case finishedChan <- ScanResult{
Err: fmt.Errorf("%s: %s", context, err.Error()),

View File

@@ -12,6 +12,7 @@ import (
"os/exec"
"runtime"
"strings"
"time"
"github.com/syncthing/syncthing/lib/fs"
@@ -103,3 +104,11 @@ func (v External) Archive(filePath string) error {
}
return errors.New("Versioner: file was not removed by external script")
}
func (v External) GetVersions() (map[string][]FileVersion, error) {
return nil, ErrRestorationNotSupported
}
func (v External) Restore(filePath string, versionTime time.Time) error {
return ErrRestorationNotSupported
}

View File

@@ -9,9 +9,9 @@ package versioner
import (
"path/filepath"
"strconv"
"time"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/util"
)
@@ -21,19 +21,21 @@ func init() {
}
type Simple struct {
keep int
fs fs.Filesystem
keep int
folderFs fs.Filesystem
versionsFs fs.Filesystem
}
func NewSimple(folderID string, fs fs.Filesystem, params map[string]string) Versioner {
func NewSimple(folderID string, folderFs fs.Filesystem, params map[string]string) Versioner {
keep, err := strconv.Atoi(params["keep"])
if err != nil {
keep = 5 // A reasonable default
}
s := Simple{
keep: keep,
fs: fs,
keep: keep,
folderFs: folderFs,
versionsFs: fsFromParams(folderFs, params),
}
l.Debugf("instantiated %#v", s)
@@ -43,51 +45,17 @@ func NewSimple(folderID string, fs fs.Filesystem, params map[string]string) Vers
// Archive moves the named file away to a version archive. If this function
// returns nil, the named file does not exist any more (has been archived).
func (v Simple) Archive(filePath string) error {
info, err := v.fs.Lstat(filePath)
if fs.IsNotExist(err) {
l.Debugln("not archiving nonexistent file", filePath)
return nil
} else if err != nil {
err := archiveFile(v.folderFs, v.versionsFs, filePath, TagFilename)
if err != nil {
return err
}
if info.IsSymlink() {
panic("bug: attempting to version a symlink")
}
versionsDir := ".stversions"
_, err = v.fs.Stat(versionsDir)
if err != nil {
if fs.IsNotExist(err) {
l.Debugln("creating versions dir .stversions")
v.fs.Mkdir(versionsDir, 0755)
v.fs.Hide(versionsDir)
} else {
return err
}
}
l.Debugln("archiving", filePath)
file := filepath.Base(filePath)
inFolderPath := filepath.Dir(filePath)
dir := filepath.Join(versionsDir, inFolderPath)
err = v.fs.MkdirAll(dir, 0755)
if err != nil && !fs.IsExist(err) {
return err
}
ver := TagFilename(file, info.ModTime().Format(TimeFormat))
dst := filepath.Join(dir, ver)
l.Debugln("moving to", dst)
err = osutil.Rename(v.fs, filePath, dst)
if err != nil {
return err
}
dir := filepath.Dir(filePath)
// Glob according to the new file~timestamp.ext pattern.
pattern := filepath.Join(dir, TagFilename(file, TimeGlob))
newVersions, err := v.fs.Glob(pattern)
newVersions, err := v.versionsFs.Glob(pattern)
if err != nil {
l.Warnln("globbing:", err, "for", pattern)
return nil
@@ -95,7 +63,7 @@ func (v Simple) Archive(filePath string) error {
// Also according to the old file.ext~timestamp pattern.
pattern = filepath.Join(dir, file+"~"+TimeGlob)
oldVersions, err := v.fs.Glob(pattern)
oldVersions, err := v.versionsFs.Glob(pattern)
if err != nil {
l.Warnln("globbing:", err, "for", pattern)
return nil
@@ -108,7 +76,7 @@ func (v Simple) Archive(filePath string) error {
if len(versions) > v.keep {
for _, toRemove := range versions[:len(versions)-v.keep] {
l.Debugln("cleaning out", toRemove)
err = v.fs.Remove(toRemove)
err = v.versionsFs.Remove(toRemove)
if err != nil {
l.Warnln("removing old version:", err)
}
@@ -117,3 +85,11 @@ func (v Simple) Archive(filePath string) error {
return nil
}
func (v Simple) GetVersions() (map[string][]FileVersion, error) {
return retrieveVersions(v.versionsFs)
}
func (v Simple) Restore(filepath string, versionTime time.Time) error {
return restoreFile(v.versionsFs, v.folderFs, filepath, versionTime, TagFilename)
}

View File

@@ -7,7 +7,6 @@
package versioner
import (
"os"
"path/filepath"
"strconv"
"time"
@@ -48,16 +47,9 @@ func NewStaggered(folderID string, folderFs fs.Filesystem, params map[string]str
cleanInterval = 3600 // Default: clean once per hour
}
// Use custom path if set, otherwise .stversions in folderPath
var versionsFs fs.Filesystem
if params["versionsPath"] == "" {
versionsFs = fs.NewFilesystem(folderFs.Type(), filepath.Join(folderFs.URI(), ".stversions"))
} else if filepath.IsAbs(params["versionsPath"]) {
versionsFs = fs.NewFilesystem(folderFs.Type(), params["versionsPath"])
} else {
versionsFs = fs.NewFilesystem(folderFs.Type(), filepath.Join(folderFs.URI(), params["versionsPath"]))
}
l.Debugln("%s folder using %s (%s) staggered versioner dir", folderID, versionsFs.URI(), versionsFs.Type())
// Backwards compatibility
params["fsPath"] = params["versionsPath"]
versionsFs := fsFromParams(folderFs, params)
s := &Staggered{
cleanInterval: cleanInterval,
@@ -225,53 +217,12 @@ func (v *Staggered) Archive(filePath string) error {
v.mutex.Lock()
defer v.mutex.Unlock()
info, err := v.folderFs.Lstat(filePath)
if fs.IsNotExist(err) {
l.Debugln("not archiving nonexistent file", filePath)
return nil
} else if err != nil {
if err := archiveFile(v.folderFs, v.versionsFs, filePath, TagFilename); err != nil {
return err
}
if info.IsSymlink() {
panic("bug: attempting to version a symlink")
}
if _, err := v.versionsFs.Stat("."); err != nil {
if fs.IsNotExist(err) {
l.Debugln("creating versions dir", v.versionsFs)
v.versionsFs.MkdirAll(".", 0755)
v.versionsFs.Hide(".")
} else {
return err
}
}
l.Debugln("archiving", filePath)
file := filepath.Base(filePath)
inFolderPath := filepath.Dir(filePath)
if err != nil {
return err
}
err = v.versionsFs.MkdirAll(inFolderPath, 0755)
if err != nil && !fs.IsExist(err) {
return err
}
ver := TagFilename(file, time.Now().Format(TimeFormat))
dst := filepath.Join(inFolderPath, ver)
l.Debugln("moving to", dst)
/// TODO: Fix this when we have an alternative filesystem implementation
if v.versionsFs.Type() != fs.FilesystemTypeBasic {
panic("bug: staggered versioner used with unsupported filesystem")
}
err = os.Rename(filepath.Join(v.folderFs.URI(), filePath), filepath.Join(v.versionsFs.URI(), dst))
if err != nil {
return err
}
// Glob according to the new file~timestamp.ext pattern.
pattern := filepath.Join(inFolderPath, TagFilename(file, TimeGlob))
@@ -295,3 +246,11 @@ func (v *Staggered) Archive(filePath string) error {
return nil
}
func (v *Staggered) GetVersions() (map[string][]FileVersion, error) {
return retrieveVersions(v.versionsFs)
}
func (v *Staggered) Restore(filepath string, versionTime time.Time) error {
return restoreFile(v.versionsFs, v.folderFs, filepath, versionTime, TagFilename)
}

View File

@@ -8,12 +8,10 @@ package versioner
import (
"fmt"
"path/filepath"
"strconv"
"time"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
)
func init() {
@@ -22,17 +20,19 @@ func init() {
}
type Trashcan struct {
fs fs.Filesystem
folderFs fs.Filesystem
versionsFs fs.Filesystem
cleanoutDays int
stop chan struct{}
}
func NewTrashcan(folderID string, fs fs.Filesystem, params map[string]string) Versioner {
func NewTrashcan(folderID string, folderFs fs.Filesystem, params map[string]string) Versioner {
cleanoutDays, _ := strconv.Atoi(params["cleanoutDays"])
// On error we default to 0, "do not clean out the trash can"
s := &Trashcan{
fs: fs,
folderFs: folderFs,
versionsFs: fsFromParams(folderFs, params),
cleanoutDays: cleanoutDays,
stop: make(chan struct{}),
}
@@ -44,49 +44,9 @@ func NewTrashcan(folderID string, fs fs.Filesystem, params map[string]string) Ve
// Archive moves the named file away to a version archive. If this function
// returns nil, the named file does not exist any more (has been archived).
func (t *Trashcan) Archive(filePath string) error {
info, err := t.fs.Lstat(filePath)
if fs.IsNotExist(err) {
l.Debugln("not archiving nonexistent file", filePath)
return nil
} else if err != nil {
return err
}
if info.IsSymlink() {
panic("bug: attempting to version a symlink")
}
versionsDir := ".stversions"
if _, err := t.fs.Stat(versionsDir); err != nil {
if !fs.IsNotExist(err) {
return err
}
l.Debugln("creating versions dir", versionsDir)
if err := t.fs.MkdirAll(versionsDir, 0777); err != nil {
return err
}
t.fs.Hide(versionsDir)
}
l.Debugln("archiving", filePath)
archivedPath := filepath.Join(versionsDir, filePath)
if err := t.fs.MkdirAll(filepath.Dir(archivedPath), 0777); err != nil && !fs.IsExist(err) {
return err
}
l.Debugln("moving to", archivedPath)
if err := osutil.Rename(t.fs, filePath, archivedPath); err != nil {
return err
}
// Set the mtime to the time the file was deleted. This is used by the
// cleanout routine. If this fails things won't work optimally but there's
// not much we can do about it so we ignore the error.
t.fs.Chtimes(archivedPath, time.Now(), time.Now())
return nil
return archiveFile(t.folderFs, t.versionsFs, filePath, func(name, tag string) string {
return name
})
}
func (t *Trashcan) Serve() {
@@ -124,8 +84,7 @@ func (t *Trashcan) String() string {
}
func (t *Trashcan) cleanoutArchive() error {
versionsDir := ".stversions"
if _, err := t.fs.Lstat(versionsDir); fs.IsNotExist(err) {
if _, err := t.versionsFs.Lstat("."); fs.IsNotExist(err) {
return nil
}
@@ -144,20 +103,45 @@ func (t *Trashcan) cleanoutArchive() error {
if info.ModTime().Before(cutoff) {
// The file is too old; remove it.
t.fs.Remove(path)
err = t.versionsFs.Remove(path)
} else {
// Keep this file, and remember it so we don't unnecessarily try
// to remove this directory.
dirTracker.addFile(path)
}
return nil
}
if err := t.fs.Walk(versionsDir, walkFn); err != nil {
return err
}
dirTracker.deleteEmptyDirs(t.fs)
if err := t.versionsFs.Walk(".", walkFn); err != nil {
return err
}
dirTracker.deleteEmptyDirs(t.versionsFs)
return nil
}
func (t *Trashcan) GetVersions() (map[string][]FileVersion, error) {
return retrieveVersions(t.versionsFs)
}
func (t *Trashcan) Restore(filepath string, versionTime time.Time) error {
// If we have an untagged file A and want to restore it on top of existing file A, we can't first archive the
// existing A as we'd overwrite the old A version, therefore when we archive existing file, we archive it with a
// tag but when the restoration is finished, we rename it (untag it). This is only important if when restoring A,
// there already exists a file at the same location
taggedName := ""
tagger := func(name, tag string) string {
// We can't use TagFilename here, as restoreFii would discover that as a valid version and restore that instead.
taggedName = fs.TempName(name)
return taggedName
}
err := restoreFile(t.versionsFs, t.folderFs, filepath, versionTime, tagger)
if taggedName == "" {
return err
}
return t.versionsFs.Rename(taggedName, filepath)
}

View File

@@ -75,3 +75,87 @@ func TestTrashcanCleanout(t *testing.T) {
t.Error("empty directory should have been removed")
}
}
func TestTrashcanArchiveRestoreSwitcharoo(t *testing.T) {
// This tests that trashcan versioner restoration correctly archives existing file, because trashcan versioner
// files are untagged, archiving existing file to replace with a restored version technically should collide in
// in names.
tmpDir1, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
tmpDir2, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
folderFs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir1)
versionsFs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmpDir2)
writeFile(t, folderFs, "file", "A")
versioner := NewTrashcan("", folderFs, map[string]string{
"fsType": "basic",
"fsPath": tmpDir2,
})
if err := versioner.Archive("file"); err != nil {
t.Fatal(err)
}
if _, err := folderFs.Stat("file"); !fs.IsNotExist(err) {
t.Fatal(err)
}
versionInfo, err := versionsFs.Stat("file")
if err != nil {
t.Fatal(err)
}
if content := readFile(t, versionsFs, "file"); content != "A" {
t.Errorf("expected A got %s", content)
}
writeFile(t, folderFs, "file", "B")
if err := versioner.Restore("file", versionInfo.ModTime().Truncate(time.Second)); err != nil {
t.Fatal(err)
}
if content := readFile(t, folderFs, "file"); content != "A" {
t.Errorf("expected A got %s", content)
}
if content := readFile(t, versionsFs, "file"); content != "B" {
t.Errorf("expected B got %s", content)
}
}
func readFile(t *testing.T, filesystem fs.Filesystem, name string) string {
fd, err := filesystem.Open(name)
if err != nil {
t.Fatal(err)
}
defer fd.Close()
buf, err := ioutil.ReadAll(fd)
if err != nil {
t.Fatal(err)
}
return string(buf)
}
func writeFile(t *testing.T, filesystem fs.Filesystem, name, content string) {
fd, err := filesystem.OpenFile(name, fs.OptReadWrite|fs.OptCreate, 0777)
if err != nil {
t.Fatal(err)
}
defer fd.Close()
if err := fd.Truncate(int64(len(content))); err != nil {
t.Fatal(err)
}
if n, err := fd.Write([]byte(content)); err != nil || n != len(content) {
t.Fatal(n, len(content), err)
}
}

View File

@@ -7,11 +7,30 @@
package versioner
import (
"fmt"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/pkg/errors"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
)
var locationLocal *time.Location
var errDirectory = fmt.Errorf("cannot restore on top of a directory")
var errNotFound = fmt.Errorf("version not found")
var errFileAlreadyExists = fmt.Errorf("file already exists")
func init() {
var err error
locationLocal, err = time.LoadLocation("Local")
if err != nil {
panic(err.Error())
}
}
// Inserts ~tag just before the extension of the filename.
func TagFilename(name, tag string) string {
dir, file := filepath.Dir(name), filepath.Base(name)
@@ -38,11 +57,215 @@ func UntagFilename(path string) (string, string) {
versionTag := ExtractTag(path)
// Files tagged with old style tags cannot be untagged.
if versionTag == "" || strings.HasSuffix(ext, versionTag) {
if versionTag == "" {
return "", ""
}
// Old style tag
if strings.HasSuffix(ext, versionTag) {
return strings.TrimSuffix(path, "~"+versionTag), versionTag
}
withoutExt := path[:len(path)-len(ext)-len(versionTag)-1]
name := withoutExt + ext
return name, versionTag
}
func retrieveVersions(fileSystem fs.Filesystem) (map[string][]FileVersion, error) {
files := make(map[string][]FileVersion)
err := fileSystem.Walk(".", func(path string, f fs.FileInfo, err error) error {
// Skip root (which is ok to be a symlink)
if path == "." {
return nil
}
// Skip walking if we cannot walk...
if err != nil {
return err
}
// Ignore symlinks
if f.IsSymlink() {
return fs.SkipDir
}
// No records for directories
if f.IsDir() {
return nil
}
path = osutil.NormalizedFilename(path)
name, tag := UntagFilename(path)
// Something invalid, assume it's an untagged file
if name == "" || tag == "" {
versionTime := f.ModTime().Truncate(time.Second)
files[path] = append(files[path], FileVersion{
VersionTime: versionTime,
ModTime: versionTime,
Size: f.Size(),
})
return nil
}
versionTime, err := time.ParseInLocation(TimeFormat, tag, locationLocal)
if err != nil {
// Can't parse it, welp, continue
return nil
}
if err == nil {
files[name] = append(files[name], FileVersion{
VersionTime: versionTime.Truncate(time.Second),
ModTime: f.ModTime().Truncate(time.Second),
Size: f.Size(),
})
}
return nil
})
if err != nil {
return nil, err
}
return files, nil
}
type fileTagger func(string, string) string
func archiveFile(srcFs, dstFs fs.Filesystem, filePath string, tagger fileTagger) error {
filePath = osutil.NativeFilename(filePath)
info, err := srcFs.Lstat(filePath)
if fs.IsNotExist(err) {
l.Debugln("not archiving nonexistent file", filePath)
return nil
} else if err != nil {
return err
}
if info.IsSymlink() {
panic("bug: attempting to version a symlink")
}
_, err = dstFs.Stat(".")
if err != nil {
if fs.IsNotExist(err) {
l.Debugln("creating versions dir")
err := dstFs.Mkdir(".", 0755)
if err != nil {
return err
}
_ = dstFs.Hide(".")
} else {
return err
}
}
l.Debugln("archiving", filePath)
file := filepath.Base(filePath)
inFolderPath := filepath.Dir(filePath)
err = dstFs.MkdirAll(inFolderPath, 0755)
if err != nil && !fs.IsExist(err) {
return err
}
ver := tagger(file, info.ModTime().Format(TimeFormat))
dst := filepath.Join(inFolderPath, ver)
l.Debugln("moving to", dst)
err = osutil.RenameOrCopy(srcFs, dstFs, filePath, dst)
// Set the mtime to the time the file was deleted. This can be used by the
// cleanout routine. If this fails things won't work optimally but there's
// not much we can do about it so we ignore the error.
_ = dstFs.Chtimes(dst, time.Now(), time.Now())
return err
}
func restoreFile(src, dst fs.Filesystem, filePath string, versionTime time.Time, tagger fileTagger) error {
// If the something already exists where we are restoring to, archive existing file for versioning
// remove if it's a symlink, or fail if it's a directory
if info, err := dst.Lstat(filePath); err == nil {
switch {
case info.IsDir():
return errDirectory
case info.IsSymlink():
// Remove existing symlinks (as we don't want to archive them)
if err := dst.Remove(filePath); err != nil {
return errors.Wrap(err, "removing existing symlink")
}
case info.IsRegular():
if err := archiveFile(dst, src, filePath, tagger); err != nil {
return errors.Wrap(err, "archiving existing file")
}
default:
panic("bug: unknown item type")
}
} else if !fs.IsNotExist(err) {
return err
}
filePath = osutil.NativeFilename(filePath)
tag := versionTime.In(locationLocal).Truncate(time.Second).Format(TimeFormat)
taggedFilename := TagFilename(filePath, tag)
oldTaggedFilename := filePath + tag
untaggedFileName := filePath
// Check that the thing we've been asked to restore is actually a file
// and that it exists.
sourceFile := ""
for _, candidate := range []string{taggedFilename, oldTaggedFilename, untaggedFileName} {
if info, err := src.Lstat(candidate); fs.IsNotExist(err) || !info.IsRegular() {
continue
} else if err != nil {
// All other errors are fatal
return err
} else if candidate == untaggedFileName && !info.ModTime().Truncate(time.Second).Equal(versionTime) {
// No error, and untagged file, but mtime does not match, skip
continue
}
sourceFile = candidate
break
}
if sourceFile == "" {
return errNotFound
}
// Check that the target location of where we are supposed to restore does not exist.
// This should have been taken care of by the first few lines of this function.
if _, err := dst.Lstat(filePath); err == nil {
return errFileAlreadyExists
} else if !fs.IsNotExist(err) {
return err
}
_ = dst.MkdirAll(filepath.Dir(filePath), 0755)
return osutil.RenameOrCopy(src, dst, sourceFile, filePath)
}
func fsFromParams(folderFs fs.Filesystem, params map[string]string) (versionsFs fs.Filesystem) {
if params["fsType"] == "" && params["fsPath"] == "" {
versionsFs = fs.NewFilesystem(folderFs.Type(), filepath.Join(folderFs.URI(), ".stversions"))
} else if params["fsType"] == "" {
uri := params["fsPath"]
// We only know how to deal with relative folders for basic filesystems, as that's the only one we know
// how to check if it's absolute or relative.
if folderFs.Type() == fs.FilesystemTypeBasic && !filepath.IsAbs(params["fsPath"]) {
uri = filepath.Join(folderFs.URI(), params["fsPath"])
}
versionsFs = fs.NewFilesystem(folderFs.Type(), uri)
} else {
var fsType fs.FilesystemType
_ = fsType.UnmarshalText([]byte(params["fsType"]))
versionsFs = fs.NewFilesystem(fsType, params["fsPath"])
}
l.Debugln("%s (%s) folder using %s (%s) versioner dir", folderFs.URI(), folderFs.Type(), versionsFs.URI(), versionsFs.Type())
return
}

View File

@@ -9,6 +9,7 @@
package versioner
import (
"fmt"
"time"
"github.com/syncthing/syncthing/lib/fs"
@@ -16,6 +17,8 @@ import (
type Versioner interface {
Archive(filePath string) error
GetVersions() (map[string][]FileVersion, error)
Restore(filePath string, versionTime time.Time) error
}
type FileVersion struct {
@@ -25,6 +28,7 @@ type FileVersion struct {
}
var Factories = map[string]func(folderID string, filesystem fs.Filesystem, params map[string]string) Versioner{}
var ErrRestorationNotSupported = fmt.Errorf("version restoration not supported with the current versioner")
const (
TimeFormat = "20060102-150405"

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "STDISCOSRV" "1" "Mar 22, 2019" "v1" "Syncthing"
.TH "STDISCOSRV" "1" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
stdiscosrv \- Syncthing Discovery Server
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "STRELAYSRV" "1" "Mar 22, 2019" "v1" "Syncthing"
.TH "STRELAYSRV" "1" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
strelaysrv \- Syncthing Relay Server
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-BEP" "7" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-BEP" "7" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing-bep \- Block Exchange Protocol v1
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-CONFIG" "5" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-CONFIG" "5" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing-config \- Syncthing Configuration
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-DEVICE-IDS" "7" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-DEVICE-IDS" "7" "Apr 13, 2019" "v1" "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" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-EVENT-API" "7" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing-event-api \- Event API
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-FAQ" "7" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-FAQ" "7" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing-faq \- Frequently Asked Questions
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-GLOBALDISCO" "7" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-GLOBALDISCO" "7" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing-globaldisco \- Global Discovery Protocol v3
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-LOCALDISCO" "7" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-LOCALDISCO" "7" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing-localdisco \- Local Discovery Protocol v4
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-NETWORKING" "7" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-NETWORKING" "7" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing-networking \- Firewall Setup
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-RELAY" "7" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-RELAY" "7" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing-relay \- Relay Protocol v1
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-REST-API" "7" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-REST-API" "7" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing-rest-api \- REST API
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-SECURITY" "7" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-SECURITY" "7" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing-security \- Security Principles
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-STIGNORE" "5" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-STIGNORE" "5" "Apr 13, 2019" "v1" "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" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING-VERSIONING" "7" "Apr 13, 2019" "v1" "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" "Mar 22, 2019" "v1" "Syncthing"
.TH "SYNCTHING" "1" "Apr 13, 2019" "v1" "Syncthing"
.SH NAME
syncthing \- Syncthing
.